codekingpro/portable-devtools
114k
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 