build-extension.js

 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)`);