@@ -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
} )
0 commit comments