mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
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.
142 lines
5.2 KiB
JavaScript
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']);
|