@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.
@@ -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
  }
@@ -115,10 +115,6 @@ module.exports = cls => class Builder extends cls {
115
115
  await this[_runScripts]('preinstall')
116
116
  if (this[_binLinks] && type !== 'links')
117
117
  await this[_linkAllBins]()
118
- if (!this[_ignoreScripts]) {
119
- await this[_runScripts]('install')
120
- await this[_runScripts]('postinstall')
121
- }
122
118
 
123
119
  // links should also run prepare scripts and only link bins after that
124
120
  if (type === 'links') {
@@ -128,6 +124,11 @@ module.exports = cls => class Builder extends cls {
128
124
  await this[_linkAllBins]()
129
125
  }
130
126
 
127
+ if (!this[_ignoreScripts]) {
128
+ await this[_runScripts]('install')
129
+ await this[_runScripts]('postinstall')
130
+ }
131
+
131
132
  process.emit('timeEnd', `build:${type}`)
132
133
  }
133
134
 
@@ -14,6 +14,7 @@ const fs = require('fs')
14
14
  const {promisify} = require('util')
15
15
  const symlink = promisify(fs.symlink)
16
16
  const mkdirp = require('mkdirp-infer-owner')
17
+ const justMkdirp = require('mkdirp')
17
18
  const moveFile = require('@npmcli/move-file')
18
19
  const rimraf = promisify(require('rimraf'))
19
20
  const packageContents = require('@npmcli/installed-package-contents')
@@ -26,6 +27,7 @@ const retirePath = require('../retire-path.js')
26
27
  const promiseAllRejectLate = require('promise-all-reject-late')
27
28
  const optionalSet = require('../optional-set.js')
28
29
  const updateRootPackageJson = require('../update-root-package-json.js')
30
+ const calcDepFlags = require('../calc-dep-flags.js')
29
31
 
30
32
  const _retiredPaths = Symbol('retiredPaths')
31
33
  const _retiredUnchanged = Symbol('retiredUnchanged')
@@ -36,6 +38,8 @@ const _retireShallowNodes = Symbol.for('retireShallowNodes')
36
38
  const _getBundlesByDepth = Symbol('getBundlesByDepth')
37
39
  const _registryResolved = Symbol('registryResolved')
38
40
  const _addNodeToTrashList = Symbol('addNodeToTrashList')
41
+ const _workspaces = Symbol.for('workspaces')
42
+
39
43
  // shared by rebuild mixin
40
44
  const _trashList = Symbol.for('trashList')
41
45
  const _handleOptionalFailure = Symbol.for('handleOptionalFailure')
@@ -45,7 +49,8 @@ const _loadTrees = Symbol.for('loadTrees')
45
49
  const _diffTrees = Symbol.for('diffTrees')
46
50
  const _createSparseTree = Symbol.for('createSparseTree')
47
51
  const _loadShrinkwrapsAndUpdateTrees = Symbol.for('loadShrinkwrapsAndUpdateTrees')
48
- const _shrinkwrapUnpacked = Symbol('shrinkwrapUnpacked')
52
+ const _shrinkwrapInflated = Symbol('shrinkwrapInflated')
53
+ const _bundleUnpacked = Symbol('bundleUnpacked')
49
54
  const _reifyNode = Symbol.for('reifyNode')
50
55
  const _extractOrLink = Symbol('extractOrLink')
51
56
  // defined by rebuild mixin
@@ -82,7 +87,6 @@ const _global = Symbol.for('global')
82
87
 
83
88
  // defined by Ideal mixin
84
89
  const _pruneBundledMetadeps = Symbol.for('pruneBundledMetadeps')
85
- const _explicitRequests = Symbol.for('explicitRequests')
86
90
  const _resolvedAdd = Symbol.for('resolvedAdd')
87
91
  const _usePackageLock = Symbol.for('usePackageLock')
88
92
  const _formatPackageLock = Symbol.for('formatPackageLock')
@@ -105,7 +109,7 @@ module.exports = cls => class Reifier extends cls {
105
109
 
106
110
  this.diff = null
107
111
  this[_retiredPaths] = {}
108
- this[_shrinkwrapUnpacked] = new Set()
112
+ this[_shrinkwrapInflated] = new Set()
109
113
  this[_retiredUnchanged] = {}
110
114
  this[_sparseTreeDirs] = new Set()
111
115
  this[_sparseTreeRoots] = new Set()
@@ -146,7 +150,10 @@ module.exports = cls => class Reifier extends cls {
146
150
  if (this[_packageLockOnly] || this[_dryRun])
147
151
  return
148
152
 
149
- await mkdirp(resolve(this.path))
153
+ // we do NOT want to set ownership on this folder, especially
154
+ // recursively, because it can have other side effects to do that
155
+ // in a project directory. We just want to make it if it's missing.
156
+ await justMkdirp(resolve(this.path))
150
157
  }
151
158
 
152
159
  async [_reifyPackages] () {
@@ -237,9 +244,25 @@ module.exports = cls => class Reifier extends cls {
237
244
  const actualOpt = this[_global] ? {
238
245
  ignoreMissing: true,
239
246
  global: true,
240
- filter: (node, kid) =>
241
- this[_explicitRequests].size === 0 || !node.isProjectRoot ? true
242
- : (this.idealTree.edgesOut.has(kid) || this[_explicitRequests].has(kid)),
247
+ filter: (node, kid) => {
248
+ // if it's not the project root, and we have no explicit requests,
249
+ // then we're already into a nested dep, so we keep it
250
+ if (this.explicitRequests.size === 0 || !node.isProjectRoot)
251
+ return true
252
+
253
+ // if we added it as an edgeOut, then we want it
254
+ if (this.idealTree.edgesOut.has(kid))
255
+ return true
256
+
257
+ // if it's an explicit request, then we want it
258
+ const hasExplicit = [...this.explicitRequests]
259
+ .some(edge => edge.name === kid)
260
+ if (hasExplicit)
261
+ return true
262
+
263
+ // ignore the rest of the global install folder
264
+ return false
265
+ },
243
266
  } : { ignoreMissing: true }
244
267
 
245
268
  if (!this[_global]) {
@@ -266,9 +289,36 @@ module.exports = cls => class Reifier extends cls {
266
289
  // to just invalidate the parts that changed, but avoid walking the
267
290
  // whole tree again.
268
291
 
292
+ const filterNodes = []
293
+ if (this[_global] && this.explicitRequests.size) {
294
+ const idealTree = this.idealTree.target || this.idealTree
295
+ const actualTree = this.actualTree.target || this.actualTree
296
+ // we ONLY are allowed to make changes in the global top-level
297
+ // children where there's an explicit request.
298
+ for (const { name } of this.explicitRequests) {
299
+ const ideal = idealTree.children.get(name)
300
+ if (ideal)
301
+ filterNodes.push(ideal)
302
+ const actual = actualTree.children.get(name)
303
+ if (actual)
304
+ filterNodes.push(actual)
305
+ }
306
+ } else {
307
+ for (const ws of this[_workspaces]) {
308
+ const ideal = this.idealTree.children.get(ws)
309
+ if (ideal)
310
+ filterNodes.push(ideal)
311
+ const actual = this.actualTree.children.get(ws)
312
+ if (actual)
313
+ filterNodes.push(actual)
314
+ }
315
+ }
316
+
269
317
  // find all the nodes that need to change between the actual
270
318
  // and ideal trees.
271
319
  this.diff = Diff.calculate({
320
+ shrinkwrapInflated: this[_shrinkwrapInflated],
321
+ filterNodes,
272
322
  actual: this.actualTree,
273
323
  ideal: this.idealTree,
274
324
  })
@@ -375,7 +425,8 @@ module.exports = cls => class Reifier extends cls {
375
425
  const dirs = this.diff.leaves
376
426
  .filter(diff => {
377
427
  return (diff.action === 'ADD' || diff.action === 'CHANGE') &&
378
- !this[_sparseTreeDirs].has(diff.ideal.path)
428
+ !this[_sparseTreeDirs].has(diff.ideal.path) &&
429
+ !diff.ideal.isLink
379
430
  })
380
431
  .map(diff => diff.ideal.path)
381
432
 
@@ -409,9 +460,9 @@ module.exports = cls => class Reifier extends cls {
409
460
  // we need to unpack them, read that shrinkwrap file, and then update
410
461
  // the tree by calling loadVirtual with the node as the root.
411
462
  [_loadShrinkwrapsAndUpdateTrees] () {
412
- const seen = this[_shrinkwrapUnpacked]
463
+ const seen = this[_shrinkwrapInflated]
413
464
  const shrinkwraps = this.diff.leaves
414
- .filter(d => (d.action === 'CHANGE' || d.action === 'ADD') &&
465
+ .filter(d => (d.action === 'CHANGE' || d.action === 'ADD' || !d.action) &&
415
466
  d.ideal.hasShrinkwrap && !seen.has(d.ideal) &&
416
467
  !this[_trashList].has(d.ideal.path))
417
468
 
@@ -424,7 +475,7 @@ module.exports = cls => class Reifier extends cls {
424
475
  return promiseAllRejectLate(shrinkwraps.map(diff => {
425
476
  const node = diff.ideal
426
477
  seen.add(node)
427
- return this[_reifyNode](node)
478
+ return diff.action ? this[_reifyNode](node) : node
428
479
  }))
429
480
  .then(nodes => promiseAllRejectLate(nodes.map(node => new Arborist({
430
481
  ...this.options,
@@ -455,7 +506,7 @@ module.exports = cls => class Reifier extends cls {
455
506
 
456
507
  const { npmVersion, nodeVersion } = this.options
457
508
  const p = Promise.resolve()
458
- .then(() => {
509
+ .then(async () => {
459
510
  // when we reify an optional node, check the engine and platform
460
511
  // first. be sure to ignore the --force and --engine-strict flags,
461
512
  // since we always want to skip any optional packages we can't install.
@@ -465,11 +516,11 @@ module.exports = cls => class Reifier extends cls {
465
516
  checkEngine(node.package, npmVersion, nodeVersion, false)
466
517
  checkPlatform(node.package, false)
467
518
  }
519
+ await this[_checkBins](node)
520
+ await this[_extractOrLink](node)
521
+ await this[_warnDeprecated](node)
522
+ await this[_loadAncientPackageDetails](node)
468
523
  })
469
- .then(() => this[_checkBins](node))
470
- .then(() => this[_extractOrLink](node))
471
- .then(() => this[_warnDeprecated](node))
472
- .then(() => this[_loadAncientPackageDetails](node))
473
524
 
474
525
  return this[_handleOptionalFailure](node, p)
475
526
  .then(() => {
@@ -515,10 +566,11 @@ module.exports = cls => class Reifier extends cls {
515
566
  })
516
567
  }
517
568
 
518
- [_symlink] (node) {
569
+ async [_symlink] (node) {
519
570
  const dir = dirname(node.path)
520
571
  const target = node.realpath
521
572
  const rel = relative(dir, target)
573
+ await mkdirp(dir)
522
574
  return symlink(rel, node.path, 'junction')
523
575
  }
524
576
 
@@ -585,8 +637,10 @@ module.exports = cls => class Reifier extends cls {
585
637
  [_loadBundlesAndUpdateTrees] (
586
638
  depth = 0, bundlesByDepth = this[_getBundlesByDepth]()
587
639
  ) {
588
- if (depth === 0)
640
+ if (depth === 0) {
641
+ this[_bundleUnpacked] = new Set()
589
642
  process.emit('time', 'reify:loadBundles')
643
+ }
590
644
  const maxBundleDepth = bundlesByDepth.get('maxBundleDepth')
591
645
  if (depth > maxBundleDepth) {
592
646
  // if we did something, then prune the tree and update the diffs
@@ -602,13 +656,17 @@ module.exports = cls => class Reifier extends cls {
602
656
  // shallower bundle overwriting them with a bundled meta-dep.
603
657
  const set = (bundlesByDepth.get(depth) || [])
604
658
  .filter(node => node.root === this.idealTree &&
659
+ node.target !== node.root &&
605
660
  !this[_trashList].has(node.path))
606
661
 
607
662
  if (!set.length)
608
663
  return this[_loadBundlesAndUpdateTrees](depth + 1, bundlesByDepth)
609
664
 
610
665
  // extract all the nodes with bundles
611
- return promiseAllRejectLate(set.map(node => this[_reifyNode](node)))
666
+ return promiseAllRejectLate(set.map(node => {
667
+ this[_bundleUnpacked].add(node)
668
+ return this[_reifyNode](node)
669
+ }))
612
670
  // then load their unpacked children and move into the ideal tree
613
671
  .then(nodes =>
614
672
  promiseAllRejectLate(nodes.map(node => new this.constructor({
@@ -630,8 +688,13 @@ module.exports = cls => class Reifier extends cls {
630
688
  tree: this.diff,
631
689
  visit: diff => {
632
690
  const node = diff.ideal
633
- if (node && !node.isProjectRoot && node.package.bundleDependencies &&
634
- node.package.bundleDependencies.length) {
691
+ if (!node)
692
+ return
693
+ if (node.isProjectRoot || (node.target && node.target.isProjectRoot))
694
+ return
695
+
696
+ const { bundleDependencies } = node.package
697
+ if (bundleDependencies && bundleDependencies.length) {
635
698
  maxBundleDepth = Math.max(maxBundleDepth, node.depth)
636
699
  if (!bundlesByDepth.has(node.depth))
637
700
  bundlesByDepth.set(node.depth, [node])
@@ -736,14 +799,14 @@ module.exports = cls => class Reifier extends cls {
736
799
  return
737
800
 
738
801
  const node = diff.ideal
739
- const bd = node.package.bundleDependencies
740
- const sw = this[_shrinkwrapUnpacked].has(node)
802
+ const bd = this[_bundleUnpacked].has(node)
803
+ const sw = this[_shrinkwrapInflated].has(node)
741
804
 
742
805
  // check whether we still need to unpack this one.
743
806
  // test the inDepBundle last, since that's potentially a tree walk.
744
807
  const doUnpack = node && // can't unpack if removed!
745
808
  !node.isRoot && // root node already exists
746
- !(bd && bd.length) && // already unpacked to read bundle
809
+ !bd && // already unpacked to read bundle
747
810
  !sw && // already unpacked to read sw
748
811
  !node.inDepBundle // already unpacked by another dep's bundle
749
812
 
@@ -886,11 +949,11 @@ module.exports = cls => class Reifier extends cls {
886
949
  // to things like git repos and tarball file/urls. However, if the
887
950
  // user requested 'foo@', and we have a foo@file:../foo, then we should
888
951
  // end up saving the spec we actually used, not whatever they gave us.
889
- if (this[_resolvedAdd]) {
952
+ if (this[_resolvedAdd].length) {
890
953
  const root = this.idealTree
891
954
  const pkg = root.package
892
955
  for (const { name } of this[_resolvedAdd]) {
893
- const req = npa(root.edgesOut.get(name).spec, root.realpath)
956
+ const req = npa.resolve(name, root.edgesOut.get(name).spec, root.realpath)
894
957
  const {rawSpec, subSpec} = req
895
958
 
896
959
  const spec = subSpec ? subSpec.rawSpec : rawSpec
@@ -966,20 +1029,85 @@ module.exports = cls => class Reifier extends cls {
966
1029
  return meta.save(saveOpt)
967
1030
  }
968
1031
 
969
- [_copyIdealToActual] () {
1032
+ async [_copyIdealToActual] () {
1033
+ // clean up any trash that is still in the tree
1034
+ for (const path of this[_trashList]) {
1035
+ const loc = relpath(this.idealTree.realpath, path)
1036
+ const node = this.idealTree.inventory.get(loc)
1037
+ if (node && node.root === this.idealTree)
1038
+ node.parent = null
1039
+ }
1040
+
1041
+ // if we filtered to only certain nodes, then anything ELSE needs
1042
+ // to be untouched in the resulting actual tree, even if it differs
1043
+ // in the idealTree. Copy over anything that was in the actual and
1044
+ // was not changed, delete anything in the ideal and not actual.
1045
+ // Then we move the entire idealTree over to this.actualTree, and
1046
+ // save the hidden lockfile.
1047
+ if (this.diff && this.diff.filterSet.size) {
1048
+ const { filterSet } = this.diff
1049
+ const seen = new Set()
1050
+ for (const [loc, ideal] of this.idealTree.inventory.entries()) {
1051
+ if (seen.has(loc))
1052
+ continue
1053
+ seen.add(loc)
1054
+
1055
+ // if it's an ideal node from the filter set, then skip it
1056
+ // because we already made whatever changes were necessary
1057
+ if (filterSet.has(ideal))
1058
+ continue
1059
+
1060
+ // otherwise, if it's not in the actualTree, then it's not a thing
1061
+ // that we actually added. And if it IS in the actualTree, then
1062
+ // it's something that we left untouched, so we need to record
1063
+ // that.
1064
+ const actual = this.actualTree.inventory.get(loc)
1065
+ if (!actual)
1066
+ ideal.root = null
1067
+ else {
1068
+ if ([...actual.linksIn].some(link => filterSet.has(link))) {
1069
+ seen.add(actual.location)
1070
+ continue
1071
+ }
1072
+ const { realpath, isLink } = actual
1073
+ if (isLink && ideal.isLink && ideal.realpath === realpath)
1074
+ continue
1075
+ else
1076
+ actual.root = this.idealTree
1077
+ }
1078
+ }
1079
+
1080
+ // now find any actual nodes that may not be present in the ideal
1081
+ // tree, but were left behind by virtue of not being in the filter
1082
+ for (const [loc, actual] of this.actualTree.inventory.entries()) {
1083
+ if (seen.has(loc))
1084
+ continue
1085
+ seen.add(loc)
1086
+ if (filterSet.has(actual))
1087
+ continue
1088
+ actual.root = this.idealTree
1089
+ }
1090
+
1091
+ // prune out any tops that lack a linkIn
1092
+ for (const top of this.idealTree.tops) {
1093
+ if (top.linksIn.size === 0)
1094
+ top.root = null
1095
+ }
1096
+
1097
+ // need to calculate dep flags, since nodes may have been marked
1098
+ // as extraneous or otherwise incorrect during transit.
1099
+ calcDepFlags(this.idealTree)
1100
+ }
1101
+
970
1102
  // save the ideal's meta as a hidden lockfile after we actualize it
971
1103
  this.idealTree.meta.filename =
972
- this.path + '/node_modules/.package-lock.json'
1104
+ this.idealTree.realpath + '/node_modules/.package-lock.json'
973
1105
  this.idealTree.meta.hiddenLockfile = true
1106
+
974
1107
  this.actualTree = this.idealTree
975
1108
  this.idealTree = null
976
- for (const path of this[_trashList]) {
977
- const loc = relpath(this.path, path)
978
- const node = this.actualTree.inventory.get(loc)
979
- if (node && node.root === this.actualTree)
980
- node.parent = null
981
- }
982
1109
 
983
- return !this[_global] && this.actualTree.meta.save()
1110
+ if (!this[_global])
1111
+ await this.actualTree.meta.save()
984
1112
  }
985
1113
  }
package/lib/debug.js CHANGED
@@ -12,6 +12,7 @@
12
12
 
13
13
  // run in debug mode if explicitly requested, running arborist tests,
14
14
  // or working in the arborist project directory.
15
+
15
16
  const debug = process.env.ARBORIST_DEBUG !== '0' && (
16
17
  process.env.ARBORIST_DEBUG === '1' ||
17
18
  /\barborist\b/.test(process.env.NODE_DEBUG || '') ||
@@ -21,4 +22,10 @@ const debug = process.env.ARBORIST_DEBUG !== '0' && (
21
22
  )
22
23
 
23
24
  module.exports = debug ? fn => fn() : () => {}
24
- module.exports.log = (...msg) => module.exports(() => console.error(...msg))
25
+ const red = process.stderr.isTTY ? msg => `\x1B[31m${msg}\x1B[39m` : m => m
26
+ module.exports.log = (...msg) => module.exports(() => {
27
+ const { format } = require('util')
28
+ const prefix = `\n${process.pid} ${red(format(msg.shift()))} `
29
+ msg = (prefix + format(...msg).trim().split('\n').join(prefix)).trim()
30
+ console.error(msg)
31
+ })
package/lib/diff.js CHANGED
@@ -11,7 +11,9 @@ const {existsSync} = require('fs')
11
11
  const ssri = require('ssri')
12
12
 
13
13
  class Diff {
14
- constructor ({actual, ideal}) {
14
+ constructor ({actual, ideal, filterSet, shrinkwrapInflated}) {
15
+ this.filterSet = filterSet
16
+ this.shrinkwrapInflated = shrinkwrapInflated
15
17
  this.children = []
16
18
  this.actual = actual
17
19
  this.ideal = ideal
@@ -29,9 +31,54 @@ class Diff {
29
31
  this.removed = []
30
32
  }
31
33
 
32
- static calculate ({actual, ideal}) {
34
+ static calculate ({actual, ideal, filterNodes = [], shrinkwrapInflated = new Set()}) {
35
+ // if there's a filterNode, then:
36
+ // - get the path from the root to the filterNode. The root or
37
+ // root.target should have an edge either to the filterNode or
38
+ // a link to the filterNode. If not, abort. Add the path to the
39
+ // filterSet.
40
+ // - Add set of Nodes depended on by the filterNode to filterSet.
41
+ // - Anything outside of that set should be ignored by getChildren
42
+ const filterSet = new Set()
43
+ for (const filterNode of filterNodes) {
44
+ const { root } = filterNode
45
+ if (root !== ideal && root !== actual)
46
+ throw new Error('invalid filterNode: outside idealTree/actualTree')
47
+ const { target } = root
48
+ const rootTarget = target || root
49
+ const edge = [...rootTarget.edgesOut.values()].filter(e => {
50
+ return e.to && (e.to === filterNode || e.to.target === filterNode)
51
+ })[0]
52
+ filterSet.add(root)
53
+ filterSet.add(rootTarget)
54
+ filterSet.add(ideal)
55
+ filterSet.add(actual)
56
+ if (edge && edge.to) {
57
+ filterSet.add(edge.to)
58
+ if (edge.to.target)
59
+ filterSet.add(edge.to.target)
60
+ }
61
+ filterSet.add(filterNode)
62
+
63
+ depth({
64
+ tree: filterNode,
65
+ visit: node => filterSet.add(node),
66
+ getChildren: node => {
67
+ node = node.target || node
68
+ const loc = node.location
69
+ const idealNode = ideal.inventory.get(loc)
70
+ const ideals = !idealNode ? []
71
+ : [...idealNode.edgesOut.values()].filter(e => e.to).map(e => e.to)
72
+ const actualNode = actual.inventory.get(loc)
73
+ const actuals = !actualNode ? []
74
+ : [...actualNode.edgesOut.values()].filter(e => e.to).map(e => e.to)
75
+ return ideals.concat(actuals)
76
+ },
77
+ })
78
+ }
79
+
33
80
  return depth({
34
- tree: new Diff({actual, ideal}),
81
+ tree: new Diff({actual, ideal, filterSet, shrinkwrapInflated}),
35
82
  getChildren,
36
83
  leave,
37
84
  })
@@ -89,20 +136,29 @@ const allChildren = node => {
89
136
  // to create the diff tree
90
137
  const getChildren = diff => {
91
138
  const children = []
92
- const {unchanged, removed} = diff
139
+ const {actual, ideal, unchanged, removed, filterSet, shrinkwrapInflated} = diff
93
140
 
94
141
  // Note: we DON'T diff fsChildren themselves, because they are either
95
142
  // included in the package contents, or part of some other project, and
96
143
  // will never appear in legacy shrinkwraps anyway. but we _do_ include the
97
144
  // child nodes of fsChildren, because those are nodes that we are typically
98
145
  // responsible for installing.
99
- const actualKids = allChildren(diff.actual)
100
- const idealKids = allChildren(diff.ideal)
146
+ const actualKids = allChildren(actual)
147
+ const idealKids = allChildren(ideal)
148
+
149
+ if (ideal && ideal.hasShrinkwrap && !shrinkwrapInflated.has(ideal)) {
150
+ // Guaranteed to get a diff.leaves here, because we always
151
+ // be called with a proper Diff object when ideal has a shrinkwrap
152
+ // that has not been inflated.
153
+ diff.leaves.push(diff)
154
+ return children
155
+ }
156
+
101
157
  const paths = new Set([...actualKids.keys(), ...idealKids.keys()])
102
158
  for (const path of paths) {
103
159
  const actual = actualKids.get(path)
104
160
  const ideal = idealKids.get(path)
105
- diffNode(actual, ideal, children, unchanged, removed)
161
+ diffNode(actual, ideal, children, unchanged, removed, filterSet, shrinkwrapInflated)
106
162
  }
107
163
 
108
164
  if (diff.leaves && !children.length)
@@ -111,15 +167,18 @@ const getChildren = diff => {
111
167
  return children
112
168
  }
113
169
 
114
- const diffNode = (actual, ideal, children, unchanged, removed) => {
170
+ const diffNode = (actual, ideal, children, unchanged, removed, filterSet, shrinkwrapInflated) => {
171
+ if (filterSet.size && !(filterSet.has(ideal) || filterSet.has(actual)))
172
+ return
173
+
115
174
  const action = getAction({actual, ideal})
116
175
 
117
176
  // if it's a match, then get its children
118
177
  // otherwise, this is the child diff node
119
- if (action) {
178
+ if (action || (!shrinkwrapInflated.has(ideal) && ideal.hasShrinkwrap)) {
120
179
  if (action === 'REMOVE')
121
180
  removed.push(actual)
122
- children.push(new Diff({actual, ideal}))
181
+ children.push(new Diff({actual, ideal, filterSet, shrinkwrapInflated}))
123
182
  } else {
124
183
  unchanged.push(ideal)
125
184
  // !*! Weird dirty hack warning !*!
@@ -150,7 +209,7 @@ const diffNode = (actual, ideal, children, unchanged, removed) => {
150
209
  for (const node of bundledChildren)
151
210
  node.parent = ideal
152
211
  }
153
- children.push(...getChildren({actual, ideal, unchanged, removed}))
212
+ children.push(...getChildren({actual, ideal, unchanged, removed, filterSet, shrinkwrapInflated}))
154
213
  }
155
214
  }
156
215
 
package/lib/index.js CHANGED
@@ -3,5 +3,6 @@ module.exports.Arborist = module.exports
3
3
  module.exports.Node = require('./node.js')
4
4
  module.exports.Link = require('./link.js')
5
5
  module.exports.Edge = require('./edge.js')
6
+ module.exports.Shrinkwrap = require('./shrinkwrap.js')
6
7
  // XXX export the other classes, too. shrinkwrap, diff, etc.
7
8
  // they're handy!