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';
16
17const __dirname = path.dirname(fileURLToPath(import.meta.url));
18const ROOT = path.resolve(__dirname, '..');
19const EXT_DIR = path.join(ROOT, 'extension');
20
21const SOURCE = path.join(ROOT, 'src/detect-antipatterns.mjs');
22const DETECTOR_OUTPUT = path.join(EXT_DIR, 'detector/detect.js');
23const AP_OUTPUT = path.join(EXT_DIR, 'detector/antipatterns.json');
24
25let code = fs.readFileSync(SOURCE, 'utf-8');
26
27// --- 1. Build detector ---
28
29// Strip shebang
30code = code.replace(/^#!.*\n/, '');
31// Strip sections between @browser-strip-start / @browser-strip-end markers
32code = code.replace(/^\/\/ @browser-strip-start\n[\s\S]*?^\/\/ @browser-strip-end\n?/gm, '');
33// Set IS_BROWSER = true (dead-code eliminates Node paths)
34code = code.replace(/^const IS_BROWSER = .*$/m, 'const IS_BROWSER = true;');
35
36const output = `/**
37 * Anti-Pattern Browser Detector for Impeccable (Extension Variant)
38 * Copyright (c) 2026 Paul Bakaus
39 * SPDX-License-Identifier: Apache-2.0
40 *
41 * GENERATED -- do not edit. Source: detect-antipatterns.mjs
42 * Rebuild: node scripts/build-extension.js
43 */
44(function () {
45if (typeof window === 'undefined') return;
46${code}
47})();
48`;
49
50fs.mkdirSync(path.dirname(DETECTOR_OUTPUT), { recursive: true });
51fs.writeFileSync(DETECTOR_OUTPUT, output);
52console.log(`Generated ${path.relative(ROOT, DETECTOR_OUTPUT)} (${(output.length / 1024).toFixed(1)} KB)`);
53
54// --- 2. Extract antipatterns.json ---
55
56const rawSource = fs.readFileSync(SOURCE, 'utf-8');
57const apMatch = rawSource.match(/const ANTIPATTERNS = \[([\s\S]*?)\n\];/);
58if (apMatch) {
59 // Convert JS object literals to JSON. Include description so the
60 // devtools panel can show the full rule explanation in tooltips —
61 // previously this dropped description and the panel had nothing to display.
62 const antipatterns = new Function(`return [${apMatch[1]}]`)();
63 const apJson = antipatterns.map(({ id, name, category, description }) => ({
64 id,
65 name,
66 category: category || 'quality',
67 description: description || '',
68 }));
69 fs.writeFileSync(AP_OUTPUT, JSON.stringify(apJson, null, 2) + '\n');
70 console.log(`Generated ${path.relative(ROOT, AP_OUTPUT)} (${antipatterns.length} rules)`);
71}
72
73// --- 3. Zip packaging ---
74
75import { execSync } from 'child_process';
76
77const zipPath = path.join(ROOT, 'dist/extension.zip');
78fs.mkdirSync(path.join(ROOT, 'dist'), { recursive: true });
79try { fs.unlinkSync(zipPath); } catch {}
80execSync(
81 `zip -r ${JSON.stringify(zipPath)} . -x "STORE_LISTING.md" ".DS_Store"`,
82 { cwd: EXT_DIR, stdio: 'pipe' },
83);
84const size = fs.statSync(zipPath).size;
85console.log(`Packaged ${path.relative(ROOT, zipPath)} (${(size / 1024).toFixed(1)} KB)`);