diff --git a/cli/engine/vendor/static-html-parsers.mjs b/cli/engine/vendor/static-html-parsers.mjs index 910475d96..9b685ccee 100644 --- a/cli/engine/vendor/static-html-parsers.mjs +++ b/cli/engine/vendor/static-html-parsers.mjs @@ -1,6 +1,7 @@ /** * GENERATED -- do not edit. Source: scripts/lib/static-html-parsers.entry.mjs * Rebuild: node scripts/build-static-html-parsers.js + * Source digest: 7169e400f73e8c91 * * Bundles htmlparser2, css-select, css-tree, and domutils for skill/plugin installs. * Third-party licenses: see NOTICE.md. diff --git a/scripts/build-static-html-parsers.js b/scripts/build-static-html-parsers.js index 9bf785cda..34783b99f 100644 --- a/scripts/build-static-html-parsers.js +++ b/scripts/build-static-html-parsers.js @@ -8,8 +8,8 @@ * Check: node scripts/build-static-html-parsers.js --check */ +import { createHash } from 'node:crypto'; import fs from 'fs'; -import os from 'node:os'; import path from 'path'; import { fileURLToPath } from 'url'; import { spawnSync } from 'node:child_process'; @@ -20,14 +20,32 @@ 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'); -const HEADER = `/** +const PARSER_PACKAGES = ['htmlparser2', 'css-select', 'css-tree', 'domutils']; +const DIGEST_RE = /^\s*\* Source digest: ([0-9a-f]+)\s*$/m; + +function sourceDigest() { + const hash = createHash('sha256'); + hash.update(fs.readFileSync(ENTRY)); + hash.update('\n'); + for (const name of PARSER_PACKAGES) { + const pkgPath = path.join(ROOT, 'node_modules', name, 'package.json'); + const version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version; + hash.update(`${name}@${version}\n`); + } + return hash.digest('hex').slice(0, 16); +} + +function header(digest) { + return `/** * GENERATED -- do not edit. Source: scripts/lib/static-html-parsers.entry.mjs * Rebuild: node scripts/build-static-html-parsers.js + * Source digest: ${digest} * * Bundles htmlparser2, css-select, css-tree, and domutils for skill/plugin installs. * Third-party licenses: see NOTICE.md. */ `; +} function generate(outfile) { fs.mkdirSync(path.dirname(outfile), { recursive: true }); @@ -40,25 +58,20 @@ function generate(outfile) { process.stderr.write(result.stderr || result.stdout || 'bun build failed\n'); process.exit(result.status ?? 1); } - const output = HEADER + fs.readFileSync(outfile, 'utf8'); + const output = header(sourceDigest()) + 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 }); + const committed = fs.readFileSync(OUTPUT, 'utf8'); + const found = committed.match(DIGEST_RE)?.[1]; + const expected = sourceDigest(); + if (found !== expected) { + process.stderr.write( + 'cli/engine/vendor/static-html-parsers.mjs is stale. Run: node scripts/build-static-html-parsers.js\n', + ); + process.exit(1); } process.exit(0); } diff --git a/tests/lib/detector-bundle.test.js b/tests/lib/detector-bundle.test.js index 60ace6920..78abaab23 100644 --- a/tests/lib/detector-bundle.test.js +++ b/tests/lib/detector-bundle.test.js @@ -20,12 +20,15 @@ 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', () => { + test('static HTML parser vendor bundle matches the source digest', () => { const result = spawnSync( process.execPath, [path.join(ROOT, 'scripts/build-static-html-parsers.js'), '--check'], { cwd: ROOT, encoding: 'utf8' }, ); + if (result.status !== 0) { + throw new Error(result.stderr || result.stdout || `--check exited ${result.status}`); + } expect(result.status).toBe(0); }); @@ -33,7 +36,7 @@ describe('skill detector bundle', () => { 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')); + fs.writeFileSync(vendor, original.replace(/Source digest: [0-9a-f]+/, 'Source digest: deadbeefdeadbeef')); const result = spawnSync( process.execPath, [path.join(ROOT, 'scripts/build-static-html-parsers.js'), '--check'],