Skip to content

Migrate to WXT and Manifest V3; upgrade all dependencies - #44

Merged
joshdick merged 7 commits into
mainfrom
feat/wxt-mv3-migration
Jul 10, 2026
Merged

Migrate to WXT and Manifest V3; upgrade all dependencies#44
joshdick merged 7 commits into
mainfrom
feat/wxt-mv3-migration

Conversation

@joshdick

Copy link
Copy Markdown
Owner

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

  • 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).

Test plan

  • npm run lint passes
  • npm run build succeeds for both chrome-mv3 and firefox-mv3, manifests diffed field-by-field against the original MV2 manifest
  • Popup copy/paste (current/all windows, include titles, background paste, pref persistence) verified in both browsers
  • Keyboard shortcuts (Ctrl+Shift+C/V) verified in both browsers, including the Include-titles bugfix and the Chrome offscreen-document clipboard path
  • FontAwesome icons render correctly in the popup

joshdick added 7 commits July 10, 2026 01:01
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.
@joshdick
joshdick merged commit adcc207 into main Jul 10, 2026
1 check passed
@joshdick
joshdick deleted the feat/wxt-mv3-migration branch July 10, 2026 06:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant