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'],