@npmcli/arborist 2.7.1 → 2.8.3

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.
Files changed (50) hide show
  1. package/bin/actual.js +4 -2
  2. package/bin/audit.js +12 -6
  3. package/bin/dedupe.js +49 -0
  4. package/bin/funding.js +4 -2
  5. package/bin/ideal.js +2 -1
  6. package/bin/lib/logging.js +4 -3
  7. package/bin/lib/options.js +14 -12
  8. package/bin/lib/timers.js +6 -3
  9. package/bin/license.js +9 -5
  10. package/bin/prune.js +6 -3
  11. package/bin/reify.js +6 -3
  12. package/bin/virtual.js +4 -2
  13. package/lib/add-rm-pkg-deps.js +25 -14
  14. package/lib/arborist/audit.js +2 -1
  15. package/lib/arborist/build-ideal-tree.js +246 -757
  16. package/lib/arborist/deduper.js +2 -1
  17. package/lib/arborist/index.js +8 -4
  18. package/lib/arborist/load-actual.js +32 -15
  19. package/lib/arborist/load-virtual.js +34 -18
  20. package/lib/arborist/load-workspaces.js +4 -2
  21. package/lib/arborist/rebuild.js +31 -16
  22. package/lib/arborist/reify.js +332 -119
  23. package/lib/audit-report.js +42 -22
  24. package/lib/calc-dep-flags.js +18 -9
  25. package/lib/can-place-dep.js +430 -0
  26. package/lib/case-insensitive-map.js +50 -0
  27. package/lib/consistent-resolve.js +2 -1
  28. package/lib/deepest-nesting-target.js +18 -0
  29. package/lib/dep-valid.js +8 -4
  30. package/lib/diff.js +74 -22
  31. package/lib/edge.js +29 -14
  32. package/lib/gather-dep-set.js +2 -1
  33. package/lib/inventory.js +12 -6
  34. package/lib/link.js +14 -9
  35. package/lib/node.js +269 -118
  36. package/lib/optional-set.js +4 -2
  37. package/lib/peer-entry-sets.js +77 -0
  38. package/lib/place-dep.js +578 -0
  39. package/lib/printable.js +48 -18
  40. package/lib/realpath.js +12 -6
  41. package/lib/shrinkwrap.js +168 -91
  42. package/lib/signal-handling.js +6 -3
  43. package/lib/spec-from-lock.js +7 -4
  44. package/lib/tracker.js +24 -18
  45. package/lib/tree-check.js +12 -6
  46. package/lib/version-from-tgz.js +4 -2
  47. package/lib/vuln.js +28 -16
  48. package/lib/yarn-lock.js +27 -15
  49. package/package.json +9 -13
  50. package/lib/peer-set.js +0 -25
@@ -5,11 +5,14 @@ const pacote = require('pacote')
5
5
  const AuditReport = require('../audit-report.js')
6
6
  const {subset, intersects} = require('semver')
7
7
  const npa = require('npm-package-arg')
8
+ const debug = require('../debug.js')
9
+ const walkUp = require('walk-up-path')
8
10
 
9
11
  const {dirname, resolve, relative} = require('path')
10
12
  const {depth: dfwalk} = require('treeverse')
11
13
  const fs = require('fs')
12
14
  const {promisify} = require('util')
15
+ const lstat = promisify(fs.lstat)
13
16
  const symlink = promisify(fs.symlink)
14
17
  const mkdirp = require('mkdirp-infer-owner')
15
18
  const justMkdirp = require('mkdirp')
@@ -18,6 +21,7 @@ const rimraf = promisify(require('rimraf'))
18
21
  const PackageJson = require('@npmcli/package-json')
19
22
  const packageContents = require('@npmcli/installed-package-contents')
20
23
  const { checkEngine, checkPlatform } = require('npm-install-checks')
24
+ const _force = Symbol.for('force')
21
25
 
22
26
  const treeCheck = require('../tree-check.js')
23
27
  const relpath = require('../relpath.js')
@@ -50,6 +54,7 @@ const _createSparseTree = Symbol.for('createSparseTree')
50
54
  const _loadShrinkwrapsAndUpdateTrees = Symbol.for('loadShrinkwrapsAndUpdateTrees')
51
55
  const _shrinkwrapInflated = Symbol('shrinkwrapInflated')
52
56
  const _bundleUnpacked = Symbol('bundleUnpacked')
57
+ const _bundleMissing = Symbol('bundleMissing')
53
58
  const _reifyNode = Symbol.for('reifyNode')
54
59
  const _extractOrLink = Symbol('extractOrLink')
55
60
  // defined by rebuild mixin
@@ -74,8 +79,10 @@ const _copyIdealToActual = Symbol('copyIdealToActual')
74
79
  const _addOmitsToTrashList = Symbol('addOmitsToTrashList')
75
80
  const _packageLockOnly = Symbol('packageLockOnly')
76
81
  const _dryRun = Symbol('dryRun')
82
+ const _validateNodeModules = Symbol('validateNodeModules')
83
+ const _nmValidated = Symbol('nmValidated')
77
84
  const _validatePath = Symbol('validatePath')
78
- const _reifyPackages = Symbol('reifyPackages')
85
+ const _reifyPackages = Symbol.for('reifyPackages')
79
86
 
80
87
  const _omitDev = Symbol('omitDev')
81
88
  const _omitOptional = Symbol('omitOptional')
@@ -83,8 +90,9 @@ const _omitPeer = Symbol('omitPeer')
83
90
 
84
91
  const _global = Symbol.for('global')
85
92
 
93
+ const _pruneBundledMetadeps = Symbol('pruneBundledMetadeps')
94
+
86
95
  // defined by Ideal mixin
87
- const _pruneBundledMetadeps = Symbol.for('pruneBundledMetadeps')
88
96
  const _resolvedAdd = Symbol.for('resolvedAdd')
89
97
  const _usePackageLock = Symbol.for('usePackageLock')
90
98
  const _formatPackageLock = Symbol.for('formatPackageLock')
@@ -112,6 +120,11 @@ module.exports = cls => class Reifier extends cls {
112
120
  this[_sparseTreeDirs] = new Set()
113
121
  this[_sparseTreeRoots] = new Set()
114
122
  this[_trashList] = new Set()
123
+ // the nodes we unpack to read their bundles
124
+ this[_bundleUnpacked] = new Set()
125
+ // child nodes we'd EXPECT to be included in a bundle, but aren't
126
+ this[_bundleMissing] = new Set()
127
+ this[_nmValidated] = new Set()
115
128
  }
116
129
 
117
130
  // public method
@@ -145,19 +158,24 @@ module.exports = cls => class Reifier extends cls {
145
158
 
146
159
  async [_validatePath] () {
147
160
  // don't create missing dirs on dry runs
148
- if (this[_packageLockOnly] || this[_dryRun])
161
+ if (this[_packageLockOnly] || this[_dryRun]) {
149
162
  return
163
+ }
150
164
 
151
165
  // we do NOT want to set ownership on this folder, especially
152
166
  // recursively, because it can have other side effects to do that
153
167
  // in a project directory. We just want to make it if it's missing.
154
168
  await justMkdirp(resolve(this.path))
169
+
170
+ // do not allow the top-level node_modules to be a symlink
171
+ await this[_validateNodeModules](resolve(this.path, 'node_modules'))
155
172
  }
156
173
 
157
174
  async [_reifyPackages] () {
158
175
  // we don't submit the audit report or write to disk on dry runs
159
- if (this[_dryRun])
176
+ if (this[_dryRun]) {
160
177
  return
178
+ }
161
179
 
162
180
  if (this[_packageLockOnly]) {
163
181
  // we already have the complete tree, so just audit it now,
@@ -204,8 +222,9 @@ module.exports = cls => class Reifier extends cls {
204
222
  for (const action of actions) {
205
223
  try {
206
224
  await this[action]()
207
- if (reifyTerminated)
225
+ if (reifyTerminated) {
208
226
  throw reifyTerminated
227
+ }
209
228
  } catch (er) {
210
229
  await this[rollback](er)
211
230
  /* istanbul ignore next - rollback throws, should never hit this */
@@ -217,8 +236,9 @@ module.exports = cls => class Reifier extends cls {
217
236
  // no rollback for this one, just exit with the error, since the
218
237
  // install completed and can't be safely recovered at this point.
219
238
  await this[_removeTrash]()
220
- if (reifyTerminated)
239
+ if (reifyTerminated) {
221
240
  throw reifyTerminated
241
+ }
222
242
 
223
243
  // done modifying the file system, no need to keep listening for sigs
224
244
  removeHandler()
@@ -245,18 +265,21 @@ module.exports = cls => class Reifier extends cls {
245
265
  filter: (node, kid) => {
246
266
  // if it's not the project root, and we have no explicit requests,
247
267
  // then we're already into a nested dep, so we keep it
248
- if (this.explicitRequests.size === 0 || !node.isProjectRoot)
268
+ if (this.explicitRequests.size === 0 || !node.isProjectRoot) {
249
269
  return true
270
+ }
250
271
 
251
272
  // if we added it as an edgeOut, then we want it
252
- if (this.idealTree.edgesOut.has(kid))
273
+ if (this.idealTree.edgesOut.has(kid)) {
253
274
  return true
275
+ }
254
276
 
255
277
  // if it's an explicit request, then we want it
256
278
  const hasExplicit = [...this.explicitRequests]
257
279
  .some(edge => edge.name === kid)
258
- if (hasExplicit)
280
+ if (hasExplicit) {
259
281
  return true
282
+ }
260
283
 
261
284
  // ignore the rest of the global install folder
262
285
  return false
@@ -264,8 +287,10 @@ module.exports = cls => class Reifier extends cls {
264
287
  } : { ignoreMissing: true }
265
288
 
266
289
  if (!this[_global]) {
267
- return Promise.all([this.loadActual(actualOpt), this.buildIdealTree(bitOpt)])
268
- .then(() => process.emit('timeEnd', 'reify:loadTrees'))
290
+ return Promise.all([
291
+ this.loadActual(actualOpt),
292
+ this.buildIdealTree(bitOpt),
293
+ ]).then(() => process.emit('timeEnd', 'reify:loadTrees'))
269
294
  }
270
295
 
271
296
  // the global install space tends to have a lot of stuff in it. don't
@@ -279,8 +304,9 @@ module.exports = cls => class Reifier extends cls {
279
304
  }
280
305
 
281
306
  [_diffTrees] () {
282
- if (this[_packageLockOnly])
307
+ if (this[_packageLockOnly]) {
283
308
  return
309
+ }
284
310
 
285
311
  process.emit('time', 'reify:diffTrees')
286
312
  // XXX if we have an existing diff already, there should be a way
@@ -295,20 +321,24 @@ module.exports = cls => class Reifier extends cls {
295
321
  // children where there's an explicit request.
296
322
  for (const { name } of this.explicitRequests) {
297
323
  const ideal = idealTree.children.get(name)
298
- if (ideal)
324
+ if (ideal) {
299
325
  filterNodes.push(ideal)
326
+ }
300
327
  const actual = actualTree.children.get(name)
301
- if (actual)
328
+ if (actual) {
302
329
  filterNodes.push(actual)
330
+ }
303
331
  }
304
332
  } else {
305
333
  for (const ws of this[_workspaces]) {
306
334
  const ideal = this.idealTree.children.get(ws)
307
- if (ideal)
335
+ if (ideal) {
308
336
  filterNodes.push(ideal)
337
+ }
309
338
  const actual = this.actualTree.children.get(ws)
310
- if (actual)
339
+ if (actual) {
311
340
  filterNodes.push(actual)
341
+ }
312
342
  }
313
343
  }
314
344
 
@@ -321,12 +351,13 @@ module.exports = cls => class Reifier extends cls {
321
351
  ideal: this.idealTree,
322
352
  })
323
353
 
324
- for (const node of this.diff.removed) {
325
- // a node in a dep bundle will only be removed if its bundling dep
326
- // is removed as well. in which case, we don't have to delete it!
327
- if (!node.inDepBundle)
328
- this[_addNodeToTrashList](node)
329
- }
354
+ // we don't have to add 'removed' folders to the trashlist, because
355
+ // they'll be moved aside to a retirement folder, and then the retired
356
+ // folder will be deleted at the end. This is important when we have
357
+ // a folder like FOO being "removed" in favor of a folder like "foo",
358
+ // because if we remove node_modules/FOO on case-insensitive systems,
359
+ // it will remove the dep that we *want* at node_modules/foo.
360
+
330
361
  process.emit('timeEnd', 'reify:diffTrees')
331
362
  }
332
363
 
@@ -334,7 +365,7 @@ module.exports = cls => class Reifier extends cls {
334
365
  // removed later on in the process. optionally, also mark them
335
366
  // as a retired paths, so that we move them out of the way and
336
367
  // replace them when rolling back on failure.
337
- [_addNodeToTrashList] (node, retire) {
368
+ [_addNodeToTrashList] (node, retire = false) {
338
369
  const paths = [node.path, ...node.binPaths]
339
370
  const moves = this[_retiredPaths]
340
371
  this.log.silly('reify', 'mark', retire ? 'retired' : 'deleted', paths)
@@ -343,8 +374,9 @@ module.exports = cls => class Reifier extends cls {
343
374
  const retired = retirePath(path)
344
375
  moves[path] = retired
345
376
  this[_trashList].add(retired)
346
- } else
377
+ } else {
347
378
  this[_trashList].add(path)
379
+ }
348
380
  }
349
381
  }
350
382
 
@@ -376,10 +408,11 @@ module.exports = cls => class Reifier extends cls {
376
408
  if (er.code === 'ENOENT') {
377
409
  return didMkdirp ? null : mkdirp(dirname(to)).then(() =>
378
410
  this[_renamePath](from, to, true))
379
- } else if (er.code === 'EEXIST')
411
+ } else if (er.code === 'EEXIST') {
380
412
  return rimraf(to).then(() => moveFile(from, to))
381
- else
413
+ } else {
382
414
  throw er
415
+ }
383
416
  })
384
417
  }
385
418
 
@@ -400,8 +433,9 @@ module.exports = cls => class Reifier extends cls {
400
433
  // adding to the trash list will skip reifying, and delete them
401
434
  // if they are currently in the tree and otherwise untouched.
402
435
  [_addOmitsToTrashList] () {
403
- if (!this[_omitDev] && !this[_omitOptional] && !this[_omitPeer])
436
+ if (!this[_omitDev] && !this[_omitOptional] && !this[_omitPeer]) {
404
437
  return
438
+ }
405
439
 
406
440
  process.emit('time', 'reify:trashOmits')
407
441
 
@@ -412,8 +446,9 @@ module.exports = cls => class Reifier extends cls {
412
446
  node.optional && this[_omitOptional] ||
413
447
  node.devOptional && this[_omitOptional] && this[_omitDev])
414
448
 
415
- for (const node of this.idealTree.inventory.filter(filter))
449
+ for (const node of this.idealTree.inventory.filter(filter)) {
416
450
  this[_addNodeToTrashList](node)
451
+ }
417
452
 
418
453
  process.emit('timeEnd', 'reify:trashOmits')
419
454
  }
@@ -422,19 +457,42 @@ module.exports = cls => class Reifier extends cls {
422
457
  process.emit('time', 'reify:createSparse')
423
458
  // if we call this fn again, we look for the previous list
424
459
  // so that we can avoid making the same directory multiple times
425
- const dirs = this.diff.leaves
460
+ const leaves = this.diff.leaves
426
461
  .filter(diff => {
427
462
  return (diff.action === 'ADD' || diff.action === 'CHANGE') &&
428
463
  !this[_sparseTreeDirs].has(diff.ideal.path) &&
429
464
  !diff.ideal.isLink
430
465
  })
431
- .map(diff => diff.ideal.path)
432
-
433
- return promiseAllRejectLate(dirs.map(d => mkdirp(d)))
434
- .then(made => {
435
- made.forEach(made => this[_sparseTreeRoots].add(made))
436
- dirs.forEach(dir => this[_sparseTreeDirs].add(dir))
437
- })
466
+ .map(diff => diff.ideal)
467
+
468
+ // we check this in parallel, so guard against multiple attempts to
469
+ // retire the same path at the same time.
470
+ const dirsChecked = new Set()
471
+ return promiseAllRejectLate(leaves.map(async node => {
472
+ for (const d of walkUp(node.path)) {
473
+ if (d === node.top.path) {
474
+ break
475
+ }
476
+ if (dirsChecked.has(d)) {
477
+ continue
478
+ }
479
+ dirsChecked.add(d)
480
+ const st = await lstat(d).catch(er => null)
481
+ // this can happen if we have a link to a package with a name
482
+ // that the filesystem treats as if it is the same thing.
483
+ // would be nice to have conditional istanbul ignores here...
484
+ /* istanbul ignore next - defense in depth */
485
+ if (st && !st.isDirectory()) {
486
+ const retired = retirePath(d)
487
+ this[_retiredPaths][d] = retired
488
+ this[_trashList].add(retired)
489
+ await this[_renamePath](d, retired)
490
+ }
491
+ }
492
+ const made = await mkdirp(node.path)
493
+ this[_sparseTreeDirs].add(node.path)
494
+ this[_sparseTreeRoots].add(made)
495
+ }))
438
496
  .then(() => process.emit('timeEnd', 'reify:createSparse'))
439
497
  }
440
498
 
@@ -449,8 +507,9 @@ module.exports = cls => class Reifier extends cls {
449
507
  .map(path => rimraf(path).catch(er => failures.push([path, er])))
450
508
  return promiseAllRejectLate(unlinks)
451
509
  .then(() => {
452
- if (failures.length)
510
+ if (failures.length) {
453
511
  this.log.warn('cleanup', 'Failed to remove some directories', failures)
512
+ }
454
513
  })
455
514
  .then(() => process.emit('timeEnd', 'reify:rollback:createSparse'))
456
515
  .then(() => this[_rollbackRetireShallowNodes](er))
@@ -466,8 +525,9 @@ module.exports = cls => class Reifier extends cls {
466
525
  d.ideal.hasShrinkwrap && !seen.has(d.ideal) &&
467
526
  !this[_trashList].has(d.ideal.path))
468
527
 
469
- if (!shrinkwraps.length)
528
+ if (!shrinkwraps.length) {
470
529
  return
530
+ }
471
531
 
472
532
  process.emit('time', 'reify:loadShrinkwraps')
473
533
 
@@ -497,8 +557,9 @@ module.exports = cls => class Reifier extends cls {
497
557
  // to the trash list
498
558
  // Always return the node.
499
559
  [_reifyNode] (node) {
500
- if (this[_trashList].has(node.path))
560
+ if (this[_trashList].has(node.path)) {
501
561
  return node
562
+ }
502
563
 
503
564
  const timer = `reifyNode:${node.location}`
504
565
  process.emit('time', timer)
@@ -529,7 +590,21 @@ module.exports = cls => class Reifier extends cls {
529
590
  })
530
591
  }
531
592
 
532
- [_extractOrLink] (node) {
593
+ // do not allow node_modules to be a symlink
594
+ async [_validateNodeModules] (nm) {
595
+ if (this[_force] || this[_nmValidated].has(nm)) {
596
+ return
597
+ }
598
+ const st = await lstat(nm).catch(() => null)
599
+ if (!st || st.isDirectory()) {
600
+ this[_nmValidated].add(nm)
601
+ return
602
+ }
603
+ this.log.warn('reify', 'Removing non-directory', nm)
604
+ await rimraf(nm)
605
+ }
606
+
607
+ async [_extractOrLink] (node) {
533
608
  // in normal cases, node.resolved should *always* be set by now.
534
609
  // however, it is possible when a lockfile is damaged, or very old,
535
610
  // or in some other race condition bugs in npm v6, that a previously
@@ -556,13 +631,29 @@ module.exports = cls => class Reifier extends cls {
556
631
  return
557
632
  }
558
633
 
559
- return node.isLink
560
- ? rimraf(node.path).then(() => this[_symlink](node))
561
- : pacote.extract(res, node.path, {
634
+ const nm = resolve(node.parent.path, 'node_modules')
635
+ await this[_validateNodeModules](nm)
636
+
637
+ if (node.isLink) {
638
+ await rimraf(node.path)
639
+ await this[_symlink](node)
640
+ } else {
641
+ await debug(async () => {
642
+ const st = await lstat(node.path).catch(e => null)
643
+ if (st && !st.isDirectory()) {
644
+ debug.log('unpacking into a non-directory', node)
645
+ throw Object.assign(new Error('ENOTDIR: not a directory'), {
646
+ code: 'ENOTDIR',
647
+ path: node.path,
648
+ })
649
+ }
650
+ })
651
+ await pacote.extract(res, node.path, {
562
652
  ...this.options,
563
653
  resolved: node.resolved,
564
654
  integrity: node.integrity,
565
655
  })
656
+ }
566
657
  }
567
658
 
568
659
  async [_symlink] (node) {
@@ -575,8 +666,9 @@ module.exports = cls => class Reifier extends cls {
575
666
 
576
667
  [_warnDeprecated] (node) {
577
668
  const {_id, deprecated} = node.package
578
- if (deprecated)
669
+ if (deprecated) {
579
670
  this.log.warn('deprecated', `${_id}: ${deprecated}`)
671
+ }
580
672
  }
581
673
 
582
674
  // if the node is optional, then the failure of the promise is nonfatal
@@ -611,9 +703,9 @@ module.exports = cls => class Reifier extends cls {
611
703
  depth = 0, bundlesByDepth = this[_getBundlesByDepth]()
612
704
  ) {
613
705
  if (depth === 0) {
614
- this[_bundleUnpacked] = new Set()
615
706
  process.emit('time', 'reify:loadBundles')
616
707
  }
708
+
617
709
  const maxBundleDepth = bundlesByDepth.get('maxBundleDepth')
618
710
  if (depth > maxBundleDepth) {
619
711
  // if we did something, then prune the tree and update the diffs
@@ -632,8 +724,9 @@ module.exports = cls => class Reifier extends cls {
632
724
  node.target !== node.root &&
633
725
  !this[_trashList].has(node.path))
634
726
 
635
- if (!set.length)
727
+ if (!set.length) {
636
728
  return this[_loadBundlesAndUpdateTrees](depth + 1, bundlesByDepth)
729
+ }
637
730
 
638
731
  // extract all the nodes with bundles
639
732
  return promiseAllRejectLate(set.map(node => {
@@ -642,14 +735,32 @@ module.exports = cls => class Reifier extends cls {
642
735
  }))
643
736
  // then load their unpacked children and move into the ideal tree
644
737
  .then(nodes =>
645
- promiseAllRejectLate(nodes.map(node => new this.constructor({
646
- ...this.options,
647
- path: node.path,
648
- }).loadActual({
649
- root: node,
650
- // don't transplant any sparse folders we created
651
- transplantFilter: node => node.package._id,
652
- }))))
738
+ promiseAllRejectLate(nodes.map(async node => {
739
+ const arb = new this.constructor({
740
+ ...this.options,
741
+ path: node.path,
742
+ })
743
+ const notTransplanted = new Set(node.children.keys())
744
+ await arb.loadActual({
745
+ root: node,
746
+ // don't transplant any sparse folders we created
747
+ // loadActual will set node.package to {} for empty directories
748
+ // if by chance there are some empty folders in the node_modules
749
+ // tree for some other reason, then ok, ignore those too.
750
+ transplantFilter: node => {
751
+ if (node.package._id) {
752
+ // it's actually in the bundle if it gets transplanted
753
+ notTransplanted.delete(node.name)
754
+ return true
755
+ } else {
756
+ return false
757
+ }
758
+ },
759
+ })
760
+ for (const name of notTransplanted) {
761
+ this[_bundleMissing].add(node.children.get(name))
762
+ }
763
+ })))
653
764
  // move onto the next level of bundled items
654
765
  .then(() => this[_loadBundlesAndUpdateTrees](depth + 1, bundlesByDepth))
655
766
  }
@@ -661,18 +772,21 @@ module.exports = cls => class Reifier extends cls {
661
772
  tree: this.diff,
662
773
  visit: diff => {
663
774
  const node = diff.ideal
664
- if (!node)
775
+ if (!node) {
665
776
  return
666
- if (node.isProjectRoot)
777
+ }
778
+ if (node.isProjectRoot) {
667
779
  return
780
+ }
668
781
 
669
782
  const { bundleDependencies } = node.package
670
783
  if (bundleDependencies && bundleDependencies.length) {
671
784
  maxBundleDepth = Math.max(maxBundleDepth, node.depth)
672
- if (!bundlesByDepth.has(node.depth))
785
+ if (!bundlesByDepth.has(node.depth)) {
673
786
  bundlesByDepth.set(node.depth, [node])
674
- else
787
+ } else {
675
788
  bundlesByDepth.get(node.depth).push(node)
789
+ }
676
790
  }
677
791
  },
678
792
  getChildren: diff => diff.children,
@@ -685,57 +799,95 @@ module.exports = cls => class Reifier extends cls {
685
799
  // https://github.com/npm/cli/issues/1597#issuecomment-667639545
686
800
  [_pruneBundledMetadeps] (bundlesByDepth) {
687
801
  const bundleShadowed = new Set()
802
+
803
+ // Example dep graph:
804
+ // root -> (a, c)
805
+ // a -> BUNDLE(b)
806
+ // b -> c
807
+ // c -> b
808
+ //
809
+ // package tree:
810
+ // root
811
+ // +-- a
812
+ // | +-- b(1)
813
+ // | +-- c(1)
814
+ // +-- b(2)
815
+ // +-- c(2)
816
+ // 1. mark everything that's shadowed by anything in the bundle. This
817
+ // marks b(2) and c(2).
818
+ // 2. anything with edgesIn from outside the set, mark not-extraneous,
819
+ // remove from set. This unmarks c(2).
820
+ // 3. continue until no change
821
+ // 4. remove everything in the set from the tree. b(2) is pruned
822
+
688
823
  // create the list of nodes shadowed by children of bundlers
689
824
  for (const bundles of bundlesByDepth.values()) {
690
825
  // skip the 'maxBundleDepth' item
691
- if (!Array.isArray(bundles))
826
+ if (!Array.isArray(bundles)) {
692
827
  continue
828
+ }
693
829
  for (const node of bundles) {
694
830
  for (const name of node.children.keys()) {
695
831
  const shadow = node.parent.resolve(name)
696
- if (!shadow)
832
+ if (!shadow) {
697
833
  continue
834
+ }
698
835
  bundleShadowed.add(shadow)
699
836
  shadow.extraneous = true
700
837
  }
701
838
  }
702
839
  }
703
- let changed = true
704
- while (changed) {
705
- changed = false
706
- for (const shadow of bundleShadowed) {
707
- if (!shadow.extraneous) {
708
- bundleShadowed.delete(shadow)
709
- continue
840
+
841
+ // lib -> (a@1.x) BUNDLE(a@1.2.3 (b@1.2.3))
842
+ // a@1.2.3 -> (b@1.2.3)
843
+ // a@1.3.0 -> (b@2)
844
+ // b@1.2.3 -> ()
845
+ // b@2 -> (c@2)
846
+ //
847
+ // root
848
+ // +-- lib
849
+ // | +-- a@1.2.3
850
+ // | +-- b@1.2.3
851
+ // +-- b@2 <-- shadowed, now extraneous
852
+ // +-- c@2 <-- also shadowed, because only dependent is shadowed
853
+ for (const shadow of bundleShadowed) {
854
+ for (const shadDep of shadow.edgesOut.values()) {
855
+ /* istanbul ignore else - pretty unusual situation, just being
856
+ * defensive here. Would mean that a bundled dep has a dependency
857
+ * that is unmet. which, weird, but if you bundle it, we take
858
+ * whatever you put there and assume the publisher knows best. */
859
+ if (shadDep.to) {
860
+ bundleShadowed.add(shadDep.to)
861
+ shadDep.to.extraneous = true
710
862
  }
863
+ }
864
+ }
711
865
 
866
+ let changed
867
+ do {
868
+ changed = false
869
+ for (const shadow of bundleShadowed) {
712
870
  for (const edge of shadow.edgesIn) {
713
- if (!edge.from.extraneous) {
871
+ if (!bundleShadowed.has(edge.from)) {
714
872
  shadow.extraneous = false
715
873
  bundleShadowed.delete(shadow)
716
874
  changed = true
717
- } else {
718
- for (const shadDep of shadow.edgesOut.values()) {
719
- /* istanbul ignore else - pretty unusual situation, just being
720
- * defensive here. Would mean that a bundled dep has a dependency
721
- * that is unmet. which, weird, but if you bundle it, we take
722
- * whatever you put there and assume the publisher knows best. */
723
- if (shadDep.to)
724
- bundleShadowed.add(shadDep.to)
725
- }
875
+ break
726
876
  }
727
877
  }
728
878
  }
729
- }
879
+ } while (changed)
880
+
730
881
  for (const shadow of bundleShadowed) {
731
- shadow.parent = null
732
882
  this[_addNodeToTrashList](shadow)
883
+ shadow.root = null
733
884
  }
734
885
  }
735
886
 
736
887
  [_submitQuickAudit] () {
737
- if (this.options.audit === false)
888
+ if (this.options.audit === false) {
738
889
  return this.auditReport = null
890
+ }
739
891
 
740
892
  // we submit the quick audit at this point in the process, as soon as
741
893
  // we have all the deps resolved, so that it can overlap with the other
@@ -748,8 +900,9 @@ module.exports = cls => class Reifier extends cls {
748
900
  const tree = this.idealTree
749
901
 
750
902
  // if we're operating on a workspace, only audit the workspace deps
751
- if (this[_workspaces] && this[_workspaces].length)
903
+ if (this[_workspaces] && this[_workspaces].length) {
752
904
  options.filterSet = this.workspaceDependencySet(tree, this[_workspaces])
905
+ }
753
906
 
754
907
  this.auditReport = AuditReport.load(tree, options)
755
908
  .then(res => {
@@ -774,23 +927,30 @@ module.exports = cls => class Reifier extends cls {
774
927
  tree: this.diff,
775
928
  visit: diff => {
776
929
  // no unpacking if we don't want to change this thing
777
- if (diff.action !== 'CHANGE' && diff.action !== 'ADD')
930
+ if (diff.action !== 'CHANGE' && diff.action !== 'ADD') {
778
931
  return
932
+ }
779
933
 
780
934
  const node = diff.ideal
781
935
  const bd = this[_bundleUnpacked].has(node)
782
936
  const sw = this[_shrinkwrapInflated].has(node)
937
+ const bundleMissing = this[_bundleMissing].has(node)
783
938
 
784
939
  // check whether we still need to unpack this one.
785
940
  // test the inDepBundle last, since that's potentially a tree walk.
786
941
  const doUnpack = node && // can't unpack if removed!
787
- !node.isRoot && // root node already exists
788
- !bd && // already unpacked to read bundle
789
- !sw && // already unpacked to read sw
790
- !node.inDepBundle // already unpacked by another dep's bundle
791
-
792
- if (doUnpack)
942
+ // root node already exists
943
+ !node.isRoot &&
944
+ // already unpacked to read bundle
945
+ !bd &&
946
+ // already unpacked to read sw
947
+ !sw &&
948
+ // already unpacked by another dep's bundle
949
+ (bundleMissing || !node.inDepBundle)
950
+
951
+ if (doUnpack) {
793
952
  unpacks.push(this[_reifyNode](node))
953
+ }
794
954
  },
795
955
  getChildren: diff => diff.children,
796
956
  })
@@ -814,17 +974,38 @@ module.exports = cls => class Reifier extends cls {
814
974
  const moves = this[_retiredPaths]
815
975
  this[_retiredUnchanged] = {}
816
976
  return promiseAllRejectLate(this.diff.children.map(diff => {
817
- const realFolder = (diff.actual || diff.ideal).path
977
+ // skip if nothing was retired
978
+ if (diff.action !== 'CHANGE' && diff.action !== 'REMOVE') {
979
+ return
980
+ }
981
+
982
+ const { path: realFolder } = diff.actual
818
983
  const retireFolder = moves[realFolder]
984
+ /* istanbul ignore next - should be impossible */
985
+ debug(() => {
986
+ if (!retireFolder) {
987
+ const er = new Error('trying to un-retire but not retired')
988
+ throw Object.assign(er, {
989
+ realFolder,
990
+ retireFolder,
991
+ actual: diff.actual,
992
+ ideal: diff.ideal,
993
+ action: diff.action,
994
+ })
995
+ }
996
+ })
997
+
819
998
  this[_retiredUnchanged][retireFolder] = []
820
999
  return promiseAllRejectLate(diff.unchanged.map(node => {
821
1000
  // no need to roll back links, since we'll just delete them anyway
822
- if (node.isLink)
1001
+ if (node.isLink) {
823
1002
  return mkdirp(dirname(node.path)).then(() => this[_reifyNode](node))
1003
+ }
824
1004
 
825
1005
  // will have been moved/unpacked along with bundler
826
- if (node.inDepBundle)
1006
+ if (node.inDepBundle && !this[_bundleMissing].has(node)) {
827
1007
  return
1008
+ }
828
1009
 
829
1010
  this[_retiredUnchanged][retireFolder].push(node)
830
1011
 
@@ -878,8 +1059,9 @@ module.exports = cls => class Reifier extends cls {
878
1059
  dfwalk({
879
1060
  tree: this.diff,
880
1061
  leave: diff => {
881
- if (!diff.ideal.isProjectRoot)
1062
+ if (!diff.ideal.isProjectRoot) {
882
1063
  nodes.push(diff.ideal)
1064
+ }
883
1065
  },
884
1066
  // process adds before changes, ignore removals
885
1067
  getChildren: diff => diff && diff.children,
@@ -894,8 +1076,9 @@ module.exports = cls => class Reifier extends cls {
894
1076
  // skip links that only live within node_modules as they are most
895
1077
  // likely managed by packages we installed, we only want to rebuild
896
1078
  // unchanged links we directly manage
897
- if (node.isLink && node.target.fsTop === tree)
1079
+ if (node.isLink && node.target.fsTop === tree) {
898
1080
  nodes.push(node)
1081
+ }
899
1082
  }
900
1083
 
901
1084
  return this.rebuild({ nodes, handleOptionalFailure: true })
@@ -912,12 +1095,14 @@ module.exports = cls => class Reifier extends cls {
912
1095
  const failures = []
913
1096
  const rm = path => rimraf(path).catch(er => failures.push([path, er]))
914
1097
 
915
- for (const path of this[_trashList])
1098
+ for (const path of this[_trashList]) {
916
1099
  promises.push(rm(path))
1100
+ }
917
1101
 
918
1102
  return promiseAllRejectLate(promises).then(() => {
919
- if (failures.length)
1103
+ if (failures.length) {
920
1104
  this.log.warn('cleanup', 'Failed to remove some directories', failures)
1105
+ }
921
1106
  })
922
1107
  .then(() => process.emit('timeEnd', 'reify:trash'))
923
1108
  }
@@ -931,8 +1116,9 @@ module.exports = cls => class Reifier extends cls {
931
1116
  // save it first, then prune out the optional trash, and then return it.
932
1117
 
933
1118
  // support save=false option
934
- if (options.save === false || this[_global] || this[_dryRun])
1119
+ if (options.save === false || this[_global] || this[_dryRun]) {
935
1120
  return false
1121
+ }
936
1122
 
937
1123
  process.emit('time', 'reify:save')
938
1124
 
@@ -953,6 +1139,14 @@ module.exports = cls => class Reifier extends cls {
953
1139
  const spec = subSpec ? subSpec.rawSpec : rawSpec
954
1140
  const child = edge.to
955
1141
 
1142
+ // if we tried to install an optional dep, but it was a version
1143
+ // that we couldn't resolve, this MAY be missing. if we haven't
1144
+ // blown up by now, it's because it was not a problem, though, so
1145
+ // just move on.
1146
+ if (!child) {
1147
+ continue
1148
+ }
1149
+
956
1150
  let newSpec
957
1151
  if (req.registry) {
958
1152
  const version = child.version
@@ -969,8 +1163,9 @@ module.exports = cls => class Reifier extends cls {
969
1163
  !isRange ||
970
1164
  spec === '*' ||
971
1165
  subset(prefixRange, spec, { loose: true })
972
- )
1166
+ ) {
973
1167
  range = prefixRange
1168
+ }
974
1169
 
975
1170
  const pname = child.packageName
976
1171
  const alias = name !== pname
@@ -979,10 +1174,11 @@ module.exports = cls => class Reifier extends cls {
979
1174
  // save the git+https url if it has auth, otherwise shortcut
980
1175
  const h = req.hosted
981
1176
  const opt = { noCommittish: false }
982
- if (h.https && h.auth)
1177
+ if (h.https && h.auth) {
983
1178
  newSpec = `git+${h.https(opt)}`
984
- else
1179
+ } else {
985
1180
  newSpec = h.shortcut(opt)
1181
+ }
986
1182
  } else if (req.type === 'directory' || req.type === 'file') {
987
1183
  // save the relative path in package.json
988
1184
  // Normally saveSpec is updated with the proper relative
@@ -992,34 +1188,41 @@ module.exports = cls => class Reifier extends cls {
992
1188
  const p = req.fetchSpec.replace(/^file:/, '')
993
1189
  const rel = relpath(addTree.realpath, p)
994
1190
  newSpec = `file:${rel}`
995
- } else
1191
+ } else {
996
1192
  newSpec = req.saveSpec
1193
+ }
997
1194
 
998
1195
  if (options.saveType) {
999
1196
  const depType = saveTypeMap.get(options.saveType)
1000
1197
  pkg[depType][name] = newSpec
1001
1198
  // rpj will have moved it here if it was in both
1002
1199
  // if it is empty it will be deleted later
1003
- if (options.saveType === 'prod' && pkg.optionalDependencies)
1200
+ if (options.saveType === 'prod' && pkg.optionalDependencies) {
1004
1201
  delete pkg.optionalDependencies[name]
1202
+ }
1005
1203
  } else {
1006
- if (hasSubKey(pkg, 'dependencies', name))
1204
+ if (hasSubKey(pkg, 'dependencies', name)) {
1007
1205
  pkg.dependencies[name] = newSpec
1206
+ }
1008
1207
 
1009
1208
  if (hasSubKey(pkg, 'devDependencies', name)) {
1010
1209
  pkg.devDependencies[name] = newSpec
1011
1210
  // don't update peer or optional if we don't have to
1012
- if (hasSubKey(pkg, 'peerDependencies', name) && !intersects(newSpec, pkg.peerDependencies[name]))
1211
+ if (hasSubKey(pkg, 'peerDependencies', name) && !intersects(newSpec, pkg.peerDependencies[name])) {
1013
1212
  pkg.peerDependencies[name] = newSpec
1213
+ }
1014
1214
 
1015
- if (hasSubKey(pkg, 'optionalDependencies', name) && !intersects(newSpec, pkg.optionalDependencies[name]))
1215
+ if (hasSubKey(pkg, 'optionalDependencies', name) && !intersects(newSpec, pkg.optionalDependencies[name])) {
1016
1216
  pkg.optionalDependencies[name] = newSpec
1217
+ }
1017
1218
  } else {
1018
- if (hasSubKey(pkg, 'peerDependencies', name))
1219
+ if (hasSubKey(pkg, 'peerDependencies', name)) {
1019
1220
  pkg.peerDependencies[name] = newSpec
1221
+ }
1020
1222
 
1021
- if (hasSubKey(pkg, 'optionalDependencies', name))
1223
+ if (hasSubKey(pkg, 'optionalDependencies', name)) {
1022
1224
  pkg.optionalDependencies[name] = newSpec
1225
+ }
1023
1226
  }
1024
1227
  }
1025
1228
 
@@ -1060,8 +1263,9 @@ module.exports = cls => class Reifier extends cls {
1060
1263
  }
1061
1264
 
1062
1265
  // grab any from explicitRequests that had deps removed
1063
- for (const { from: tree } of this.explicitRequests)
1266
+ for (const { from: tree } of this.explicitRequests) {
1064
1267
  updatedTrees.add(tree)
1268
+ }
1065
1269
 
1066
1270
  for (const tree of updatedTrees) {
1067
1271
  // refresh the edges so they have the correct specs
@@ -1075,8 +1279,9 @@ module.exports = cls => class Reifier extends cls {
1075
1279
  }
1076
1280
 
1077
1281
  async [_saveLockFile] (saveOpt) {
1078
- if (!this[_usePackageLock])
1282
+ if (!this[_usePackageLock]) {
1079
1283
  return
1284
+ }
1080
1285
 
1081
1286
  const { meta } = this.idealTree
1082
1287
 
@@ -1088,8 +1293,9 @@ module.exports = cls => class Reifier extends cls {
1088
1293
  for (const path of this[_trashList]) {
1089
1294
  const loc = relpath(this.idealTree.realpath, path)
1090
1295
  const node = this.idealTree.inventory.get(loc)
1091
- if (node && node.root === this.idealTree)
1296
+ if (node && node.root === this.idealTree) {
1092
1297
  node.parent = null
1298
+ }
1093
1299
  }
1094
1300
 
1095
1301
  // if we filtered to only certain nodes, then anything ELSE needs
@@ -1108,54 +1314,60 @@ module.exports = cls => class Reifier extends cls {
1108
1314
 
1109
1315
  // if it's an ideal node from the filter set, then skip it
1110
1316
  // because we already made whatever changes were necessary
1111
- if (filterSet.has(ideal))
1317
+ if (filterSet.has(ideal)) {
1112
1318
  continue
1319
+ }
1113
1320
 
1114
1321
  // otherwise, if it's not in the actualTree, then it's not a thing
1115
1322
  // that we actually added. And if it IS in the actualTree, then
1116
1323
  // it's something that we left untouched, so we need to record
1117
1324
  // that.
1118
1325
  const actual = this.actualTree.inventory.get(loc)
1119
- if (!actual)
1326
+ if (!actual) {
1120
1327
  ideal.root = null
1121
- else {
1328
+ } else {
1122
1329
  if ([...actual.linksIn].some(link => filterSet.has(link))) {
1123
1330
  seen.add(actual.location)
1124
1331
  continue
1125
1332
  }
1126
1333
  const { realpath, isLink } = actual
1127
- if (isLink && ideal.isLink && ideal.realpath === realpath)
1334
+ if (isLink && ideal.isLink && ideal.realpath === realpath) {
1128
1335
  continue
1129
- else
1336
+ } else {
1130
1337
  reroot.add(actual)
1338
+ }
1131
1339
  }
1132
1340
  }
1133
1341
 
1134
1342
  // now find any actual nodes that may not be present in the ideal
1135
1343
  // tree, but were left behind by virtue of not being in the filter
1136
1344
  for (const [loc, actual] of this.actualTree.inventory.entries()) {
1137
- if (seen.has(loc))
1345
+ if (seen.has(loc)) {
1138
1346
  continue
1347
+ }
1139
1348
  seen.add(loc)
1140
1349
 
1141
1350
  // we know that this is something that ISN'T in the idealTree,
1142
1351
  // or else we will have addressed it in the previous loop.
1143
1352
  // If it's in the filterSet, that means we intentionally removed
1144
1353
  // it, so nothing to do here.
1145
- if (filterSet.has(actual))
1354
+ if (filterSet.has(actual)) {
1146
1355
  continue
1356
+ }
1147
1357
 
1148
1358
  reroot.add(actual)
1149
1359
  }
1150
1360
 
1151
1361
  // go through the rerooted actual nodes, and move them over.
1152
- for (const actual of reroot)
1362
+ for (const actual of reroot) {
1153
1363
  actual.root = this.idealTree
1364
+ }
1154
1365
 
1155
1366
  // prune out any tops that lack a linkIn, they are no longer relevant.
1156
1367
  for (const top of this.idealTree.tops) {
1157
- if (top.linksIn.size === 0)
1368
+ if (top.linksIn.size === 0) {
1158
1369
  top.root = null
1370
+ }
1159
1371
  }
1160
1372
 
1161
1373
  // need to calculate dep flags, since nodes may have been marked
@@ -1171,7 +1383,8 @@ module.exports = cls => class Reifier extends cls {
1171
1383
  this.actualTree = this.idealTree
1172
1384
  this.idealTree = null
1173
1385
 
1174
- if (!this[_global])
1386
+ if (!this[_global]) {
1175
1387
  await this.actualTree.meta.save()
1388
+ }
1176
1389
  }
1177
1390
  }