@npmcli/arborist 2.2.6 → 2.3.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.
@@ -33,7 +33,13 @@ for (const arg of process.argv.slice(2)) {
33
33
  options.omit.push(arg.substr('--omit='.length))
34
34
  } else if (/^--before=/.test(arg))
35
35
  options.before = new Date(arg.substr('--before='.length))
36
- else if (/^--[^=]+=/.test(arg)) {
36
+ else if (/^-w.+/.test(arg)) {
37
+ options.workspaces = options.workspaces || []
38
+ options.workspaces.push(arg.replace(/^-w/, ''))
39
+ } else if (/^--workspace=/.test(arg)) {
40
+ options.workspaces = options.workspaces || []
41
+ options.workspaces.push(arg.replace(/^--workspace=/, ''))
42
+ } else if (/^--[^=]+=/.test(arg)) {
37
43
  const [key, ...v] = arg.replace(/^--/, '').split('=')
38
44
  const val = v.join('=')
39
45
  options[key] = val === 'false' ? false : val === 'true' ? true : val
@@ -71,7 +71,7 @@ const addSingle = ({pkg, spec, saveBundle, saveType}) => {
71
71
  pkg.devDependencies[name] = pkg.peerDependencies[name]
72
72
  }
73
73
 
74
- if (saveBundle) {
74
+ if (saveBundle && saveType !== 'peer' && saveType !== 'peerOptional') {
75
75
  // keep it sorted, keep it unique
76
76
  const bd = new Set(pkg.bundleDependencies || [])
77
77
  bd.add(spec.name)
@@ -44,12 +44,14 @@ const _currentDep = Symbol('currentDep')
44
44
  const _updateAll = Symbol('updateAll')
45
45
  const _mutateTree = Symbol('mutateTree')
46
46
  const _flagsSuspect = Symbol.for('flagsSuspect')
47
+ const _workspaces = Symbol.for('workspaces')
47
48
  const _prune = Symbol('prune')
48
49
  const _preferDedupe = Symbol('preferDedupe')
49
50
  const _legacyBundling = Symbol('legacyBundling')
50
51
  const _parseSettings = Symbol('parseSettings')
51
52
  const _initTree = Symbol('initTree')
52
53
  const _applyUserRequests = Symbol('applyUserRequests')
54
+ const _applyUserRequestsToNode = Symbol('applyUserRequestsToNode')
53
55
  const _inflateAncientLockfile = Symbol('inflateAncientLockfile')
54
56
  const _buildDeps = Symbol('buildDeps')
55
57
  const _buildDepStep = Symbol('buildDepStep')
@@ -109,7 +111,7 @@ const _peerSetSource = Symbol.for('peerSetSource')
109
111
 
110
112
  // used by Reify mixin
111
113
  const _force = Symbol.for('force')
112
- const _explicitRequests = Symbol.for('explicitRequests')
114
+ const _explicitRequests = Symbol('explicitRequests')
113
115
  const _global = Symbol.for('global')
114
116
  const _idealTreePrune = Symbol.for('idealTreePrune')
115
117
 
@@ -130,8 +132,10 @@ module.exports = cls => class IdealTreeBuilder extends cls {
130
132
  force = false,
131
133
  packageLock = true,
132
134
  strictPeerDeps = false,
135
+ workspaces = [],
133
136
  } = options
134
137
 
138
+ this[_workspaces] = workspaces || []
135
139
  this[_force] = !!force
136
140
  this[_strictPeerDeps] = !!strictPeerDeps
137
141
 
@@ -143,6 +147,9 @@ module.exports = cls => class IdealTreeBuilder extends cls {
143
147
  this[_globalStyle] = this[_global] || globalStyle
144
148
  this[_follow] = !!follow
145
149
 
150
+ if (this[_workspaces].length && this[_global])
151
+ throw new Error('Cannot operate on workspaces in global mode')
152
+
146
153
  this[_explicitRequests] = new Set()
147
154
  this[_preferDedupe] = false
148
155
  this[_legacyBundling] = false
@@ -157,6 +164,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
157
164
  this[_manifests] = new Map()
158
165
  this[_peerConflict] = null
159
166
  this[_edgesOverridden] = new Set()
167
+ this[_resolvedAdd] = []
160
168
 
161
169
  // a map of each module in a peer set to the thing that depended on
162
170
  // that set of peers in the first place. Use a WeakMap so that we
@@ -204,8 +212,8 @@ module.exports = cls => class IdealTreeBuilder extends cls {
204
212
 
205
213
  try {
206
214
  await this[_initTree]()
207
- await this[_applyUserRequests](options)
208
215
  await this[_inflateAncientLockfile]()
216
+ await this[_applyUserRequests](options)
209
217
  await this[_buildDeps]()
210
218
  await this[_fixDepFlags]()
211
219
  await this[_pruneFailedOptional]()
@@ -266,6 +274,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
266
274
  this[_preferDedupe] = !!options.preferDedupe
267
275
  this[_legacyBundling] = !!options.legacyBundling
268
276
  this[_updateNames] = update.names
277
+
269
278
  this[_updateAll] = update.all
270
279
  // we prune by default unless explicitly set to boolean false
271
280
  this[_prune] = options.prune !== false
@@ -387,6 +396,42 @@ module.exports = cls => class IdealTreeBuilder extends cls {
387
396
  async [_applyUserRequests] (options) {
388
397
  process.emit('time', 'idealTree:userRequests')
389
398
  const tree = this.idealTree.target || this.idealTree
399
+
400
+ if (!this[_workspaces].length) {
401
+ return this[_applyUserRequestsToNode](tree, options).then(() =>
402
+ process.emit('timeEnd', 'idealTree:userRequests'))
403
+ }
404
+
405
+ const wsMap = tree.workspaces
406
+ if (!wsMap) {
407
+ this.log.warn('idealTree', 'Workspace filter set, but no workspaces present')
408
+ return
409
+ }
410
+
411
+ const promises = []
412
+ for (const name of this[_workspaces]) {
413
+ const path = wsMap.get(name)
414
+ if (!path) {
415
+ this.log.warn('idealTree', `Workspace ${name} in filter set, but not in workspaces`)
416
+ continue
417
+ }
418
+ const loc = relpath(tree.realpath, path)
419
+ const node = tree.inventory.get(loc)
420
+
421
+ /* istanbul ignore if - should be impossible */
422
+ if (!node) {
423
+ this.log.warn('idealTree', `Workspace ${name} in filter set, but no workspace folder present`)
424
+ continue
425
+ }
426
+
427
+ promises.push(this[_applyUserRequestsToNode](node, options))
428
+ }
429
+
430
+ return Promise.all(promises).then(() =>
431
+ process.emit('timeEnd', 'idealTree:userRequests'))
432
+ }
433
+
434
+ async [_applyUserRequestsToNode] (tree, options) {
390
435
  // If we have a list of package names to update, and we know it's
391
436
  // going to update them wherever they are, add any paths into those
392
437
  // named nodes to the buildIdealTree queue.
@@ -395,51 +440,63 @@ module.exports = cls => class IdealTreeBuilder extends cls {
395
440
 
396
441
  // global updates only update the globalTop nodes, but we need to know
397
442
  // that they're there, and not reinstall the world unnecessarily.
443
+ const globalExplicitUpdateNames = []
398
444
  if (this[_global] && (this[_updateAll] || this[_updateNames].length)) {
399
445
  const nm = resolve(this.path, 'node_modules')
400
446
  for (const name of await readdir(nm).catch(() => [])) {
401
- if (this[_updateNames].includes(name))
402
- this[_explicitRequests].add(name)
403
447
  tree.package.dependencies = tree.package.dependencies || {}
404
- if (this[_updateAll] || this[_updateNames].includes(name))
448
+ const updateName = this[_updateNames].includes(name)
449
+ if (this[_updateAll] || updateName) {
450
+ if (updateName)
451
+ globalExplicitUpdateNames.push(name)
405
452
  tree.package.dependencies[name] = '*'
453
+ }
406
454
  }
407
455
  }
408
456
 
409
457
  if (this.auditReport && this.auditReport.size > 0)
410
458
  this[_queueVulnDependents](options)
411
459
 
412
- if (options.rm && options.rm.length) {
413
- addRmPkgDeps.rm(tree.package, options.rm)
414
- for (const name of options.rm)
415
- this[_explicitRequests].add(name)
460
+ const { add, rm } = options
461
+
462
+ if (rm && rm.length) {
463
+ addRmPkgDeps.rm(tree.package, rm)
464
+ for (const name of rm)
465
+ this[_explicitRequests].add({ from: tree, name, action: 'DELETE' })
416
466
  }
417
467
 
418
- if (options.add)
419
- await this[_add](options)
468
+ if (add && add.length)
469
+ await this[_add](tree, options)
420
470
 
421
- // triggers a refresh of all edgesOut
422
- if (options.add && options.add.length || options.rm && options.rm.length || this[_global])
471
+ // triggers a refresh of all edgesOut. this has to be done BEFORE
472
+ // adding the edges to explicitRequests, because the package setter
473
+ // resets all edgesOut.
474
+ if (add && add.length || rm && rm.length || this[_global])
423
475
  tree.package = tree.package
424
- process.emit('timeEnd', 'idealTree:userRequests')
476
+
477
+ for (const spec of this[_resolvedAdd])
478
+ this[_explicitRequests].add(tree.edgesOut.get(spec.name))
479
+ for (const name of globalExplicitUpdateNames)
480
+ this[_explicitRequests].add(tree.edgesOut.get(name))
425
481
  }
426
482
 
427
483
  // This returns a promise because we might not have the name yet,
428
484
  // and need to call pacote.manifest to find the name.
429
- [_add] ({add, saveType = null, saveBundle = false}) {
485
+ [_add] (tree, {add, saveType = null, saveBundle = false}) {
430
486
  // get the name for each of the specs in the list.
431
487
  // ie, doing `foo@bar` we just return foo
432
488
  // but if it's a url or git, we don't know the name until we
433
489
  // fetch it and look in its manifest.
434
- return Promise.all(add.map(rawSpec =>
435
- this[_retrieveSpecName](npa(rawSpec))
490
+ return Promise.all(add.map(rawSpec => {
491
+ // We do NOT provide the path here, because user-additions need
492
+ // to be resolved relative to the CWD the user is in.
493
+ return this[_retrieveSpecName](npa(rawSpec))
436
494
  .then(add => this[_updateFilePath](add))
437
495
  .then(add => this[_followSymlinkPath](add))
438
- )).then(add => {
439
- this[_resolvedAdd] = add
496
+ })).then(add => {
497
+ this[_resolvedAdd].push(...add)
440
498
  // now add is a list of spec objects with names.
441
499
  // find a home for each of them!
442
- const tree = this.idealTree.target || this.idealTree
443
500
  addRmPkgDeps.add({
444
501
  pkg: tree.package,
445
502
  add,
@@ -447,8 +504,6 @@ module.exports = cls => class IdealTreeBuilder extends cls {
447
504
  saveType,
448
505
  path: this.path,
449
506
  })
450
- for (const spec of add)
451
- this[_explicitRequests].add(spec.name)
452
507
  })
453
508
  }
454
509
 
@@ -881,6 +936,8 @@ This is a one-time fix-up, please be patient...
881
936
  // create a virtual root node with the same deps as the node that
882
937
  // is requesting this one, so that we can get all the peer deps in
883
938
  // a context where they're likely to be resolvable.
939
+ // Note that the virtual root will also have virtual copies of the
940
+ // targets of any child Links, so that they resolve appropriately.
884
941
  const parent = parent_ || this[_virtualRoot](edge.from)
885
942
  const realParent = edge.peer ? edge.from.resolveParent : edge.from
886
943
 
@@ -934,11 +991,23 @@ This is a one-time fix-up, please be patient...
934
991
  return this[_virtualRoots].get(node)
935
992
 
936
993
  const vr = new Node({
937
- path: '/virtual-root',
994
+ path: node.realpath,
938
995
  sourceReference: node,
939
996
  legacyPeerDeps: this.legacyPeerDeps,
940
997
  })
941
998
 
999
+ // also need to set up any targets from any link deps, so that
1000
+ // they are properly reflected in the virtual environment
1001
+ for (const child of node.children.values()) {
1002
+ if (child.isLink) {
1003
+ new Node({
1004
+ path: child.realpath,
1005
+ sourceReference: child.target,
1006
+ root: vr,
1007
+ })
1008
+ }
1009
+ }
1010
+
942
1011
  this[_virtualRoots].set(node, vr)
943
1012
  return vr
944
1013
  }
@@ -975,7 +1044,7 @@ This is a one-time fix-up, please be patient...
975
1044
  // if it's peerOptional and not explicitly requested.
976
1045
  if (!edge.to) {
977
1046
  return edge.type !== 'peerOptional' ||
978
- this[_explicitRequests].has(edge.name)
1047
+ this[_explicitRequests].has(edge)
979
1048
  }
980
1049
 
981
1050
  // If the edge has an error, there's a problem.
@@ -991,7 +1060,7 @@ This is a one-time fix-up, please be patient...
991
1060
  return true
992
1061
 
993
1062
  // If the user has explicitly asked to install this package, it's a problem.
994
- if (node.isProjectRoot && this[_explicitRequests].has(edge.name))
1063
+ if (node.isProjectRoot && this[_explicitRequests].has(edge))
995
1064
  return true
996
1065
 
997
1066
  // No problems!
@@ -1115,7 +1184,7 @@ This is a one-time fix-up, please be patient...
1115
1184
  continue
1116
1185
 
1117
1186
  // problem
1118
- this[_failPeerConflict](edge)
1187
+ this[_failPeerConflict](edge, parentEdge)
1119
1188
  }
1120
1189
  }
1121
1190
 
@@ -1131,17 +1200,17 @@ This is a one-time fix-up, please be patient...
1131
1200
  continue
1132
1201
 
1133
1202
  // ok, it's the root, or we're in unforced strict mode, so this is bad
1134
- this[_failPeerConflict](edge)
1203
+ this[_failPeerConflict](edge, parentEdge)
1135
1204
  }
1136
1205
  return node
1137
1206
  }
1138
1207
 
1139
- [_failPeerConflict] (edge) {
1140
- const expl = this[_explainPeerConflict](edge)
1208
+ [_failPeerConflict] (edge, currentEdge) {
1209
+ const expl = this[_explainPeerConflict](edge, currentEdge)
1141
1210
  throw Object.assign(new Error('unable to resolve dependency tree'), expl)
1142
1211
  }
1143
1212
 
1144
- [_explainPeerConflict] (edge) {
1213
+ [_explainPeerConflict] (edge, currentEdge) {
1145
1214
  const node = edge.from
1146
1215
  const curNode = node.resolve(edge.name)
1147
1216
  const pc = this[_peerConflict] || { peer: null, current: null }
@@ -1150,6 +1219,10 @@ This is a one-time fix-up, please be patient...
1150
1219
  return {
1151
1220
  code: 'ERESOLVE',
1152
1221
  current,
1222
+ // it SHOULD be impossible to get here without a current node in place,
1223
+ // but this at least gives us something report on when bugs creep into
1224
+ // the tree handling logic.
1225
+ currentEdge: currentEdge ? currentEdge.explain() : null,
1153
1226
  edge: edge.explain(),
1154
1227
  peerConflict,
1155
1228
  strictPeerDeps: this[_strictPeerDeps],
@@ -1174,7 +1247,7 @@ This is a one-time fix-up, please be patient...
1174
1247
  [_placeDep] (dep, node, edge, peerEntryEdge = null, peerPath = []) {
1175
1248
  if (edge.to &&
1176
1249
  !edge.error &&
1177
- !this[_explicitRequests].has(edge.name) &&
1250
+ !this[_explicitRequests].has(edge) &&
1178
1251
  !this[_updateNames].includes(edge.name) &&
1179
1252
  !this[_isVulnerable](edge.to))
1180
1253
  return []
@@ -1464,9 +1537,15 @@ This is a one-time fix-up, please be patient...
1464
1537
  if (target.children.has(edge.name)) {
1465
1538
  const current = target.children.get(edge.name)
1466
1539
 
1467
- // same thing = keep
1468
- if (dep.matches(current))
1469
- return KEEP
1540
+ // same thing = keep, UNLESS the current doesn't satisfy and new
1541
+ // one does satisfy. This can happen if it's a link to a matching target
1542
+ // at a different location, which satisfies a version dep, but not a
1543
+ // file: dep. If neither of them satisfy, then we can replace it,
1544
+ // because presumably it's better for a peer or something.
1545
+ if (dep.matches(current)) {
1546
+ if (current.satisfies(edge) || !dep.satisfies(edge))
1547
+ return KEEP
1548
+ }
1470
1549
 
1471
1550
  const { version: curVer } = current
1472
1551
  const { version: newVer } = dep
@@ -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
 
@@ -6,6 +6,7 @@ const rpj = require('read-package-json-fast')
6
6
  const { updateDepSpec } = require('../dep-spec.js')
7
7
  const AuditReport = require('../audit-report.js')
8
8
  const {subset} = require('semver')
9
+ const npa = require('npm-package-arg')
9
10
 
10
11
  const {dirname, resolve, relative} = require('path')
11
12
  const {depth: dfwalk} = require('treeverse')
@@ -13,6 +14,7 @@ const fs = require('fs')
13
14
  const {promisify} = require('util')
14
15
  const symlink = promisify(fs.symlink)
15
16
  const mkdirp = require('mkdirp-infer-owner')
17
+ const justMkdirp = require('mkdirp')
16
18
  const moveFile = require('@npmcli/move-file')
17
19
  const rimraf = promisify(require('rimraf'))
18
20
  const packageContents = require('@npmcli/installed-package-contents')
@@ -25,6 +27,7 @@ const retirePath = require('../retire-path.js')
25
27
  const promiseAllRejectLate = require('promise-all-reject-late')
26
28
  const optionalSet = require('../optional-set.js')
27
29
  const updateRootPackageJson = require('../update-root-package-json.js')
30
+ const calcDepFlags = require('../calc-dep-flags.js')
28
31
 
29
32
  const _retiredPaths = Symbol('retiredPaths')
30
33
  const _retiredUnchanged = Symbol('retiredUnchanged')
@@ -35,6 +38,8 @@ const _retireShallowNodes = Symbol.for('retireShallowNodes')
35
38
  const _getBundlesByDepth = Symbol('getBundlesByDepth')
36
39
  const _registryResolved = Symbol('registryResolved')
37
40
  const _addNodeToTrashList = Symbol('addNodeToTrashList')
41
+ const _workspaces = Symbol.for('workspaces')
42
+
38
43
  // shared by rebuild mixin
39
44
  const _trashList = Symbol.for('trashList')
40
45
  const _handleOptionalFailure = Symbol.for('handleOptionalFailure')
@@ -81,7 +86,6 @@ const _global = Symbol.for('global')
81
86
 
82
87
  // defined by Ideal mixin
83
88
  const _pruneBundledMetadeps = Symbol.for('pruneBundledMetadeps')
84
- const _explicitRequests = Symbol.for('explicitRequests')
85
89
  const _resolvedAdd = Symbol.for('resolvedAdd')
86
90
  const _usePackageLock = Symbol.for('usePackageLock')
87
91
  const _formatPackageLock = Symbol.for('formatPackageLock')
@@ -145,7 +149,10 @@ module.exports = cls => class Reifier extends cls {
145
149
  if (this[_packageLockOnly] || this[_dryRun])
146
150
  return
147
151
 
148
- await mkdirp(resolve(this.path))
152
+ // we do NOT want to set ownership on this folder, especially
153
+ // recursively, because it can have other side effects to do that
154
+ // in a project directory. We just want to make it if it's missing.
155
+ await justMkdirp(resolve(this.path))
149
156
  }
150
157
 
151
158
  async [_reifyPackages] () {
@@ -236,9 +243,25 @@ module.exports = cls => class Reifier extends cls {
236
243
  const actualOpt = this[_global] ? {
237
244
  ignoreMissing: true,
238
245
  global: true,
239
- filter: (node, kid) =>
240
- this[_explicitRequests].size === 0 || !node.isProjectRoot ? true
241
- : (this.idealTree.edgesOut.has(kid) || this[_explicitRequests].has(kid)),
246
+ filter: (node, kid) => {
247
+ // if it's not the project root, and we have no explicit requests,
248
+ // then we're already into a nested dep, so we keep it
249
+ if (this.explicitRequests.size === 0 || !node.isProjectRoot)
250
+ return true
251
+
252
+ // if we added it as an edgeOut, then we want it
253
+ if (this.idealTree.edgesOut.has(kid))
254
+ return true
255
+
256
+ // if it's an explicit request, then we want it
257
+ const hasExplicit = [...this.explicitRequests]
258
+ .some(edge => edge.name === kid)
259
+ if (hasExplicit)
260
+ return true
261
+
262
+ // ignore the rest of the global install folder
263
+ return false
264
+ },
242
265
  } : { ignoreMissing: true }
243
266
 
244
267
  if (!this[_global]) {
@@ -265,9 +288,35 @@ module.exports = cls => class Reifier extends cls {
265
288
  // to just invalidate the parts that changed, but avoid walking the
266
289
  // whole tree again.
267
290
 
291
+ const filterNodes = []
292
+ if (this[_global] && this.explicitRequests.size) {
293
+ const idealTree = this.idealTree.target || this.idealTree
294
+ const actualTree = this.actualTree.target || this.actualTree
295
+ // we ONLY are allowed to make changes in the global top-level
296
+ // children where there's an explicit request.
297
+ for (const { name } of this.explicitRequests) {
298
+ const ideal = idealTree.children.get(name)
299
+ if (ideal)
300
+ filterNodes.push(ideal)
301
+ const actual = actualTree.children.get(name)
302
+ if (actual)
303
+ filterNodes.push(actual)
304
+ }
305
+ } else {
306
+ for (const ws of this[_workspaces]) {
307
+ const ideal = this.idealTree.children.get(ws)
308
+ if (ideal)
309
+ filterNodes.push(ideal)
310
+ const actual = this.actualTree.children.get(ws)
311
+ if (actual)
312
+ filterNodes.push(actual)
313
+ }
314
+ }
315
+
268
316
  // find all the nodes that need to change between the actual
269
317
  // and ideal trees.
270
318
  this.diff = Diff.calculate({
319
+ filterNodes,
271
320
  actual: this.actualTree,
272
321
  ideal: this.idealTree,
273
322
  })
@@ -881,11 +930,17 @@ module.exports = cls => class Reifier extends cls {
881
930
 
882
931
  process.emit('time', 'reify:save')
883
932
 
884
- if (this[_resolvedAdd]) {
933
+ // resolvedAdd is the list of user add requests, but with names added
934
+ // to things like git repos and tarball file/urls. However, if the
935
+ // user requested 'foo@', and we have a foo@file:../foo, then we should
936
+ // end up saving the spec we actually used, not whatever they gave us.
937
+ if (this[_resolvedAdd].length) {
885
938
  const root = this.idealTree
886
939
  const pkg = root.package
887
- for (const req of this[_resolvedAdd]) {
888
- const {name, rawSpec, subSpec} = req
940
+ for (const { name } of this[_resolvedAdd]) {
941
+ const req = npa.resolve(name, root.edgesOut.get(name).spec, root.realpath)
942
+ const {rawSpec, subSpec} = req
943
+
889
944
  const spec = subSpec ? subSpec.rawSpec : rawSpec
890
945
  const child = root.children.get(name)
891
946
 
@@ -910,6 +965,15 @@ module.exports = cls => class Reifier extends cls {
910
965
  const save = h.https && h.auth ? `git+${h.https(opt)}`
911
966
  : h.shortcut(opt)
912
967
  updateDepSpec(pkg, name, save)
968
+ } else if (req.type === 'directory' || req.type === 'file') {
969
+ // save the relative path in package.json
970
+ // Normally saveSpec is updated with the proper relative
971
+ // path already, but it's possible to specify a full absolute
972
+ // path initially, in which case we can end up with the wrong
973
+ // thing, so just get the ultimate fetchSpec and relativize it.
974
+ const p = req.fetchSpec.replace(/^file:/, '')
975
+ const rel = relpath(root.realpath, p)
976
+ updateDepSpec(pkg, name, `file:${rel}`)
913
977
  } else
914
978
  updateDepSpec(pkg, name, req.saveSpec)
915
979
  }
@@ -950,20 +1014,85 @@ module.exports = cls => class Reifier extends cls {
950
1014
  return meta.save(saveOpt)
951
1015
  }
952
1016
 
953
- [_copyIdealToActual] () {
1017
+ async [_copyIdealToActual] () {
1018
+ // clean up any trash that is still in the tree
1019
+ for (const path of this[_trashList]) {
1020
+ const loc = relpath(this.idealTree.realpath, path)
1021
+ const node = this.idealTree.inventory.get(loc)
1022
+ if (node && node.root === this.idealTree)
1023
+ node.parent = null
1024
+ }
1025
+
1026
+ // if we filtered to only certain nodes, then anything ELSE needs
1027
+ // to be untouched in the resulting actual tree, even if it differs
1028
+ // in the idealTree. Copy over anything that was in the actual and
1029
+ // was not changed, delete anything in the ideal and not actual.
1030
+ // Then we move the entire idealTree over to this.actualTree, and
1031
+ // save the hidden lockfile.
1032
+ if (this.diff && this.diff.filterSet.size) {
1033
+ const { filterSet } = this.diff
1034
+ const seen = new Set()
1035
+ for (const [loc, ideal] of this.idealTree.inventory.entries()) {
1036
+ if (seen.has(loc))
1037
+ continue
1038
+ seen.add(loc)
1039
+
1040
+ // if it's an ideal node from the filter set, then skip it
1041
+ // because we already made whatever changes were necessary
1042
+ if (filterSet.has(ideal))
1043
+ continue
1044
+
1045
+ // otherwise, if it's not in the actualTree, then it's not a thing
1046
+ // that we actually added. And if it IS in the actualTree, then
1047
+ // it's something that we left untouched, so we need to record
1048
+ // that.
1049
+ const actual = this.actualTree.inventory.get(loc)
1050
+ if (!actual)
1051
+ ideal.root = null
1052
+ else {
1053
+ if ([...actual.linksIn].some(link => filterSet.has(link))) {
1054
+ seen.add(actual.location)
1055
+ continue
1056
+ }
1057
+ const { realpath, isLink } = actual
1058
+ if (isLink && ideal.isLink && ideal.realpath === realpath)
1059
+ continue
1060
+ else
1061
+ actual.root = this.idealTree
1062
+ }
1063
+ }
1064
+
1065
+ // now find any actual nodes that may not be present in the ideal
1066
+ // tree, but were left behind by virtue of not being in the filter
1067
+ for (const [loc, actual] of this.actualTree.inventory.entries()) {
1068
+ if (seen.has(loc))
1069
+ continue
1070
+ seen.add(loc)
1071
+ if (filterSet.has(actual))
1072
+ continue
1073
+ actual.root = this.idealTree
1074
+ }
1075
+
1076
+ // prune out any tops that lack a linkIn
1077
+ for (const top of this.idealTree.tops) {
1078
+ if (top.linksIn.size === 0)
1079
+ top.root = null
1080
+ }
1081
+
1082
+ // need to calculate dep flags, since nodes may have been marked
1083
+ // as extraneous or otherwise incorrect during transit.
1084
+ calcDepFlags(this.idealTree)
1085
+ }
1086
+
954
1087
  // save the ideal's meta as a hidden lockfile after we actualize it
955
1088
  this.idealTree.meta.filename =
956
- this.path + '/node_modules/.package-lock.json'
1089
+ this.idealTree.realpath + '/node_modules/.package-lock.json'
957
1090
  this.idealTree.meta.hiddenLockfile = true
1091
+
958
1092
  this.actualTree = this.idealTree
959
1093
  this.idealTree = null
960
- for (const path of this[_trashList]) {
961
- const loc = relpath(this.path, path)
962
- const node = this.actualTree.inventory.get(loc)
963
- if (node && node.root === this.actualTree)
964
- node.parent = null
965
- }
966
1094
 
967
- return !this[_global] && this.actualTree.meta.save()
1095
+ if (!this[_global])
1096
+ await this.actualTree.meta.save()
968
1097
  }
969
1098
  }
package/lib/diff.js CHANGED
@@ -11,7 +11,8 @@ 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}) {
15
+ this.filterSet = filterSet
15
16
  this.children = []
16
17
  this.actual = actual
17
18
  this.ideal = ideal
@@ -29,9 +30,54 @@ class Diff {
29
30
  this.removed = []
30
31
  }
31
32
 
32
- static calculate ({actual, ideal}) {
33
+ static calculate ({actual, ideal, filterNodes = []}) {
34
+ // if there's a filterNode, then:
35
+ // - get the path from the root to the filterNode. The root or
36
+ // root.target should have an edge either to the filterNode or
37
+ // a link to the filterNode. If not, abort. Add the path to the
38
+ // filterSet.
39
+ // - Add set of Nodes depended on by the filterNode to filterSet.
40
+ // - Anything outside of that set should be ignored by getChildren
41
+ const filterSet = new Set()
42
+ for (const filterNode of filterNodes) {
43
+ const { root } = filterNode
44
+ if (root !== ideal && root !== actual)
45
+ throw new Error('invalid filterNode: outside idealTree/actualTree')
46
+ const { target } = root
47
+ const rootTarget = target || root
48
+ const edge = [...rootTarget.edgesOut.values()].filter(e => {
49
+ return e.to && (e.to === filterNode || e.to.target === filterNode)
50
+ })[0]
51
+ filterSet.add(root)
52
+ filterSet.add(rootTarget)
53
+ filterSet.add(ideal)
54
+ filterSet.add(actual)
55
+ if (edge && edge.to) {
56
+ filterSet.add(edge.to)
57
+ if (edge.to.target)
58
+ filterSet.add(edge.to.target)
59
+ }
60
+ filterSet.add(filterNode)
61
+
62
+ depth({
63
+ tree: filterNode,
64
+ visit: node => filterSet.add(node),
65
+ getChildren: node => {
66
+ node = node.target || node
67
+ const loc = node.location
68
+ const idealNode = ideal.inventory.get(loc)
69
+ const ideals = !idealNode ? []
70
+ : [...idealNode.edgesOut.values()].filter(e => e.to).map(e => e.to)
71
+ const actualNode = actual.inventory.get(loc)
72
+ const actuals = !actualNode ? []
73
+ : [...actualNode.edgesOut.values()].filter(e => e.to).map(e => e.to)
74
+ return ideals.concat(actuals)
75
+ },
76
+ })
77
+ }
78
+
33
79
  return depth({
34
- tree: new Diff({actual, ideal}),
80
+ tree: new Diff({actual, ideal, filterSet}),
35
81
  getChildren,
36
82
  leave,
37
83
  })
@@ -89,20 +135,20 @@ const allChildren = node => {
89
135
  // to create the diff tree
90
136
  const getChildren = diff => {
91
137
  const children = []
92
- const {unchanged, removed} = diff
138
+ const {actual, ideal, unchanged, removed, filterSet} = diff
93
139
 
94
140
  // Note: we DON'T diff fsChildren themselves, because they are either
95
141
  // included in the package contents, or part of some other project, and
96
142
  // will never appear in legacy shrinkwraps anyway. but we _do_ include the
97
143
  // child nodes of fsChildren, because those are nodes that we are typically
98
144
  // responsible for installing.
99
- const actualKids = allChildren(diff.actual)
100
- const idealKids = allChildren(diff.ideal)
145
+ const actualKids = allChildren(actual)
146
+ const idealKids = allChildren(ideal)
101
147
  const paths = new Set([...actualKids.keys(), ...idealKids.keys()])
102
148
  for (const path of paths) {
103
149
  const actual = actualKids.get(path)
104
150
  const ideal = idealKids.get(path)
105
- diffNode(actual, ideal, children, unchanged, removed)
151
+ diffNode(actual, ideal, children, unchanged, removed, filterSet)
106
152
  }
107
153
 
108
154
  if (diff.leaves && !children.length)
@@ -111,7 +157,10 @@ const getChildren = diff => {
111
157
  return children
112
158
  }
113
159
 
114
- const diffNode = (actual, ideal, children, unchanged, removed) => {
160
+ const diffNode = (actual, ideal, children, unchanged, removed, filterSet) => {
161
+ if (filterSet.size && !(filterSet.has(ideal) || filterSet.has(actual)))
162
+ return
163
+
115
164
  const action = getAction({actual, ideal})
116
165
 
117
166
  // if it's a match, then get its children
@@ -119,7 +168,7 @@ const diffNode = (actual, ideal, children, unchanged, removed) => {
119
168
  if (action) {
120
169
  if (action === 'REMOVE')
121
170
  removed.push(actual)
122
- children.push(new Diff({actual, ideal}))
171
+ children.push(new Diff({actual, ideal, filterSet}))
123
172
  } else {
124
173
  unchanged.push(ideal)
125
174
  // !*! Weird dirty hack warning !*!
@@ -150,7 +199,7 @@ const diffNode = (actual, ideal, children, unchanged, removed) => {
150
199
  for (const node of bundledChildren)
151
200
  node.parent = ideal
152
201
  }
153
- children.push(...getChildren({actual, ideal, unchanged, removed}))
202
+ children.push(...getChildren({actual, ideal, unchanged, removed, filterSet}))
154
203
  }
155
204
  }
156
205
 
package/lib/link.js CHANGED
@@ -23,13 +23,19 @@ class Link extends Node {
23
23
  : null),
24
24
  })
25
25
 
26
- this.target = target || new Node({
27
- ...options,
28
- path: realpath,
29
- parent: null,
30
- fsParent: null,
31
- root: this.root,
32
- })
26
+ if (target)
27
+ this.target = target
28
+ else if (this.realpath === this.root.path)
29
+ this.target = this.root
30
+ else {
31
+ this.target = new Node({
32
+ ...options,
33
+ path: realpath,
34
+ parent: null,
35
+ fsParent: null,
36
+ root: this.root,
37
+ })
38
+ }
33
39
  }
34
40
 
35
41
  get version () {
package/lib/node.js CHANGED
@@ -685,6 +685,7 @@ class Node {
685
685
  ...this.children.values(),
686
686
  ...this.inventory.values(),
687
687
  ].filter(n => n !== this))
688
+
688
689
  for (const child of family) {
689
690
  if (child.root !== root) {
690
691
  child[_delistFromMeta]()
@@ -704,12 +705,14 @@ class Node {
704
705
  }
705
706
 
706
707
  // if we had a target, and didn't find one in the new root, then bring
707
- // it over as well.
708
- if (this.isLink && target && !this.target)
708
+ // it over as well, but only if we're setting the link into a new root,
709
+ // as we don't want to lose the target any time we remove a link.
710
+ if (this.isLink && target && !this.target && root !== this)
709
711
  target.root = root
710
712
 
711
713
  // tree should always be valid upon root setter completion.
712
714
  treeCheck(this)
715
+ treeCheck(root)
713
716
  }
714
717
 
715
718
  get root () {
package/lib/printable.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // of the current node and its descendents
3
3
 
4
4
  const util = require('util')
5
+ const relpath = require('./relpath.js')
5
6
 
6
7
  class ArboristNode {
7
8
  constructor (tree, path) {
@@ -47,6 +48,11 @@ class ArboristNode {
47
48
  .map(edge => new EdgeIn(edge)))
48
49
  }
49
50
 
51
+ if (tree.workspaces && tree.workspaces.size) {
52
+ this.workspaces = new Map([...tree.workspaces.entries()]
53
+ .map(([name, path]) => [name, relpath(tree.root.realpath, path)]))
54
+ }
55
+
50
56
  // fsChildren sorted by path
51
57
  if (tree.fsChildren.size) {
52
58
  this.fsChildren = new Set([...tree.fsChildren]
@@ -63,6 +69,13 @@ class ArboristNode {
63
69
  }
64
70
  }
65
71
 
72
+ class ArboristVirtualNode extends ArboristNode {
73
+ constructor (tree, path) {
74
+ super(tree, path)
75
+ this.sourceReference = printableTree(tree.sourceReference, path)
76
+ }
77
+ }
78
+
66
79
  class ArboristLink extends ArboristNode {
67
80
  constructor (tree, path) {
68
81
  super(tree, path)
@@ -119,10 +132,17 @@ class EdgeIn extends Edge {
119
132
  }
120
133
 
121
134
  const printableTree = (tree, path = []) => {
122
- if (path.includes(tree))
123
- return { location: tree.location }
135
+ if (!tree)
136
+ return tree
137
+
138
+ const Cls = tree.isLink ? ArboristLink
139
+ : tree.sourceReference ? ArboristVirtualNode
140
+ : ArboristNode
141
+ if (path.includes(tree)) {
142
+ const obj = Object.create(Cls.prototype)
143
+ return Object.assign(obj, { location: tree.location })
144
+ }
124
145
  path.push(tree)
125
- const Cls = tree.isLink ? ArboristLink : ArboristNode
126
146
  return new Cls(tree, path)
127
147
  }
128
148
 
package/lib/shrinkwrap.js CHANGED
@@ -41,6 +41,7 @@ const readFile = promisify(fs.readFile)
41
41
  const writeFile = promisify(fs.writeFile)
42
42
  const stat = promisify(fs.stat)
43
43
  const readdir_ = promisify(fs.readdir)
44
+ const readlink = promisify(fs.readlink)
44
45
 
45
46
  // XXX remove when drop support for node v10
46
47
  const lstat = promisify(fs.lstat)
@@ -176,10 +177,19 @@ const assertNoNewer = async (path, data, lockTime, dir = path, seen = null) => {
176
177
  : readdir(parent, { withFileTypes: true })
177
178
 
178
179
  return children.catch(() => [])
179
- .then(ents => Promise.all(
180
- ents.filter(ent => ent.isDirectory() && !/^\./.test(ent.name))
181
- .map(ent => assertNoNewer(path, data, lockTime, resolve(parent, ent.name), seen))
182
- )).then(() => {
180
+ .then(ents => Promise.all(ents.map(async ent => {
181
+ const child = resolve(parent, ent.name)
182
+ if (ent.isDirectory() && !/^\./.test(ent.name))
183
+ await assertNoNewer(path, data, lockTime, child, seen)
184
+ else if (ent.isSymbolicLink()) {
185
+ const target = resolve(parent, await readlink(child))
186
+ const tstat = await stat(target).catch(() => null)
187
+ seen.add(relpath(path, child))
188
+ if (tstat && tstat.isDirectory() && !seen.has(relpath(path, target)))
189
+ await assertNoNewer(path, data, lockTime, target, seen)
190
+ }
191
+ })))
192
+ .then(() => {
183
193
  if (dir !== path)
184
194
  return
185
195
 
package/lib/tree-check.js CHANGED
@@ -1,6 +1,8 @@
1
1
  const debug = require('./debug.js')
2
2
 
3
3
  const checkTree = (tree, checkUnreachable = true) => {
4
+ const log = [['START TREE CHECK', tree.path]]
5
+
4
6
  // this can only happen in tests where we have a "tree" object
5
7
  // that isn't actually a tree.
6
8
  if (!tree.root || !tree.root.inventory)
@@ -9,8 +11,21 @@ const checkTree = (tree, checkUnreachable = true) => {
9
11
  const { inventory } = tree.root
10
12
  const seen = new Set()
11
13
  const check = (node, via = tree, viaType = 'self') => {
14
+ log.push([
15
+ 'CHECK',
16
+ node && node.location,
17
+ via && via.location,
18
+ viaType,
19
+ 'seen=' + seen.has(node),
20
+ 'promise=' + !!(node && node.then),
21
+ 'root=' + !!(node && node.isRoot),
22
+ ])
23
+
12
24
  if (!node || seen.has(node) || node.then)
13
25
  return
26
+
27
+ seen.add(node)
28
+
14
29
  if (node.isRoot && node !== tree.root) {
15
30
  throw Object.assign(new Error('double root'), {
16
31
  node: node.path,
@@ -19,6 +34,7 @@ const checkTree = (tree, checkUnreachable = true) => {
19
34
  root: tree.root.path,
20
35
  via: via.path,
21
36
  viaType,
37
+ log,
22
38
  })
23
39
  }
24
40
 
@@ -31,6 +47,7 @@ const checkTree = (tree, checkUnreachable = true) => {
31
47
  via: via.path,
32
48
  viaType,
33
49
  otherRoot: node.root && node.root.path,
50
+ log,
34
51
  })
35
52
  }
36
53
 
@@ -43,6 +60,7 @@ const checkTree = (tree, checkUnreachable = true) => {
43
60
  viaType,
44
61
  inventory: [...node.inventory.values()].map(node =>
45
62
  [node.path, node.location]),
63
+ log,
46
64
  })
47
65
  }
48
66
 
@@ -53,6 +71,7 @@ const checkTree = (tree, checkUnreachable = true) => {
53
71
  root: tree.root.path,
54
72
  via: via.path,
55
73
  viaType,
74
+ log,
56
75
  })
57
76
  }
58
77
 
@@ -65,14 +84,38 @@ const checkTree = (tree, checkUnreachable = true) => {
65
84
  via: via.path,
66
85
  viaType,
67
86
  devEdges: devEdges.map(e => [e.type, e.name, e.spec, e.error]),
87
+ log,
88
+ })
89
+ }
90
+
91
+ if (node.path === tree.root.path && node !== tree.root) {
92
+ throw Object.assign(new Error('node with same path as root'), {
93
+ node: node.path,
94
+ tree: tree.path,
95
+ root: tree.root.path,
96
+ via: via.path,
97
+ viaType,
98
+ log,
99
+ })
100
+ }
101
+
102
+ if (!node.isLink && node.path !== node.realpath) {
103
+ throw Object.assign(new Error('non-link with mismatched path/realpath'), {
104
+ node: node.path,
105
+ tree: tree.path,
106
+ realpath: node.realpath,
107
+ root: tree.root.path,
108
+ via: via.path,
109
+ viaType,
110
+ log,
68
111
  })
69
112
  }
70
113
 
71
114
  const { parent, fsParent, target } = node
72
- seen.add(node)
73
115
  check(parent, node, 'parent')
74
116
  check(fsParent, node, 'fsParent')
75
117
  check(target, node, 'target')
118
+ log.push(['CHILDREN', node.location, ...node.children.keys()])
76
119
  for (const kid of node.children.values())
77
120
  check(kid, node, 'children')
78
121
  for (const kid of node.fsChildren)
@@ -81,6 +124,7 @@ const checkTree = (tree, checkUnreachable = true) => {
81
124
  check(link, node, 'linksIn')
82
125
  for (const top of node.tops)
83
126
  check(top, node, 'tops')
127
+ log.push(['DONE', node.location])
84
128
  }
85
129
  check(tree)
86
130
  if (checkUnreachable) {
@@ -92,6 +136,7 @@ const checkTree = (tree, checkUnreachable = true) => {
92
136
  location: node.location,
93
137
  root: tree.root.path,
94
138
  tree: tree.path,
139
+ log,
95
140
  })
96
141
  }
97
142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "2.2.6",
3
+ "version": "2.3.0",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@npmcli/installed-package-contents": "^1.0.7",
@@ -14,7 +14,7 @@
14
14
  "cacache": "^15.0.3",
15
15
  "common-ancestor-path": "^1.0.1",
16
16
  "json-parse-even-better-errors": "^2.3.1",
17
- "json-stringify-nice": "^1.1.1",
17
+ "json-stringify-nice": "^1.1.2",
18
18
  "mkdirp-infer-owner": "^2.0.0",
19
19
  "npm-install-checks": "^4.0.0",
20
20
  "npm-package-arg": "^8.1.0",
@@ -26,7 +26,7 @@
26
26
  "promise-call-limit": "^1.0.1",
27
27
  "read-package-json-fast": "^2.0.2",
28
28
  "readdir-scoped-modules": "^1.1.0",
29
- "semver": "^7.3.4",
29
+ "semver": "^7.3.5",
30
30
  "tar": "^6.1.0",
31
31
  "treeverse": "^1.0.4",
32
32
  "walk-up-path": "^1.0.0"