Skip to content

Commit ce7acf6

Browse files
committed
feat(arborist): allow for selectors and function names with :semver pseudo selector
1 parent 7497274 commit ce7acf6

File tree

2 files changed

+214
-3
lines changed

2 files changed

+214
-3
lines changed

workspaces/arborist/lib/query-selector-all.js

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,11 +291,90 @@ class Results {
291291
}
292292

293293
semverPseudo () {
294-
if (!this.currentAstNode.semverValue) {
294+
const { lookupProperties, attributeMatcher, semverFunc, semverValue } = this.currentAstNode
295+
if (!semverValue || (!semver.valid(semverValue) && !semver.validRange(semverValue))) {
295296
return this.initialItems
296297
}
297-
return this.initialItems.filter(node =>
298-
semver.satisfies(node.version, this.currentAstNode.semverValue))
298+
299+
const valueIsVersion = !!semver.valid(semverValue)
300+
301+
const nodeMatches = (node, obj) => {
302+
// if we already have an operator, the user provided some test as part of the selector
303+
// we evaluate that first because if it fails we don't want this node anyway
304+
if (attributeMatcher.operator) {
305+
if (!attributeMatch(attributeMatcher, obj)) {
306+
// if the initial operator doesn't match, we're done
307+
return false
308+
}
309+
}
310+
311+
// read the value from the provided object
312+
const foundValue = obj[attributeMatcher.qualifiedAttribute]
313+
// if the value is not a valid semver version or range, we can't match so we're done
314+
if (!foundValue || (!semver.valid(foundValue) && !semver.validRange(foundValue))) {
315+
return false
316+
}
317+
const foundIsVersion = !!semver.valid(foundValue)
318+
319+
// make a new matcher inline to do the semver checks
320+
const semverMatcher = {
321+
qualifiedAttribute: attributeMatcher.qualifiedAttribute,
322+
value: semverValue,
323+
}
324+
325+
// if we were provided an operator function, see if we can use that
326+
if (semverFunc) {
327+
// these functions each need 2 versions _not_ ranges or they'll throw, so we skip any nodes
328+
// that would cause an error to be thrown
329+
if (['eq', 'neq', 'gt', 'gte', 'lt', 'lte'].includes(semverFunc)) {
330+
if (!valueIsVersion || !foundIsVersion) {
331+
return false
332+
}
333+
}
334+
335+
// since versions are valid ranges, any other supported function is fine
336+
semverMatcher.operator = `semver.${semverFunc}`
337+
} else {
338+
// otherwise infer what type of operator we need based on the types of the two values
339+
if (foundIsVersion && valueIsVersion) {
340+
// 2 versions
341+
semverMatcher.operator = 'semver.eq'
342+
} else if (!foundIsVersion && !valueIsVersion) {
343+
// 2 ranges
344+
semverMatcher.operator = 'semver.intersects'
345+
} else {
346+
// 1 version, 1 range
347+
semverMatcher.operator = 'semver.satisfies'
348+
}
349+
}
350+
351+
return attributeMatch(semverMatcher, obj)
352+
}
353+
354+
return this.initialItems.filter((node) => {
355+
// no lookupProperties just means its a top level property, see if it matches
356+
if (!lookupProperties.length) {
357+
return nodeMatches(node, node.package)
358+
}
359+
360+
// this code is mostly duplicated from attrPseudo to traverse into the package until we get
361+
// to our deepest requested object
362+
let objs = [node.package]
363+
for (const prop of lookupProperties) {
364+
if (prop === arrayDelimiter) {
365+
objs = objs.flat()
366+
continue
367+
}
368+
369+
objs = objs.flatMap(obj => obj[prop] || [])
370+
const noAttr = objs.every(obj => !obj)
371+
if (noAttr) {
372+
return false
373+
}
374+
375+
return objs.some(obj => nodeMatches(node, obj))
376+
}
377+
})
299378
}
300379

301380
typePseudo () {
@@ -345,6 +424,50 @@ const attributeOperators = {
345424
'$=' ({ attr, value, insensitive }) {
346425
return attr.endsWith(value)
347426
},
427+
// semver comparisons, these are internal operators used by semverPseudo
428+
// functions that take 2 versions
429+
'semver.eq' ({ attr, value }) {
430+
return semver.eq(attr, value)
431+
},
432+
'semver.neq' ({ attr, value }) {
433+
return semver.neq(attr, value)
434+
},
435+
'semver.gt' ({ attr, value }) {
436+
return semver.gt(attr, value)
437+
},
438+
'semver.gte' ({ attr, value }) {
439+
return semver.gte(attr, value)
440+
},
441+
'semver.lt' ({ attr, value }) {
442+
return semver.lt(attr, value)
443+
},
444+
'semver.lte' ({ attr, value }) {
445+
return semver.lte(attr, value)
446+
},
447+
// functions that take 2 ranges
448+
'semver.intersects' ({ attr, value }) {
449+
return semver.intersects(attr, value)
450+
},
451+
'semver.subset' ({ attr, value }) {
452+
return semver.subset(attr, value)
453+
},
454+
// functions that take 1 version and 1 range, for these we infer the order to pass
455+
// parameters on the user's behalf
456+
'semver.gtr' ({ attr, value }) {
457+
return semver.valid(attr)
458+
? semver.gtr(attr, value)
459+
: semver.gtr(value, attr)
460+
},
461+
'semver.ltr' ({ attr, value }) {
462+
return semver.valid(attr)
463+
? semver.ltr(attr, value)
464+
: semver.ltr(value, attr)
465+
},
466+
'semver.satisfies' ({ attr, value }) {
467+
return semver.valid(attr)
468+
? semver.satisfies(attr, value)
469+
: semver.satisfies(value, attr)
470+
},
348471
}
349472

350473
const attributeOperator = ({ attr, value, insensitive, operator }) => {
@@ -358,6 +481,13 @@ const attributeOperator = ({ attr, value, insensitive, operator }) => {
358481
if (insensitive) {
359482
attr = attr.toLowerCase()
360483
}
484+
485+
if (!attributeOperators[operator]) {
486+
throw Object.assign(
487+
new Error(`\`${operator}\` is not a supported operator.`),
488+
{ code: 'EQUERYINVALIDOPERATOR' })
489+
}
490+
361491
return attributeOperators[operator]({
362492
attr,
363493
insensitive,

workspaces/arborist/test/query-selector-all.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ t.test('query-selector-all', async t => {
5252
name: 'abbrev',
5353
version: '1.1.1',
5454
license: 'ISC',
55+
engines: {
56+
node: '^16.0.0',
57+
},
5558
}),
5659
},
5760
b: t.fixture('symlink', '../b'),
@@ -62,6 +65,9 @@ t.test('query-selector-all', async t => {
6265
dependencies: {
6366
moo: '3.0.0',
6467
},
68+
engines: {
69+
node: '>= 14.0.0',
70+
},
6571
arbitrary: {
6672
foo: [
6773
false,
@@ -89,6 +95,10 @@ t.test('query-selector-all', async t => {
8995
scripts: {
9096
test: 'tap',
9197
},
98+
engines: {
99+
// intentionally invalid range
100+
node: 'nope',
101+
},
92102
}),
93103
},
94104
foo: {
@@ -254,6 +264,12 @@ t.test('query-selector-all', async t => {
254264
'should throw in invalid selector'
255265
)
256266

267+
t.rejects(
268+
q(tree, ':semver(1.0.0, [version], eqqq)'),
269+
{ code: 'EQUERYINVALIDOPERATOR' },
270+
'should throw on invalid semver operator'
271+
)
272+
257273
// :scope pseudo
258274
const [nodeFoo] = await q(tree, '#foo')
259275
const scopeRes = await querySelectorAll(nodeFoo, ':scope')
@@ -559,6 +575,71 @@ t.test('query-selector-all', async t => {
559575
]],
560576
[':semver(=1.4.0)', ['[email protected]']],
561577
[':semver(1.4.0 || 2.2.2)', ['[email protected]', '[email protected]']],
578+
[':semver(^16.0.0, :attr(engines, [node]))', ['[email protected]', '[email protected]']],
579+
[':semver(18.0.0, :attr(engines, [node]))', ['[email protected]']],
580+
[':semver(^16.0.0, :attr(engines, [node^=">="]))', ['[email protected]']],
581+
[':semver(3.0.0, [version], eq)', ['[email protected]']],
582+
[':semver(^3.0.0, [version], eq)', []],
583+
[':semver(1.0.0, [version], neq)', [
584+
'@npmcli/[email protected]',
585+
586+
587+
588+
589+
590+
591+
]],
592+
[':semver(^1.0.0, [version], neq)', []],
593+
[':semver(2.0.0, [version], gt)', ['[email protected]', '[email protected]']],
594+
[':semver(^2.0.0, [version], gt)', []],
595+
[':semver(2.0.0, [version], gte)', [
596+
597+
598+
599+
600+
]],
601+
[':semver(^2.0.0, [version], gte)', []],
602+
[':semver(1.1.1, [version], lt)', [
603+
604+
605+
606+
607+
608+
'ipsum@npm:[email protected]',
609+
610+
611+
612+
]],
613+
[':semver(^1.1.1, [version], lt)', []],
614+
[':semver(1.1.1, [version], lte)', [
615+
616+
617+
618+
619+
620+
621+
'ipsum@npm:[email protected]',
622+
623+
624+
625+
]],
626+
[':semver(^1.1.1, [version], lte)', []],
627+
[':semver(^14.0.0, :attr(engines, [node]), intersects)', ['[email protected]']],
628+
[':semver(>=14, :attr(engines, [node]), subset)', ['[email protected]', '[email protected]']],
629+
[':semver(^2.0.0, [version], gtr)', ['[email protected]']],
630+
[':semver(20.0.0, :attr(engines, [node]), gtr)', ['[email protected]']],
631+
[':semver(^1.1.1, [version], ltr)', [
632+
633+
634+
635+
636+
637+
'ipsum@npm:[email protected]',
638+
639+
640+
641+
]],
642+
[':semver(0.0.1, :attr(engines, [node]), ltr)', ['[email protected]', '[email protected]']],
562643

563644
// attr pseudo
564645
[':attr([name=dasher])', ['[email protected]']],

0 commit comments

Comments
 (0)