From 894b95b988897886982b663cae0be54124e09ece Mon Sep 17 00:00:00 2001 From: Abdul Wahab Date: Tue, 1 Sep 2026 15:17:45 +0500 Subject: [PATCH] Fix: fail CI when the vendored HTML parser bundle is stale. Greptile caught that editing the bundle entry did not refresh the committed vendor file, and neither build nor the detector suite would notice. --check compares a fresh rebuild, bun run build runs that check, and the detector suite now triggers on the entry. AI-assisted. Co-authored-by: Cursor --- package.json | 4 +- scripts/build-static-html-parsers.js | 59 +++++++++++++++++++--------- scripts/test-suites.mjs | 3 +- tests/lib/detector-bundle.test.js | 26 ++++++++++++ 4 files changed, 70 insertions(+), 22 deletions(-) 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');