@npmcli/arborist 6.0.0-pre.1 → 6.0.0-pre.2

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/lib/node.js CHANGED
@@ -103,6 +103,9 @@ class Node {
103
103
  dummy = false,
104
104
  sourceReference = null,
105
105
  } = options
106
+ // this object gives querySelectorAll somewhere to stash context about a node
107
+ // while processing a query
108
+ this.queryContext = {}
106
109
 
107
110
  // true if part of a global install
108
111
  this[_global] = global
@@ -1455,8 +1458,8 @@ class Node {
1455
1458
 
1456
1459
  // maybe accept both string value or array of strings
1457
1460
  // seems to be what dom API does
1458
- querySelectorAll (query) {
1459
- return querySelectorAll(this, query)
1461
+ querySelectorAll (query, opts) {
1462
+ return querySelectorAll(this, query, opts)
1460
1463
  }
1461
1464
 
1462
1465
  toJSON () {
@@ -6,6 +6,7 @@ const localeCompare = require('@isaacs/string-locale-compare')('en')
6
6
  const log = require('proc-log')
7
7
  const minimatch = require('minimatch')
8
8
  const npa = require('npm-package-arg')
9
+ const pacote = require('pacote')
9
10
  const semver = require('semver')
10
11
 
11
12
  // handle results for parsed query asts, results are stored in a map that has a
@@ -16,6 +17,7 @@ class Results {
16
17
  #currentAstSelector
17
18
  #initialItems
18
19
  #inventory
20
+ #outdatedCache = new Map()
19
21
  #pendingCombinator
20
22
  #results = new Map()
21
23
  #targetNode
@@ -28,6 +30,9 @@ class Results {
28
30
 
29
31
  this.currentResults = this.#initialItems
30
32
 
33
+ // We get this when first called and need to pass it to pacote
34
+ this.flatOptions = opts.flatOptions || {}
35
+
31
36
  // reset by rootAstNode walker
32
37
  this.currentAstNode = opts.rootAstNode
33
38
  }
@@ -58,6 +63,7 @@ class Results {
58
63
  if (firstParsed) {
59
64
  return this.#initialItems
60
65
  }
66
+
61
67
  if (this.currentAstNode.prev().type === 'combinator') {
62
68
  return this.#inventory
63
69
  }
@@ -125,7 +131,7 @@ class Results {
125
131
  }
126
132
 
127
133
  // pseudo selectors (prefixed with :)
128
- pseudoType () {
134
+ async pseudoType () {
129
135
  const pseudoFn = `${this.currentAstNode.value.slice(1)}Pseudo`
130
136
  if (!this[pseudoFn]) {
131
137
  throw Object.assign(
@@ -134,7 +140,7 @@ class Results {
134
140
  { code: 'EQUERYNOPSEUDO' }
135
141
  )
136
142
  }
137
- const nextResults = this[pseudoFn]()
143
+ const nextResults = await this[pseudoFn]()
138
144
  this.processPendingCombinator(nextResults)
139
145
  }
140
146
 
@@ -195,11 +201,12 @@ class Results {
195
201
  return this.initialItems.filter(node => node.extraneous)
196
202
  }
197
203
 
198
- hasPseudo () {
204
+ async hasPseudo () {
199
205
  const found = []
200
206
  for (const item of this.initialItems) {
201
- const res = retrieveNodesFromParsedAst({
202
- // This is the one time initialItems differs from inventory
207
+ // This is the one time initialItems differs from inventory
208
+ const res = await retrieveNodesFromParsedAst({
209
+ flatOptions: this.flatOptions,
203
210
  initialItems: [item],
204
211
  inventory: this.#inventory,
205
212
  rootAstNode: this.currentAstNode.nestedNode,
@@ -225,8 +232,9 @@ class Results {
225
232
  return found
226
233
  }
227
234
 
228
- isPseudo () {
229
- const res = retrieveNodesFromParsedAst({
235
+ async isPseudo () {
236
+ const res = await retrieveNodesFromParsedAst({
237
+ flatOptions: this.flatOptions,
230
238
  initialItems: this.initialItems,
231
239
  inventory: this.#inventory,
232
240
  rootAstNode: this.currentAstNode.nestedNode,
@@ -251,8 +259,9 @@ class Results {
251
259
  }, [])
252
260
  }
253
261
 
254
- notPseudo () {
255
- const res = retrieveNodesFromParsedAst({
262
+ async notPseudo () {
263
+ const res = await retrieveNodesFromParsedAst({
264
+ flatOptions: this.flatOptions,
256
265
  initialItems: this.initialItems,
257
266
  inventory: this.#inventory,
258
267
  rootAstNode: this.currentAstNode.nestedNode,
@@ -422,6 +431,135 @@ class Results {
422
431
  dedupedPseudo () {
423
432
  return this.initialItems.filter(node => node.target.edgesIn.size > 1)
424
433
  }
434
+
435
+ async outdatedPseudo () {
436
+ const { outdatedKind = 'any' } = this.currentAstNode
437
+
438
+ // filter the initialItems
439
+ // NOTE: this uses a Promise.all around a map without in-line concurrency handling
440
+ // since the only async action taken is retrieving the packument, which is limited
441
+ // based on the max-sockets config in make-fetch-happen
442
+ const initialResults = await Promise.all(this.initialItems.map(async (node) => {
443
+ // the root can't be outdated, skip it
444
+ if (node.isProjectRoot) {
445
+ return false
446
+ }
447
+
448
+ // we cache the promise representing the full versions list, this helps reduce the
449
+ // number of requests we send by keeping population of the cache in a single tick
450
+ // making it less likely that multiple requests for the same package will be inflight
451
+ if (!this.#outdatedCache.has(node.name)) {
452
+ this.#outdatedCache.set(node.name, getPackageVersions(node.name, this.flatOptions))
453
+ }
454
+ const availableVersions = await this.#outdatedCache.get(node.name)
455
+
456
+ // we attach _all_ versions to the queryContext to allow consumers to do their own
457
+ // filtering and comparisons
458
+ node.queryContext.versions = availableVersions
459
+
460
+ // next we further reduce the set to versions that are greater than the current one
461
+ const greaterVersions = availableVersions.filter((available) => {
462
+ return semver.gt(available, node.version)
463
+ })
464
+
465
+ // no newer versions than the current one, drop this node from the result set
466
+ if (!greaterVersions.length) {
467
+ return false
468
+ }
469
+
470
+ // if we got here, we know that newer versions exist, if the kind is 'any' we're done
471
+ if (outdatedKind === 'any') {
472
+ return node
473
+ }
474
+
475
+ // look for newer versions that differ from current by a specific part of the semver version
476
+ if (['major', 'minor', 'patch'].includes(outdatedKind)) {
477
+ // filter the versions greater than our current one based on semver.diff
478
+ const filteredVersions = greaterVersions.filter((version) => {
479
+ return semver.diff(node.version, version) === outdatedKind
480
+ })
481
+
482
+ // no available versions are of the correct diff type
483
+ if (!filteredVersions.length) {
484
+ return false
485
+ }
486
+
487
+ return node
488
+ }
489
+
490
+ // look for newer versions that satisfy at least one edgeIn to this node
491
+ if (outdatedKind === 'in-range') {
492
+ const inRangeContext = []
493
+ for (const edge of node.edgesIn) {
494
+ const inRangeVersions = greaterVersions.filter((version) => {
495
+ return semver.satisfies(version, edge.spec)
496
+ })
497
+
498
+ // this edge has no in-range candidates, just move on
499
+ if (!inRangeVersions.length) {
500
+ continue
501
+ }
502
+
503
+ inRangeContext.push({
504
+ from: edge.from.location,
505
+ versions: inRangeVersions,
506
+ })
507
+ }
508
+
509
+ // if we didn't find at least one match, drop this node
510
+ if (!inRangeContext.length) {
511
+ return false
512
+ }
513
+
514
+ // now add to the context each version that is in-range for each edgeIn
515
+ node.queryContext.outdated = {
516
+ ...node.queryContext.outdated,
517
+ inRange: inRangeContext,
518
+ }
519
+
520
+ return node
521
+ }
522
+
523
+ // look for newer versions that _do not_ satisfy at least one edgeIn
524
+ if (outdatedKind === 'out-of-range') {
525
+ const outOfRangeContext = []
526
+ for (const edge of node.edgesIn) {
527
+ const outOfRangeVersions = greaterVersions.filter((version) => {
528
+ return !semver.satisfies(version, edge.spec)
529
+ })
530
+
531
+ // this edge has no out-of-range candidates, skip it
532
+ if (!outOfRangeVersions.length) {
533
+ continue
534
+ }
535
+
536
+ outOfRangeContext.push({
537
+ from: edge.from.location,
538
+ versions: outOfRangeVersions,
539
+ })
540
+ }
541
+
542
+ // if we didn't add at least one thing to the context, this node is not a match
543
+ if (!outOfRangeContext.length) {
544
+ return false
545
+ }
546
+
547
+ // attach the out-of-range context to the node
548
+ node.queryContext.outdated = {
549
+ ...node.queryContext.outdated,
550
+ outOfRange: outOfRangeContext,
551
+ }
552
+
553
+ return node
554
+ }
555
+
556
+ // any other outdatedKind is unknown and will never match
557
+ return false
558
+ }))
559
+
560
+ // return an array with the holes for non-matching nodes removed
561
+ return initialResults.filter(Boolean)
562
+ }
425
563
  }
426
564
 
427
565
  // operators for attribute selectors
@@ -622,7 +760,41 @@ const combinators = {
622
760
  },
623
761
  }
624
762
 
625
- const retrieveNodesFromParsedAst = (opts) => {
763
+ // get a list of available versions of a package filtered to respect --before
764
+ // NOTE: this runs over each node and should not throw
765
+ const getPackageVersions = async (name, opts) => {
766
+ let packument
767
+ try {
768
+ packument = await pacote.packument(name, {
769
+ ...opts,
770
+ fullMetadata: false, // we only need the corgi
771
+ })
772
+ } catch (err) {
773
+ // if the fetch fails, log a warning and pretend there are no versions
774
+ log.warn('query', `could not retrieve packument for ${name}: ${err.message}`)
775
+ return []
776
+ }
777
+
778
+ // start with a sorted list of all versions (lowest first)
779
+ let candidates = Object.keys(packument.versions).sort(semver.compare)
780
+
781
+ // if the packument has a time property, and the user passed a before flag, then
782
+ // we filter this list down to only those versions that existed before the specified date
783
+ if (packument.time && opts.before) {
784
+ candidates = candidates.filter((version) => {
785
+ // this version isn't found in the times at all, drop it
786
+ if (!packument.time[version]) {
787
+ return false
788
+ }
789
+
790
+ return Date.parse(packument.time[version]) <= opts.before
791
+ })
792
+ }
793
+
794
+ return candidates
795
+ }
796
+
797
+ const retrieveNodesFromParsedAst = async (opts) => {
626
798
  // when we first call this it's the parsed query. all other times it's
627
799
  // results.currentNode.nestedNode
628
800
  const rootAstNode = opts.rootAstNode
@@ -633,7 +805,13 @@ const retrieveNodesFromParsedAst = (opts) => {
633
805
 
634
806
  const results = new Results(opts)
635
807
 
808
+ const astNodeQueue = new Set()
809
+ // walk is sync, so we have to build up our async functions and then await them later
636
810
  rootAstNode.walk((nextAstNode) => {
811
+ astNodeQueue.add(nextAstNode)
812
+ })
813
+
814
+ for (const nextAstNode of astNodeQueue) {
637
815
  // This is the only place we reset currentAstNode
638
816
  results.currentAstNode = nextAstNode
639
817
  const updateFn = `${results.currentAstNode.type}Type`
@@ -643,23 +821,24 @@ const retrieveNodesFromParsedAst = (opts) => {
643
821
  { code: 'EQUERYNOSELECTOR' }
644
822
  )
645
823
  }
646
- results[updateFn]()
647
- })
824
+ await results[updateFn]()
825
+ }
648
826
 
649
827
  return results.collect(rootAstNode)
650
828
  }
651
829
 
652
830
  // We are keeping this async in the event that we do add async operators, we
653
831
  // won't have to have a breaking change on this function signature.
654
- const querySelectorAll = async (targetNode, query) => {
832
+ const querySelectorAll = async (targetNode, query, flatOptions) => {
655
833
  // This never changes ever we just pass it around. But we can't scope it to
656
834
  // this whole file if we ever want to support concurrent calls to this
657
835
  // function.
658
836
  const inventory = [...targetNode.root.inventory.values()]
659
837
  // res is a Set of items returned for each parsed css ast selector
660
- const res = retrieveNodesFromParsedAst({
838
+ const res = await retrieveNodesFromParsedAst({
661
839
  initialItems: inventory,
662
840
  inventory,
841
+ flatOptions,
663
842
  rootAstNode: parser(query),
664
843
  targetNode,
665
844
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "6.0.0-pre.1",
3
+ "version": "6.0.0-pre.2",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",
@@ -11,7 +11,7 @@
11
11
  "@npmcli/name-from-folder": "^1.0.1",
12
12
  "@npmcli/node-gyp": "^2.0.0",
13
13
  "@npmcli/package-json": "^2.0.0",
14
- "@npmcli/query": "^1.2.0",
14
+ "@npmcli/query": "^2.0.0",
15
15
  "@npmcli/run-script": "^4.1.3",
16
16
  "bin-links": "^3.0.3",
17
17
  "cacache": "^16.1.3",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "@npmcli/eslint-config": "^3.1.0",
45
- "@npmcli/template-oss": "4.1.2",
45
+ "@npmcli/template-oss": "4.4.1",
46
46
  "benchmark": "^2.1.4",
47
47
  "chalk": "^4.1.0",
48
48
  "minify-registry-metadata": "^2.1.0",
@@ -52,13 +52,13 @@
52
52
  },
53
53
  "scripts": {
54
54
  "test": "tap",
55
- "posttest": "npm run lint",
55
+ "posttest": "node ../.. run lint",
56
56
  "snap": "tap",
57
57
  "postsnap": "npm run lintfix",
58
58
  "test-proxy": "ARBORIST_TEST_PROXY=1 tap --snapshot",
59
59
  "eslint": "eslint",
60
60
  "lint": "eslint \"**/*.js\"",
61
- "lintfix": "npm run lint -- --fix",
61
+ "lintfix": "node ../.. run lint -- --fix",
62
62
  "benchmark": "node scripts/benchmark.js",
63
63
  "benchclean": "rm -rf scripts/benchmark/*/",
64
64
  "npmclilint": "npmcli-lint",
@@ -93,13 +93,18 @@
93
93
  "--no-warnings",
94
94
  "--no-deprecation"
95
95
  ],
96
- "timeout": "360"
96
+ "timeout": "360",
97
+ "nyc-arg": [
98
+ "--exclude",
99
+ "tap-snapshots/**"
100
+ ]
97
101
  },
98
102
  "engines": {
99
103
  "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
100
104
  },
101
105
  "templateOSS": {
102
106
  "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
103
- "version": "4.1.2"
107
+ "version": "4.4.1",
108
+ "content": "../../scripts/template-oss/index.js"
104
109
  }
105
110
  }