@npmcli/arborist 2.2.9 → 2.4.2

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.
@@ -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,19 +495,17 @@ 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,
448
504
  saveBundle,
449
505
  saveType,
450
506
  path: this.path,
507
+ log: this.log,
451
508
  })
452
- for (const spec of add)
453
- this[_explicitRequests].add(spec.name)
454
509
  })
455
510
  }
456
511
 
@@ -991,7 +1046,7 @@ This is a one-time fix-up, please be patient...
991
1046
  // if it's peerOptional and not explicitly requested.
992
1047
  if (!edge.to) {
993
1048
  return edge.type !== 'peerOptional' ||
994
- this[_explicitRequests].has(edge.name)
1049
+ this[_explicitRequests].has(edge)
995
1050
  }
996
1051
 
997
1052
  // If the edge has an error, there's a problem.
@@ -1007,7 +1062,7 @@ This is a one-time fix-up, please be patient...
1007
1062
  return true
1008
1063
 
1009
1064
  // If the user has explicitly asked to install this package, it's a problem.
1010
- if (node.isProjectRoot && this[_explicitRequests].has(edge.name))
1065
+ if (node.isProjectRoot && this[_explicitRequests].has(edge))
1011
1066
  return true
1012
1067
 
1013
1068
  // No problems!
@@ -1131,10 +1186,20 @@ This is a one-time fix-up, please be patient...
1131
1186
  continue
1132
1187
 
1133
1188
  // problem
1134
- this[_failPeerConflict](edge)
1189
+ this[_failPeerConflict](edge, parentEdge)
1135
1190
  }
1136
1191
  }
1137
1192
 
1193
+ // There is something present already, and we're not happy about it
1194
+ // See if the thing we WOULD be happy with is also going to satisfy
1195
+ // the other dependents on the current node.
1196
+ const current = edge.to
1197
+ const dep = await this[_nodeFromEdge](edge, null, null, required)
1198
+ if (dep.canReplace(current)) {
1199
+ await this[_nodeFromEdge](edge, node.parent, null, required)
1200
+ continue
1201
+ }
1202
+
1138
1203
  // at this point we know that there is a dep there, and
1139
1204
  // we don't like it. always fail strictly, always allow forcibly or
1140
1205
  // in non-strict mode if it's not our fault. don't warn here, because
@@ -1147,17 +1212,17 @@ This is a one-time fix-up, please be patient...
1147
1212
  continue
1148
1213
 
1149
1214
  // ok, it's the root, or we're in unforced strict mode, so this is bad
1150
- this[_failPeerConflict](edge)
1215
+ this[_failPeerConflict](edge, parentEdge)
1151
1216
  }
1152
1217
  return node
1153
1218
  }
1154
1219
 
1155
- [_failPeerConflict] (edge) {
1156
- const expl = this[_explainPeerConflict](edge)
1220
+ [_failPeerConflict] (edge, currentEdge) {
1221
+ const expl = this[_explainPeerConflict](edge, currentEdge)
1157
1222
  throw Object.assign(new Error('unable to resolve dependency tree'), expl)
1158
1223
  }
1159
1224
 
1160
- [_explainPeerConflict] (edge) {
1225
+ [_explainPeerConflict] (edge, currentEdge) {
1161
1226
  const node = edge.from
1162
1227
  const curNode = node.resolve(edge.name)
1163
1228
  const pc = this[_peerConflict] || { peer: null, current: null }
@@ -1166,6 +1231,10 @@ This is a one-time fix-up, please be patient...
1166
1231
  return {
1167
1232
  code: 'ERESOLVE',
1168
1233
  current,
1234
+ // it SHOULD be impossible to get here without a current node in place,
1235
+ // but this at least gives us something report on when bugs creep into
1236
+ // the tree handling logic.
1237
+ currentEdge: currentEdge ? currentEdge.explain() : null,
1169
1238
  edge: edge.explain(),
1170
1239
  peerConflict,
1171
1240
  strictPeerDeps: this[_strictPeerDeps],
@@ -1190,7 +1259,7 @@ This is a one-time fix-up, please be patient...
1190
1259
  [_placeDep] (dep, node, edge, peerEntryEdge = null, peerPath = []) {
1191
1260
  if (edge.to &&
1192
1261
  !edge.error &&
1193
- !this[_explicitRequests].has(edge.name) &&
1262
+ !this[_explicitRequests].has(edge) &&
1194
1263
  !this[_updateNames].includes(edge.name) &&
1195
1264
  !this[_isVulnerable](edge.to))
1196
1265
  return []
@@ -1275,6 +1344,21 @@ This is a one-time fix-up, please be patient...
1275
1344
  // this is an overridden peer dep
1276
1345
  this[_warnPeerConflict](edge)
1277
1346
  }
1347
+
1348
+ // if we get a KEEP in a update scenario, then we MAY have something
1349
+ // already duplicating this unnecessarily! For example:
1350
+ // ```
1351
+ // root
1352
+ // +-- x (dep: y@1.x)
1353
+ // | +-- y@1.0.0
1354
+ // +-- y@1.1.0
1355
+ // ```
1356
+ // Now say we do `reify({update:['y']})`, and the latest version is
1357
+ // 1.1.0, which we already have in the root. We'll try to place y@1.1.0
1358
+ // first in x, then in the root, ending with KEEP, because we already
1359
+ // have it. In that case, we ought to REMOVE the nm/x/nm/y node, because
1360
+ // it is an unnecessary duplicate.
1361
+ this[_pruneDedupable](target, true)
1278
1362
  return []
1279
1363
  }
1280
1364
 
@@ -1330,8 +1414,8 @@ This is a one-time fix-up, please be patient...
1330
1414
  // MAY end up putting a better/identical node further up the tree in
1331
1415
  // a way that causes an unnecessary duplication. If so, remove the
1332
1416
  // now-unnecessary node.
1333
- if (edge.valid && edge.to.parent !== target && newDep.canReplace(edge.to))
1334
- edge.to.parent = null
1417
+ if (edge.valid && edge.to && edge.to !== newDep)
1418
+ this[_pruneDedupable](edge.to, false)
1335
1419
 
1336
1420
  // visit any dependents who are upset by this change
1337
1421
  // if it's an angry overridden peer edge, however, make sure we
@@ -1347,30 +1431,8 @@ This is a one-time fix-up, please be patient...
1347
1431
  // prune anything deeper in the tree that can be replaced by this
1348
1432
  if (this.idealTree) {
1349
1433
  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
- }
1434
+ if (node.isDescendantOf(target))
1435
+ this[_pruneDedupable](node, false)
1374
1436
  }
1375
1437
  }
1376
1438
 
@@ -1403,6 +1465,21 @@ This is a one-time fix-up, please be patient...
1403
1465
  return placed
1404
1466
  }
1405
1467
 
1468
+ // prune all the nodes in a branch of the tree that can be safely removed
1469
+ // This is only the most basic duplication detection; it finds if there
1470
+ // is another satisfying node further up the tree, and if so, dedupes.
1471
+ // Even in legacyBundling mode, we do this amount of deduplication.
1472
+ [_pruneDedupable] (node, descend = true) {
1473
+ if (node.canDedupe(this[_preferDedupe])) {
1474
+ node.root = null
1475
+ return
1476
+ }
1477
+ if (descend) {
1478
+ for (const child of node.children.values())
1479
+ this[_pruneDedupable](child)
1480
+ }
1481
+ }
1482
+
1406
1483
  [_pruneForReplacement] (node, oldDeps) {
1407
1484
  // gather up all the invalid edgesOut, and any now-extraneous
1408
1485
  // deps that the new node doesn't depend on but the old one did.
@@ -1480,9 +1557,15 @@ This is a one-time fix-up, please be patient...
1480
1557
  if (target.children.has(edge.name)) {
1481
1558
  const current = target.children.get(edge.name)
1482
1559
 
1483
- // same thing = keep
1484
- if (dep.matches(current))
1485
- return KEEP
1560
+ // same thing = keep, UNLESS the current doesn't satisfy and new
1561
+ // one does satisfy. This can happen if it's a link to a matching target
1562
+ // at a different location, which satisfies a version dep, but not a
1563
+ // file: dep. If neither of them satisfy, then we can replace it,
1564
+ // because presumably it's better for a peer or something.
1565
+ if (dep.matches(current)) {
1566
+ if (current.satisfies(edge) || !dep.satisfies(edge))
1567
+ return KEEP
1568
+ }
1486
1569
 
1487
1570
  const { version: curVer } = current
1488
1571
  const { version: newVer } = dep
@@ -1549,32 +1632,137 @@ This is a one-time fix-up, please be patient...
1549
1632
  // placed here as well. the virtualRoot already has the appropriate
1550
1633
  // overrides applied.
1551
1634
  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.
1635
+ const currentPeerSet = getPeerSet(current)
1636
+
1637
+ // We are effectively replacing currentPeerSet with newPeerSet
1638
+ // If there are any non-peer deps coming into the currentPeerSet,
1639
+ // which are currently valid, and are from the target, then that
1640
+ // means that we have to ensure that they're not going to be made
1641
+ // invalid by putting the newPeerSet in place.
1642
+ // If the edge comes from somewhere deeper than the target, then
1643
+ // that's fine, because we'll create an invalid edge, detect it,
1644
+ // and duplicate the node further into the tree.
1645
+ // loop through the currentPeerSet checking for valid edges on
1646
+ // the members of the peer set which will be made invalid.
1647
+ const targetEdges = new Set()
1648
+ for (const p of currentPeerSet) {
1556
1649
  for (const edge of p.edgesIn) {
1557
- if (peerSet.has(edge.from))
1650
+ // edge from within the peerSet, ignore
1651
+ if (currentPeerSet.has(edge.from))
1652
+ continue
1653
+ // only care about valid edges from target.
1654
+ // edges from elsewhere can dupe if offended, invalid edges
1655
+ // are already being fixed or will be later.
1656
+ if (edge.from !== target || !edge.valid)
1558
1657
  continue
1658
+ targetEdges.add(edge)
1659
+ }
1660
+ }
1559
1661
 
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) {
1662
+ for (const edge of targetEdges) {
1663
+ // see if we intend to replace this one anyway
1664
+ const rep = dep.parent.children.get(edge.name)
1665
+ const current = edge.to
1666
+ if (!rep) {
1667
+ // this isn't one we're replacing. but it WAS included in the
1668
+ // peerSet for some reason, so make sure that it's still
1669
+ // ok with the replacements in the new peerSet
1670
+ for (const curEdge of current.edgesOut.values()) {
1671
+ const newRepDep = dep.parent.children.get(curEdge.name)
1672
+ if (curEdge.valid && newRepDep && !newRepDep.satisfies(curEdge)) {
1571
1673
  canReplace = false
1572
- break OUTER
1674
+ break
1573
1675
  }
1574
1676
  }
1677
+ continue
1678
+ }
1679
+
1680
+ // was this replacement already an override of some sort?
1681
+ const override = [...rep.edgesIn].some(e => !e.valid)
1682
+ // if we have a rep, and it's ok to put in this location, and
1683
+ // it's not already part of an override in the peerSet, then
1684
+ // we can continue with it.
1685
+ if (rep.satisfies(edge) && !override)
1686
+ continue
1687
+ // Otherwise, we cannot replace.
1688
+ canReplace = false
1689
+ break
1690
+ }
1691
+ // if we're going to be replacing the peerSet, we have to remove
1692
+ // and re-resolve any members of the old peerSet that are not
1693
+ // present in the new one, and which will have invalid edges.
1694
+ // We know that they're not depended upon by the target, or else
1695
+ // they would have caused a conflict, so they'll get landed deeper
1696
+ // in the tree, if possible.
1697
+ if (canReplace) {
1698
+ let needNesting = false
1699
+ OUTER: for (const node of currentPeerSet) {
1700
+ const rep = dep.parent.children.get(node.name)
1701
+ // has a replacement, already addressed above
1702
+ if (rep)
1703
+ continue
1704
+
1705
+ // ok, it has been placed here to dedupe, see if it needs to go
1706
+ // back deeper within the tree.
1707
+ for (const edge of node.edgesOut.values()) {
1708
+ const repDep = dep.parent.children.get(edge.name)
1709
+ // not in new peerSet, maybe fine.
1710
+ if (!repDep)
1711
+ continue
1712
+
1713
+ // new thing will be fine, no worries
1714
+ if (repDep.satisfies(edge))
1715
+ continue
1716
+
1717
+ // uhoh, we'll have to nest them.
1718
+ needNesting = true
1719
+ break OUTER
1720
+ }
1721
+ }
1722
+
1723
+ // to nest, just delete everything without a target dep
1724
+ // that's in the current peerSet, and add their dependants
1725
+ // to the _depsQueue for evaluation. Some of these MAY end
1726
+ // up in the same location again, and that's fine.
1727
+ if (needNesting) {
1728
+ // avoid mutating the tree while we're examining it
1729
+ const dependants = new Set()
1730
+ const reresolve = new Set()
1731
+ OUTER: for (const node of currentPeerSet) {
1732
+ const rep = dep.parent.children.get(node.name)
1733
+ if (rep)
1734
+ continue
1735
+ // create a separate set for each one, so we can skip any
1736
+ // that might somehow have an incoming target edge
1737
+ const deps = new Set()
1738
+ for (const edge of node.edgesIn) {
1739
+ // a target dep, skip this dep entirely, already addressed
1740
+ // ignoring for coverage, because it really ought to be
1741
+ // impossible, but I can't prove it yet, so this is here
1742
+ // for safety.
1743
+ /* istanbul ignore if - should be impossible */
1744
+ if (edge.from === target)
1745
+ continue OUTER
1746
+ // ignore this edge, it'll either be replaced or re-resolved
1747
+ if (currentPeerSet.has(edge.from))
1748
+ continue
1749
+ // ok, we care about this one.
1750
+ deps.add(edge.from)
1751
+ }
1752
+ reresolve.add(node)
1753
+ for (const d of deps)
1754
+ dependants.add(d)
1755
+ }
1756
+ for (const dependant of dependants) {
1757
+ this[_depsQueue].push(dependant)
1758
+ this[_depsSeen].delete(dependant)
1759
+ }
1760
+ for (const node of reresolve)
1761
+ node.root = null
1575
1762
  }
1576
1763
  }
1577
1764
  }
1765
+
1578
1766
  if (canReplace) {
1579
1767
  const ret = this[_canPlacePeers](dep, target, edge, REPLACE, peerEntryEdge, peerPath, isSource)
1580
1768
  /* istanbul ignore else - extremely rare that the peer set would
@@ -29,6 +29,7 @@
29
29
  const {resolve} = require('path')
30
30
  const {homedir} = require('os')
31
31
  const procLog = require('../proc-log.js')
32
+ const { saveTypeMap } = require('../add-rm-pkg-deps.js')
32
33
 
33
34
  const mixins = [
34
35
  require('../tracker.js'),
@@ -54,9 +55,11 @@ class Arborist extends Base {
54
55
  ...options,
55
56
  path: options.path || '.',
56
57
  cache: options.cache || `${homedir()}/.npm/_cacache`,
57
- packumentCache: new Map(),
58
+ packumentCache: options.packumentCache || new Map(),
58
59
  log: options.log || procLog,
59
60
  }
61
+ if (options.saveType && !saveTypeMap.get(options.saveType))
62
+ throw new Error(`Invalid saveType ${options.saveType}`)
60
63
  this.cache = resolve(this.options.cache)
61
64
  this.path = resolve(this.options.path)
62
65
  process.emit('timeEnd', 'arborist:ctor')
@@ -32,6 +32,7 @@ const _loadActual = Symbol('loadActual')
32
32
  const _loadActualVirtually = Symbol('loadActualVirtually')
33
33
  const _loadActualActually = Symbol('loadActualActually')
34
34
  const _loadWorkspaces = Symbol.for('loadWorkspaces')
35
+ const _loadWorkspaceTargets = Symbol('loadWorkspaceTargets')
35
36
  const _actualTreePromise = Symbol('actualTreePromise')
36
37
  const _actualTree = Symbol('actualTree')
37
38
  const _transplant = Symbol('transplant')
@@ -150,18 +151,22 @@ module.exports = cls => class ActualLoader extends cls {
150
151
  await new this.constructor({...this.options}).loadVirtual({
151
152
  root: this[_actualTree],
152
153
  })
154
+ await this[_loadWorkspaces](this[_actualTree])
155
+ if (this[_actualTree].workspaces && this[_actualTree].workspaces.size)
156
+ calcDepFlags(this[_actualTree], !root)
153
157
  this[_transplant](root)
154
158
  return this[_actualTree]
155
159
  }
156
160
 
157
161
  async [_loadActualActually] ({ root, ignoreMissing, global }) {
158
162
  await this[_loadFSTree](this[_actualTree])
163
+ await this[_loadWorkspaces](this[_actualTree])
164
+ await this[_loadWorkspaceTargets](this[_actualTree])
159
165
  if (!ignoreMissing)
160
166
  await this[_findMissingEdges]()
161
167
  this[_findFSParents]()
162
168
  this[_transplant](root)
163
169
 
164
- await this[_loadWorkspaces](this[_actualTree])
165
170
  if (global) {
166
171
  // need to depend on the children, or else all of them
167
172
  // will end up being flagged as extraneous, since the
@@ -178,16 +183,37 @@ module.exports = cls => class ActualLoader extends cls {
178
183
  return this[_actualTree]
179
184
  }
180
185
 
186
+ // if there are workspace targets without Link nodes created, load
187
+ // the targets, so that we know what they are.
188
+ async [_loadWorkspaceTargets] (tree) {
189
+ if (!tree.workspaces || !tree.workspaces.size)
190
+ return
191
+
192
+ const promises = []
193
+ for (const path of tree.workspaces.values()) {
194
+ if (!this[_cache].has(path)) {
195
+ const p = this[_loadFSNode]({ path, root: this[_actualTree] })
196
+ .then(node => this[_loadFSTree](node))
197
+ promises.push(p)
198
+ }
199
+ }
200
+ await Promise.all(promises)
201
+ }
202
+
181
203
  [_transplant] (root) {
182
204
  if (!root || root === this[_actualTree])
183
205
  return
206
+
184
207
  this[_actualTree][_changePath](root.path)
185
208
  for (const node of this[_actualTree].children.values()) {
186
209
  if (!this[_transplantFilter](node))
187
- node.parent = null
210
+ node.root = null
188
211
  }
189
212
 
190
213
  root.replace(this[_actualTree])
214
+ for (const node of this[_actualTree].fsChildren)
215
+ node.root = this[_transplantFilter](node) ? root : null
216
+
191
217
  this[_actualTree] = root
192
218
  }
193
219
 
@@ -93,7 +93,8 @@ module.exports = cls => class VirtualLoader extends cls {
93
93
  this.virtualTree = root
94
94
  const {links, nodes} = this[resolveNodes](s, root)
95
95
  await this[resolveLinks](links, nodes)
96
- this[assignBundles](nodes)
96
+ if (!(s.originalLockfileVersion >= 2))
97
+ this[assignBundles](nodes)
97
98
  if (this[flagsSuspect])
98
99
  this[reCalcDepFlags](nodes.values())
99
100
  return root
@@ -220,22 +221,24 @@ module.exports = cls => class VirtualLoader extends cls {
220
221
  [assignBundles] (nodes) {
221
222
  for (const [location, node] of nodes) {
222
223
  // Skip assignment of parentage for the root package
223
- if (!location)
224
+ if (!location || node.target && !node.target.location)
224
225
  continue
225
226
  const { name, parent, package: { inBundle }} = node
227
+
226
228
  if (!parent)
227
229
  continue
228
230
 
229
231
  // read inBundle from package because 'package' here is
230
232
  // actually a v2 lockfile metadata entry.
231
- // If the *parent* is also bundled, though, then we assume
232
- // that it's being pulled in just by virtue of that.
233
+ // If the *parent* is also bundled, though, or if the parent has
234
+ // no dependency on it, then we assume that it's being pulled in
235
+ // just by virtue of its parent or a transitive dep being bundled.
233
236
  const { package: ppkg } = parent
234
237
  const { inBundle: parentBundled } = ppkg
235
- if (inBundle && !parentBundled) {
238
+ if (inBundle && !parentBundled && parent.edgesOut.has(node.name)) {
236
239
  if (!ppkg.bundleDependencies)
237
240
  ppkg.bundleDependencies = [name]
238
- else if (!ppkg.bundleDependencies.includes(name))
241
+ else
239
242
  ppkg.bundleDependencies.push(name)
240
243
  }
241
244
  }