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