@npmcli/arborist 5.6.2 → 6.0.0-pre.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.
@@ -15,34 +15,30 @@ const Node = require('../node.js')
15
15
  const Link = require('../link.js')
16
16
  const realpath = require('../realpath.js')
17
17
 
18
- const _loadFSNode = Symbol('loadFSNode')
19
- const _newNode = Symbol('newNode')
20
- const _newLink = Symbol('newLink')
21
- const _loadFSTree = Symbol('loadFSTree')
22
- const _loadFSChildren = Symbol('loadFSChildren')
23
- const _findMissingEdges = Symbol('findMissingEdges')
24
- const _findFSParents = Symbol('findFSParents')
25
- const _resetDepFlags = Symbol('resetDepFlags')
26
-
27
- const _actualTreeLoaded = Symbol('actualTreeLoaded')
18
+ // public symbols
19
+ const _changePath = Symbol.for('_changePath')
20
+ const _global = Symbol.for('global')
21
+ const _loadWorkspaces = Symbol.for('loadWorkspaces')
28
22
  const _rpcache = Symbol.for('realpathCache')
29
23
  const _stcache = Symbol.for('statCache')
30
- const _topNodes = Symbol('linkTargets')
24
+
25
+ // private symbols
26
+ const _actualTree = Symbol('actualTree')
27
+ const _actualTreeLoaded = Symbol('actualTreeLoaded')
28
+ const _actualTreePromise = Symbol('actualTreePromise')
31
29
  const _cache = Symbol('nodeLoadingCache')
30
+ const _filter = Symbol('filter')
31
+ const _findMissingEdges = Symbol('findMissingEdges')
32
32
  const _loadActual = Symbol('loadActual')
33
- const _loadActualVirtually = Symbol('loadActualVirtually')
34
- const _loadActualActually = Symbol('loadActualActually')
35
- const _loadWorkspaces = Symbol.for('loadWorkspaces')
36
- const _loadWorkspaceTargets = Symbol('loadWorkspaceTargets')
37
- const _actualTreePromise = Symbol('actualTreePromise')
38
- const _actualTree = Symbol('actualTree')
33
+ const _loadFSChildren = Symbol('loadFSChildren')
34
+ const _loadFSNode = Symbol('loadFSNode')
35
+ const _loadFSTree = Symbol('loadFSTree')
36
+ const _newLink = Symbol('newLink')
37
+ const _newNode = Symbol('newNode')
38
+ const _topNodes = Symbol('linkTargets')
39
39
  const _transplant = Symbol('transplant')
40
40
  const _transplantFilter = Symbol('transplantFilter')
41
41
 
42
- const _filter = Symbol('filter')
43
- const _global = Symbol.for('global')
44
- const _changePath = Symbol.for('_changePath')
45
-
46
42
  module.exports = cls => class ActualLoader extends cls {
47
43
  constructor (options) {
48
44
  super(options)
@@ -75,37 +71,44 @@ module.exports = cls => class ActualLoader extends cls {
75
71
  this[_topNodes] = new Set()
76
72
  }
77
73
 
78
- [_resetDepFlags] (tree, root) {
79
- // reset all deps to extraneous prior to recalc
80
- if (!root) {
81
- for (const node of tree.inventory.values()) {
82
- node.extraneous = true
83
- }
84
- }
85
-
86
- // only reset root flags if we're not re-rooting,
87
- // otherwise leave as-is
88
- calcDepFlags(tree, !root)
89
- return tree
90
- }
91
-
92
74
  // public method
93
75
  async loadActual (options = {}) {
94
- // allow the user to set options on the ctor as well.
95
- // XXX: deprecate separate method options objects.
96
- options = { ...this.options, ...options }
97
-
98
- // stash the promise so that we don't ever have more than one
99
- // going at the same time. This is so that buildIdealTree can
100
- // default to the actualTree if no shrinkwrap present, but
101
- // reify() can still call buildIdealTree and loadActual in parallel
102
- // safely.
103
- return this.actualTree ? this.actualTree
104
- : this[_actualTreePromise] ? this[_actualTreePromise]
105
- : this[_actualTreePromise] = this[_loadActual](options)
106
- .then(tree => this[_resetDepFlags](tree, options.root))
107
- .then(tree => this.actualTree = treeCheck(tree))
76
+ // In the past this.actualTree was set as a promise that eventually
77
+ // resolved, and overwrite this.actualTree with the resolved value. This
78
+ // was a problem because virtually no other code expects this.actualTree to
79
+ // be a promise. Instead we only set it once resolved, and also return it
80
+ // from the promise so that it is what's returned from this function when
81
+ // awaited.
82
+ if (this.actualTree) {
83
+ return this.actualTree
84
+ }
85
+ if (!this[_actualTreePromise]) {
86
+ // allow the user to set options on the ctor as well.
87
+ // XXX: deprecate separate method options objects.
88
+ options = { ...this.options, ...options }
89
+
90
+ this[_actualTreePromise] = this[_loadActual](options)
91
+ .then(tree => {
92
+ // reset all deps to extraneous prior to recalc
93
+ if (!options.root) {
94
+ for (const node of tree.inventory.values()) {
95
+ node.extraneous = true
96
+ }
97
+ }
98
+
99
+ // only reset root flags if we're not re-rooting,
100
+ // otherwise leave as-is
101
+ calcDepFlags(tree, !options.root)
102
+ this.actualTree = treeCheck(tree)
103
+ return this.actualTree
104
+ })
105
+ }
106
+ return this[_actualTreePromise]
108
107
  }
108
+ // return the promise so that we don't ever have more than one going at the
109
+ // same time. This is so that buildIdealTree can default to the actualTree
110
+ // if no shrinkwrap present, but reify() can still call buildIdealTree and
111
+ // loadActual in parallel safely.
109
112
 
110
113
  async [_loadActual] (options) {
111
114
  // mostly realpath to throw if the root doesn't exist
@@ -122,75 +125,102 @@ module.exports = cls => class ActualLoader extends cls {
122
125
 
123
126
  if (global) {
124
127
  const real = await realpath(this.path, this[_rpcache], this[_stcache])
125
- const newNodeOrLink = this.path === real ? _newNode : _newLink
126
- this[_actualTree] = await this[newNodeOrLink]({
128
+ const params = {
127
129
  path: this.path,
128
130
  realpath: real,
129
131
  pkg: {},
130
132
  global,
131
133
  loadOverrides: true,
134
+ }
135
+ if (this.path === real) {
136
+ this[_actualTree] = this[_newNode](params)
137
+ } else {
138
+ this[_actualTree] = await this[_newLink](params)
139
+ }
140
+ } else {
141
+ // not in global mode, hidden lockfile is allowed, load root pkg too
142
+ this[_actualTree] = await this[_loadFSNode]({
143
+ path: this.path,
144
+ real: await realpath(this.path, this[_rpcache], this[_stcache]),
145
+ loadOverrides: true,
132
146
  })
133
- return this[_loadActualActually]({ root, ignoreMissing, global })
134
- }
135
147
 
136
- // not in global mode, hidden lockfile is allowed, load root pkg too
137
- this[_actualTree] = await this[_loadFSNode]({
138
- path: this.path,
139
- real: await realpath(this.path, this[_rpcache], this[_stcache]),
140
- loadOverrides: true,
141
- })
148
+ this[_actualTree].assertRootOverrides()
149
+
150
+ // if forceActual is set, don't even try the hidden lockfile
151
+ if (!forceActual) {
152
+ // Note: hidden lockfile will be rejected if it's not the latest thing
153
+ // in the folder, or if any of the entries in the hidden lockfile are
154
+ // missing.
155
+ const meta = await Shrinkwrap.load({
156
+ path: this[_actualTree].path,
157
+ hiddenLockfile: true,
158
+ resolveOptions: this.options,
159
+ })
160
+
161
+ if (meta.loadedFromDisk) {
162
+ this[_actualTree].meta = meta
163
+ // have to load on a new Arborist object, so we don't assign
164
+ // the virtualTree on this one! Also, the weird reference is because
165
+ // we can't easily get a ref to Arborist in this module, without
166
+ // creating a circular reference, since this class is a mixin used
167
+ // to build up the Arborist class itself.
168
+ await new this.constructor({ ...this.options }).loadVirtual({
169
+ root: this[_actualTree],
170
+ })
171
+ await this[_loadWorkspaces](this[_actualTree])
142
172
 
143
- this[_actualTree].assertRootOverrides()
173
+ this[_transplant](root)
174
+ return this[_actualTree]
175
+ }
176
+ }
144
177
 
145
- // if forceActual is set, don't even try the hidden lockfile
146
- if (!forceActual) {
147
- // Note: hidden lockfile will be rejected if it's not the latest thing
148
- // in the folder, or if any of the entries in the hidden lockfile are
149
- // missing.
150
178
  const meta = await Shrinkwrap.load({
151
179
  path: this[_actualTree].path,
152
- hiddenLockfile: true,
180
+ lockfileVersion: this.options.lockfileVersion,
153
181
  resolveOptions: this.options,
154
182
  })
155
-
156
- if (meta.loadedFromDisk) {
157
- this[_actualTree].meta = meta
158
- return this[_loadActualVirtually]({ root })
159
- }
183
+ this[_actualTree].meta = meta
160
184
  }
161
185
 
162
- const meta = await Shrinkwrap.load({
163
- path: this[_actualTree].path,
164
- lockfileVersion: this.options.lockfileVersion,
165
- resolveOptions: this.options,
166
- })
167
- this[_actualTree].meta = meta
168
- return this[_loadActualActually]({ root, ignoreMissing })
169
- }
170
-
171
- async [_loadActualVirtually] ({ root }) {
172
- // have to load on a new Arborist object, so we don't assign
173
- // the virtualTree on this one! Also, the weird reference is because
174
- // we can't easily get a ref to Arborist in this module, without
175
- // creating a circular reference, since this class is a mixin used
176
- // to build up the Arborist class itself.
177
- await new this.constructor({ ...this.options }).loadVirtual({
178
- root: this[_actualTree],
179
- })
186
+ await this[_loadFSTree](this[_actualTree])
180
187
  await this[_loadWorkspaces](this[_actualTree])
181
188
 
182
- this[_transplant](root)
183
- return this[_actualTree]
184
- }
189
+ // if there are workspace targets without Link nodes created, load
190
+ // the targets, so that we know what they are.
191
+ if (this[_actualTree].workspaces && this[_actualTree].workspaces.size) {
192
+ const promises = []
193
+ for (const path of this[_actualTree].workspaces.values()) {
194
+ if (!this[_cache].has(path)) {
195
+ // workspace overrides use the root overrides
196
+ const p = this[_loadFSNode]({ path, root: this[_actualTree], useRootOverrides: true })
197
+ .then(node => this[_loadFSTree](node))
198
+ promises.push(p)
199
+ }
200
+ }
201
+ await Promise.all(promises)
202
+ }
185
203
 
186
- async [_loadActualActually] ({ root, ignoreMissing, global }) {
187
- await this[_loadFSTree](this[_actualTree])
188
- await this[_loadWorkspaces](this[_actualTree])
189
- await this[_loadWorkspaceTargets](this[_actualTree])
190
204
  if (!ignoreMissing) {
191
205
  await this[_findMissingEdges]()
192
206
  }
193
- this[_findFSParents]()
207
+
208
+ // try to find a node that is the parent in a fs tree sense, but not a
209
+ // node_modules tree sense, of any link targets. this allows us to
210
+ // resolve deps that node will find, but a legacy npm view of the
211
+ // world would not have noticed.
212
+ for (const path of this[_topNodes]) {
213
+ const node = this[_cache].get(path)
214
+ if (node && !node.parent && !node.fsParent) {
215
+ for (const p of walkUp(dirname(path))) {
216
+ if (this[_cache].has(p)) {
217
+ node.fsParent = this[_cache].get(p)
218
+ break
219
+ }
220
+ }
221
+ }
222
+ }
223
+
194
224
  this[_transplant](root)
195
225
 
196
226
  if (global) {
@@ -209,25 +239,6 @@ module.exports = cls => class ActualLoader extends cls {
209
239
  return this[_actualTree]
210
240
  }
211
241
 
212
- // if there are workspace targets without Link nodes created, load
213
- // the targets, so that we know what they are.
214
- async [_loadWorkspaceTargets] (tree) {
215
- if (!tree.workspaces || !tree.workspaces.size) {
216
- return
217
- }
218
-
219
- const promises = []
220
- for (const path of tree.workspaces.values()) {
221
- if (!this[_cache].has(path)) {
222
- // workspace overrides use the root overrides
223
- const p = this[_loadFSNode]({ path, root: this[_actualTree], useRootOverrides: true })
224
- .then(node => this[_loadFSTree](node))
225
- promises.push(p)
226
- }
227
- }
228
- await Promise.all(promises)
229
- }
230
-
231
242
  [_transplant] (root) {
232
243
  if (!root || root === this[_actualTree]) {
233
244
  return
@@ -248,80 +259,63 @@ module.exports = cls => class ActualLoader extends cls {
248
259
  this[_actualTree] = root
249
260
  }
250
261
 
251
- [_loadFSNode] ({ path, parent, real, root, loadOverrides, useRootOverrides }) {
262
+ async [_loadFSNode] ({ path, parent, real, root, loadOverrides, useRootOverrides }) {
252
263
  if (!real) {
253
- return realpath(path, this[_rpcache], this[_stcache])
254
- .then(
255
- real => this[_loadFSNode]({
256
- path,
257
- parent,
258
- real,
259
- root,
260
- loadOverrides,
261
- useRootOverrides,
262
- }),
263
- // if realpath fails, just provide a dummy error node
264
- error => new Node({
265
- error,
266
- path,
267
- realpath: path,
268
- parent,
269
- root,
270
- loadOverrides,
271
- })
272
- )
273
- }
274
-
275
- // cache temporarily holds a promise placeholder so we don't try to create
276
- // the same node multiple times. this is rare to encounter, given the
277
- // aggressive caching on realpath and lstat calls, but it's possible that
278
- // it's already loaded as a tree top, and then gets its parent loaded
279
- // later, if a symlink points deeper in the tree.
280
- const cached = this[_cache].get(path)
281
- if (cached && !cached.dummy) {
282
- return Promise.resolve(cached).then(node => {
283
- node.parent = parent
284
- return node
285
- })
286
- }
287
-
288
- const p = rpj(join(real, 'package.json'))
289
- // soldier on if read-package-json raises an error
290
- .then(pkg => [pkg, null], error => [null, error])
291
- .then(([pkg, error]) => {
292
- return this[normalize(path) === real ? _newNode : _newLink]({
293
- installLinks: this.installLinks,
294
- legacyPeerDeps: this.legacyPeerDeps,
295
- path,
296
- realpath: real,
297
- pkg,
264
+ try {
265
+ real = await realpath(path, this[_rpcache], this[_stcache])
266
+ } catch (error) {
267
+ // if realpath fails, just provide a dummy error node
268
+ return new Node({
298
269
  error,
270
+ path,
271
+ realpath: path,
299
272
  parent,
300
273
  root,
301
274
  loadOverrides,
302
- ...(useRootOverrides && root.overrides
303
- ? { overrides: root.overrides.getNodeRule({ name: pkg.name, version: pkg.version }) }
304
- : {}),
305
275
  })
306
- })
307
- .then(node => {
308
- this[_cache].set(path, node)
309
- return node
310
- })
276
+ }
277
+ }
278
+
279
+ const cached = this[_cache].get(path)
280
+ let node
281
+ // missing edges get a dummy node, assign the parent and return it
282
+ if (cached && !cached.dummy) {
283
+ cached.parent = parent
284
+ return cached
285
+ } else {
286
+ const params = {
287
+ installLinks: this.installLinks,
288
+ legacyPeerDeps: this.legacyPeerDeps,
289
+ path,
290
+ realpath: real,
291
+ parent,
292
+ root,
293
+ loadOverrides,
294
+ }
295
+
296
+ try {
297
+ const pkg = await rpj(join(real, 'package.json'))
298
+ params.pkg = pkg
299
+ if (useRootOverrides && root.overrides) {
300
+ params.overrides = root.overrides.getNodeRule({ name: pkg.name, version: pkg.version })
301
+ }
302
+ } catch (err) {
303
+ params.error = err
304
+ }
311
305
 
312
- this[_cache].set(path, p)
313
- return p
306
+ // soldier on if read-package-json raises an error, passing it to the
307
+ // Node which will attach it to its errors array (Link passes it along to
308
+ // its target node)
309
+ if (normalize(path) === real) {
310
+ node = this[_newNode](params)
311
+ } else {
312
+ node = await this[_newLink](params)
313
+ }
314
+ }
315
+ this[_cache].set(path, node)
316
+ return node
314
317
  }
315
318
 
316
- // this is the way it is to expose a timing issue which is difficult to
317
- // test otherwise. The creation of a Node may take slightly longer than
318
- // the creation of a Link that targets it. If the Node has _begun_ its
319
- // creation phase (and put a Promise in the cache) then the Link will
320
- // get a Promise as its cachedTarget instead of an actual Node object.
321
- // This is not a problem, because it gets resolved prior to returning
322
- // the tree or attempting to load children. However, it IS remarkably
323
- // difficult to get to happen in a test environment to verify reliably.
324
- // Hence this kludge.
325
319
  [_newNode] (options) {
326
320
  // check it for an fsParent if it's a tree top. there's a decent chance
327
321
  // it'll get parented later, making the fsParent scan a no-op, but better
@@ -330,70 +324,56 @@ module.exports = cls => class ActualLoader extends cls {
330
324
  if (!parent) {
331
325
  this[_topNodes].add(realpath)
332
326
  }
333
- return process.env._TEST_ARBORIST_SLOW_LINK_TARGET_ === '1'
334
- ? new Promise(res => setTimeout(() => res(new Node(options)), 100))
335
- : new Node(options)
327
+ return new Node(options)
336
328
  }
337
329
 
338
- [_newLink] (options) {
330
+ async [_newLink] (options) {
339
331
  const { realpath } = options
340
332
  this[_topNodes].add(realpath)
341
333
  const target = this[_cache].get(realpath)
342
334
  const link = new Link({ ...options, target })
343
335
 
344
336
  if (!target) {
337
+ // Link set its target itself in this case
345
338
  this[_cache].set(realpath, link.target)
346
339
  // if a link target points at a node outside of the root tree's
347
340
  // node_modules hierarchy, then load that node as well.
348
- return this[_loadFSTree](link.target).then(() => link)
349
- } else if (target.then) {
350
- // eslint-disable-next-line promise/catch-or-return
351
- target.then(node => link.target = node)
341
+ await this[_loadFSTree](link.target)
352
342
  }
353
343
 
354
344
  return link
355
345
  }
356
346
 
357
- [_loadFSTree] (node) {
347
+ async [_loadFSTree] (node) {
358
348
  const did = this[_actualTreeLoaded]
359
- node = node.target
360
-
361
- // if a Link target has started, but not completed, then
362
- // a Promise will be in the cache to indicate this.
363
- if (node.then) {
364
- return node.then(node => this[_loadFSTree](node))
365
- }
366
-
367
- // impossible except in pathological ELOOP cases
368
- /* istanbul ignore if */
369
- if (did.has(node.realpath)) {
370
- return Promise.resolve(node)
371
- }
372
-
373
- did.add(node.realpath)
374
- return this[_loadFSChildren](node)
375
- .then(() => Promise.all(
376
- [...node.children.entries()]
349
+ if (!did.has(node.target.realpath)) {
350
+ did.add(node.target.realpath)
351
+ await this[_loadFSChildren](node.target)
352
+ return Promise.all(
353
+ [...node.target.children.entries()]
377
354
  .filter(([name, kid]) => !did.has(kid.realpath))
378
- .map(([name, kid]) => this[_loadFSTree](kid))))
355
+ .map(([name, kid]) => this[_loadFSTree](kid))
356
+ )
357
+ }
379
358
  }
380
359
 
381
360
  // create child nodes for all the entries in node_modules
382
361
  // and attach them to the node as a parent
383
- [_loadFSChildren] (node) {
362
+ async [_loadFSChildren] (node) {
384
363
  const nm = resolve(node.realpath, 'node_modules')
385
- return readdir(nm).then(kids => {
364
+ try {
365
+ const kids = await readdir(nm)
386
366
  return Promise.all(
387
- // ignore . dirs and retired scoped package folders
367
+ // ignore . dirs and retired scoped package folders
388
368
  kids.filter(kid => !/^(@[^/]+\/)?\./.test(kid))
389
369
  .filter(kid => this[_filter](node, kid))
390
370
  .map(kid => this[_loadFSNode]({
391
371
  parent: node,
392
372
  path: resolve(nm, kid),
393
373
  })))
394
- },
395
- // error in the readdir is not fatal, just means no kids
396
- () => {})
374
+ } catch {
375
+ // error in the readdir is not fatal, just means no kids
376
+ }
397
377
  }
398
378
 
399
379
  async [_findMissingEdges] () {
@@ -440,6 +420,7 @@ module.exports = cls => class ActualLoader extends cls {
440
420
 
441
421
  const d = this[_cache].has(p) ? await this[_cache].get(p)
442
422
  : new Node({ path: p, root: node.root, dummy: true })
423
+ // not a promise
443
424
  this[_cache].set(p, d)
444
425
  if (d.dummy) {
445
426
  // it's a placeholder, so likely would not have loaded this dep,
@@ -460,22 +441,4 @@ module.exports = cls => class ActualLoader extends cls {
460
441
  await Promise.all(depPromises)
461
442
  }
462
443
  }
463
-
464
- // try to find a node that is the parent in a fs tree sense, but not a
465
- // node_modules tree sense, of any link targets. this allows us to
466
- // resolve deps that node will find, but a legacy npm view of the
467
- // world would not have noticed.
468
- [_findFSParents] () {
469
- for (const path of this[_topNodes]) {
470
- const node = this[_cache].get(path)
471
- if (node && !node.parent && !node.fsParent) {
472
- for (const p of walkUp(dirname(path))) {
473
- if (this[_cache].has(p)) {
474
- node.fsParent = this[_cache].get(p)
475
- break
476
- }
477
- }
478
- }
479
- }
480
- }
481
444
  }
package/lib/dep-valid.js CHANGED
@@ -109,8 +109,8 @@ const depValid = (child, requested, requestor) => {
109
109
  const linkValid = (child, requested, requestor) => {
110
110
  const isLink = !!child.isLink
111
111
  // if we're installing links and the node is a link, then it's invalid because we want
112
- // a real node to be there
113
- if (requestor.installLinks) {
112
+ // a real node to be there. Except for workspaces. They are always links.
113
+ if (requestor.installLinks && !child.isWorkspace) {
114
114
  return !isLink
115
115
  }
116
116
 
package/lib/link.js CHANGED
@@ -1,4 +1,3 @@
1
- const debug = require('./debug.js')
2
1
  const relpath = require('./relpath.js')
3
2
  const Node = require('./node.js')
4
3
  const _loadDeps = Symbol.for('Arborist.Node._loadDeps')
@@ -53,27 +52,6 @@ class Link extends Node {
53
52
  return
54
53
  }
55
54
 
56
- if (current && current.then) {
57
- debug(() => {
58
- throw Object.assign(new Error('cannot set target while awaiting'), {
59
- path: this.path,
60
- realpath: this.realpath,
61
- })
62
- })
63
- }
64
-
65
- if (target && target.then) {
66
- // can set to a promise during an async tree build operation
67
- // wait until then to assign it.
68
- this[_target] = target
69
- // eslint-disable-next-line promise/always-return, promise/catch-or-return
70
- target.then(node => {
71
- this[_target] = null
72
- this.target = node
73
- })
74
- return
75
- }
76
-
77
55
  if (!target) {
78
56
  if (current && current.linksIn) {
79
57
  current.linksIn.delete(this)
@@ -3,8 +3,9 @@
3
3
  const { resolve } = require('path')
4
4
  const { parser, arrayDelimiter } = require('@npmcli/query')
5
5
  const localeCompare = require('@isaacs/string-locale-compare')('en')
6
- const npa = require('npm-package-arg')
6
+ const log = require('proc-log')
7
7
  const minimatch = require('minimatch')
8
+ const npa = require('npm-package-arg')
8
9
  const semver = require('semver')
9
10
 
10
11
  // handle results for parsed query asts, results are stored in a map that has a
@@ -291,11 +292,115 @@ class Results {
291
292
  }
292
293
 
293
294
  semverPseudo () {
294
- if (!this.currentAstNode.semverValue) {
295
+ const {
296
+ attributeMatcher,
297
+ lookupProperties,
298
+ semverFunc = 'infer',
299
+ semverValue,
300
+ } = this.currentAstNode
301
+ const { qualifiedAttribute } = attributeMatcher
302
+
303
+ if (!semverValue) {
304
+ // DEPRECATED: remove this warning and throw an error as part of @npmcli/arborist@6
305
+ log.warn('query', 'usage of :semver() with no parameters is deprecated')
295
306
  return this.initialItems
296
307
  }
297
- return this.initialItems.filter(node =>
298
- semver.satisfies(node.version, this.currentAstNode.semverValue))
308
+
309
+ if (!semver.valid(semverValue) && !semver.validRange(semverValue)) {
310
+ throw Object.assign(
311
+ new Error(`\`${semverValue}\` is not a valid semver version or range`),
312
+ { code: 'EQUERYINVALIDSEMVER' })
313
+ }
314
+
315
+ const valueIsVersion = !!semver.valid(semverValue)
316
+
317
+ const nodeMatches = (node, obj) => {
318
+ // if we already have an operator, the user provided some test as part of the selector
319
+ // we evaluate that first because if it fails we don't want this node anyway
320
+ if (attributeMatcher.operator) {
321
+ if (!attributeMatch(attributeMatcher, obj)) {
322
+ // if the initial operator doesn't match, we're done
323
+ return false
324
+ }
325
+ }
326
+
327
+ const attrValue = obj[qualifiedAttribute]
328
+ // both valid and validRange return null for undefined, so this will skip both nodes that
329
+ // do not have the attribute defined as well as those where the attribute value is invalid
330
+ // and those where the value from the package.json is not a string
331
+ if ((!semver.valid(attrValue) && !semver.validRange(attrValue)) ||
332
+ typeof attrValue !== 'string') {
333
+ return false
334
+ }
335
+
336
+ const attrIsVersion = !!semver.valid(attrValue)
337
+
338
+ let actualFunc = semverFunc
339
+
340
+ // if we're asked to infer, we examine outputs to make a best guess
341
+ if (actualFunc === 'infer') {
342
+ if (valueIsVersion && attrIsVersion) {
343
+ // two versions -> semver.eq
344
+ actualFunc = 'eq'
345
+ } else if (!valueIsVersion && !attrIsVersion) {
346
+ // two ranges -> semver.intersects
347
+ actualFunc = 'intersects'
348
+ } else {
349
+ // anything else -> semver.satisfies
350
+ actualFunc = 'satisfies'
351
+ }
352
+ }
353
+
354
+ if (['eq', 'neq', 'gt', 'gte', 'lt', 'lte'].includes(actualFunc)) {
355
+ // both sides must be versions, but one is not
356
+ if (!valueIsVersion || !attrIsVersion) {
357
+ return false
358
+ }
359
+
360
+ return semver[actualFunc](attrValue, semverValue)
361
+ } else if (['gtr', 'ltr', 'satisfies'].includes(actualFunc)) {
362
+ // at least one side must be a version, but neither is
363
+ if (!valueIsVersion && !attrIsVersion) {
364
+ return false
365
+ }
366
+
367
+ return valueIsVersion
368
+ ? semver[actualFunc](semverValue, attrValue)
369
+ : semver[actualFunc](attrValue, semverValue)
370
+ } else if (['intersects', 'subset'].includes(actualFunc)) {
371
+ // these accept two ranges and since a version is also a range, anything goes
372
+ return semver[actualFunc](attrValue, semverValue)
373
+ } else {
374
+ // user provided a function we don't know about, throw an error
375
+ throw Object.assign(new Error(`\`semver.${actualFunc}\` is not a supported operator.`),
376
+ { code: 'EQUERYINVALIDOPERATOR' })
377
+ }
378
+ }
379
+
380
+ return this.initialItems.filter((node) => {
381
+ // no lookupProperties just means its a top level property, see if it matches
382
+ if (!lookupProperties.length) {
383
+ return nodeMatches(node, node.package)
384
+ }
385
+
386
+ // this code is mostly duplicated from attrPseudo to traverse into the package until we get
387
+ // to our deepest requested object
388
+ let objs = [node.package]
389
+ for (const prop of lookupProperties) {
390
+ if (prop === arrayDelimiter) {
391
+ objs = objs.flat()
392
+ continue
393
+ }
394
+
395
+ objs = objs.flatMap(obj => obj[prop] || [])
396
+ const noAttr = objs.every(obj => !obj)
397
+ if (noAttr) {
398
+ return false
399
+ }
400
+
401
+ return objs.some(obj => nodeMatches(node, obj))
402
+ }
403
+ })
299
404
  }
300
405
 
301
406
  typePseudo () {
@@ -358,6 +463,7 @@ const attributeOperator = ({ attr, value, insensitive, operator }) => {
358
463
  if (insensitive) {
359
464
  attr = attr.toLowerCase()
360
465
  }
466
+
361
467
  return attributeOperators[operator]({
362
468
  attr,
363
469
  insensitive,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "5.6.2",
3
+ "version": "6.0.0-pre.1",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "@npmcli/eslint-config": "^3.1.0",
45
- "@npmcli/template-oss": "4.1.1",
45
+ "@npmcli/template-oss": "4.1.2",
46
46
  "benchmark": "^2.1.4",
47
47
  "chalk": "^4.1.0",
48
48
  "minify-registry-metadata": "^2.1.0",
@@ -96,10 +96,10 @@
96
96
  "timeout": "360"
97
97
  },
98
98
  "engines": {
99
- "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
99
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
100
100
  },
101
101
  "templateOSS": {
102
102
  "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
103
- "version": "4.1.1"
103
+ "version": "4.1.2"
104
104
  }
105
105
  }