#!/usr/bin/env node // The set of npm packages whose bytes reach web/dist must be exactly the set // recorded in licenses/dependency-identity.txt (milestone-14 ruling 3). // // The shipped build carries no sourcemaps, so this makes a second build with // them into its own directory: the `sources` list of each chunk names the // modules that went into it, and the artifact `npm run build` produced stays // untouched. Runs from web/ as `npm run assert-bundled`, on a laptop exactly as // on the runner. import { execFileSync } from "node:child_process"; import { readdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { bundledPackages, comparePackages, formatDiff, recordedPackages } from "./bundledPackages.mjs"; const webRoot = dirname(dirname(fileURLToPath(import.meta.url))); const outDir = "dist-sourcemap"; const identityFile = join(webRoot, "..", "licenses", "dependency-identity.txt"); function fail(message) { process.stderr.write(`${message}\n`); process.exit(1); } function mapFiles(relativeDir) { const absolute = join(webRoot, relativeDir); let entries; try { entries = readdirSync(absolute, { withFileTypes: true }); } catch (err) { fail(`assert-bundled: cannot read ${relativeDir}: ${err.message}`); } const found = []; for (const entry of entries) { const child = `${relativeDir}/${entry.name}`; if (entry.isDirectory()) { found.push(...mapFiles(child)); } else if (entry.isFile() && entry.name.endsWith(".map")) { found.push(child); } } return found.sort(); } // The binary npm ci installed, never `npx`: npx silently downloads a package it // cannot find locally, so a wrong working directory would turn a licence check // into an unpinned fetch from the network. try { execFileSync( join(webRoot, "node_modules", ".bin", "vite"), ["build", "--sourcemap", "--outDir", outDir, "--emptyOutDir"], { cwd: webRoot, stdio: ["ignore", "ignore", "inherit"], }, ); } catch (err) { fail(`assert-bundled: the sourcemap build failed: ${err.message}`); } const maps = mapFiles(outDir); if (maps.length === 0) fail("assert-bundled: the sourcemap build produced no .map files; this check cannot run blind"); const sourceLists = maps.map((path) => { const raw = readFileSync(join(webRoot, path), "utf8"); let parsed; try { parsed = JSON.parse(raw); } catch (err) { fail(`assert-bundled: ${path} is not JSON: ${err.message}`); } return Array.isArray(parsed.sources) ? parsed.sources : []; }); const bundled = bundledPackages(sourceLists); let identity; try { identity = readFileSync(identityFile, "utf8"); } catch (err) { fail(`assert-bundled: cannot read licenses/dependency-identity.txt: ${err.message}`); } const recorded = recordedPackages(identity); if (recorded === null) { fail("assert-bundled: licenses/dependency-identity.txt has no '[npm packages bundled into web/dist]' section"); } if (recorded.length === 0) { fail("assert-bundled: the '[npm packages bundled into web/dist]' section is empty"); } const { added, removed } = comparePackages(recorded, bundled); if (added.length !== 0 || removed.length !== 0) { process.stderr.write(`${formatDiff(recorded, bundled)}\n\n`); fail( [ "the set of npm packages in web/dist has changed (-recorded +current).", "Work out what the change means for licenses/inventory.zon first, then record", "the new list in that section of licenses/dependency-identity.txt.", ].join("\n"), ); } process.stdout.write(`web/dist bundles exactly the ${bundled.length} recorded packages:\n`); for (const name of bundled) process.stdout.write(`${name}\n`);