@npmcli/arborist 4.0.5 → 4.1.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.
package/README.md CHANGED
@@ -4,8 +4,8 @@ Inspect and manage `node_modules` trees.
4
4
 
5
5
  ![a tree with the word ARBORIST superimposed on it](https://raw.githubusercontent.com/npm/arborist/main/docs/logo.svg?sanitize=true)
6
6
 
7
- There's more documentation [in the notes
8
- folder](https://github.com/npm/arborist/tree/main/notes).
7
+ There's more documentation [in the docs
8
+ folder](https://github.com/npm/arborist/tree/main/docs).
9
9
 
10
10
  ## USAGE
11
11
 
@@ -379,6 +379,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
379
379
  optional: false,
380
380
  global: this[_global],
381
381
  legacyPeerDeps: this.legacyPeerDeps,
382
+ loadOverrides: true,
382
383
  })
383
384
  if (root.isLink) {
384
385
  root.target = new Node({
@@ -676,6 +677,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
676
677
  // calls rather than walking over everything in the tree.
677
678
  const set = this.idealTree.inventory
678
679
  .filter(n => this[_shouldUpdateNode](n))
680
+ // XXX add any invalid edgesOut to the queue
679
681
  for (const node of set) {
680
682
  for (const edge of node.edgesIn) {
681
683
  this.addTracker('idealTree', edge.from.name, edge.from.location)
@@ -772,7 +774,10 @@ This is a one-time fix-up, please be patient...
772
774
  [_buildDeps] () {
773
775
  process.emit('time', 'idealTree:buildDeps')
774
776
  const tree = this.idealTree.target
777
+ tree.assertRootOverrides()
775
778
  this[_depsQueue].push(tree)
779
+ // XXX also push anything that depends on a node with a name
780
+ // in the override list
776
781
  this.log.silly('idealTree', 'buildDeps')
777
782
  this.addTracker('idealTree', tree.name, '')
778
783
  return this[_buildDepStep]()
@@ -1112,6 +1117,7 @@ This is a one-time fix-up, please be patient...
1112
1117
  path: node.realpath,
1113
1118
  sourceReference: node,
1114
1119
  legacyPeerDeps: this.legacyPeerDeps,
1120
+ overrides: node.overrides,
1115
1121
  })
1116
1122
 
1117
1123
  // also need to set up any targets from any link deps, so that
@@ -127,6 +127,7 @@ module.exports = cls => class ActualLoader extends cls {
127
127
  realpath: real,
128
128
  pkg: {},
129
129
  global,
130
+ loadOverrides: true,
130
131
  })
131
132
  return this[_loadActualActually]({ root, ignoreMissing, global })
132
133
  }
@@ -135,8 +136,11 @@ module.exports = cls => class ActualLoader extends cls {
135
136
  this[_actualTree] = await this[_loadFSNode]({
136
137
  path: this.path,
137
138
  real: await realpath(this.path, this[_rpcache], this[_stcache]),
139
+ loadOverrides: true,
138
140
  })
139
141
 
142
+ this[_actualTree].assertRootOverrides()
143
+
140
144
  // Note: hidden lockfile will be rejected if it's not the latest thing
141
145
  // in the folder, or if any of the entries in the hidden lockfile are
142
146
  // missing.
@@ -236,13 +240,26 @@ module.exports = cls => class ActualLoader extends cls {
236
240
  this[_actualTree] = root
237
241
  }
238
242
 
239
- [_loadFSNode] ({ path, parent, real, root }) {
243
+ [_loadFSNode] ({ path, parent, real, root, loadOverrides }) {
240
244
  if (!real) {
241
245
  return realpath(path, this[_rpcache], this[_stcache])
242
246
  .then(
243
- real => this[_loadFSNode]({ path, parent, real, root }),
247
+ real => this[_loadFSNode]({
248
+ path,
249
+ parent,
250
+ real,
251
+ root,
252
+ loadOverrides,
253
+ }),
244
254
  // if realpath fails, just provide a dummy error node
245
- error => new Node({ error, path, realpath: path, parent, root })
255
+ error => new Node({
256
+ error,
257
+ path,
258
+ realpath: path,
259
+ parent,
260
+ root,
261
+ loadOverrides,
262
+ })
246
263
  )
247
264
  }
248
265
 
@@ -271,6 +288,7 @@ module.exports = cls => class ActualLoader extends cls {
271
288
  error,
272
289
  parent,
273
290
  root,
291
+ loadOverrides,
274
292
  })
275
293
  })
276
294
  .then(node => {
@@ -72,6 +72,7 @@ module.exports = cls => class VirtualLoader extends cls {
72
72
  this[rootOptionProvided] = options.root
73
73
 
74
74
  await this[loadFromShrinkwrap](s, root)
75
+ root.assertRootOverrides()
75
76
  return treeCheck(this.virtualTree)
76
77
  }
77
78
 
package/lib/edge.js CHANGED
@@ -29,6 +29,7 @@ class ArboristEdge {}
29
29
  const printableEdge = (edge) => {
30
30
  const edgeFrom = edge.from && edge.from.location
31
31
  const edgeTo = edge.to && edge.to.location
32
+ const override = edge.overrides && edge.overrides.value
32
33
 
33
34
  return Object.assign(new ArboristEdge(), {
34
35
  name: edge.name,
@@ -38,12 +39,13 @@ const printableEdge = (edge) => {
38
39
  ...(edgeTo ? { to: edgeTo } : {}),
39
40
  ...(edge.error ? { error: edge.error } : {}),
40
41
  ...(edge.peerConflicted ? { peerConflicted: true } : {}),
42
+ ...(override ? { overridden: override } : {}),
41
43
  })
42
44
  }
43
45
 
44
46
  class Edge {
45
47
  constructor (options) {
46
- const { type, name, spec, accept, from } = options
48
+ const { type, name, spec, accept, from, overrides } = options
47
49
 
48
50
  if (typeof spec !== 'string') {
49
51
  throw new TypeError('must provide string spec')
@@ -55,6 +57,10 @@ class Edge {
55
57
 
56
58
  this[_spec] = spec
57
59
 
60
+ if (overrides !== undefined) {
61
+ this.overrides = overrides
62
+ }
63
+
58
64
  if (accept !== undefined) {
59
65
  if (typeof accept !== 'string') {
60
66
  throw new TypeError('accept field must be a string if provided')
@@ -82,8 +88,11 @@ class Edge {
82
88
  }
83
89
 
84
90
  satisfiedBy (node) {
85
- return node.name === this.name &&
86
- depValid(node, this.spec, this.accept, this.from)
91
+ if (node.name !== this.name) {
92
+ return false
93
+ }
94
+
95
+ return depValid(node, this.spec, this.accept, this.from)
87
96
  }
88
97
 
89
98
  explain (seen = []) {
@@ -101,6 +110,10 @@ class Edge {
101
110
  type: this.type,
102
111
  name: this.name,
103
112
  spec: this.spec,
113
+ ...(this.rawSpec !== this.spec ? {
114
+ rawSpec: this.rawSpec,
115
+ overridden: true,
116
+ } : {}),
104
117
  ...(bundled ? { bundled } : {}),
105
118
  ...(error ? { error } : {}),
106
119
  ...(from ? { from: from.explain(null, seen) } : {}),
@@ -143,7 +156,28 @@ class Edge {
143
156
  return this[_name]
144
157
  }
145
158
 
159
+ get rawSpec () {
160
+ return this[_spec]
161
+ }
162
+
146
163
  get spec () {
164
+ if (this.overrides && this.overrides.value && this.overrides.name === this.name) {
165
+ if (this.overrides.value.startsWith('$')) {
166
+ const ref = this.overrides.value.slice(1)
167
+ const pkg = this.from.root.package
168
+ const overrideSpec = (pkg.devDependencies && pkg.devDependencies[ref]) ||
169
+ (pkg.optionalDependencies && pkg.optionalDependencies[ref]) ||
170
+ (pkg.dependencies && pkg.dependencies[ref]) ||
171
+ (pkg.peerDependencies && pkg.peerDependencies[ref])
172
+
173
+ if (overrideSpec) {
174
+ return overrideSpec
175
+ }
176
+
177
+ throw new Error(`Unable to resolve reference ${this.overrides.value}`)
178
+ }
179
+ return this.overrides.value
180
+ }
147
181
  return this[_spec]
148
182
  }
149
183
 
@@ -213,6 +247,7 @@ class Edge {
213
247
  if (node.edgesOut.has(this.name)) {
214
248
  node.edgesOut.get(this.name).detach()
215
249
  }
250
+
216
251
  node.addEdgeOut(this)
217
252
  this.reload()
218
253
  }
package/lib/node.js CHANGED
@@ -32,6 +32,7 @@ const semver = require('semver')
32
32
  const nameFromFolder = require('@npmcli/name-from-folder')
33
33
  const Edge = require('./edge.js')
34
34
  const Inventory = require('./inventory.js')
35
+ const OverrideSet = require('./override-set.js')
35
36
  const { normalize } = require('read-package-json-fast')
36
37
  const { getPaths: getBinPaths } = require('bin-links')
37
38
  const npa = require('npm-package-arg')
@@ -88,6 +89,8 @@ class Node {
88
89
  legacyPeerDeps = false,
89
90
  linksIn,
90
91
  hasShrinkwrap,
92
+ overrides,
93
+ loadOverrides = false,
91
94
  extraneous = true,
92
95
  dev = true,
93
96
  optional = true,
@@ -190,6 +193,17 @@ class Node {
190
193
  // because this.package is read when adding to inventory
191
194
  this[_package] = pkg && typeof pkg === 'object' ? pkg : {}
192
195
 
196
+ if (overrides) {
197
+ this.overrides = overrides
198
+ } else if (loadOverrides) {
199
+ const overrides = this[_package].overrides || {}
200
+ if (Object.keys(overrides).length > 0) {
201
+ this.overrides = new OverrideSet({
202
+ overrides: this[_package].overrides,
203
+ })
204
+ }
205
+ }
206
+
193
207
  // only relevant for the root and top nodes
194
208
  this.meta = meta
195
209
 
@@ -963,6 +977,11 @@ class Node {
963
977
  return false
964
978
  }
965
979
 
980
+ // XXX need to check for two root nodes?
981
+ if (node.overrides !== this.overrides) {
982
+ return false
983
+ }
984
+
966
985
  ignorePeers = new Set(ignorePeers)
967
986
 
968
987
  // gather up all the deps of this node and that are only depended
@@ -1208,6 +1227,10 @@ class Node {
1208
1227
  this[_changePath](newPath)
1209
1228
  }
1210
1229
 
1230
+ if (parent.overrides) {
1231
+ this.overrides = parent.overrides.getNodeRule(this)
1232
+ }
1233
+
1211
1234
  // clobbers anything at that path, resets all appropriate references
1212
1235
  this.root = parent.root
1213
1236
  }
@@ -1279,11 +1302,33 @@ class Node {
1279
1302
  }
1280
1303
  }
1281
1304
 
1305
+ assertRootOverrides () {
1306
+ if (!this.isProjectRoot || !this.overrides) {
1307
+ return
1308
+ }
1309
+
1310
+ for (const edge of this.edgesOut.values()) {
1311
+ // if these differ an override has been applied, those are not allowed
1312
+ // for top level dependencies so throw an error
1313
+ if (edge.spec !== edge.rawSpec && !edge.spec.startsWith('$')) {
1314
+ throw Object.assign(new Error(`Override for ${edge.name}@${edge.rawSpec} conflicts with direct dependency`), { code: 'EOVERRIDE' })
1315
+ }
1316
+ }
1317
+ }
1318
+
1282
1319
  addEdgeOut (edge) {
1320
+ if (this.overrides) {
1321
+ edge.overrides = this.overrides.getEdgeRule(edge)
1322
+ }
1323
+
1283
1324
  this.edgesOut.set(edge.name, edge)
1284
1325
  }
1285
1326
 
1286
1327
  addEdgeIn (edge) {
1328
+ if (edge.overrides) {
1329
+ this.overrides = edge.overrides
1330
+ }
1331
+
1287
1332
  this.edgesIn.add(edge)
1288
1333
 
1289
1334
  // try to get metadata from the yarn.lock file
@@ -0,0 +1,123 @@
1
+ const npa = require('npm-package-arg')
2
+ const semver = require('semver')
3
+
4
+ class OverrideSet {
5
+ constructor ({ overrides, key, parent }) {
6
+ this.parent = parent
7
+ this.children = new Map()
8
+
9
+ if (typeof overrides === 'string') {
10
+ overrides = { '.': overrides }
11
+ }
12
+
13
+ // change a literal empty string to * so we can use truthiness checks on
14
+ // the value property later
15
+ if (overrides['.'] === '') {
16
+ overrides['.'] = '*'
17
+ }
18
+
19
+ if (parent) {
20
+ const spec = npa(key)
21
+ if (!spec.name) {
22
+ throw new Error(`Override without name: ${key}`)
23
+ }
24
+
25
+ this.name = spec.name
26
+ spec.name = ''
27
+ this.key = key
28
+ this.keySpec = spec.rawSpec === '' ? '' : spec.toString()
29
+ this.value = overrides['.'] || this.keySpec
30
+ }
31
+
32
+ for (const [key, childOverrides] of Object.entries(overrides)) {
33
+ if (key === '.') {
34
+ continue
35
+ }
36
+
37
+ const child = new OverrideSet({
38
+ parent: this,
39
+ key,
40
+ overrides: childOverrides,
41
+ })
42
+
43
+ this.children.set(child.key, child)
44
+ }
45
+ }
46
+
47
+ getEdgeRule (edge) {
48
+ for (const rule of this.ruleset.values()) {
49
+ if (rule.name !== edge.name) {
50
+ continue
51
+ }
52
+
53
+ if (rule.keySpec === '' ||
54
+ semver.intersects(edge.spec, rule.keySpec)) {
55
+ return rule
56
+ }
57
+ }
58
+
59
+ return this
60
+ }
61
+
62
+ getNodeRule (node) {
63
+ for (const rule of this.ruleset.values()) {
64
+ if (rule.name !== node.name) {
65
+ continue
66
+ }
67
+
68
+ if (rule.keySpec === '' ||
69
+ semver.satisfies(node.version, rule.keySpec) ||
70
+ semver.satisfies(node.version, rule.value)) {
71
+ return rule
72
+ }
73
+ }
74
+
75
+ return this
76
+ }
77
+
78
+ getMatchingRule (node) {
79
+ for (const rule of this.ruleset.values()) {
80
+ if (rule.name !== node.name) {
81
+ continue
82
+ }
83
+
84
+ if (rule.keySpec === '' ||
85
+ semver.satisfies(node.version, rule.keySpec) ||
86
+ semver.satisfies(node.version, rule.value)) {
87
+ return rule
88
+ }
89
+ }
90
+
91
+ return null
92
+ }
93
+
94
+ * ancestry () {
95
+ for (let ancestor = this; ancestor; ancestor = ancestor.parent) {
96
+ yield ancestor
97
+ }
98
+ }
99
+
100
+ get isRoot () {
101
+ return !this.parent
102
+ }
103
+
104
+ get ruleset () {
105
+ const ruleset = new Map()
106
+
107
+ for (const override of this.ancestry()) {
108
+ for (const kid of override.children.values()) {
109
+ if (!ruleset.has(kid.key)) {
110
+ ruleset.set(kid.key, kid)
111
+ }
112
+ }
113
+
114
+ if (!override.isRoot && !ruleset.has(override.key)) {
115
+ ruleset.set(override.key, override)
116
+ }
117
+ }
118
+
119
+ return ruleset
120
+ }
121
+ }
122
+
123
+ module.exports = OverrideSet
package/lib/place-dep.js CHANGED
@@ -295,6 +295,7 @@ class PlaceDep {
295
295
  integrity: dep.integrity,
296
296
  legacyPeerDeps: this.legacyPeerDeps,
297
297
  error: dep.errors[0],
298
+ ...(dep.overrides ? { overrides: dep.overrides } : {}),
298
299
  ...(dep.isLink ? { target: dep.target, realpath: dep.realpath } : {}),
299
300
  })
300
301
 
package/lib/printable.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // helper function to output a clearer visualization
2
2
  // of the current node and its descendents
3
-
4
3
  const localeCompare = require('@isaacs/string-locale-compare')('en')
5
4
  const util = require('util')
6
5
  const relpath = require('./relpath.js')
@@ -65,6 +64,11 @@ class ArboristNode {
65
64
  this.errors = tree.errors.map(treeError)
66
65
  }
67
66
 
67
+ if (tree.overrides) {
68
+ this.overrides = new Map([...tree.overrides.ruleset.values()]
69
+ .map((override) => [override.key, override.value]))
70
+ }
71
+
68
72
  // edgesOut sorted by name
69
73
  if (tree.edgesOut.size) {
70
74
  this.edgesOut = new Map([...tree.edgesOut.entries()]
@@ -126,7 +130,10 @@ class Edge {
126
130
  constructor (edge) {
127
131
  this.type = edge.type
128
132
  this.name = edge.name
129
- this.spec = edge.spec || '*'
133
+ this.spec = edge.rawSpec || '*'
134
+ if (edge.rawSpec !== edge.spec) {
135
+ this.override = edge.spec
136
+ }
130
137
  if (edge.error) {
131
138
  this.error = edge.error
132
139
  }
@@ -145,6 +152,8 @@ class EdgeOut extends Edge {
145
152
 
146
153
  [util.inspect.custom] () {
147
154
  return `{ ${this.type} ${this.name}@${this.spec}${
155
+ this.override ? ` overridden:${this.override}` : ''
156
+ }${
148
157
  this.to ? ' -> ' + this.to : ''
149
158
  }${
150
159
  this.error ? ' ' + this.error : ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "4.0.5",
3
+ "version": "4.1.0",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",
@@ -37,10 +37,11 @@
37
37
  "walk-up-path": "^1.0.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@npmcli/template-oss": "^2.3.0",
40
+ "@npmcli/template-oss": "^2.3.1",
41
41
  "benchmark": "^2.1.4",
42
42
  "chalk": "^4.1.0",
43
43
  "minify-registry-metadata": "^2.1.0",
44
+ "nock": "^13.2.0",
44
45
  "tap": "^15.1.2",
45
46
  "tcompare": "^5.0.6"
46
47
  },
@@ -93,7 +94,7 @@
93
94
  "engines": {
94
95
  "node": "^12.13.0 || ^14.15.0 || >=16"
95
96
  },
96
- "templateVersion": "2.3.0",
97
+ "templateVersion": "2.3.1",
97
98
  "eslintIgnore": [
98
99
  "test/fixtures/",
99
100
  "!test/fixtures/*.js"