@vltpkg/graph 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actual/load.js +1 -0
- package/dist/dependencies.d.ts +16 -0
- package/dist/dependencies.js +24 -0
- package/dist/fixup-added-names.js +4 -2
- package/dist/graph.d.ts +41 -4
- package/dist/graph.js +145 -25
- package/dist/ideal/append-nodes.d.ts +2 -2
- package/dist/ideal/append-nodes.js +226 -60
- package/dist/ideal/build-ideal-from-starting-graph.js +28 -0
- package/dist/ideal/canonicalize-peer-ids.d.ts +44 -0
- package/dist/ideal/canonicalize-peer-ids.js +421 -0
- package/dist/ideal/get-importer-specs.d.ts +5 -1
- package/dist/ideal/get-importer-specs.js +51 -24
- package/dist/ideal/move-peer-store-dirs.d.ts +7 -0
- package/dist/ideal/move-peer-store-dirs.js +59 -0
- package/dist/ideal/peers.d.ts +16 -5
- package/dist/ideal/peers.js +187 -31
- package/dist/ideal/refresh-ideal-graph.d.ts +11 -2
- package/dist/ideal/refresh-ideal-graph.js +62 -4
- package/dist/ideal/remove-satisfied-specs.d.ts +11 -2
- package/dist/ideal/remove-satisfied-specs.js +18 -1
- package/dist/ideal/types.d.ts +20 -0
- package/dist/install.js +22 -3
- package/dist/lockfile/format-options-change.d.ts +5 -0
- package/dist/lockfile/format-options-change.js +20 -0
- package/dist/lockfile/load-nodes.js +3 -1
- package/dist/lockfile/load.d.ts +7 -1
- package/dist/lockfile/load.js +63 -7
- package/dist/lockfile/types.d.ts +17 -0
- package/dist/modifiers.d.ts +10 -0
- package/dist/modifiers.js +45 -9
- package/dist/node.d.ts +12 -0
- package/dist/node.js +42 -0
- package/dist/reify/index.js +30 -18
- package/dist/reify/update-importers-package-json.js +12 -2
- package/dist/transfer-data/load.js +3 -3
- package/dist/uninstall.js +2 -0
- package/package.json +21 -21
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { joinDepIDTuple, joinExtra, splitDepID } from '@vltpkg/dep-id';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { copyPackageMetadata } from "../node.js";
|
|
4
|
+
const NUL = '\0';
|
|
5
|
+
const NL = '\n';
|
|
6
|
+
const MISSING = 'MISSING';
|
|
7
|
+
const PEER_PREFIX = 'peer.';
|
|
8
|
+
/**
|
|
9
|
+
* Any change to this digest, to `serializeNodeEnv`, to `unitEntry` or to
|
|
10
|
+
* the suffix format changes every peer id in the lockfile, and the ideal
|
|
11
|
+
* builder no longer re-hashes a graph it did not otherwise touch. Bump
|
|
12
|
+
* `LOCKFILE_VERSION` alongside it so old lockfiles hard-fail instead of
|
|
13
|
+
* lingering with stale ids.
|
|
14
|
+
*/
|
|
15
|
+
export const peerEnvDigest = (input, length = 16) => createHash('sha256').update(input).digest('hex').slice(0, length);
|
|
16
|
+
/**
|
|
17
|
+
* `peer.<ordinal>` as minted by retrievePeerContextHash() during a
|
|
18
|
+
* rebuild, or written by lockfiles from before the hash format.
|
|
19
|
+
* Canonical suffixes are 16 or 32 hex chars, optionally followed by
|
|
20
|
+
* `.<role>` or `.x`; an all-digit hash is possible, hence the length
|
|
21
|
+
* bound rather than a plain `\d+`.
|
|
22
|
+
*/
|
|
23
|
+
export const isProvisionalPeerSuffix = (suffix) => /^peer\.\d{1,15}$/.test(suffix);
|
|
24
|
+
/**
|
|
25
|
+
* True when any node still carries a provisional id, so the graph needs
|
|
26
|
+
* a canonicalization pass even if this build did not write to it.
|
|
27
|
+
*/
|
|
28
|
+
export const hasProvisionalPeerIds = (graph) => {
|
|
29
|
+
for (const node of graph.nodes.values()) {
|
|
30
|
+
if (node.peerSetHash &&
|
|
31
|
+
isProvisionalPeerSuffix(node.peerSetHash)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
};
|
|
37
|
+
export const byteCompare = (a, b) => a < b ? -1
|
|
38
|
+
: a > b ? 1
|
|
39
|
+
: 0;
|
|
40
|
+
const canCarryPeerSuffix = (node) => {
|
|
41
|
+
if (node.importer)
|
|
42
|
+
return false;
|
|
43
|
+
const type = splitDepID(node.id)[0];
|
|
44
|
+
return type !== 'file' && type !== 'workspace';
|
|
45
|
+
};
|
|
46
|
+
const hasPeerDependencies = (node, graph) => {
|
|
47
|
+
const mani = node.manifest ?? graph.manifests.get(node.id);
|
|
48
|
+
return !!(mani?.peerDependencies &&
|
|
49
|
+
Object.keys(mani.peerDependencies).length > 0);
|
|
50
|
+
};
|
|
51
|
+
const hasPeerOutEdge = (node) => {
|
|
52
|
+
for (const edge of node.edgesOut.values()) {
|
|
53
|
+
if (edge.type === 'peer' || edge.type === 'peerOptional') {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
};
|
|
59
|
+
export const isPeerScoped = (node, graph) => {
|
|
60
|
+
if (!canCarryPeerSuffix(node))
|
|
61
|
+
return false;
|
|
62
|
+
return (!!node.peerSetHash ||
|
|
63
|
+
hasPeerDependencies(node, graph) ||
|
|
64
|
+
hasPeerOutEdge(node));
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The dependency environment of a node: its out edges as
|
|
68
|
+
* `name NUL type NUL ref`, byte-sorted and NL-joined. Only how a target
|
|
69
|
+
* is referenced varies, so callers pass that policy in; the field set,
|
|
70
|
+
* delimiters, missing-target marker and ordering stay one definition.
|
|
71
|
+
*/
|
|
72
|
+
const serializeEdges = (node, ref) => {
|
|
73
|
+
const entries = [];
|
|
74
|
+
for (const edge of node.edgesOut.values()) {
|
|
75
|
+
entries.push(`${edge.name}${NUL}${edge.type}${NUL}${edge.to ? ref(edge.to) : MISSING}`);
|
|
76
|
+
}
|
|
77
|
+
entries.sort(byteCompare);
|
|
78
|
+
return entries.join(NL);
|
|
79
|
+
};
|
|
80
|
+
export const serializeNodeEnv = (node, opts = {}) => {
|
|
81
|
+
const { resolvedIds, intraSccIndex } = opts;
|
|
82
|
+
return serializeEdges(node, to => {
|
|
83
|
+
const idx = intraSccIndex?.get(to);
|
|
84
|
+
return idx === undefined ?
|
|
85
|
+
(resolvedIds?.get(to) ?? to.id)
|
|
86
|
+
: `#${idx}`;
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
const withPeerSuffix = (node, peerSetHash) => {
|
|
90
|
+
const extra = joinExtra({
|
|
91
|
+
modifier: node.modifier,
|
|
92
|
+
peerSetHash,
|
|
93
|
+
});
|
|
94
|
+
const parsed = splitDepID(node.id);
|
|
95
|
+
switch (parsed[0]) {
|
|
96
|
+
case 'registry':
|
|
97
|
+
return joinDepIDTuple(['registry', parsed[1], parsed[2], extra]);
|
|
98
|
+
case 'git':
|
|
99
|
+
return joinDepIDTuple(['git', parsed[1], parsed[2], extra]);
|
|
100
|
+
case 'remote':
|
|
101
|
+
return joinDepIDTuple(['remote', parsed[1], extra]);
|
|
102
|
+
/* c8 ignore next 2 */
|
|
103
|
+
default:
|
|
104
|
+
return node.id;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* The node's identity minus only its peer component. `baseDepID()` also
|
|
109
|
+
* drops the modifier, and intra-SCC targets serialize positionally, so
|
|
110
|
+
* with a bare base a cycle member's modifier would leave no trace in the
|
|
111
|
+
* cycle's identity and two cycles differing only by it would merge.
|
|
112
|
+
*/
|
|
113
|
+
const peerlessDepID = (node) => withPeerSuffix(node, '');
|
|
114
|
+
/**
|
|
115
|
+
* One member's contribution to an SCC's identity. A singleton and a
|
|
116
|
+
* one-class SCC must produce the same string, or collapsing a cycle
|
|
117
|
+
* renames the survivor on the next pass.
|
|
118
|
+
*/
|
|
119
|
+
const unitEntry = (node, opts) => `${peerlessDepID(node)}${NL}${serializeNodeEnv(node, opts)}`;
|
|
120
|
+
const tarjan = (nodes, adj) => {
|
|
121
|
+
let index = 0;
|
|
122
|
+
const indices = new Map();
|
|
123
|
+
const lowlink = new Map();
|
|
124
|
+
const onStack = new Set();
|
|
125
|
+
const stack = [];
|
|
126
|
+
const sccs = [];
|
|
127
|
+
const strongconnect = (v) => {
|
|
128
|
+
indices.set(v, index);
|
|
129
|
+
lowlink.set(v, index);
|
|
130
|
+
index++;
|
|
131
|
+
stack.push(v);
|
|
132
|
+
onStack.add(v);
|
|
133
|
+
/* c8 ignore next */
|
|
134
|
+
const neighbors = adj.get(v) ?? [];
|
|
135
|
+
for (const w of neighbors) {
|
|
136
|
+
if (!indices.has(w)) {
|
|
137
|
+
strongconnect(w);
|
|
138
|
+
const lv = lowlink.get(v);
|
|
139
|
+
const lw = lowlink.get(w);
|
|
140
|
+
if (lv !== undefined && lw !== undefined && lw < lv) {
|
|
141
|
+
lowlink.set(v, lw);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (onStack.has(w)) {
|
|
145
|
+
const lv = lowlink.get(v);
|
|
146
|
+
const iw = indices.get(w);
|
|
147
|
+
if (lv !== undefined && iw !== undefined && iw < lv) {
|
|
148
|
+
lowlink.set(v, iw);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (lowlink.get(v) === indices.get(v)) {
|
|
153
|
+
const scc = [];
|
|
154
|
+
for (;;) {
|
|
155
|
+
const w = stack.pop();
|
|
156
|
+
/* c8 ignore next */
|
|
157
|
+
if (!w)
|
|
158
|
+
break;
|
|
159
|
+
onStack.delete(w);
|
|
160
|
+
scc.push(w);
|
|
161
|
+
if (w === v)
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
sccs.push(scc);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
for (const v of nodes) {
|
|
168
|
+
if (!indices.has(v))
|
|
169
|
+
strongconnect(v);
|
|
170
|
+
}
|
|
171
|
+
return sccs;
|
|
172
|
+
};
|
|
173
|
+
const hasSelfEdge = (node) => {
|
|
174
|
+
for (const edge of node.edgesOut.values()) {
|
|
175
|
+
if (edge.to === node)
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
};
|
|
180
|
+
const serializeWithColors = (node, sccSet, color, resolvedIds) => serializeEdges(node, to => sccSet.has(to) ?
|
|
181
|
+
/* c8 ignore next */
|
|
182
|
+
(color.get(to) ?? MISSING)
|
|
183
|
+
: (resolvedIds.get(to) ?? to.id));
|
|
184
|
+
const mapGet = (m, key) => {
|
|
185
|
+
const v = m.get(key);
|
|
186
|
+
/* c8 ignore next */
|
|
187
|
+
return v ?? '';
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Weisfeiler–Lehman color refinement to a partition fixpoint, then a
|
|
191
|
+
* canonical member order. Colors are dense ranks so the loop can
|
|
192
|
+
* stabilize; positional `#idx` refs are assigned to color classes.
|
|
193
|
+
*/
|
|
194
|
+
const refineSccOrder = (scc, resolvedIds) => {
|
|
195
|
+
const sccSet = new Set(scc);
|
|
196
|
+
const bases = [...new Set(scc.map(peerlessDepID))].sort(byteCompare);
|
|
197
|
+
const initRank = new Map(bases.map((b, i) => [b, String(i)]));
|
|
198
|
+
let color = new Map(scc.map(n => [n, mapGet(initRank, peerlessDepID(n))]));
|
|
199
|
+
// classic WL: each signature includes the node's own current color so
|
|
200
|
+
// refinement is monotone (the partition only ever gets finer), and the
|
|
201
|
+
// loop stops when the number of classes stops growing rather than when
|
|
202
|
+
// rank labels are literally identical, so it terminates in at most
|
|
203
|
+
// `scc.length` rounds even when labels permute on a stable partition
|
|
204
|
+
let classCount = new Set(color.values()).size;
|
|
205
|
+
for (;;) {
|
|
206
|
+
const rows = scc.map(n => ({
|
|
207
|
+
n,
|
|
208
|
+
key: `${peerlessDepID(n)}${NUL}${mapGet(color, n)}${NUL}${serializeWithColors(n, sccSet, color, resolvedIds)}`,
|
|
209
|
+
}));
|
|
210
|
+
const uniq = [...new Set(rows.map(r => r.key))].sort(byteCompare);
|
|
211
|
+
const rank = new Map(uniq.map((k, i) => [k, String(i)]));
|
|
212
|
+
color = new Map(rows.map(r => [r.n, mapGet(rank, r.key)]));
|
|
213
|
+
if (uniq.length === classCount)
|
|
214
|
+
break;
|
|
215
|
+
classCount = uniq.length;
|
|
216
|
+
}
|
|
217
|
+
const order = [...scc].sort((a, b) => byteCompare(peerlessDepID(a), peerlessDepID(b)) ||
|
|
218
|
+
byteCompare(mapGet(color, a), mapGet(color, b)) ||
|
|
219
|
+
byteCompare(a.id, b.id));
|
|
220
|
+
return { order, colors: color };
|
|
221
|
+
};
|
|
222
|
+
const colorClassKey = (node, colors) => `${peerlessDepID(node)}${NUL}${mapGet(colors, node)}`;
|
|
223
|
+
const mergeNode = (graph, winner, loser) => {
|
|
224
|
+
winner.dev &&= loser.dev;
|
|
225
|
+
winner.optional &&= loser.optional;
|
|
226
|
+
// the loser may be the copy carrying lockfile/registry metadata or the
|
|
227
|
+
// manifest; preserve anything the winner is missing before deletion
|
|
228
|
+
copyPackageMetadata(winner, loser);
|
|
229
|
+
if (!winner.manifest && loser.manifest) {
|
|
230
|
+
winner.manifest = loser.manifest;
|
|
231
|
+
graph.manifests.set(winner.id, loser.manifest);
|
|
232
|
+
}
|
|
233
|
+
for (const edge of loser.edgesIn) {
|
|
234
|
+
edge.to = winner;
|
|
235
|
+
winner.edgesIn.add(edge);
|
|
236
|
+
}
|
|
237
|
+
loser.edgesIn.clear();
|
|
238
|
+
for (const edge of loser.edgesOut.values()) {
|
|
239
|
+
graph.edges.delete(edge);
|
|
240
|
+
edge.to?.edgesIn.delete(edge);
|
|
241
|
+
}
|
|
242
|
+
loser.edgesOut.clear();
|
|
243
|
+
const nbn = graph.nodesByName.get(loser.name);
|
|
244
|
+
if (nbn) {
|
|
245
|
+
nbn.delete(loser);
|
|
246
|
+
if (nbn.size === 0)
|
|
247
|
+
graph.nodesByName.delete(loser.name);
|
|
248
|
+
}
|
|
249
|
+
// the resolution caches are not read again before applyAssignments()
|
|
250
|
+
// clears both of them, so there is nothing to invalidate per loser
|
|
251
|
+
};
|
|
252
|
+
const applyAssignments = (graph, keep, losers) => {
|
|
253
|
+
const newKeys = new Map();
|
|
254
|
+
for (const a of keep)
|
|
255
|
+
newKeys.set(a.newId, a.node);
|
|
256
|
+
for (const a of keep) {
|
|
257
|
+
if (a.node.id !== a.newId && !newKeys.has(a.node.id)) {
|
|
258
|
+
graph.nodes.delete(a.node.id);
|
|
259
|
+
graph.manifests.delete(a.node.id);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const loser of losers.keys()) {
|
|
263
|
+
graph.nodes.delete(loser.id);
|
|
264
|
+
graph.manifests.delete(loser.id);
|
|
265
|
+
}
|
|
266
|
+
const moves = [];
|
|
267
|
+
for (const a of keep) {
|
|
268
|
+
if (a.node.id !== a.newId) {
|
|
269
|
+
const from = a.node.id;
|
|
270
|
+
a.node.setPeerIdentity(a.newId, a.suffix);
|
|
271
|
+
if (a.node.extracted) {
|
|
272
|
+
moves.push({ node: a.node, from, to: a.newId });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
a.node.peerSetHash = a.suffix;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const a of keep) {
|
|
280
|
+
graph.nodes.set(a.newId, a.node);
|
|
281
|
+
if (a.node.manifest) {
|
|
282
|
+
graph.manifests.set(a.newId, a.node.manifest);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const [loser, winner] of losers) {
|
|
286
|
+
if (loser.extracted) {
|
|
287
|
+
moves.push({ node: loser, from: loser.id });
|
|
288
|
+
}
|
|
289
|
+
mergeNode(graph, winner, loser);
|
|
290
|
+
}
|
|
291
|
+
const names = new Set();
|
|
292
|
+
for (const a of keep)
|
|
293
|
+
names.add(a.node.name);
|
|
294
|
+
for (const loser of losers.keys())
|
|
295
|
+
names.add(loser.name);
|
|
296
|
+
for (const name of names) {
|
|
297
|
+
const nbn = graph.nodesByName.get(name);
|
|
298
|
+
if (!nbn)
|
|
299
|
+
continue;
|
|
300
|
+
graph.nodesByName.set(name, new Set([...nbn].sort((a, b) => a.id.localeCompare(b.id, 'en'))));
|
|
301
|
+
}
|
|
302
|
+
graph.resolutions.clear();
|
|
303
|
+
graph.resolutionsReverse.clear();
|
|
304
|
+
if (losers.size > 0)
|
|
305
|
+
graph.gc();
|
|
306
|
+
graph.sortNodes();
|
|
307
|
+
return moves;
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* Replace provisional / legacy `peer.<N>` DepID suffixes with a content
|
|
311
|
+
* hash of each node's resolved out-edge set. Called after `graph.gc()`.
|
|
312
|
+
*/
|
|
313
|
+
export const canonicalizePeerIds = (graph, options = {}) => {
|
|
314
|
+
const digest = options.digest ?? peerEnvDigest;
|
|
315
|
+
const scoped = [...graph.nodes.values()]
|
|
316
|
+
.filter(n => isPeerScoped(n, graph))
|
|
317
|
+
.sort((a, b) => byteCompare(a.id, b.id));
|
|
318
|
+
if (!scoped.length)
|
|
319
|
+
return [];
|
|
320
|
+
const scopedSet = new Set(scoped);
|
|
321
|
+
const adj = new Map();
|
|
322
|
+
for (const node of scoped) {
|
|
323
|
+
const next = [];
|
|
324
|
+
const seen = new Set();
|
|
325
|
+
for (const edge of node.edgesOut.values()) {
|
|
326
|
+
const to = edge.to;
|
|
327
|
+
if (!to || !scopedSet.has(to) || seen.has(to))
|
|
328
|
+
continue;
|
|
329
|
+
seen.add(to);
|
|
330
|
+
next.push(to);
|
|
331
|
+
}
|
|
332
|
+
next.sort((a, b) => byteCompare(a.id, b.id));
|
|
333
|
+
adj.set(node, next);
|
|
334
|
+
}
|
|
335
|
+
const resolvedIds = new Map();
|
|
336
|
+
const keep = [];
|
|
337
|
+
const byNewId = new Map();
|
|
338
|
+
const losers = new Map();
|
|
339
|
+
const takeSuffix = (node, ser, hex, roleIdx, roleCount) => {
|
|
340
|
+
const makeSuffix = (h) => roleCount > 1 ?
|
|
341
|
+
`${PEER_PREFIX}${h}.${roleIdx}`
|
|
342
|
+
: `${PEER_PREFIX}${h}`;
|
|
343
|
+
let suffix = makeSuffix(hex);
|
|
344
|
+
let newId = withPeerSuffix(node, suffix);
|
|
345
|
+
let occupant = byNewId.get(newId);
|
|
346
|
+
if (occupant && occupant.ser !== ser) {
|
|
347
|
+
suffix = makeSuffix(digest(ser, 32));
|
|
348
|
+
newId = withPeerSuffix(node, suffix);
|
|
349
|
+
occupant = byNewId.get(newId);
|
|
350
|
+
/* c8 ignore start */
|
|
351
|
+
if (occupant && occupant.ser !== ser) {
|
|
352
|
+
suffix = `${suffix}.x`;
|
|
353
|
+
newId = withPeerSuffix(node, suffix);
|
|
354
|
+
occupant = undefined;
|
|
355
|
+
}
|
|
356
|
+
/* c8 ignore stop */
|
|
357
|
+
}
|
|
358
|
+
if (occupant?.ser === ser) {
|
|
359
|
+
losers.set(node, occupant.node);
|
|
360
|
+
resolvedIds.set(node, occupant.newId);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const assigned = { node, ser, suffix, newId };
|
|
364
|
+
byNewId.set(newId, assigned);
|
|
365
|
+
keep.push(assigned);
|
|
366
|
+
resolvedIds.set(node, newId);
|
|
367
|
+
};
|
|
368
|
+
for (const scc of tarjan(scoped, adj)) {
|
|
369
|
+
if (scc.length === 1 && scc[0]) {
|
|
370
|
+
const node = scc[0];
|
|
371
|
+
const intraSccIndex = hasSelfEdge(node) ? new Map([[node, 0]]) : undefined;
|
|
372
|
+
const ser = unitEntry(node, { resolvedIds, intraSccIndex });
|
|
373
|
+
takeSuffix(node, ser, digest(ser, 16), 0, 1);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const { order, colors } = refineSccOrder(scc, resolvedIds);
|
|
377
|
+
const seenClass = new Set();
|
|
378
|
+
const reps = [];
|
|
379
|
+
for (const n of order) {
|
|
380
|
+
const key = colorClassKey(n, colors);
|
|
381
|
+
if (seenClass.has(key))
|
|
382
|
+
continue;
|
|
383
|
+
seenClass.add(key);
|
|
384
|
+
reps.push(n);
|
|
385
|
+
}
|
|
386
|
+
const classIdx = new Map();
|
|
387
|
+
for (const [i, n] of reps.entries()) {
|
|
388
|
+
classIdx.set(colorClassKey(n, colors), i);
|
|
389
|
+
}
|
|
390
|
+
const intraSccIndex = new Map();
|
|
391
|
+
for (const n of order) {
|
|
392
|
+
intraSccIndex.set(n,
|
|
393
|
+
/* c8 ignore next */
|
|
394
|
+
classIdx.get(colorClassKey(n, colors)) ?? 0);
|
|
395
|
+
}
|
|
396
|
+
const unit = reps
|
|
397
|
+
.map(n => unitEntry(n, { resolvedIds, intraSccIndex }))
|
|
398
|
+
.join(NL);
|
|
399
|
+
const hex = digest(unit, 16);
|
|
400
|
+
const roleIdx = new Map();
|
|
401
|
+
const rolesPerBase = new Map();
|
|
402
|
+
for (const n of reps) {
|
|
403
|
+
const base = peerlessDepID(n);
|
|
404
|
+
const key = colorClassKey(n, colors);
|
|
405
|
+
if (!roleIdx.has(key)) {
|
|
406
|
+
const next = rolesPerBase.get(base) ?? 0;
|
|
407
|
+
roleIdx.set(key, next);
|
|
408
|
+
rolesPerBase.set(base, next + 1);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
for (const n of order) {
|
|
412
|
+
const key = colorClassKey(n, colors);
|
|
413
|
+
/* c8 ignore next */
|
|
414
|
+
const idx = roleIdx.get(key) ?? 0;
|
|
415
|
+
/* c8 ignore next */
|
|
416
|
+
const count = rolesPerBase.get(peerlessDepID(n)) ?? 1;
|
|
417
|
+
takeSuffix(n, unit, hex, idx, count);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return applyAssignments(graph, keep, losers);
|
|
421
|
+
};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type { AddImportersDependenciesMap, RemoveImportersDependenciesMap } from '../dependencies.ts';
|
|
1
|
+
import type { AddImportersDependenciesMap, Dependency, RemoveImportersDependenciesMap } from '../dependencies.ts';
|
|
2
2
|
import type { BuildIdealAddOptions, BuildIdealFromGraphOptions, BuildIdealRemoveOptions, TransientAddMap, TransientRemoveMap } from './types.ts';
|
|
3
|
+
import type { Edge } from '../edge.ts';
|
|
3
4
|
import type { SpecOptions } from '@vltpkg/spec';
|
|
4
5
|
import type { PackageJson } from '@vltpkg/package-json';
|
|
5
6
|
import type { PathScurry } from 'path-scurry';
|
|
7
|
+
import type { GraphModifier } from '../modifiers.ts';
|
|
6
8
|
export type GetImporterSpecsOptions = BuildIdealAddOptions & BuildIdealFromGraphOptions & BuildIdealRemoveOptions & SpecOptions & {
|
|
7
9
|
scurry: PathScurry;
|
|
8
10
|
packageJson: PackageJson;
|
|
11
|
+
modifiers?: GraphModifier;
|
|
9
12
|
};
|
|
10
13
|
/**
|
|
11
14
|
* Given a {@link Graph} and a list of {@link Dependency}, merges the
|
|
@@ -15,6 +18,7 @@ export type GetImporterSpecsOptions = BuildIdealAddOptions & BuildIdealFromGraph
|
|
|
15
18
|
export declare const getImporterSpecs: (options: GetImporterSpecsOptions) => {
|
|
16
19
|
add: AddImportersDependenciesMap;
|
|
17
20
|
remove: RemoveImportersDependenciesMap;
|
|
21
|
+
staleSpecs: Map<Edge, Dependency>;
|
|
18
22
|
transientAdd: TransientAddMap;
|
|
19
23
|
transientRemove: TransientRemoveMap;
|
|
20
24
|
};
|
|
@@ -40,33 +40,49 @@ export const getImporterSpecs = (options) => {
|
|
|
40
40
|
removeResult.modifiedDependencies = true;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
//
|
|
44
|
-
//
|
|
43
|
+
// which declaration each name is placed with, same rule as
|
|
44
|
+
// getRawDependencies: the last type listing it wins, except that a
|
|
45
|
+
// peer entry for a name already listed elsewhere only constrains
|
|
46
|
+
// the dependency, it is not its own dependency to place
|
|
47
|
+
const declared = new Map();
|
|
45
48
|
for (const depType of longDependencyTypes) {
|
|
46
49
|
const deps = Object.entries(importer.manifest?.[depType] ?? {});
|
|
47
50
|
for (const [depName, depSpec] of deps) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
if (depType === 'peerDependencies' && declared.has(depName)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
declared.set(depName, [depType, depSpec]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// if a dependency is listed in the manifest but not in the graph,
|
|
58
|
+
// add that dependency to the list of dependencies to be added
|
|
59
|
+
for (const [depName, [depType, depSpec]] of declared) {
|
|
60
|
+
const edge = importer.edgesOut.get(depName);
|
|
61
|
+
// skip if the edge exists and already uses the same spec and
|
|
62
|
+
// type. dangling (MISSING) targets still need to be queued so
|
|
63
|
+
// the ideal rebuild runs resetEdges() and places in traversal
|
|
64
|
+
// order. checked before parsing: the text parsed fine when the
|
|
65
|
+
// edge was loaded, and the workspace check below only fires on
|
|
66
|
+
// !edge?.to
|
|
67
|
+
const type = shorten(depType, depName, importer.manifest);
|
|
68
|
+
if (edge?.to &&
|
|
69
|
+
edge.spec.bareSpec === depSpec &&
|
|
70
|
+
edge.type === type) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const spec = Spec.parse(depName, depSpec, options);
|
|
74
|
+
// if a workspace dep references a workspace that no longer exists
|
|
75
|
+
// (folder was removed), mark it for removal instead of trying to
|
|
76
|
+
// resolve it — the workspace can't be satisfied
|
|
77
|
+
if (spec.type === 'workspace' && !edge?.to) {
|
|
78
|
+
const wsExists = [...graph.importers].some(n => n.name === depName);
|
|
79
|
+
if (!wsExists) {
|
|
80
|
+
removeDeps.add(depName);
|
|
81
|
+
removeResult.modifiedDependencies = true;
|
|
51
82
|
continue;
|
|
52
|
-
const spec = Spec.parse(depName, depSpec, options);
|
|
53
|
-
// if a workspace dep references a workspace that no longer exists
|
|
54
|
-
// (folder was removed), mark it for removal instead of trying to
|
|
55
|
-
// resolve it — the workspace can't be satisfied
|
|
56
|
-
if (spec.type === 'workspace' && !edge?.to) {
|
|
57
|
-
const wsExists = [...graph.importers].some(n => n.name === depName);
|
|
58
|
-
if (!wsExists) {
|
|
59
|
-
removeDeps.add(depName);
|
|
60
|
-
removeResult.modifiedDependencies = true;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
83
|
}
|
|
64
|
-
const dependency = asDependency({
|
|
65
|
-
spec,
|
|
66
|
-
type: shorten(depType, depName, importer.manifest),
|
|
67
|
-
});
|
|
68
|
-
addDeps.set(depName, dependency);
|
|
69
84
|
}
|
|
85
|
+
addDeps.set(depName, asDependency({ spec, type }));
|
|
70
86
|
}
|
|
71
87
|
addResult.set(importer.id, addDeps);
|
|
72
88
|
removeResult.set(importer.id, removeDeps);
|
|
@@ -107,8 +123,9 @@ export const getImporterSpecs = (options) => {
|
|
|
107
123
|
const deps = Object.entries(manifest[depType] ?? {});
|
|
108
124
|
for (const [depName, depSpec] of deps) {
|
|
109
125
|
const edge = node.edgesOut.get(depName);
|
|
110
|
-
// skip if the edge exists and already uses the same spec
|
|
111
|
-
|
|
126
|
+
// skip if the edge exists and already uses the same spec.
|
|
127
|
+
// dangling targets still need to be queued for the ideal rebuild.
|
|
128
|
+
if (edge?.to && edge.spec.bareSpec === depSpec)
|
|
112
129
|
continue;
|
|
113
130
|
// add the dependency to the addDeps map
|
|
114
131
|
const dependency = asDependency({
|
|
@@ -171,10 +188,19 @@ export const getImporterSpecs = (options) => {
|
|
|
171
188
|
}
|
|
172
189
|
}
|
|
173
190
|
// removes already satisfied dependencies from the dependencies list
|
|
174
|
-
removeSatisfiedSpecs({
|
|
191
|
+
const staleSpecs = removeSatisfiedSpecs({
|
|
175
192
|
add: addResult,
|
|
176
193
|
graph,
|
|
194
|
+
modifiers: options.modifiers,
|
|
177
195
|
});
|
|
196
|
+
// a name the caller asked for is left to appendNodes and reify's
|
|
197
|
+
// package.json update: healing it here would put the request text in
|
|
198
|
+
// the lockfile without ever touching package.json
|
|
199
|
+
for (const edge of staleSpecs.keys()) {
|
|
200
|
+
if (add.get(edge.from.id)?.has(edge.name)) {
|
|
201
|
+
staleSpecs.delete(edge);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
178
204
|
// set the modifiedDependencies flag if any
|
|
179
205
|
// of the importers have modified dependencies
|
|
180
206
|
for (const addDeps of addResult.values()) {
|
|
@@ -186,6 +212,7 @@ export const getImporterSpecs = (options) => {
|
|
|
186
212
|
return {
|
|
187
213
|
add: addResult,
|
|
188
214
|
remove: removeResult,
|
|
215
|
+
staleSpecs,
|
|
189
216
|
transientAdd,
|
|
190
217
|
transientRemove,
|
|
191
218
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PathScurry } from 'path-scurry';
|
|
2
|
+
import type { RollbackRemove } from '@vltpkg/rollback-remove';
|
|
3
|
+
import type { PeerStoreMove } from './canonicalize-peer-ids.ts';
|
|
4
|
+
export declare const movePeerStoreDirs: (moves: PeerStoreMove[], { scurry, remover, }: {
|
|
5
|
+
scurry: PathScurry;
|
|
6
|
+
remover: RollbackRemove;
|
|
7
|
+
}) => Promise<void>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { rename } from 'node:fs/promises';
|
|
3
|
+
export const movePeerStoreDirs = async (moves, { scurry, remover, }) => {
|
|
4
|
+
if (!moves.length)
|
|
5
|
+
return;
|
|
6
|
+
const store = scurry.resolve('node_modules/.vlt');
|
|
7
|
+
const key = String(Math.random()).substring(2);
|
|
8
|
+
// phase 1: vacate every source before any destination is written.
|
|
9
|
+
// a canonical id can equal another node's previous id, so a
|
|
10
|
+
// destination may still hold a directory that a sibling move is about
|
|
11
|
+
// to relocate; parking sources on unique hidden paths first keeps the
|
|
12
|
+
// moves from racing each other
|
|
13
|
+
const staged = await Promise.all(moves.map(async ({ node, from, to }) => {
|
|
14
|
+
const src = scurry.resolve(store, from);
|
|
15
|
+
if (!to) {
|
|
16
|
+
// nothing links to a merged-away copy any more, so one that
|
|
17
|
+
// cannot be discarded is a stale dir, not a broken install
|
|
18
|
+
try {
|
|
19
|
+
await remover.rm(src);
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const tmp = scurry.resolve(store, `.VLT.MOVE.${key}.${to}`);
|
|
25
|
+
try {
|
|
26
|
+
await rename(src, tmp);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
node.extracted = false;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
return { node, tmp, dest: scurry.resolve(store, to) };
|
|
33
|
+
}));
|
|
34
|
+
// phase 2: settle each parked directory on its canonical id
|
|
35
|
+
await Promise.all(staged.map(async (s) => {
|
|
36
|
+
if (!s)
|
|
37
|
+
return;
|
|
38
|
+
const { node, tmp, dest } = s;
|
|
39
|
+
try {
|
|
40
|
+
// frees the path and registers it for rollback; also the EPERM
|
|
41
|
+
// mitigation Windows needs before renaming a directory
|
|
42
|
+
await remover.rm(dest);
|
|
43
|
+
await rename(tmp, dest);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
node.extracted = false;
|
|
47
|
+
// the node is re-extracted on its canonical id, so the parked
|
|
48
|
+
// copy would only linger as a `.VLT.MOVE` orphan in the store
|
|
49
|
+
try {
|
|
50
|
+
await remover.rm(tmp);
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!existsSync(node.resolvedLocation(scurry))) {
|
|
56
|
+
node.extracted = false;
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
};
|
package/dist/ideal/peers.d.ts
CHANGED
|
@@ -13,10 +13,16 @@ import type { Node } from '../node.ts';
|
|
|
13
13
|
*/
|
|
14
14
|
type PeerEdgeCompatResult = {
|
|
15
15
|
compatible: boolean;
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
/**
|
|
17
|
+
* When incompatible, entry to add to forked context. The target is
|
|
18
|
+
* absent only when the parent declares the peer but has not placed it
|
|
19
|
+
* yet: the fork still has to be minted so the re-placed copy gets a
|
|
20
|
+
* distinct id, and the parent's own edge resolves the peer at the end
|
|
21
|
+
* of the level.
|
|
22
|
+
*/
|
|
23
|
+
forkEntry?: PeerContextEntryInput;
|
|
24
|
+
/** the existing version is fine, only an optional peer link differs */
|
|
25
|
+
keepVersion?: boolean;
|
|
20
26
|
};
|
|
21
27
|
/**
|
|
22
28
|
* Check if an existing node's peer edges would still resolve to the same
|
|
@@ -36,7 +42,7 @@ type PeerEdgeCompatResult = {
|
|
|
36
42
|
* on the same package, and there's a graph node that would satisfy it but
|
|
37
43
|
* differs from what the existing node expects.
|
|
38
44
|
*/
|
|
39
|
-
export declare const checkPeerEdgesCompatible: (existingNode: Node, fromNode: Node, peerContext: PeerContext, graph: Graph) => PeerEdgeCompatResult;
|
|
45
|
+
export declare const checkPeerEdgesCompatible: (existingNode: Node, fromNode: Node, peerContext: PeerContext, graph: Graph, pending?: Map<string, Dependency>) => PeerEdgeCompatResult;
|
|
40
46
|
/**
|
|
41
47
|
* Retrieve a unique hash value for a given peer context set.
|
|
42
48
|
*/
|
|
@@ -80,6 +86,11 @@ export declare const checkEntriesToPeerContext: (peerContext: PeerContext, entri
|
|
|
80
86
|
* peer context set. Extra info such as a target or dependent nodes is
|
|
81
87
|
* optional.
|
|
82
88
|
*
|
|
89
|
+
* When an entry gains a target that satisfies every spec collected so far,
|
|
90
|
+
* dependents of that entry are re-pointed at it only if their current target
|
|
91
|
+
* no longer satisfies one of those specs, so two equally valid resolutions
|
|
92
|
+
* never swap back and forth across rebuilds.
|
|
93
|
+
*
|
|
83
94
|
* Returns true if forking is needed, false otherwise.
|
|
84
95
|
*/
|
|
85
96
|
export declare const addEntriesToPeerContext: (peerContext: PeerContext, entries: PeerContextEntryInput[], fromNode: Node, monorepo?: Monorepo) => boolean;
|