@vltpkg/graph 1.0.0-rc.13 → 1.0.0-rc.15
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/README.md +97 -22
- package/dist/actual/load.d.ts.map +1 -1
- package/dist/actual/load.js +3 -2
- package/dist/actual/load.js.map +1 -1
- package/dist/diff.d.ts +2 -0
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +5 -1
- package/dist/diff.js.map +1 -1
- package/dist/fixup-added-names.d.ts +4 -1
- package/dist/fixup-added-names.d.ts.map +1 -1
- package/dist/fixup-added-names.js +16 -0
- package/dist/fixup-added-names.js.map +1 -1
- package/dist/graph.d.ts +7 -0
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +17 -2
- package/dist/graph.js.map +1 -1
- package/dist/ideal/append-nodes.d.ts +14 -2
- package/dist/ideal/append-nodes.d.ts.map +1 -1
- package/dist/ideal/append-nodes.js +188 -57
- package/dist/ideal/append-nodes.js.map +1 -1
- package/dist/ideal/build.d.ts.map +1 -1
- package/dist/ideal/build.js +11 -1
- package/dist/ideal/build.js.map +1 -1
- package/dist/ideal/peers.d.ts +90 -6
- package/dist/ideal/peers.d.ts.map +1 -1
- package/dist/ideal/peers.js +387 -131
- package/dist/ideal/peers.js.map +1 -1
- package/dist/ideal/refresh-ideal-graph.d.ts +0 -4
- package/dist/ideal/refresh-ideal-graph.d.ts.map +1 -1
- package/dist/ideal/refresh-ideal-graph.js +8 -24
- package/dist/ideal/refresh-ideal-graph.js.map +1 -1
- package/dist/ideal/sorting.d.ts +46 -0
- package/dist/ideal/sorting.d.ts.map +1 -0
- package/dist/ideal/sorting.js +71 -0
- package/dist/ideal/sorting.js.map +1 -0
- package/dist/ideal/types.d.ts +2 -6
- package/dist/ideal/types.d.ts.map +1 -1
- package/dist/ideal/types.js.map +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +12 -0
- package/dist/install.js.map +1 -1
- package/dist/lockfile/load.d.ts.map +1 -1
- package/dist/lockfile/load.js +21 -0
- package/dist/lockfile/load.js.map +1 -1
- package/dist/lockfile/save.d.ts.map +1 -1
- package/dist/lockfile/save.js +2 -2
- package/dist/lockfile/save.js.map +1 -1
- package/dist/lockfile/types.d.ts +7 -0
- package/dist/lockfile/types.d.ts.map +1 -1
- package/dist/lockfile/types.js +6 -0
- package/dist/lockfile/types.js.map +1 -1
- package/dist/node.d.ts +2 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js.map +1 -1
- package/dist/reify/extract-node.d.ts.map +1 -1
- package/dist/reify/extract-node.js.map +1 -1
- package/dist/reify/index.d.ts +1 -0
- package/dist/reify/index.d.ts.map +1 -1
- package/dist/reify/index.js +2 -1
- package/dist/reify/index.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +1 -0
- package/dist/update.js.map +1 -1
- package/dist/visualization/mermaid-output.d.ts +2 -1
- package/dist/visualization/mermaid-output.d.ts.map +1 -1
- package/dist/visualization/mermaid-output.js +28 -16
- package/dist/visualization/mermaid-output.js.map +1 -1
- package/package.json +22 -22
- package/dist/ideal/get-ordered-dependencies.d.ts +0 -10
- package/dist/ideal/get-ordered-dependencies.d.ts.map +0 -1
- package/dist/ideal/get-ordered-dependencies.js +0 -42
- package/dist/ideal/get-ordered-dependencies.js.map +0 -1
package/dist/ideal/peers.js
CHANGED
|
@@ -2,8 +2,197 @@
|
|
|
2
2
|
// during the ideal graph building process.
|
|
3
3
|
import { intersects } from '@vltpkg/semver';
|
|
4
4
|
import { satisfies } from '@vltpkg/satisfies';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { Spec } from '@vltpkg/spec';
|
|
6
|
+
import { getDependencies, shorten } from "../dependencies.js";
|
|
7
|
+
import { compareByType, getOrderedDependencies } from "./sorting.js";
|
|
8
|
+
import { longDependencyTypes } from '@vltpkg/types';
|
|
9
|
+
/**
|
|
10
|
+
* Check if a node satisfies a spec within a given context.
|
|
11
|
+
*
|
|
12
|
+
* Wraps the common `satisfies()` call pattern used throughout peer dependency
|
|
13
|
+
* resolution. The satisfaction check requires:
|
|
14
|
+
* - `node.id`: The DepID of the candidate node
|
|
15
|
+
* - `spec`: The spec to satisfy (e.g., `^18.0.0`)
|
|
16
|
+
* - `fromNode.location`: Where the dependency is declared (affects file: specs)
|
|
17
|
+
* - `projectRoot`: For resolving workspace specs
|
|
18
|
+
* - `monorepo`: For workspace-aware resolution
|
|
19
|
+
*/
|
|
20
|
+
const nodeSatisfiesSpec = (node, spec, fromNode, graph) => satisfies(node.id, spec, fromNode.location, fromNode.projectRoot, graph.monorepo);
|
|
21
|
+
/**
|
|
22
|
+
* Parse a spec with registry options from a parent node context.
|
|
23
|
+
*
|
|
24
|
+
* Inherits registry configuration from `graph.mainImporter.options` to ensure
|
|
25
|
+
* consistent scope-registry and custom registry mappings. The `fromNode.registry`
|
|
26
|
+
* override allows scoped packages to use their configured registry.
|
|
27
|
+
*/
|
|
28
|
+
const parseSpec = (name, bareSpec, fromNode, graph) => Spec.parse(name, bareSpec, {
|
|
29
|
+
...graph.mainImporter.options,
|
|
30
|
+
registry: fromNode.registry,
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Generate a unique cache key for a peer context fork operation.
|
|
34
|
+
*
|
|
35
|
+
* Format: `{baseIndex}::{sortedEntrySignatures}`
|
|
36
|
+
* - `baseIndex`: The parent context's index (0 for initial context)
|
|
37
|
+
* - Entry signature: `{name}|{type}|{targetId}|{spec}` sorted alphabetically
|
|
38
|
+
*
|
|
39
|
+
* This enables caching identical fork operations to avoid creating duplicate
|
|
40
|
+
* peer contexts when the same entries would be added to the same base context.
|
|
41
|
+
*/
|
|
42
|
+
const getForkKey = (peerContext, entries) => {
|
|
43
|
+
const base = peerContext.index ?? 0;
|
|
44
|
+
const sig = entries
|
|
45
|
+
.map(e => `${e.spec.final.name}|${e.type}|${e.target?.id ?? '∅'}|${e.spec}`)
|
|
46
|
+
.sort()
|
|
47
|
+
.join(';');
|
|
48
|
+
return `${base}::${sig}`;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Check if parent declares a dep for peerName that the context target doesn't satisfy.
|
|
52
|
+
* If so, the context entry isn't applicable - return true to ignore the mismatch.
|
|
53
|
+
*
|
|
54
|
+
* This prevents cross-importer peer context leakage. Example scenario:
|
|
55
|
+
* - Root importer has `react@^18` in peer context
|
|
56
|
+
* - Workspace A declares `react@^19` as a dependency
|
|
57
|
+
* - When checking compatibility for Workspace A's deps, the `react@^18` context
|
|
58
|
+
* entry shouldn't force a fork because Workspace A will resolve its own react
|
|
59
|
+
*
|
|
60
|
+
* The logic: if parent declares peerName and the context target doesn't satisfy
|
|
61
|
+
* parent's declared spec, the context entry won't be used anyway, so ignore it.
|
|
62
|
+
*/
|
|
63
|
+
const shouldIgnoreContextMismatch = (peerName, contextTarget, fromNode, graph) => {
|
|
64
|
+
const parentManifest = fromNode.manifest;
|
|
65
|
+
/* c8 ignore next - edge case: fromNode always has manifest in practice */
|
|
66
|
+
if (!parentManifest)
|
|
67
|
+
return false;
|
|
68
|
+
// Search all dependency types for a declaration of peerName
|
|
69
|
+
for (const depType of longDependencyTypes) {
|
|
70
|
+
const declared = parentManifest[depType]?.[peerName];
|
|
71
|
+
if (!declared)
|
|
72
|
+
continue;
|
|
73
|
+
// Parent declares this package - check if context target satisfies it
|
|
74
|
+
const parentSpec = parseSpec(peerName, declared, fromNode, graph);
|
|
75
|
+
// If context target doesn't satisfy parent's spec, ignore the mismatch
|
|
76
|
+
// because parent will resolve its own version anyway
|
|
77
|
+
return !nodeSatisfiesSpec(contextTarget, parentSpec, fromNode, graph);
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Build incompatible result if target satisfies the peer spec.
|
|
83
|
+
*
|
|
84
|
+
* Returns an incompatible result only when the target node actually satisfies
|
|
85
|
+
* the peer spec. This matters because:
|
|
86
|
+
* - If target satisfies the spec, it's a valid alternative that conflicts with
|
|
87
|
+
* the existing node's peer edge target
|
|
88
|
+
* - If target doesn't satisfy the spec, it's not a valid peer resolution, so
|
|
89
|
+
* there's no conflict to report
|
|
90
|
+
*
|
|
91
|
+
* The returned `forkEntry` contains the conflicting spec and target, which will
|
|
92
|
+
* be used to create a forked peer context with the alternative resolution.
|
|
93
|
+
*/
|
|
94
|
+
const buildIncompatibleResult = (target, peerSpec, type, fromNode, graph) => {
|
|
95
|
+
if (nodeSatisfiesSpec(target, peerSpec, fromNode, graph)) {
|
|
96
|
+
return {
|
|
97
|
+
compatible: false,
|
|
98
|
+
forkEntry: { spec: peerSpec, target, type },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Check if an existing node's peer edges would still resolve to the same
|
|
105
|
+
* targets from a new parent's context. Returns incompatible info if any
|
|
106
|
+
* peer would resolve differently, meaning the node should NOT be reused.
|
|
107
|
+
*
|
|
108
|
+
* This is crucial for avoiding incorrect node reuse that would break peer
|
|
109
|
+
* dependency contracts. Three sources of conflict are checked:
|
|
110
|
+
*
|
|
111
|
+
* 1. **Peer context entries**: The global peer context may have resolved a
|
|
112
|
+
* different version of a peer dependency than what the existing node expects.
|
|
113
|
+
*
|
|
114
|
+
* 2. **Already-placed siblings**: The parent node may already have an edge to
|
|
115
|
+
* a different version of the peer dependency.
|
|
116
|
+
*
|
|
117
|
+
* 3. **Not-yet-placed siblings**: The parent's manifest declares a dependency
|
|
118
|
+
* on the same package, and there's a graph node that would satisfy it but
|
|
119
|
+
* differs from what the existing node expects.
|
|
120
|
+
*/
|
|
121
|
+
export const checkPeerEdgesCompatible = (existingNode, fromNode, peerContext, graph) => {
|
|
122
|
+
const peerDeps = existingNode.manifest?.peerDependencies;
|
|
123
|
+
// No peer deps = always compatible
|
|
124
|
+
if (!peerDeps || Object.keys(peerDeps).length === 0) {
|
|
125
|
+
return { compatible: true };
|
|
126
|
+
}
|
|
127
|
+
for (const [peerName, peerBareSpec] of Object.entries(peerDeps)) {
|
|
128
|
+
const existingEdge = existingNode.edgesOut.get(peerName);
|
|
129
|
+
// CHECK 0: Reject reuse if peer edge doesn't exist yet (node unprocessed).
|
|
130
|
+
// Cannot verify compatibility since peer resolution depends on original
|
|
131
|
+
// placement context, which may differ from current parent's context.
|
|
132
|
+
// Note: Dangling edges (edge exists, no target) are handled separately below.
|
|
133
|
+
// This conservative check prevents incorrect reuse when placement order varies.
|
|
134
|
+
if (existingEdge === undefined) {
|
|
135
|
+
return { compatible: false };
|
|
136
|
+
}
|
|
137
|
+
// Dangling peer edge (edge exists but unresolved) - skip, nothing to conflict with
|
|
138
|
+
if (!existingEdge.to)
|
|
139
|
+
continue;
|
|
140
|
+
const peerSpec = parseSpec(peerName, peerBareSpec, fromNode, graph);
|
|
141
|
+
// CHECK 1: Does peer context have a different target for this peer?
|
|
142
|
+
const contextEntry = peerContext.get(peerName);
|
|
143
|
+
if (contextEntry?.target &&
|
|
144
|
+
contextEntry.target.id !== existingEdge.to.id &&
|
|
145
|
+
!shouldIgnoreContextMismatch(peerName, contextEntry.target, fromNode, graph)) {
|
|
146
|
+
const result = buildIncompatibleResult(contextEntry.target, peerSpec, contextEntry.type, fromNode, graph);
|
|
147
|
+
if (result)
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
// CHECK 2: Does parent already have an edge to a different version?
|
|
151
|
+
const siblingEdge = fromNode.edgesOut.get(peerName);
|
|
152
|
+
if (siblingEdge?.to && siblingEdge.to.id !== existingEdge.to.id) {
|
|
153
|
+
const result = buildIncompatibleResult(siblingEdge.to, peerSpec, siblingEdge.type, fromNode, graph);
|
|
154
|
+
if (result)
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
// CHECK 3: Does parent's manifest declare this peer, with a different
|
|
158
|
+
// satisfying node already in the graph?
|
|
159
|
+
const manifest = fromNode.manifest;
|
|
160
|
+
let declared;
|
|
161
|
+
let declaredType;
|
|
162
|
+
if (manifest) {
|
|
163
|
+
for (const depType of longDependencyTypes) {
|
|
164
|
+
const deps = manifest[depType];
|
|
165
|
+
if (deps &&
|
|
166
|
+
typeof deps === 'object' &&
|
|
167
|
+
!Array.isArray(deps) &&
|
|
168
|
+
peerName in deps) {
|
|
169
|
+
declared = deps[peerName];
|
|
170
|
+
declaredType = depType;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (declared && declaredType) {
|
|
176
|
+
const parentSpec = parseSpec(peerName, declared, fromNode, graph);
|
|
177
|
+
for (const candidateNode of graph.nodes.values()) {
|
|
178
|
+
if (candidateNode.name === peerName &&
|
|
179
|
+
candidateNode.id !== existingEdge.to.id &&
|
|
180
|
+
nodeSatisfiesSpec(candidateNode, parentSpec, fromNode, graph) &&
|
|
181
|
+
nodeSatisfiesSpec(candidateNode, peerSpec, fromNode, graph)) {
|
|
182
|
+
return {
|
|
183
|
+
compatible: false,
|
|
184
|
+
forkEntry: {
|
|
185
|
+
spec: peerSpec,
|
|
186
|
+
target: candidateNode,
|
|
187
|
+
type: shorten(declaredType),
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { compatible: true };
|
|
195
|
+
};
|
|
7
196
|
/**
|
|
8
197
|
* Retrieve a unique hash value for a given peer context set.
|
|
9
198
|
*/
|
|
@@ -11,25 +200,34 @@ export const retrievePeerContextHash = (peerContext) => {
|
|
|
11
200
|
// skips creating the initial peer context ref
|
|
12
201
|
if (!peerContext?.index)
|
|
13
202
|
return undefined;
|
|
14
|
-
return
|
|
203
|
+
return `peer.${peerContext.index}`;
|
|
15
204
|
};
|
|
16
205
|
/**
|
|
17
206
|
* Checks if a given spec is compatible with the specs already
|
|
18
207
|
* assigned to a peer context entry.
|
|
19
208
|
*
|
|
20
|
-
* Returns true if
|
|
209
|
+
* Returns true if INCOMPATIBLE, false if compatible.
|
|
210
|
+
*
|
|
211
|
+
* Compatibility rules:
|
|
212
|
+
* - **Registry specs**: Uses semver range intersection. `^18.0.0` and `^18.2.0`
|
|
213
|
+
* intersect (compatible), but `^18.0.0` and `^19.0.0` don't (incompatible).
|
|
214
|
+
* - **Non-registry specs** (git, file, etc.): Requires exact bareSpec match.
|
|
215
|
+
* `github:foo/bar#v1` only matches itself.
|
|
216
|
+
*
|
|
217
|
+
* This is used to determine when peer context forking is needed - if specs
|
|
218
|
+
* are incompatible, a new peer context must be created.
|
|
21
219
|
*/
|
|
22
220
|
export const incompatibleSpecs = (spec, entry) => {
|
|
23
221
|
if (entry.specs.size > 0) {
|
|
24
|
-
for (const
|
|
222
|
+
for (const s_ of entry.specs) {
|
|
223
|
+
const s = s_.final;
|
|
25
224
|
if (
|
|
26
|
-
//
|
|
225
|
+
// Registry types: check semver range intersection
|
|
27
226
|
(spec.type === 'registry' &&
|
|
28
227
|
(!spec.range ||
|
|
29
228
|
!s.range ||
|
|
30
229
|
!intersects(spec.range, s.range))) ||
|
|
31
|
-
//
|
|
32
|
-
// they use the very same bareSpec value
|
|
230
|
+
// Non-registry types: require exact bareSpec match
|
|
33
231
|
(spec.type !== 'registry' && spec.bareSpec !== s.bareSpec)) {
|
|
34
232
|
return true;
|
|
35
233
|
}
|
|
@@ -41,15 +239,7 @@ export const incompatibleSpecs = (spec, entry) => {
|
|
|
41
239
|
* Sort peer context entry inputs for deterministic processing.
|
|
42
240
|
* Orders: non-peer dependencies first, then peer dependencies, alphabetically by name.
|
|
43
241
|
*/
|
|
44
|
-
export const getOrderedPeerContextEntries = (entries) => [...entries].sort(
|
|
45
|
-
const aIsPeer = a.type === 'peer' || a.type === 'peerOptional' ? 1 : 0;
|
|
46
|
-
const bIsPeer = b.type === 'peer' || b.type === 'peerOptional' ? 1 : 0;
|
|
47
|
-
if (aIsPeer !== bIsPeer)
|
|
48
|
-
return aIsPeer - bIsPeer;
|
|
49
|
-
const aName = a.target?.name ?? a.spec.name;
|
|
50
|
-
const bName = b.target?.name ?? b.spec.name;
|
|
51
|
-
return aName.localeCompare(bName, 'en');
|
|
52
|
-
});
|
|
242
|
+
export const getOrderedPeerContextEntries = (entries) => [...entries].sort(compareByType);
|
|
53
243
|
/*
|
|
54
244
|
* Checks if there are any conflicting versions for a given dependency
|
|
55
245
|
* to be added to a peer context set which will require forking.
|
|
@@ -65,7 +255,7 @@ export const checkEntriesToPeerContext = (peerContext, entries) => {
|
|
|
65
255
|
if (!entry?.active)
|
|
66
256
|
continue;
|
|
67
257
|
// validate if the provided spec is compatible with existing specs
|
|
68
|
-
if (incompatibleSpecs(spec, entry)) {
|
|
258
|
+
if (incompatibleSpecs(spec.final, entry)) {
|
|
69
259
|
return true;
|
|
70
260
|
}
|
|
71
261
|
}
|
|
@@ -80,17 +270,13 @@ export const checkEntriesToPeerContext = (peerContext, entries) => {
|
|
|
80
270
|
* Returns true if forking is needed, false otherwise.
|
|
81
271
|
*/
|
|
82
272
|
export const addEntriesToPeerContext = (peerContext, entries, fromNode, monorepo) => {
|
|
83
|
-
// pre check
|
|
84
|
-
// provided peer context set conflicts with existing ones
|
|
85
|
-
// if that's already the case we can skip processing them and
|
|
86
|
-
// will return that a fork is needed right away
|
|
273
|
+
// pre check for conflicts before processing
|
|
87
274
|
if (checkEntriesToPeerContext(peerContext, entries))
|
|
88
275
|
return true;
|
|
89
|
-
// iterate on every entry to be added to the peer context set
|
|
90
276
|
for (const { dependent, spec, target, type } of entries) {
|
|
91
277
|
const name = target?.name ?? spec.final.name;
|
|
92
|
-
// if there's no existing entry, create one
|
|
93
278
|
let entry = peerContext.get(name);
|
|
279
|
+
// create new entry if none exists
|
|
94
280
|
if (!entry) {
|
|
95
281
|
entry = {
|
|
96
282
|
active: true,
|
|
@@ -104,39 +290,27 @@ export const addEntriesToPeerContext = (peerContext, entries, fromNode, monorepo
|
|
|
104
290
|
entry.contextDependents.add(dependent);
|
|
105
291
|
continue;
|
|
106
292
|
}
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
// case of adding sibling deps that conflicts with one another
|
|
110
|
-
if (incompatibleSpecs(spec, entry))
|
|
293
|
+
// check for sibling dep conflicts
|
|
294
|
+
if (incompatibleSpecs(spec.final, entry))
|
|
111
295
|
return true;
|
|
296
|
+
// update target if compatible with all specs
|
|
112
297
|
if (target &&
|
|
113
298
|
[...entry.specs].every(s => satisfies(target.id, s, fromNode.location, fromNode.projectRoot, monorepo))) {
|
|
114
299
|
if (target.id !== entry.target?.id &&
|
|
115
300
|
target.version !== entry.target?.version) {
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Only update all dependents to point to the new target if
|
|
124
|
-
// the existing target doesn't satisfy the new spec
|
|
125
|
-
for (const dependents of entry.contextDependents) {
|
|
126
|
-
const edge = dependents.edgesOut.get(name);
|
|
127
|
-
if (edge?.to && edge.to !== target) {
|
|
128
|
-
edge.to.edgesIn.delete(edge);
|
|
129
|
-
edge.to = target;
|
|
130
|
-
target.edgesIn.add(edge);
|
|
131
|
-
}
|
|
301
|
+
// update dependents to point to new target
|
|
302
|
+
for (const dep of entry.contextDependents) {
|
|
303
|
+
const edge = dep.edgesOut.get(name);
|
|
304
|
+
if (edge?.to && edge.to !== target) {
|
|
305
|
+
edge.to.edgesIn.delete(edge);
|
|
306
|
+
edge.to = target;
|
|
307
|
+
target.edgesIn.add(edge);
|
|
132
308
|
}
|
|
133
|
-
entry.target = target;
|
|
134
309
|
}
|
|
310
|
+
entry.target = target;
|
|
135
311
|
}
|
|
136
|
-
// otherwise sets the value in case it was nullish
|
|
137
312
|
entry.target ??= target;
|
|
138
313
|
}
|
|
139
|
-
// update specs and dependents values
|
|
140
314
|
entry.specs.add(spec);
|
|
141
315
|
if (dependent)
|
|
142
316
|
entry.contextDependents.add(dependent);
|
|
@@ -147,24 +321,25 @@ export const addEntriesToPeerContext = (peerContext, entries, fromNode, monorepo
|
|
|
147
321
|
* Create and returns a forked copy of a given peer context set.
|
|
148
322
|
*/
|
|
149
323
|
export const forkPeerContext = (graph, peerContext, entries) => {
|
|
324
|
+
const forkKey = getForkKey(peerContext, entries);
|
|
325
|
+
const cached = graph.peerContextForkCache.get(forkKey);
|
|
326
|
+
if (cached) {
|
|
327
|
+
return cached;
|
|
328
|
+
}
|
|
150
329
|
// create a new peer context set
|
|
151
330
|
const nextPeerContext = new Map();
|
|
152
331
|
nextPeerContext.index = graph.nextPeerContextIndex();
|
|
153
332
|
// register it in the graph
|
|
154
333
|
graph.peerContexts[nextPeerContext.index] = nextPeerContext;
|
|
334
|
+
graph.peerContextForkCache.set(forkKey, nextPeerContext);
|
|
155
335
|
// copy existing entries marking them as inactive, it's also important
|
|
156
336
|
// to note that specs and contextDependents are new objects so that changes
|
|
157
|
-
// to those in the new context do not affect the previous one
|
|
158
|
-
// IMPORTANT: We preserve the target from the parent context so that packages
|
|
159
|
-
// in the forked context can still resolve peer deps to the same version as
|
|
160
|
-
// the parent context. This fixes an issue where forked contexts would lose
|
|
161
|
-
// track of already-resolved peer dependencies (like a pinned typescript version)
|
|
162
|
-
// and resolve to different versions.
|
|
337
|
+
// to those in the new context do not affect the previous one
|
|
163
338
|
for (const [name, entry] of peerContext.entries()) {
|
|
164
339
|
nextPeerContext.set(name, {
|
|
165
340
|
active: false,
|
|
166
341
|
specs: new Set(entry.specs),
|
|
167
|
-
target:
|
|
342
|
+
target: undefined,
|
|
168
343
|
type: entry.type,
|
|
169
344
|
contextDependents: new Set(entry.contextDependents),
|
|
170
345
|
});
|
|
@@ -174,17 +349,10 @@ export const forkPeerContext = (graph, peerContext, entries) => {
|
|
|
174
349
|
for (const entry of entries) {
|
|
175
350
|
const { dependent, spec, target, type } = entry;
|
|
176
351
|
const name = target?.name /* c8 ignore next */ ?? spec.final.name;
|
|
177
|
-
// IMPORTANT: If the new entry has no target but the parent context had one,
|
|
178
|
-
// preserve the parent's target. This ensures that when a fork happens due to
|
|
179
|
-
// spec "incompatibility" (e.g., pinned version vs range), we don't lose the
|
|
180
|
-
// already-resolved target. The satisfies check in resolvePeerDeps will later
|
|
181
|
-
// verify if the preserved target actually satisfies the new spec.
|
|
182
|
-
const existingEntry = nextPeerContext.get(name);
|
|
183
|
-
const preservedTarget = !target && existingEntry?.target ? existingEntry.target : target;
|
|
184
352
|
const newEntry = {
|
|
185
353
|
active: true,
|
|
186
354
|
specs: new Set([spec]),
|
|
187
|
-
target
|
|
355
|
+
target,
|
|
188
356
|
type,
|
|
189
357
|
contextDependents: dependent ? new Set([dependent]) : new Set(),
|
|
190
358
|
};
|
|
@@ -192,6 +360,51 @@ export const forkPeerContext = (graph, peerContext, entries) => {
|
|
|
192
360
|
}
|
|
193
361
|
return nextPeerContext;
|
|
194
362
|
};
|
|
363
|
+
/**
|
|
364
|
+
* Find a peer from queued entries' peer edge closure using BFS.
|
|
365
|
+
*
|
|
366
|
+
* This handles peer dependency cycles like `@isaacs/peer-dep-cycle-a/b/c` where:
|
|
367
|
+
* - A depends on B (peer)
|
|
368
|
+
* - B depends on C (peer)
|
|
369
|
+
* - C depends on A (peer)
|
|
370
|
+
*
|
|
371
|
+
* The BFS explores:
|
|
372
|
+
* 1. Start nodes: All resolved targets from `queuedEntries` (sibling deps)
|
|
373
|
+
* 2. For each node, check if it has an edge to `name` that satisfies `peerSpec`
|
|
374
|
+
* 3. If not found, follow peer edges to explore their peer edges (up to depth 3)
|
|
375
|
+
*
|
|
376
|
+
* Prefers "local" providers (found via sibling's peer edges) over global context.
|
|
377
|
+
*/
|
|
378
|
+
const findFromPeerClosure = (name, peerSpec, queuedEntries, fromNode, graph) => {
|
|
379
|
+
// Start BFS from all resolved sibling targets
|
|
380
|
+
const start = queuedEntries
|
|
381
|
+
.map(e => e.target)
|
|
382
|
+
.filter((n) => !!n);
|
|
383
|
+
const seen = new Set();
|
|
384
|
+
const q = start.map(n => ({
|
|
385
|
+
n,
|
|
386
|
+
depth: 0,
|
|
387
|
+
}));
|
|
388
|
+
while (q.length) {
|
|
389
|
+
const cur = q.shift();
|
|
390
|
+
if (!cur || seen.has(cur.n.id))
|
|
391
|
+
continue;
|
|
392
|
+
seen.add(cur.n.id);
|
|
393
|
+
// Check if this node has an edge to the peer we're looking for
|
|
394
|
+
const edge = cur.n.edgesOut.get(name);
|
|
395
|
+
if (edge?.to &&
|
|
396
|
+
nodeSatisfiesSpec(edge.to, peerSpec, fromNode, graph)) {
|
|
397
|
+
return edge.to;
|
|
398
|
+
}
|
|
399
|
+
// Follow peer edges only (not regular deps) to stay in peer closure
|
|
400
|
+
for (const e of cur.n.edgesOut.values()) {
|
|
401
|
+
if ((e.type === 'peer' || e.type === 'peerOptional') && e.to) {
|
|
402
|
+
q.push({ n: e.to, depth: cur.depth + 1 });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return undefined;
|
|
407
|
+
};
|
|
195
408
|
/**
|
|
196
409
|
* Starts the peer dependency placement process
|
|
197
410
|
* for a given node being processed and placed.
|
|
@@ -238,14 +451,28 @@ export const startPeerPlacement = (peerContext, manifest, fromNode, options) =>
|
|
|
238
451
|
* Ends the peer dependency placement process, returning the functions that
|
|
239
452
|
* are going to be used to update the peer context set, forking when needed
|
|
240
453
|
* and resolving peer dependencies if possible.
|
|
454
|
+
*
|
|
455
|
+
* Returns two deferred functions:
|
|
456
|
+
* - `putEntries()`: Adds entries to peer context; returns fork entries if conflict
|
|
457
|
+
* - `resolvePeerDeps()`: Resolves peer deps from context/siblings or adds to nextDeps
|
|
458
|
+
*
|
|
459
|
+
* These are deferred (not executed immediately) so that all siblings at a level
|
|
460
|
+
* can be processed before peer context updates, enabling context reuse optimization.
|
|
241
461
|
*/
|
|
242
462
|
export const endPeerPlacement = (peerContext, nextDeps, nextPeerDeps, graph, spec, fromNode, node, type, queuedEntries) => ({
|
|
243
463
|
/**
|
|
244
464
|
* Add the new entries to the current peer context set.
|
|
465
|
+
*
|
|
466
|
+
* Two sets of entries are checked:
|
|
467
|
+
* - `prevEntries`: Parent's queued entries + self-reference
|
|
468
|
+
* - `nextEntries`: This node's deps + peer deps (with node as dependent)
|
|
469
|
+
*
|
|
470
|
+
* If either conflicts with the current context, returns ALL entries to be
|
|
471
|
+
* added to a forked context (prevEntries last for priority).
|
|
472
|
+
*
|
|
473
|
+
* Returns `undefined` if no fork needed (entries added directly to context).
|
|
245
474
|
*/
|
|
246
475
|
putEntries: () => {
|
|
247
|
-
// keep track of whether we need to fork the current peer context set
|
|
248
|
-
let needsToForkPeerContext = false;
|
|
249
476
|
// add queued entries from this node parents along
|
|
250
477
|
// with a self-ref to the current peer context set
|
|
251
478
|
const prevEntries = [
|
|
@@ -256,8 +483,6 @@ export const endPeerPlacement = (peerContext, nextDeps, nextPeerDeps, graph, spe
|
|
|
256
483
|
type,
|
|
257
484
|
},
|
|
258
485
|
];
|
|
259
|
-
addEntriesToPeerContext(peerContext, prevEntries, fromNode, graph.monorepo);
|
|
260
|
-
// add this node's direct dependencies next
|
|
261
486
|
const nextEntries = [
|
|
262
487
|
...nextDeps.map(dep => ({ ...dep, dependent: node })),
|
|
263
488
|
...[...nextPeerDeps.values()].map(dep => ({
|
|
@@ -265,67 +490,85 @@ export const endPeerPlacement = (peerContext, nextDeps, nextPeerDeps, graph, spe
|
|
|
265
490
|
dependent: node,
|
|
266
491
|
})),
|
|
267
492
|
];
|
|
493
|
+
const conflictPrev = checkEntriesToPeerContext(peerContext, prevEntries);
|
|
494
|
+
const conflictNext = nextEntries.length > 0 &&
|
|
495
|
+
checkEntriesToPeerContext(peerContext, nextEntries);
|
|
496
|
+
if (conflictPrev || conflictNext) {
|
|
497
|
+
// returns all entries that need to be added to a forked context
|
|
498
|
+
// giving priority to parent entries (prevEntries) by placing them last
|
|
499
|
+
return [...nextEntries, ...prevEntries];
|
|
500
|
+
}
|
|
501
|
+
addEntriesToPeerContext(peerContext, prevEntries, fromNode, graph.monorepo);
|
|
268
502
|
if (nextEntries.length > 0) {
|
|
269
|
-
|
|
503
|
+
addEntriesToPeerContext(peerContext, nextEntries, node, graph.monorepo);
|
|
270
504
|
}
|
|
271
|
-
|
|
272
|
-
// context or undefined if the current context was updated directly
|
|
273
|
-
return needsToForkPeerContext ? nextEntries : undefined;
|
|
505
|
+
return undefined;
|
|
274
506
|
},
|
|
275
507
|
/**
|
|
276
508
|
* Try to resolve peer dependencies using already seen target
|
|
277
509
|
* values from the current peer context set.
|
|
278
|
-
*
|
|
510
|
+
*
|
|
511
|
+
* Resolution priority (highest to lowest):
|
|
512
|
+
* 1. Sibling deps from parent (workspace direct deps take priority)
|
|
513
|
+
* 2. Peer-edge closure of sibling targets (handles peer cycles)
|
|
514
|
+
* 3. Global peer context set entries
|
|
515
|
+
* 4. Add to nextDeps for normal resolution (or create dangling edge for optional)
|
|
279
516
|
*/
|
|
280
|
-
resolvePeerDeps: (
|
|
281
|
-
// iterate on the set of peer dependencies of the current node
|
|
282
|
-
// and try to resolve them from the existing peer context set,
|
|
283
|
-
// when possible, add them as edges in the graph right away, if not,
|
|
284
|
-
// then we move them back to the `nextDeps` list for processing
|
|
285
|
-
// along with the rest of the regular dependencies
|
|
517
|
+
resolvePeerDeps: () => {
|
|
286
518
|
for (const nextDep of nextPeerDeps.values()) {
|
|
287
519
|
const { spec, type } = nextDep;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// the current peer context set (which may have been forked)
|
|
306
|
-
const entry = currentContext.get(spec.final.name);
|
|
307
|
-
if (!node.edgesOut.has(spec.final.name) &&
|
|
308
|
-
entry?.target &&
|
|
309
|
-
satisfies(entry.target.id, spec, fromNode.location, fromNode.projectRoot, graph.monorepo)) {
|
|
310
|
-
// entry satisfied, create edge in the graph
|
|
311
|
-
graph.addEdge(type, spec, node, entry.target);
|
|
312
|
-
entry.specs.add(spec.final);
|
|
313
|
-
}
|
|
314
|
-
else if (type === 'peerOptional') {
|
|
315
|
-
// skip unsatisfied peerOptional dependencies,
|
|
316
|
-
// just create a dangling edge
|
|
317
|
-
graph.addEdge(type, spec, node);
|
|
318
|
-
}
|
|
319
|
-
else if (siblingEntry &&
|
|
320
|
-
siblingEntry.spec.bareSpec !== spec.bareSpec) {
|
|
321
|
-
// Sibling has a more specific spec for this package,
|
|
322
|
-
// use it when resolving to ensure we get the right version
|
|
323
|
-
nextDeps.push({ ...nextDep, spec: siblingEntry.spec });
|
|
520
|
+
/* c8 ignore next - only peer types reach here by design */
|
|
521
|
+
if (type !== 'peer' && type !== 'peerOptional')
|
|
522
|
+
continue;
|
|
523
|
+
const name = spec.final.name;
|
|
524
|
+
// PRIORITY 1: Sibling deps from parent
|
|
525
|
+
// These take priority because workspace's direct deps should win over
|
|
526
|
+
// versions from other workspaces that may be in the peer context
|
|
527
|
+
const siblingEntry = queuedEntries.find(e => (e.target?.name ?? e.spec.final.name) === name);
|
|
528
|
+
const siblingTarget = siblingEntry?.target ?? fromNode.edgesOut.get(name)?.to;
|
|
529
|
+
if (siblingTarget &&
|
|
530
|
+
nodeSatisfiesSpec(siblingTarget, spec, fromNode, graph)) {
|
|
531
|
+
// Override existing edge if pointing elsewhere (sibling must win)
|
|
532
|
+
const existingEdge = node.edgesOut.get(name);
|
|
533
|
+
if (existingEdge?.to && existingEdge.to !== siblingTarget) {
|
|
534
|
+
existingEdge.to.edgesIn.delete(existingEdge);
|
|
535
|
+
existingEdge.to = siblingTarget;
|
|
536
|
+
siblingTarget.edgesIn.add(existingEdge);
|
|
324
537
|
}
|
|
325
|
-
else {
|
|
326
|
-
|
|
327
|
-
nextDeps.push(nextDep);
|
|
538
|
+
else if (!existingEdge) {
|
|
539
|
+
graph.addEdge(type, spec, node, siblingTarget);
|
|
328
540
|
}
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
// PRIORITY 2: Peer-edge closure of sibling targets
|
|
544
|
+
// Handles cycles like A->B(peer)->C(peer)->A(peer)
|
|
545
|
+
const localPeer = findFromPeerClosure(name, spec, queuedEntries, fromNode, graph);
|
|
546
|
+
if (localPeer && !node.edgesOut.has(name)) {
|
|
547
|
+
graph.addEdge(type, spec, node, localPeer);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
// PRIORITY 3: Global peer context set
|
|
551
|
+
const entry = peerContext.get(name);
|
|
552
|
+
if (!node.edgesOut.has(name) &&
|
|
553
|
+
entry?.target &&
|
|
554
|
+
nodeSatisfiesSpec(entry.target, spec, fromNode, graph)) {
|
|
555
|
+
graph.addEdge(type, spec, node, entry.target);
|
|
556
|
+
entry.specs.add(spec.final);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
// PRIORITY 4: Fallback - add to nextDeps or create dangling edge
|
|
560
|
+
if (type === 'peerOptional') {
|
|
561
|
+
// Optional peers that can't be resolved get a dangling edge
|
|
562
|
+
graph.addEdge(type, spec, node);
|
|
563
|
+
}
|
|
564
|
+
else if (siblingEntry &&
|
|
565
|
+
siblingEntry.spec.bareSpec !== spec.bareSpec) {
|
|
566
|
+
// Sibling has a more specific spec - use it for resolution
|
|
567
|
+
nextDeps.push({ ...nextDep, spec: siblingEntry.spec });
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
// Add to next deps for normal resolution in upcoming levels
|
|
571
|
+
nextDeps.push(nextDep);
|
|
329
572
|
}
|
|
330
573
|
}
|
|
331
574
|
},
|
|
@@ -336,47 +579,60 @@ export const endPeerPlacement = (peerContext, nextDeps, nextPeerDeps, graph, spe
|
|
|
336
579
|
* dependencies and track peer dependencies in their appropriate peer context
|
|
337
580
|
* sets, forking as needed and resolving peer dependencies using suitable
|
|
338
581
|
* nodes already present in the graph if possible.
|
|
582
|
+
*
|
|
583
|
+
* This is the core peer context management algorithm, executed after each
|
|
584
|
+
* BFS level. It runs in three phases:
|
|
585
|
+
*
|
|
586
|
+
* **Phase 1: Collect fork requirements**
|
|
587
|
+
* Call `putEntries()` on each child dep to add entries to peer context.
|
|
588
|
+
* Collect which children need forked contexts (due to conflicts).
|
|
589
|
+
*
|
|
590
|
+
* **Phase 2: Fork or reuse contexts**
|
|
591
|
+
* For children needing forks, try to reuse a sibling's forked context if
|
|
592
|
+
* compatible. This optimization reduces the number of peer contexts created.
|
|
593
|
+
*
|
|
594
|
+
* **Phase 3: Resolve peer deps**
|
|
595
|
+
* With contexts finalized, call `resolvePeerDeps()` to create edges for
|
|
596
|
+
* peers that can be satisfied from context/siblings, or add them to nextDeps.
|
|
597
|
+
*
|
|
598
|
+
* All operations are sorted by `node.id` for deterministic, reproducible builds.
|
|
339
599
|
*/
|
|
340
600
|
export const postPlacementPeerCheck = (graph, sortedLevelResults) => {
|
|
341
|
-
// Update peer contexts in a sorted manner after processing all nodes
|
|
342
|
-
// at a given level to ensure deterministic behavior when it comes to
|
|
343
|
-
// forking new peer contexts
|
|
344
601
|
for (const childDepsToProcess of sortedLevelResults) {
|
|
345
|
-
// Sort
|
|
602
|
+
// Sort by node.id for deterministic processing order
|
|
346
603
|
const sortedChildDeps = [...childDepsToProcess].sort((a, b) => a.node.id.localeCompare(b.node.id, 'en'));
|
|
604
|
+
// PHASE 1: Collect which children need forked contexts
|
|
347
605
|
const needsForking = new Map();
|
|
348
|
-
// first iterate on all child deps, adding entries to the current
|
|
349
|
-
// context and collect the information on which ones need forking
|
|
350
606
|
for (const childDep of sortedChildDeps) {
|
|
351
607
|
const needsFork = childDep.updateContext.putEntries();
|
|
352
608
|
if (needsFork) {
|
|
353
609
|
needsForking.set(childDep, needsFork);
|
|
354
610
|
}
|
|
355
611
|
}
|
|
356
|
-
// Sort
|
|
612
|
+
// Sort forking entries for deterministic fork order
|
|
357
613
|
const sortedNeedsForkingEntries = [
|
|
358
614
|
...needsForking.entries(),
|
|
359
615
|
].sort(([a], [b]) => a.node.id.localeCompare(b.node.id, 'en'));
|
|
360
|
-
//
|
|
361
|
-
//
|
|
616
|
+
// PHASE 2: Fork or reuse sibling contexts
|
|
617
|
+
// Track previous context for potential reuse by next sibling
|
|
362
618
|
let prevContext;
|
|
363
619
|
for (const [childDep, nextEntries] of sortedNeedsForkingEntries) {
|
|
620
|
+
// Optimization: try to reuse previous sibling's forked context
|
|
621
|
+
// if its entries are compatible with this child's entries
|
|
364
622
|
if (prevContext &&
|
|
365
623
|
!checkEntriesToPeerContext(prevContext, nextEntries)) {
|
|
366
|
-
// the context of the previous sibling can be reused
|
|
367
624
|
addEntriesToPeerContext(prevContext, nextEntries, childDep.node, graph.monorepo);
|
|
368
625
|
childDep.peerContext = prevContext;
|
|
369
626
|
continue;
|
|
370
627
|
}
|
|
628
|
+
// Can't reuse - create a new forked context
|
|
371
629
|
childDep.peerContext = forkPeerContext(graph, childDep.peerContext, nextEntries);
|
|
372
630
|
prevContext = childDep.peerContext;
|
|
373
631
|
}
|
|
374
|
-
//
|
|
375
|
-
// the context is fully set up
|
|
632
|
+
// PHASE 3: Resolve peer deps with finalized contexts
|
|
376
633
|
for (const childDep of sortedChildDeps) {
|
|
377
|
-
|
|
378
|
-
//
|
|
379
|
-
childDep.updateContext.resolvePeerDeps(childDep.peerContext);
|
|
634
|
+
childDep.updateContext.resolvePeerDeps();
|
|
635
|
+
// Re-order deps for deterministic next-level processing
|
|
380
636
|
childDep.deps = getOrderedDependencies(childDep.deps);
|
|
381
637
|
}
|
|
382
638
|
}
|