From 3d1be6238c88a1069952811202186428d53f8643 Mon Sep 17 00:00:00 2001 From: thelooter Date: Tue, 9 Jun 2026 00:09:09 +0200 Subject: [PATCH] feat(extension): build and ship a Firefox add-on (#188) Derive a Gecko-compatible manifest at build time and package extension-firefox.zip alongside the Chrome zip: - background service worker is declared as an event-page `scripts` entry (top-level listeners + in-memory Map run unchanged on Gecko) - browser_specific_settings.gecko with id, strict_min_version 140.0, and data_collection_permissions (required by AMO; honored on 140+) - packZip helper parameterized over cwd/excludes; `*.DS_Store` strips junk at every depth and .DS_Store is excluded from the staging copy - guard against a missing background.service_worker shape CI now builds the extension and runs a pinned `web-ext@8 lint` over the staged Firefox tree (innerHTML warnings are non-blocking); the unpacked staging dir is excluded from the uploaded artifact. The release script attaches both zips and points to AMO. Bumps the extension to v1.2.0 with a changelog entry. Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Paul Bakaus --- .github/workflows/ci.yml | 22 +++++++--- extension/manifest.json | 2 +- scripts/build-extension.js | 88 +++++++++++++++++++++++++++++++++----- scripts/release.mjs | 7 +-- site/pages/changelog.astro | 7 +++ 5 files changed, 106 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60f449b9c..eaf1337a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,21 +84,31 @@ jobs: if: needs.changes.outputs.detector == 'true' run: bun run build:browser - - name: Rebuild extension detector - if: needs.changes.outputs.detector == 'true' - run: bun run build:extension - - name: Build run: bun run build + - name: Build extension + if: needs.changes.outputs.detector == 'true' + run: bun run build:extension + + - name: Lint Firefox extension (web-ext) + if: needs.changes.outputs.detector == 'true' + # Pinned for reproducible CI. Fails on AMO errors; innerHTML style + # warnings in the panel renderer are non-blocking and not promoted to + # errors here. + run: npx --yes web-ext@8 lint --source-dir dist/extension-firefox + - name: Verify generated tracked outputs - run: git diff --exit-code -- .agents .claude .cursor .gemini .github/skills plugin cli/engine/detect-antipatterns-browser.js + run: git diff --exit-code -- .agents .claude .cursor .gemini .github/skills plugin cli/engine/detect-antipatterns-browser.js extension/detector - name: Upload build artifacts uses: actions/upload-artifact@v7 with: name: impeccable-dist - path: dist/ + # Ship the packaged zips, not the unpacked Firefox staging tree. + path: | + dist/ + !dist/extension-firefox/ retention-days: 7 cli-remote-e2e: diff --git a/extension/manifest.json b/extension/manifest.json index e0c8b208d..3a3a4c45b 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Impeccable", "description": "Detect common UI anti-patterns in any web page", - "version": "1.1.0", + "version": "1.2.0", "permissions": ["activeTab", "scripting", "storage", "webNavigation"], "host_permissions": [""], "background": { diff --git a/scripts/build-extension.js b/scripts/build-extension.js index 65d693de8..fa9962c26 100644 --- a/scripts/build-extension.js +++ b/scripts/build-extension.js @@ -1,11 +1,16 @@ #!/usr/bin/env node /** - * Builds the Chrome DevTools extension. + * Builds the browser DevTools extension (Chrome + Firefox). * * 1. Generates the extension variant of the browser detector * 2. Extracts antipatterns.json for the panel UI - * 3. Packages as extension.zip for Chrome Web Store upload + * 3. Packages extension.zip (Chrome Web Store) and extension-firefox.zip (AMO) + * + * The source `extension/manifest.json` is the Chrome manifest. The Firefox + * variant is derived at build time: the MV3 background service worker is + * declared as an event-page `scripts` entry (the universally-supported path on + * Gecko), and `browser_specific_settings.gecko` is added for AMO signing. * * Run: node scripts/build-extension.js */ @@ -80,12 +85,75 @@ console.log(`Generated ${path.relative(ROOT, AP_OUTPUT)} (${ANTIPATTERNS.length} import { execSync } from 'child_process'; -const zipPath = path.join(ROOT, 'dist/extension.zip'); -fs.mkdirSync(path.join(ROOT, 'dist'), { recursive: true }); -try { fs.unlinkSync(zipPath); } catch {} -execSync( - `zip -r ${JSON.stringify(zipPath)} . -x "STORE_LISTING.md" ".DS_Store"`, - { cwd: EXT_DIR, stdio: 'pipe' }, +const DIST = path.join(ROOT, 'dist'); +fs.mkdirSync(DIST, { recursive: true }); + +// `excludes` are passed to `zip -x`; patterns match the full archive path with +// `*` spanning `/`, so `*.DS_Store` strips the file at every depth, not just root. +function packZip(zipPath, cwd, excludes = []) { + try { fs.unlinkSync(zipPath); } catch {} + const exArgs = excludes.map((e) => `-x ${JSON.stringify(e)}`).join(' '); + execSync( + `zip -r ${JSON.stringify(zipPath)} .${exArgs ? ' ' + exArgs : ''}`, + { cwd, stdio: 'pipe' }, + ); + const size = fs.statSync(zipPath).size; + console.log(`Packaged ${path.relative(ROOT, zipPath)} (${(size / 1024).toFixed(1)} KB)`); +} + +// --- 3a. Chrome zip (manifest unchanged) --- + +packZip(path.join(DIST, 'extension.zip'), EXT_DIR, ['STORE_LISTING.md', '*.DS_Store']); + +// --- 3b. Firefox: derive a Gecko-compatible manifest and stage an unpacked +// build (consumed by `web-ext lint` in CI), then zip it for AMO. --- + +const chromeManifest = JSON.parse(fs.readFileSync(path.join(EXT_DIR, 'manifest.json'), 'utf-8')); + +const serviceWorker = chromeManifest.background?.service_worker; +if (!serviceWorker) { + throw new Error( + 'extension/manifest.json: expected background.service_worker to derive the Firefox manifest', + ); +} + +const firefoxManifest = { + ...chromeManifest, + // Gecko supports MV3 via non-persistent event pages. Declaring `scripts` + // (rather than `service_worker`) is the path supported across all MV3 Firefox + // releases; service-worker.js uses only top-level listeners + an in-memory + // Map, so it runs unchanged as an event page. + background: { scripts: [serviceWorker] }, + // Required by AMO for signing/distribution. Ignored by Chrome. + browser_specific_settings: { + gecko: { + id: 'impeccable@bakaus.com', + // `data_collection_permissions` (below) is required by AMO for new + // submissions and is only honored on Firefox 140+. We set the floor to + // 140 so the declared min version actually supports every key we ship; + // everything else this extension uses (MV3 action, scripting, devtools, + // object-form web_accessible_resources, storage.sync) landed long before. + strict_min_version: '140.0', + // The detector runs entirely in-page; nothing is transmitted off-device. + data_collection_permissions: { required: ['none'] }, + }, + }, +}; + +const ffStageDir = path.join(DIST, 'extension-firefox'); +fs.rmSync(ffStageDir, { recursive: true, force: true }); +fs.cpSync(EXT_DIR, ffStageDir, { + recursive: true, + filter: (src) => { + const base = path.basename(src); + return base !== 'STORE_LISTING.md' && base !== '.DS_Store'; + }, +}); +fs.writeFileSync( + path.join(ffStageDir, 'manifest.json'), + JSON.stringify(firefoxManifest, null, 2) + '\n', ); -const size = fs.statSync(zipPath).size; -console.log(`Packaged ${path.relative(ROOT, zipPath)} (${(size / 1024).toFixed(1)} KB)`); +console.log(`Staged ${path.relative(ROOT, ffStageDir)}/ (Firefox manifest)`); + +// STORE_LISTING.md is already filtered out of the stage dir above. +packZip(path.join(DIST, 'extension-firefox.zip'), ffStageDir, ['*.DS_Store']); diff --git a/scripts/release.mjs b/scripts/release.mjs index e98ca0482..28f0b7cf7 100755 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -46,9 +46,10 @@ const COMPONENTS = { label: 'Extension', changelogLabel: 'Extension v', buildCmd: 'bun run build:extension', - artifacts: ['dist/extension.zip'], - postReleaseHint: 'Upload `dist/extension.zip` to the Chrome Web Store dashboard to publish.', - tweetHeader: (v) => `Impeccable Chrome extension v${v} is out.`, + artifacts: ['dist/extension.zip', 'dist/extension-firefox.zip'], + postReleaseHint: + 'Upload `dist/extension.zip` to the Chrome Web Store dashboard, and `dist/extension-firefox.zip` to addons.mozilla.org (AMO), to publish.', + tweetHeader: (v) => `Impeccable browser extension v${v} is out.`, tweetCta: null, }, }; diff --git a/site/pages/changelog.astro b/site/pages/changelog.astro index fbd3de17b..0bc885d9a 100644 --- a/site/pages/changelog.astro +++ b/site/pages/changelog.astro @@ -104,6 +104,13 @@ import '../styles/changelog-faq-kinpaku.css'; +
+
Extension v1.2.0June 1, 2026
+
    +
  • Firefox build. The same detector, popup, DevTools panel, and per-rule toggles now ship as a Firefox add-on. bun run build:extension emits a Gecko-compatible package next to the Chrome one, with the background worker declared as an event page and a data-collection declaration that states what the extension already does: the scan runs in the page, and nothing leaves your machine.
  • +
+
+
Extension v1.1.0May 28, 2026