diff --git a/README.md b/README.md index 2e7fbed..d2d6753 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,18 @@ Range APIs accept strings or mutable `SemVerRange` objects. They support comparators, unions, hyphens, wildcards, tilde, caret, loose parsing, and prereleases. +Range options are fixed when a `SemVerRange` is created. APIs that receive a +parsed range use its stored options and do not accept another options argument: + +```ts +const prereleases = parseRange('1.x', { includePrerelease: true }) + +satisfies('1.0.0-rc.1', prereleases) // true +``` + +To use different options, pass the original range string again or create +another parsed range. + ## API See the [API reference](https://npmx.dev/package-docs/verkit). @@ -122,12 +134,16 @@ such as `clean`, `coerce`, `compare`, and `satisfies` are omitted. is `normalize`. Use `isValid` when you only need a boolean. Use options objects such as `{ loose: true }` and `{ identifier, identifierBase }`. +Range options belong to the string-parsing step; parsed `SemVerRange` objects +already contain them. ## Differences from node-semver -verkit follows [node-semver] semantics with three user-visible differences: +verkit follows [node-semver] semantics with four user-visible differences: - Array helpers never mutate their inputs. +- Parsed `SemVerRange` objects retain their parse-time options. node-semver + helpers may reparse a `Range` from `raw` using call-site options. - verkit is ESM-only, with no CommonJS, CLI, or `NODE_DEBUG=semver` output. - Error text, stack traces, and supported runtimes may differ. diff --git a/__snapshots__/tsnapi/index.snapshot.d.ts b/__snapshots__/tsnapi/index.snapshot.d.ts index e9e75da..263d619 100644 --- a/__snapshots__/tsnapi/index.snapshot.d.ts +++ b/__snapshots__/tsnapi/index.snapshot.d.ts @@ -62,9 +62,12 @@ export declare function comparePrerelease(_: VersionInput, _: VersionInput, _?: export declare function compareReversed(_: VersionInput, _: VersionInput, _?: VersionOptions): Comparison; export declare function compareWithOperator(_: VersionInput, _: ComparisonOperator, _: VersionInput, _?: VersionOptions): boolean; export declare function difference(_: VersionInput, _: VersionInput): VersionDifference | null; -export declare function findMaxSatisfying(_: readonly T[], _: RangeInput, _?: RangeOptions): T | null; -export declare function findMinimumForRange(_: RangeInput, _?: RangeOptions): string | null; -export declare function findMinSatisfying(_: readonly T[], _: RangeInput, _?: RangeOptions): T | null; +export declare function findMaxSatisfying(_: readonly T[], _: string, _?: RangeOptions): T | null; +export declare function findMaxSatisfying(_: readonly T[], _: RangeInput): T | null; +export declare function findMinimumForRange(_: string, _?: RangeOptions): string | null; +export declare function findMinimumForRange(_: RangeInput): string | null; +export declare function findMinSatisfying(_: readonly T[], _: string, _?: RangeOptions): T | null; +export declare function findMinSatisfying(_: readonly T[], _: RangeInput): T | null; export declare function getBuild(_: VersionInput, _?: VersionOptions): string[] | null; export declare function getMajor(_: VersionInput, _?: VersionOptions): number; export declare function getMinor(_: VersionInput, _?: VersionOptions): number; @@ -74,31 +77,43 @@ export declare function increment(_: VersionInput, _: IncrementType, _?: Increme export declare function isEqual(_: VersionInput, _: VersionInput, _?: VersionOptions): boolean; export declare function isGreater(_: VersionInput, _: VersionInput, _?: VersionOptions): boolean; export declare function isGreaterOrEqual(_: VersionInput, _: VersionInput, _?: VersionOptions): boolean; -export declare function isGreaterThanRange(_: VersionInput, _: RangeInput, _?: RangeOptions): boolean; +export declare function isGreaterThanRange(_: VersionInput, _: string, _?: RangeOptions): boolean; +export declare function isGreaterThanRange(_: VersionInput, _: RangeInput): boolean; export declare function isLess(_: VersionInput, _: VersionInput, _?: VersionOptions): boolean; export declare function isLessOrEqual(_: VersionInput, _: VersionInput, _?: VersionOptions): boolean; -export declare function isLessThanRange(_: VersionInput, _: RangeInput, _?: RangeOptions): boolean; +export declare function isLessThanRange(_: VersionInput, _: string, _?: RangeOptions): boolean; +export declare function isLessThanRange(_: VersionInput, _: RangeInput): boolean; export declare function isNotEqual(_: VersionInput, _: VersionInput, _?: VersionOptions): boolean; -export declare function isOutsideRange(_: VersionInput, _: RangeInput, _: RangeDirection, _?: RangeOptions): boolean; -export declare function isRangeSubset(_: RangeInput, _: RangeInput, _?: RangeOptions): boolean; +export declare function isOutsideRange(_: VersionInput, _: string, _: RangeDirection, _?: RangeOptions): boolean; +export declare function isOutsideRange(_: VersionInput, _: RangeInput, _: RangeDirection): boolean; +export declare function isRangeSubset(_: string, _: string, _?: RangeOptions): boolean; +export declare function isRangeSubset(_: RangeInput, _: RangeInput): boolean; export declare function isValid(_: VersionInput, _?: VersionOptions): boolean; -export declare function isValidRange(_: RangeInput, _?: RangeOptions): boolean; +export declare function isValidRange(_: string, _?: RangeOptions): boolean; +export declare function isValidRange(_: RangeInput): boolean; export declare function normalize(_: VersionInput, _?: VersionOptions): string | null; export declare function normalizeComparator(_: string, _?: RangeOptions): string; export declare function normalizeFull(_: VersionInput, _?: VersionOptions): string | null; -export declare function normalizeRange(_: RangeInput, _?: RangeOptions): string | null; +export declare function normalizeRange(_: string, _?: RangeOptions): string | null; +export declare function normalizeRange(_: RangeInput): string | null; export declare function parse(_: VersionInput, _?: VersionOptions): SemVer; -export declare function parseRange(_: RangeInput, _?: RangeOptions): SemVerRange; -export declare function rangesIntersect(_: RangeInput, _: RangeInput, _?: RangeOptions): boolean; -export declare function rangeToComparators(_: RangeInput, _?: RangeOptions): string[][]; -export declare function satisfies(_: VersionInput, _: RangeInput, _?: RangeOptions): boolean; +export declare function parseRange(_: string, _?: RangeOptions): SemVerRange; +export declare function parseRange(_: RangeInput): SemVerRange; +export declare function rangesIntersect(_: string, _: string, _?: RangeOptions): boolean; +export declare function rangesIntersect(_: RangeInput, _: RangeInput): boolean; +export declare function rangeToComparators(_: string, _?: RangeOptions): string[][]; +export declare function rangeToComparators(_: RangeInput): string[][]; +export declare function satisfies(_: VersionInput, _: string, _?: RangeOptions): boolean; +export declare function satisfies(_: VersionInput, _: RangeInput): boolean; export declare function satisfiesComparator(_: VersionInput, _: string, _?: RangeOptions): boolean; -export declare function simplifyRange(_: readonly T[], _: RangeInput, _?: RangeOptions): string; +export declare function simplifyRange(_: readonly T[], _: string, _?: RangeOptions): string; +export declare function simplifyRange(_: readonly T[], _: RangeInput): string; export declare function sort(_: readonly T[], _?: VersionOptions): T[]; export declare function sortReversed(_: readonly T[], _?: VersionOptions): T[]; export declare function truncate(_: VersionInput, _: TruncationType, _?: VersionOptions): string | null; export declare function tryParse(_: VersionInput, _?: VersionOptions): SemVer | null; -export declare function tryParseRange(_: RangeInput, _?: RangeOptions): SemVerRange | null; +export declare function tryParseRange(_: string, _?: RangeOptions): SemVerRange | null; +export declare function tryParseRange(_: RangeInput): SemVerRange | null; // #endregion // #region Variables diff --git a/src/internal/range.ts b/src/internal/range.ts index 2534b28..5b4cb56 100644 --- a/src/internal/range.ts +++ b/src/internal/range.ts @@ -30,6 +30,11 @@ export interface SemVerRange { export type RangeInput = SemVerRange | string +export type ParseRangeInput = ( + range: RangeInput, + options?: RangeOptions, +) => SemVerRange + const BUILD_STRIP = new RegExp(BUILD, 'g') const BUILD_SAFE = safeRegex(BUILD) const STRICT_HYPHEN = safeRegex( @@ -300,6 +305,8 @@ function parseSimpleRange( return [...unique.values()] } +export function parseRange(range: string, options?: RangeOptions): SemVerRange +export function parseRange(range: RangeInput): SemVerRange export function parseRange( range: RangeInput, options: RangeOptions = {}, @@ -335,12 +342,17 @@ export function parseRange( } } +export function tryParseRange( + range: string, + options?: RangeOptions, +): SemVerRange | null +export function tryParseRange(range: RangeInput): SemVerRange | null export function tryParseRange( range: RangeInput, options: RangeOptions = {}, ): SemVerRange | null { try { - return parseRange(range, options) + return (parseRange as ParseRangeInput)(range, options) } catch { return null } diff --git a/src/range.ts b/src/range.ts index bcb52ba..53e8275 100644 --- a/src/range.ts +++ b/src/range.ts @@ -6,7 +6,9 @@ import { testParsedRange, testRangeVersion, tryParseRange, + type ParseRangeInput, type RangeInput, + type SemVerRange, } from './internal/range.ts' import { compareParsed, @@ -25,35 +27,65 @@ import type { export { parseRange, tryParseRange } export type { RangeInput, SemVerRange } from './internal/range.ts' +type TryParseRangeInput = ( + range: RangeInput, + options?: RangeOptions, +) => SemVerRange | null + +type IsOutsideRangeInput = ( + version: VersionInput, + range: RangeInput, + direction: RangeDirection, + options?: RangeOptions, +) => boolean + +export function isValidRange(range: string, options?: RangeOptions): boolean +export function isValidRange(range: RangeInput): boolean export function isValidRange( range: RangeInput, options: RangeOptions = {}, ): boolean { - return tryParseRange(range, options) !== null + return (tryParseRange as TryParseRangeInput)(range, options) !== null } +export function normalizeRange( + range: string, + options?: RangeOptions, +): string | null +export function normalizeRange(range: RangeInput): string | null export function normalizeRange( range: RangeInput, options: RangeOptions = {}, ): string | null { - const parsed = tryParseRange(range, options) + const parsed = (tryParseRange as TryParseRangeInput)(range, options) return parsed ? parsed.normalized || '*' : null } +export function satisfies( + version: VersionInput, + range: string, + options?: RangeOptions, +): boolean +export function satisfies(version: VersionInput, range: RangeInput): boolean export function satisfies( version: VersionInput, range: RangeInput, options: RangeOptions = {}, ): boolean { - const parsed = tryParseRange(range, options) + const parsed = (tryParseRange as TryParseRangeInput)(range, options) return parsed ? testRangeVersion(parsed, version) : false } +export function rangeToComparators( + range: string, + options?: RangeOptions, +): string[][] +export function rangeToComparators(range: RangeInput): string[][] export function rangeToComparators( range: RangeInput, options: RangeOptions = {}, ): string[][] { - return parseRange(range, options).sets.map((set) => + return (parseRange as ParseRangeInput)(range, options).sets.map((set) => set .map((comparator) => comparator.value) .join(' ') @@ -62,12 +94,21 @@ export function rangeToComparators( ) } +export function findMaxSatisfying( + versions: readonly T[], + range: string, + options?: RangeOptions, +): T | null +export function findMaxSatisfying( + versions: readonly T[], + range: RangeInput, +): T | null export function findMaxSatisfying( versions: readonly T[], range: RangeInput, options: RangeOptions = {}, ): T | null { - const parsedRange = tryParseRange(range, options) + const parsedRange = (tryParseRange as TryParseRangeInput)(range, options) if (!parsedRange) return null let maximum: T | null = null let maximumParsed: SemVer | null = null @@ -82,12 +123,21 @@ export function findMaxSatisfying( return maximum } +export function findMinSatisfying( + versions: readonly T[], + range: string, + options?: RangeOptions, +): T | null +export function findMinSatisfying( + versions: readonly T[], + range: RangeInput, +): T | null export function findMinSatisfying( versions: readonly T[], range: RangeInput, options: RangeOptions = {}, ): T | null { - const parsedRange = tryParseRange(range, options) + const parsedRange = (tryParseRange as TryParseRangeInput)(range, options) if (!parsedRange) return null let minimum: T | null = null let minimumParsed: SemVer | null = null @@ -121,11 +171,16 @@ function nextVersionAfter(version: SemVer): SemVer { return parse(comparable) } +export function findMinimumForRange( + range: string, + options?: RangeOptions, +): string | null +export function findMinimumForRange(range: RangeInput): string | null export function findMinimumForRange( range: RangeInput, options: RangeOptions = {}, ): string | null { - const parsedRange = parseRange(range, options) + const parsedRange = (parseRange as ParseRangeInput)(range, options) const zero = parse('0.0.0') if (testParsedRange(parsedRange, zero)) return formatComparableVersion(zero) const zeroPrerelease = parse('0.0.0-0') @@ -187,6 +242,17 @@ function compareOpposite( return direction === '>' ? comparison < 0 : comparison > 0 } +export function isOutsideRange( + version: VersionInput, + range: string, + direction: RangeDirection, + options?: RangeOptions, +): boolean +export function isOutsideRange( + version: VersionInput, + range: RangeInput, + direction: RangeDirection, +): boolean export function isOutsideRange( version: VersionInput, range: RangeInput, @@ -196,7 +262,7 @@ export function isOutsideRange( if (direction !== '>' && direction !== '<') { throw new TypeError('Must provide a direction of "<" or ">"') } - const parsedRange = parseRange(range, options) + const parsedRange = (parseRange as ParseRangeInput)(range, options) const parsedVersion = parse(version, parsedRange.options) if (testParsedRange(parsedRange, parsedVersion)) return false @@ -236,29 +302,53 @@ export function isOutsideRange( return true } +export function isGreaterThanRange( + version: VersionInput, + range: string, + options?: RangeOptions, +): boolean +export function isGreaterThanRange( + version: VersionInput, + range: RangeInput, +): boolean export function isGreaterThanRange( version: VersionInput, range: RangeInput, options: RangeOptions = {}, ): boolean { - return isOutsideRange(version, range, '>', options) + return (isOutsideRange as IsOutsideRangeInput)(version, range, '>', options) } +export function isLessThanRange( + version: VersionInput, + range: string, + options?: RangeOptions, +): boolean +export function isLessThanRange( + version: VersionInput, + range: RangeInput, +): boolean export function isLessThanRange( version: VersionInput, range: RangeInput, options: RangeOptions = {}, ): boolean { - return isOutsideRange(version, range, '<', options) + return (isOutsideRange as IsOutsideRangeInput)(version, range, '<', options) } +export function rangesIntersect( + left: string, + right: string, + options?: RangeOptions, +): boolean +export function rangesIntersect(left: RangeInput, right: RangeInput): boolean export function rangesIntersect( left: RangeInput, right: RangeInput, options: RangeOptions = {}, ): boolean { - const parsedLeft = parseRange(left, options) - const parsedRight = parseRange(right, options) + const parsedLeft = (parseRange as ParseRangeInput)(left, options) + const parsedRight = (parseRange as ParseRangeInput)(right, options) return parsedRangesIntersect(parsedLeft, parsedRight, { ...parsedLeft.options, ...parsedRight.options, @@ -266,12 +356,21 @@ export function rangesIntersect( }) } +export function simplifyRange( + versions: readonly T[], + range: string, + options?: RangeOptions, +): string +export function simplifyRange( + versions: readonly T[], + range: RangeInput, +): string export function simplifyRange( versions: readonly T[], range: RangeInput, options: RangeOptions = {}, ): string { - const parsedRange = parseRange(range, options) + const parsedRange = (parseRange as ParseRangeInput)(range, options) const sorted = sort(versions, parsedRange.options) const sets: [T, T | null][] = [] let first: T | null = null @@ -475,14 +574,20 @@ function simpleRangeSubset( return !needsLowerPrerelease && !needsUpperPrerelease } +export function isRangeSubset( + subset: string, + superset: string, + options?: RangeOptions, +): boolean +export function isRangeSubset(subset: RangeInput, superset: RangeInput): boolean export function isRangeSubset( subset: RangeInput, superset: RangeInput, options: RangeOptions = {}, ): boolean { if (subset === superset) return true - const parsedSubset = parseRange(subset, options) - const parsedSuperset = parseRange(superset, options) + const parsedSubset = (parseRange as ParseRangeInput)(subset, options) + const parsedSuperset = (parseRange as ParseRangeInput)(superset, options) const effectiveOptions = { ...parsedSubset.options, ...parsedSuperset.options, diff --git a/tests/range.test.ts b/tests/range.test.ts index 57f3eb6..3e2404e 100644 --- a/tests/range.test.ts +++ b/tests/range.test.ts @@ -57,6 +57,16 @@ describe('range parsing and satisfaction', () => { expect(isRangeSubset(range, '1.x')).toBe(true) }) + it('fixes range options at parse time', () => { + const version = '1.0.0-rc.1' + const range = parseRange('1.x', { includePrerelease: true }) + + expect(range.options.includePrerelease).toBe(true) + expect(normalizeRange(range)).toBe('>=1.0.0-0 <2.0.0-0') + expect(satisfies(version, range)).toBe(true) + expect(findMaxSatisfying([version], range)).toBe(version) + }) + it('returns independent mutable SemVerRange objects', () => { const options: RangeOptions = {} const first = parseRange('^1.2.3', options) diff --git a/tests/range.types.ts b/tests/range.types.ts new file mode 100644 index 0000000..2b0f425 --- /dev/null +++ b/tests/range.types.ts @@ -0,0 +1,70 @@ +import { + findMaxSatisfying, + findMinimumForRange, + findMinSatisfying, + isGreaterThanRange, + isLessThanRange, + isOutsideRange, + isRangeSubset, + isValidRange, + normalizeRange, + parseRange, + rangesIntersect, + rangeToComparators, + satisfies, + simplifyRange, + tryParseRange, + type RangeOptions, + type SemVerRange, +} from '../src/index.ts' + +declare const options: RangeOptions +declare const parsed: SemVerRange +declare const versions: readonly string[] + +parseRange(parsed) +tryParseRange(parsed) +isValidRange(parsed) +normalizeRange(parsed) +satisfies('1.0.0', parsed) +rangeToComparators(parsed) +findMaxSatisfying(versions, parsed) +findMinSatisfying(versions, parsed) +findMinimumForRange(parsed) +isOutsideRange('1.0.0', parsed, '>') +isGreaterThanRange('1.0.0', parsed) +isLessThanRange('1.0.0', parsed) +rangesIntersect(parsed, '1.x') +simplifyRange(versions, parsed) +isRangeSubset(parsed, '1.x') + +// @ts-expect-error A parsed range already contains its parsing options. +parseRange(parsed, options) +// @ts-expect-error A parsed range already contains its parsing options. +tryParseRange(parsed, options) +// @ts-expect-error A parsed range already contains its parsing options. +isValidRange(parsed, options) +// @ts-expect-error A parsed range already contains its parsing options. +normalizeRange(parsed, options) +// @ts-expect-error A parsed range already contains its parsing options. +satisfies('1.0.0', parsed, options) +// @ts-expect-error A parsed range already contains its parsing options. +rangeToComparators(parsed, options) +// @ts-expect-error A parsed range already contains its parsing options. +findMaxSatisfying(versions, parsed, options) +// @ts-expect-error A parsed range already contains its parsing options. +findMinSatisfying(versions, parsed, options) +// @ts-expect-error A parsed range already contains its parsing options. +findMinimumForRange(parsed, options) +// @ts-expect-error A parsed range already contains its parsing options. +isOutsideRange('1.0.0', parsed, '>', options) +// @ts-expect-error A parsed range already contains its parsing options. +isGreaterThanRange('1.0.0', parsed, options) +// @ts-expect-error A parsed range already contains its parsing options. +isLessThanRange('1.0.0', parsed, options) +// @ts-expect-error A parsed range already contains its parsing options. +rangesIntersect(parsed, '1.x', options) +// @ts-expect-error A parsed range already contains its parsing options. +simplifyRange(versions, parsed, options) +// @ts-expect-error A parsed range already contains its parsing options. +isRangeSubset(parsed, '1.x', options)