Stabilize bundled parser generation

Use exact-pinned esbuild so the committed bundle is reproducible across CI runners, generate and ship complete dependency licenses, keep the bundle under a size ceiling, and route degraded scans through the shared operational-failure exit handling.

AI assistance: Codex audited PR #693 and implemented this hardening.
This commit is contained in:
Paul Bakaus
2026-09-03 08:31:27 -07:00
parent 894b95b988
commit ff94918ddc
9 changed files with 475 additions and 9640 deletions
+102 -30
View File
@@ -8,60 +8,132 @@
* 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';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { build } from 'esbuild';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
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 BUNDLE_NAME = 'static-html-parsers.mjs';
const LICENSES_NAME = 'static-html-parsers.LICENSES.txt';
const MAX_BUNDLE_BYTES = 256 * 1024;
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.
* Third-party licenses: see static-html-parsers.LICENSES.txt.
*/
`;
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);
function packageRoots(inputs) {
const roots = new Map();
for (const input of Object.keys(inputs)) {
const segments = input.replaceAll('\\', '/').split('/');
const marker = segments.lastIndexOf('node_modules');
if (marker < 0 || marker + 1 >= segments.length) continue;
const nameParts = segments[marker + 1].startsWith('@')
? segments.slice(marker + 1, marker + 3)
: segments.slice(marker + 1, marker + 2);
const packageName = nameParts.join('/');
const packageRoot = path.resolve(ROOT, ...segments.slice(0, marker + 1), ...nameParts);
roots.set(packageRoot, packageName);
}
const output = HEADER + fs.readFileSync(outfile, 'utf8');
fs.writeFileSync(outfile, output);
return output;
return [...roots]
.map(([packageRoot, packageName]) => [packageName, packageRoot])
.sort(([aName, aRoot], [bName, bRoot]) =>
aName.localeCompare(bName) || aRoot.localeCompare(bRoot));
}
function buildLicenseFile(inputs) {
const sections = packageRoots(inputs).map(([packageName, packageRoot]) => {
const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
const licenseFile = fs.readdirSync(packageRoot)
.filter((name) => /^licen[cs]e(?:\.|$)/i.test(name))
.sort()[0];
if (!licenseFile) throw new Error(`No license file found for bundled package ${packageName}`);
const licenseText = fs.readFileSync(path.join(packageRoot, licenseFile), 'utf8')
.replaceAll('\r\n', '\n')
.replace(/[ \t]+$/gm, '')
.trim();
return [
`Package: ${packageName}@${manifest.version}`,
`License: ${manifest.license}`,
'',
licenseText,
].join('\n');
});
return [
'Static HTML parser bundle: third-party licenses',
'Generated by scripts/build-static-html-parsers.js. Do not edit.',
'',
sections.join('\n\n------------------------------------------------------------------------\n\n'),
'',
].join('\n');
}
async function generate() {
const result = await build({
absWorkingDir: ROOT,
bundle: true,
entryPoints: [ENTRY],
format: 'esm',
legalComments: 'none',
metafile: true,
minify: true,
platform: 'node',
target: 'node22',
write: false,
});
const bundle = HEADER + result.outputFiles[0].text;
if (Buffer.byteLength(bundle) > MAX_BUNDLE_BYTES) {
throw new Error(
`${BUNDLE_NAME} is ${(Buffer.byteLength(bundle) / 1024).toFixed(1)} KB; ` +
`the ${MAX_BUNDLE_BYTES / 1024} KB limit prevents provider-copy bloat`,
);
}
return new Map([
[BUNDLE_NAME, bundle],
[LICENSES_NAME, buildLicenseFile(result.metafile.inputs)],
]);
}
function checkDirectoryArg() {
const index = process.argv.indexOf('--check-dir');
if (index < 0) return OUT_DIR;
if (!process.argv.includes('--check') || !process.argv[index + 1]) {
throw new Error('--check-dir requires --check and a directory path');
}
return path.resolve(process.argv[index + 1]);
}
const generated = await generate();
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');
const checkDir = checkDirectoryArg();
for (const [name, fresh] of generated) {
const committedPath = path.join(checkDir, name);
const committed = fs.existsSync(committedPath) ? fs.readFileSync(committedPath, 'utf8') : null;
if (fresh !== committed) {
process.stderr.write(
'cli/engine/vendor/static-html-parsers.mjs is stale. Run: node scripts/build-static-html-parsers.js\n',
`${path.relative(ROOT, committedPath)} 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)`);
fs.mkdirSync(OUT_DIR, { recursive: true });
for (const [name, contents] of generated) {
const output = path.join(OUT_DIR, name);
fs.writeFileSync(output, contents);
console.log(`Generated ${path.relative(ROOT, output)} (${(Buffer.byteLength(contents) / 1024).toFixed(1)} KB)`);
}