Pulse/scripts/check-frontend-serialization.mjs
pulse-triage[bot] 699deceaa4 Reject stale Solid serialization overrides before frontend installation
Bind the existing qualified security override to its exact consumer and serializer pair instead of letting npm overrides silently carry it into a newer Solid runtime. Resolve nested lock edges, retain the strict complete audit and security floors, and cover the two stale dependencies actually present in PR 2759 with positive, partial-repair and nested-resolution controls.

Change-source: pulse-maintainer
2026-10-14 01:53:13 +01:00

99 lines
4.5 KiB
JavaScript

#!/usr/bin/env node
// A security override is not blanket compatibility approval for a new Solid.
// Check the locked runtime edges before npm ci can hide them behind overrides.
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const defaultLock = new URL('../frontend-modern/package-lock.json', import.meta.url);
const stableVersion = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
function versionParts(value) {
const match = typeof value === 'string' && stableVersion.exec(value);
const parts = match ? match.slice(1).map(Number) : null;
return parts?.every(Number.isSafeInteger) ? parts : null;
}
function resolvedDependency(packages, consumerPath, dependency) {
let parent = consumerPath;
while (true) {
const nested = `${parent}/node_modules/${dependency}`;
if (Object.hasOwn(packages, nested)) return { path: nested, entry: packages[nested] };
const boundary = parent.lastIndexOf('/node_modules/');
if (boundary < 0) break;
parent = parent.slice(0, boundary);
}
const root = `node_modules/${dependency}`;
return Object.hasOwn(packages, root) ? { path: root, entry: packages[root] } : null;
}
export function serializationGraphErrors(lock) {
const packages = lock?.packages;
if (lock?.lockfileVersion !== 3 || !packages || typeof packages !== 'object' || Array.isArray(packages)) {
return ['Expected an npm v3 lock with a complete packages map.'];
}
const consumers = Object.entries(packages).filter(([path]) =>
path === 'node_modules/solid-js' || path.endsWith('/node_modules/solid-js'),
);
if (!consumers.some(([path]) => path === 'node_modules/solid-js')) {
return ['The frontend lock must contain its direct Solid runtime.'];
}
const errors = [];
for (const [path, consumer] of consumers) {
if (!versionParts(consumer?.version)) {
errors.push('Solid must have an exact stable locked version.');
continue;
}
for (const dependency of ['seroval', 'seroval-plugins']) {
const range = consumer.dependencies?.[dependency];
// These runtime edges currently use full stable tilde ranges. A changed
// range format needs review rather than an invented semver interpretation.
const floor = typeof range === 'string' && range.startsWith('~') && versionParts(range.slice(1));
if (!floor) {
errors.push(`Solid ${consumer.version}: review the new ${dependency} requirement (expected ~major.minor.patch).`);
continue;
}
const resolved = resolvedDependency(packages, path, dependency);
const installed = versionParts(resolved?.entry?.version);
if (!installed) {
errors.push(`Solid ${consumer.version}: ${dependency} must resolve to an exact stable locked version.`);
continue;
}
const compatible = installed[0] === floor[0] && installed[1] === floor[1] && installed[2] >= floor[2];
// 795e534517 qualified this one security override with actual cyclic,
// typed-array and plugin consumers. Preserve it, but do not apply that
// old proof to a new consumer, dependency, declared range or nested copy.
const qualifiedOverride = path === 'node_modules/solid-js' &&
consumer.version === '1.9.15' && dependency === 'seroval' &&
range === '~1.5.4' && resolved.path === 'node_modules/seroval' &&
resolved.entry.version === '1.6.3';
if (!compatible && !qualifiedOverride) {
errors.push(`Solid ${consumer.version} requires ${dependency} ${range}, but resolves ${resolved.entry.version}. Update the coupled graph and retain consumer compatibility proof; an npm override is not that proof.`);
}
}
}
return errors;
}
export function main(args = process.argv.slice(2)) {
if (args.length !== 0 && (args.length !== 2 || args[0] !== '--lockfile')) {
console.error('Usage: node scripts/check-frontend-serialization.mjs [--lockfile FILE]');
return 2;
}
let lock;
try {
lock = JSON.parse(readFileSync(args.length ? args[1] : defaultLock, 'utf8'));
} catch {
console.error('Frontend serialization graph: lock read/JSON parse failed.');
return 2;
}
const errors = serializationGraphErrors(lock);
for (const error of errors) console.error(`Frontend serialization graph: ${error}`);
if (errors.length) return 1;
console.log('Frontend serialization graph: Solid runtime edges match, or use the exact qualified security override.');
return 0;
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
process.exitCode = main();
}