@npmcli/arborist 2.2.8 → 2.4.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.
@@ -20,13 +20,21 @@ const levelMap = new Map(levels.reduce((set, level, index) => {
20
20
  }, []))
21
21
 
22
22
  const { inspect, format } = require('util')
23
+ const colors = process.stderr.isTTY
24
+ const magenta = colors ? msg => `\x1B[35m${msg}\x1B[39m` : m => m
23
25
  if (loglevel !== 'silent') {
24
26
  process.on('log', (level, ...args) => {
25
27
  if (levelMap.get(level) < levelMap.get(loglevel))
26
28
  return
27
- const pref = `${process.pid} ${level} `
29
+ const pref = `${process.pid} ${magenta(level)} `
28
30
  if (level === 'warn' && args[0] === 'ERESOLVE')
29
- args[2] = inspect(args[2], { depth: 10 })
31
+ args[2] = inspect(args[2], { depth: 10, colors })
32
+ else {
33
+ args = args.map(a => {
34
+ return typeof a === 'string' ? a
35
+ : inspect(a, { depth: 10, colors })
36
+ })
37
+ }
30
38
  const msg = pref + format(...args).trim().split('\n').join(`\n${pref}`)
31
39
  console.error(msg)
32
40
  })
@@ -33,7 +33,13 @@ for (const arg of process.argv.slice(2)) {
33
33
  options.omit.push(arg.substr('--omit='.length))
34
34
  } else if (/^--before=/.test(arg))
35
35
  options.before = new Date(arg.substr('--before='.length))
36
- else if (/^--[^=]+=/.test(arg)) {
36
+ else if (/^-w.+/.test(arg)) {
37
+ options.workspaces = options.workspaces || []
38
+ options.workspaces.push(arg.replace(/^-w/, ''))
39
+ } else if (/^--workspace=/.test(arg)) {
40
+ options.workspaces = options.workspaces || []
41
+ options.workspaces.push(arg.replace(/^--workspace=/, ''))
42
+ } else if (/^--[^=]+=/.test(arg)) {
37
43
  const [key, ...v] = arg.replace(/^--/, '').split('=')
38
44
  const val = v.join('=')
39
45
  options[key] = val === 'false' ? false : val === 'true' ? true : val
package/bin/lib/timers.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const timers = Object.create(null)
2
+ const { format } = require('util')
2
3
 
3
4
  process.on('time', name => {
4
5
  if (timers[name])
@@ -6,17 +7,20 @@ process.on('time', name => {
6
7
  timers[name] = process.hrtime()
7
8
  })
8
9
 
10
+ const dim = process.stderr.isTTY ? msg => `\x1B[2m${msg}\x1B[22m` : m => m
11
+ const red = process.stderr.isTTY ? msg => `\x1B[31m${msg}\x1B[39m` : m => m
9
12
  process.on('timeEnd', name => {
10
13
  if (!timers[name])
11
14
  throw new Error('timer not started! ' + name)
12
15
  const res = process.hrtime(timers[name])
13
16
  delete timers[name]
14
- console.error(`${process.pid} ${name}`, res[0] * 1e3 + res[1] / 1e6)
17
+ const msg = format(`${process.pid} ${name}`, res[0] * 1e3 + res[1] / 1e6)
18
+ console.error(dim(msg))
15
19
  })
16
20
 
17
21
  process.on('exit', () => {
18
22
  for (const name of Object.keys(timers)) {
19
- console.error('Dangling timer: ', name)
23
+ console.error(red('Dangling timer:'), name)
20
24
  process.exitCode = 1
21
25
  }
22
26
  })
package/bin/virtual.js CHANGED
@@ -8,7 +8,8 @@ require('./lib/timers.js')
8
8
  const start = process.hrtime()
9
9
  new Arborist(options).loadVirtual().then(tree => {
10
10
  const end = process.hrtime(start)
11
- print(tree)
11
+ if (!options.quiet)
12
+ print(tree)
12
13
  if (options.save)
13
14
  tree.meta.save()
14
15
  console.error(`read ${tree.inventory.size} deps in ${end[0] * 1000 + end[1] / 1e6}ms`)
@@ -71,7 +71,7 @@ const addSingle = ({pkg, spec, saveBundle, saveType}) => {
71
71
  pkg.devDependencies[name] = pkg.peerDependencies[name]
72
72
  }
73
73
 
74
- if (saveBundle) {
74
+ if (saveBundle && saveType !== 'peer' && saveType !== 'peerOptional') {
75
75
  // keep it sorted, keep it unique
76
76
  const bd = new Set(pkg.bundleDependencies || [])
77
77
  bd.add(spec.name)
@@ -44,12 +44,15 @@ const _currentDep = Symbol('currentDep')
44
44
  const _updateAll = Symbol('updateAll')
45
45
  const _mutateTree = Symbol('mutateTree')
46
46
  const _flagsSuspect = Symbol.for('flagsSuspect')
47
+ const _workspaces = Symbol.for('workspaces')
47
48
  const _prune = Symbol('prune')
48
49
  const _preferDedupe = Symbol('preferDedupe')
50
+ const _pruneDedupable = Symbol('pruneDedupable')
49
51
  const _legacyBundling = Symbol('legacyBundling')
50
52
  const _parseSettings = Symbol('parseSettings')
51
53
  const _initTree = Symbol('initTree')
52
54
  const _applyUserRequests = Symbol('applyUserRequests')
55
+ const _applyUserRequestsToNode = Symbol('applyUserRequestsToNode')
53
56
  const _inflateAncientLockfile = Symbol('inflateAncientLockfile')
54
57
  const _buildDeps = Symbol('buildDeps')
55
58
  const _buildDepStep = Symbol('buildDepStep')
@@ -109,7 +112,7 @@ const _peerSetSource = Symbol.for('peerSetSource')
109
112
 
110
113
  // used by Reify mixin
111
114
  const _force = Symbol.for('force')
112
- const _explicitRequests = Symbol.for('explicitRequests')
115
+ const _explicitRequests = Symbol('explicitRequests')
113
116
  const _global = Symbol.for('global')
114
117
  const _idealTreePrune = Symbol.for('idealTreePrune')
115
118
 
@@ -130,8 +133,10 @@ module.exports = cls => class IdealTreeBuilder extends cls {
130
133
  force = false,
131
134
  packageLock = true,
132
135
  strictPeerDeps = false,
136
+ workspaces = [],
133
137
  } = options
134
138
 
139
+ this[_workspaces] = workspaces || []
135
140
  this[_force] = !!force
136
141
  this[_strictPeerDeps] = !!strictPeerDeps
137
142
 
@@ -143,6 +148,9 @@ module.exports = cls => class IdealTreeBuilder extends cls {
143
148
  this[_globalStyle] = this[_global] || globalStyle
144
149
  this[_follow] = !!follow
145
150
 
151
+ if (this[_workspaces].length && this[_global])
152
+ throw new Error('Cannot operate on workspaces in global mode')
153
+
146
154
  this[_explicitRequests] = new Set()
147
155
  this[_preferDedupe] = false
148
156
  this[_legacyBundling] = false
@@ -157,6 +165,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
157
165
  this[_manifests] = new Map()
158
166
  this[_peerConflict] = null
159
167
  this[_edgesOverridden] = new Set()
168
+ this[_resolvedAdd] = []
160
169
 
161
170
  // a map of each module in a peer set to the thing that depended on
162
171
  // that set of peers in the first place. Use a WeakMap so that we
@@ -204,8 +213,8 @@ module.exports = cls => class IdealTreeBuilder extends cls {
204
213
 
205
214
  try {
206
215
  await this[_initTree]()
207
- await this[_applyUserRequests](options)
208
216
  await this[_inflateAncientLockfile]()
217
+ await this[_applyUserRequests](options)
209
218
  await this[_buildDeps]()
210
219
  await this[_fixDepFlags]()
211
220
  await this[_pruneFailedOptional]()
@@ -266,6 +275,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
266
275
  this[_preferDedupe] = !!options.preferDedupe
267
276
  this[_legacyBundling] = !!options.legacyBundling
268
277
  this[_updateNames] = update.names
278
+
269
279
  this[_updateAll] = update.all
270
280
  // we prune by default unless explicitly set to boolean false
271
281
  this[_prune] = options.prune !== false
@@ -387,6 +397,42 @@ module.exports = cls => class IdealTreeBuilder extends cls {
387
397
  async [_applyUserRequests] (options) {
388
398
  process.emit('time', 'idealTree:userRequests')
389
399
  const tree = this.idealTree.target || this.idealTree
400
+
401
+ if (!this[_workspaces].length) {
402
+ return this[_applyUserRequestsToNode](tree, options).then(() =>
403
+ process.emit('timeEnd', 'idealTree:userRequests'))
404
+ }
405
+
406
+ const wsMap = tree.workspaces
407
+ if (!wsMap) {
408
+ this.log.warn('idealTree', 'Workspace filter set, but no workspaces present')
409
+ return
410
+ }
411
+
412
+ const promises = []
413
+ for (const name of this[_workspaces]) {
414
+ const path = wsMap.get(name)
415
+ if (!path) {
416
+ this.log.warn('idealTree', `Workspace ${name} in filter set, but not in workspaces`)
417
+ continue
418
+ }
419
+ const loc = relpath(tree.realpath, path)
420
+ const node = tree.inventory.get(loc)
421
+
422
+ /* istanbul ignore if - should be impossible */
423
+ if (!node) {
424
+ this.log.warn('idealTree', `Workspace ${name} in filter set, but no workspace folder present`)
425
+ continue
426
+ }
427
+
428
+ promises.push(this[_applyUserRequestsToNode](node, options))
429
+ }
430
+
431
+ return Promise.all(promises).then(() =>
432
+ process.emit('timeEnd', 'idealTree:userRequests'))
433
+ }
434
+
435
+ async [_applyUserRequestsToNode] (tree, options) {
390
436
  // If we have a list of package names to update, and we know it's
391
437
  // going to update them wherever they are, add any paths into those
392
438
  // named nodes to the buildIdealTree queue.
@@ -395,38 +441,49 @@ module.exports = cls => class IdealTreeBuilder extends cls {
395
441
 
396
442
  // global updates only update the globalTop nodes, but we need to know
397
443
  // that they're there, and not reinstall the world unnecessarily.
444
+ const globalExplicitUpdateNames = []
398
445
  if (this[_global] && (this[_updateAll] || this[_updateNames].length)) {
399
446
  const nm = resolve(this.path, 'node_modules')
400
447
  for (const name of await readdir(nm).catch(() => [])) {
401
- if (this[_updateNames].includes(name))
402
- this[_explicitRequests].add(name)
403
448
  tree.package.dependencies = tree.package.dependencies || {}
404
- if (this[_updateAll] || this[_updateNames].includes(name))
449
+ const updateName = this[_updateNames].includes(name)
450
+ if (this[_updateAll] || updateName) {
451
+ if (updateName)
452
+ globalExplicitUpdateNames.push(name)
405
453
  tree.package.dependencies[name] = '*'
454
+ }
406
455
  }
407
456
  }
408
457
 
409
458
  if (this.auditReport && this.auditReport.size > 0)
410
459
  this[_queueVulnDependents](options)
411
460
 
412
- if (options.rm && options.rm.length) {
413
- addRmPkgDeps.rm(tree.package, options.rm)
414
- for (const name of options.rm)
415
- this[_explicitRequests].add(name)
461
+ const { add, rm } = options
462
+
463
+ if (rm && rm.length) {
464
+ addRmPkgDeps.rm(tree.package, rm)
465
+ for (const name of rm)
466
+ this[_explicitRequests].add({ from: tree, name, action: 'DELETE' })
416
467
  }
417
468
 
418
- if (options.add)
419
- await this[_add](options)
469
+ if (add && add.length)
470
+ await this[_add](tree, options)
420
471
 
421
- // triggers a refresh of all edgesOut
422
- if (options.add && options.add.length || options.rm && options.rm.length || this[_global])
472
+ // triggers a refresh of all edgesOut. this has to be done BEFORE
473
+ // adding the edges to explicitRequests, because the package setter
474
+ // resets all edgesOut.
475
+ if (add && add.length || rm && rm.length || this[_global])
423
476
  tree.package = tree.package
424
- process.emit('timeEnd', 'idealTree:userRequests')
477
+
478
+ for (const spec of this[_resolvedAdd])
479
+ this[_explicitRequests].add(tree.edgesOut.get(spec.name))
480
+ for (const name of globalExplicitUpdateNames)
481
+ this[_explicitRequests].add(tree.edgesOut.get(name))
425
482
  }
426
483
 
427
484
  // This returns a promise because we might not have the name yet,
428
485
  // and need to call pacote.manifest to find the name.
429
- [_add] ({add, saveType = null, saveBundle = false}) {
486
+ [_add] (tree, {add, saveType = null, saveBundle = false}) {
430
487
  // get the name for each of the specs in the list.
431
488
  // ie, doing `foo@bar` we just return foo
432
489
  // but if it's a url or git, we don't know the name until we
@@ -438,10 +495,9 @@ module.exports = cls => class IdealTreeBuilder extends cls {
438
495
  .then(add => this[_updateFilePath](add))
439
496
  .then(add => this[_followSymlinkPath](add))
440
497
  })).then(add => {
441
- this[_resolvedAdd] = add
498
+ this[_resolvedAdd].push(...add)
442
499
  // now add is a list of spec objects with names.
443
500
  // find a home for each of them!
444
- const tree = this.idealTree.target || this.idealTree
445
501
  addRmPkgDeps.add({
446
502
  pkg: tree.package,
447
503
  add,
@@ -449,8 +505,6 @@ module.exports = cls => class IdealTreeBuilder extends cls {
449
505
  saveType,
450
506
  path: this.path,
451
507
  })
452
- for (const spec of add)
453
- this[_explicitRequests].add(spec.name)
454
508
  })
455
509
  }
456
510
 
@@ -991,7 +1045,7 @@ This is a one-time fix-up, please be patient...
991
1045
  // if it's peerOptional and not explicitly requested.
992
1046
  if (!edge.to) {
993
1047
  return edge.type !== 'peerOptional' ||
994
- this[_explicitRequests].has(edge.name)
1048
+ this[_explicitRequests].has(edge)
995
1049
  }
996
1050
 
997
1051
  // If the edge has an error, there's a problem.
@@ -1007,7 +1061,7 @@ This is a one-time fix-up, please be patient...
1007
1061
  return true
1008
1062
 
1009
1063
  // If the user has explicitly asked to install this package, it's a problem.
1010
- if (node.isProjectRoot && this[_explicitRequests].has(edge.name))
1064
+ if (node.isProjectRoot && this[_explicitRequests].has(edge))
1011
1065
  return true
1012
1066
 
1013
1067
  // No problems!
@@ -1131,10 +1185,20 @@ This is a one-time fix-up, please be patient...
1131
1185
  continue
1132
1186
 
1133
1187
  // problem
1134
- this[_failPeerConflict](edge)
1188
+ this[_failPeerConflict](edge, parentEdge)
1135
1189
  }
1136
1190
  }
1137
1191
 
1192
+ // There is something present already, and we're not happy about it
1193
+ // See if the thing we WOULD be happy with is also going to satisfy
1194
+ // the other dependents on the current node.
1195
+ const current = edge.to
1196
+ const dep = await this[_nodeFromEdge](edge, null, null, required)
1197
+ if (dep.canReplace(current)) {
1198
+ await this[_nodeFromEdge](edge, node.parent, null, required)
1199
+ continue
1200
+ }
1201
+
1138
1202
  // at this point we know that there is a dep there, and
1139
1203
  // we don't like it. always fail strictly, always allow forcibly or
1140
1204
  // in non-strict mode if it's not our fault. don't warn here, because
@@ -1147,17 +1211,17 @@ This is a one-time fix-up, please be patient...
1147
1211
  continue
1148
1212
 
1149
1213
  // ok, it's the root, or we're in unforced strict mode, so this is bad
1150
- this[_failPeerConflict](edge)
1214
+ this[_failPeerConflict](edge, parentEdge)
1151
1215
  }
1152
1216
  return node
1153
1217
  }
1154
1218
 
1155
- [_failPeerConflict] (edge) {
1156
- const expl = this[_explainPeerConflict](edge)
1219
+ [_failPeerConflict] (edge, currentEdge) {
1220
+ const expl = this[_explainPeerConflict](edge, currentEdge)
1157
1221
  throw Object.assign(new Error('unable to resolve dependency tree'), expl)
1158
1222
  }
1159
1223
 
1160
- [_explainPeerConflict] (edge) {
1224
+ [_explainPeerConflict] (edge, currentEdge) {
1161
1225
  const node = edge.from
1162
1226
  const curNode = node.resolve(edge.name)
1163
1227
  const pc = this[_peerConflict] || { peer: null, current: null }
@@ -1166,6 +1230,10 @@ This is a one-time fix-up, please be patient...
1166
1230
  return {
1167
1231
  code: 'ERESOLVE',
1168
1232
  current,
1233
+ // it SHOULD be impossible to get here without a current node in place,
1234
+ // but this at least gives us something report on when bugs creep into
1235
+ // the tree handling logic.
1236
+ currentEdge: currentEdge ? currentEdge.explain() : null,
1169
1237
  edge: edge.explain(),
1170
1238
  peerConflict,
1171
1239
  strictPeerDeps: this[_strictPeerDeps],
@@ -1190,7 +1258,7 @@ This is a one-time fix-up, please be patient...
1190
1258
  [_placeDep] (dep, node, edge, peerEntryEdge = null, peerPath = []) {
1191
1259
  if (edge.to &&
1192
1260
  !edge.error &&
1193
- !this[_explicitRequests].has(edge.name) &&
1261
+ !this[_explicitRequests].has(edge) &&
1194
1262
  !this[_updateNames].includes(edge.name) &&
1195
1263
  !this[_isVulnerable](edge.to))
1196
1264
  return []
@@ -1275,6 +1343,21 @@ This is a one-time fix-up, please be patient...
1275
1343
  // this is an overridden peer dep
1276
1344
  this[_warnPeerConflict](edge)
1277
1345
  }
1346
+
1347
+ // if we get a KEEP in a update scenario, then we MAY have something
1348
+ // already duplicating this unnecessarily! For example:
1349
+ // ```
1350
+ // root
1351
+ // +-- x (dep: y@1.x)
1352
+ // | +-- y@1.0.0
1353
+ // +-- y@1.1.0
1354
+ // ```
1355
+ // Now say we do `reify({update:['y']})`, and the latest version is
1356
+ // 1.1.0, which we already have in the root. We'll try to place y@1.1.0
1357
+ // first in x, then in the root, ending with KEEP, because we already
1358
+ // have it. In that case, we ought to REMOVE the nm/x/nm/y node, because
1359
+ // it is an unnecessary duplicate.
1360
+ this[_pruneDedupable](target, true)
1278
1361
  return []
1279
1362
  }
1280
1363
 
@@ -1330,8 +1413,8 @@ This is a one-time fix-up, please be patient...
1330
1413
  // MAY end up putting a better/identical node further up the tree in
1331
1414
  // a way that causes an unnecessary duplication. If so, remove the
1332
1415
  // now-unnecessary node.
1333
- if (edge.valid && edge.to.parent !== target && newDep.canReplace(edge.to))
1334
- edge.to.parent = null
1416
+ if (edge.valid && edge.to && edge.to !== newDep)
1417
+ this[_pruneDedupable](edge.to, false)
1335
1418
 
1336
1419
  // visit any dependents who are upset by this change
1337
1420
  // if it's an angry overridden peer edge, however, make sure we
@@ -1347,30 +1430,8 @@ This is a one-time fix-up, please be patient...
1347
1430
  // prune anything deeper in the tree that can be replaced by this
1348
1431
  if (this.idealTree) {
1349
1432
  for (const node of this.idealTree.inventory.query('name', newDep.name)) {
1350
- if (node !== newDep &&
1351
- node.isDescendantOf(target) &&
1352
- !node.inShrinkwrap &&
1353
- !node.inBundle &&
1354
- node.canReplaceWith(newDep)) {
1355
- // don't prune if the dupe is necessary!
1356
- // root (a, d)
1357
- // +-- a (b, c2)
1358
- // | +-- b (c2) <-- place c2 for b, lands at root
1359
- // +-- d (e)
1360
- // +-- e (c1, d)
1361
- // +-- c1
1362
- // +-- f (c2)
1363
- // +-- c2 <-- pruning this would be bad
1364
-
1365
- const mask = node.parent !== target &&
1366
- node.parent &&
1367
- node.parent.parent &&
1368
- node.parent.parent !== target &&
1369
- node.parent.parent.resolve(newDep.name)
1370
-
1371
- if (!mask || mask === newDep || node.canReplaceWith(mask))
1372
- node.parent = null
1373
- }
1433
+ if (node.isDescendantOf(target))
1434
+ this[_pruneDedupable](node, false)
1374
1435
  }
1375
1436
  }
1376
1437
 
@@ -1403,6 +1464,21 @@ This is a one-time fix-up, please be patient...
1403
1464
  return placed
1404
1465
  }
1405
1466
 
1467
+ // prune all the nodes in a branch of the tree that can be safely removed
1468
+ // This is only the most basic duplication detection; it finds if there
1469
+ // is another satisfying node further up the tree, and if so, dedupes.
1470
+ // Even in legacyBundling mode, we do this amount of deduplication.
1471
+ [_pruneDedupable] (node, descend = true) {
1472
+ if (node.canDedupe(this[_preferDedupe])) {
1473
+ node.root = null
1474
+ return
1475
+ }
1476
+ if (descend) {
1477
+ for (const child of node.children.values())
1478
+ this[_pruneDedupable](child)
1479
+ }
1480
+ }
1481
+
1406
1482
  [_pruneForReplacement] (node, oldDeps) {
1407
1483
  // gather up all the invalid edgesOut, and any now-extraneous
1408
1484
  // deps that the new node doesn't depend on but the old one did.
@@ -1480,9 +1556,15 @@ This is a one-time fix-up, please be patient...
1480
1556
  if (target.children.has(edge.name)) {
1481
1557
  const current = target.children.get(edge.name)
1482
1558
 
1483
- // same thing = keep
1484
- if (dep.matches(current))
1485
- return KEEP
1559
+ // same thing = keep, UNLESS the current doesn't satisfy and new
1560
+ // one does satisfy. This can happen if it's a link to a matching target
1561
+ // at a different location, which satisfies a version dep, but not a
1562
+ // file: dep. If neither of them satisfy, then we can replace it,
1563
+ // because presumably it's better for a peer or something.
1564
+ if (dep.matches(current)) {
1565
+ if (current.satisfies(edge) || !dep.satisfies(edge))
1566
+ return KEEP
1567
+ }
1486
1568
 
1487
1569
  const { version: curVer } = current
1488
1570
  const { version: newVer } = dep
@@ -1549,32 +1631,137 @@ This is a one-time fix-up, please be patient...
1549
1631
  // placed here as well. the virtualRoot already has the appropriate
1550
1632
  // overrides applied.
1551
1633
  if (peerEntryEdge) {
1552
- const peerSet = getPeerSet(current)
1553
- OUTER: for (const p of peerSet) {
1554
- // if any have a non-peer dep from the target, or a peer dep if
1555
- // the target is root, then cannot safely replace and dupe deeper.
1634
+ const currentPeerSet = getPeerSet(current)
1635
+
1636
+ // We are effectively replacing currentPeerSet with newPeerSet
1637
+ // If there are any non-peer deps coming into the currentPeerSet,
1638
+ // which are currently valid, and are from the target, then that
1639
+ // means that we have to ensure that they're not going to be made
1640
+ // invalid by putting the newPeerSet in place.
1641
+ // If the edge comes from somewhere deeper than the target, then
1642
+ // that's fine, because we'll create an invalid edge, detect it,
1643
+ // and duplicate the node further into the tree.
1644
+ // loop through the currentPeerSet checking for valid edges on
1645
+ // the members of the peer set which will be made invalid.
1646
+ const targetEdges = new Set()
1647
+ for (const p of currentPeerSet) {
1556
1648
  for (const edge of p.edgesIn) {
1557
- if (peerSet.has(edge.from))
1649
+ // edge from within the peerSet, ignore
1650
+ if (currentPeerSet.has(edge.from))
1651
+ continue
1652
+ // only care about valid edges from target.
1653
+ // edges from elsewhere can dupe if offended, invalid edges
1654
+ // are already being fixed or will be later.
1655
+ if (edge.from !== target || !edge.valid)
1558
1656
  continue
1657
+ targetEdges.add(edge)
1658
+ }
1659
+ }
1559
1660
 
1560
- // only respect valid edges, however, since we're likely trying
1561
- // to fix the very one that's currently broken! If the virtual
1562
- // root's replacement is ok, and doesn't have any invalid edges
1563
- // indicating that it was an overridden peer, then ignore the
1564
- // conflict and continue. If it WAS an override, then we need
1565
- // to get the conflict here so that we can decide whether to
1566
- // accept the current dep node, clobber it, or fail the install.
1567
- if (edge.from === target && edge.valid) {
1568
- const rep = dep.parent.children.get(edge.name)
1569
- const override = rep && ([...rep.edgesIn].some(e => !e.valid))
1570
- if (!rep || !rep.satisfies(edge) || override) {
1661
+ for (const edge of targetEdges) {
1662
+ // see if we intend to replace this one anyway
1663
+ const rep = dep.parent.children.get(edge.name)
1664
+ const current = edge.to
1665
+ if (!rep) {
1666
+ // this isn't one we're replacing. but it WAS included in the
1667
+ // peerSet for some reason, so make sure that it's still
1668
+ // ok with the replacements in the new peerSet
1669
+ for (const curEdge of current.edgesOut.values()) {
1670
+ const newRepDep = dep.parent.children.get(curEdge.name)
1671
+ if (curEdge.valid && newRepDep && !newRepDep.satisfies(curEdge)) {
1571
1672
  canReplace = false
1572
- break OUTER
1673
+ break
1573
1674
  }
1574
1675
  }
1676
+ continue
1677
+ }
1678
+
1679
+ // was this replacement already an override of some sort?
1680
+ const override = [...rep.edgesIn].some(e => !e.valid)
1681
+ // if we have a rep, and it's ok to put in this location, and
1682
+ // it's not already part of an override in the peerSet, then
1683
+ // we can continue with it.
1684
+ if (rep.satisfies(edge) && !override)
1685
+ continue
1686
+ // Otherwise, we cannot replace.
1687
+ canReplace = false
1688
+ break
1689
+ }
1690
+ // if we're going to be replacing the peerSet, we have to remove
1691
+ // and re-resolve any members of the old peerSet that are not
1692
+ // present in the new one, and which will have invalid edges.
1693
+ // We know that they're not depended upon by the target, or else
1694
+ // they would have caused a conflict, so they'll get landed deeper
1695
+ // in the tree, if possible.
1696
+ if (canReplace) {
1697
+ let needNesting = false
1698
+ OUTER: for (const node of currentPeerSet) {
1699
+ const rep = dep.parent.children.get(node.name)
1700
+ // has a replacement, already addressed above
1701
+ if (rep)
1702
+ continue
1703
+
1704
+ // ok, it has been placed here to dedupe, see if it needs to go
1705
+ // back deeper within the tree.
1706
+ for (const edge of node.edgesOut.values()) {
1707
+ const repDep = dep.parent.children.get(edge.name)
1708
+ // not in new peerSet, maybe fine.
1709
+ if (!repDep)
1710
+ continue
1711
+
1712
+ // new thing will be fine, no worries
1713
+ if (repDep.satisfies(edge))
1714
+ continue
1715
+
1716
+ // uhoh, we'll have to nest them.
1717
+ needNesting = true
1718
+ break OUTER
1719
+ }
1720
+ }
1721
+
1722
+ // to nest, just delete everything without a target dep
1723
+ // that's in the current peerSet, and add their dependants
1724
+ // to the _depsQueue for evaluation. Some of these MAY end
1725
+ // up in the same location again, and that's fine.
1726
+ if (needNesting) {
1727
+ // avoid mutating the tree while we're examining it
1728
+ const dependants = new Set()
1729
+ const reresolve = new Set()
1730
+ OUTER: for (const node of currentPeerSet) {
1731
+ const rep = dep.parent.children.get(node.name)
1732
+ if (rep)
1733
+ continue
1734
+ // create a separate set for each one, so we can skip any
1735
+ // that might somehow have an incoming target edge
1736
+ const deps = new Set()
1737
+ for (const edge of node.edgesIn) {
1738
+ // a target dep, skip this dep entirely, already addressed
1739
+ // ignoring for coverage, because it really ought to be
1740
+ // impossible, but I can't prove it yet, so this is here
1741
+ // for safety.
1742
+ /* istanbul ignore if - should be impossible */
1743
+ if (edge.from === target)
1744
+ continue OUTER
1745
+ // ignore this edge, it'll either be replaced or re-resolved
1746
+ if (currentPeerSet.has(edge.from))
1747
+ continue
1748
+ // ok, we care about this one.
1749
+ deps.add(edge.from)
1750
+ }
1751
+ reresolve.add(node)
1752
+ for (const d of deps)
1753
+ dependants.add(d)
1754
+ }
1755
+ for (const dependant of dependants) {
1756
+ this[_depsQueue].push(dependant)
1757
+ this[_depsSeen].delete(dependant)
1758
+ }
1759
+ for (const node of reresolve)
1760
+ node.root = null
1575
1761
  }
1576
1762
  }
1577
1763
  }
1764
+
1578
1765
  if (canReplace) {
1579
1766
  const ret = this[_canPlacePeers](dep, target, edge, REPLACE, peerEntryEdge, peerPath, isSource)
1580
1767
  /* istanbul ignore else - extremely rare that the peer set would
@@ -54,7 +54,7 @@ class Arborist extends Base {
54
54
  ...options,
55
55
  path: options.path || '.',
56
56
  cache: options.cache || `${homedir()}/.npm/_cacache`,
57
- packumentCache: new Map(),
57
+ packumentCache: options.packumentCache || new Map(),
58
58
  log: options.log || procLog,
59
59
  }
60
60
  this.cache = resolve(this.options.cache)