// The decisions behind `npm run assert-bundled`, kept separate from the script // that does the I/O so they can be unit-tested (milestone-14 deviation 24). // // The licence inventory has to cover every package whose bytes ship, and the // lockfile does not answer that question: it lists what could be reached, not // what rollup kept. Several packages of the non-dev closure are recorded as // tree-shaken away, and if application code starts importing one of them, no // lockfile, no version and no dependency set changes — only the bundle does. So // the bundle is what this reads. const sectionHeading = "[npm packages bundled into web/dist]"; // A sourcemap `sources` entry for a dependency ends in // `node_modules//` or `node_modules/@//`. Only // the last `node_modules/` matters: a nested dependency's path carries two. export function packageFromSource(source) { const marker = "node_modules/"; const at = source.lastIndexOf(marker); if (at === -1) return null; const rest = source.slice(at + marker.length); const parts = rest.split("/"); if (parts.length === 0 || parts[0] === "") return null; if (parts[0].startsWith("@")) { if (parts.length < 2 || parts[1] === "") return null; return `${parts[0]}/${parts[1]}`; } return parts[0]; } /// The sorted, deduplicated package set of a list of sourcemap `sources` arrays. export function bundledPackages(sourceLists) { const found = new Set(); for (const sources of sourceLists) { for (const source of sources) { const name = packageFromSource(source); if (name !== null) found.add(name); } } return [...found].sort(); } /// The recorded section of `licenses/dependency-identity.txt`: every non-blank /// line after the heading, up to the next `[section]`. export function recordedPackages(text) { const recorded = new Set(); let grabbing = false; for (const raw of text.split("\n")) { const line = raw.trim(); if (!grabbing) { if (line === sectionHeading) grabbing = true; continue; } if (line.startsWith("[")) break; if (line !== "") recorded.add(line); } return grabbing ? [...recorded].sort() : null; } /// What changed, in the two directions that mean different things: a package /// that started shipping needs a licence decision, and one that stopped needs /// the record corrected. export function comparePackages(recorded, bundled) { const inBundle = new Set(bundled); const inRecord = new Set(recorded); return { added: bundled.filter((name) => !inRecord.has(name)), removed: recorded.filter((name) => !inBundle.has(name)), }; } export function formatDiff(recorded, bundled) { const { added, removed } = comparePackages(recorded, bundled); const lines = []; for (const name of removed) lines.push(`-${name}`); for (const name of added) lines.push(`+${name}`); return lines.join("\n"); }