Files
renovate/lib/modules/versioning/generic-range.ts
T
Aaron Paterson 6a1a69c3ef feat(versioning): support ranges for the regex scheme (#44834)
* feat(versioning): comparator ranges for the regex scheme

The regex versioning orders versions by the release array built from its
captured groups (major, minor, patch, build, revision) but matches every range
by exact equality, so a comparator range in `allowedVersions` (`<=1.2.3` and
friends) never resolves against a regex-versioned dependency.

Add a scheme-independent comparator parser (`generic-range.ts`) and wire it
into the regex scheme's `isValid`/`matches`/`getSatisfyingVersion`/
`minSatisfyingVersion`, evaluating each `<`, `<=`, `>`, `>=`, `=`, `==` term
through the scheme's own `_compare`. A pattern that captures a `-ee.N` suffix
as an ordered `build` component then resolves `<=X-ee.0` correctly, where npm
`semver` cannot: as a prerelease the suffix breaks the range, as build metadata
it is ignored, and a four-part release is not valid semver.

The parser defers operand validation and comparison to the caller, so it stays
independent of npm `semver` (used by the semver family) and consistent with the
scheme's own ordering. Dependency values stay exact tags, so `supportsRanges`
remains `false`.

* test(versioning): cover regex ordering by capture-group name, not position

Add a calendar-versioning case (`YYYY.DD.MM`) showing comparison follows the `major`/`minor`/`patch` capture-group names in precedence order, not their left-to-right position: naming the month `minor` and the day `patch` sorts by year, then month, then day even though the day is the middle field.

Also reword the readme, which described this as "the same capture-group ordering" and read as positional, to state the major/minor/patch precedence explicitly.

* refactor(versioning): declare RangeComparator in types.ts

The `renovate/types-location` rule requires an exported type to live in a
colocated `types.ts`, so `RangeComparator` is declared there and
`generic-range.ts` imports it.
2026-09-20 18:21:28 +00:00

65 lines
2.0 KiB
TypeScript

import { isNonEmptyString } from '@sindresorhus/is';
import { regEx } from '../../util/regex.ts';
import type { RangeComparator } from './types.ts';
// Comparator operators, longest first so `<=` is matched ahead of `<`, `>=`
// ahead of `>`, and `==` ahead of `=`.
const operators = ['<=', '>=', '==', '<', '>', '='];
const separator = regEx(/[\s,]+/);
/**
* Parse a comparator range (for example `<=1.2.3`, `>1.0 <2.0`, or `=1.2.3`)
* into its `{ operator, version }` terms, which are ANDed together. Returns
* `null` when the input is not a comparator range (such as a bare version), so
* callers can fall back to treating it as a version.
*
* The `isVersion` predicate validates each operand against the caller's own
* version format and the caller compares with its own ordering, so this parser
* stays independent of any single versioning scheme (unlike npm `semver`,
* whose parser also coerces and normalizes the operands).
*/
export function parseRange(
range: string,
isVersion: (version: string) => boolean,
): RangeComparator[] | null {
if (!isNonEmptyString(range)) {
return null;
}
const comparators: RangeComparator[] = [];
for (const term of range.trim().split(separator)) {
const operator = operators.find((op) => term.startsWith(op));
if (!operator) {
return null;
}
const version = term.slice(operator.length);
if (!isVersion(version)) {
return null;
}
comparators.push({ operator, version });
}
return comparators;
}
/**
* Given the result of comparing a version against a comparator's version
* (negative when lower, zero when equal, positive when higher), return whether
* the comparator's operator is satisfied.
*/
export function satisfiesComparator(
comparison: number,
operator: string,
): boolean {
switch (operator) {
case '<':
return comparison < 0;
case '<=':
return comparison <= 0;
case '>':
return comparison > 0;
case '>=':
return comparison >= 0;
default:
return comparison === 0;
}
}