mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-20 01:56:37 +03:00
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:
@@ -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)`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user