Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
query-selector-all.js959 linesDownload Raw Back to lib
1'use strict'
2
3const { resolve } = require('node: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')
11const npmFetch = require('npm-registry-fetch')
12
13// handle results for parsed query asts, results are stored in a map that has a
14// key that points to each ast selector node and stores the resulting array of
15// arborist nodes as its value, that is essential to how we handle multiple
16// query selectors, e.g: `#a, #b, #c` <- 3 diff ast selector nodes
17class Results {
18  #currentAstSelector
19  #initialItems
20  #inventory
21  #outdatedCache = new Map()
22  #vulnCache
23  #pendingCombinator
24  #results = new Map()
25  #targetNode
26
27  constructor (opts) {
28    this.#currentAstSelector = opts.rootAstNode.nodes[0]
29    this.#inventory = opts.inventory
30    this.#initialItems = opts.initialItems
31    this.#vulnCache = opts.vulnCache
32    this.#targetNode = opts.targetNode
33
34    this.currentResults = this.#initialItems
35
36    // We get this when first called and need to pass it to pacote
37    this.flatOptions = opts.flatOptions || {}
38
39    // reset by rootAstNode walker
40    this.currentAstNode = opts.rootAstNode
41  }
42
43  get currentResults () {
44    return this.#results.get(this.#currentAstSelector)
45  }
46
47  set currentResults (value) {
48    this.#results.set(this.#currentAstSelector, value)
49  }
50
51  // retrieves the initial items to which start the filtering / matching
52  // for most of the different types of recognized ast nodes, e.g: class (aka
53  // depType), id, *, etc in different contexts we need to start with the
54  // current list of filtered results, for example a query for `.workspace`
55  // actually means the same as `*.workspace` so we want to start with the full
56  // inventory if that's the first ast node we're reading but if it appears in
57  // the middle of a query it should respect the previous filtered results,
58  // combinators are a special case in which we always want to have the
59  // complete inventory list in order to use the left-hand side ast node as a
60  // filter combined with the element on its right-hand side
61  get initialItems () {
62    const firstParsed =
63      (this.currentAstNode.parent.nodes[0] === this.currentAstNode) &&
64      (this.currentAstNode.parent.parent.type === 'root')
65
66    if (firstParsed) {
67      return this.#initialItems
68    }
69
70    if (this.currentAstNode.prev().type === 'combinator') {
71      return this.#inventory
72    }
73    return this.currentResults
74  }
75
76  // combinators need information about previously filtered items along
77  // with info of the items parsed / retrieved from the selector right
78  // past the combinator, for this reason combinators are stored and
79  // only ran as the last part of each selector logic
80  processPendingCombinator (nextResults) {
81    if (this.#pendingCombinator) {
82      const res = this.#pendingCombinator(this.currentResults, nextResults)
83      this.#pendingCombinator = null
84      this.currentResults = res
85    } else {
86      this.currentResults = nextResults
87    }
88  }
89
90  // when collecting results to a root astNode, we traverse the list of child
91  // selector nodes and collect all of their resulting arborist nodes into a
92  // single/flat Set of items, this ensures we also deduplicate items
93  collect (rootAstNode) {
94    return new Set(rootAstNode.nodes.flatMap(n => this.#results.get(n)))
95  }
96
97  // selector types map to the '.type' property of the ast nodes via `${astNode.type}Type`
98  //
99  // attribute selector [name=value], etc
100  attributeType () {
101    const nextResults = this.initialItems.filter(node =>
102      attributeMatch(this.currentAstNode, node.package)
103    )
104    this.processPendingCombinator(nextResults)
105  }
106
107  // dependency type selector (i.e. .prod, .dev, etc)
108  // css calls this class, we interpret is as dependency type
109  classType () {
110    const depTypeFn = depTypes[String(this.currentAstNode)]
111    if (!depTypeFn) {
112      throw Object.assign(
113        new Error(`\`${String(this.currentAstNode)}\` is not a supported dependency type.`),
114        { code: 'EQUERYNODEPTYPE' }
115      )
116    }
117    const nextResults = depTypeFn(this.initialItems)
118    this.processPendingCombinator(nextResults)
119  }
120
121  // combinators (i.e. '>', ' ', '~')
122  combinatorType () {
123    this.#pendingCombinator = combinators[String(this.currentAstNode)]
124  }
125
126  // name selectors (i.e. #foo)
127  // css calls this id, we interpret it as name
128  idType () {
129    const name = this.currentAstNode.value
130    const nextResults = this.initialItems.filter(node =>
131      (name === node.name) || (name === node.package.name)
132    )
133    this.processPendingCombinator(nextResults)
134  }
135
136  // pseudo selectors (prefixed with :)
137  async pseudoType () {
138    const pseudoFn = `${this.currentAstNode.value.slice(1)}Pseudo`
139    if (!this[pseudoFn]) {
140      throw Object.assign(
141        new Error(`\`${this.currentAstNode.value
142        }\` is not a supported pseudo selector.`),
143        { code: 'EQUERYNOPSEUDO' }
144      )
145    }
146    const nextResults = await this[pseudoFn]()
147    this.processPendingCombinator(nextResults)
148  }
149
150  selectorType () {
151    this.#currentAstSelector = this.currentAstNode
152    // starts a new array in which resulting items
153    // can be stored for each given ast selector
154    if (!this.currentResults) {
155      this.currentResults = []
156    }
157  }
158
159  universalType () {
160    this.processPendingCombinator(this.initialItems)
161  }
162
163  // pseudo selectors map to the 'value' property of the pseudo selectors in the ast nodes
164  // via selectors via `${value.slice(1)}Pseudo`
165  attrPseudo () {
166    const { lookupProperties, attributeMatcher } = this.currentAstNode
167
168    return this.initialItems.filter(node => {
169      let objs = [node.package]
170      for (const prop of lookupProperties) {
171        // if an isArray symbol is found that means we'll need to iterate
172        // over the previous found array to basically make sure we traverse
173        // all its indexes testing for possible objects that may eventually
174        // hold more keys specified in a selector
175        if (prop === arrayDelimiter) {
176          objs = objs.flat()
177          continue
178        }
179
180        // otherwise just maps all currently found objs
181        // to the next prop from the lookup properties list,
182        // filters out any empty key lookup
183        objs = objs.flatMap(obj => obj[prop] || [])
184
185        // in case there's no property found in the lookup
186        // just filters that item out
187        const noAttr = objs.every(obj => !obj)
188        if (noAttr) {
189          return false
190        }
191      }
192
193      // if any of the potential object matches
194      // that item should be in the final result
195      return objs.some(obj => attributeMatch(attributeMatcher, obj))
196    })
197  }
198
199  emptyPseudo () {
200    return this.initialItems.filter(node => node.edgesOut.size === 0)
201  }
202
203  extraneousPseudo () {
204    return this.initialItems.filter(node => node.extraneous)
205  }
206
207  async hasPseudo () {
208    const found = []
209    for (const item of this.initialItems) {
210      // This is the one time initialItems differs from inventory
211      const res = await retrieveNodesFromParsedAst({
212        flatOptions: this.flatOptions,
213        initialItems: [item],
214        inventory: this.#inventory,
215        rootAstNode: this.currentAstNode.nestedNode,
216        targetNode: item,
217        vulnCache: this.#vulnCache,
218      })
219      if (res.size > 0) {
220        found.push(item)
221      }
222    }
223    return found
224  }
225
226  invalidPseudo () {
227    const found = []
228    for (const node of this.initialItems) {
229      for (const edge of node.edgesIn) {
230        if (edge.invalid) {
231          found.push(node)
232          break
233        }
234      }
235    }
236    return found
237  }
238
239  async isPseudo () {
240    const res = await retrieveNodesFromParsedAst({
241      flatOptions: this.flatOptions,
242      initialItems: this.initialItems,
243      inventory: this.#inventory,
244      rootAstNode: this.currentAstNode.nestedNode,
245      targetNode: this.currentAstNode,
246      vulnCache: this.#vulnCache,
247    })
248    return [...res]
249  }
250
251  linkPseudo () {
252    return this.initialItems.filter(node => node.isLink || (node.isTop && !node.isRoot))
253  }
254
255  missingPseudo () {
256    return this.#inventory.reduce((res, node) => {
257      for (const edge of node.edgesOut.values()) {
258        if (edge.missing) {
259          const pkg = { name: edge.name, version: edge.spec }
260          const item = new this.#targetNode.constructor({ pkg })
261          item.queryContext = {
262            missing: true,
263          }
264          item.edgesIn = new Set([edge])
265          res.push(item)
266        }
267      }
268      return res
269    }, [])
270  }
271
272  async notPseudo () {
273    const res = await retrieveNodesFromParsedAst({
274      flatOptions: this.flatOptions,
275      initialItems: this.initialItems,
276      inventory: this.#inventory,
277      rootAstNode: this.currentAstNode.nestedNode,
278      targetNode: this.currentAstNode,
279      vulnCache: this.#vulnCache,
280    })
281    const internalSelector = new Set(res)
282    return this.initialItems.filter(node =>
283      !internalSelector.has(node))
284  }
285
286  overriddenPseudo () {
287    return this.initialItems.filter(node => node.overridden)
288  }
289
290  pathPseudo () {
291    return this.initialItems.filter(node => {
292      if (!this.currentAstNode.pathValue) {
293        return true
294      }
295      return minimatch(
296        node.realpath.replace(/\\+/g, '/'),
297        resolve(node.root.realpath, this.currentAstNode.pathValue).replace(/\\+/g, '/')
298      )
299    })
300  }
301
302  privatePseudo () {
303    return this.initialItems.filter(node => node.package.private)
304  }
305
306  rootPseudo () {
307    return this.initialItems.filter(node => node === this.#targetNode.root)
308  }
309
310  scopePseudo () {
311    return this.initialItems.filter(node => node === this.#targetNode)
312  }
313
314  semverPseudo () {
315    const {
316      attributeMatcher,
317      lookupProperties,
318      semverFunc = 'infer',
319      semverValue,
320    } = this.currentAstNode
321    const { qualifiedAttribute } = attributeMatcher
322
323    if (!semverValue) {
324      // DEPRECATED: remove this warning and throw an error as part of @npmcli/arborist@6
325      log.warn('query', 'usage of :semver() with no parameters is deprecated')
326      return this.initialItems
327    }
328
329    if (!semver.valid(semverValue) && !semver.validRange(semverValue)) {
330      throw Object.assign(
331        new Error(`\`${semverValue}\` is not a valid semver version or range`),
332        { code: 'EQUERYINVALIDSEMVER' })
333    }
334
335    const valueIsVersion = !!semver.valid(semverValue)
336
337    const nodeMatches = (node, obj) => {
338      // if we already have an operator, the user provided some test as part of the selector
339      // we evaluate that first because if it fails we don't want this node anyway
340      if (attributeMatcher.operator) {
341        if (!attributeMatch(attributeMatcher, obj)) {
342          // if the initial operator doesn't match, we're done
343          return false
344        }
345      }
346
347      const attrValue = obj[qualifiedAttribute]
348      // both valid and validRange return null for undefined, so this will skip both nodes that
349      // do not have the attribute defined as well as those where the attribute value is invalid
350      // and those where the value from the package.json is not a string
351      if ((!semver.valid(attrValue) && !semver.validRange(attrValue)) ||
352          typeof attrValue !== 'string') {
353        return false
354      }
355
356      const attrIsVersion = !!semver.valid(attrValue)
357
358      let actualFunc = semverFunc
359
360      // if we're asked to infer, we examine outputs to make a best guess
361      if (actualFunc === 'infer') {
362        if (valueIsVersion && attrIsVersion) {
363          // two versions -> semver.eq
364          actualFunc = 'eq'
365        } else if (!valueIsVersion && !attrIsVersion) {
366          // two ranges -> semver.intersects
367          actualFunc = 'intersects'
368        } else {
369          // anything else -> semver.satisfies
370          actualFunc = 'satisfies'
371        }
372      }
373
374      if (['eq', 'neq', 'gt', 'gte', 'lt', 'lte'].includes(actualFunc)) {
375        // both sides must be versions, but one is not
376        if (!valueIsVersion || !attrIsVersion) {
377          return false
378        }
379
380        return semver[actualFunc](attrValue, semverValue)
381      } else if (['gtr', 'ltr', 'satisfies'].includes(actualFunc)) {
382        // at least one side must be a version, but neither is
383        if (!valueIsVersion && !attrIsVersion) {
384          return false
385        }
386
387        return valueIsVersion
388          ? semver[actualFunc](semverValue, attrValue)
389          : semver[actualFunc](attrValue, semverValue)
390      } else if (['intersects', 'subset'].includes(actualFunc)) {
391        // these accept two ranges and since a version is also a range, anything goes
392        return semver[actualFunc](attrValue, semverValue)
393      } else {
394        // user provided a function we don't know about, throw an error
395        throw Object.assign(new Error(`\`semver.${actualFunc}\` is not a supported operator.`),
396          { code: 'EQUERYINVALIDOPERATOR' })
397      }
398    }
399
400    return this.initialItems.filter((node) => {
401      // no lookupProperties just means its a top level property, see if it matches
402      if (!lookupProperties.length) {
403        return nodeMatches(node, node.package)
404      }
405
406      // this code is mostly duplicated from attrPseudo to traverse into the package until we get
407      // to our deepest requested object
408      let objs = [node.package]
409      for (const prop of lookupProperties) {
410        if (prop === arrayDelimiter) {
411          objs = objs.flat()
412          continue
413        }
414
415        objs = objs.flatMap(obj => obj[prop] || [])
416        const noAttr = objs.every(obj => !obj)
417        if (noAttr) {
418          return false
419        }
420
421        return objs.some(obj => nodeMatches(node, obj))
422      }
423    })
424  }
425
426  typePseudo () {
427    if (!this.currentAstNode.typeValue) {
428      return this.initialItems
429    }
430    // TODO this differs subtly with `:type()` because it now iterates on edgesIn, which means extraneous deps won't show up
431    // note how "@npmcli/abbrev@2.0.0-beta.45" is in the `:type()` results in the test but not in any of the other results.
432    return this.initialItems
433      .flatMap(node => {
434        const found = []
435        const { typeValue } = this.currentAstNode
436        for (const edge of node.edgesIn) {
437          const parsedArg = npa(`${edge.name}@${edge.spec}`)
438          if (typeValue === 'registry') {
439            if (parsedArg.registry) {
440              found.push(edge.to)
441            }
442          } else if (parsedArg.type === typeValue) {
443            found.push(edge.to)
444          }
445        }
446        return found
447      })
448  }
449
450  dedupedPseudo () {
451    return this.initialItems.filter(node => node.target.edgesIn.size > 1)
452  }
453
454  async vulnPseudo () {
455    if (!this.initialItems.length) {
456      return this.initialItems
457    }
458    if (!this.#vulnCache) {
459      const packages = {}
460      // We have to map the items twice, once to get the request, and a second time to filter out the results of that request
461      this.initialItems.map((node) => {
462        if (node.isProjectRoot || node.package.private) {
463          return
464        }
465        if (!packages[node.name]) {
466          packages[node.name] = []
467        }
468        if (!packages[node.name].includes(node.version)) {
469          packages[node.name].push(node.version)
470        }
471      })
472      const res = await npmFetch('/-/npm/v1/security/advisories/bulk', {
473        ...this.flatOptions,
474        registry: this.flatOptions.auditRegistry || this.flatOptions.registry,
475        method: 'POST',
476        gzip: true,
477        body: packages,
478      })
479      this.#vulnCache = await res.json()
480    }
481    const advisories = this.#vulnCache
482    const { vulns } = this.currentAstNode
483    return this.initialItems.filter(item => {
484      const vulnerable = advisories[item.name]?.filter(advisory => {
485        // This could be for another version of this package elsewhere in the tree
486        if (!semver.intersects(advisory.vulnerable_versions, item.version)) {
487          return false
488        }
489        if (!vulns) {
490          return true
491        }
492        // vulns are OR with each other, if any one matches we're done
493        for (const vuln of vulns) {
494          if (vuln.severity && !vuln.severity.includes('*')) {
495            if (!vuln.severity.includes(advisory.severity)) {
496              continue
497            }
498          }
499
500          if (vuln?.cwe) {
501            // * is special, it means "has a cwe"
502            if (vuln.cwe.includes('*')) {
503              if (!advisory.cwe.length) {
504                continue
505              }
506            } else if (!vuln.cwe.every(cwe => advisory.cwe.includes(`CWE-${cwe}`))) {
507              continue
508            }
509          }
510          return true
511        }
512      })
513      if (vulnerable?.length) {
514        item.queryContext = {
515          advisories: vulnerable,
516        }
517        return true
518      }
519      return false
520    })
521  }
522
523  async outdatedPseudo () {
524    const { outdatedKind = 'any' } = this.currentAstNode
525
526    // filter the initialItems
527    // NOTE: this uses a Promise.all around a map without in-line concurrency handling
528    // since the only async action taken is retrieving the packument, which is limited
529    // based on the max-sockets config in make-fetch-happen
530    const initialResults = await Promise.all(this.initialItems.map(async (node) => {
531      // the root can't be outdated, skip it
532      if (node.isProjectRoot) {
533        return false
534      }
535
536      // private packages can't be published, skip them
537      if (node.package.private) {
538        return false
539      }
540
541      // we cache the promise representing the full versions list, this helps reduce the
542      // number of requests we send by keeping population of the cache in a single tick
543      // making it less likely that multiple requests for the same package will be inflight
544      if (!this.#outdatedCache.has(node.name)) {
545        this.#outdatedCache.set(node.name, getPackageVersions(node.name, this.flatOptions))
546      }
547      const availableVersions = await this.#outdatedCache.get(node.name)
548
549      // we attach _all_ versions to the queryContext to allow consumers to do their own
550      // filtering and comparisons
551      node.queryContext.versions = availableVersions
552
553      // next we further reduce the set to versions that are greater than the current one
554      const greaterVersions = availableVersions.filter((available) => {
555        return semver.gt(available, node.version)
556      })
557
558      // no newer versions than the current one, drop this node from the result set
559      if (!greaterVersions.length) {
560        return false
561      }
562
563      // if we got here, we know that newer versions exist, if the kind is 'any' we're done
564      if (outdatedKind === 'any') {
565        return node
566      }
567
568      // look for newer versions that differ from current by a specific part of the semver version
569      if (['major', 'minor', 'patch'].includes(outdatedKind)) {
570        // filter the versions greater than our current one based on semver.diff
571        const filteredVersions = greaterVersions.filter((version) => {
572          return semver.diff(node.version, version) === outdatedKind
573        })
574
575        // no available versions are of the correct diff type
576        if (!filteredVersions.length) {
577          return false
578        }
579
580        return node
581      }
582
583      // look for newer versions that satisfy at least one edgeIn to this node
584      if (outdatedKind === 'in-range') {
585        const inRangeContext = []
586        for (const edge of node.edgesIn) {
587          const inRangeVersions = greaterVersions.filter((version) => {
588            return semver.satisfies(version, edge.spec)
589          })
590
591          // this edge has no in-range candidates, just move on
592          if (!inRangeVersions.length) {
593            continue
594          }
595
596          inRangeContext.push({
597            from: edge.from.location,
598            versions: inRangeVersions,
599          })
600        }
601
602        // if we didn't find at least one match, drop this node
603        if (!inRangeContext.length) {
604          return false
605        }
606
607        // now add to the context each version that is in-range for each edgeIn
608        node.queryContext.outdated = {
609          ...node.queryContext.outdated,
610          inRange: inRangeContext,
611        }
612
613        return node
614      }
615
616      // look for newer versions that _do not_ satisfy at least one edgeIn
617      if (outdatedKind === 'out-of-range') {
618        const outOfRangeContext = []
619        for (const edge of node.edgesIn) {
620          const outOfRangeVersions = greaterVersions.filter((version) => {
621            return !semver.satisfies(version, edge.spec)
622          })
623
624          // this edge has no out-of-range candidates, skip it
625          if (!outOfRangeVersions.length) {
626            continue
627          }
628
629          outOfRangeContext.push({
630            from: edge.from.location,
631            versions: outOfRangeVersions,
632          })
633        }
634
635        // if we didn't add at least one thing to the context, this node is not a match
636        if (!outOfRangeContext.length) {
637          return false
638        }
639
640        // attach the out-of-range context to the node
641        node.queryContext.outdated = {
642          ...node.queryContext.outdated,
643          outOfRange: outOfRangeContext,
644        }
645
646        return node
647      }
648
649      // any other outdatedKind is unknown and will never match
650      return false
651    }))
652
653    // return an array with the holes for non-matching nodes removed
654    return initialResults.filter(Boolean)
655  }
656}
657
658// operators for attribute selectors
659const attributeOperators = {
660  // attribute value is equivalent
661  '=' ({ attr, value }) {
662    return attr === value
663  },
664  // attribute value contains word
665  '~=' ({ attr, value }) {
666    return (attr.match(/\w+/g) || []).includes(value)
667  },
668  // attribute value contains string
669  '*=' ({ attr, value }) {
670    return attr.includes(value)
671  },
672  // attribute value is equal or starts with
673  '|=' ({ attr, value }) {
674    return attr.startsWith(`${value}-`)
675  },
676  // attribute value starts with
677  '^=' ({ attr, value }) {
678    return attr.startsWith(value)
679  },
680  // attribute value ends with
681  '$=' ({ attr, value }) {
682    return attr.endsWith(value)
683  },
684}
685
686const attributeOperator = ({ attr, value, insensitive, operator }) => {
687  if (typeof attr === 'number') {
688    attr = String(attr)
689  }
690  if (typeof attr !== 'string') {
691    // It's an object or an array, bail
692    return false
693  }
694  if (insensitive) {
695    attr = attr.toLowerCase()
696  }
697
698  return attributeOperators[operator]({
699    attr,
700    insensitive,
701    value,
702  })
703}
704
705const attributeMatch = (matcher, obj) => {
706  const insensitive = !!matcher.insensitive
707  const operator = matcher.operator || ''
708  const attribute = matcher.qualifiedAttribute
709  let value = matcher.value || ''
710  // return early if checking existence
711  if (operator === '') {
712    return Boolean(obj[attribute])
713  }
714  if (insensitive) {
715    value = value.toLowerCase()
716  }
717  // in case the current object is an array
718  // then we try to match every item in the array
719  if (Array.isArray(obj[attribute])) {
720    return obj[attribute].find((i, index) => {
721      const attr = obj[attribute][index] || ''
722      return attributeOperator({ attr, value, insensitive, operator })
723    })
724  } else {
725    const attr = obj[attribute] || ''
726    return attributeOperator({ attr, value, insensitive, operator })
727  }
728}
729
730const edgeIsType = (node, type, seen = new Set()) => {
731  for (const edgeIn of node.edgesIn) {
732    // TODO Need a test with an infinite loop
733    if (seen.has(edgeIn)) {
734      continue
735    }
736    seen.add(edgeIn)
737    if (edgeIn.type === type || edgeIn.from[type] || edgeIsType(edgeIn.from, type, seen)) {
738      return true
739    }
740  }
741  return false
742}
743
744const filterByType = (nodes, type) => {
745  const found = []
746  for (const node of nodes) {
747    if (node[type] || edgeIsType(node, type)) {
748      found.push(node)
749    }
750  }
751  return found
752}
753
754const depTypes = {
755  // dependency
756  '.prod' (prevResults) {
757    const found = []
758    for (const node of prevResults) {
759      if (!node.dev) {
760        found.push(node)
761      }
762    }
763    return found
764  },
765  // devDependency
766  '.dev' (prevResults) {
767    return filterByType(prevResults, 'dev')
768  },
769  // optionalDependency
770  '.optional' (prevResults) {
771    return filterByType(prevResults, 'optional')
772  },
773  // peerDependency
774  '.peer' (prevResults) {
775    return filterByType(prevResults, 'peer')
776  },
777  // workspace
778  '.workspace' (prevResults) {
779    return prevResults.filter(node => node.isWorkspace)
780  },
781  // bundledDependency
782  '.bundled' (prevResults) {
783    return prevResults.filter(node => node.inBundle)
784  },
785}
786
787// checks if a given node has a direct parent in any of the nodes provided in
788// the compare nodes array
789const hasParent = (node, compareNodes) => {
790  // All it takes is one so we loop and return on the first hit
791  for (let compareNode of compareNodes) {
792    if (compareNode.isLink) {
793      compareNode = compareNode.target
794    }
795
796    // Follows logical parent for link ancestors (e.g. workspaces whose target lives outside node_modules).
797    // Only match if the node has a link whose parent is the compareNode. Without this check, nodes deep in the store (linked strategy) would incorrectly match as children of root via their fsParent chain.
798    if (node.isTop && (node.resolveParent === compareNode)) {
799      for (const link of node.linksIn) {
800        if (link.parent === compareNode) {
801          return true
802        }
803      }
804    }
805    // follows edges-in to check if they match a possible parent
806    for (const edge of node.edgesIn) {
807      if (edge && edge.from === compareNode) {
808        return true
809      }
810    }
811  }
812  return false
813}
814
815// checks if a given node is a descendant of any of the nodes provided in the
816// compareNodes array
817const hasAscendant = (node, compareNodes, seen = new Set()) => {
818  // TODO (future) loop over ancestry property
819  if (hasParent(node, compareNodes)) {
820    return true
821  }
822
823  if (node.isTop && node.resolveParent) {
824    /* istanbul ignore if - investigate if linksIn check obviates need for this */
825    if (hasAscendant(node.resolveParent, compareNodes)) {
826      return true
827    }
828  }
829  for (const edge of node.edgesIn) {
830    // TODO Need a test with an infinite loop
831    if (seen.has(edge)) {
832      continue
833    }
834    seen.add(edge)
835    if (edge && edge.from && hasAscendant(edge.from, compareNodes, seen)) {
836      return true
837    }
838  }
839  for (const linkNode of node.linksIn) {
840    if (hasAscendant(linkNode, compareNodes, seen)) {
841      return true
842    }
843  }
844  return false
845}
846
847const combinators = {
848  // direct descendant
849  '>' (prevResults, nextResults) {
850    return nextResults.filter(node => hasParent(node, prevResults))
851  },
852  // any descendant
853  ' ' (prevResults, nextResults) {
854    return nextResults.filter(node => hasAscendant(node, prevResults))
855  },
856  // sibling
857  '~' (prevResults, nextResults) {
858    // Return any node in nextResults that is a sibling of (aka shares a
859    // parent with) a node in prevResults
860    const parentNodes = new Set() // Parents of everything in prevResults
861    for (const node of prevResults) {
862      for (const edge of node.edgesIn) {
863        // edge.from always exists cause it's from another node's edgesIn
864        parentNodes.add(edge.from)
865      }
866    }
867    return nextResults.filter(node =>
868      !prevResults.includes(node) && hasParent(node, [...parentNodes])
869    )
870  },
871}
872
873// get a list of available versions of a package filtered to respect --before
874// NOTE: this runs over each node and should not throw
875const getPackageVersions = async (name, opts) => {
876  let packument
877  try {
878    packument = await pacote.packument(name, {
879      ...opts,
880      fullMetadata: false, // we only need the corgi
881    })
882  } catch (err) {
883    // if the fetch fails, log a warning and pretend there are no versions
884    log.warn('query', `could not retrieve packument for ${name}: ${err.message}`)
885    return []
886  }
887
888  // start with a sorted list of all versions (lowest first)
889  let candidates = Object.keys(packument.versions).sort(semver.compare)
890
891  // if the packument has a time property, and the user passed a before flag, then
892  // we filter this list down to only those versions that existed before the specified date
893  if (packument.time && opts.before) {
894    candidates = candidates.filter((version) => {
895      // this version isn't found in the times at all, drop it
896      if (!packument.time[version]) {
897        return false
898      }
899
900      return Date.parse(packument.time[version]) <= opts.before
901    })
902  }
903
904  return candidates
905}
906
907const retrieveNodesFromParsedAst = async (opts) => {
908  // when we first call this it's the parsed query.  all other times it's
909  // results.currentNode.nestedNode
910  const rootAstNode = opts.rootAstNode
911
912  if (!rootAstNode.nodes) {
913    return new Set()
914  }
915
916  const results = new Results(opts)
917
918  const astNodeQueue = new Set()
919  // walk is sync, so we have to build up our async functions and then await them later
920  rootAstNode.walk((nextAstNode) => {
921    astNodeQueue.add(nextAstNode)
922  })
923
924  for (const nextAstNode of astNodeQueue) {
925    // This is the only place we reset currentAstNode
926    results.currentAstNode = nextAstNode
927    const updateFn = `${results.currentAstNode.type}Type`
928    if (typeof results[updateFn] !== 'function') {
929      throw Object.assign(
930        new Error(`\`${results.currentAstNode.type}\` is not a supported selector.`),
931        { code: 'EQUERYNOSELECTOR' }
932      )
933    }
934    await results[updateFn]()
935  }
936
937  return results.collect(rootAstNode)
938}
939
940const querySelectorAll = async (targetNode, query, flatOptions) => {
941  // This never changes ever we just pass it around. But we can't scope it to
942  // this whole file if we ever want to support concurrent calls to this
943  // function.
944  const inventory = [...targetNode.root.inventory.values()]
945  // res is a Set of items returned for each parsed css ast selector
946  const res = await retrieveNodesFromParsedAst({
947    initialItems: inventory,
948    inventory,
949    flatOptions,
950    rootAstNode: parser(query),
951    targetNode,
952  })
953
954  // returns nodes ordered by realpath
955  return [...res].sort((a, b) => localeCompare(a.location, b.location))
956}
957
958module.exports = querySelectorAll
959 
codekingpro/portable-devtools · Team Ai