@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
package/dist/ideal/peers.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { intersects } from '@vltpkg/semver';
|
|
4
4
|
import { satisfies } from '@vltpkg/satisfies';
|
|
5
5
|
import { Spec } from '@vltpkg/spec';
|
|
6
|
-
import { getDependencies, shorten } from "../dependencies.js";
|
|
6
|
+
import { getDependencies, shorten, shouldInstallDepType, } from "../dependencies.js";
|
|
7
7
|
import { compareByType, getOrderedDependencies } from "./sorting.js";
|
|
8
8
|
import { longDependencyTypes } from '@vltpkg/types';
|
|
9
9
|
/**
|
|
@@ -29,15 +29,40 @@ const parseSpec = (name, bareSpec, fromNode, graph) => Spec.parse(name, bareSpec
|
|
|
29
29
|
...graph.mainImporter.options,
|
|
30
30
|
registry: fromNode.registry ?? graph.mainImporter.options.registry,
|
|
31
31
|
});
|
|
32
|
+
/**
|
|
33
|
+
* Take private ownership of an entry's `specs` before writing to it: a
|
|
34
|
+
* fork shares the base's map until either side writes. Any other holder
|
|
35
|
+
* keeps `sharedSpecs` and clones on its own first write, so no refcount
|
|
36
|
+
* is needed.
|
|
37
|
+
*/
|
|
38
|
+
const ownSpecs = (entry) => {
|
|
39
|
+
if (entry.sharedSpecs) {
|
|
40
|
+
entry.specs = new Map(entry.specs);
|
|
41
|
+
entry.sharedSpecs = false;
|
|
42
|
+
}
|
|
43
|
+
return entry.specs;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Bump the base's revision: an entry was added, or an entry's target
|
|
47
|
+
* moved. Invalidates every fork key taken from it.
|
|
48
|
+
*/
|
|
49
|
+
const bumpRev = (peerContext) => {
|
|
50
|
+
peerContext.rev = (peerContext.rev ?? 0) + 1;
|
|
51
|
+
};
|
|
32
52
|
/**
|
|
33
53
|
* Generate a unique cache key for a peer context fork operation.
|
|
34
54
|
*
|
|
35
|
-
* Format: `{baseIndex}::{sortedEntrySignatures}`
|
|
55
|
+
* Format: `{baseIndex}::{sortedEntrySignatures}::r{baseRev}`
|
|
36
56
|
* - `baseIndex`: The parent context's index (0 for initial context)
|
|
37
57
|
* - Entry signature: `{name}|{type}|{targetId}|{spec}` sorted alphabetically
|
|
58
|
+
* - `baseRev`: the base's revision, bumped on every mapping change
|
|
38
59
|
*
|
|
39
60
|
* This enables caching identical fork operations to avoid creating duplicate
|
|
40
61
|
* peer contexts when the same entries would be added to the same base context.
|
|
62
|
+
* Forks snapshot the base's targets, so the base index alone is not enough:
|
|
63
|
+
* once a base target moves, an identical request must get a fresh fork
|
|
64
|
+
* instead of the snapshot taken before the move. The revision stands in for
|
|
65
|
+
* serialising the whole inherited mapping, which was O(context) per fork.
|
|
41
66
|
*/
|
|
42
67
|
const getForkKey = (peerContext, entries) => {
|
|
43
68
|
const base = peerContext.index ?? 0;
|
|
@@ -45,7 +70,7 @@ const getForkKey = (peerContext, entries) => {
|
|
|
45
70
|
.map(e => `${e.spec.final.name}|${e.type}|${e.target?.id ?? '∅'}|${e.spec}`)
|
|
46
71
|
.sort()
|
|
47
72
|
.join(';');
|
|
48
|
-
return `${base}::${sig}`;
|
|
73
|
+
return `${base}::${sig}::r${peerContext.rev ?? 0}`;
|
|
49
74
|
};
|
|
50
75
|
/**
|
|
51
76
|
* Check if parent declares a dep for peerName that the context target doesn't satisfy.
|
|
@@ -65,8 +90,12 @@ const shouldIgnoreContextMismatch = (peerName, contextTarget, fromNode, graph) =
|
|
|
65
90
|
/* c8 ignore next - edge case: fromNode always has manifest in practice */
|
|
66
91
|
if (!parentManifest)
|
|
67
92
|
return false;
|
|
68
|
-
// Search all dependency types for a declaration of peerName
|
|
93
|
+
// Search all dependency types for a declaration of peerName. Only types
|
|
94
|
+
// the parent actually installs count: a registry parent's devDependencies
|
|
95
|
+
// are never placed, so they do not mean it resolves its own copy.
|
|
69
96
|
for (const depType of longDependencyTypes) {
|
|
97
|
+
if (!shouldInstallDepType(fromNode, depType))
|
|
98
|
+
continue;
|
|
70
99
|
const declared = parentManifest[depType]?.[peerName];
|
|
71
100
|
if (!declared)
|
|
72
101
|
continue;
|
|
@@ -100,6 +129,67 @@ const buildIncompatibleResult = (target, peerSpec, type, fromNode, graph) => {
|
|
|
100
129
|
}
|
|
101
130
|
return undefined;
|
|
102
131
|
};
|
|
132
|
+
/**
|
|
133
|
+
* Which node does `fromNode` (or its placement context) offer for
|
|
134
|
+
* `peerName`? Checked in the order `resolvePeerDeps` will use: the
|
|
135
|
+
* parent's own edge, then a dependency the parent declares but has not
|
|
136
|
+
* placed yet, then the context entry. Only targets satisfying `peerSpec`
|
|
137
|
+
* count.
|
|
138
|
+
*/
|
|
139
|
+
const findProvidedPeer = (peerName, peerSpec, fromNode, peerContext, parseOpts, satisfiesNodeSpec, pending) => {
|
|
140
|
+
const sibling = fromNode.edgesOut.get(peerName);
|
|
141
|
+
if (sibling?.to && satisfiesNodeSpec(sibling.to, peerSpec)) {
|
|
142
|
+
return { target: sibling.to, spec: peerSpec, type: sibling.type };
|
|
143
|
+
}
|
|
144
|
+
// what the parent is about to place at this level: it already reflects
|
|
145
|
+
// explicit adds and removes, transient deps and modifier swaps, none of
|
|
146
|
+
// which reach the manifest before reify. when given it is
|
|
147
|
+
// authoritative; the manifest is only consulted without it
|
|
148
|
+
let declared;
|
|
149
|
+
const pendingDep = pending?.get(peerName);
|
|
150
|
+
if (pendingDep) {
|
|
151
|
+
declared = { spec: pendingDep.spec, type: pendingDep.type };
|
|
152
|
+
}
|
|
153
|
+
else if (!pending && fromNode.manifest) {
|
|
154
|
+
const manifest = fromNode.manifest;
|
|
155
|
+
for (const depType of longDependencyTypes) {
|
|
156
|
+
if (!shouldInstallDepType(fromNode, depType))
|
|
157
|
+
continue;
|
|
158
|
+
const bare = manifest[depType]?.[peerName];
|
|
159
|
+
if (!bare)
|
|
160
|
+
continue;
|
|
161
|
+
const type = shorten(depType, peerName, manifest);
|
|
162
|
+
// the parent's own optional peer may never resolve; a required one
|
|
163
|
+
// is placed or resolved, so it does become the parent's edge
|
|
164
|
+
if (type === 'peerOptional')
|
|
165
|
+
continue;
|
|
166
|
+
declared = { spec: Spec.parse(peerName, bare, parseOpts), type };
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (declared) {
|
|
171
|
+
// what the parent installs is decided by its own placement (existing
|
|
172
|
+
// edge, then lock, then findResolution), so no node is predicted
|
|
173
|
+
// here: a target-less fork entry lets the parent's live edge resolve
|
|
174
|
+
// the peer at the end of the level. disjoint ranges cannot both be
|
|
175
|
+
// satisfied, so there is nothing to fork for
|
|
176
|
+
const pf = declared.spec.final;
|
|
177
|
+
const sf = peerSpec.final;
|
|
178
|
+
if (pf.type === 'registry' &&
|
|
179
|
+
sf.type === 'registry' &&
|
|
180
|
+
pf.range &&
|
|
181
|
+
sf.range &&
|
|
182
|
+
!intersects(pf.range, sf.range)) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
return { spec: declared.spec, type: declared.type };
|
|
186
|
+
}
|
|
187
|
+
const entry = peerContext.get(peerName);
|
|
188
|
+
if (entry?.target && satisfiesNodeSpec(entry.target, peerSpec)) {
|
|
189
|
+
return { target: entry.target, spec: peerSpec, type: entry.type };
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
};
|
|
103
193
|
/**
|
|
104
194
|
* Check if an existing node's peer edges would still resolve to the same
|
|
105
195
|
* targets from a new parent's context. Returns incompatible info if any
|
|
@@ -118,7 +208,7 @@ const buildIncompatibleResult = (target, peerSpec, type, fromNode, graph) => {
|
|
|
118
208
|
* on the same package, and there's a graph node that would satisfy it but
|
|
119
209
|
* differs from what the existing node expects.
|
|
120
210
|
*/
|
|
121
|
-
export const checkPeerEdgesCompatible = (existingNode, fromNode, peerContext, graph) => {
|
|
211
|
+
export const checkPeerEdgesCompatible = (existingNode, fromNode, peerContext, graph, pending) => {
|
|
122
212
|
const peerDeps = existingNode.manifest?.peerDependencies;
|
|
123
213
|
// No peer deps = always compatible
|
|
124
214
|
if (!peerDeps || Object.keys(peerDeps).length === 0) {
|
|
@@ -163,9 +253,27 @@ export const checkPeerEdgesCompatible = (existingNode, fromNode, peerContext, gr
|
|
|
163
253
|
if (existingEdge === undefined) {
|
|
164
254
|
return { compatible: false };
|
|
165
255
|
}
|
|
166
|
-
// Dangling peer edge (edge exists but unresolved)
|
|
167
|
-
|
|
256
|
+
// Dangling peer edge (edge exists but unresolved). An optional one
|
|
257
|
+
// means the copy was placed where nothing provided the peer: a parent
|
|
258
|
+
// that does provide it cannot share that copy, it has to fork so the
|
|
259
|
+
// peer gets linked. A dangling required peer is skipped as before.
|
|
260
|
+
if (!existingEdge.to) {
|
|
261
|
+
if (existingEdge.type === 'peerOptional') {
|
|
262
|
+
const provided = findProvidedPeer(peerName, Spec.parse(peerName, peerBareSpec, parseOpts), fromNode, peerContext, parseOpts, satisfiesNodeSpec, pending);
|
|
263
|
+
if (provided) {
|
|
264
|
+
return {
|
|
265
|
+
compatible: false,
|
|
266
|
+
forkEntry: {
|
|
267
|
+
spec: provided.spec,
|
|
268
|
+
target: provided.target,
|
|
269
|
+
type: provided.type,
|
|
270
|
+
},
|
|
271
|
+
keepVersion: true,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
168
275
|
continue;
|
|
276
|
+
}
|
|
169
277
|
const peerSpec = Spec.parse(peerName, peerBareSpec, parseOpts);
|
|
170
278
|
// CHECK 1: Does peer context have a different target for this peer?
|
|
171
279
|
const contextEntry = peerContext.get(peerName);
|
|
@@ -208,6 +316,8 @@ export const checkPeerEdgesCompatible = (existingNode, fromNode, peerContext, gr
|
|
|
208
316
|
let declaredType;
|
|
209
317
|
if (manifest) {
|
|
210
318
|
for (const depType of longDependencyTypes) {
|
|
319
|
+
if (!shouldInstallDepType(fromNode, depType))
|
|
320
|
+
continue;
|
|
211
321
|
const deps = manifest[depType];
|
|
212
322
|
if (deps &&
|
|
213
323
|
typeof deps === 'object' &&
|
|
@@ -231,6 +341,8 @@ export const checkPeerEdgesCompatible = (existingNode, fromNode, peerContext, gr
|
|
|
231
341
|
const candidates = graph.nodesByName.get(peerName);
|
|
232
342
|
if (candidates) {
|
|
233
343
|
for (const candidateNode of candidates) {
|
|
344
|
+
if (candidateNode.detached)
|
|
345
|
+
continue;
|
|
234
346
|
if (candidateNode.id !== existingEdge.to.id &&
|
|
235
347
|
satisfiesNodeSpec(candidateNode, parentSpec) &&
|
|
236
348
|
satisfiesNodeSpec(candidateNode, peerSpec)) {
|
|
@@ -254,10 +366,9 @@ export const checkPeerEdgesCompatible = (existingNode, fromNode, peerContext, gr
|
|
|
254
366
|
* Retrieve a unique hash value for a given peer context set.
|
|
255
367
|
*/
|
|
256
368
|
export const retrievePeerContextHash = (peerContext) => {
|
|
257
|
-
|
|
258
|
-
if (!peerContext?.index)
|
|
369
|
+
if (!peerContext)
|
|
259
370
|
return undefined;
|
|
260
|
-
return `peer.${peerContext.index}`;
|
|
371
|
+
return `peer.${peerContext.index ?? 0}`;
|
|
261
372
|
};
|
|
262
373
|
/**
|
|
263
374
|
* Generate a dedupe key for a spec stored in a peer context entry's
|
|
@@ -338,12 +449,19 @@ export const checkEntriesToPeerContext = (peerContext, entries) => {
|
|
|
338
449
|
* peer context set. Extra info such as a target or dependent nodes is
|
|
339
450
|
* optional.
|
|
340
451
|
*
|
|
452
|
+
* When an entry gains a target that satisfies every spec collected so far,
|
|
453
|
+
* dependents of that entry are re-pointed at it only if their current target
|
|
454
|
+
* no longer satisfies one of those specs, so two equally valid resolutions
|
|
455
|
+
* never swap back and forth across rebuilds.
|
|
456
|
+
*
|
|
341
457
|
* Returns true if forking is needed, false otherwise.
|
|
342
458
|
*/
|
|
343
459
|
export const addEntriesToPeerContext = (peerContext, entries, fromNode, monorepo) => {
|
|
344
460
|
// pre check for conflicts before processing
|
|
345
461
|
if (checkEntriesToPeerContext(peerContext, entries))
|
|
346
462
|
return true;
|
|
463
|
+
/** Does `node` satisfy every spec collected in `entry`? */
|
|
464
|
+
const satisfiesEntrySpecs = (node, entry, from) => [...entry.specs.values()].every(s => satisfies(node.id, s, from.location, from.projectRoot, monorepo));
|
|
347
465
|
for (const { dependent, spec, target, type } of entries) {
|
|
348
466
|
const name = target?.name ?? spec.final.name;
|
|
349
467
|
let entry = peerContext.get(name);
|
|
@@ -357,6 +475,7 @@ export const addEntriesToPeerContext = (peerContext, entries, fromNode, monorepo
|
|
|
357
475
|
contextDependents: new Set(),
|
|
358
476
|
};
|
|
359
477
|
peerContext.set(name, entry);
|
|
478
|
+
bumpRev(peerContext);
|
|
360
479
|
if (dependent)
|
|
361
480
|
entry.contextDependents.add(dependent);
|
|
362
481
|
continue;
|
|
@@ -364,27 +483,41 @@ export const addEntriesToPeerContext = (peerContext, entries, fromNode, monorepo
|
|
|
364
483
|
// check for sibling dep conflicts
|
|
365
484
|
if (incompatibleSpecs(spec.final, entry))
|
|
366
485
|
return true;
|
|
486
|
+
// collect the incoming spec before anything reads the entry: it
|
|
487
|
+
// constrains both the new target and any dependent kept on an older one
|
|
488
|
+
const specKey = peerSpecKey(spec);
|
|
489
|
+
if (!entry.specs.has(specKey))
|
|
490
|
+
ownSpecs(entry).set(specKey, spec);
|
|
367
491
|
// update target if compatible with all specs
|
|
368
|
-
if (target &&
|
|
369
|
-
|
|
492
|
+
if (target && satisfiesEntrySpecs(target, entry, fromNode)) {
|
|
493
|
+
// two peer copies of the same version are the same resolution here:
|
|
494
|
+
// moving a dependent between them would only change its peer set
|
|
370
495
|
if (target.id !== entry.target?.id &&
|
|
371
496
|
target.version !== entry.target?.version) {
|
|
372
|
-
//
|
|
497
|
+
// re-point only dependents whose target this context has outgrown;
|
|
498
|
+
// one that still satisfies every spec is as valid as the new target,
|
|
499
|
+
// so a rebuild never swaps between two valid resolutions
|
|
373
500
|
for (const dep of entry.contextDependents) {
|
|
374
501
|
const edge = dep.edgesOut.get(name);
|
|
375
|
-
if (edge?.to
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
502
|
+
if (!edge?.to || edge.to === target)
|
|
503
|
+
continue;
|
|
504
|
+
if (satisfiesEntrySpecs(edge.to, entry, dep))
|
|
505
|
+
continue;
|
|
506
|
+
dep.graph.mutations++;
|
|
507
|
+
edge.to.edgesIn.delete(edge);
|
|
508
|
+
edge.to = target;
|
|
509
|
+
target.edgesIn.add(edge);
|
|
380
510
|
}
|
|
381
511
|
entry.target = target;
|
|
512
|
+
bumpRev(peerContext);
|
|
513
|
+
}
|
|
514
|
+
else if (!entry.target) {
|
|
515
|
+
// only reachable when the branch above was skipped because
|
|
516
|
+
// target.version is undefined
|
|
517
|
+
entry.target = target;
|
|
518
|
+
bumpRev(peerContext);
|
|
382
519
|
}
|
|
383
|
-
entry.target ??= target;
|
|
384
520
|
}
|
|
385
|
-
const specKey = peerSpecKey(spec);
|
|
386
|
-
if (!entry.specs.has(specKey))
|
|
387
|
-
entry.specs.set(specKey, spec);
|
|
388
521
|
if (dependent)
|
|
389
522
|
entry.contextDependents.add(dependent);
|
|
390
523
|
}
|
|
@@ -405,16 +538,24 @@ export const forkPeerContext = (graph, peerContext, entries) => {
|
|
|
405
538
|
// register it in the graph
|
|
406
539
|
graph.peerContexts[nextPeerContext.index] = nextPeerContext;
|
|
407
540
|
graph.peerContextForkCache.set(forkKey, nextPeerContext);
|
|
408
|
-
// copy existing entries marking them as inactive
|
|
409
|
-
//
|
|
410
|
-
//
|
|
541
|
+
// copy existing entries marking them as inactive. specs are shared with
|
|
542
|
+
// the base until either side writes (both are flagged, so a base write
|
|
543
|
+
// after the fork cannot leak into it), but dependents are NOT inherited:
|
|
544
|
+
// they were placed in the parent context, so a target update in this
|
|
545
|
+
// fork must never re-point their edges.
|
|
546
|
+
// the target IS inherited: what a context resolves a name to must not
|
|
547
|
+
// depend on which fork a subtree happened to land in. it stays a
|
|
548
|
+
// snapshot (`getForkKey` invalidates it when the base moves) and is
|
|
549
|
+
// still guarded by `nodeSatisfiesSpec` at resolution time.
|
|
411
550
|
for (const [name, entry] of peerContext.entries()) {
|
|
551
|
+
entry.sharedSpecs = true;
|
|
412
552
|
nextPeerContext.set(name, {
|
|
413
553
|
active: false,
|
|
414
|
-
|
|
415
|
-
|
|
554
|
+
sharedSpecs: true,
|
|
555
|
+
specs: entry.specs,
|
|
556
|
+
target: entry.target,
|
|
416
557
|
type: entry.type,
|
|
417
|
-
contextDependents: new Set(
|
|
558
|
+
contextDependents: new Set(),
|
|
418
559
|
});
|
|
419
560
|
}
|
|
420
561
|
// add the new entries to this peer context set, marking them as active
|
|
@@ -422,9 +563,20 @@ export const forkPeerContext = (graph, peerContext, entries) => {
|
|
|
422
563
|
for (const entry of entries) {
|
|
423
564
|
const { dependent, spec, target, type } = entry;
|
|
424
565
|
const name = target?.name /* c8 ignore next */ ?? spec.final.name;
|
|
566
|
+
// a fork entry pointing the name at the node the base already
|
|
567
|
+
// resolved it to is the same resolution, so keep the constraints the
|
|
568
|
+
// base collected. dropping them leaves only this entry's (often wide)
|
|
569
|
+
// peer range, and CHECK 1's leniency then reads that as "any copy
|
|
570
|
+
// whose peer satisfies the range is fine", sharing a copy that is
|
|
571
|
+
// linked to a different node
|
|
572
|
+
const inherited = peerContext.get(name);
|
|
573
|
+
const specs = inherited?.target && inherited.target === target ?
|
|
574
|
+
new Map(inherited.specs)
|
|
575
|
+
: new Map();
|
|
576
|
+
specs.set(peerSpecKey(spec), spec);
|
|
425
577
|
const newEntry = {
|
|
426
578
|
active: true,
|
|
427
|
-
specs
|
|
579
|
+
specs,
|
|
428
580
|
target,
|
|
429
581
|
type,
|
|
430
582
|
contextDependents: dependent ? new Set([dependent]) : new Set(),
|
|
@@ -598,12 +750,16 @@ export const endPeerPlacement = (peerContext, nextDeps, nextPeerDeps, graph, spe
|
|
|
598
750
|
// These take priority because workspace's direct deps should win over
|
|
599
751
|
// versions from other workspaces that may be in the peer context
|
|
600
752
|
const siblingEntry = queuedEntries.find(e => (e.target?.name ?? e.spec.final.name) === name);
|
|
601
|
-
|
|
753
|
+
// prefer the parent's live edge target over the queued snapshot:
|
|
754
|
+
// the snapshot is captured at placement time and may be stale if a
|
|
755
|
+
// peer context target update has since re-pointed the parent's edge
|
|
756
|
+
const siblingTarget = fromNode.edgesOut.get(name)?.to ?? siblingEntry?.target;
|
|
602
757
|
if (siblingTarget &&
|
|
603
758
|
nodeSatisfiesSpec(siblingTarget, spec, fromNode, graph)) {
|
|
604
759
|
// Override existing edge if pointing elsewhere (sibling must win)
|
|
605
760
|
const existingEdge = node.edgesOut.get(name);
|
|
606
761
|
if (existingEdge?.to && existingEdge.to !== siblingTarget) {
|
|
762
|
+
graph.mutations++;
|
|
607
763
|
existingEdge.to.edgesIn.delete(existingEdge);
|
|
608
764
|
existingEdge.to = siblingTarget;
|
|
609
765
|
siblingTarget.edgesIn.add(existingEdge);
|
|
@@ -628,7 +784,7 @@ export const endPeerPlacement = (peerContext, nextDeps, nextPeerDeps, graph, spe
|
|
|
628
784
|
graph.addEdge(type, spec, node, entry.target);
|
|
629
785
|
const finalKey = peerSpecKey(spec.final);
|
|
630
786
|
if (!entry.specs.has(finalKey)) {
|
|
631
|
-
entry.
|
|
787
|
+
ownSpecs(entry).set(finalKey, spec.final);
|
|
632
788
|
}
|
|
633
789
|
continue;
|
|
634
790
|
}
|
|
@@ -2,7 +2,7 @@ import type { PathScurry } from 'path-scurry';
|
|
|
2
2
|
import type { PackageInfoClient } from '@vltpkg/package-info';
|
|
3
3
|
import type { SpecOptions } from '@vltpkg/spec';
|
|
4
4
|
import type { RollbackRemove } from '@vltpkg/rollback-remove';
|
|
5
|
-
import type { BuildIdealAddOptions, BuildIdealFromGraphOptions, BuildIdealRemoveOptions, TransientAddMap, TransientRemoveMap } from './types.ts';
|
|
5
|
+
import type { BuildIdealAddOptions, BuildIdealFromGraphOptions, BuildIdealRemoveOptions, ExplicitAddMap, TransientAddMap, TransientRemoveMap } from './types.ts';
|
|
6
6
|
import type { GraphModifier } from '../modifiers.ts';
|
|
7
7
|
import type { Graph } from '../graph.ts';
|
|
8
8
|
export type RefreshIdealGraphOptions = BuildIdealAddOptions & BuildIdealRemoveOptions & BuildIdealFromGraphOptions & SpecOptions & {
|
|
@@ -26,6 +26,11 @@ export type RefreshIdealGraphOptions = BuildIdealAddOptions & BuildIdealRemoveOp
|
|
|
26
26
|
* A {@link RollbackRemove} instance to handle extraction rollbacks
|
|
27
27
|
*/
|
|
28
28
|
remover: RollbackRemove;
|
|
29
|
+
/**
|
|
30
|
+
* Dependency names that came from an explicit user request, keyed by
|
|
31
|
+
* the id of the node receiving them.
|
|
32
|
+
*/
|
|
33
|
+
explicit?: ExplicitAddMap;
|
|
29
34
|
/**
|
|
30
35
|
* Dependencies to be added to non-importer nodes when they are placed.
|
|
31
36
|
* Used for nested folder dependencies that are not importers.
|
|
@@ -36,8 +41,12 @@ export type RefreshIdealGraphOptions = BuildIdealAddOptions & BuildIdealRemoveOp
|
|
|
36
41
|
* Used for nested folder dependencies that are not importers.
|
|
37
42
|
*/
|
|
38
43
|
transientRemove?: TransientRemoveMap;
|
|
44
|
+
/**
|
|
45
|
+
* Only the lockfile is being written, so nothing is extracted.
|
|
46
|
+
*/
|
|
47
|
+
lockfileOnly?: boolean;
|
|
39
48
|
};
|
|
40
49
|
/**
|
|
41
50
|
* Rebuilds the provided ideal graph.
|
|
42
51
|
*/
|
|
43
|
-
export declare const refreshIdealGraph: ({ add, graph, modifiers, packageInfo, scurry, actual, remove, remover, transientAdd, transientRemove, ...specOptions }: RefreshIdealGraphOptions) => Promise<void>;
|
|
52
|
+
export declare const refreshIdealGraph: ({ add, graph, modifiers, packageInfo, scurry, actual, explicit, remove, remover, transientAdd, transientRemove, lockfileOnly, ...specOptions }: RefreshIdealGraphOptions) => Promise<void>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appendNodes } from "./append-nodes.js";
|
|
2
2
|
import { compareByHasPeerDeps, getNodeOrderedDependencies, } from "./sorting.js";
|
|
3
|
+
import { baseDepID } from '@vltpkg/dep-id';
|
|
3
4
|
/**
|
|
4
5
|
* Returns an ordered list of importer nodes.
|
|
5
6
|
*/
|
|
@@ -18,12 +19,25 @@ const getOrderedImporters = (graph) => {
|
|
|
18
19
|
/**
|
|
19
20
|
* Rebuilds the provided ideal graph.
|
|
20
21
|
*/
|
|
21
|
-
export const refreshIdealGraph = async ({ add, graph, modifiers, packageInfo, scurry, actual, remove, remover, transientAdd, transientRemove, ...specOptions }) => {
|
|
22
|
+
export const refreshIdealGraph = async ({ add, graph, modifiers, packageInfo, scurry, actual, explicit, remove, remover, transientAdd, transientRemove, lockfileOnly, ...specOptions }) => {
|
|
22
23
|
const seen = new Set();
|
|
23
|
-
|
|
24
|
+
// early extraction only warms the store for the reify that follows;
|
|
25
|
+
// a lockfile-only run has none, and the next install re-extracts
|
|
26
|
+
// everything anyway since it never wrote a hidden lockfile
|
|
27
|
+
const extractPromises = lockfileOnly ? undefined : [];
|
|
24
28
|
const seenExtracted = new Set();
|
|
25
29
|
// gets an ordered list of importers to ensure deterministic processing
|
|
26
30
|
const orderedImporters = getOrderedImporters(graph);
|
|
31
|
+
// importer edge spec text as the starting graph had it. a rebuild that
|
|
32
|
+
// only changes the text (`vlt i foo@1.2.3` over a `foo@^1.2.3` edge)
|
|
33
|
+
// produces no node diff, so reify has to be told the lockfile on disk
|
|
34
|
+
// no longer describes the graph
|
|
35
|
+
const specTexts = new Map();
|
|
36
|
+
for (const importer of graph.importers) {
|
|
37
|
+
for (const [name, edge] of importer.edgesOut) {
|
|
38
|
+
specTexts.set(`${importer.id}\0${name}`, edge.spec.bareSpec);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
27
41
|
const depsPerImporter = new Map();
|
|
28
42
|
for (const importer of orderedImporters) {
|
|
29
43
|
// gets an ordered list of dependencies for this importer
|
|
@@ -35,7 +49,41 @@ export const refreshIdealGraph = async ({ add, graph, modifiers, packageInfo, sc
|
|
|
35
49
|
if (add.modifiedDependencies ||
|
|
36
50
|
remove.modifiedDependencies ||
|
|
37
51
|
graph.optionsChanged) {
|
|
52
|
+
const locked = new Map();
|
|
53
|
+
const ambiguous = new Set();
|
|
54
|
+
const record = (key, id) => {
|
|
55
|
+
if (ambiguous.has(key))
|
|
56
|
+
return;
|
|
57
|
+
const prev = locked.get(key);
|
|
58
|
+
if (prev === undefined) {
|
|
59
|
+
locked.set(key, id);
|
|
60
|
+
}
|
|
61
|
+
else if (prev !== id) {
|
|
62
|
+
// two peer copies of the same base resolve the name differently;
|
|
63
|
+
// a base-keyed lookup would be a guess, so drop the key
|
|
64
|
+
locked.delete(key);
|
|
65
|
+
ambiguous.add(key);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
for (const edge of graph.edges) {
|
|
69
|
+
if (!edge.to)
|
|
70
|
+
continue;
|
|
71
|
+
record(`${edge.from.id}\0${edge.spec.name}`, edge.to.id);
|
|
72
|
+
// peer-fork rebuilds mint provisional `peer.N` parent ids that miss
|
|
73
|
+
// the canonical `peer.<hash>` keys captured here, so also key by
|
|
74
|
+
// the base id when that is unambiguous across peer copies
|
|
75
|
+
const base = baseDepID(edge.from.id);
|
|
76
|
+
if (base !== edge.from.id) {
|
|
77
|
+
record(`${base}\0${edge.spec.name}`, edge.to.id);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
graph.lockedResolutions = locked;
|
|
38
81
|
graph.resetEdges();
|
|
82
|
+
// the options block on disk no longer matches the config this graph
|
|
83
|
+
// was built with, so the lockfile has to be written even when the
|
|
84
|
+
// rebuild lands on the same nodes and edges
|
|
85
|
+
if (graph.optionsChanged)
|
|
86
|
+
graph.lockfileStale = true;
|
|
39
87
|
}
|
|
40
88
|
// iterates on the list of dependencies per importer updating
|
|
41
89
|
// the graph using metadata fetch from the registry manifest files
|
|
@@ -51,14 +99,24 @@ export const refreshIdealGraph = async ({ add, graph, modifiers, packageInfo, sc
|
|
|
51
99
|
const modifierRefs = modifiers?.tryDependencies(importer, deps);
|
|
52
100
|
// Add new nodes for packages defined in the dependencies list fetching
|
|
53
101
|
// metadata from the registry manifests and updating the graph
|
|
54
|
-
await appendNodes(packageInfo, graph, importer, deps, scurry, specOptions, seen, addedDeps, modifiers, modifierRefs, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove);
|
|
102
|
+
await appendNodes(packageInfo, graph, importer, deps, scurry, specOptions, seen, addedDeps, modifiers, modifierRefs, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove, explicit);
|
|
103
|
+
}
|
|
104
|
+
// locked resolutions only apply to the rebuild that captured them
|
|
105
|
+
graph.lockedResolutions = undefined;
|
|
106
|
+
for (const importer of graph.importers) {
|
|
107
|
+
for (const [name, edge] of importer.edgesOut) {
|
|
108
|
+
if (specTexts.get(`${importer.id}\0${name}`) !==
|
|
109
|
+
edge.spec.bareSpec) {
|
|
110
|
+
graph.lockfileStale = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
55
113
|
}
|
|
56
114
|
// set default node locations, if possible
|
|
57
115
|
for (const node of graph.nodes.values()) {
|
|
58
116
|
node.setDefaultLocation();
|
|
59
117
|
}
|
|
60
118
|
// Wait for all extraction promises to complete
|
|
61
|
-
if (extractPromises
|
|
119
|
+
if (extractPromises?.length) {
|
|
62
120
|
await Promise.all(extractPromises);
|
|
63
121
|
}
|
|
64
122
|
};
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import type { Edge } from '../edge.ts';
|
|
2
|
+
import type { Dependency } from '../dependencies.ts';
|
|
1
3
|
import type { BuildIdealAddOptions, BuildIdealFromGraphOptions } from './types.ts';
|
|
2
|
-
|
|
4
|
+
import type { GraphModifier } from '../modifiers.ts';
|
|
5
|
+
export type RemoveSatisfiedSpecsOptions = BuildIdealAddOptions & BuildIdealFromGraphOptions & {
|
|
6
|
+
modifiers?: GraphModifier;
|
|
7
|
+
};
|
|
3
8
|
/**
|
|
4
9
|
* Traverse the objects defined in `add` and removes any references to specs
|
|
5
10
|
* that are already satisfied by the contents of the actual `graph`.
|
|
11
|
+
*
|
|
12
|
+
* Returns the satisfied edges whose spec text or dependency type differs
|
|
13
|
+
* from the one they were pruned against, e.g. a lockfile edge reading
|
|
14
|
+
* `peer latest` where `package.json` reads `devDependencies: ^1.2.3`.
|
|
6
15
|
*/
|
|
7
|
-
export declare const removeSatisfiedSpecs: ({ add, graph, }: RemoveSatisfiedSpecsOptions) =>
|
|
16
|
+
export declare const removeSatisfiedSpecs: ({ add, graph, modifiers, }: RemoveSatisfiedSpecsOptions) => Map<Edge, Dependency>;
|
|
@@ -3,8 +3,13 @@ import { satisfies } from '@vltpkg/satisfies';
|
|
|
3
3
|
/**
|
|
4
4
|
* Traverse the objects defined in `add` and removes any references to specs
|
|
5
5
|
* that are already satisfied by the contents of the actual `graph`.
|
|
6
|
+
*
|
|
7
|
+
* Returns the satisfied edges whose spec text or dependency type differs
|
|
8
|
+
* from the one they were pruned against, e.g. a lockfile edge reading
|
|
9
|
+
* `peer latest` where `package.json` reads `devDependencies: ^1.2.3`.
|
|
6
10
|
*/
|
|
7
|
-
export const removeSatisfiedSpecs = ({ add, graph, }) => {
|
|
11
|
+
export const removeSatisfiedSpecs = ({ add, graph, modifiers, }) => {
|
|
12
|
+
const staleSpecs = new Map();
|
|
8
13
|
for (const [depID, dependencies] of add.entries()) {
|
|
9
14
|
const importer = graph.nodes.get(depID);
|
|
10
15
|
if (!importer) {
|
|
@@ -29,6 +34,17 @@ export const removeSatisfiedSpecs = ({ add, graph, }) => {
|
|
|
29
34
|
// If the current graph edge is already valid, then we remove that
|
|
30
35
|
// dependency item from the list of items to be added to the graph
|
|
31
36
|
if (satisfies(edge.to?.id, dependency.spec, edge.from.location, graph.projectRoot, graph.monorepo)) {
|
|
37
|
+
// a governed edge carries the modifier value by construction:
|
|
38
|
+
// healing it to the manifest text would only be undone by the
|
|
39
|
+
// rebuild that re-applies the override. `implicit` is not a real
|
|
40
|
+
// edge type, it means "keep what the edge already is", so it
|
|
41
|
+
// never makes an edge stale
|
|
42
|
+
if ((edge.spec.bareSpec !== dependency.spec.bareSpec ||
|
|
43
|
+
(dependency.type !== 'implicit' &&
|
|
44
|
+
edge.type !== dependency.type)) &&
|
|
45
|
+
!modifiers?.targetsImporterEdge(importer, dependency.spec)) {
|
|
46
|
+
staleSpecs.set(edge, dependency);
|
|
47
|
+
}
|
|
32
48
|
dependencies.delete(name);
|
|
33
49
|
}
|
|
34
50
|
}
|
|
@@ -39,4 +55,5 @@ export const removeSatisfiedSpecs = ({ add, graph, }) => {
|
|
|
39
55
|
add.delete(depID);
|
|
40
56
|
}
|
|
41
57
|
}
|
|
58
|
+
return staleSpecs;
|
|
42
59
|
};
|
package/dist/ideal/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DepID } from '@vltpkg/dep-id';
|
|
1
2
|
import type { PackageInfoClient } from '@vltpkg/package-info';
|
|
2
3
|
import type { Spec } from '@vltpkg/spec';
|
|
3
4
|
import type { DependencySaveType } from '@vltpkg/types';
|
|
@@ -18,6 +19,13 @@ export type TransientAddMap = Omit<AddImportersDependenciesMap, 'modifiedDepende
|
|
|
18
19
|
* When these nodes are processed, their dependencies in this map are excluded.
|
|
19
20
|
*/
|
|
20
21
|
export type TransientRemoveMap = Omit<RemoveImportersDependenciesMap, 'modifiedDependencies'>;
|
|
22
|
+
/**
|
|
23
|
+
* Dependency names the user asked for on the command line, keyed by the
|
|
24
|
+
* {@link DepID} of the node receiving them. Snapshotted before
|
|
25
|
+
* manifest-derived deltas are merged into `add`, so it is the only
|
|
26
|
+
* reliable signal that a spec came from an explicit request.
|
|
27
|
+
*/
|
|
28
|
+
export type ExplicitAddMap = Map<DepID, Set<string>>;
|
|
21
29
|
export type BuildIdealAddOptions = {
|
|
22
30
|
/**
|
|
23
31
|
* A {@link AddImportersDependenciesMap} in which keys are {@link DepID}
|
|
@@ -87,6 +95,11 @@ export type PeerContextEntry = {
|
|
|
87
95
|
* instead of piling up per dependent.
|
|
88
96
|
*/
|
|
89
97
|
specs: Map<string, Spec>;
|
|
98
|
+
/**
|
|
99
|
+
* `specs` is shared with the entry this one was forked from or into.
|
|
100
|
+
* Write through `ownSpecs()` so the other side keeps its snapshot.
|
|
101
|
+
*/
|
|
102
|
+
sharedSpecs?: boolean;
|
|
90
103
|
/** The target Node that satisfies all specs for this peer context entry */
|
|
91
104
|
target: Node | undefined;
|
|
92
105
|
/** The type of dependency this entry represents */
|
|
@@ -108,4 +121,11 @@ export type PeerContextEntryInput = {
|
|
|
108
121
|
*/
|
|
109
122
|
export type PeerContext = Map<string, PeerContextEntry> & {
|
|
110
123
|
index?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Bumped whenever an entry is added or an entry's target changes, i.e.
|
|
126
|
+
* whenever the name -> target mapping a fork snapshots moves. Part of
|
|
127
|
+
* the fork cache key so a base that moved cannot hand out the fork
|
|
128
|
+
* taken before the move.
|
|
129
|
+
*/
|
|
130
|
+
rev?: number;
|
|
111
131
|
};
|