@vltpkg/graph 1.0.0-rc.14 → 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/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/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 +7 -0
- 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 +179 -63
- 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 +70 -7
- package/dist/ideal/peers.d.ts.map +1 -1
- package/dist/ideal/peers.js +350 -170
- 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 +2 -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 +1 -5
- 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 +6 -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/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/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
|
@@ -4,97 +4,189 @@ import { intersects } from '@vltpkg/semver';
|
|
|
4
4
|
import { satisfies } from '@vltpkg/satisfies';
|
|
5
5
|
import { Spec } from '@vltpkg/spec';
|
|
6
6
|
import { getDependencies, shorten } from "../dependencies.js";
|
|
7
|
-
import { getOrderedDependencies } from "./
|
|
7
|
+
import { compareByType, getOrderedDependencies } from "./sorting.js";
|
|
8
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
|
+
};
|
|
9
103
|
/**
|
|
10
104
|
* Check if an existing node's peer edges would still resolve to the same
|
|
11
105
|
* targets from a new parent's context. Returns incompatible info if any
|
|
12
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.
|
|
13
120
|
*/
|
|
14
121
|
export const checkPeerEdgesCompatible = (existingNode, fromNode, peerContext, graph) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
122
|
+
const peerDeps = existingNode.manifest?.peerDependencies;
|
|
123
|
+
// No peer deps = always compatible
|
|
124
|
+
if (!peerDeps || Object.keys(peerDeps).length === 0) {
|
|
18
125
|
return { compatible: true };
|
|
19
|
-
|
|
20
|
-
for (const [peerName,
|
|
126
|
+
}
|
|
127
|
+
for (const [peerName, peerBareSpec] of Object.entries(peerDeps)) {
|
|
21
128
|
const existingEdge = existingNode.edgesOut.get(peerName);
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
//
|
|
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?
|
|
25
142
|
const contextEntry = peerContext.get(peerName);
|
|
26
|
-
// If context has a different target for this peer, not compatible
|
|
27
143
|
if (contextEntry?.target &&
|
|
28
|
-
contextEntry.target.id !== existingEdge.to.id
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
});
|
|
34
|
-
if (satisfies(contextEntry.target.id, spec, fromNode.location, fromNode.projectRoot, graph.monorepo)) {
|
|
35
|
-
return {
|
|
36
|
-
compatible: false,
|
|
37
|
-
forkEntry: {
|
|
38
|
-
spec,
|
|
39
|
-
target: contextEntry.target,
|
|
40
|
-
type: contextEntry.type,
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
}
|
|
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;
|
|
44
149
|
}
|
|
45
|
-
//
|
|
150
|
+
// CHECK 2: Does parent already have an edge to a different version?
|
|
46
151
|
const siblingEdge = fromNode.edgesOut.get(peerName);
|
|
47
152
|
if (siblingEdge?.to && siblingEdge.to.id !== existingEdge.to.id) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
-
if (satisfies(siblingEdge.to.id, spec, fromNode.location, fromNode.projectRoot, graph.monorepo)) {
|
|
53
|
-
return {
|
|
54
|
-
compatible: false,
|
|
55
|
-
forkEntry: {
|
|
56
|
-
spec,
|
|
57
|
-
target: siblingEdge.to,
|
|
58
|
-
type: siblingEdge.type,
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
}
|
|
153
|
+
const result = buildIncompatibleResult(siblingEdge.to, peerSpec, siblingEdge.type, fromNode, graph);
|
|
154
|
+
if (result)
|
|
155
|
+
return result;
|
|
62
156
|
}
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
|
|
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) {
|
|
67
163
|
for (const depType of longDependencyTypes) {
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
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
|
+
};
|
|
98
190
|
}
|
|
99
191
|
}
|
|
100
192
|
}
|
|
@@ -108,26 +200,34 @@ export const retrievePeerContextHash = (peerContext) => {
|
|
|
108
200
|
// skips creating the initial peer context ref
|
|
109
201
|
if (!peerContext?.index)
|
|
110
202
|
return undefined;
|
|
111
|
-
return
|
|
203
|
+
return `peer.${peerContext.index}`;
|
|
112
204
|
};
|
|
113
205
|
/**
|
|
114
206
|
* Checks if a given spec is compatible with the specs already
|
|
115
207
|
* assigned to a peer context entry.
|
|
116
208
|
*
|
|
117
|
-
* 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.
|
|
118
219
|
*/
|
|
119
220
|
export const incompatibleSpecs = (spec, entry) => {
|
|
120
221
|
if (entry.specs.size > 0) {
|
|
121
222
|
for (const s_ of entry.specs) {
|
|
122
223
|
const s = s_.final;
|
|
123
224
|
if (
|
|
124
|
-
//
|
|
225
|
+
// Registry types: check semver range intersection
|
|
125
226
|
(spec.type === 'registry' &&
|
|
126
227
|
(!spec.range ||
|
|
127
228
|
!s.range ||
|
|
128
229
|
!intersects(spec.range, s.range))) ||
|
|
129
|
-
//
|
|
130
|
-
// they use the very same bareSpec value
|
|
230
|
+
// Non-registry types: require exact bareSpec match
|
|
131
231
|
(spec.type !== 'registry' && spec.bareSpec !== s.bareSpec)) {
|
|
132
232
|
return true;
|
|
133
233
|
}
|
|
@@ -139,15 +239,7 @@ export const incompatibleSpecs = (spec, entry) => {
|
|
|
139
239
|
* Sort peer context entry inputs for deterministic processing.
|
|
140
240
|
* Orders: non-peer dependencies first, then peer dependencies, alphabetically by name.
|
|
141
241
|
*/
|
|
142
|
-
export const getOrderedPeerContextEntries = (entries) => [...entries].sort(
|
|
143
|
-
const aIsPeer = a.type === 'peer' || a.type === 'peerOptional' ? 1 : 0;
|
|
144
|
-
const bIsPeer = b.type === 'peer' || b.type === 'peerOptional' ? 1 : 0;
|
|
145
|
-
if (aIsPeer !== bIsPeer)
|
|
146
|
-
return aIsPeer - bIsPeer;
|
|
147
|
-
const aName = a.target?.name ?? a.spec.name;
|
|
148
|
-
const bName = b.target?.name ?? b.spec.name;
|
|
149
|
-
return aName.localeCompare(bName, 'en');
|
|
150
|
-
});
|
|
242
|
+
export const getOrderedPeerContextEntries = (entries) => [...entries].sort(compareByType);
|
|
151
243
|
/*
|
|
152
244
|
* Checks if there are any conflicting versions for a given dependency
|
|
153
245
|
* to be added to a peer context set which will require forking.
|
|
@@ -178,17 +270,13 @@ export const checkEntriesToPeerContext = (peerContext, entries) => {
|
|
|
178
270
|
* Returns true if forking is needed, false otherwise.
|
|
179
271
|
*/
|
|
180
272
|
export const addEntriesToPeerContext = (peerContext, entries, fromNode, monorepo) => {
|
|
181
|
-
// pre check
|
|
182
|
-
// provided peer context set conflicts with existing ones
|
|
183
|
-
// if that's already the case we can skip processing them and
|
|
184
|
-
// will return that a fork is needed right away
|
|
273
|
+
// pre check for conflicts before processing
|
|
185
274
|
if (checkEntriesToPeerContext(peerContext, entries))
|
|
186
275
|
return true;
|
|
187
|
-
// iterate on every entry to be added to the peer context set
|
|
188
276
|
for (const { dependent, spec, target, type } of entries) {
|
|
189
277
|
const name = target?.name ?? spec.final.name;
|
|
190
|
-
// if there's no existing entry, create one
|
|
191
278
|
let entry = peerContext.get(name);
|
|
279
|
+
// create new entry if none exists
|
|
192
280
|
if (!entry) {
|
|
193
281
|
entry = {
|
|
194
282
|
active: true,
|
|
@@ -202,20 +290,17 @@ export const addEntriesToPeerContext = (peerContext, entries, fromNode, monorepo
|
|
|
202
290
|
entry.contextDependents.add(dependent);
|
|
203
291
|
continue;
|
|
204
292
|
}
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
// case of adding sibling deps that conflicts with one another
|
|
208
|
-
if (incompatibleSpecs(spec.final, entry)) {
|
|
293
|
+
// check for sibling dep conflicts
|
|
294
|
+
if (incompatibleSpecs(spec.final, entry))
|
|
209
295
|
return true;
|
|
210
|
-
|
|
296
|
+
// update target if compatible with all specs
|
|
211
297
|
if (target &&
|
|
212
298
|
[...entry.specs].every(s => satisfies(target.id, s, fromNode.location, fromNode.projectRoot, monorepo))) {
|
|
213
299
|
if (target.id !== entry.target?.id &&
|
|
214
300
|
target.version !== entry.target?.version) {
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const edge = dependents.edgesOut.get(name);
|
|
301
|
+
// update dependents to point to new target
|
|
302
|
+
for (const dep of entry.contextDependents) {
|
|
303
|
+
const edge = dep.edgesOut.get(name);
|
|
219
304
|
if (edge?.to && edge.to !== target) {
|
|
220
305
|
edge.to.edgesIn.delete(edge);
|
|
221
306
|
edge.to = target;
|
|
@@ -224,10 +309,8 @@ export const addEntriesToPeerContext = (peerContext, entries, fromNode, monorepo
|
|
|
224
309
|
}
|
|
225
310
|
entry.target = target;
|
|
226
311
|
}
|
|
227
|
-
// otherwise sets the value in case it was nullish
|
|
228
312
|
entry.target ??= target;
|
|
229
313
|
}
|
|
230
|
-
// update specs and dependents values
|
|
231
314
|
entry.specs.add(spec);
|
|
232
315
|
if (dependent)
|
|
233
316
|
entry.contextDependents.add(dependent);
|
|
@@ -238,11 +321,17 @@ export const addEntriesToPeerContext = (peerContext, entries, fromNode, monorepo
|
|
|
238
321
|
* Create and returns a forked copy of a given peer context set.
|
|
239
322
|
*/
|
|
240
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
|
+
}
|
|
241
329
|
// create a new peer context set
|
|
242
330
|
const nextPeerContext = new Map();
|
|
243
331
|
nextPeerContext.index = graph.nextPeerContextIndex();
|
|
244
332
|
// register it in the graph
|
|
245
333
|
graph.peerContexts[nextPeerContext.index] = nextPeerContext;
|
|
334
|
+
graph.peerContextForkCache.set(forkKey, nextPeerContext);
|
|
246
335
|
// copy existing entries marking them as inactive, it's also important
|
|
247
336
|
// to note that specs and contextDependents are new objects so that changes
|
|
248
337
|
// to those in the new context do not affect the previous one
|
|
@@ -271,6 +360,51 @@ export const forkPeerContext = (graph, peerContext, entries) => {
|
|
|
271
360
|
}
|
|
272
361
|
return nextPeerContext;
|
|
273
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
|
+
};
|
|
274
408
|
/**
|
|
275
409
|
* Starts the peer dependency placement process
|
|
276
410
|
* for a given node being processed and placed.
|
|
@@ -317,14 +451,28 @@ export const startPeerPlacement = (peerContext, manifest, fromNode, options) =>
|
|
|
317
451
|
* Ends the peer dependency placement process, returning the functions that
|
|
318
452
|
* are going to be used to update the peer context set, forking when needed
|
|
319
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.
|
|
320
461
|
*/
|
|
321
462
|
export const endPeerPlacement = (peerContext, nextDeps, nextPeerDeps, graph, spec, fromNode, node, type, queuedEntries) => ({
|
|
322
463
|
/**
|
|
323
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).
|
|
324
474
|
*/
|
|
325
475
|
putEntries: () => {
|
|
326
|
-
// keep track of whether we need to fork the current peer context set
|
|
327
|
-
let needsToForkPeerContext = false;
|
|
328
476
|
// add queued entries from this node parents along
|
|
329
477
|
// with a self-ref to the current peer context set
|
|
330
478
|
const prevEntries = [
|
|
@@ -335,8 +483,6 @@ export const endPeerPlacement = (peerContext, nextDeps, nextPeerDeps, graph, spe
|
|
|
335
483
|
type,
|
|
336
484
|
},
|
|
337
485
|
];
|
|
338
|
-
addEntriesToPeerContext(peerContext, prevEntries, fromNode, graph.monorepo);
|
|
339
|
-
// add this node's direct dependencies next
|
|
340
486
|
const nextEntries = [
|
|
341
487
|
...nextDeps.map(dep => ({ ...dep, dependent: node })),
|
|
342
488
|
...[...nextPeerDeps.values()].map(dep => ({
|
|
@@ -344,66 +490,85 @@ export const endPeerPlacement = (peerContext, nextDeps, nextPeerDeps, graph, spe
|
|
|
344
490
|
dependent: node,
|
|
345
491
|
})),
|
|
346
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);
|
|
347
502
|
if (nextEntries.length > 0) {
|
|
348
|
-
|
|
503
|
+
addEntriesToPeerContext(peerContext, nextEntries, node, graph.monorepo);
|
|
349
504
|
}
|
|
350
|
-
|
|
351
|
-
// context or undefined if the current context was updated directly
|
|
352
|
-
return needsToForkPeerContext ? nextEntries : undefined;
|
|
505
|
+
return undefined;
|
|
353
506
|
},
|
|
354
507
|
/**
|
|
355
508
|
* Try to resolve peer dependencies using already seen target
|
|
356
509
|
* values from the current peer context set.
|
|
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)
|
|
357
516
|
*/
|
|
358
517
|
resolvePeerDeps: () => {
|
|
359
|
-
// iterate on the set of peer dependencies of the current node
|
|
360
|
-
// and try to resolve them from the existing peer context set,
|
|
361
|
-
// when possible, add them as edges in the graph right away, if not,
|
|
362
|
-
// then we move them back to the `nextDeps` list for processing
|
|
363
|
-
// along with the rest of the regular dependencies
|
|
364
518
|
for (const nextDep of nextPeerDeps.values()) {
|
|
365
519
|
const { spec, type } = nextDep;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// the current peer context set
|
|
384
|
-
const entry = peerContext.get(spec.final.name);
|
|
385
|
-
if (!node.edgesOut.has(spec.final.name) &&
|
|
386
|
-
entry?.target &&
|
|
387
|
-
satisfies(entry.target.id, spec, fromNode.location, fromNode.projectRoot, graph.monorepo)) {
|
|
388
|
-
// entry satisfied, create edge in the graph
|
|
389
|
-
graph.addEdge(type, spec, node, entry.target);
|
|
390
|
-
entry.specs.add(spec.final);
|
|
391
|
-
}
|
|
392
|
-
else if (type === 'peerOptional') {
|
|
393
|
-
// skip unsatisfied peerOptional dependencies,
|
|
394
|
-
// just create a dangling edge
|
|
395
|
-
graph.addEdge(type, spec, node);
|
|
396
|
-
}
|
|
397
|
-
else if (siblingEntry &&
|
|
398
|
-
siblingEntry.spec.bareSpec !== spec.bareSpec) {
|
|
399
|
-
// Sibling has a more specific spec for this package,
|
|
400
|
-
// use it when resolving to ensure we get the right version
|
|
401
|
-
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);
|
|
402
537
|
}
|
|
403
|
-
else {
|
|
404
|
-
|
|
405
|
-
nextDeps.push(nextDep);
|
|
538
|
+
else if (!existingEdge) {
|
|
539
|
+
graph.addEdge(type, spec, node, siblingTarget);
|
|
406
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);
|
|
407
572
|
}
|
|
408
573
|
}
|
|
409
574
|
},
|
|
@@ -414,45 +579,60 @@ export const endPeerPlacement = (peerContext, nextDeps, nextPeerDeps, graph, spe
|
|
|
414
579
|
* dependencies and track peer dependencies in their appropriate peer context
|
|
415
580
|
* sets, forking as needed and resolving peer dependencies using suitable
|
|
416
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.
|
|
417
599
|
*/
|
|
418
600
|
export const postPlacementPeerCheck = (graph, sortedLevelResults) => {
|
|
419
|
-
// Update peer contexts in a sorted manner after processing all nodes
|
|
420
|
-
// at a given level to ensure deterministic behavior when it comes to
|
|
421
|
-
// forking new peer contexts
|
|
422
601
|
for (const childDepsToProcess of sortedLevelResults) {
|
|
423
|
-
// Sort
|
|
602
|
+
// Sort by node.id for deterministic processing order
|
|
424
603
|
const sortedChildDeps = [...childDepsToProcess].sort((a, b) => a.node.id.localeCompare(b.node.id, 'en'));
|
|
604
|
+
// PHASE 1: Collect which children need forked contexts
|
|
425
605
|
const needsForking = new Map();
|
|
426
|
-
// first iterate on all child deps, adding entries to the current
|
|
427
|
-
// context and collect the information on which ones need forking
|
|
428
606
|
for (const childDep of sortedChildDeps) {
|
|
429
607
|
const needsFork = childDep.updateContext.putEntries();
|
|
430
608
|
if (needsFork) {
|
|
431
609
|
needsForking.set(childDep, needsFork);
|
|
432
610
|
}
|
|
433
611
|
}
|
|
434
|
-
// Sort
|
|
612
|
+
// Sort forking entries for deterministic fork order
|
|
435
613
|
const sortedNeedsForkingEntries = [
|
|
436
614
|
...needsForking.entries(),
|
|
437
615
|
].sort(([a], [b]) => a.node.id.localeCompare(b.node.id, 'en'));
|
|
438
|
-
//
|
|
439
|
-
//
|
|
616
|
+
// PHASE 2: Fork or reuse sibling contexts
|
|
617
|
+
// Track previous context for potential reuse by next sibling
|
|
440
618
|
let prevContext;
|
|
441
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
|
|
442
622
|
if (prevContext &&
|
|
443
623
|
!checkEntriesToPeerContext(prevContext, nextEntries)) {
|
|
444
|
-
// the context of the previous sibling can be reused
|
|
445
624
|
addEntriesToPeerContext(prevContext, nextEntries, childDep.node, graph.monorepo);
|
|
446
625
|
childDep.peerContext = prevContext;
|
|
447
626
|
continue;
|
|
448
627
|
}
|
|
628
|
+
// Can't reuse - create a new forked context
|
|
449
629
|
childDep.peerContext = forkPeerContext(graph, childDep.peerContext, nextEntries);
|
|
450
630
|
prevContext = childDep.peerContext;
|
|
451
631
|
}
|
|
452
|
-
//
|
|
453
|
-
// the context is fully set up
|
|
632
|
+
// PHASE 3: Resolve peer deps with finalized contexts
|
|
454
633
|
for (const childDep of sortedChildDeps) {
|
|
455
634
|
childDep.updateContext.resolvePeerDeps();
|
|
635
|
+
// Re-order deps for deterministic next-level processing
|
|
456
636
|
childDep.deps = getOrderedDependencies(childDep.deps);
|
|
457
637
|
}
|
|
458
638
|
}
|