Skip to content

support for firefox - claude did 2 days of work in 15m?#16

Merged
jjpaulino merged 3 commits into
masterfrom
firefox-support
May 16, 2026
Merged

support for firefox - claude did 2 days of work in 15m?#16
jjpaulino merged 3 commits into
masterfrom
firefox-support

Conversation

@macgyver
Copy link
Copy Markdown
Contributor

@macgyver macgyver commented May 15, 2026

Summary

Adds first-class Firefox 121+ support alongside the existing Chromium build. Same source, dual build pipeline, dual zip in every release.

Originally opened by a contributor with the working baseline (polyfill + gecko block + manifest postbuild). Pushed a follow-up commit that hardened it: target-aware tooling, full test coverage of the manifest contract, documentation parity for end-users, and CI that ships both zips.

What's in the branch

Code

  • webextension-polyfill (runtime): every chrome.* import → import browser from 'webextension-polyfill'. Promise-based on both browsers, behavior identical on Chromium.
  • src/manifest.ts: branches on process.env.TARGET === 'firefox'. Firefox build adds browser_specific_settings.gecko (id, strict_min_version: 121.0, data_collection_permissions: { required: ['none'] }); Chromium build keeps minimum_chrome_version: 116. Manifest description capped at 132 chars (CWS + AMO ceiling) with a build-time guard.
  • scripts/firefox-manifest.mjs (new, pure helper): rewrites background.service_workerbackground.scripts (array, type: 'module' preserved) and strips Chromium-only use_dynamic_url from web_accessible_resources. Throws if there's no service_worker to rewrite — silent no-op there would ship a broken Firefox build.
  • scripts/firefox-postbuild.mjs: now a thin file-IO wrapper around the helper. Runs after vite build when TARGET=firefox.
  • scripts/zip-extension.mjs: target-aware "Next steps" output — Chromium gets chrome://extensions / Load unpacked, Firefox gets about:debugging / Load Temporary Add-on plus the temporary-install caveat and the Developer-Edition-with-signatures-off path. Verify-manifest-at-root error message now mentions both stores + both rebuild commands.
  • vite.config.ts: outDir switches to dist-firefox/ when TARGET=firefox.
  • eslint.config.js / .gitignore / .prettierignore: pick up dist-firefox/ alongside dist/.

CI

  • .github/workflows/release.yml now builds + packages BOTH targets and attaches both zips to the draft GitHub release. Uses the same npm run zip[:firefox] scripts as locally so the manifest-at-root verification + sourcemap stripping run in CI exactly as they do on a dev machine.

Tests (+9, total 176)

  • tests/scripts/firefox-manifest.test.ts (5): rewrite swaps service_workerscripts with type: 'module' preserved; throws when there's no service_worker to rewrite; strips use_dynamic_url from every WAR entry; no-ops on missing WAR; mutates in place + returns same reference.
  • tests/manifest.test.ts (4): default build adds minimum_chrome_version + omits gecko; TARGET=firefox adds full gecko block + omits minimum_chrome_version (mutual exclusion); shared MV3 fields stay identical between targets; description respects the 132-char store ceiling.

Docs

  • README.md: full Firefox install/update section parallel to the Chromium one — Option A (temporary add-on, any FF 121+, resets on restart) and Option B (persistent on Developer Edition / Nightly: xpinstall.signatures.required = false + .zip.xpi rename). Troubleshooting table gains the Firefox "extension appears to be corrupt" row. Scripts table covers build:firefox / zip:firefox / release:dry:firefox. Releasing section explains the dual-target CI flow. Architecture comment notes manifest.ts's TARGET branching.
  • PRIVACY.md: storage area names use the cross-browser form (storage.sync / storage.local); permission justifications mention both chrome.tabs.captureVisibleTab and browser.tabs.captureVisibleTab; remote-code section calls out both build commands.

Local commands

# Chromium build + zip
npm run release:dry         # → clay-slip-vX.Y.Z.zip

# Firefox build + zip (post-build rewrite included)
npm run release:dry:firefox # → clay-slip-vX.Y.Z-firefox.zip

Verification

  • npm run validate → typecheck / lint / format / 176 tests green.
  • npm run release:dry + npm run release:dry:firefox → both succeed.
  • Zip contents inspected:
    • Chromium: background.service_worker, minimum_chrome_version: 116, no gecko block.
    • Firefox: background.scripts: [...] (type: 'module' preserved), no minimum_chrome_version, full gecko block with data_collection_permissions: { required: ['none'] }, zero use_dynamic_url leftovers in WAR.

Install for testers

Firefox 121+ (any channel)

  1. Download clay-slip-vX.Y.Z-firefox.zip from the next release and unzip.
  2. about:debugging#/runtime/this-firefoxLoad Temporary Add-on… → pick the unzipped manifest.json.

(Resets on browser restart. For persistent install, see README → Install → Firefox → Option B.)

Chromium (Chrome / Edge / Brave / Arc / Vivaldi / Opera)

Unchanged from before — clay-slip-vX.Y.Z.zip + chrome://extensionsLoad unpacked.

@macgyver macgyver requested a review from jjpaulino May 15, 2026 16:48
@jjpaulino
Copy link
Copy Markdown
Member

Oh amazing! Give me a chance to test the functionality and we can merge and release no biggie! 🍕

jjpaulino and others added 2 commits May 15, 2026 14:30
The original commit got Firefox loading end-to-end (polyfill,
gecko block, postbuild rewrite). This pass shores up everything
around it so the second target is a first-class citizen, not a
side experiment.

Code / build:

- Extract the Firefox manifest rewrite into a pure helper at
  `scripts/firefox-manifest.mjs` (in-place mutation that takes
  the Chromium-shaped manifest and emits the Firefox shape).
  `firefox-postbuild.mjs` is now a thin file-IO wrapper around it.
  Postbuild also throws loudly if `background.service_worker` is
  missing — a silent no-op there would ship a broken Firefox build.
- `src/manifest.ts`: rewrite the comments to explain WHY each
  branch field exists (gecko id, strict_min_version=121.0,
  data_collection_permissions=['none'], minimum_chrome_version=116)
  and why we cap manifest description at 132 chars (CWS + AMO).
- `scripts/zip-extension.mjs`:
  · Branch the "Next steps" output by target — Chromium gets
    "Open chrome://extensions / Load unpacked", Firefox gets
    "Open about:debugging / Load Temporary Add-on" + the temporary
    install caveat + the Developer-Edition-with-signatures-off path.
  · Verify-manifest-at-root error message now mentions both stores
    and both sideload flows, and points at the right rebuild
    command for the failing target.
- `.github/workflows/release.yml`: build + zip BOTH targets and
  attach both to the draft GitHub release. CI now runs the same
  `npm run zip` / `npm run zip:firefox` scripts as locally so the
  manifest-at-root verification + sourcemap stripping run in CI.

Tests (+9, total now 176):

- `tests/scripts/firefox-manifest.test.ts` (5 tests): rewrite
  swaps service_worker → scripts (with type=module preserved);
  throws when there's no service_worker to rewrite; strips
  use_dynamic_url from every WAR entry; no-ops on missing WAR;
  mutates in place and returns the same reference.
- `tests/manifest.test.ts` (4 tests): default build adds
  minimum_chrome_version + omits gecko; TARGET=firefox adds the
  full gecko block + omits minimum_chrome_version (mutual
  exclusion); shared MV3 fields (name, version, permissions,
  host_permissions, icons, description) stay identical between
  targets; description respects the 132-char store ceiling.

Docs:

- `README.md`: full Firefox install/update section parallel to
  the existing Chromium one — both Option A (temporary add-on,
  any FF 121+, resets on restart) and Option B (persistent on
  Developer Edition / Nightly with signatures off + .zip → .xpi
  rename). Troubleshooting table gains the Firefox "extension
  appears to be corrupt" row. Scripts table covers the Firefox
  build/zip/release-dry commands. Releasing section explains the
  dual-target CI flow. Architecture comment notes manifest.ts's
  TARGET branching.
- `PRIVACY.md`: WebExtension storage terminology now spans
  Chromium + Firefox (chrome.storage / browser.storage),
  Screenshot permission justification mentions both APIs, remote-
  code section mentions both build commands.
- Top-of-file comments in `firefox-manifest.mjs`,
  `firefox-postbuild.mjs`, and `zip-extension.mjs` document
  every rewrite, every fallback path, and the temporary-vs-
  persistent install matrix.

Verified: validate (typecheck/lint/format/176 tests) green; both
`npm run release:dry` and `npm run release:dry:firefox` produce
zips with the right manifest shape (verified service_worker vs
scripts, gecko presence, minimum_chrome_version mutual exclusion,
no use_dynamic_url leftovers in the Firefox build).

Co-authored-by: Cursor <cursoragent@cursor.com>
Composes validate (once) + Chromium build + Chromium zip + Firefox
build + Firefox zip. Mirrors the full release workflow exactly,
which was previously only reachable by running both per-target
release:dry scripts back-to-back (and revalidating each time, ~7s
of duplicated typecheck/lint/format/tests).

This is now the recommended local smoke-test before tagging a
release. The single-target scripts stay around for the case where
you only care about one browser family.

README: scripts table gains the new entry; Releasing section
recommends `release:dry:both` first, with the per-target scripts
as the narrower-scope alternatives.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jjpaulino jjpaulino merged commit 2bde262 into master May 16, 2026
1 check passed
@jjpaulino jjpaulino deleted the firefox-support branch May 16, 2026 04:53
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.

2 participants