@npmcli/arborist 2.6.4 → 2.8.1
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/bin/dedupe.js +46 -0
- package/lib/arborist/build-ideal-tree.js +118 -704
- package/lib/arborist/index.js +1 -1
- package/lib/arborist/load-actual.js +1 -1
- package/lib/arborist/load-virtual.js +1 -1
- package/lib/arborist/rebuild.js +3 -3
- package/lib/arborist/reify.js +128 -41
- package/lib/calc-dep-flags.js +3 -3
- package/lib/can-place-dep.js +402 -0
- package/lib/deepest-nesting-target.js +16 -0
- package/lib/diff.js +3 -5
- package/lib/edge.js +2 -0
- package/lib/node.js +40 -6
- package/lib/peer-entry-sets.js +72 -0
- package/lib/place-dep.js +551 -0
- package/lib/printable.js +10 -0
- package/lib/shrinkwrap.js +12 -8
- package/package.json +7 -4
- package/lib/peer-set.js +0 -25
package/lib/place-dep.js
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
// Given a dep, a node that depends on it, and the edge representing that
|
|
2
|
+
// dependency, place the dep somewhere in the node's tree, and all of its
|
|
3
|
+
// peer dependencies.
|
|
4
|
+
//
|
|
5
|
+
// Handles all of the tree updating needed to place the dep, including
|
|
6
|
+
// removing replaced nodes, pruning now-extraneous or invalidated nodes,
|
|
7
|
+
// and saves a set of what was placed and what needs re-evaluation as
|
|
8
|
+
// a result.
|
|
9
|
+
|
|
10
|
+
const log = require('proc-log')
|
|
11
|
+
const deepestNestingTarget = require('./deepest-nesting-target.js')
|
|
12
|
+
const CanPlaceDep = require('./can-place-dep.js')
|
|
13
|
+
const {
|
|
14
|
+
KEEP,
|
|
15
|
+
CONFLICT,
|
|
16
|
+
} = CanPlaceDep
|
|
17
|
+
const debug = require('./debug.js')
|
|
18
|
+
|
|
19
|
+
const Link = require('./link.js')
|
|
20
|
+
const gatherDepSet = require('./gather-dep-set.js')
|
|
21
|
+
const peerEntrySets = require('./peer-entry-sets.js')
|
|
22
|
+
|
|
23
|
+
class PlaceDep {
|
|
24
|
+
constructor (options) {
|
|
25
|
+
const {
|
|
26
|
+
dep,
|
|
27
|
+
edge,
|
|
28
|
+
parent = null,
|
|
29
|
+
} = options
|
|
30
|
+
this.name = edge.name
|
|
31
|
+
this.dep = dep
|
|
32
|
+
this.edge = edge
|
|
33
|
+
this.canPlace = null
|
|
34
|
+
|
|
35
|
+
this.target = null
|
|
36
|
+
this.placed = null
|
|
37
|
+
|
|
38
|
+
// inherit all these fields from the parent to ensure consistency.
|
|
39
|
+
const {
|
|
40
|
+
preferDedupe,
|
|
41
|
+
force,
|
|
42
|
+
explicitRequest,
|
|
43
|
+
updateNames,
|
|
44
|
+
auditReport,
|
|
45
|
+
legacyBundling,
|
|
46
|
+
strictPeerDeps,
|
|
47
|
+
legacyPeerDeps,
|
|
48
|
+
globalStyle,
|
|
49
|
+
} = parent || options
|
|
50
|
+
Object.assign(this, {
|
|
51
|
+
preferDedupe,
|
|
52
|
+
force,
|
|
53
|
+
explicitRequest,
|
|
54
|
+
updateNames,
|
|
55
|
+
auditReport,
|
|
56
|
+
legacyBundling,
|
|
57
|
+
strictPeerDeps,
|
|
58
|
+
legacyPeerDeps,
|
|
59
|
+
globalStyle,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
this.children = []
|
|
63
|
+
this.parent = parent
|
|
64
|
+
this.peerConflict = null
|
|
65
|
+
|
|
66
|
+
this.checks = new Map()
|
|
67
|
+
|
|
68
|
+
this.place()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
place () {
|
|
72
|
+
const {
|
|
73
|
+
edge,
|
|
74
|
+
dep,
|
|
75
|
+
preferDedupe,
|
|
76
|
+
globalStyle,
|
|
77
|
+
legacyBundling,
|
|
78
|
+
explicitRequest,
|
|
79
|
+
updateNames,
|
|
80
|
+
checks,
|
|
81
|
+
} = this
|
|
82
|
+
|
|
83
|
+
// nothing to do if the edge is fine as it is
|
|
84
|
+
if (edge.to &&
|
|
85
|
+
!edge.error &&
|
|
86
|
+
!explicitRequest &&
|
|
87
|
+
!updateNames.includes(edge.name) &&
|
|
88
|
+
!this.isVulnerable(edge.to))
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
// walk up the tree until we hit either a top/root node, or a place
|
|
92
|
+
// where the dep is not a peer dep.
|
|
93
|
+
const start = this.getStartNode()
|
|
94
|
+
|
|
95
|
+
let canPlace = null
|
|
96
|
+
let canPlaceSelf = null
|
|
97
|
+
for (const target of start.ancestry()) {
|
|
98
|
+
// if the current location has a peerDep on it, then we can't place here
|
|
99
|
+
// this is pretty rare to hit, since we always prefer deduping peers,
|
|
100
|
+
// and the getStartNode will start us out above any peers from the
|
|
101
|
+
// thing that depends on it. but we could hit it with something like:
|
|
102
|
+
//
|
|
103
|
+
// a -> (b@1, c@1)
|
|
104
|
+
// +-- c@1
|
|
105
|
+
// +-- b -> PEEROPTIONAL(v) (c@2)
|
|
106
|
+
// +-- c@2 -> (v)
|
|
107
|
+
//
|
|
108
|
+
// So we check if we can place v under c@2, that's fine.
|
|
109
|
+
// Then we check under b, and can't, because of the optional peer dep.
|
|
110
|
+
// but we CAN place it under a, so the correct thing to do is keep
|
|
111
|
+
// walking up the tree.
|
|
112
|
+
const targetEdge = target.edgesOut.get(edge.name)
|
|
113
|
+
if (!target.isTop && targetEdge && targetEdge.peer)
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
const cpd = new CanPlaceDep({
|
|
117
|
+
dep,
|
|
118
|
+
edge,
|
|
119
|
+
// note: this sets the parent's canPlace as the parent of this
|
|
120
|
+
// canPlace, but it does NOT add this canPlace to the parent's
|
|
121
|
+
// children. This way, we can know that it's a peer dep, and
|
|
122
|
+
// get the top edge easily, while still maintaining the
|
|
123
|
+
// tree of checks that factored into the original decision.
|
|
124
|
+
parent: this.parent && this.parent.canPlace,
|
|
125
|
+
target,
|
|
126
|
+
preferDedupe,
|
|
127
|
+
explicitRequest: this.explicitRequest,
|
|
128
|
+
})
|
|
129
|
+
checks.set(target, cpd)
|
|
130
|
+
|
|
131
|
+
// It's possible that a "conflict" is a conflict among the *peers* of
|
|
132
|
+
// a given node we're trying to place, but there actually is no current
|
|
133
|
+
// node. Eg,
|
|
134
|
+
// root -> (a, b)
|
|
135
|
+
// a -> PEER(c)
|
|
136
|
+
// b -> PEER(d)
|
|
137
|
+
// d -> PEER(c@2)
|
|
138
|
+
// We place (a), and get a peer of (c) along with it.
|
|
139
|
+
// then we try to place (b), and get CONFLICT in the check, because
|
|
140
|
+
// of the conflicting peer from (b)->(d)->(c@2). In that case, we
|
|
141
|
+
// should treat (b) and (d) as OK, and place them in the last place
|
|
142
|
+
// where they did not themselves conflict, and skip c@2 if conflict
|
|
143
|
+
// is ok by virtue of being forced or not ours and not strict.
|
|
144
|
+
if (cpd.canPlaceSelf !== CONFLICT)
|
|
145
|
+
canPlaceSelf = cpd
|
|
146
|
+
|
|
147
|
+
// we found a place this can go, along with all its peer friends.
|
|
148
|
+
// we break when we get the first conflict
|
|
149
|
+
if (cpd.canPlace !== CONFLICT)
|
|
150
|
+
canPlace = cpd
|
|
151
|
+
else
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
// if it's a load failure, just plop it in the first place attempted,
|
|
155
|
+
// since we're going to crash the build or prune it out anyway.
|
|
156
|
+
// but, this will frequently NOT be a successful canPlace, because
|
|
157
|
+
// it'll have no version or other information.
|
|
158
|
+
if (dep.errors.length)
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
// nest packages like npm v1 and v2
|
|
162
|
+
// very disk-inefficient
|
|
163
|
+
if (legacyBundling)
|
|
164
|
+
break
|
|
165
|
+
|
|
166
|
+
// when installing globally, or just in global style, we never place
|
|
167
|
+
// deps above the first level.
|
|
168
|
+
if (globalStyle) {
|
|
169
|
+
const rp = target.resolveParent
|
|
170
|
+
if (rp && rp.isProjectRoot)
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
Object.assign(this, {
|
|
176
|
+
canPlace,
|
|
177
|
+
canPlaceSelf,
|
|
178
|
+
})
|
|
179
|
+
this.current = edge.to
|
|
180
|
+
|
|
181
|
+
// if we can't find a target, that means that the last place checked,
|
|
182
|
+
// and all the places before it, had a conflict.
|
|
183
|
+
if (!canPlace) {
|
|
184
|
+
// if not forced, or it's our dep, or strictPeerDeps is set, then
|
|
185
|
+
// this is an ERESOLVE error.
|
|
186
|
+
if (!this.conflictOk)
|
|
187
|
+
return this.failPeerConflict()
|
|
188
|
+
|
|
189
|
+
// ok! we're gonna allow the conflict, but we should still warn
|
|
190
|
+
// if we have a current, then we treat CONFLICT as a KEEP.
|
|
191
|
+
// otherwise, we just skip it. Only warn on the one that actually
|
|
192
|
+
// could not be placed somewhere.
|
|
193
|
+
if (!canPlaceSelf) {
|
|
194
|
+
this.warnPeerConflict()
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.canPlace = canPlaceSelf
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// now we have a target, a tree of CanPlaceDep results for the peer group,
|
|
202
|
+
// and we are ready to go
|
|
203
|
+
this.placeInTree()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
placeInTree () {
|
|
207
|
+
const {
|
|
208
|
+
dep,
|
|
209
|
+
canPlace,
|
|
210
|
+
edge,
|
|
211
|
+
} = this
|
|
212
|
+
|
|
213
|
+
/* istanbul ignore next */
|
|
214
|
+
if (!canPlace) {
|
|
215
|
+
debug(() => {
|
|
216
|
+
throw new Error('canPlace not set, but trying to place in tree')
|
|
217
|
+
})
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const { target } = canPlace
|
|
222
|
+
|
|
223
|
+
log.silly(
|
|
224
|
+
'placeDep',
|
|
225
|
+
target.location || 'ROOT',
|
|
226
|
+
`${dep.name}@${dep.version}`,
|
|
227
|
+
canPlace.description,
|
|
228
|
+
`for: ${this.edge.from.package._id || this.edge.from.location}`,
|
|
229
|
+
`want: ${edge.spec || '*'}`
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
const placementType = canPlace.canPlace === CONFLICT
|
|
233
|
+
? canPlace.canPlaceSelf
|
|
234
|
+
: canPlace.canPlace
|
|
235
|
+
|
|
236
|
+
// if we're placing in the tree with --force, we can get here even though
|
|
237
|
+
// it's a conflict. Treat it as a KEEP, but warn and move on.
|
|
238
|
+
if (placementType === KEEP) {
|
|
239
|
+
// this was an overridden peer dep
|
|
240
|
+
if (edge.peer && !edge.valid)
|
|
241
|
+
this.warnPeerConflict()
|
|
242
|
+
|
|
243
|
+
// if we get a KEEP in a update scenario, then we MAY have something
|
|
244
|
+
// already duplicating this unnecessarily! For example:
|
|
245
|
+
// ```
|
|
246
|
+
// root (dep: y@1)
|
|
247
|
+
// +-- x (dep: y@1.1)
|
|
248
|
+
// | +-- y@1.1.0 (replacing with 1.1.2, got KEEP at the root)
|
|
249
|
+
// +-- y@1.1.2 (updated already from 1.0.0)
|
|
250
|
+
// ```
|
|
251
|
+
// Now say we do `reify({update:['y']})`, and the latest version is
|
|
252
|
+
// 1.1.2, which we now have in the root. We'll try to place y@1.1.2
|
|
253
|
+
// first in x, then in the root, ending with KEEP, because we already
|
|
254
|
+
// have it. In that case, we ought to REMOVE the nm/x/nm/y node, because
|
|
255
|
+
// it is an unnecessary duplicate.
|
|
256
|
+
this.pruneDedupable(target)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// we were told to place it here in the target, so either it does not
|
|
261
|
+
// already exist in the tree, OR it's shadowed.
|
|
262
|
+
// handle otherwise unresolvable dependency nesting loops by
|
|
263
|
+
// creating a symbolic link
|
|
264
|
+
// a1 -> b1 -> a2 -> b2 -> a1 -> ...
|
|
265
|
+
// instead of nesting forever, when the loop occurs, create
|
|
266
|
+
// a symbolic link to the earlier instance
|
|
267
|
+
for (let p = target; p; p = p.resolveParent) {
|
|
268
|
+
if (p.matches(dep) && !p.isTop) {
|
|
269
|
+
this.placed = new Link({ parent: target, target: p })
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// XXX if we are replacing SOME of a peer entry group, we will need to
|
|
275
|
+
// remove any that are not being replaced and will now be invalid, and
|
|
276
|
+
// re-evaluate them deeper into the tree.
|
|
277
|
+
|
|
278
|
+
const virtualRoot = dep.parent
|
|
279
|
+
this.placed = new dep.constructor({
|
|
280
|
+
name: dep.name,
|
|
281
|
+
pkg: dep.package,
|
|
282
|
+
resolved: dep.resolved,
|
|
283
|
+
integrity: dep.integrity,
|
|
284
|
+
legacyPeerDeps: this.legacyPeerDeps,
|
|
285
|
+
error: dep.errors[0],
|
|
286
|
+
...(dep.isLink ? { target: dep.target, realpath: dep.realpath } : {}),
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
this.oldDep = target.children.get(this.name)
|
|
290
|
+
if (this.oldDep)
|
|
291
|
+
this.replaceOldDep()
|
|
292
|
+
else
|
|
293
|
+
this.placed.parent = target
|
|
294
|
+
|
|
295
|
+
// if it's an overridden peer dep, warn about it
|
|
296
|
+
if (edge.peer && !this.placed.satisfies(edge))
|
|
297
|
+
this.warnPeerConflict()
|
|
298
|
+
|
|
299
|
+
// If the edge is not an error, then we're updating something, and
|
|
300
|
+
// MAY end up putting a better/identical node further up the tree in
|
|
301
|
+
// a way that causes an unnecessary duplication. If so, remove the
|
|
302
|
+
// now-unnecessary node.
|
|
303
|
+
if (edge.valid && edge.to && edge.to !== this.placed)
|
|
304
|
+
this.pruneDedupable(edge.to, false)
|
|
305
|
+
|
|
306
|
+
// in case we just made some duplicates that can be removed,
|
|
307
|
+
// prune anything deeper in the tree that can be replaced by this
|
|
308
|
+
for (const node of target.root.inventory.query('name', this.name)) {
|
|
309
|
+
if (node.isDescendantOf(target) && !node.isTop) {
|
|
310
|
+
this.pruneDedupable(node, false)
|
|
311
|
+
// only walk the direct children of the ones we kept
|
|
312
|
+
if (node.root === target.root) {
|
|
313
|
+
for (const kid of node.children.values())
|
|
314
|
+
this.pruneDedupable(kid, false)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// also place its unmet or invalid peer deps at this location
|
|
320
|
+
// loop through any peer deps from the thing we just placed, and place
|
|
321
|
+
// those ones as well. it's safe to do this with the virtual nodes,
|
|
322
|
+
// because we're copying rather than moving them out of the virtual root,
|
|
323
|
+
// otherwise they'd be gone and the peer set would change throughout
|
|
324
|
+
// this loop.
|
|
325
|
+
for (const peerEdge of this.placed.edgesOut.values()) {
|
|
326
|
+
if (peerEdge.valid || !peerEdge.peer || peerEdge.overridden)
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
const peer = virtualRoot.children.get(peerEdge.name)
|
|
330
|
+
|
|
331
|
+
// Note: if the virtualRoot *doesn't* have the peer, then that means
|
|
332
|
+
// it's an optional peer dep. If it's not being properly met (ie,
|
|
333
|
+
// peerEdge.valid is false), then this is likely heading for an
|
|
334
|
+
// ERESOLVE error, unless it can walk further up the tree.
|
|
335
|
+
if (!peer)
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
// overridden peerEdge, just accept what's there already
|
|
339
|
+
if (!peer.satisfies(peerEdge))
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
this.children.push(new PlaceDep({
|
|
343
|
+
parent: this,
|
|
344
|
+
dep: peer,
|
|
345
|
+
node: this.placed,
|
|
346
|
+
edge: peerEdge,
|
|
347
|
+
}))
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
replaceOldDep () {
|
|
352
|
+
// XXX handle replacing an entire peer group?
|
|
353
|
+
// what about cases where we need to push some other peer groups deeper
|
|
354
|
+
// into the tree? all the tree updating should be done here, and track
|
|
355
|
+
// all the things that we add and remove, so that we can know what
|
|
356
|
+
// to re-evaluate.
|
|
357
|
+
|
|
358
|
+
// if we're replacing, we should also remove any nodes for edges that
|
|
359
|
+
// are now invalid, and where this (or its deps) is the only dependent,
|
|
360
|
+
// and also recurse on that pruning. Otherwise leaving that dep node
|
|
361
|
+
// around can result in spurious conflicts pushing nodes deeper into
|
|
362
|
+
// the tree than needed in the case of cycles that will be removed
|
|
363
|
+
// later anyway.
|
|
364
|
+
const oldDeps = []
|
|
365
|
+
for (const [name, edge] of this.oldDep.edgesOut.entries()) {
|
|
366
|
+
if (!this.placed.edgesOut.has(name) && edge.to)
|
|
367
|
+
oldDeps.push(...gatherDepSet([edge.to], e => e.to !== edge.to))
|
|
368
|
+
}
|
|
369
|
+
this.placed.replace(this.oldDep)
|
|
370
|
+
this.pruneForReplacement(this.placed, oldDeps)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
pruneForReplacement (node, oldDeps) {
|
|
374
|
+
// gather up all the now-invalid/extraneous edgesOut, as long as they are
|
|
375
|
+
// only depended upon by the old node/deps
|
|
376
|
+
const invalidDeps = new Set([...node.edgesOut.values()]
|
|
377
|
+
.filter(e => e.to && !e.valid).map(e => e.to))
|
|
378
|
+
for (const dep of oldDeps) {
|
|
379
|
+
const set = gatherDepSet([dep], e => e.to !== dep && e.valid)
|
|
380
|
+
for (const dep of set)
|
|
381
|
+
invalidDeps.add(dep)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ignore dependency edges from the node being replaced, but
|
|
385
|
+
// otherwise filter the set down to just the set with no
|
|
386
|
+
// dependencies from outside the set, except the node in question.
|
|
387
|
+
const deps = gatherDepSet(invalidDeps, edge =>
|
|
388
|
+
edge.from !== node && edge.to !== node && edge.valid)
|
|
389
|
+
|
|
390
|
+
// now just delete whatever's left, because it's junk
|
|
391
|
+
for (const dep of deps)
|
|
392
|
+
dep.root = null
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// prune all the nodes in a branch of the tree that can be safely removed
|
|
396
|
+
// This is only the most basic duplication detection; it finds if there
|
|
397
|
+
// is another satisfying node further up the tree, and if so, dedupes.
|
|
398
|
+
// Even in legacyBundling mode, we do this amount of deduplication.
|
|
399
|
+
pruneDedupable (node, descend = true) {
|
|
400
|
+
if (node.canDedupe(this.preferDedupe)) {
|
|
401
|
+
// gather up all deps that have no valid edges in from outside
|
|
402
|
+
// the dep set, except for this node we're deduping, so that we
|
|
403
|
+
// also prune deps that would be made extraneous.
|
|
404
|
+
const deps = gatherDepSet([node], e => e.to !== node && e.valid)
|
|
405
|
+
for (const node of deps)
|
|
406
|
+
node.root = null
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
if (descend) {
|
|
410
|
+
// sort these so that they're deterministically ordered
|
|
411
|
+
// otherwise, resulting tree shape is dependent on the order
|
|
412
|
+
// in which they happened to be resolved.
|
|
413
|
+
const nodeSort = (a, b) => a.location.localeCompare(b.location, 'en')
|
|
414
|
+
|
|
415
|
+
const children = [...node.children.values()].sort(nodeSort)
|
|
416
|
+
for (const child of children)
|
|
417
|
+
this.pruneDedupable(child)
|
|
418
|
+
const fsChildren = [...node.fsChildren].sort(nodeSort)
|
|
419
|
+
for (const topNode of fsChildren) {
|
|
420
|
+
const children = [...topNode.children.values()].sort(nodeSort)
|
|
421
|
+
for (const child of children)
|
|
422
|
+
this.pruneDedupable(child)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
get conflictOk () {
|
|
428
|
+
return this.force || (!this.isMine && !this.strictPeerDeps)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
get isMine () {
|
|
432
|
+
const { edge } = this.top
|
|
433
|
+
const { from: node } = edge
|
|
434
|
+
|
|
435
|
+
if (node.isWorkspace || node.isProjectRoot)
|
|
436
|
+
return true
|
|
437
|
+
|
|
438
|
+
if (!edge.peer)
|
|
439
|
+
return false
|
|
440
|
+
|
|
441
|
+
// re-entry case. check if any non-peer edges come from the project,
|
|
442
|
+
// or any entryEdges on peer groups are from the root.
|
|
443
|
+
let hasPeerEdges = false
|
|
444
|
+
for (const edge of node.edgesIn) {
|
|
445
|
+
if (edge.peer) {
|
|
446
|
+
hasPeerEdges = true
|
|
447
|
+
continue
|
|
448
|
+
}
|
|
449
|
+
if (edge.from.isWorkspace || edge.from.isProjectRoot)
|
|
450
|
+
return true
|
|
451
|
+
}
|
|
452
|
+
if (hasPeerEdges) {
|
|
453
|
+
for (const edge of peerEntrySets(node).keys()) {
|
|
454
|
+
if (edge.from.isWorkspace || edge.from.isProjectRoot)
|
|
455
|
+
return true
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return false
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
warnPeerConflict () {
|
|
463
|
+
this.edge.overridden = true
|
|
464
|
+
const expl = this.explainPeerConflict()
|
|
465
|
+
log.warn('ERESOLVE', 'overriding peer dependency', expl)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
failPeerConflict () {
|
|
469
|
+
const expl = this.explainPeerConflict()
|
|
470
|
+
throw Object.assign(new Error('could not resolve'), expl)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
explainPeerConflict () {
|
|
474
|
+
const { edge, dep } = this.top
|
|
475
|
+
const { from: node } = edge
|
|
476
|
+
const curNode = node.resolve(edge.name)
|
|
477
|
+
|
|
478
|
+
const expl = {
|
|
479
|
+
code: 'ERESOLVE',
|
|
480
|
+
edge: edge.explain(),
|
|
481
|
+
dep: dep.explain(edge),
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (this.parent) {
|
|
485
|
+
// this is the conflicted peer
|
|
486
|
+
expl.current = curNode && curNode.explain(edge)
|
|
487
|
+
expl.peerConflict = this.current && this.current.explain(this.edge)
|
|
488
|
+
} else {
|
|
489
|
+
expl.current = curNode && curNode.explain()
|
|
490
|
+
if (this.canPlaceSelf && this.canPlaceSelf.canPlaceSelf !== CONFLICT) {
|
|
491
|
+
// failed while checking for a child dep
|
|
492
|
+
const cps = this.canPlaceSelf
|
|
493
|
+
for (const peer of cps.conflictChildren) {
|
|
494
|
+
if (peer.current) {
|
|
495
|
+
expl.peerConflict = {
|
|
496
|
+
current: peer.current.explain(),
|
|
497
|
+
peer: peer.dep.explain(peer.edge),
|
|
498
|
+
}
|
|
499
|
+
break
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
expl.peerConflict = {
|
|
504
|
+
current: this.current && this.current.explain(),
|
|
505
|
+
peer: this.dep.explain(this.edge),
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const {
|
|
511
|
+
strictPeerDeps,
|
|
512
|
+
force,
|
|
513
|
+
isMine,
|
|
514
|
+
} = this
|
|
515
|
+
Object.assign(expl, {
|
|
516
|
+
strictPeerDeps,
|
|
517
|
+
force,
|
|
518
|
+
isMine,
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
// XXX decorate more with this.canPlace and this.canPlaceSelf,
|
|
522
|
+
// this.checks, this.children, walk over conflicted peers, etc.
|
|
523
|
+
return expl
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
getStartNode () {
|
|
527
|
+
// if we are a peer, then we MUST be at least as shallow as the
|
|
528
|
+
// peer dependent
|
|
529
|
+
const from = this.parent ? this.parent.getStartNode() : this.edge.from
|
|
530
|
+
return deepestNestingTarget(from, this.name)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
get top () {
|
|
534
|
+
return this.parent ? this.parent.top : this
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
isVulnerable (node) {
|
|
538
|
+
return this.auditReport && this.auditReport.isVulnerable(node)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
get allChildren () {
|
|
542
|
+
const set = new Set(this.children)
|
|
543
|
+
for (const child of set) {
|
|
544
|
+
for (const grandchild of child.children)
|
|
545
|
+
set.add(grandchild)
|
|
546
|
+
}
|
|
547
|
+
return [...set]
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
module.exports = PlaceDep
|
package/lib/printable.js
CHANGED
|
@@ -31,6 +31,10 @@ class ArboristNode {
|
|
|
31
31
|
this.bundled = true
|
|
32
32
|
if (tree.inDepBundle)
|
|
33
33
|
this.bundler = tree.getBundler().location
|
|
34
|
+
if (tree.isProjectRoot)
|
|
35
|
+
this.isProjectRoot = true
|
|
36
|
+
if (tree.isWorkspace)
|
|
37
|
+
this.isWorkspace = true
|
|
34
38
|
const bd = tree.package && tree.package.bundleDependencies
|
|
35
39
|
if (bd && bd.length)
|
|
36
40
|
this.bundleDependencies = bd
|
|
@@ -107,6 +111,8 @@ class Edge {
|
|
|
107
111
|
this.spec = edge.spec || '*'
|
|
108
112
|
if (edge.error)
|
|
109
113
|
this.error = edge.error
|
|
114
|
+
if (edge.overridden)
|
|
115
|
+
this.overridden = edge.overridden
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
|
|
@@ -122,6 +128,8 @@ class EdgeOut extends Edge {
|
|
|
122
128
|
this.to ? ' -> ' + this.to : ''
|
|
123
129
|
}${
|
|
124
130
|
this.error ? ' ' + this.error : ''
|
|
131
|
+
}${
|
|
132
|
+
this.overridden ? ' overridden' : ''
|
|
125
133
|
} }`
|
|
126
134
|
}
|
|
127
135
|
}
|
|
@@ -136,6 +144,8 @@ class EdgeIn extends Edge {
|
|
|
136
144
|
[util.inspect.custom] () {
|
|
137
145
|
return `{ ${this.from || '""'} ${this.type} ${this.name}@${this.spec}${
|
|
138
146
|
this.error ? ' ' + this.error : ''
|
|
147
|
+
}${
|
|
148
|
+
this.overridden ? ' overridden' : ''
|
|
139
149
|
} }`
|
|
140
150
|
}
|
|
141
151
|
}
|
package/lib/shrinkwrap.js
CHANGED
|
@@ -183,8 +183,10 @@ const assertNoNewer = async (path, data, lockTime, dir = path, seen = null) => {
|
|
|
183
183
|
await assertNoNewer(path, data, lockTime, child, seen)
|
|
184
184
|
else if (ent.isSymbolicLink()) {
|
|
185
185
|
const target = resolve(parent, await readlink(child))
|
|
186
|
-
const tstat = await stat(target).catch(
|
|
186
|
+
const tstat = await stat(target).catch(
|
|
187
|
+
/* istanbul ignore next - windows */ () => null)
|
|
187
188
|
seen.add(relpath(path, child))
|
|
189
|
+
/* istanbul ignore next - windows cannot do this */
|
|
188
190
|
if (tstat && tstat.isDirectory() && !seen.has(relpath(path, target)))
|
|
189
191
|
await assertNoNewer(path, data, lockTime, target, seen)
|
|
190
192
|
}
|
|
@@ -253,9 +255,11 @@ class Shrinkwrap {
|
|
|
253
255
|
if (val)
|
|
254
256
|
meta[key.replace(/^_/, '')] = val
|
|
255
257
|
})
|
|
256
|
-
// we only include name if different from the node path name
|
|
258
|
+
// we only include name if different from the node path name, and for the
|
|
259
|
+
// root to help prevent churn based on the name of the directory the
|
|
260
|
+
// project is in
|
|
257
261
|
const pname = node.packageName
|
|
258
|
-
if (pname && pname !== node.name)
|
|
262
|
+
if (pname && (node === node.root || pname !== node.name))
|
|
259
263
|
meta.name = pname
|
|
260
264
|
|
|
261
265
|
if (node.isTop && node.package.devDependencies)
|
|
@@ -802,7 +806,7 @@ class Shrinkwrap {
|
|
|
802
806
|
if (this.tree) {
|
|
803
807
|
if (this.yarnLock)
|
|
804
808
|
this.yarnLock.fromTree(this.tree)
|
|
805
|
-
const root = Shrinkwrap.metaFromNode(this.tree.target
|
|
809
|
+
const root = Shrinkwrap.metaFromNode(this.tree.target, this.path)
|
|
806
810
|
this.data.packages = {}
|
|
807
811
|
if (Object.keys(root).length)
|
|
808
812
|
this.data.packages[''] = root
|
|
@@ -864,7 +868,7 @@ class Shrinkwrap {
|
|
|
864
868
|
const spec = !edge ? rSpec
|
|
865
869
|
: npa.resolve(node.name, edge.spec, edge.from.realpath)
|
|
866
870
|
|
|
867
|
-
if (node.
|
|
871
|
+
if (node.isLink)
|
|
868
872
|
lock.version = `file:${relpath(this.path, node.realpath)}`
|
|
869
873
|
else if (spec && (spec.type === 'file' || spec.type === 'remote'))
|
|
870
874
|
lock.version = spec.saveSpec
|
|
@@ -888,7 +892,7 @@ class Shrinkwrap {
|
|
|
888
892
|
// when we didn't resolve to git, file, or dir, and didn't request
|
|
889
893
|
// git, file, dir, or remote, then the resolved value is necessary.
|
|
890
894
|
if (node.resolved &&
|
|
891
|
-
!node.
|
|
895
|
+
!node.isLink &&
|
|
892
896
|
rSpec.type !== 'git' &&
|
|
893
897
|
rSpec.type !== 'file' &&
|
|
894
898
|
rSpec.type !== 'directory' &&
|
|
@@ -917,7 +921,7 @@ class Shrinkwrap {
|
|
|
917
921
|
lock.optional = true
|
|
918
922
|
}
|
|
919
923
|
|
|
920
|
-
const depender = node.target
|
|
924
|
+
const depender = node.target
|
|
921
925
|
if (depender.edgesOut.size > 0) {
|
|
922
926
|
if (node !== this.tree) {
|
|
923
927
|
lock.requires = [...depender.edgesOut.entries()].reduce((set, [k, v]) => {
|
|
@@ -942,7 +946,7 @@ class Shrinkwrap {
|
|
|
942
946
|
}
|
|
943
947
|
|
|
944
948
|
// now we walk the children, putting them in the 'dependencies' object
|
|
945
|
-
const {children} = node.target
|
|
949
|
+
const {children} = node.target
|
|
946
950
|
if (!children.size)
|
|
947
951
|
delete lock.dependencies
|
|
948
952
|
else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npmcli/arborist",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"description": "Manage node_modules trees",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@npmcli/installed-package-contents": "^1.0.7",
|
|
@@ -16,19 +16,22 @@
|
|
|
16
16
|
"common-ancestor-path": "^1.0.1",
|
|
17
17
|
"json-parse-even-better-errors": "^2.3.1",
|
|
18
18
|
"json-stringify-nice": "^1.1.4",
|
|
19
|
+
"mkdirp": "^1.0.4",
|
|
19
20
|
"mkdirp-infer-owner": "^2.0.0",
|
|
20
21
|
"npm-install-checks": "^4.0.0",
|
|
21
|
-
"npm-package-arg": "^8.1.
|
|
22
|
+
"npm-package-arg": "^8.1.5",
|
|
22
23
|
"npm-pick-manifest": "^6.1.0",
|
|
23
24
|
"npm-registry-fetch": "^11.0.0",
|
|
24
|
-
"pacote": "^11.
|
|
25
|
+
"pacote": "^11.3.5",
|
|
25
26
|
"parse-conflict-json": "^1.1.1",
|
|
26
27
|
"proc-log": "^1.0.0",
|
|
27
28
|
"promise-all-reject-late": "^1.0.0",
|
|
28
29
|
"promise-call-limit": "^1.0.1",
|
|
29
30
|
"read-package-json-fast": "^2.0.2",
|
|
30
31
|
"readdir-scoped-modules": "^1.1.0",
|
|
32
|
+
"rimraf": "^3.0.2",
|
|
31
33
|
"semver": "^7.3.5",
|
|
34
|
+
"ssri": "^8.0.1",
|
|
32
35
|
"tar": "^6.1.0",
|
|
33
36
|
"treeverse": "^1.0.4",
|
|
34
37
|
"walk-up-path": "^1.0.0"
|
|
@@ -50,7 +53,7 @@
|
|
|
50
53
|
"test-only": "tap",
|
|
51
54
|
"posttest": "npm run lint",
|
|
52
55
|
"snap": "tap",
|
|
53
|
-
"postsnap": "npm run
|
|
56
|
+
"postsnap": "npm run lintfix",
|
|
54
57
|
"test-proxy": "ARBORIST_TEST_PROXY=1 tap --snapshot",
|
|
55
58
|
"preversion": "npm test",
|
|
56
59
|
"postversion": "npm publish",
|