#!/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)`); }