diff --git a/package.json b/package.json index cf44ebe9b..619883cae 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ "scripts": { "build:skills": "bun run scripts/build.js --skip-root-sync", "build:skills:release": "bun run scripts/build.js", - "build": "bun run build:skills && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist", - "build:release": "bun run build:skills:release && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist", + "build": "node scripts/build-static-html-parsers.js --check && bun run build:skills && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist", + "build:release": "node scripts/build-static-html-parsers.js --check && bun run build:skills:release && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist", "build:browser": "node scripts/build-browser-detector.js", "build:static-html-parsers": "node scripts/build-static-html-parsers.js", "build:extension": "node scripts/build-extension.js", diff --git a/scripts/build-static-html-parsers.js b/scripts/build-static-html-parsers.js index ee51e4415..9bf785cda 100644 --- a/scripts/build-static-html-parsers.js +++ b/scripts/build-static-html-parsers.js @@ -5,9 +5,11 @@ * by bundling htmlparser2, css-select, css-tree, and domutils for skill/plugin installs. * * Run: node scripts/build-static-html-parsers.js + * Check: node scripts/build-static-html-parsers.js --check */ import fs from 'fs'; +import os from 'node:os'; import path from 'path'; import { fileURLToPath } from 'url'; import { spawnSync } from 'node:child_process'; @@ -18,29 +20,48 @@ const ROOT = path.resolve(__dirname, '..'); const ENTRY = path.join(__dirname, 'lib/static-html-parsers.entry.mjs'); const OUT_DIR = path.join(ROOT, 'cli/engine/vendor'); const OUTPUT = path.join(OUT_DIR, 'static-html-parsers.mjs'); - -fs.mkdirSync(OUT_DIR, { recursive: true }); - -const result = spawnSync( - 'bun', - ['build', ENTRY, '--outfile', OUTPUT, '--target', 'node', '--format', 'esm'], - { cwd: ROOT, encoding: 'utf8' }, -); - -if (result.status !== 0) { - process.stderr.write(result.stderr || result.stdout || 'bun build failed\n'); - process.exit(result.status ?? 1); -} - -const bundled = fs.readFileSync(OUTPUT, 'utf8'); -const output = `/** +const HEADER = `/** * GENERATED -- do not edit. Source: scripts/lib/static-html-parsers.entry.mjs * Rebuild: node scripts/build-static-html-parsers.js * * Bundles htmlparser2, css-select, css-tree, and domutils for skill/plugin installs. * Third-party licenses: see NOTICE.md. */ -${bundled}`; +`; -fs.writeFileSync(OUTPUT, output); -console.log(`Generated ${path.relative(ROOT, OUTPUT)} (${(output.length / 1024).toFixed(1)} KB)`); +function generate(outfile) { + fs.mkdirSync(path.dirname(outfile), { recursive: true }); + const result = spawnSync( + 'bun', + ['build', ENTRY, '--outfile', outfile, '--target', 'node', '--format', 'esm'], + { cwd: ROOT, encoding: 'utf8' }, + ); + if (result.status !== 0) { + process.stderr.write(result.stderr || result.stdout || 'bun build failed\n'); + process.exit(result.status ?? 1); + } + const output = HEADER + fs.readFileSync(outfile, 'utf8'); + fs.writeFileSync(outfile, output); + return output; +} + +if (process.argv.includes('--check')) { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-static-html-parsers-')); + const tmpFile = path.join(tmpDir, 'static-html-parsers.mjs'); + try { + const fresh = generate(tmpFile); + const committed = fs.readFileSync(OUTPUT, 'utf8'); + if (fresh !== committed) { + process.stderr.write( + 'cli/engine/vendor/static-html-parsers.mjs is stale. Run: node scripts/build-static-html-parsers.js\n', + ); + process.exit(1); + } + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + process.exit(0); +} + +generate(OUTPUT); +console.log(`Generated ${path.relative(ROOT, OUTPUT)} (${(fs.statSync(OUTPUT).size / 1024).toFixed(1)} KB)`); diff --git a/scripts/test-suites.mjs b/scripts/test-suites.mjs index ec2b2c47c..31c190e3a 100644 --- a/scripts/test-suites.mjs +++ b/scripts/test-suites.mjs @@ -25,7 +25,7 @@ export const SUITES = { description: 'Build, provider transforms, CLI helpers, context, and storage unit tests.', triggers: [ ...COMMON_INFRA_PATTERNS, - /^scripts\/(?!benchmark-detector|build-browser-detector|build-static-html-parsers|build-extension)/, + /^scripts\/(?!benchmark-detector|build-browser-detector|build-static-html-parsers|build-extension|lib\/static-html-parsers\.entry)/, /^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|comp-diff|comp-spec|build-phase|font-match|data\/font-index|concept-seed|generate-image|context|context-signals|critique-storage|design-parser|doctor|hook|impeccable-paths|is-generated|lib\/(artifact-schema|png|raster|image-metrics|font-fingerprint|font-index|hero-checks|composition-catalog|concept-catalog|provider|staleness|staleness-deep|staleness-notice|surface-briefs|target-slug|template-extensions)|pin|surface-brief))/, /^README(\.npm)?\.md$/, /^cli\/bin\//, @@ -96,6 +96,7 @@ export const SUITES = { /^cli\/engine\//, /^extension\/(background|content|detector|devtools|popup|manifest\.json)/, /^scripts\/(benchmark-detector|build-browser-detector|build-static-html-parsers|build-extension)\.js$/, + /^scripts\/lib\/static-html-parsers\.entry\.mjs$/, /^site\/(pages\/detector|public\/antipattern|data\/anti-patterns-catalog\.js)/, /^tests\/fixtures\/antipatterns/, ], diff --git a/tests/lib/detector-bundle.test.js b/tests/lib/detector-bundle.test.js index 5c10789ff..60ace6920 100644 --- a/tests/lib/detector-bundle.test.js +++ b/tests/lib/detector-bundle.test.js @@ -1,4 +1,5 @@ import { describe, expect, test } from 'bun:test'; +import { spawnSync } from 'node:child_process'; import fs from 'fs'; import path from 'path'; import { readSourceFiles } from '../../scripts/lib/utils.js'; @@ -19,6 +20,31 @@ describe('skill detector bundle', () => { expect(scriptNames.has('detector/vendor/static-html-parsers.mjs')).toBe(true); }); + test('static HTML parser vendor bundle matches a fresh rebuild', () => { + const result = spawnSync( + process.execPath, + [path.join(ROOT, 'scripts/build-static-html-parsers.js'), '--check'], + { cwd: ROOT, encoding: 'utf8' }, + ); + expect(result.status).toBe(0); + }); + + test('static HTML parser --check fails when the vendor bundle is stale', () => { + const vendor = path.join(ROOT, 'cli/engine/vendor/static-html-parsers.mjs'); + const original = fs.readFileSync(vendor, 'utf8'); + try { + fs.writeFileSync(vendor, original.replace('htmlparser2', 'htmlparser2-stale')); + const result = spawnSync( + process.execPath, + [path.join(ROOT, 'scripts/build-static-html-parsers.js'), '--check'], + { cwd: ROOT, encoding: 'utf8' }, + ); + expect(result.status).toBe(1); + } finally { + fs.writeFileSync(vendor, original); + } + }); + test('critique references the bundled detector command', () => { const critique = fs.readFileSync(path.join(ROOT, 'skill/reference/critique.md'), 'utf-8');