Files
pbakaus_impeccable/scripts/build-extension.js
T
Paul BakausandGitHub d6a9891066 Share browser detector bundling (#498)
Centralize the browser-safe module set and source transformation so the browser and extension builders cannot drift.

AI assistance: This refactor was prepared by Codex under pbakaus's scheduled architecture-simplification authorization.
2026-08-03 14:51:46 -07:00

142 lines
5.2 KiB
JavaScript

#!/usr/bin/env node
/**
* 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 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
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs';
import { bundleBrowserDetectorModules } from './lib/browser-detector-bundle.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const EXT_DIR = path.join(ROOT, 'extension');
const DETECTOR_OUTPUT = path.join(EXT_DIR, 'detector/detect.js');
const AP_OUTPUT = path.join(EXT_DIR, 'detector/antipatterns.json');
const code = bundleBrowserDetectorModules(ROOT);
// --- 1. Build detector ---
const output = `/**
* Anti-Pattern Browser Detector for Impeccable (Extension Variant)
* Copyright (c) 2026 Paul Bakaus
* SPDX-License-Identifier: Apache-2.0
*
* GENERATED -- do not edit. Source: cli/engine/browser/injected/index.mjs
* Rebuild: node scripts/build-extension.js
*/
(function () {
if (typeof window === 'undefined') return;
${code}
})();
`;
fs.mkdirSync(path.dirname(DETECTOR_OUTPUT), { recursive: true });
fs.writeFileSync(DETECTOR_OUTPUT, output);
console.log(`Generated ${path.relative(ROOT, DETECTOR_OUTPUT)} (${(output.length / 1024).toFixed(1)} KB)`);
// --- 2. Extract antipatterns.json ---
// Include description so the devtools panel can show the full rule explanation
// in tooltips.
const apJson = ANTIPATTERNS.map(({ id, name, category, description }) => ({
id,
name,
category: category || 'quality',
description: description || '',
}));
fs.writeFileSync(AP_OUTPUT, JSON.stringify(apJson, null, 2) + '\n');
console.log(`Generated ${path.relative(ROOT, AP_OUTPUT)} (${ANTIPATTERNS.length} rules)`);
// --- 3. Zip packaging ---
import { execSync } from 'child_process';
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',
);
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']);