Migrate to WXT and Manifest V3; upgrade all dependencies - #44
Merged
Conversation
Modernizes tabclip's toolchain and extension platform target, with no
intended change to the extension's look, feel, or behavior beyond the bug
fixes noted below.
Bundler: Webpack -> WXT
- WXT is a Vite-based, actively-maintained framework built specifically for
cross-browser WebExtensions, and generates the Chrome/Firefox manifest
divergence (service_worker vs. scripts background, offscreen permission,
etc.) from a single config instead of a hand-rolled multi-entry Webpack
config.
- Source restructured into WXT's entrypoints/ convention: src/entrypoints/
{background.js, popup/, offscreen/}, with shared logic split out of the
old shared.js into src/lib/{prefs,tabs,clipboard-dom,
clipboard-offscreen-client,fontawesome-icons}.js.
- ESLint migrated to v10's flat config (eslint.config.js), preserving the
existing style rules (tabs, single quotes, no semicolons, unix
linebreaks).
- CI moved from CircleCI to GitHub Actions (.github/workflows/ci.yml),
using actions/setup-node's built-in npm caching and npm ci.
Manifest V2 -> V3
- browser_action -> action, background.page -> service_worker (Chrome) /
scripts (Firefox), MV3 permissions incl. Chrome-only "offscreen".
- Chrome's MV3 background is a real service worker with no DOM at all, but
the extension's clipboard read/write relies on a hidden contenteditable
div and document.execCommand. Firefox's MV3 background is still a
DOM-having event page and can keep using that technique directly, but
Chrome's keyboard-shortcut copy/paste now delegates to a lazily-created
offscreen document (Chrome's documented pattern for clipboard access from
a service worker) instead. The popup is unaffected either way, since it
always has its own DOM.
- Added the extension's existing AMO-assigned Firefox ID via
browser_specific_settings.gecko.id, required for Firefox MV3 submissions
so the update isn't mistaken for a new listing.
Bug fixes
- The copy-tabs keyboard shortcut handler destructured
PREFERENCE_NAMES.INCLDUE_TITLES (a typo for INCLUDE_TITLES), so the
"Include titles" preference was silently ignored when copying via
Ctrl+Shift+C (it worked fine from the popup's Copy button).
- browser.extension.getURL (a deprecated WebExtension API) is now
browser.runtime.getURL.
Dependency upgrades
- All dependencies upgraded to latest. The deprecated
@fortawesome/fontawesome + @fortawesome/fontawesome-free-solid packages
are replaced with the current @fortawesome/fontawesome-svg-core +
@fortawesome/free-solid-svg-icons, reproducing identical icon rendering
via library.add() + dom.watch().
- bootstrap-css-only stays at 4.4.1 (no newer version has ever been
published, and jumping to Bootstrap 5 risked visual changes).
Also bumps the extension version to 1.5, now derived from package.json's
version field (previously hardcoded separately in the WXT config, out of
sync with package.json's long-stale 1.0.0).
actions/checkout@v4, actions/setup-node@v4, and actions/upload-artifact@v4 all target Node 20 internally, which GitHub Actions runners now force onto Node 24 with a deprecation warning (Node 20 support is being fully removed in September 2026). Bumped to checkout@v7, setup-node@v6, and upload-artifact@v6, all of which target Node 24 natively.
If browser.offscreen.createDocument() ever rejected, the `creating` guard was left pointing at the rejected promise forever (the reset line never ran), permanently wedging every future copy/paste shortcut until the extension was reloaded.
- Share clipboard message-type strings between the sender and listener via a new constants module instead of duplicating literals, so a typo in either file can no longer break the bridge silently. - Treat a missing sendMessage response as an explicit failure: Chrome resolves (rather than rejects) sendMessage with undefined when no listener responds in time, which previously let pasteTabs crash on undefined input and let copyTabs report success without having written anything. - Close the offscreen document after each read/write instead of leaving it open for the rest of the browser session.
- Guard pasteTabs against a non-string clipboard read result instead of crashing on .match() of undefined/null. - Restore the hasListener guard around the command listener that the old MV2 background.js had, preventing duplicate registration if the background entrypoint is ever re-executed (e.g. dev-mode HMR). - Align wxt.config.ts's offscreen-permission condition with the runtime's FIREFOX check so a future non-Chrome, non-Firefox build target can't end up needing the offscreen client without the permission to use it. - Request window focus before falling back to execCommand-based clipboard access, since the offscreen document (unlike the old MV2 background page) is never visibly focused.
WXT's sources zip (submitted to AMO for review) has its own independent exclude list (zip.excludeSources) that never consults .gitignore or .git/info/exclude - by default it only excludes node_modules, dotfiles, test files, and the output dir, so both the README-only assets/ directory and the local-only untracked/ scratch directory were being swept into it. Shrinks the sources zip from ~1.17 MB/40 files to ~123 KB/28 files, containing only what's actually needed to review/reproduce the build.
.output/ is dot-prefixed, so actions/upload-artifact treats it as hidden and skips it entirely by default (include-hidden-files defaults to false), even though .output/*.zip matched real files every time. This has been broken since the very first CI run of this workflow - only noticed now while investigating an unrelated sources-zip issue.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Modernizes tabclip's toolchain and extension platform target, with no intended change to the extension's look, feel, or behavior beyond the bug fixes noted below. Manually verified end-to-end in both Chrome and Firefox.
Bundler: Webpack -> WXT
service_workervs.scriptsbackground,offscreenpermission, etc.) from a single config instead of a hand-rolled multi-entry Webpack config.entrypoints/convention:src/entrypoints/{background.js, popup/, offscreen/}, with shared logic split out of the oldshared.jsintosrc/lib/{prefs,tabs,clipboard-dom,clipboard-offscreen-client,fontawesome-icons}.js.eslint.config.js), preserving the existing style rules (tabs, single quotes, no semicolons, unix linebreaks)..github/workflows/ci.yml), usingactions/setup-node's built-in npm caching andnpm ci.Manifest V2 -> V3
browser_action->action,background.page->service_worker(Chrome) /scripts(Firefox), MV3 permissions incl. Chrome-onlyoffscreen.document.execCommand. Firefox's MV3 background is still a DOM-having event page and can keep using that technique directly, but Chrome's keyboard-shortcut copy/paste now delegates to a lazily-created offscreen document (Chrome's documented pattern for clipboard access from a service worker) instead. The popup is unaffected either way, since it always has its own DOM.browser_specific_settings.gecko.id, required for Firefox MV3 submissions so the update isn't mistaken for a new listing.Bug fixes
PREFERENCE_NAMES.INCLDUE_TITLES(a typo forINCLUDE_TITLES), so the "Include titles" preference was silently ignored when copying via Ctrl+Shift+C (it worked fine from the popup's Copy button).browser.extension.getURL(a deprecated WebExtension API) is nowbrowser.runtime.getURL.Dependency upgrades
@fortawesome/fontawesome+@fortawesome/fontawesome-free-solidpackages are replaced with the current@fortawesome/fontawesome-svg-core+@fortawesome/free-solid-svg-icons, reproducing identical icon rendering vialibrary.add()+dom.watch().bootstrap-css-onlystays at 4.4.1 (no newer version has ever been published, and jumping to Bootstrap 5 risked visual changes).Also bumps the extension version to 1.5, now derived from
package.json's version field (previously hardcoded separately in the WXT config, out of sync withpackage.json's long-stale 1.0.0).Test plan
npm run lintpassesnpm run buildsucceeds for bothchrome-mv3andfirefox-mv3, manifests diffed field-by-field against the original MV2 manifest