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.`,
);