Spaces:
Running
Running
File size: 8,225 Bytes
1a6d0d2 2936030 1a6d0d2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | // Repairs flagged package copies inside the shipped global npm tree.
//
// npm pins its dependency tree exactly, so `npm install -g npm@latest`
// cannot lift bundled copies off flagged versions. The patched releases are
// installed into the /tmp/overlay scratch project by the Dockerfile (the
// only place the versions appear), and this script replaces every flagged
// copy under /usr/local/lib/node_modules with the scratch copy - using
// real Node resolution semantics, because the bundle deliberately keeps
// several majors of the same package alive in nested scopes (for example
// minipass 3.x under minipass-pipeline, 7.x at the top).
//
// After the repair it re-walks the live tree: every package's own
// dependency declarations must resolve to copies that satisfy them, and
// no same-major copy of a target package may remain below the patched
// release. Any violation fails the build.
const fs = require("node:fs");
const path = require("node:path");
const ROOT = "/usr/local/lib/node_modules";
const SCRATCH = "/tmp/overlay/node_modules";
const TARGETS = [
"tar",
"brace-expansion",
"ip-address",
"undici",
"postcss-selector-parser",
];
let semver;
try {
semver = require(path.join(ROOT, "npm/node_modules/semver"));
} catch {
console.error("Cannot load semver from the npm bundle; repair aborted.");
process.exit(1);
}
const readPkg = (p) => JSON.parse(fs.readFileSync(p, "utf8"));
const pkgAt = (dir) => {
const f = path.join(dir, "package.json");
return fs.existsSync(f) ? readPkg(f) : null;
};
const scratchPkg = (name) => readPkg(path.join(SCRATCH, name, "package.json"));
const scratchVersion = (name) => scratchPkg(name).version;
// All package dirs under container dirs laid out like a node_modules:
// scoped and plain packages, recursing into every nested node_modules.
function collectPackageDirs(containers, acc = []) {
for (const cont of containers) {
if (!fs.existsSync(cont)) continue;
const pkgs = [];
for (const entry of fs.readdirSync(cont, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const full = path.join(cont, entry.name);
if (entry.name.startsWith("@")) {
for (const s of fs.readdirSync(full, { withFileTypes: true })) {
const sf = path.join(full, s.name);
if (s.isDirectory() && fs.existsSync(path.join(sf, "package.json"))) {
pkgs.push(sf);
}
}
} else if (fs.existsSync(path.join(full, "package.json"))) {
pkgs.push(full);
}
}
acc.push(...pkgs);
collectPackageDirs(
pkgs.map((p) => path.join(p, "node_modules")),
acc,
);
}
return acc;
}
// What `require(dep)` from inside startDir actually resolves to: walk the
// ancestors of startDir checking each one's node_modules child.
function resolveCopy(startDir, dep) {
let dir = startDir;
for (;;) {
const candidate = path.join(dir, "node_modules", dep);
if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
const parent = path.dirname(dir);
if (parent === dir) return null;
dir = parent;
}
}
const replaceWith = (dir, scratchName) => {
fs.rmSync(dir, { recursive: true, force: true });
fs.cpSync(path.join(SCRATCH, scratchName), dir, { recursive: true });
};
// Phase 1: replace copies of each target that sit below the patched release.
const replaced = [];
for (const target of TARGETS) {
const wanted = scratchVersion(target);
const copies = collectPackageDirs([ROOT]).filter(
(d) => pkgAt(d) && pkgAt(d).name === target,
);
if (copies.length === 0) {
console.log(`target absent from the tree, nothing to repair: ${target}`);
continue;
}
for (const dir of copies) {
const current = pkgAt(dir).version;
if (!semver.lt(current, wanted)) {
console.log(`${target}@${current} already at or above ${wanted}, kept`);
continue;
}
replaceWith(dir, target);
replaced.push({ dir, name: target, from: current, to: wanted });
console.log(`repaired ${target} ${current} -> ${wanted} at ${dir}`);
}
}
// Phase 2: closure. Every dependency the replaced code newly needs must be
// resolvable from where the replaced copy lives: where the walked copy is
// missing, copy it in from the scratch install; where it fails the new
// range, bump it the same way. Loop until no further changes.
const queue = replaced.map((r) => r.dir);
let changes = queue.length;
while (queue.length > 0) {
const dir = queue.shift();
const pkg = pkgAt(dir);
for (const [dep, range] of Object.entries(pkg.dependencies || {})) {
const copy = resolveCopy(dir, dep);
if (!copy) {
const host = path.dirname(dir); // the node_modules that contains `dir`
const target = path.join(host, dep);
if (!fs.existsSync(path.join(SCRATCH, dep, "package.json"))) {
console.error(
`Missing ${dep} required by ${pkg.name}, not in overlay: fail.`,
);
process.exit(1);
}
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.cpSync(path.join(SCRATCH, dep), target, { recursive: true });
console.log(`added ${dep}@${scratchVersion(dep)} at ${target}`);
queue.push(target);
changes += 1;
continue;
}
const current = pkgAt(copy).version;
if (semver.satisfies(current, range)) continue;
if (!fs.existsSync(path.join(SCRATCH, dep, "package.json"))) {
console.error(
`Cannot bump ${dep} (not in overlay) for ${pkg.name}@${range}: fail.`,
);
process.exit(1);
}
const wanted = scratchVersion(dep);
if (!semver.lt(current, wanted)) {
console.error(
`${copy}@${current} fails ${pkg.name}'s ${range} but is not below the ` +
`overlay version ${wanted}; refusing to guess.`,
);
process.exit(1);
}
replaceWith(copy, dep);
console.log(`bumped ${dep} ${current} -> ${wanted} at ${copy}`);
queue.push(copy);
changes += 1;
}
}
// Phase 3: full resolution validation over the final live tree. Every
// package's own dependency and optionalDependency declarations must
// resolve to a copy that satisfies the declared range. (peerDependencies
// are skipped: the scratch install resolved and provided what the replaced
// packages need; untouched packages kept the tree they were installed for.)
let unsatisfied = 0;
for (const dir of collectPackageDirs([ROOT])) {
const pkg = pkgAt(dir);
if (!pkg) continue;
for (const group of ["dependencies", "optionalDependencies"]) {
for (const [dep, range] of Object.entries(pkg[group] || {})) {
const copy = resolveCopy(dir, dep);
if (!copy) {
console.error(
`RESOLVE FAILED: ${pkg.name}@${pkg.version} -> ${dep}@${range} missing`,
);
unsatisfied += 1;
continue;
}
if (!semver.satisfies(pkgAt(copy).version, range)) {
console.error(
`RANGE FAILED: ${pkg.name}@${pkg.version} declares ${dep}@${range}, ` +
`resolved copy is ${pkgAt(copy).version}`,
);
unsatisfied += 1;
}
}
}
}
if (unsatisfied > 0) {
console.error(`Refusing to ship: ${unsatisfied} unsatisfied declaration(s).`);
process.exit(1);
}
// Phase 4: the target invariant itself. No copy of a target package in the
// same major line as the patched release may remain below it. Different
// majors (e.g. a legitimately old nested brace-expansion 1.x serving a
// nested minimatch 3) are out of this repair's claim.
let stale = 0;
for (const target of TARGETS) {
const wanted = scratchVersion(target);
for (const dir of collectPackageDirs([ROOT])) {
const pkg = pkgAt(dir);
if (!pkg || pkg.name !== target) continue;
if (
semver.major(pkg.version) === semver.major(wanted) &&
!semver.gte(pkg.version, wanted)
) {
console.error(`STALE: ${target}@${pkg.version} at ${dir} (< ${wanted})`);
stale += 1;
}
}
}
if (stale > 0) {
console.error(
`Invariant broken: ${stale} stale same-major copy(ies) remain.`,
);
process.exit(1);
}
console.log(
`npm bundle repair complete: ${replaced.length} replaced, ` +
`${changes - replaced.length} closure changes, tree resolves clean.`,
);
|