• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const { resolve } = require('path')
4const { parser, arrayDelimiter } = require('@npmcli/query')
5const localeCompare = require('@isaacs/string-locale-compare')('en')
6const log = require('proc-log')
7const { minimatch } = require('minimatch')
8const npa = require('npm-package-arg')
9const pacote = require('pacote')
10const semver = require('semver')
11
12// handle results for parsed query asts, results are stored in a map that has a
13// key that points to each ast selector node and stores the resulting array of
14// arborist nodes as its value, that is essential to how we handle multiple
15// query selectors, e.g: `#a, #b, #c` <- 3 diff ast selector nodes
16class Results {
17  #currentAstSelector
18  #initialItems
19  #inventory
20  #outdatedCache = new Map()
21  #pendingCombinator
22  #results = new Map()
23  #targetNode
24
25  constructor (opts) {
26    this.#currentAstSelector = opts.rootAstNode.nodes[0]
27    this.#inventory = opts.inventory
28    this.#initialItems = opts.initialItems
29    this.#targetNode = opts.targetNode
30
31    this.currentResults = this.#initialItems
32
33    // We get this when first called and need to pass it to pacote
34    this.flatOptions = opts.flatOptions || {}
35
36    // reset by rootAstNode walker
37    this.currentAstNode = opts.rootAstNode
38  }
39
40  get currentResults () {
41    return this.#results.get(this.#currentAstSelector)
42  }
43
44  set currentResults (value) {
45    this.#results.set(this.#currentAstSelector, value)
46  }
47
48  // retrieves the initial items to which start the filtering / matching
49  // for most of the different types of recognized ast nodes, e.g: class (aka
50  // depType), id, *, etc in different contexts we need to start with the
51  // current list of filtered results, for example a query for `.workspace`
52  // actually means the same as `*.workspace` so we want to start with the full
53  // inventory if that's the first ast node we're reading but if it appears in
54  // the middle of a query it should respect the previous filtered results,
55  // combinators are a special case in which we always want to have the
56  // complete inventory list in order to use the left-hand side ast node as a
57  // filter combined with the element on its right-hand side
58  get initialItems () {
59    const firstParsed =
60      (this.currentAstNode.parent.nodes[0] === this.currentAstNode) &&
61      (this.currentAstNode.parent.parent.type === 'root')
62
63    if (firstParsed) {
64      return this.#initialItems
65    }
66
67    if (this.currentAstNode.prev().type === 'combinator') {
68      return this.#inventory
69    }
70    return this.currentResults
71  }
72
73  // combinators need information about previously filtered items along
74  // with info of the items parsed / retrieved from the selector right
75  // past the combinator, for this reason combinators are stored and
76  // only ran as the last part of each selector logic
77  processPendingCombinator (nextResults) {
78    if (this.#pendingCombinator) {
79      const res = this.#pendingCombinator(this.currentResults, nextResults)
80      this.#pendingCombinator = null
81      this.currentResults = res
82    } else {
83      this.currentResults = nextResults
84    }
85  }
86
87  // when collecting results to a root astNode, we traverse the list of child
88  // selector nodes and collect all of their resulting arborist nodes into a
89  // single/flat Set of items, this ensures we also deduplicate items
90  collect (rootAstNode) {
91    return new Set(rootAstNode.nodes.flatMap(n => this.#results.get(n)))
92  }
93
94  // selector types map to the '.type' property of the ast nodes via `${astNode.type}Type`
95  //
96  // attribute selector [name=value], etc
97  attributeType () {
98    const nextResults = this.initialItems.filter(node =>
99      attributeMatch(this.currentAstNode, node.package)
100    )
101    this.processPendingCombinator(nextResults)
102  }
103
104  // dependency type selector (i.e. .prod, .dev, etc)
105  // css calls this class, we interpret is as dependency type
106  classType () {
107    const depTypeFn = depTypes[String(this.currentAstNode)]
108    if (!depTypeFn) {
109      throw Object.assign(
110        new Error(`\`${String(this.currentAstNode)}\` is not a supported dependency type.`),
111        { code: 'EQUERYNODEPTYPE' }
112      )
113    }
114    const nextResults = depTypeFn(this.initialItems)
115    this.processPendingCombinator(nextResults)
116  }
117
118  // combinators (i.e. '>', ' ', '~')
119  combinatorType () {
120    this.#pendingCombinator = combinators[String(this.currentAstNode)]
121  }
122
123  // name selectors (i.e. #foo)
124  // css calls this id, we interpret it as name
125  idType () {
126    const name = this.currentAstNode.value
127    const nextResults = this.initialItems.filter(node =>
128      (name === node.name) || (name === node.package.name)
129    )
130    this.processPendingCombinator(nextResults)
131  }
132
133  // pseudo selectors (prefixed with :)
134  async pseudoType () {
135    const pseudoFn = `${this.currentAstNode.value.slice(1)}Pseudo`
136    if (!this[pseudoFn]) {
137      throw Object.assign(
138        new Error(`\`${this.currentAstNode.value
139        }\` is not a supported pseudo selector.`),
140        { code: 'EQUERYNOPSEUDO' }
141      )
142    }
143    const nextResults = await this[pseudoFn]()
144    this.processPendingCombinator(nextResults)
145  }
146
147  selectorType () {
148    this.#currentAstSelector = this.currentAstNode
149    // starts a new array in which resulting items
150    // can be stored for each given ast selector
151    if (!this.currentResults) {
152      this.currentResults = []
153    }
154  }
155
156  universalType () {
157    this.processPendingCombinator(this.initialItems)
158  }
159
160  // pseudo selectors map to the 'value' property of the pseudo selectors in the ast nodes
161  // via selectors via `${value.slice(1)}Pseudo`
162  attrPseudo () {
163    const { lookupProperties, attributeMatcher } = this.currentAstNode
164
165    return this.initialItems.filter(node => {
166      let objs = [node.package]
167      for (const prop of lookupProperties) {
168        // if an isArray symbol is found that means we'll need to iterate
169        // over the previous found array to basically make sure we traverse
170        // all its indexes testing for possible objects that may eventually
171        // hold more keys specified in a selector
172        if (prop === arrayDelimiter) {
173          objs = objs.flat()
174          continue
175        }
176
177        // otherwise just maps all currently found objs
178        // to the next prop from the lookup properties list,
179        // filters out any empty key lookup
180        objs = objs.flatMap(obj => obj[prop] || [])
181
182        // in case there's no property found in the lookup
183        // just filters that item out
184        const noAttr = objs.every(obj => !obj)
185        if (noAttr) {
186          return false
187        }
188      }
189
190      // if any of the potential object matches
191      // that item should be in the final result
192      return objs.some(obj => attributeMatch(attributeMatcher, obj))
193    })
194  }
195
196  emptyPseudo () {
197    return this.initialItems.filter(node => node.edgesOut.size === 0)
198  }
199
200  extraneousPseudo () {
201    return this.initialItems.filter(node => node.extraneous)
202  }
203
204  async hasPseudo () {
205    const found = []
206    for (const item of this.initialItems) {
207      // This is the one time initialItems differs from inventory
208      const res = await retrieveNodesFromParsedAst({
209        flatOptions: this.flatOptions,
210        initialItems: [item],
211        inventory: this.#inventory,
212        rootAstNode: this.currentAstNode.nestedNode,
213        targetNode: item,
214      })
215      if (res.size > 0) {
216        found.push(item)
217      }
218    }
219    return found
220  }
221
222  invalidPseudo () {
223    const found = []
224    for (const node of this.initialItems) {
225      for (const edge of node.edgesIn) {
226        if (edge.invalid) {
227          found.push(node)
228          break
229        }
230      }
231    }
232    return found
233  }
234
235  async isPseudo () {
236    const res = await retrieveNodesFromParsedAst({
237      flatOptions: this.flatOptions,
238      initialItems: this.initialItems,
239      inventory: this.#inventory,
240      rootAstNode: this.currentAstNode.nestedNode,
241      targetNode: this.currentAstNode,
242    })
243    return [...res]
244  }
245
246  linkPseudo () {
247    return this.initialItems.filter(node => node.isLink || (node.isTop && !node.isRoot))
248  }
249
250  missingPseudo () {
251    return this.#inventory.reduce((res, node) => {
252      for (const edge of node.edgesOut.values()) {
253        if (edge.missing) {
254          const pkg = { name: edge.name, version: edge.spec }
255          res.push(new this.#targetNode.constructor({ pkg }))
256        }
257      }
258      return res
259    }, [])
260  }
261
262  async notPseudo () {
263    const res = await retrieveNodesFromParsedAst({
264      flatOptions: this.flatOptions,
265      initialItems: this.initialItems,
266      inventory: this.#inventory,
267      rootAstNode: this.currentAstNode.nestedNode,
268      targetNode: this.currentAstNode,
269    })
270    const internalSelector = new Set(res)
271    return this.initialItems.filter(node =>
272      !internalSelector.has(node))
273  }
274
275  overriddenPseudo () {
276    return this.initialItems.filter(node => node.overridden)
277  }
278
279  pathPseudo () {
280    return this.initialItems.filter(node => {
281      if (!this.currentAstNode.pathValue) {
282        return true
283      }
284      return minimatch(
285        node.realpath.replace(/\\+/g, '/'),
286        resolve(node.root.realpath, this.currentAstNode.pathValue).replace(/\\+/g, '/')
287      )
288    })
289  }
290
291  privatePseudo () {
292    return this.initialItems.filter(node => node.package.private)
293  }
294
295  rootPseudo () {
296    return this.initialItems.filter(node => node === this.#targetNode.root)
297  }
298
299  scopePseudo () {
300    return this.initialItems.filter(node => node === this.#targetNode)
301  }
302
303  semverPseudo () {
304    const {
305      attributeMatcher,
306      lookupProperties,
307      semverFunc = 'infer',
308      semverValue,
309    } = this.currentAstNode
310    const { qualifiedAttribute } = attributeMatcher
311
312    if (!semverValue) {
313      // DEPRECATED: remove this warning and throw an error as part of @npmcli/arborist@6
314      log.warn('query', 'usage of :semver() with no parameters is deprecated')
315      return this.initialItems
316    }
317
318    if (!semver.valid(semverValue) && !semver.validRange(semverValue)) {
319      throw Object.assign(
320        new Error(`\`${semverValue}\` is not a valid semver version or range`),
321        { code: 'EQUERYINVALIDSEMVER' })
322    }
323
324    const valueIsVersion = !!semver.valid(semverValue)
325
326    const nodeMatches = (node, obj) => {
327      // if we already have an operator, the user provided some test as part of the selector
328      // we evaluate that first because if it fails we don't want this node anyway
329      if (attributeMatcher.operator) {
330        if (!attributeMatch(attributeMatcher, obj)) {
331          // if the initial operator doesn't match, we're done
332          return false
333        }
334      }
335
336      const attrValue = obj[qualifiedAttribute]
337      // both valid and validRange return null for undefined, so this will skip both nodes that
338      // do not have the attribute defined as well as those where the attribute value is invalid
339      // and those where the value from the package.json is not a string
340      if ((!semver.valid(attrValue) && !semver.validRange(attrValue)) ||
341          typeof attrValue !== 'string') {
342        return false
343      }
344
345      const attrIsVersion = !!semver.valid(attrValue)
346
347      let actualFunc = semverFunc
348
349      // if we're asked to infer, we examine outputs to make a best guess
350      if (actualFunc === 'infer') {
351        if (valueIsVersion && attrIsVersion) {
352          // two versions -> semver.eq
353          actualFunc = 'eq'
354        } else if (!valueIsVersion && !attrIsVersion) {
355          // two ranges -> semver.intersects
356          actualFunc = 'intersects'
357        } else {
358          // anything else -> semver.satisfies
359          actualFunc = 'satisfies'
360        }
361      }
362
363      if (['eq', 'neq', 'gt', 'gte', 'lt', 'lte'].includes(actualFunc)) {
364        // both sides must be versions, but one is not
365        if (!valueIsVersion || !attrIsVersion) {
366          return false
367        }
368
369        return semver[actualFunc](attrValue, semverValue)
370      } else if (['gtr', 'ltr', 'satisfies'].includes(actualFunc)) {
371        // at least one side must be a version, but neither is
372        if (!valueIsVersion && !attrIsVersion) {
373          return false
374        }
375
376        return valueIsVersion
377          ? semver[actualFunc](semverValue, attrValue)
378          : semver[actualFunc](attrValue, semverValue)
379      } else if (['intersects', 'subset'].includes(actualFunc)) {
380        // these accept two ranges and since a version is also a range, anything goes
381        return semver[actualFunc](attrValue, semverValue)
382      } else {
383        // user provided a function we don't know about, throw an error
384        throw Object.assign(new Error(`\`semver.${actualFunc}\` is not a supported operator.`),
385          { code: 'EQUERYINVALIDOPERATOR' })
386      }
387    }
388
389    return this.initialItems.filter((node) => {
390      // no lookupProperties just means its a top level property, see if it matches
391      if (!lookupProperties.length) {
392        return nodeMatches(node, node.package)
393      }
394
395      // this code is mostly duplicated from attrPseudo to traverse into the package until we get
396      // to our deepest requested object
397      let objs = [node.package]
398      for (const prop of lookupProperties) {
399        if (prop === arrayDelimiter) {
400          objs = objs.flat()
401          continue
402        }
403
404        objs = objs.flatMap(obj => obj[prop] || [])
405        const noAttr = objs.every(obj => !obj)
406        if (noAttr) {
407          return false
408        }
409
410        return objs.some(obj => nodeMatches(node, obj))
411      }
412    })
413  }
414
415  typePseudo () {
416    if (!this.currentAstNode.typeValue) {
417      return this.initialItems
418    }
419    return this.initialItems
420      .flatMap(node => {
421        const found = []
422        for (const edge of node.edgesIn) {
423          if (npa(`${edge.name}@${edge.spec}`).type === this.currentAstNode.typeValue) {
424            found.push(edge.to)
425          }
426        }
427        return found
428      })
429  }
430
431  dedupedPseudo () {
432    return this.initialItems.filter(node => node.target.edgesIn.size > 1)
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  }
563}
564
565// operators for attribute selectors
566const attributeOperators = {
567  // attribute value is equivalent
568  '=' ({ attr, value, insensitive }) {
569    return attr === value
570  },
571  // attribute value contains word
572  '~=' ({ attr, value, insensitive }) {
573    return (attr.match(/\w+/g) || []).includes(value)
574  },
575  // attribute value contains string
576  '*=' ({ attr, value, insensitive }) {
577    return attr.includes(value)
578  },
579  // attribute value is equal or starts with
580  '|=' ({ attr, value, insensitive }) {
581    return attr.startsWith(`${value}-`)
582  },
583  // attribute value starts with
584  '^=' ({ attr, value, insensitive }) {
585    return attr.startsWith(value)
586  },
587  // attribute value ends with
588  '$=' ({ attr, value, insensitive }) {
589    return attr.endsWith(value)
590  },
591}
592
593const attributeOperator = ({ attr, value, insensitive, operator }) => {
594  if (typeof attr === 'number') {
595    attr = String(attr)
596  }
597  if (typeof attr !== 'string') {
598    // It's an object or an array, bail
599    return false
600  }
601  if (insensitive) {
602    attr = attr.toLowerCase()
603  }
604
605  return attributeOperators[operator]({
606    attr,
607    insensitive,
608    value,
609  })
610}
611
612const attributeMatch = (matcher, obj) => {
613  const insensitive = !!matcher.insensitive
614  const operator = matcher.operator || ''
615  const attribute = matcher.qualifiedAttribute
616  let value = matcher.value || ''
617  // return early if checking existence
618  if (operator === '') {
619    return Boolean(obj[attribute])
620  }
621  if (insensitive) {
622    value = value.toLowerCase()
623  }
624  // in case the current object is an array
625  // then we try to match every item in the array
626  if (Array.isArray(obj[attribute])) {
627    return obj[attribute].find((i, index) => {
628      const attr = obj[attribute][index] || ''
629      return attributeOperator({ attr, value, insensitive, operator })
630    })
631  } else {
632    const attr = obj[attribute] || ''
633    return attributeOperator({ attr, value, insensitive, operator })
634  }
635}
636
637const edgeIsType = (node, type, seen = new Set()) => {
638  for (const edgeIn of node.edgesIn) {
639    // TODO Need a test with an infinite loop
640    if (seen.has(edgeIn)) {
641      continue
642    }
643    seen.add(edgeIn)
644    if (edgeIn.type === type || edgeIn.from[type] || edgeIsType(edgeIn.from, type, seen)) {
645      return true
646    }
647  }
648  return false
649}
650
651const filterByType = (nodes, type) => {
652  const found = []
653  for (const node of nodes) {
654    if (node[type] || edgeIsType(node, type)) {
655      found.push(node)
656    }
657  }
658  return found
659}
660
661const depTypes = {
662  // dependency
663  '.prod' (prevResults) {
664    const found = []
665    for (const node of prevResults) {
666      if (!node.dev) {
667        found.push(node)
668      }
669    }
670    return found
671  },
672  // devDependency
673  '.dev' (prevResults) {
674    return filterByType(prevResults, 'dev')
675  },
676  // optionalDependency
677  '.optional' (prevResults) {
678    return filterByType(prevResults, 'optional')
679  },
680  // peerDependency
681  '.peer' (prevResults) {
682    return filterByType(prevResults, 'peer')
683  },
684  // workspace
685  '.workspace' (prevResults) {
686    return prevResults.filter(node => node.isWorkspace)
687  },
688  // bundledDependency
689  '.bundled' (prevResults) {
690    return prevResults.filter(node => node.inBundle)
691  },
692}
693
694// checks if a given node has a direct parent in any of the nodes provided in
695// the compare nodes array
696const hasParent = (node, compareNodes) => {
697  // All it takes is one so we loop and return on the first hit
698  for (const compareNode of compareNodes) {
699    // follows logical parent for link anscestors
700    if (node.isTop && (node.resolveParent === compareNode)) {
701      return true
702    }
703    // follows edges-in to check if they match a possible parent
704    for (const edge of node.edgesIn) {
705      if (edge && edge.from === compareNode) {
706        return true
707      }
708    }
709  }
710  return false
711}
712
713// checks if a given node is a descendant of any of the nodes provided in the
714// compareNodes array
715const hasAscendant = (node, compareNodes, seen = new Set()) => {
716  // TODO (future) loop over ancestry property
717  if (hasParent(node, compareNodes)) {
718    return true
719  }
720
721  if (node.isTop && node.resolveParent) {
722    return hasAscendant(node.resolveParent, compareNodes)
723  }
724  for (const edge of node.edgesIn) {
725    // TODO Need a test with an infinite loop
726    if (seen.has(edge)) {
727      continue
728    }
729    seen.add(edge)
730    if (edge && edge.from && hasAscendant(edge.from, compareNodes, seen)) {
731      return true
732    }
733  }
734  return false
735}
736
737const combinators = {
738  // direct descendant
739  '>' (prevResults, nextResults) {
740    return nextResults.filter(node => hasParent(node, prevResults))
741  },
742  // any descendant
743  ' ' (prevResults, nextResults) {
744    return nextResults.filter(node => hasAscendant(node, prevResults))
745  },
746  // sibling
747  '~' (prevResults, nextResults) {
748    // Return any node in nextResults that is a sibling of (aka shares a
749    // parent with) a node in prevResults
750    const parentNodes = new Set() // Parents of everything in prevResults
751    for (const node of prevResults) {
752      for (const edge of node.edgesIn) {
753        // edge.from always exists cause it's from another node's edgesIn
754        parentNodes.add(edge.from)
755      }
756    }
757    return nextResults.filter(node =>
758      !prevResults.includes(node) && hasParent(node, [...parentNodes])
759    )
760  },
761}
762
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
765const 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
797const retrieveNodesFromParsedAst = async (opts) => {
798  // when we first call this it's the parsed query.  all other times it's
799  // results.currentNode.nestedNode
800  const rootAstNode = opts.rootAstNode
801
802  if (!rootAstNode.nodes) {
803    return new Set()
804  }
805
806  const results = new Results(opts)
807
808  const astNodeQueue = new Set()
809  // walk is sync, so we have to build up our async functions and then await them later
810  rootAstNode.walk((nextAstNode) => {
811    astNodeQueue.add(nextAstNode)
812  })
813
814  for (const nextAstNode of astNodeQueue) {
815    // This is the only place we reset currentAstNode
816    results.currentAstNode = nextAstNode
817    const updateFn = `${results.currentAstNode.type}Type`
818    if (typeof results[updateFn] !== 'function') {
819      throw Object.assign(
820        new Error(`\`${results.currentAstNode.type}\` is not a supported selector.`),
821        { code: 'EQUERYNOSELECTOR' }
822      )
823    }
824    await results[updateFn]()
825  }
826
827  return results.collect(rootAstNode)
828}
829
830// We are keeping this async in the event that we do add async operators, we
831// won't have to have a breaking change on this function signature.
832const querySelectorAll = async (targetNode, query, flatOptions) => {
833  // This never changes ever we just pass it around. But we can't scope it to
834  // this whole file if we ever want to support concurrent calls to this
835  // function.
836  const inventory = [...targetNode.root.inventory.values()]
837  // res is a Set of items returned for each parsed css ast selector
838  const res = await retrieveNodesFromParsedAst({
839    initialItems: inventory,
840    inventory,
841    flatOptions,
842    rootAstNode: parser(query),
843    targetNode,
844  })
845
846  // returns nodes ordered by realpath
847  return [...res].sort((a, b) => localeCompare(a.location, b.location))
848}
849
850module.exports = querySelectorAll
851