@npmcli/arborist 9.0.0 → 9.0.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.
@@ -447,7 +447,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
447
447
  .catch(/* istanbul ignore next */ () => null)
448
448
  if (st && st.isSymbolicLink()) {
449
449
  const target = await readlink(dir)
450
- const real = resolve(dirname(dir), target).replace(/#/g, '%23')
450
+ const real = resolve(dirname(dir), target)
451
451
  tree.package.dependencies[name] = `file:${real}`
452
452
  } else {
453
453
  tree.package.dependencies[name] = '*'
@@ -522,12 +522,12 @@ module.exports = cls => class IdealTreeBuilder extends cls {
522
522
 
523
523
  const { name } = spec
524
524
  if (spec.type === 'file') {
525
- spec = npa(`file:${relpath(path, spec.fetchSpec).replace(/#/g, '%23')}`, path)
525
+ spec = npa(`file:${relpath(path, spec.fetchSpec)}`, path)
526
526
  spec.name = name
527
527
  } else if (spec.type === 'directory') {
528
528
  try {
529
529
  const real = await realpath(spec.fetchSpec, this[_rpcache], this[_stcache])
530
- spec = npa(`file:${relpath(path, real).replace(/#/g, '%23')}`, path)
530
+ spec = npa(`file:${relpath(path, real)}`, path)
531
531
  spec.name = name
532
532
  } catch {
533
533
  // TODO: create synthetic test case to simulate realpath failure
@@ -216,7 +216,7 @@ module.exports = cls => class ActualLoader extends cls {
216
216
  const actualRoot = tree.isLink ? tree.target : tree
217
217
  const { dependencies = {} } = actualRoot.package
218
218
  for (const [name, kid] of actualRoot.children.entries()) {
219
- const def = kid.isLink ? `file:${kid.realpath.replace(/#/g, '%23')}` : '*'
219
+ const def = kid.isLink ? `file:${kid.realpath}` : '*'
220
220
  dependencies[name] = dependencies[name] || def
221
221
  }
222
222
  actualRoot.package = { ...actualRoot.package, dependencies }
@@ -149,7 +149,7 @@ module.exports = cls => class VirtualLoader extends cls {
149
149
  })
150
150
 
151
151
  for (const [name, path] of workspaces.entries()) {
152
- lockWS[name] = `file:${path.replace(/#/g, '%23')}`
152
+ lockWS[name] = `file:${path}`
153
153
  }
154
154
 
155
155
  // Should rootNames exclude optional?
@@ -1364,7 +1364,7 @@ module.exports = cls => class Reifier extends cls {
1364
1364
  // path initially, in which case we can end up with the wrong
1365
1365
  // thing, so just get the ultimate fetchSpec and relativize it.
1366
1366
  const p = req.fetchSpec.replace(/^file:/, '')
1367
- const rel = relpath(addTree.realpath, p).replace(/#/g, '%23')
1367
+ const rel = relpath(addTree.realpath, p)
1368
1368
  newSpec = `file:${rel}`
1369
1369
  }
1370
1370
  } else {
@@ -20,11 +20,10 @@ const consistentResolve = (resolved, fromPath, toPath, relPaths = false) => {
20
20
  raw,
21
21
  } = npa(resolved, fromPath)
22
22
  if (type === 'file' || type === 'directory') {
23
- const cleanFetchSpec = fetchSpec.replace(/#/g, '%23')
24
23
  if (relPaths && toPath) {
25
- return `file:${relpath(toPath, cleanFetchSpec)}`
24
+ return `file:${relpath(toPath, fetchSpec)}`
26
25
  }
27
- return `file:${cleanFetchSpec}`
26
+ return `file:${fetchSpec}`
28
27
  }
29
28
  if (hosted) {
30
29
  return `git+${hosted.auth ? hosted.https(hostedOpt) : hosted.sshurl(hostedOpt)}`
package/lib/dep-valid.js CHANGED
@@ -101,7 +101,7 @@ const depValid = (child, requested, requestor) => {
101
101
  })
102
102
  }
103
103
 
104
- default: // unpossible, just being cautious
104
+ default: // impossible, just being cautious
105
105
  break
106
106
  }
107
107
 
package/lib/edge.js CHANGED
@@ -4,6 +4,7 @@
4
4
  const util = require('node:util')
5
5
  const npa = require('npm-package-arg')
6
6
  const depValid = require('./dep-valid.js')
7
+ const OverrideSet = require('./override-set.js')
7
8
 
8
9
  class ArboristEdge {
9
10
  constructor (edge) {
@@ -103,7 +104,7 @@ class Edge {
103
104
  }
104
105
 
105
106
  satisfiedBy (node) {
106
- if (node.name !== this.#name) {
107
+ if (node.name !== this.#name || !this.#from) {
107
108
  return false
108
109
  }
109
110
 
@@ -112,7 +113,31 @@ class Edge {
112
113
  if (node.hasShrinkwrap || node.inShrinkwrap || node.inBundle) {
113
114
  return depValid(node, this.rawSpec, this.#accept, this.#from)
114
115
  }
115
- return depValid(node, this.spec, this.#accept, this.#from)
116
+
117
+ // If there's no override we just use the spec.
118
+ if (!this.overrides?.keySpec) {
119
+ return depValid(node, this.spec, this.#accept, this.#from)
120
+ }
121
+ // There's some override. If the target node satisfies the overriding spec
122
+ // then it's okay.
123
+ if (depValid(node, this.spec, this.#accept, this.#from)) {
124
+ return true
125
+ }
126
+ // If it doesn't, then it should at least satisfy the original spec.
127
+ if (!depValid(node, this.rawSpec, this.#accept, this.#from)) {
128
+ return false
129
+ }
130
+ // It satisfies the original spec, not the overriding spec. We need to make
131
+ // sure it doesn't use the overridden spec.
132
+ // For example:
133
+ // we might have an ^8.0.0 rawSpec, and an override that makes
134
+ // keySpec=8.23.0 and the override value spec=9.0.0.
135
+ // If the node is 9.0.0, then it's okay because it's consistent with spec.
136
+ // If the node is 8.24.0, then it's okay because it's consistent with the rawSpec.
137
+ // If the node is 8.23.0, then it's not okay because even though it's consistent
138
+ // with the rawSpec, it's also consistent with the keySpec.
139
+ // So we're looking for ^8.0.0 or 9.0.0 and not 8.23.0.
140
+ return !depValid(node, this.overrides.keySpec, this.#accept, this.#from)
116
141
  }
117
142
 
118
143
  // return the edge data, and an explanation of how that edge came to be here
@@ -181,11 +206,9 @@ class Edge {
181
206
  if (this.overrides?.value && this.overrides.value !== '*' && this.overrides.name === this.#name) {
182
207
  if (this.overrides.value.startsWith('$')) {
183
208
  const ref = this.overrides.value.slice(1)
184
- // we may be a virtual root, if we are we want to resolve reference overrides
185
- // from the real root, not the virtual one
186
- const pkg = this.#from.sourceReference
187
- ? this.#from.sourceReference.root.package
188
- : this.#from.root.package
209
+ const pkg = this.#from?.sourceReference
210
+ ? this.#from?.sourceReference.root.package
211
+ : this.#from?.root?.package
189
212
  if (pkg.devDependencies?.[ref]) {
190
213
  return pkg.devDependencies[ref]
191
214
  }
@@ -234,10 +257,15 @@ class Edge {
234
257
  } else {
235
258
  this.#error = 'MISSING'
236
259
  }
237
- } else if (this.peer && this.#from === this.#to.parent && !this.#from.isTop) {
260
+ } else if (this.peer && this.#from === this.#to.parent && !this.#from?.isTop) {
238
261
  this.#error = 'PEER LOCAL'
239
262
  } else if (!this.satisfiedBy(this.#to)) {
240
263
  this.#error = 'INVALID'
264
+ } else if (this.overrides && this.#to.edgesOut.size && OverrideSet.doOverrideSetsConflict(this.overrides, this.#to.overrides)) {
265
+ // Any inconsistency between the edge's override set and the target's override set is potentially problematic.
266
+ // But we only say the edge is in error if the override sets are plainly conflicting.
267
+ // Note that if the target doesn't have any dependencies of their own, then this inconsistency is irrelevant.
268
+ this.#error = 'INVALID'
241
269
  } else {
242
270
  this.#error = 'OK'
243
271
  }
@@ -250,15 +278,26 @@ class Edge {
250
278
 
251
279
  reload (hard = false) {
252
280
  this.#explanation = null
253
- if (this.#from.overrides) {
254
- this.overrides = this.#from.overrides.getEdgeRule(this)
281
+
282
+ let needToUpdateOverrideSet = false
283
+ let newOverrideSet
284
+ let oldOverrideSet
285
+ if (this.#from?.overrides) {
286
+ newOverrideSet = this.#from.overrides.getEdgeRule(this)
287
+ if (newOverrideSet && !newOverrideSet.isEqual(this.overrides)) {
288
+ // If there's a new different override set we need to propagate it to the nodes.
289
+ // If we're deleting the override set then there's no point propagating it right now since it will be filled with another value later.
290
+ needToUpdateOverrideSet = true
291
+ oldOverrideSet = this.overrides
292
+ this.overrides = newOverrideSet
293
+ }
255
294
  } else {
256
295
  delete this.overrides
257
296
  }
258
- const newTo = this.#from.resolve(this.#name)
297
+ const newTo = this.#from?.resolve(this.#name)
259
298
  if (newTo !== this.#to) {
260
299
  if (this.#to) {
261
- this.#to.edgesIn.delete(this)
300
+ this.#to.deleteEdgeIn(this)
262
301
  }
263
302
  this.#to = newTo
264
303
  this.#error = null
@@ -267,15 +306,19 @@ class Edge {
267
306
  }
268
307
  } else if (hard) {
269
308
  this.#error = null
309
+ } else if (needToUpdateOverrideSet && this.#to) {
310
+ // Propagate the new override set to the target node.
311
+ this.#to.updateOverridesEdgeInRemoved(oldOverrideSet)
312
+ this.#to.updateOverridesEdgeInAdded(newOverrideSet)
270
313
  }
271
314
  }
272
315
 
273
316
  detach () {
274
317
  this.#explanation = null
275
318
  if (this.#to) {
276
- this.#to.edgesIn.delete(this)
319
+ this.#to.deleteEdgeIn(this)
277
320
  }
278
- this.#from.edgesOut.delete(this.#name)
321
+ this.#from?.edgesOut.delete(this.#name)
279
322
  this.#to = null
280
323
  this.#error = 'DETACHED'
281
324
  this.#from = null
package/lib/link.js CHANGED
@@ -99,7 +99,7 @@ class Link extends Node {
99
99
  // the path/realpath guard is there for the benefit of setting
100
100
  // these things in the "wrong" order
101
101
  return this.path && this.realpath
102
- ? `file:${relpath(dirname(this.path), this.realpath).replace(/#/g, '%23')}`
102
+ ? `file:${relpath(dirname(this.path), this.realpath)}`
103
103
  : null
104
104
  }
105
105
 
package/lib/node.js CHANGED
@@ -40,6 +40,7 @@ const debug = require('./debug.js')
40
40
  const gatherDepSet = require('./gather-dep-set.js')
41
41
  const treeCheck = require('./tree-check.js')
42
42
  const { walkUp } = require('walk-up-path')
43
+ const { log } = require('proc-log')
43
44
 
44
45
  const { resolve, relative, dirname, basename } = require('node:path')
45
46
  const util = require('node:util')
@@ -344,7 +345,28 @@ class Node {
344
345
  }
345
346
 
346
347
  get overridden () {
347
- return !!(this.overrides && this.overrides.value && this.overrides.name === this.name)
348
+ if (!this.overrides) {
349
+ return false
350
+ }
351
+ if (!this.overrides.value) {
352
+ return false
353
+ }
354
+ if (this.overrides.name !== this.name) {
355
+ return false
356
+ }
357
+
358
+ // The overrides rule is for a package with this name, but some override rules only apply to specific
359
+ // versions. To make sure this package was actually overridden, we check whether any edge going in
360
+ // had the rule applied to it, in which case its overrides set is different than its source node.
361
+ for (const edge of this.edgesIn) {
362
+ if (edge.overrides && edge.overrides.name === this.name && edge.overrides.value === this.version) {
363
+ if (!edge.overrides.isEqual(edge.from.overrides)) {
364
+ return true
365
+ }
366
+ }
367
+ }
368
+
369
+ return false
348
370
  }
349
371
 
350
372
  get package () {
@@ -822,9 +844,6 @@ class Node {
822
844
  target.root = root
823
845
  }
824
846
 
825
- if (!this.overrides && this.parent && this.parent.overrides) {
826
- this.overrides = this.parent.overrides.getNodeRule(this)
827
- }
828
847
  // tree should always be valid upon root setter completion.
829
848
  treeCheck(this)
830
849
  if (this !== root) {
@@ -842,7 +861,7 @@ class Node {
842
861
  }
843
862
 
844
863
  for (const [name, path] of this.#workspaces.entries()) {
845
- new Edge({ from: this, name, spec: `file:${path.replace(/#/g, '%23')}`, type: 'workspace' })
864
+ new Edge({ from: this, name, spec: `file:${path}`, type: 'workspace' })
846
865
  }
847
866
  }
848
867
 
@@ -1006,10 +1025,21 @@ class Node {
1006
1025
  return false
1007
1026
  }
1008
1027
 
1009
- // XXX need to check for two root nodes?
1010
- if (node.overrides !== this.overrides) {
1011
- return false
1028
+ // If this node has no dependencies, then it's irrelevant to check the override
1029
+ // rules of the replacement node.
1030
+ if (this.edgesOut.size) {
1031
+ // XXX need to check for two root nodes?
1032
+ if (node.overrides) {
1033
+ if (!node.overrides.isEqual(this.overrides)) {
1034
+ return false
1035
+ }
1036
+ } else {
1037
+ if (this.overrides) {
1038
+ return false
1039
+ }
1040
+ }
1012
1041
  }
1042
+
1013
1043
  ignorePeers = new Set(ignorePeers)
1014
1044
 
1015
1045
  // gather up all the deps of this node and that are only depended
@@ -1077,8 +1107,13 @@ class Node {
1077
1107
  return false
1078
1108
  }
1079
1109
 
1080
- // if we prefer dedupe, or if the version is greater/equal, take the other
1081
- if (preferDedupe || semver.gte(other.version, this.version)) {
1110
+ // if we prefer dedupe, or if the version is equal, take the other
1111
+ if (preferDedupe || semver.eq(other.version, this.version)) {
1112
+ return true
1113
+ }
1114
+
1115
+ // if our current version isn't the result of an override, then prefer to take the greater version
1116
+ if (!this.overridden && semver.gt(other.version, this.version)) {
1082
1117
  return true
1083
1118
  }
1084
1119
 
@@ -1249,10 +1284,6 @@ class Node {
1249
1284
  this[_changePath](newPath)
1250
1285
  }
1251
1286
 
1252
- if (parent.overrides) {
1253
- this.overrides = parent.overrides.getNodeRule(this)
1254
- }
1255
-
1256
1287
  // clobbers anything at that path, resets all appropriate references
1257
1288
  this.root = parent.root
1258
1289
  }
@@ -1346,9 +1377,87 @@ class Node {
1346
1377
  this.edgesOut.set(edge.name, edge)
1347
1378
  }
1348
1379
 
1349
- addEdgeIn (edge) {
1380
+ recalculateOutEdgesOverrides () {
1381
+ // For each edge out propogate the new overrides through.
1382
+ for (const edge of this.edgesOut.values()) {
1383
+ edge.reload(true)
1384
+ if (edge.to) {
1385
+ edge.to.updateOverridesEdgeInAdded(edge.overrides)
1386
+ }
1387
+ }
1388
+ }
1389
+
1390
+ updateOverridesEdgeInRemoved (otherOverrideSet) {
1391
+ // If this edge's overrides isn't equal to this node's overrides, then removing it won't change newOverrideSet later.
1392
+ if (!this.overrides || !this.overrides.isEqual(otherOverrideSet)) {
1393
+ return false
1394
+ }
1395
+ let newOverrideSet
1396
+ for (const edge of this.edgesIn) {
1397
+ if (newOverrideSet && edge.overrides) {
1398
+ newOverrideSet = OverrideSet.findSpecificOverrideSet(edge.overrides, newOverrideSet)
1399
+ } else {
1400
+ newOverrideSet = edge.overrides
1401
+ }
1402
+ }
1403
+ if (this.overrides.isEqual(newOverrideSet)) {
1404
+ return false
1405
+ }
1406
+ this.overrides = newOverrideSet
1407
+ if (this.overrides) {
1408
+ // Optimization: if there's any override set at all, then no non-extraneous node has an empty override set. So if we temporarily have no
1409
+ // override set (for example, we removed all the edges in), there's no use updating all the edges out right now. Let's just wait until
1410
+ // we have an actual override set later.
1411
+ this.recalculateOutEdgesOverrides()
1412
+ }
1413
+ return true
1414
+ }
1415
+
1416
+ // This logic isn't perfect either. When we have two edges in that have different override sets, then we have to decide which set is correct.
1417
+ // This function assumes the more specific override set is applicable, so if we have dependencies A->B->C and A->C
1418
+ // and an override set that specifies what happens for C under A->B, this will work even if the new A->C edge comes along and tries to change
1419
+ // the override set.
1420
+ // The strictly correct logic is not to allow two edges with different overrides to point to the same node, because even if this node can satisfy
1421
+ // both, one of its dependencies might need to be different depending on the edge leading to it.
1422
+ // However, this might cause a lot of duplication, because the conflict in the dependencies might never actually happen.
1423
+ updateOverridesEdgeInAdded (otherOverrideSet) {
1424
+ if (!otherOverrideSet) {
1425
+ // Assuming there are any overrides at all, the overrides field is never undefined for any node at the end state of the tree.
1426
+ // So if the new edge's overrides is undefined it will be updated later. So we can wait with updating the node's overrides field.
1427
+ return false
1428
+ }
1429
+ if (!this.overrides) {
1430
+ this.overrides = otherOverrideSet
1431
+ this.recalculateOutEdgesOverrides()
1432
+ return true
1433
+ }
1434
+ if (this.overrides.isEqual(otherOverrideSet)) {
1435
+ return false
1436
+ }
1437
+ const newOverrideSet = OverrideSet.findSpecificOverrideSet(this.overrides, otherOverrideSet)
1438
+ if (newOverrideSet) {
1439
+ if (!this.overrides.isEqual(newOverrideSet)) {
1440
+ this.overrides = newOverrideSet
1441
+ this.recalculateOutEdgesOverrides()
1442
+ return true
1443
+ }
1444
+ return false
1445
+ }
1446
+ // This is an error condition. We can only get here if the new override set is in conflict with the existing.
1447
+ log.silly('Conflicting override sets', this.name)
1448
+ }
1449
+
1450
+ deleteEdgeIn (edge) {
1451
+ this.edgesIn.delete(edge)
1350
1452
  if (edge.overrides) {
1351
- this.overrides = edge.overrides
1453
+ this.updateOverridesEdgeInRemoved(edge.overrides)
1454
+ }
1455
+ }
1456
+
1457
+ addEdgeIn (edge) {
1458
+ // We need to handle the case where the new edge in has an overrides field which is different from the current value.
1459
+ if (!this.overrides || !this.overrides.isEqual(edge.overrides)) {
1460
+ this.updateOverridesEdgeInAdded(edge.overrides)
1352
1461
  }
1353
1462
 
1354
1463
  this.edgesIn.add(edge)
@@ -1,5 +1,6 @@
1
1
  const npa = require('npm-package-arg')
2
2
  const semver = require('semver')
3
+ const { log } = require('proc-log')
3
4
 
4
5
  class OverrideSet {
5
6
  constructor ({ overrides, key, parent }) {
@@ -44,6 +45,43 @@ class OverrideSet {
44
45
  }
45
46
  }
46
47
 
48
+ childrenAreEqual (other) {
49
+ if (this.children.size !== other.children.size) {
50
+ return false
51
+ }
52
+ for (const [key] of this.children) {
53
+ if (!other.children.has(key)) {
54
+ return false
55
+ }
56
+ if (this.children.get(key).value !== other.children.get(key).value) {
57
+ return false
58
+ }
59
+ if (!this.children.get(key).childrenAreEqual(other.children.get(key))) {
60
+ return false
61
+ }
62
+ }
63
+ return true
64
+ }
65
+
66
+ isEqual (other) {
67
+ if (this === other) {
68
+ return true
69
+ }
70
+ if (!other) {
71
+ return false
72
+ }
73
+ if (this.key !== other.key || this.value !== other.value) {
74
+ return false
75
+ }
76
+ if (!this.childrenAreEqual(other)) {
77
+ return false
78
+ }
79
+ if (!this.parent) {
80
+ return !other.parent
81
+ }
82
+ return this.parent.isEqual(other.parent)
83
+ }
84
+
47
85
  getEdgeRule (edge) {
48
86
  for (const rule of this.ruleset.values()) {
49
87
  if (rule.name !== edge.name) {
@@ -55,7 +93,9 @@ class OverrideSet {
55
93
  return rule
56
94
  }
57
95
 
58
- let spec = npa(`${edge.name}@${edge.spec}`)
96
+ // We need to use the rawSpec here, because the spec has the overrides applied to it already.
97
+ // rawSpec can be undefined, so we need to use the fallback value of spec if it is.
98
+ let spec = npa(`${edge.name}@${edge.rawSpec || edge.spec}`)
59
99
  if (spec.type === 'alias') {
60
100
  spec = spec.subSpec
61
101
  }
@@ -142,6 +182,28 @@ class OverrideSet {
142
182
 
143
183
  return ruleset
144
184
  }
185
+
186
+ static findSpecificOverrideSet (first, second) {
187
+ for (let overrideSet = second; overrideSet; overrideSet = overrideSet.parent) {
188
+ if (overrideSet.isEqual(first)) {
189
+ return second
190
+ }
191
+ }
192
+ for (let overrideSet = first; overrideSet; overrideSet = overrideSet.parent) {
193
+ if (overrideSet.isEqual(second)) {
194
+ return first
195
+ }
196
+ }
197
+
198
+ // The override sets are incomparable. Neither one contains the other.
199
+ log.silly('Conflicting override sets', first, second)
200
+ }
201
+
202
+ static doOverrideSetsConflict (first, second) {
203
+ // If override sets contain one another then we can try to use the more specific one.
204
+ // If neither one is more specific, then we consider them to be in conflict.
205
+ return (this.findSpecificOverrideSet(first, second) === undefined)
206
+ }
145
207
  }
146
208
 
147
209
  module.exports = OverrideSet
package/lib/shrinkwrap.js CHANGED
@@ -817,7 +817,7 @@ class Shrinkwrap {
817
817
  if (!/^file:/.test(resolved)) {
818
818
  pathFixed = resolved
819
819
  } else {
820
- pathFixed = `file:${resolve(this.path, resolved.slice(5)).replace(/#/g, '%23')}`
820
+ pathFixed = `file:${resolve(this.path, resolved.slice(5))}`
821
821
  }
822
822
  }
823
823
 
@@ -1011,7 +1011,7 @@ class Shrinkwrap {
1011
1011
  }
1012
1012
 
1013
1013
  if (node.isLink) {
1014
- lock.version = `file:${relpath(this.path, node.realpath).replace(/#/g, '%23')}`
1014
+ lock.version = `file:${relpath(this.path, node.realpath)}`
1015
1015
  } else if (spec && (spec.type === 'file' || spec.type === 'remote')) {
1016
1016
  lock.version = spec.saveSpec
1017
1017
  } else if (spec && spec.type === 'git' || rSpec.type === 'git') {
@@ -1089,7 +1089,7 @@ class Shrinkwrap {
1089
1089
  // this especially shows up with workspace edges when the root
1090
1090
  // node is also a workspace in the set.
1091
1091
  const p = resolve(node.realpath, spec.slice('file:'.length))
1092
- set[k] = `file:${relpath(node.realpath, p).replace(/#/g, '%23')}`
1092
+ set[k] = `file:${relpath(node.realpath, p)}`
1093
1093
  } else {
1094
1094
  set[k] = spec
1095
1095
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "9.0.0",
3
+ "version": "9.0.1",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",