@vltpkg/graph 1.0.7 → 1.0.8
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
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
import { joinDepIDTuple, joinExtra } from '@vltpkg/dep-id';
|
|
1
|
+
import { baseDepID, getTuple, hydrate, joinDepIDTuple, joinExtra, splitDepID, } from '@vltpkg/dep-id';
|
|
2
2
|
import { error } from '@vltpkg/error-cause';
|
|
3
3
|
import { Spec } from '@vltpkg/spec';
|
|
4
4
|
import { satisfies } from '@vltpkg/satisfies';
|
|
5
|
+
import { parse as parseVersion } from '@vltpkg/semver';
|
|
5
6
|
import { longDependencyTypes, normalizeManifest } from '@vltpkg/types';
|
|
6
7
|
import { fixupAddedNames } from "../fixup-added-names.js";
|
|
7
|
-
import { shorten } from "../dependencies.js";
|
|
8
|
+
import { addKey, shorten, shouldInstallDepType, } from "../dependencies.js";
|
|
8
9
|
import { removeOptionalSubgraph } from "../remove-optional-subgraph.js";
|
|
9
10
|
import { isPathSecurityError } from "../path-security-error.js";
|
|
10
11
|
import { extractNode } from "../reify/extract-node.js";
|
|
11
12
|
import { checkPeerEdgesCompatible, endPeerPlacement, forkPeerContext, postPlacementPeerCheck, startPeerPlacement, } from "./peers.js";
|
|
12
13
|
import { compareByHasPeerDeps } from "./sorting.js";
|
|
13
|
-
/**
|
|
14
|
-
* Only install devDeps for git dependencies and importers
|
|
15
|
-
* Everything else always gets installed
|
|
16
|
-
*/
|
|
17
|
-
const shouldInstallDepType = (node, depType) => depType !== 'devDependencies' ||
|
|
18
|
-
node.importer ||
|
|
19
|
-
node.id.startsWith('git');
|
|
20
14
|
/**
|
|
21
15
|
* Retrieve the {@link DepID} and location for a `file:` type {@link Node}.
|
|
22
16
|
*/
|
|
@@ -42,61 +36,99 @@ const getFileTypeInfo = (spec, fromNode, scurry) => {
|
|
|
42
36
|
};
|
|
43
37
|
};
|
|
44
38
|
const isStringArray = (a) => Array.isArray(a) && !a.some(b => typeof b !== 'string');
|
|
39
|
+
const actualBases = new WeakMap();
|
|
40
|
+
const hasActualBase = (actual, id) => {
|
|
41
|
+
let bases = actualBases.get(actual);
|
|
42
|
+
if (!bases) {
|
|
43
|
+
bases = new Set([...actual.nodes.keys()].map(baseDepID));
|
|
44
|
+
actualBases.set(actual, bases);
|
|
45
|
+
}
|
|
46
|
+
return bases.has(baseDepID(id));
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Can the lockfile target still serve this spec? Exact for sources
|
|
50
|
+
* satisfies() understands; loose fallback for sources it cannot verify
|
|
51
|
+
* (named jsr registries, dist-tags): only for a registry node stamped
|
|
52
|
+
* with the same type+registry segments a fresh node for this spec would
|
|
53
|
+
* get, so a locked node from another source is never reused.
|
|
54
|
+
*/
|
|
55
|
+
const lockedFits = (n, spec, fromNode, graph) => {
|
|
56
|
+
const final = spec.final;
|
|
57
|
+
if (satisfies(n.id, final, fromNode.location, graph.projectRoot, graph.monorepo)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (final.type !== 'registry')
|
|
61
|
+
return false;
|
|
62
|
+
const [idType, idRegistry] = splitDepID(n.id);
|
|
63
|
+
if (idType !== 'registry')
|
|
64
|
+
return false;
|
|
65
|
+
const expected = getTuple(spec, {
|
|
66
|
+
name: n.name,
|
|
67
|
+
version: n.version,
|
|
68
|
+
});
|
|
69
|
+
if (idRegistry !== expected[1])
|
|
70
|
+
return false;
|
|
71
|
+
// the package the final spec resolves, never the edge name: an alias
|
|
72
|
+
// (`foo@npm:bar@^1`, `semver@jsr:@std/semver@^1`) points the same edge
|
|
73
|
+
// at a different package, and `final.name` already carries the jsr
|
|
74
|
+
// registry form of the name
|
|
75
|
+
if (n.name !== final.name)
|
|
76
|
+
return false;
|
|
77
|
+
if (!final.range)
|
|
78
|
+
return true;
|
|
79
|
+
/* c8 ignore next */
|
|
80
|
+
if (!n.version)
|
|
81
|
+
return false;
|
|
82
|
+
const version = parseVersion(n.version);
|
|
83
|
+
return !!version && final.range.test(version);
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* The lockfile target captured for `fromNode → name` before resetEdges().
|
|
87
|
+
* A peer-fork rebuild gives fromNode a provisional `peer.N` id that
|
|
88
|
+
* misses the canonical id key, so retry with the base id key, recorded
|
|
89
|
+
* only when unambiguous across peer copies.
|
|
90
|
+
*/
|
|
91
|
+
const findLockedNode = (graph, fromNode, name) => {
|
|
92
|
+
const locked = graph.lockedResolutions;
|
|
93
|
+
if (!locked)
|
|
94
|
+
return;
|
|
95
|
+
let id = locked.get(`${fromNode.id}\0${name}`);
|
|
96
|
+
if (id === undefined) {
|
|
97
|
+
const baseFrom = baseDepID(fromNode.id);
|
|
98
|
+
if (baseFrom !== fromNode.id) {
|
|
99
|
+
id = locked.get(`${baseFrom}\0${name}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return id ? graph.nodes.get(id) : undefined;
|
|
103
|
+
};
|
|
45
104
|
/**
|
|
46
105
|
* Try to find a compatible resolution for a dependency, checking peer context.
|
|
47
106
|
* If the first resolution candidate is incompatible with the peer context,
|
|
48
107
|
* try other candidates.
|
|
49
108
|
*/
|
|
50
|
-
const findCompatibleResolution = (spec, fromNode, graph, peerContext,
|
|
51
|
-
// Hoist invariants once
|
|
52
|
-
const fromLoc = fromNode.location;
|
|
53
|
-
const projectRoot = graph.projectRoot;
|
|
54
|
-
const monorepo = graph.monorepo;
|
|
109
|
+
const findCompatibleResolution = (spec, fromNode, graph, peerContext, candidate, pending) => {
|
|
55
110
|
const final = spec.final;
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
const satisfiesFinal = (n) => {
|
|
59
|
-
const key = n.id;
|
|
60
|
-
const cached = satisfiesCache.get(key);
|
|
61
|
-
/* c8 ignore next 3 - optimization: cache hit when same node checked multiple times */
|
|
62
|
-
if (cached !== undefined) {
|
|
63
|
-
return cached;
|
|
64
|
-
}
|
|
65
|
-
const result = satisfies(key, final, fromLoc, projectRoot, monorepo);
|
|
66
|
-
satisfiesCache.set(key, result);
|
|
67
|
-
return result;
|
|
68
|
-
};
|
|
69
|
-
// Prefer existing edge target if it satisfies the spec.
|
|
70
|
-
// This ensures lockfile resolutions are preserved when still valid,
|
|
71
|
-
// rather than potentially picking a different satisfying version.
|
|
72
|
-
const existingEdge = fromNode.edgesOut.get(spec.name);
|
|
73
|
-
let existingNode;
|
|
74
|
-
if (existingEdge?.to &&
|
|
75
|
-
!existingEdge.to.detached &&
|
|
76
|
-
satisfiesFinal(existingEdge.to)) {
|
|
77
|
-
existingNode = existingEdge.to;
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
existingNode = graph.findResolution(spec, fromNode, queryModifier);
|
|
81
|
-
}
|
|
111
|
+
// the candidate was picked in the pass before the hydrations
|
|
112
|
+
let existingNode = candidate;
|
|
82
113
|
let peerCompatResult = existingNode ?
|
|
83
|
-
checkPeerEdgesCompatible(existingNode, fromNode, peerContext, graph)
|
|
114
|
+
checkPeerEdgesCompatible(existingNode, fromNode, peerContext, graph, pending)
|
|
84
115
|
: { compatible: true };
|
|
85
116
|
// CANDIDATE FALLBACK: If first candidate is peer-incompatible, try others
|
|
86
117
|
// Lazy-load candidates only when fallback needed
|
|
87
118
|
if (existingNode && !peerCompatResult.compatible) {
|
|
88
119
|
const candidates = graph.nodesByName.get(final.name);
|
|
89
120
|
if (candidates && candidates.size > 1) {
|
|
90
|
-
for (const
|
|
91
|
-
if (
|
|
121
|
+
for (const alt of candidates) {
|
|
122
|
+
if (alt === existingNode)
|
|
92
123
|
continue;
|
|
93
|
-
if (
|
|
124
|
+
if (alt.detached)
|
|
94
125
|
continue;
|
|
95
|
-
if (!
|
|
126
|
+
if (!satisfies(alt.id, final, fromNode.location, graph.projectRoot, graph.monorepo)) {
|
|
96
127
|
continue;
|
|
97
|
-
|
|
128
|
+
}
|
|
129
|
+
const compat = checkPeerEdgesCompatible(alt, fromNode, peerContext, graph, pending);
|
|
98
130
|
if (compat.compatible) {
|
|
99
|
-
existingNode =
|
|
131
|
+
existingNode = alt;
|
|
100
132
|
peerCompatResult = compat;
|
|
101
133
|
break;
|
|
102
134
|
}
|
|
@@ -105,6 +137,55 @@ const findCompatibleResolution = (spec, fromNode, graph, peerContext, queryModif
|
|
|
105
137
|
}
|
|
106
138
|
return { existingNode, peerCompatResult };
|
|
107
139
|
};
|
|
140
|
+
/**
|
|
141
|
+
* A detached lockfile node without a manifest still knows its exact
|
|
142
|
+
* version through its DepID. Rebuild the spec that produced it so the
|
|
143
|
+
* manifest fetch pins the locked version instead of re-resolving the
|
|
144
|
+
* range to the newest satisfying release. Only registry nodes carry a
|
|
145
|
+
* version; anything else keeps the range fetch.
|
|
146
|
+
*/
|
|
147
|
+
const lockedVersionFetch = (node, spec, options) => {
|
|
148
|
+
if (spec.final.type !== 'registry' || !node.version)
|
|
149
|
+
return;
|
|
150
|
+
// hydrate() reproduces alias / named / URL / jsr forms from the DepID
|
|
151
|
+
// but memoizes per (name, id) ignoring options, so re-parse its
|
|
152
|
+
// bareSpec with the current options
|
|
153
|
+
const bare = hydrate(node.id, spec.name, options).bareSpec;
|
|
154
|
+
const exact = Spec.parse(spec.name, bare, options);
|
|
155
|
+
// lockedFits only compares the DepID registry name segment; never
|
|
156
|
+
// fetch a locked version from a source other than the one the edge
|
|
157
|
+
// resolves to right now
|
|
158
|
+
if (exact.final.registry !== spec.final.registry)
|
|
159
|
+
return;
|
|
160
|
+
return { spec: exact, version: node.version };
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* A rebuild without node_modules loads lockfile nodes with no manifest.
|
|
164
|
+
* Fetch the locked version up front and hydrate the node so the reuse
|
|
165
|
+
* and peer compatibility decisions see exactly what the hidden-lockfile
|
|
166
|
+
* path sees; on any miss the node stays bare and the range fetch runs as
|
|
167
|
+
* before. Hydrations are shared across the parallel fetches of one
|
|
168
|
+
* appendNodes run so each node is fetched once.
|
|
169
|
+
*/
|
|
170
|
+
const hydrateLockedNode = (node, spec, graph, packageInfo, options, from, hydrations) => {
|
|
171
|
+
const seen = hydrations.get(node.id);
|
|
172
|
+
if (seen)
|
|
173
|
+
return seen;
|
|
174
|
+
const locked = lockedVersionFetch(node, spec, options);
|
|
175
|
+
if (!locked)
|
|
176
|
+
return;
|
|
177
|
+
const hydration = packageInfo
|
|
178
|
+
.manifest(locked.spec, { from })
|
|
179
|
+
.then(manifest => manifest, () => undefined)
|
|
180
|
+
.then(manifest => {
|
|
181
|
+
if (manifest?.version !== locked.version)
|
|
182
|
+
return;
|
|
183
|
+
node.manifest = normalizeManifest(manifest, String(locked.spec));
|
|
184
|
+
graph.manifests.set(node.id, node.manifest);
|
|
185
|
+
});
|
|
186
|
+
hydrations.set(node.id, hydration);
|
|
187
|
+
return hydration;
|
|
188
|
+
};
|
|
108
189
|
/**
|
|
109
190
|
* Fetch manifests for dependencies and create placement tasks.
|
|
110
191
|
*
|
|
@@ -121,15 +202,20 @@ const findCompatibleResolution = (spec, fromNode, graph, peerContext, queryModif
|
|
|
121
202
|
* **Read-only**: This function no longer mutates the graph. It returns
|
|
122
203
|
* tasks that will be applied serially in the BFS loop for deterministic ordering.
|
|
123
204
|
*/
|
|
124
|
-
const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry, peerContext, modifierRefs, depth = 0) => {
|
|
205
|
+
const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry, options, hydrations, peerContext, modifierRefs, depth = 0, explicit) => {
|
|
125
206
|
const fetchTasks = [];
|
|
126
207
|
const placementTasks = [];
|
|
127
208
|
const reuseTasks = [];
|
|
128
209
|
const forkRequests = [];
|
|
210
|
+
// resolve the spec each dep is placed with before any lookup
|
|
211
|
+
const entries = [];
|
|
129
212
|
for (const { spec: originalSpec, type } of deps) {
|
|
130
213
|
let spec = originalSpec;
|
|
131
214
|
const fileTypeInfo = getFileTypeInfo(spec, fromNode, scurry);
|
|
132
215
|
const activeModifier = modifierRefs?.get(spec.name);
|
|
216
|
+
const isExplicit = !!explicit
|
|
217
|
+
?.get(fromNode.id)
|
|
218
|
+
?.has(addKey(originalSpec));
|
|
133
219
|
// MODIFIER HANDLING: Swap spec if an edge modifier is fully matched
|
|
134
220
|
// Example: `vlt install --override "react:^19"` changes react's spec
|
|
135
221
|
const queryModifier = activeModifier?.modifier.query;
|
|
@@ -145,18 +231,75 @@ const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry,
|
|
|
145
231
|
continue;
|
|
146
232
|
}
|
|
147
233
|
}
|
|
234
|
+
entries.push({
|
|
235
|
+
spec,
|
|
236
|
+
type,
|
|
237
|
+
fileTypeInfo,
|
|
238
|
+
activeModifier,
|
|
239
|
+
queryModifier,
|
|
240
|
+
isExplicit,
|
|
241
|
+
// read after the modifier swap, so an --override that replaces the
|
|
242
|
+
// tag with a range keeps today's reuse
|
|
243
|
+
explicitTag: isExplicit && !!spec.final.distTag,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// everything this parent places at this level, by name: the provision
|
|
247
|
+
// source for optional peers of reuse candidates, explicit adds included
|
|
248
|
+
const pendingDeps = new Map();
|
|
249
|
+
for (const e of entries)
|
|
250
|
+
pendingDeps.set(e.spec.name, e);
|
|
251
|
+
// pick the reuse candidate once, here, so the placement loop below
|
|
252
|
+
// does not repeat the lookup, and hydrate it when it is a bare
|
|
253
|
+
// detached copy from a lockfile loaded without node_modules
|
|
254
|
+
const from = scurry.resolve(fromNode.location);
|
|
255
|
+
const pending = [];
|
|
256
|
+
for (const e of entries) {
|
|
257
|
+
// prefer the existing edge target when it still satisfies: lockfile
|
|
258
|
+
// resolutions are preserved rather than re-picked. a rebuild reset
|
|
259
|
+
// every edge, so this only fires when there was no reset, where no
|
|
260
|
+
// node is detached and there is nothing to hydrate either
|
|
261
|
+
const existing = fromNode.edgesOut.get(e.spec.name)?.to;
|
|
262
|
+
if (existing &&
|
|
263
|
+
!existing.detached &&
|
|
264
|
+
satisfies(existing.id, e.spec.final, fromNode.location, graph.projectRoot, graph.monorepo)) {
|
|
265
|
+
e.candidate = existing;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const locked = findLockedNode(graph, fromNode, e.spec.name);
|
|
269
|
+
e.candidate =
|
|
270
|
+
locked && lockedFits(locked, e.spec, fromNode, graph) ? locked
|
|
271
|
+
: graph.findResolution(e.spec, fromNode, e.queryModifier);
|
|
272
|
+
// the tag is fetched below anyway, the locked version is dead weight
|
|
273
|
+
if (e.explicitTag)
|
|
274
|
+
continue;
|
|
275
|
+
if (e.candidate?.detached && !e.candidate.manifest) {
|
|
276
|
+
const hydration = hydrateLockedNode(e.candidate, e.spec, graph, packageInfo, options, from, hydrations);
|
|
277
|
+
if (hydration)
|
|
278
|
+
pending.push(hydration);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
await Promise.all(pending);
|
|
282
|
+
for (const { spec, type, fileTypeInfo, activeModifier, queryModifier, isExplicit, explicitTag, candidate, } of entries) {
|
|
148
283
|
const peer = type === 'peer' || type === 'peerOptional';
|
|
149
284
|
// NODE REUSE LOGIC with peer compatibility
|
|
150
|
-
const
|
|
285
|
+
const resolved = findCompatibleResolution(spec, fromNode, graph, peerContext, candidate, pendingDeps);
|
|
286
|
+
// a dist-tag satisfies any node of that name, so reuse is what
|
|
287
|
+
// installs an arbitrary version for an explicit `pkg@tag` request.
|
|
288
|
+
// drop the candidate and fetch the tag instead; a registry spec
|
|
289
|
+
// naming a workspace still links that workspace.
|
|
290
|
+
const dropForTag = explicitTag &&
|
|
291
|
+
!!resolved.existingNode &&
|
|
292
|
+
!resolved.existingNode.importer;
|
|
293
|
+
const existingNode = dropForTag ? undefined : resolved.existingNode;
|
|
294
|
+
const peerCompatResult = dropForTag ? { compatible: true } : resolved.peerCompatResult;
|
|
151
295
|
// Accumulate fork request if incompatible peer edges detected (defer actual fork)
|
|
152
296
|
const effectivePeerContext = peerContext;
|
|
153
|
-
/* c8 ignore start */
|
|
154
297
|
if (!peerCompatResult.compatible && peerCompatResult.forkEntry) {
|
|
155
298
|
forkRequests.push(peerCompatResult.forkEntry);
|
|
156
299
|
// All fork entries from this fromNode will be applied together in Phase B
|
|
157
300
|
}
|
|
158
|
-
|
|
159
|
-
//
|
|
301
|
+
// Locked detached nodes still go through placePackage so
|
|
302
|
+
// their child edges are rebuilt after resetEdges().
|
|
160
303
|
const validExistingNode = existingNode &&
|
|
161
304
|
!existingNode.detached &&
|
|
162
305
|
// Regular deps can always reuse.
|
|
@@ -194,21 +337,28 @@ const fetchManifestsForDeps = async (packageInfo, graph, fromNode, deps, scurry,
|
|
|
194
337
|
// Start manifest fetch immediately for parallel processing
|
|
195
338
|
const manifestPromise =
|
|
196
339
|
// the "detached" node state means that it has already been load as
|
|
197
|
-
// part of a graph (either lockfile or actual)
|
|
198
|
-
//
|
|
199
|
-
|
|
340
|
+
// part of a graph (either lockfile or actual), or was hydrated with
|
|
341
|
+
// its locked version above, and it has valid manifest data so we
|
|
342
|
+
// shortcut the package info manifest fetch here
|
|
343
|
+
// `keepVersion` marks a fork that only differs by an optional peer
|
|
344
|
+
// link, so the copy keeps the version it forked from instead of
|
|
345
|
+
// re-resolving the range to the newest satisfying release
|
|
346
|
+
(existingNode?.manifest &&
|
|
347
|
+
(existingNode.detached || peerCompatResult.keepVersion)) ?
|
|
200
348
|
Promise.resolve(existingNode.manifest)
|
|
201
349
|
// this is the entry point to fetch calls to retrieve manifests
|
|
202
350
|
// from the build ideal graph point of view
|
|
203
351
|
: packageInfo
|
|
204
|
-
.manifest(spec, { from
|
|
352
|
+
.manifest(spec, { from })
|
|
205
353
|
.then(manifest => manifest)
|
|
206
354
|
.catch((er) => {
|
|
207
355
|
// optional deps ignored if inaccessible, but a dep that tried
|
|
208
356
|
// to escape the project is never silently dropped
|
|
209
357
|
if (isPathSecurityError(er))
|
|
210
358
|
throw er;
|
|
211
|
-
|
|
359
|
+
// an explicit request has no version to save if it never
|
|
360
|
+
// resolved, so it fails the install even when optional
|
|
361
|
+
if (!isExplicit && (edgeOptional || fromNode.optional)) {
|
|
212
362
|
return undefined;
|
|
213
363
|
}
|
|
214
364
|
throw er;
|
|
@@ -324,7 +474,14 @@ const processPlacementTasks = async (graph, options, placementTasks, add, modifi
|
|
|
324
474
|
!node.isOptional() &&
|
|
325
475
|
// this fixes an issue with installing `file:pathname` specs
|
|
326
476
|
/* c8 ignore next */ !fileTypeInfo?.isDirectory &&
|
|
327
|
-
!node.importer
|
|
477
|
+
!node.importer &&
|
|
478
|
+
// provisional peer suffixes are rewritten by canonicalizePeerIds,
|
|
479
|
+
// which moves the store dir into place. Skip when a dir for this
|
|
480
|
+
// package version already exists: the rewrite most likely lands
|
|
481
|
+
// back on it, and re-extracting would be pure waste.
|
|
482
|
+
(!node.peerSetHash ||
|
|
483
|
+
!!fileTypeInfo ||
|
|
484
|
+
!hasActualBase(actual, node.id));
|
|
328
485
|
// extract the node if it meets the criteria for early extraction
|
|
329
486
|
if (eligibleForExtraction) {
|
|
330
487
|
/* c8 ignore start */
|
|
@@ -432,6 +589,12 @@ const processPlacementTasks = async (graph, options, placementTasks, add, modifi
|
|
|
432
589
|
}
|
|
433
590
|
}
|
|
434
591
|
}
|
|
592
|
+
// a name declared both as a regular dependency (manifest or
|
|
593
|
+
// transient) and as a peer is the package's own dependency: placing
|
|
594
|
+
// it once, as the regular type, keeps the edge type independent of
|
|
595
|
+
// when the peer would have resolved
|
|
596
|
+
for (const dep of nextDeps)
|
|
597
|
+
nextPeerDeps.delete(dep.spec.name);
|
|
435
598
|
// finish peer placement for this node, resolving satisfied peers
|
|
436
599
|
// to seen nodes from the peer context and adding unsatisfied peers
|
|
437
600
|
// to `nextDeps` so they get processed along regular dependencies
|
|
@@ -465,12 +628,15 @@ const processPlacementTasks = async (graph, options, placementTasks, add, modifi
|
|
|
465
628
|
* **Early Extraction**: When `actual` graph is provided, nodes are extracted
|
|
466
629
|
* to the vlt store during graph construction (not after), improving performance.
|
|
467
630
|
*/
|
|
468
|
-
export const appendNodes = async (packageInfo, graph, fromNode, deps, scurry, options, seen, add, modifiers, modifierRefs, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove) => {
|
|
631
|
+
export const appendNodes = async (packageInfo, graph, fromNode, deps, scurry, options, seen, add, modifiers, modifierRefs, extractPromises, actual, seenExtracted, remover, transientAdd, transientRemove, explicit) => {
|
|
469
632
|
// Cycle detection: skip if already processed
|
|
470
633
|
/* c8 ignore next */
|
|
471
634
|
if (seen.has(fromNode.id))
|
|
472
635
|
return;
|
|
473
636
|
seen.add(fromNode.id);
|
|
637
|
+
// locked lockfile nodes hydrated with their exact manifest, at most
|
|
638
|
+
// once per node across the parallel fetches
|
|
639
|
+
const hydrations = new Map();
|
|
474
640
|
// PEER CONTEXT ISOLATION: Each workspace importer needs its own context
|
|
475
641
|
// to prevent peer targets from one workspace affecting another.
|
|
476
642
|
// The main importer (index 0) uses the initial context; others get fresh ones.
|
|
@@ -516,7 +682,7 @@ export const appendNodes = async (packageInfo, graph, fromNode, deps, scurry, op
|
|
|
516
682
|
// Fetch manifests and collect tasks (no graph mutations)
|
|
517
683
|
const result = await fetchManifestsForDeps(packageInfo, graph, node,
|
|
518
684
|
// Sort by name for deterministic ordering (reproducible builds)
|
|
519
|
-
nodeDeps.sort((a, b) => a.spec.name.localeCompare(b.spec.name, 'en')), scurry, peerContext, nodeModifierRefs, depth);
|
|
685
|
+
nodeDeps.sort((a, b) => a.spec.name.localeCompare(b.spec.name, 'en')), scurry, options, hydrations, peerContext, nodeModifierRefs, depth, explicit);
|
|
520
686
|
return {
|
|
521
687
|
entry: {
|
|
522
688
|
node,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getImporterSpecs } from "./get-importer-specs.js";
|
|
2
|
+
import { canonicalizePeerIds, hasProvisionalPeerIds, } from "./canonicalize-peer-ids.js";
|
|
3
|
+
import { movePeerStoreDirs } from "./move-peer-store-dirs.js";
|
|
2
4
|
import { refreshIdealGraph } from "./refresh-ideal-graph.js";
|
|
3
5
|
import { resolveSaveType } from "../resolve-save-type.js";
|
|
4
6
|
/**
|
|
@@ -9,11 +11,21 @@ import { resolveSaveType } from "../resolve-save-type.js";
|
|
|
9
11
|
* listed in the `add` and `remove` properties.
|
|
10
12
|
*/
|
|
11
13
|
export const buildIdealFromStartingGraph = async (options) => {
|
|
14
|
+
// a graph loaded from a lockfile written by this code is canonical by
|
|
15
|
+
// construction; only a structural write during this build, or
|
|
16
|
+
// provisional ids left by an older lockfile, can change that
|
|
17
|
+
const mutationsBefore = options.graph.mutations;
|
|
12
18
|
// Gets a map of dependencies that are keyed to its importer node ids,
|
|
13
19
|
// merging values already found in the graph with user specified values.
|
|
14
20
|
// Any dependencies that are already satisfied in the starting `graph`
|
|
15
21
|
// are going to be pruned from the resulting object.
|
|
16
22
|
const importerSpecs = getImporterSpecs(options);
|
|
23
|
+
// snapshot what the user actually asked for before the merge below
|
|
24
|
+
// folds manifest-derived deltas into `options.add`
|
|
25
|
+
const explicit = new Map();
|
|
26
|
+
for (const [id, deps] of options.add) {
|
|
27
|
+
explicit.set(id, new Set(deps.keys()));
|
|
28
|
+
}
|
|
17
29
|
// merge modifiedDependencies flags
|
|
18
30
|
options.add.modifiedDependencies =
|
|
19
31
|
options.add.modifiedDependencies ||
|
|
@@ -56,14 +68,30 @@ export const buildIdealFromStartingGraph = async (options) => {
|
|
|
56
68
|
options.remove.get(nodeId)?.add(depName);
|
|
57
69
|
}
|
|
58
70
|
}
|
|
71
|
+
// an importer edge whose lockfile spec or type no longer matches
|
|
72
|
+
// package.json while its target still satisfies it: rewrite both so
|
|
73
|
+
// the rebuild reads the manifest values and the lockfile is saved
|
|
74
|
+
// carrying them
|
|
75
|
+
for (const [edge, dep] of importerSpecs.staleSpecs) {
|
|
76
|
+
edge.spec = dep.spec;
|
|
77
|
+
edge.type = resolveSaveType(edge.from, edge.name, dep.type);
|
|
78
|
+
}
|
|
79
|
+
if (importerSpecs.staleSpecs.size) {
|
|
80
|
+
options.graph.lockfileStale = true;
|
|
81
|
+
}
|
|
59
82
|
// refreshs the current graph adding the nodes marked for addition
|
|
60
83
|
// and removing the ones marked for removal, while also recalculating
|
|
61
84
|
// peer dependencies and default locations
|
|
62
85
|
await refreshIdealGraph({
|
|
63
86
|
...options,
|
|
87
|
+
explicit,
|
|
64
88
|
transientAdd: importerSpecs.transientAdd,
|
|
65
89
|
transientRemove: importerSpecs.transientRemove,
|
|
66
90
|
});
|
|
67
91
|
options.graph.gc();
|
|
92
|
+
if (options.graph.mutations !== mutationsBefore ||
|
|
93
|
+
hasProvisionalPeerIds(options.graph)) {
|
|
94
|
+
await movePeerStoreDirs(canonicalizePeerIds(options.graph), options);
|
|
95
|
+
}
|
|
68
96
|
return options.graph;
|
|
69
97
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { DepID } from '@vltpkg/dep-id';
|
|
2
|
+
import type { Graph } from '../graph.ts';
|
|
3
|
+
import type { Node } from '../node.ts';
|
|
4
|
+
export type CanonicalizePeerIdsOptions = {
|
|
5
|
+
digest?: (input: string, length: number) => string;
|
|
6
|
+
};
|
|
7
|
+
export type PeerStoreMove = {
|
|
8
|
+
node: Node;
|
|
9
|
+
from: DepID;
|
|
10
|
+
/** absent = merged-away copy, discard its dir */
|
|
11
|
+
to?: DepID;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Any change to this digest, to `serializeNodeEnv`, to `unitEntry` or to
|
|
15
|
+
* the suffix format changes every peer id in the lockfile, and the ideal
|
|
16
|
+
* builder no longer re-hashes a graph it did not otherwise touch. Bump
|
|
17
|
+
* `LOCKFILE_VERSION` alongside it so old lockfiles hard-fail instead of
|
|
18
|
+
* lingering with stale ids.
|
|
19
|
+
*/
|
|
20
|
+
export declare const peerEnvDigest: (input: string, length?: number) => string;
|
|
21
|
+
/**
|
|
22
|
+
* `peer.<ordinal>` as minted by retrievePeerContextHash() during a
|
|
23
|
+
* rebuild, or written by lockfiles from before the hash format.
|
|
24
|
+
* Canonical suffixes are 16 or 32 hex chars, optionally followed by
|
|
25
|
+
* `.<role>` or `.x`; an all-digit hash is possible, hence the length
|
|
26
|
+
* bound rather than a plain `\d+`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const isProvisionalPeerSuffix: (suffix: string) => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* True when any node still carries a provisional id, so the graph needs
|
|
31
|
+
* a canonicalization pass even if this build did not write to it.
|
|
32
|
+
*/
|
|
33
|
+
export declare const hasProvisionalPeerIds: (graph: Graph) => boolean;
|
|
34
|
+
export declare const byteCompare: (a: string, b: string) => number;
|
|
35
|
+
export declare const isPeerScoped: (node: Node, graph: Graph) => boolean;
|
|
36
|
+
export declare const serializeNodeEnv: (node: Node, opts?: {
|
|
37
|
+
resolvedIds?: Map<Node, DepID>;
|
|
38
|
+
intraSccIndex?: Map<Node, number>;
|
|
39
|
+
}) => string;
|
|
40
|
+
/**
|
|
41
|
+
* Replace provisional / legacy `peer.<N>` DepID suffixes with a content
|
|
42
|
+
* hash of each node's resolved out-edge set. Called after `graph.gc()`.
|
|
43
|
+
*/
|
|
44
|
+
export declare const canonicalizePeerIds: (graph: Graph, options?: CanonicalizePeerIdsOptions) => PeerStoreMove[];
|