1#!/usr/bin/env node
2
3/**
4 * Builds the Chrome DevTools extension.
5 *
6 * 1. Generates the extension variant of the browser detector
7 * 2. Extracts antipatterns.json for the panel UI
8 * 3. Packages as extension.zip for Chrome Web Store upload
9 *
10 * Run: node scripts/build-extension.js
11 */
12
13import fs from 'fs';
14import path from 'path';
15import { fileURLToPath } from 'url';
16import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs';
17
18const __dirname = path.dirname(fileURLToPath(import.meta.url));
19const ROOT = path.resolve(__dirname, '..');
20const EXT_DIR = path.join(ROOT, 'extension');
21
22const BROWSER_MODULES = [
23 'cli/engine/shared/constants.mjs',
24 'cli/engine/registry/antipatterns.mjs',
25 'cli/engine/shared/color.mjs',
26 'cli/engine/rules/checks.mjs',
27 'cli/engine/browser/injected/index.mjs',
28];
29const DETECTOR_OUTPUT = path.join(EXT_DIR, 'detector/detect.js');
30const AP_OUTPUT = path.join(EXT_DIR, 'detector/antipatterns.json');
31
32function browserSafeModule(relPath) {
33 let code = fs.readFileSync(path.join(ROOT, relPath), 'utf-8');
34 if (relPath === 'cli/engine/registry/antipatterns.mjs') {
35 const match = code.match(/const ANTIPATTERNS = \[[\s\S]*?\n\];/);
36 if (!match) throw new Error('Could not extract browser antipattern registry');
37 code = match[0];
38 }
39 code = code.replace(/^import[\s\S]*?;\n/gm, '');
40 code = code.replace(/^export\s+\{[\s\S]*?^};\n?/gm, '');
41 return `// --- ${relPath} ---\n${code.trim()}\n`;
42}
43
44const code = BROWSER_MODULES.map(browserSafeModule).join('\n');
45
46// --- 1. Build detector ---
47
48const output = `/**
49 * Anti-Pattern Browser Detector for Impeccable (Extension Variant)
50 * Copyright (c) 2026 Paul Bakaus
51 * SPDX-License-Identifier: Apache-2.0
52 *
53 * GENERATED -- do not edit. Source: cli/engine/browser/injected/index.mjs
54 * Rebuild: node scripts/build-extension.js
55 */
56(function () {
57if (typeof window === 'undefined') return;
58${code}
59})();
60`;
61
62fs.mkdirSync(path.dirname(DETECTOR_OUTPUT), { recursive: true });
63fs.writeFileSync(DETECTOR_OUTPUT, output);
64console.log(`Generated ${path.relative(ROOT, DETECTOR_OUTPUT)} (${(output.length / 1024).toFixed(1)} KB)`);
65
66// --- 2. Extract antipatterns.json ---
67
68// Include description so the devtools panel can show the full rule explanation
69// in tooltips.
70const apJson = ANTIPATTERNS.map(({ id, name, category, description }) => ({
71 id,
72 name,
73 category: category || 'quality',
74 description: description || '',
75}));
76fs.writeFileSync(AP_OUTPUT, JSON.stringify(apJson, null, 2) + '\n');
77console.log(`Generated ${path.relative(ROOT, AP_OUTPUT)} (${ANTIPATTERNS.length} rules)`);
78
79// --- 3. Zip packaging ---
80
81import { execSync } from 'child_process';
82
83const zipPath = path.join(ROOT, 'dist/extension.zip');
84fs.mkdirSync(path.join(ROOT, 'dist'), { recursive: true });
85try { fs.unlinkSync(zipPath); } catch {}
86execSync(
87 `zip -r ${JSON.stringify(zipPath)} . -x "STORE_LISTING.md" ".DS_Store"`,
88 { cwd: EXT_DIR, stdio: 'pipe' },
89);
90const size = fs.statSync(zipPath).size;
91console.log(`Packaged ${path.relative(ROOT, zipPath)} (${(size / 1024).toFixed(1)} KB)`);