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') 11const fetch = 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 res.push(new this.#targetNode.constructor({ pkg })) 261 } 262 } 263 return res 264 }, []) 265 } 266 267 async notPseudo () { 268 const res = await retrieveNodesFromParsedAst({ 269 flatOptions: this.flatOptions, 270 initialItems: this.initialItems, 271 inventory: this.#inventory, 272 rootAstNode: this.currentAstNode.nestedNode, 273 targetNode: this.currentAstNode, 274 vulnCache: this.#vulnCache, 275 }) 276 const internalSelector = new Set(res) 277 return this.initialItems.filter(node => 278 !internalSelector.has(node)) 279 } 280 281 overriddenPseudo () { 282 return this.initialItems.filter(node => node.overridden) 283 } 284 285 pathPseudo () { 286 return this.initialItems.filter(node => { 287 if (!this.currentAstNode.pathValue) { 288 return true 289 } 290 return minimatch( 291 node.realpath.replace(/\\+/g, '/'), 292 resolve(node.root.realpath, this.currentAstNode.pathValue).replace(/\\+/g, '/') 293 ) 294 }) 295 } 296 297 privatePseudo () { 298 return this.initialItems.filter(node => node.package.private) 299 } 300 301 rootPseudo () { 302 return this.initialItems.filter(node => node === this.#targetNode.root) 303 } 304 305 scopePseudo () { 306 return this.initialItems.filter(node => node === this.#targetNode) 307 } 308 309 semverPseudo () { 310 const { 311 attributeMatcher, 312 lookupProperties, 313 semverFunc = 'infer', 314 semverValue, 315 } = this.currentAstNode 316 const { qualifiedAttribute } = attributeMatcher 317 318 if (!semverValue) { 319 // DEPRECATED: remove this warning and throw an error as part of @npmcli/arborist@6 320 log.warn('query', 'usage of :semver() with no parameters is deprecated') 321 return this.initialItems 322 } 323 324 if (!semver.valid(semverValue) && !semver.validRange(semverValue)) { 325 throw Object.assign( 326 new Error(`\`${semverValue}\` is not a valid semver version or range`), 327 { code: 'EQUERYINVALIDSEMVER' }) 328 } 329 330 const valueIsVersion = !!semver.valid(semverValue) 331 332 const nodeMatches = (node, obj) => { 333 // if we already have an operator, the user provided some test as part of the selector 334 // we evaluate that first because if it fails we don't want this node anyway 335 if (attributeMatcher.operator) { 336 if (!attributeMatch(attributeMatcher, obj)) { 337 // if the initial operator doesn't match, we're done 338 return false 339 } 340 } 341 342 const attrValue = obj[qualifiedAttribute] 343 // both valid and validRange return null for undefined, so this will skip both nodes that 344 // do not have the attribute defined as well as those where the attribute value is invalid 345 // and those where the value from the package.json is not a string 346 if ((!semver.valid(attrValue) && !semver.validRange(attrValue)) || 347 typeof attrValue !== 'string') { 348 return false 349 } 350 351 const attrIsVersion = !!semver.valid(attrValue) 352 353 let actualFunc = semverFunc 354 355 // if we're asked to infer, we examine outputs to make a best guess 356 if (actualFunc === 'infer') { 357 if (valueIsVersion && attrIsVersion) { 358 // two versions -> semver.eq 359 actualFunc = 'eq' 360 } else if (!valueIsVersion && !attrIsVersion) { 361 // two ranges -> semver.intersects 362 actualFunc = 'intersects' 363 } else { 364 // anything else -> semver.satisfies 365 actualFunc = 'satisfies' 366 } 367 } 368 369 if (['eq', 'neq', 'gt', 'gte', 'lt', 'lte'].includes(actualFunc)) { 370 // both sides must be versions, but one is not 371 if (!valueIsVersion || !attrIsVersion) { 372 return false 373 } 374 375 return semver[actualFunc](attrValue, semverValue) 376 } else if (['gtr', 'ltr', 'satisfies'].includes(actualFunc)) { 377 // at least one side must be a version, but neither is 378 if (!valueIsVersion && !attrIsVersion) { 379 return false 380 } 381 382 return valueIsVersion 383 ? semver[actualFunc](semverValue, attrValue) 384 : semver[actualFunc](attrValue, semverValue) 385 } else if (['intersects', 'subset'].includes(actualFunc)) { 386 // these accept two ranges and since a version is also a range, anything goes 387 return semver[actualFunc](attrValue, semverValue) 388 } else { 389 // user provided a function we don't know about, throw an error 390 throw Object.assign(new Error(`\`semver.${actualFunc}\` is not a supported operator.`), 391 { code: 'EQUERYINVALIDOPERATOR' }) 392 } 393 } 394 395 return this.initialItems.filter((node) => { 396 // no lookupProperties just means its a top level property, see if it matches 397 if (!lookupProperties.length) { 398 return nodeMatches(node, node.package) 399 } 400 401 // this code is mostly duplicated from attrPseudo to traverse into the package until we get 402 // to our deepest requested object 403 let objs = [node.package] 404 for (const prop of lookupProperties) { 405 if (prop === arrayDelimiter) { 406 objs = objs.flat() 407 continue 408 } 409 410 objs = objs.flatMap(obj => obj[prop] || []) 411 const noAttr = objs.every(obj => !obj) 412 if (noAttr) { 413 return false 414 } 415 416 return objs.some(obj => nodeMatches(node, obj)) 417 } 418 }) 419 } 420 421 typePseudo () { 422 if (!this.currentAstNode.typeValue) { 423 return this.initialItems 424 } 425 return this.initialItems 426 .flatMap(node => { 427 const found = [] 428 for (const edge of node.edgesIn) { 429 if (npa(`${edge.name}@${edge.spec}`).type === this.currentAstNode.typeValue) { 430 found.push(edge.to) 431 } 432 } 433 return found 434 }) 435 } 436 437 dedupedPseudo () { 438 return this.initialItems.filter(node => node.target.edgesIn.size > 1) 439 } 440 441 async vulnPseudo () { 442 if (!this.initialItems.length) { 443 return this.initialItems 444 } 445 if (!this.#vulnCache) { 446 const packages = {} 447 // We have to map the items twice, once to get the request, and a second time to filter out the results of that request 448 this.initialItems.map((node) => { 449 if (node.isProjectRoot || node.package.private) { 450 return 451 } 452 if (!packages[node.name]) { 453 packages[node.name] = [] 454 } 455 if (!packages[node.name].includes(node.version)) { 456 packages[node.name].push(node.version) 457 } 458 }) 459 const res = await fetch('/-/npm/v1/security/advisories/bulk', { 460 ...this.flatOptions, 461 registry: this.flatOptions.auditRegistry || this.flatOptions.registry, 462 method: 'POST', 463 gzip: true, 464 body: packages, 465 }) 466 this.#vulnCache = await res.json() 467 } 468 const advisories = this.#vulnCache 469 const { vulns } = this.currentAstNode 470 return this.initialItems.filter(item => { 471 const vulnerable = advisories[item.name]?.filter(advisory => { 472 // This could be for another version of this package elsewhere in the tree 473 if (!semver.intersects(advisory.vulnerable_versions, item.version)) { 474 return false 475 } 476 if (!vulns) { 477 return true 478 } 479 // vulns are OR with each other, if any one matches we're done 480 for (const vuln of vulns) { 481 if (vuln.severity && !vuln.severity.includes('*')) { 482 if (!vuln.severity.includes(advisory.severity)) { 483 continue 484 } 485 } 486 487 if (vuln?.cwe) { 488 // * is special, it means "has a cwe" 489 if (vuln.cwe.includes('*')) { 490 if (!advisory.cwe.length) { 491 continue 492 } 493 } else if (!vuln.cwe.every(cwe => advisory.cwe.includes(`CWE-${cwe}`))) { 494 continue 495 } 496 } 497 return true 498 } 499 }) 500 if (vulnerable?.length) { 501 item.queryContext = { 502 advisories: vulnerable, 503 } 504 return true 505 } 506 return false 507 }) 508 } 509 510 async outdatedPseudo () { 511 const { outdatedKind = 'any' } = this.currentAstNode 512 513 // filter the initialItems 514 // NOTE: this uses a Promise.all around a map without in-line concurrency handling 515 // since the only async action taken is retrieving the packument, which is limited 516 // based on the max-sockets config in make-fetch-happen 517 const initialResults = await Promise.all(this.initialItems.map(async (node) => { 518 // the root can't be outdated, skip it 519 if (node.isProjectRoot) { 520 return false 521 } 522 523 // private packages can't be published, skip them 524 if (node.package.private) { 525 return false 526 } 527 528 // we cache the promise representing the full versions list, this helps reduce the 529 // number of requests we send by keeping population of the cache in a single tick 530 // making it less likely that multiple requests for the same package will be inflight 531 if (!this.#outdatedCache.has(node.name)) { 532 this.#outdatedCache.set(node.name, getPackageVersions(node.name, this.flatOptions)) 533 } 534 const availableVersions = await this.#outdatedCache.get(node.name) 535 536 // we attach _all_ versions to the queryContext to allow consumers to do their own 537 // filtering and comparisons 538 node.queryContext.versions = availableVersions 539 540 // next we further reduce the set to versions that are greater than the current one 541 const greaterVersions = availableVersions.filter((available) => { 542 return semver.gt(available, node.version) 543 }) 544 545 // no newer versions than the current one, drop this node from the result set 546 if (!greaterVersions.length) { 547 return false 548 } 549 550 // if we got here, we know that newer versions exist, if the kind is 'any' we're done 551 if (outdatedKind === 'any') { 552 return node 553 } 554 555 // look for newer versions that differ from current by a specific part of the semver version 556 if (['major', 'minor', 'patch'].includes(outdatedKind)) { 557 // filter the versions greater than our current one based on semver.diff 558 const filteredVersions = greaterVersions.filter((version) => { 559 return semver.diff(node.version, version) === outdatedKind 560 }) 561 562 // no available versions are of the correct diff type 563 if (!filteredVersions.length) { 564 return false 565 } 566 567 return node 568 } 569 570 // look for newer versions that satisfy at least one edgeIn to this node 571 if (outdatedKind === 'in-range') { 572 const inRangeContext = [] 573 for (const edge of node.edgesIn) { 574 const inRangeVersions = greaterVersions.filter((version) => { 575 return semver.satisfies(version, edge.spec) 576 }) 577 578 // this edge has no in-range candidates, just move on 579 if (!inRangeVersions.length) { 580 continue 581 } 582 583 inRangeContext.push({ 584 from: edge.from.location, 585 versions: inRangeVersions, 586 }) 587 } 588 589 // if we didn't find at least one match, drop this node 590 if (!inRangeContext.length) { 591 return false 592 } 593 594 // now add to the context each version that is in-range for each edgeIn 595 node.queryContext.outdated = { 596 ...node.queryContext.outdated, 597 inRange: inRangeContext, 598 } 599 600 return node 601 } 602 603 // look for newer versions that _do not_ satisfy at least one edgeIn 604 if (outdatedKind === 'out-of-range') { 605 const outOfRangeContext = [] 606 for (const edge of node.edgesIn) { 607 const outOfRangeVersions = greaterVersions.filter((version) => { 608 return !semver.satisfies(version, edge.spec) 609 }) 610 611 // this edge has no out-of-range candidates, skip it 612 if (!outOfRangeVersions.length) { 613 continue 614 } 615 616 outOfRangeContext.push({ 617 from: edge.from.location, 618 versions: outOfRangeVersions, 619 }) 620 } 621 622 // if we didn't add at least one thing to the context, this node is not a match 623 if (!outOfRangeContext.length) { 624 return false 625 } 626 627 // attach the out-of-range context to the node 628 node.queryContext.outdated = { 629 ...node.queryContext.outdated, 630 outOfRange: outOfRangeContext, 631 } 632 633 return node 634 } 635 636 // any other outdatedKind is unknown and will never match 637 return false 638 })) 639 640 // return an array with the holes for non-matching nodes removed 641 return initialResults.filter(Boolean) 642 } 643} 644 645// operators for attribute selectors 646const attributeOperators = { 647 // attribute value is equivalent 648 '=' ({ attr, value, insensitive }) { 649 return attr === value 650 }, 651 // attribute value contains word 652 '~=' ({ attr, value, insensitive }) { 653 return (attr.match(/\w+/g) || []).includes(value) 654 }, 655 // attribute value contains string 656 '*=' ({ attr, value, insensitive }) { 657 return attr.includes(value) 658 }, 659 // attribute value is equal or starts with 660 '|=' ({ attr, value, insensitive }) { 661 return attr.startsWith(`${value}-`) 662 }, 663 // attribute value starts with 664 '^=' ({ attr, value, insensitive }) { 665 return attr.startsWith(value) 666 }, 667 // attribute value ends with 668 '$=' ({ attr, value, insensitive }) { 669 return attr.endsWith(value) 670 }, 671} 672 673const attributeOperator = ({ attr, value, insensitive, operator }) => { 674 if (typeof attr === 'number') { 675 attr = String(attr) 676 } 677 if (typeof attr !== 'string') { 678 // It's an object or an array, bail 679 return false 680 } 681 if (insensitive) { 682 attr = attr.toLowerCase() 683 } 684 685 return attributeOperators[operator]({ 686 attr, 687 insensitive, 688 value, 689 }) 690} 691 692const attributeMatch = (matcher, obj) => { 693 const insensitive = !!matcher.insensitive 694 const operator = matcher.operator || '' 695 const attribute = matcher.qualifiedAttribute 696 let value = matcher.value || '' 697 // return early if checking existence 698 if (operator === '') { 699 return Boolean(obj[attribute]) 700 } 701 if (insensitive) { 702 value = value.toLowerCase() 703 } 704 // in case the current object is an array 705 // then we try to match every item in the array 706 if (Array.isArray(obj[attribute])) { 707 return obj[attribute].find((i, index) => { 708 const attr = obj[attribute][index] || '' 709 return attributeOperator({ attr, value, insensitive, operator }) 710 }) 711 } else { 712 const attr = obj[attribute] || '' 713 return attributeOperator({ attr, value, insensitive, operator }) 714 } 715} 716 717const edgeIsType = (node, type, seen = new Set()) => { 718 for (const edgeIn of node.edgesIn) { 719 // TODO Need a test with an infinite loop 720 if (seen.has(edgeIn)) { 721 continue 722 } 723 seen.add(edgeIn) 724 if (edgeIn.type === type || edgeIn.from[type] || edgeIsType(edgeIn.from, type, seen)) { 725 return true 726 } 727 } 728 return false 729} 730 731const filterByType = (nodes, type) => { 732 const found = [] 733 for (const node of nodes) { 734 if (node[type] || edgeIsType(node, type)) { 735 found.push(node) 736 } 737 } 738 return found 739} 740 741const depTypes = { 742 // dependency 743 '.prod' (prevResults) { 744 const found = [] 745 for (const node of prevResults) { 746 if (!node.dev) { 747 found.push(node) 748 } 749 } 750 return found 751 }, 752 // devDependency 753 '.dev' (prevResults) { 754 return filterByType(prevResults, 'dev') 755 }, 756 // optionalDependency 757 '.optional' (prevResults) { 758 return filterByType(prevResults, 'optional') 759 }, 760 // peerDependency 761 '.peer' (prevResults) { 762 return filterByType(prevResults, 'peer') 763 }, 764 // workspace 765 '.workspace' (prevResults) { 766 return prevResults.filter(node => node.isWorkspace) 767 }, 768 // bundledDependency 769 '.bundled' (prevResults) { 770 return prevResults.filter(node => node.inBundle) 771 }, 772} 773 774// checks if a given node has a direct parent in any of the nodes provided in 775// the compare nodes array 776const hasParent = (node, compareNodes) => { 777 // All it takes is one so we loop and return on the first hit 778 for (let compareNode of compareNodes) { 779 if (compareNode.isLink) { 780 compareNode = compareNode.target 781 } 782 783 // follows logical parent for link anscestors 784 if (node.isTop && (node.resolveParent === compareNode)) { 785 return true 786 } 787 // follows edges-in to check if they match a possible parent 788 for (const edge of node.edgesIn) { 789 if (edge && edge.from === compareNode) { 790 return true 791 } 792 } 793 } 794 return false 795} 796 797// checks if a given node is a descendant of any of the nodes provided in the 798// compareNodes array 799const hasAscendant = (node, compareNodes, seen = new Set()) => { 800 // TODO (future) loop over ancestry property 801 if (hasParent(node, compareNodes)) { 802 return true 803 } 804 805 if (node.isTop && node.resolveParent) { 806 /* istanbul ignore if - investigate if linksIn check obviates need for this */ 807 if (hasAscendant(node.resolveParent, compareNodes)) { 808 return true 809 } 810 } 811 for (const edge of node.edgesIn) { 812 // TODO Need a test with an infinite loop 813 if (seen.has(edge)) { 814 continue 815 } 816 seen.add(edge) 817 if (edge && edge.from && hasAscendant(edge.from, compareNodes, seen)) { 818 return true 819 } 820 } 821 for (const linkNode of node.linksIn) { 822 if (hasAscendant(linkNode, compareNodes, seen)) { 823 return true 824 } 825 } 826 return false 827} 828 829const combinators = { 830 // direct descendant 831 '>' (prevResults, nextResults) { 832 return nextResults.filter(node => hasParent(node, prevResults)) 833 }, 834 // any descendant 835 ' ' (prevResults, nextResults) { 836 return nextResults.filter(node => hasAscendant(node, prevResults)) 837 }, 838 // sibling 839 '~' (prevResults, nextResults) { 840 // Return any node in nextResults that is a sibling of (aka shares a 841 // parent with) a node in prevResults 842 const parentNodes = new Set() // Parents of everything in prevResults 843 for (const node of prevResults) { 844 for (const edge of node.edgesIn) { 845 // edge.from always exists cause it's from another node's edgesIn 846 parentNodes.add(edge.from) 847 } 848 } 849 return nextResults.filter(node => 850 !prevResults.includes(node) && hasParent(node, [...parentNodes]) 851 ) 852 }, 853} 854 855// get a list of available versions of a package filtered to respect --before 856// NOTE: this runs over each node and should not throw 857const getPackageVersions = async (name, opts) => { 858 let packument 859 try { 860 packument = await pacote.packument(name, { 861 ...opts, 862 fullMetadata: false, // we only need the corgi 863 }) 864 } catch (err) { 865 // if the fetch fails, log a warning and pretend there are no versions 866 log.warn('query', `could not retrieve packument for ${name}: ${err.message}`) 867 return [] 868 } 869 870 // start with a sorted list of all versions (lowest first) 871 let candidates = Object.keys(packument.versions).sort(semver.compare) 872 873 // if the packument has a time property, and the user passed a before flag, then 874 // we filter this list down to only those versions that existed before the specified date 875 if (packument.time && opts.before) { 876 candidates = candidates.filter((version) => { 877 // this version isn't found in the times at all, drop it 878 if (!packument.time[version]) { 879 return false 880 } 881 882 return Date.parse(packument.time[version]) <= opts.before 883 }) 884 } 885 886 return candidates 887} 888 889const retrieveNodesFromParsedAst = async (opts) => { 890 // when we first call this it's the parsed query. all other times it's 891 // results.currentNode.nestedNode 892 const rootAstNode = opts.rootAstNode 893 894 if (!rootAstNode.nodes) { 895 return new Set() 896 } 897 898 const results = new Results(opts) 899 900 const astNodeQueue = new Set() 901 // walk is sync, so we have to build up our async functions and then await them later 902 rootAstNode.walk((nextAstNode) => { 903 astNodeQueue.add(nextAstNode) 904 }) 905 906 for (const nextAstNode of astNodeQueue) { 907 // This is the only place we reset currentAstNode 908 results.currentAstNode = nextAstNode 909 const updateFn = `${results.currentAstNode.type}Type` 910 if (typeof results[updateFn] !== 'function') { 911 throw Object.assign( 912 new Error(`\`${results.currentAstNode.type}\` is not a supported selector.`), 913 { code: 'EQUERYNOSELECTOR' } 914 ) 915 } 916 await results[updateFn]() 917 } 918 919 return results.collect(rootAstNode) 920} 921 922const querySelectorAll = async (targetNode, query, flatOptions) => { 923 // This never changes ever we just pass it around. But we can't scope it to 924 // this whole file if we ever want to support concurrent calls to this 925 // function. 926 const inventory = [...targetNode.root.inventory.values()] 927 // res is a Set of items returned for each parsed css ast selector 928 const res = await retrieveNodesFromParsedAst({ 929 initialItems: inventory, 930 inventory, 931 flatOptions, 932 rootAstNode: parser(query), 933 targetNode, 934 }) 935 936 // returns nodes ordered by realpath 937 return [...res].sort((a, b) => localeCompare(a.location, b.location)) 938} 939 940module.exports = querySelectorAll 941