The server gate adds `vite build` (and with it devOnlyGuard, which until now first ran inside the image build, after main was pushed) and `npm audit --omit=dev --audit-level=high`. pip-audit is blocking: when it was finally read it held three urllib3 advisories with a fix available. urllib3 goes 2.7.0 -> 2.8.0 as a single in-place pin, passing pip check, rather than a regeneration that would move every package. The dev toolchain's 18 advisories are gone: - vite 5 -> 6.4.3 (path traversal, fs.deny bypass, and esbuild <= 0.24.2 through it); - vitest 3 -> 4.1.11 (mocker path traversal); - typescript-eslint 8.46 -> 8.71; - `npm audit fix` for the rest. tsc, eslint, knip, all 2269 tests and the build pass unchanged. scripts/check-node-modules.mjs checks that frontend/node_modules holds exactly what package-lock.json pins. The local gate's frontend lanes judge the host tree, and nothing tied it to the lock the image installs.
41 lines
2.1 KiB
JavaScript
41 lines
2.1 KiB
JavaScript
#!/usr/bin/env node
|
|
// Does frontend/node_modules hold exactly what frontend/package-lock.json pins? (E32)
|
|
//
|
|
// The local gate's frontend lanes run against the HOST's node_modules, and nothing tied that tree to
|
|
// the lock: a `git pull` that bumped a dependency, or an `npm install` that was never committed, left
|
|
// tsc/eslint/vitest judging the code against packages the build image (`npm ci`) does not use — a
|
|
// green that proves nothing about what ships, in either direction. CI is immune (it runs `npm ci`);
|
|
// this makes the local lanes say so instead of quietly differing.
|
|
//
|
|
// Checks every package the lock places under node_modules/: present, at the pinned version. A package
|
|
// marked `optional` may be absent (a platform binary for another OS). Exit 1 with the list and the
|
|
// one command that fixes it.
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const FRONTEND = join(dirname(fileURLToPath(import.meta.url)), "..", "frontend");
|
|
const lock = JSON.parse(readFileSync(join(FRONTEND, "package-lock.json"), "utf8"));
|
|
|
|
const problems = [];
|
|
for (const [path, entry] of Object.entries(lock.packages ?? {})) {
|
|
if (!path.startsWith("node_modules/") || entry.link) continue;
|
|
const manifest = join(FRONTEND, path, "package.json");
|
|
if (!existsSync(manifest)) {
|
|
if (!entry.optional) problems.push(`missing ${path.slice(13)}@${entry.version}`);
|
|
continue;
|
|
}
|
|
const installed = JSON.parse(readFileSync(manifest, "utf8")).version;
|
|
if (installed !== entry.version)
|
|
problems.push(`mismatch ${path.slice(13)}: installed ${installed}, lock pins ${entry.version}`);
|
|
}
|
|
|
|
if (problems.length) {
|
|
console.error(`frontend/node_modules does not match package-lock.json (${problems.length}):`);
|
|
for (const p of problems.slice(0, 20)) console.error(` ${p}`);
|
|
if (problems.length > 20) console.error(` … and ${problems.length - 20} more`);
|
|
console.error("Fix: cd frontend && npm ci");
|
|
process.exit(1);
|
|
}
|
|
console.log(`node_modules matches package-lock.json (${Object.keys(lock.packages).length - 1} entries)`);
|