@npmcli/arborist 2.6.3 → 2.8.0

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.
@@ -81,7 +81,7 @@ class Arborist extends Base {
81
81
  const dep = edge.to
82
82
  if (dep) {
83
83
  set.add(dep)
84
- if (dep.target)
84
+ if (dep.isLink)
85
85
  set.add(dep.target)
86
86
  }
87
87
  }
@@ -315,7 +315,7 @@ module.exports = cls => class ActualLoader extends cls {
315
315
 
316
316
  [_loadFSTree] (node) {
317
317
  const did = this[_actualTreeLoaded]
318
- node = node.target || node
318
+ node = node.target
319
319
 
320
320
  // if a Link target has started, but not completed, then
321
321
  // a Promise will be in the cache to indicate this.
@@ -221,7 +221,7 @@ module.exports = cls => class VirtualLoader extends cls {
221
221
  [assignBundles] (nodes) {
222
222
  for (const [location, node] of nodes) {
223
223
  // Skip assignment of parentage for the root package
224
- if (!location || node.target && !node.target.location)
224
+ if (!location || node.isLink && !node.target.location)
225
225
  continue
226
226
  const { name, parent, package: { inBundle }} = node
227
227
 
@@ -169,7 +169,7 @@ module.exports = cls => class Builder extends cls {
169
169
  const queue = [...set].sort(sortNodes)
170
170
 
171
171
  for (const node of queue) {
172
- const { package: { bin, scripts = {} } } = node
172
+ const { package: { bin, scripts = {} } } = node.target
173
173
  const { preinstall, install, postinstall, prepare } = scripts
174
174
  const tests = { bin, preinstall, install, postinstall, prepare }
175
175
  for (const [key, has] of Object.entries(tests)) {
@@ -202,7 +202,7 @@ module.exports = cls => class Builder extends cls {
202
202
  !(meta.originalLockfileVersion >= 2)
203
203
  }
204
204
 
205
- const { package: pkg, hasInstallScript } = node
205
+ const { package: pkg, hasInstallScript } = node.target
206
206
  const { gypfile, bin, scripts = {} } = pkg
207
207
 
208
208
  const { preinstall, install, postinstall, prepare } = scripts
@@ -263,7 +263,7 @@ module.exports = cls => class Builder extends cls {
263
263
  devOptional,
264
264
  package: pkg,
265
265
  location,
266
- } = node.target || node
266
+ } = node.target
267
267
 
268
268
  // skip any that we know we'll be deleting
269
269
  if (this[_trashList].has(path))
@@ -5,6 +5,7 @@ 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')
8
9
 
9
10
  const {dirname, resolve, relative} = require('path')
10
11
  const {depth: dfwalk} = require('treeverse')
@@ -15,6 +16,7 @@ const mkdirp = require('mkdirp-infer-owner')
15
16
  const justMkdirp = require('mkdirp')
16
17
  const moveFile = require('@npmcli/move-file')
17
18
  const rimraf = promisify(require('rimraf'))
19
+ const PackageJson = require('@npmcli/package-json')
18
20
  const packageContents = require('@npmcli/installed-package-contents')
19
21
  const { checkEngine, checkPlatform } = require('npm-install-checks')
20
22
 
@@ -24,7 +26,6 @@ const Diff = require('../diff.js')
24
26
  const retirePath = require('../retire-path.js')
25
27
  const promiseAllRejectLate = require('promise-all-reject-late')
26
28
  const optionalSet = require('../optional-set.js')
27
- const updateRootPackageJson = require('../update-root-package-json.js')
28
29
  const calcDepFlags = require('../calc-dep-flags.js')
29
30
  const { saveTypeMap, hasSubKey } = require('../add-rm-pkg-deps.js')
30
31
 
@@ -50,6 +51,7 @@ const _createSparseTree = Symbol.for('createSparseTree')
50
51
  const _loadShrinkwrapsAndUpdateTrees = Symbol.for('loadShrinkwrapsAndUpdateTrees')
51
52
  const _shrinkwrapInflated = Symbol('shrinkwrapInflated')
52
53
  const _bundleUnpacked = Symbol('bundleUnpacked')
54
+ const _bundleMissing = Symbol('bundleMissing')
53
55
  const _reifyNode = Symbol.for('reifyNode')
54
56
  const _extractOrLink = Symbol('extractOrLink')
55
57
  // defined by rebuild mixin
@@ -83,8 +85,9 @@ const _omitPeer = Symbol('omitPeer')
83
85
 
84
86
  const _global = Symbol.for('global')
85
87
 
88
+ const _pruneBundledMetadeps = Symbol('pruneBundledMetadeps')
89
+
86
90
  // defined by Ideal mixin
87
- const _pruneBundledMetadeps = Symbol.for('pruneBundledMetadeps')
88
91
  const _resolvedAdd = Symbol.for('resolvedAdd')
89
92
  const _usePackageLock = Symbol.for('usePackageLock')
90
93
  const _formatPackageLock = Symbol.for('formatPackageLock')
@@ -112,6 +115,10 @@ module.exports = cls => class Reifier extends cls {
112
115
  this[_sparseTreeDirs] = new Set()
113
116
  this[_sparseTreeRoots] = new Set()
114
117
  this[_trashList] = new Set()
118
+ // the nodes we unpack to read their bundles
119
+ this[_bundleUnpacked] = new Set()
120
+ // child nodes we'd EXPECT to be included in a bundle, but aren't
121
+ this[_bundleMissing] = new Set()
115
122
  }
116
123
 
117
124
  // public method
@@ -289,8 +296,8 @@ module.exports = cls => class Reifier extends cls {
289
296
 
290
297
  const filterNodes = []
291
298
  if (this[_global] && this.explicitRequests.size) {
292
- const idealTree = this.idealTree.target || this.idealTree
293
- const actualTree = this.actualTree.target || this.actualTree
299
+ const idealTree = this.idealTree.target
300
+ const actualTree = this.actualTree.target
294
301
  // we ONLY are allowed to make changes in the global top-level
295
302
  // children where there's an explicit request.
296
303
  for (const { name } of this.explicitRequests) {
@@ -334,7 +341,7 @@ module.exports = cls => class Reifier extends cls {
334
341
  // removed later on in the process. optionally, also mark them
335
342
  // as a retired paths, so that we move them out of the way and
336
343
  // replace them when rolling back on failure.
337
- [_addNodeToTrashList] (node, retire) {
344
+ [_addNodeToTrashList] (node, retire = false) {
338
345
  const paths = [node.path, ...node.binPaths]
339
346
  const moves = this[_retiredPaths]
340
347
  this.log.silly('reify', 'mark', retire ? 'retired' : 'deleted', paths)
@@ -404,10 +411,9 @@ module.exports = cls => class Reifier extends cls {
404
411
  return
405
412
 
406
413
  process.emit('time', 'reify:trashOmits')
407
- // node.parent is checked to make sure this is a node that's in the tree, and
408
- // not the parent-less top level nodes
414
+
409
415
  const filter = node =>
410
- node.isDescendantOf(this.idealTree) &&
416
+ node.top.isProjectRoot &&
411
417
  (node.peer && this[_omitPeer] ||
412
418
  node.dev && this[_omitDev] ||
413
419
  node.optional && this[_omitOptional] ||
@@ -611,10 +617,9 @@ module.exports = cls => class Reifier extends cls {
611
617
  [_loadBundlesAndUpdateTrees] (
612
618
  depth = 0, bundlesByDepth = this[_getBundlesByDepth]()
613
619
  ) {
614
- if (depth === 0) {
615
- this[_bundleUnpacked] = new Set()
620
+ if (depth === 0)
616
621
  process.emit('time', 'reify:loadBundles')
617
- }
622
+
618
623
  const maxBundleDepth = bundlesByDepth.get('maxBundleDepth')
619
624
  if (depth > maxBundleDepth) {
620
625
  // if we did something, then prune the tree and update the diffs
@@ -643,14 +648,30 @@ module.exports = cls => class Reifier extends cls {
643
648
  }))
644
649
  // then load their unpacked children and move into the ideal tree
645
650
  .then(nodes =>
646
- promiseAllRejectLate(nodes.map(node => new this.constructor({
647
- ...this.options,
648
- path: node.path,
649
- }).loadActual({
650
- root: node,
651
- // don't transplant any sparse folders we created
652
- transplantFilter: node => node.package._id,
653
- }))))
651
+ promiseAllRejectLate(nodes.map(async node => {
652
+ const arb = new this.constructor({
653
+ ...this.options,
654
+ path: node.path,
655
+ })
656
+ const notTransplanted = new Set(node.children.keys())
657
+ await arb.loadActual({
658
+ root: node,
659
+ // don't transplant any sparse folders we created
660
+ // loadActual will set node.package to {} for empty directories
661
+ // if by chance there are some empty folders in the node_modules
662
+ // tree for some other reason, then ok, ignore those too.
663
+ transplantFilter: node => {
664
+ if (node.package._id) {
665
+ // it's actually in the bundle if it gets transplanted
666
+ notTransplanted.delete(node.name)
667
+ return true
668
+ } else
669
+ return false
670
+ },
671
+ })
672
+ for (const name of notTransplanted)
673
+ this[_bundleMissing].add(node.children.get(name))
674
+ })))
654
675
  // move onto the next level of bundled items
655
676
  .then(() => this[_loadBundlesAndUpdateTrees](depth + 1, bundlesByDepth))
656
677
  }
@@ -664,7 +685,7 @@ module.exports = cls => class Reifier extends cls {
664
685
  const node = diff.ideal
665
686
  if (!node)
666
687
  return
667
- if (node.isProjectRoot || (node.target && node.target.isProjectRoot))
688
+ if (node.isProjectRoot)
668
689
  return
669
690
 
670
691
  const { bundleDependencies } = node.package
@@ -686,6 +707,27 @@ module.exports = cls => class Reifier extends cls {
686
707
  // https://github.com/npm/cli/issues/1597#issuecomment-667639545
687
708
  [_pruneBundledMetadeps] (bundlesByDepth) {
688
709
  const bundleShadowed = new Set()
710
+
711
+ // Example dep graph:
712
+ // root -> (a, c)
713
+ // a -> BUNDLE(b)
714
+ // b -> c
715
+ // c -> b
716
+ //
717
+ // package tree:
718
+ // root
719
+ // +-- a
720
+ // | +-- b(1)
721
+ // | +-- c(1)
722
+ // +-- b(2)
723
+ // +-- c(2)
724
+ // 1. mark everything that's shadowed by anything in the bundle. This
725
+ // marks b(2) and c(2).
726
+ // 2. anything with edgesIn from outside the set, mark not-extraneous,
727
+ // remove from set. This unmarks c(2).
728
+ // 3. continue until no change
729
+ // 4. remove everything in the set from the tree. b(2) is pruned
730
+
689
731
  // create the list of nodes shadowed by children of bundlers
690
732
  for (const bundles of bundlesByDepth.values()) {
691
733
  // skip the 'maxBundleDepth' item
@@ -701,36 +743,50 @@ module.exports = cls => class Reifier extends cls {
701
743
  }
702
744
  }
703
745
  }
704
- let changed = true
705
- while (changed) {
706
- changed = false
707
- for (const shadow of bundleShadowed) {
708
- if (!shadow.extraneous) {
709
- bundleShadowed.delete(shadow)
710
- continue
746
+
747
+ // lib -> (a@1.x) BUNDLE(a@1.2.3 (b@1.2.3))
748
+ // a@1.2.3 -> (b@1.2.3)
749
+ // a@1.3.0 -> (b@2)
750
+ // b@1.2.3 -> ()
751
+ // b@2 -> (c@2)
752
+ //
753
+ // root
754
+ // +-- lib
755
+ // | +-- a@1.2.3
756
+ // | +-- b@1.2.3
757
+ // +-- b@2 <-- shadowed, now extraneous
758
+ // +-- c@2 <-- also shadowed, because only dependent is shadowed
759
+ for (const shadow of bundleShadowed) {
760
+ for (const shadDep of shadow.edgesOut.values()) {
761
+ /* istanbul ignore else - pretty unusual situation, just being
762
+ * defensive here. Would mean that a bundled dep has a dependency
763
+ * that is unmet. which, weird, but if you bundle it, we take
764
+ * whatever you put there and assume the publisher knows best. */
765
+ if (shadDep.to) {
766
+ bundleShadowed.add(shadDep.to)
767
+ shadDep.to.extraneous = true
711
768
  }
769
+ }
770
+ }
712
771
 
772
+ let changed
773
+ do {
774
+ changed = false
775
+ for (const shadow of bundleShadowed) {
713
776
  for (const edge of shadow.edgesIn) {
714
- if (!edge.from.extraneous) {
777
+ if (!bundleShadowed.has(edge.from)) {
715
778
  shadow.extraneous = false
716
779
  bundleShadowed.delete(shadow)
717
780
  changed = true
718
- } else {
719
- for (const shadDep of shadow.edgesOut.values()) {
720
- /* istanbul ignore else - pretty unusual situation, just being
721
- * defensive here. Would mean that a bundled dep has a dependency
722
- * that is unmet. which, weird, but if you bundle it, we take
723
- * whatever you put there and assume the publisher knows best. */
724
- if (shadDep.to)
725
- bundleShadowed.add(shadDep.to)
726
- }
781
+ break
727
782
  }
728
783
  }
729
784
  }
730
- }
785
+ } while (changed)
786
+
731
787
  for (const shadow of bundleShadowed) {
732
- shadow.parent = null
733
788
  this[_addNodeToTrashList](shadow)
789
+ shadow.root = null
734
790
  }
735
791
  }
736
792
 
@@ -781,6 +837,7 @@ module.exports = cls => class Reifier extends cls {
781
837
  const node = diff.ideal
782
838
  const bd = this[_bundleUnpacked].has(node)
783
839
  const sw = this[_shrinkwrapInflated].has(node)
840
+ const bundleMissing = this[_bundleMissing].has(node)
784
841
 
785
842
  // check whether we still need to unpack this one.
786
843
  // test the inDepBundle last, since that's potentially a tree walk.
@@ -788,7 +845,7 @@ module.exports = cls => class Reifier extends cls {
788
845
  !node.isRoot && // root node already exists
789
846
  !bd && // already unpacked to read bundle
790
847
  !sw && // already unpacked to read sw
791
- !node.inDepBundle // already unpacked by another dep's bundle
848
+ (bundleMissing || !node.inDepBundle) // already unpacked by another dep's bundle
792
849
 
793
850
  if (doUnpack)
794
851
  unpacks.push(this[_reifyNode](node))
@@ -815,8 +872,26 @@ module.exports = cls => class Reifier extends cls {
815
872
  const moves = this[_retiredPaths]
816
873
  this[_retiredUnchanged] = {}
817
874
  return promiseAllRejectLate(this.diff.children.map(diff => {
818
- const realFolder = (diff.actual || diff.ideal).path
875
+ // skip if nothing was retired
876
+ if (diff.action !== 'CHANGE' && diff.action !== 'REMOVE')
877
+ return
878
+
879
+ const { path: realFolder } = diff.actual
819
880
  const retireFolder = moves[realFolder]
881
+ /* istanbul ignore next - should be impossible */
882
+ debug(() => {
883
+ if (!retireFolder) {
884
+ const er = new Error('trying to un-retire but not retired')
885
+ throw Object.assign(er, {
886
+ realFolder,
887
+ retireFolder,
888
+ actual: diff.actual,
889
+ ideal: diff.ideal,
890
+ action: diff.action,
891
+ })
892
+ }
893
+ })
894
+
820
895
  this[_retiredUnchanged][retireFolder] = []
821
896
  return promiseAllRejectLate(diff.unchanged.map(node => {
822
897
  // no need to roll back links, since we'll just delete them anyway
@@ -824,7 +899,7 @@ module.exports = cls => class Reifier extends cls {
824
899
  return mkdirp(dirname(node.path)).then(() => this[_reifyNode](node))
825
900
 
826
901
  // will have been moved/unpacked along with bundler
827
- if (node.inDepBundle)
902
+ if (node.inDepBundle && !this[_bundleMissing].has(node))
828
903
  return
829
904
 
830
905
  this[_retiredUnchanged][retireFolder].push(node)
@@ -887,6 +962,18 @@ module.exports = cls => class Reifier extends cls {
887
962
  filter: diff => diff.action === 'ADD' || diff.action === 'CHANGE',
888
963
  })
889
964
 
965
+ // pick up link nodes from the unchanged list as we want to run their
966
+ // scripts in every install despite of having a diff status change
967
+ for (const node of this.diff.unchanged) {
968
+ const tree = node.root.target
969
+
970
+ // skip links that only live within node_modules as they are most
971
+ // likely managed by packages we installed, we only want to rebuild
972
+ // unchanged links we directly manage
973
+ if (node.isLink && node.target.fsTop === tree)
974
+ nodes.push(node)
975
+ }
976
+
890
977
  return this.rebuild({ nodes, handleOptionalFailure: true })
891
978
  .then(() => process.emit('timeEnd', 'reify:build'))
892
979
  }
@@ -1029,6 +1116,25 @@ module.exports = cls => class Reifier extends cls {
1029
1116
 
1030
1117
  const promises = [this[_saveLockFile](saveOpt)]
1031
1118
 
1119
+ const updatePackageJson = async (tree) => {
1120
+ const pkgJson = await PackageJson.load(tree.path)
1121
+ .catch(() => new PackageJson(tree.path))
1122
+ const {
1123
+ dependencies = {},
1124
+ devDependencies = {},
1125
+ optionalDependencies = {},
1126
+ peerDependencies = {},
1127
+ } = tree.package
1128
+
1129
+ pkgJson.update({
1130
+ dependencies,
1131
+ devDependencies,
1132
+ optionalDependencies,
1133
+ peerDependencies,
1134
+ })
1135
+ await pkgJson.save()
1136
+ }
1137
+
1032
1138
  // grab any from explicitRequests that had deps removed
1033
1139
  for (const { from: tree } of this.explicitRequests)
1034
1140
  updatedTrees.add(tree)
@@ -1036,7 +1142,7 @@ module.exports = cls => class Reifier extends cls {
1036
1142
  for (const tree of updatedTrees) {
1037
1143
  // refresh the edges so they have the correct specs
1038
1144
  tree.package = tree.package
1039
- promises.push(updateRootPackageJson(tree))
1145
+ promises.push(updatePackageJson(tree))
1040
1146
  }
1041
1147
 
1042
1148
  await Promise.all(promises)
@@ -29,7 +29,7 @@ const calcDepFlagsStep = (node) => {
29
29
  resetParents(node, 'optional')
30
30
 
31
31
  // for links, map their hierarchy appropriately
32
- if (node.target) {
32
+ if (node.isLink) {
33
33
  node.target.dev = node.dev
34
34
  node.target.optional = node.optional
35
35
  node.target.devOptional = node.devOptional
@@ -92,10 +92,10 @@ const unsetFlag = (node, flag) => {
92
92
  tree: node,
93
93
  visit: node => {
94
94
  node.extraneous = node[flag] = false
95
- if (node.target)
95
+ if (node.isLink)
96
96
  node.target.extraneous = node.target[flag] = false
97
97
  },
98
- getChildren: node => [...(node.target || node).edgesOut.values()]
98
+ getChildren: node => [...node.target.edgesOut.values()]
99
99
  .filter(edge => edge.to && edge.to[flag] &&
100
100
  (flag !== 'peer' && edge.type === 'peer' || edge.type === 'prod'))
101
101
  .map(edge => edge.to),