Files
pbakaus_impeccable/scripts/build-static-html-parsers.js
T
Paul Bakaus ff94918ddc 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.
2026-09-03 08:31:27 -07:00

140 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Generates cli/engine/vendor/static-html-parsers.mjs
* 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 '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 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 static-html-parsers.LICENSES.txt.
*/
`;
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);
}
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 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(
`${path.relative(ROOT, committedPath)} is stale. ` +
'Run: node scripts/build-static-html-parsers.js\n',
);
process.exit(1);
}
}
process.exit(0);
}
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)`);
}