@npmcli/arborist 7.3.1 → 7.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.
@@ -38,22 +38,15 @@ const resetDepFlags = require('../reset-dep-flags.js')
38
38
  // them with unit tests and reuse them across mixins
39
39
  const _updateAll = Symbol.for('updateAll')
40
40
  const _flagsSuspect = Symbol.for('flagsSuspect')
41
- const _workspaces = Symbol.for('workspaces')
42
41
  const _setWorkspaces = Symbol.for('setWorkspaces')
43
42
  const _updateNames = Symbol.for('updateNames')
44
43
  const _resolvedAdd = Symbol.for('resolvedAdd')
45
44
  const _usePackageLock = Symbol.for('usePackageLock')
46
45
  const _rpcache = Symbol.for('realpathCache')
47
46
  const _stcache = Symbol.for('statCache')
48
- const _includeWorkspaceRoot = Symbol.for('includeWorkspaceRoot')
49
-
50
- // exposed symbol for unit testing the placeDep method directly
51
- const _peerSetSource = Symbol.for('peerSetSource')
52
47
 
53
48
  // used by Reify mixin
54
- const _force = Symbol.for('force')
55
- const _global = Symbol.for('global')
56
- const _idealTreePrune = Symbol.for('idealTreePrune')
49
+ const _addNodeToTrashList = Symbol.for('addNodeToTrashList')
57
50
 
58
51
  // Push items in, pop them sorted by depth and then path
59
52
  // Sorts physically shallower deps up to the front of the queue, because
@@ -117,6 +110,10 @@ module.exports = cls => class IdealTreeBuilder extends cls {
117
110
  #loadFailures = new Set()
118
111
  #manifests = new Map()
119
112
  #mutateTree = false
113
+ // a map of each module in a peer set to the thing that depended on
114
+ // that set of peers in the first place. Use a WeakMap so that we
115
+ // don't hold onto references for nodes that are garbage collected.
116
+ #peerSetSource = new WeakMap()
120
117
  #preferDedupe = false
121
118
  #prune
122
119
  #strictPeerDeps
@@ -131,20 +128,16 @@ module.exports = cls => class IdealTreeBuilder extends cls {
131
128
 
132
129
  const {
133
130
  follow = false,
134
- force = false,
135
- global = false,
136
131
  installStrategy = 'hoisted',
137
132
  idealTree = null,
138
- includeWorkspaceRoot = false,
139
133
  installLinks = false,
140
134
  legacyPeerDeps = false,
141
135
  packageLock = true,
142
136
  strictPeerDeps = false,
143
- workspaces = [],
137
+ workspaces,
138
+ global,
144
139
  } = options
145
140
 
146
- this[_workspaces] = workspaces || []
147
- this[_force] = !!force
148
141
  this.#strictPeerDeps = !!strictPeerDeps
149
142
 
150
143
  this.idealTree = idealTree
@@ -152,24 +145,16 @@ module.exports = cls => class IdealTreeBuilder extends cls {
152
145
  this.legacyPeerDeps = legacyPeerDeps
153
146
 
154
147
  this[_usePackageLock] = packageLock
155
- this[_global] = !!global
156
148
  this.#installStrategy = global ? 'shallow' : installStrategy
157
149
  this.#follow = !!follow
158
150
 
159
- if (this[_workspaces].length && this[_global]) {
151
+ if (workspaces?.length && global) {
160
152
  throw new Error('Cannot operate on workspaces in global mode')
161
153
  }
162
154
 
163
155
  this[_updateAll] = false
164
156
  this[_updateNames] = []
165
157
  this[_resolvedAdd] = []
166
-
167
- // a map of each module in a peer set to the thing that depended on
168
- // that set of peers in the first place. Use a WeakMap so that we
169
- // don't hold onto references for nodes that are garbage collected.
170
- this[_peerSetSource] = new WeakMap()
171
-
172
- this[_includeWorkspaceRoot] = includeWorkspaceRoot
173
158
  }
174
159
 
175
160
  get explicitRequests () {
@@ -196,7 +181,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
196
181
 
197
182
  process.emit('time', 'idealTree')
198
183
 
199
- if (!options.add && !options.rm && !options.update && this[_global]) {
184
+ if (!options.add && !options.rm && !options.update && this.options.global) {
200
185
  throw new Error('global requires add, rm, or update option')
201
186
  }
202
187
 
@@ -232,7 +217,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
232
217
  for (const node of this.idealTree.inventory.values()) {
233
218
  if (!node.optional) {
234
219
  try {
235
- checkEngine(node.package, npmVersion, nodeVersion, this[_force])
220
+ checkEngine(node.package, npmVersion, nodeVersion, this.options.force)
236
221
  } catch (err) {
237
222
  if (engineStrict) {
238
223
  throw err
@@ -243,7 +228,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
243
228
  current: err.current,
244
229
  })
245
230
  }
246
- checkPlatform(node.package, this[_force])
231
+ checkPlatform(node.package, this.options.force)
247
232
  }
248
233
  }
249
234
  }
@@ -295,7 +280,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
295
280
  async #initTree () {
296
281
  process.emit('time', 'idealTree:init')
297
282
  let root
298
- if (this[_global]) {
283
+ if (this.options.global) {
299
284
  root = await this.#globalRootNode()
300
285
  } else {
301
286
  try {
@@ -313,7 +298,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
313
298
  // When updating all, we load the shrinkwrap, but don't bother
314
299
  // to build out the full virtual tree from it, since we'll be
315
300
  // reconstructing it anyway.
316
- .then(root => this[_global] ? root
301
+ .then(root => this.options.global ? root
317
302
  : !this[_usePackageLock] || this[_updateAll]
318
303
  ? Shrinkwrap.reset({
319
304
  path: this.path,
@@ -329,7 +314,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
329
314
  // Load on a new Arborist object, so the Nodes aren't the same,
330
315
  // or else it'll get super confusing when we change them!
331
316
  .then(async root => {
332
- if ((!this[_updateAll] && !this[_global] && !root.meta.loadedFromDisk) || (this[_global] && this[_updateNames].length)) {
317
+ if ((!this[_updateAll] && !this.options.global && !root.meta.loadedFromDisk) || (this.options.global && this[_updateNames].length)) {
333
318
  await new this.constructor(this.options).loadActual({ root })
334
319
  const tree = root.target
335
320
  // even though we didn't load it from a package-lock.json FILE,
@@ -408,7 +393,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
408
393
  devOptional: false,
409
394
  peer: false,
410
395
  optional: false,
411
- global: this[_global],
396
+ global: this.options.global,
412
397
  installLinks: this.installLinks,
413
398
  legacyPeerDeps: this.legacyPeerDeps,
414
399
  loadOverrides: true,
@@ -423,7 +408,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
423
408
  devOptional: false,
424
409
  peer: false,
425
410
  optional: false,
426
- global: this[_global],
411
+ global: this.options.global,
427
412
  installLinks: this.installLinks,
428
413
  legacyPeerDeps: this.legacyPeerDeps,
429
414
  root,
@@ -438,11 +423,11 @@ module.exports = cls => class IdealTreeBuilder extends cls {
438
423
  process.emit('time', 'idealTree:userRequests')
439
424
  const tree = this.idealTree.target
440
425
 
441
- if (!this[_workspaces].length) {
426
+ if (!this.options.workspaces.length) {
442
427
  await this.#applyUserRequestsToNode(tree, options)
443
428
  } else {
444
- const nodes = this.workspaceNodes(tree, this[_workspaces])
445
- if (this[_includeWorkspaceRoot]) {
429
+ const nodes = this.workspaceNodes(tree, this.options.workspaces)
430
+ if (this.options.includeWorkspaceRoot) {
446
431
  nodes.push(tree)
447
432
  }
448
433
  const appliedRequests = nodes.map(
@@ -458,14 +443,14 @@ module.exports = cls => class IdealTreeBuilder extends cls {
458
443
  // If we have a list of package names to update, and we know it's
459
444
  // going to update them wherever they are, add any paths into those
460
445
  // named nodes to the buildIdealTree queue.
461
- if (!this[_global] && this[_updateNames].length) {
446
+ if (!this.options.global && this[_updateNames].length) {
462
447
  this.#queueNamedUpdates()
463
448
  }
464
449
 
465
450
  // global updates only update the globalTop nodes, but we need to know
466
451
  // that they're there, and not reinstall the world unnecessarily.
467
452
  const globalExplicitUpdateNames = []
468
- if (this[_global] && (this[_updateAll] || this[_updateNames].length)) {
453
+ if (this.options.global && (this[_updateAll] || this[_updateNames].length)) {
469
454
  const nm = resolve(this.path, 'node_modules')
470
455
  const paths = await readdirScoped(nm).catch(() => [])
471
456
  for (const p of paths) {
@@ -510,7 +495,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
510
495
  // triggers a refresh of all edgesOut. this has to be done BEFORE
511
496
  // adding the edges to explicitRequests, because the package setter
512
497
  // resets all edgesOut.
513
- if (add && add.length || rm && rm.length || this[_global]) {
498
+ if (add && add.length || rm && rm.length || this.options.global) {
514
499
  tree.package = tree.package
515
500
  }
516
501
 
@@ -616,7 +601,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
616
601
  //
617
602
  // XXX: how to handle top nodes that aren't the root? Maybe the report
618
603
  // just tells the user to cd into that directory and fix it?
619
- if (this[_force] && this.auditReport && this.auditReport.topVulns.size) {
604
+ if (this.options.force && this.auditReport && this.auditReport.topVulns.size) {
620
605
  options.add = options.add || []
621
606
  options.rm = options.rm || []
622
607
  const nodesTouched = new Set()
@@ -900,7 +885,7 @@ This is a one-time fix-up, please be patient...
900
885
  // dep if allowed.
901
886
 
902
887
  const tasks = []
903
- const peerSource = this[_peerSetSource].get(node) || node
888
+ const peerSource = this.#peerSetSource.get(node) || node
904
889
  for (const edge of this.#problemEdges(node)) {
905
890
  if (edge.peerConflicted) {
906
891
  continue
@@ -958,7 +943,7 @@ This is a one-time fix-up, please be patient...
958
943
 
959
944
  auditReport: this.auditReport,
960
945
  explicitRequest: this.#explicitRequests.has(edge),
961
- force: this[_force],
946
+ force: this.options.force,
962
947
  installLinks: this.installLinks,
963
948
  installStrategy: this.#installStrategy,
964
949
  legacyPeerDeps: this.legacyPeerDeps,
@@ -1099,13 +1084,13 @@ This is a one-time fix-up, please be patient...
1099
1084
 
1100
1085
  // keep track of the thing that caused this node to be included.
1101
1086
  const src = parent.sourceReference
1102
- this[_peerSetSource].set(node, src)
1087
+ this.#peerSetSource.set(node, src)
1103
1088
 
1104
1089
  // do not load the peers along with the set if this is a global top pkg
1105
1090
  // otherwise we'll be tempted to put peers as other top-level installed
1106
1091
  // things, potentially clobbering what's there already, which is not
1107
1092
  // what we want. the missing edges will be picked up on the next pass.
1108
- if (this[_global] && edge.from.isProjectRoot) {
1093
+ if (this.options.global && edge.from.isProjectRoot) {
1109
1094
  return node
1110
1095
  }
1111
1096
 
@@ -1328,7 +1313,7 @@ This is a one-time fix-up, please be patient...
1328
1313
  const parentEdge = node.parent.edgesOut.get(edge.name)
1329
1314
  const { isProjectRoot, isWorkspace } = node.parent.sourceReference
1330
1315
  const isMine = isProjectRoot || isWorkspace
1331
- const conflictOK = this[_force] || !isMine && !this.#strictPeerDeps
1316
+ const conflictOK = this.options.force || !isMine && !this.#strictPeerDeps
1332
1317
 
1333
1318
  if (!edge.to) {
1334
1319
  if (!parentEdge) {
@@ -1415,7 +1400,7 @@ This is a one-time fix-up, please be patient...
1415
1400
  currentEdge: currentEdge ? currentEdge.explain() : null,
1416
1401
  edge: edge.explain(),
1417
1402
  strictPeerDeps: this.#strictPeerDeps,
1418
- force: this[_force],
1403
+ force: this.options.force,
1419
1404
  }
1420
1405
  }
1421
1406
 
@@ -1503,7 +1488,7 @@ This is a one-time fix-up, please be patient...
1503
1488
  // otherwise, don't bother.
1504
1489
  const needPrune = metaFromDisk && (mutateTree || flagsSuspect)
1505
1490
  if (this.#prune && needPrune) {
1506
- this[_idealTreePrune]()
1491
+ this.#idealTreePrune()
1507
1492
  for (const node of this.idealTree.inventory.values()) {
1508
1493
  if (node.extraneous) {
1509
1494
  node.parent = null
@@ -1514,7 +1499,7 @@ This is a one-time fix-up, please be patient...
1514
1499
  process.emit('timeEnd', 'idealTree:fixDepFlags')
1515
1500
  }
1516
1501
 
1517
- [_idealTreePrune] () {
1502
+ #idealTreePrune () {
1518
1503
  for (const node of this.idealTree.inventory.values()) {
1519
1504
  if (node.extraneous) {
1520
1505
  node.parent = null
@@ -1534,4 +1519,29 @@ This is a one-time fix-up, please be patient...
1534
1519
  }
1535
1520
  }
1536
1521
  }
1522
+
1523
+ async prune (options = {}) {
1524
+ // allow the user to set options on the ctor as well.
1525
+ // XXX: deprecate separate method options objects.
1526
+ options = { ...this.options, ...options }
1527
+
1528
+ await this.buildIdealTree(options)
1529
+
1530
+ this.#idealTreePrune()
1531
+
1532
+ if (!this.options.workspacesEnabled) {
1533
+ const excludeNodes = this.excludeWorkspacesDependencySet(this.idealTree)
1534
+ for (const node of this.idealTree.inventory.values()) {
1535
+ if (
1536
+ node.parent !== null
1537
+ && !node.isProjectRoot
1538
+ && !excludeNodes.has(node)
1539
+ ) {
1540
+ this[_addNodeToTrashList](node)
1541
+ }
1542
+ }
1543
+ }
1544
+
1545
+ return this.reify(options)
1546
+ }
1537
1547
  }
@@ -29,15 +29,16 @@
29
29
  const { resolve } = require('path')
30
30
  const { homedir } = require('os')
31
31
  const { depth } = require('treeverse')
32
+ const mapWorkspaces = require('@npmcli/map-workspaces')
33
+ const log = require('proc-log')
34
+
32
35
  const { saveTypeMap } = require('../add-rm-pkg-deps.js')
36
+ const AuditReport = require('../audit-report.js')
37
+ const relpath = require('../relpath.js')
33
38
 
34
39
  const mixins = [
35
40
  require('../tracker.js'),
36
- require('./pruner.js'),
37
- require('./deduper.js'),
38
- require('./audit.js'),
39
41
  require('./build-ideal-tree.js'),
40
- require('./set-workspaces.js'),
41
42
  require('./load-actual.js'),
42
43
  require('./load-virtual.js'),
43
44
  require('./rebuild.js'),
@@ -45,9 +46,8 @@ const mixins = [
45
46
  require('./isolated-reifier.js'),
46
47
  ]
47
48
 
48
- const _workspacesEnabled = Symbol.for('workspacesEnabled')
49
+ const _setWorkspaces = Symbol.for('setWorkspaces')
49
50
  const Base = mixins.reduce((a, b) => b(a), require('events'))
50
- const getWorkspaceNodes = require('../get-workspace-nodes.js')
51
51
 
52
52
  // if it's 1, 2, or 3, set it explicitly that.
53
53
  // if undefined or null, set it null
@@ -72,20 +72,26 @@ class Arborist extends Base {
72
72
  nodeVersion: process.version,
73
73
  ...options,
74
74
  Arborist: this.constructor,
75
- path: options.path || '.',
75
+ binLinks: 'binLinks' in options ? !!options.binLinks : true,
76
76
  cache: options.cache || `${homedir()}/.npm/_cacache`,
77
+ force: !!options.force,
78
+ global: !!options.global,
79
+ ignoreScripts: !!options.ignoreScripts,
80
+ installStrategy: options.global ? 'shallow' : (options.installStrategy ? options.installStrategy : 'hoisted'),
81
+ lockfileVersion: lockfileVersion(options.lockfileVersion),
77
82
  packumentCache: options.packumentCache || new Map(),
78
- workspacesEnabled: options.workspacesEnabled !== false,
83
+ path: options.path || '.',
84
+ rebuildBundle: 'rebuildBundle' in options ? !!options.rebuildBundle : true,
79
85
  replaceRegistryHost: options.replaceRegistryHost,
80
- lockfileVersion: lockfileVersion(options.lockfileVersion),
81
- installStrategy: options.global ? 'shallow' : (options.installStrategy ? options.installStrategy : 'hoisted'),
86
+ scriptShell: options.scriptShell,
87
+ workspaces: options.workspaces || [],
88
+ workspacesEnabled: options.workspacesEnabled !== false,
82
89
  }
90
+ // TODO is this even used? If not is that a bug?
83
91
  this.replaceRegistryHost = this.options.replaceRegistryHost =
84
92
  (!this.options.replaceRegistryHost || this.options.replaceRegistryHost === 'npmjs') ?
85
93
  'registry.npmjs.org' : this.options.replaceRegistryHost
86
94
 
87
- this[_workspacesEnabled] = this.options.workspacesEnabled
88
-
89
95
  if (options.saveType && !saveTypeMap.get(options.saveType)) {
90
96
  throw new Error(`Invalid saveType ${options.saveType}`)
91
97
  }
@@ -97,12 +103,40 @@ class Arborist extends Base {
97
103
  // TODO: We should change these to static functions instead
98
104
  // of methods for the next major version
99
105
 
100
- // returns an array of the actual nodes for all the workspaces
106
+ // Get the actual nodes corresponding to a root node's child workspaces,
107
+ // given a list of workspace names.
101
108
  workspaceNodes (tree, workspaces) {
102
- return getWorkspaceNodes(tree, workspaces)
109
+ const wsMap = tree.workspaces
110
+ if (!wsMap) {
111
+ log.warn('workspaces', 'filter set, but no workspaces present')
112
+ return []
113
+ }
114
+
115
+ const nodes = []
116
+ for (const name of workspaces) {
117
+ const path = wsMap.get(name)
118
+ if (!path) {
119
+ log.warn('workspaces', `${name} in filter set, but not in workspaces`)
120
+ continue
121
+ }
122
+
123
+ const loc = relpath(tree.realpath, path)
124
+ const node = tree.inventory.get(loc)
125
+
126
+ if (!node) {
127
+ log.warn('workspaces', `${name} in filter set, but no workspace folder present`)
128
+ continue
129
+ }
130
+
131
+ nodes.push(node)
132
+ }
133
+
134
+ return nodes
103
135
  }
104
136
 
105
137
  // returns a set of workspace nodes and all their deps
138
+ // TODO why is includeWorkspaceRoot a param?
139
+ // TODO why is workspaces a param?
106
140
  workspaceDependencySet (tree, workspaces, includeWorkspaceRoot) {
107
141
  const wsNodes = this.workspaceNodes(tree, workspaces)
108
142
  if (includeWorkspaceRoot) {
@@ -162,6 +196,60 @@ class Arborist extends Base {
162
196
  })
163
197
  return rootDepSet
164
198
  }
199
+
200
+ async [_setWorkspaces] (node) {
201
+ const workspaces = await mapWorkspaces({
202
+ cwd: node.path,
203
+ pkg: node.package,
204
+ })
205
+
206
+ if (node && workspaces.size) {
207
+ node.workspaces = workspaces
208
+ }
209
+
210
+ return node
211
+ }
212
+
213
+ async audit (options = {}) {
214
+ this.addTracker('audit')
215
+ if (this.options.global) {
216
+ throw Object.assign(
217
+ new Error('`npm audit` does not support testing globals'),
218
+ { code: 'EAUDITGLOBAL' }
219
+ )
220
+ }
221
+
222
+ // allow the user to set options on the ctor as well.
223
+ // XXX: deprecate separate method options objects.
224
+ options = { ...this.options, ...options }
225
+
226
+ process.emit('time', 'audit')
227
+ let tree
228
+ if (options.packageLock === false) {
229
+ // build ideal tree
230
+ await this.loadActual(options)
231
+ await this.buildIdealTree()
232
+ tree = this.idealTree
233
+ } else {
234
+ tree = await this.loadVirtual()
235
+ }
236
+ if (this.options.workspaces.length) {
237
+ options.filterSet = this.workspaceDependencySet(
238
+ tree,
239
+ this.options.workspaces,
240
+ this.options.includeWorkspaceRoot
241
+ )
242
+ }
243
+ if (!options.workspacesEnabled) {
244
+ options.filterSet =
245
+ this.excludeWorkspacesDependencySet(tree)
246
+ }
247
+ this.auditReport = await AuditReport.load(tree, options)
248
+ const ret = options.fix ? this.reify(options) : this.auditReport
249
+ process.emit('timeEnd', 'audit')
250
+ this.finishTracker('audit')
251
+ return ret
252
+ }
165
253
  }
166
254
 
167
255
  module.exports = Arborist
@@ -16,7 +16,6 @@ const realpath = require('../realpath.js')
16
16
 
17
17
  // public symbols
18
18
  const _changePath = Symbol.for('_changePath')
19
- const _global = Symbol.for('global')
20
19
  const _setWorkspaces = Symbol.for('setWorkspaces')
21
20
  const _rpcache = Symbol.for('realpathCache')
22
21
  const _stcache = Symbol.for('statCache')
@@ -45,8 +44,6 @@ module.exports = cls => class ActualLoader extends cls {
45
44
  constructor (options) {
46
45
  super(options)
47
46
 
48
- this[_global] = !!options.global
49
-
50
47
  // the tree of nodes on disk
51
48
  this.actualTree = options.actualTree
52
49
 
@@ -58,6 +55,7 @@ module.exports = cls => class ActualLoader extends cls {
58
55
  }
59
56
 
60
57
  // public method
58
+ // TODO remove options param in next semver major
61
59
  async loadActual (options = {}) {
62
60
  // In the past this.actualTree was set as a promise that eventually
63
61
  // resolved, and overwrite this.actualTree with the resolved value. This
@@ -100,7 +98,7 @@ module.exports = cls => class ActualLoader extends cls {
100
98
  async #loadActual (options) {
101
99
  // mostly realpath to throw if the root doesn't exist
102
100
  const {
103
- global = false,
101
+ global,
104
102
  filter = () => true,
105
103
  root = null,
106
104
  transplantFilter = () => true,