mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 23:26:39 +03:00
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.
140 lines
4.7 KiB
JavaScript
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)`);
|
|
}
|