diff --git a/README.md b/README.md index e44c5ae..834f3fd 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ tree-shakeable APIs. - 💙 First-class TypeScript declarations. - 🌳 Functional, tree-shakeable named exports. - 🔁 Mutable `SemVer` and `SemVerRange` records. -- ⚡ 24.8% smaller for full CDN imports. -- 🪶 59.9% smaller with common bundled imports. +- ⚡ 23.8% smaller for full CDN imports. +- 🪶 60.3% smaller with common bundled imports. - 🛡️ Immutable collection operations. ## Install @@ -38,16 +38,18 @@ import { } from 'verkit' const version = parse('1.2.3-rc.1+sha.abc') +const coerced = coerce('release 42.6.7.9', { rtl: true }) version.patch = 4 normalizeFull(version) // '1.2.4-rc.1+sha.abc' normalize(version) // '1.2.4-rc.1' increment(version, 'minor') // '1.3.0' truncate(version, 'patch') // '1.2.4' -coerce('release 42.6.7.9', { rtl: true }) // '6.7.9' +coerced?.major // 6 ``` -Version APIs accept strings or mutable `SemVer` objects returned by `parse`. +Version APIs accept strings or mutable `SemVer` objects returned by `parse` or +`coerce`. `normalizeFull` keeps build metadata; normalized, incremented, and truncated versions omit it. @@ -102,33 +104,34 @@ See the [API reference](https://npmx.dev/package-docs/verkit). ## Invalid input behavior -`parse` and `parseRange` throw detailed `TypeError`s. Their safe wrappers, -`tryParse` and `tryParseRange`, return `null`; other safe transforms and -predicates keep their documented `null`/`false` behavior. +`parse`, `parseComparator`, and `parseRange` throw detailed `TypeError`s. Their +`tryParse*` wrappers return `null`; other safe transforms and predicates keep +their documented `null`/`false` behavior. ## Migrating from node-semver Only renamed or reshaped [node-semver] APIs are listed; same-named functions such as `clean`, `coerce`, `compare`, and `satisfies` are omitted. -| node-semver | verkit | -| ---------------------------------------------- | ---------------------------------------------------------------------------------------- | -| `SemVer` | `parse` | -| `parse` | `tryParse` | -| `valid` | `normalize` | -| `inc`, `diff` | `increment`, `difference` | -| `major`, `minor`, `patch`, `prerelease` | `getMajor`, `getMinor`, `getPatch`, `getPrerelease` | -| `rcompare`, `compareLoose`, `cmp` | `compareReversed`, `compare` with `{ loose: true }`, `compareWithOperator` | -| `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | `isEqual`, `isNotEqual`, `isGreater`, `isGreaterOrEqual`, `isLess`, `isLessOrEqual` | -| `rsort` | `sortReversed` | -| `rcompareIdentifiers` | `compareIdentifiersReversed` | -| `Comparator` | `SemVerComparator`, `normalizeComparator`, `satisfiesComparator`, `comparatorsIntersect` | -| `Range` | `parseRange` | -| `toComparators`, `validRange` | `rangeToComparators`, `normalizeRange` | -| `maxSatisfying`, `minSatisfying`, `minVersion` | `findMaxSatisfying`, `findMinSatisfying`, `findMinimumForRange` | -| `outside`, `gtr`, `ltr` | `isOutsideRange`, `isGreaterThanRange`, `isLessThanRange` | -| `intersects`, `subset` | `rangesIntersect`, `isRangeSubset` | -| `RELEASE_TYPES` | `INCREMENT_TYPES` (also includes `release`) | +| node-semver | verkit | +| ---------------------------------------------- | ----------------------------------------------------------------------------------- | +| `SemVer` | `parse` | +| `parse` | `tryParse` | +| `valid` | `normalize` | +| `inc`, `diff` | `increment`, `difference` | +| `major`, `minor`, `patch`, `prerelease` | `getMajor`, `getMinor`, `getPatch`, `getPrerelease` | +| `rcompare`, `compareLoose`, `cmp` | `compareReversed`, `compare` with `{ loose: true }`, `compareWithOperator` | +| `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | `isEqual`, `isNotEqual`, `isGreater`, `isGreaterOrEqual`, `isLess`, `isLessOrEqual` | +| `rsort` | `sortReversed` | +| `rcompareIdentifiers` | `compareIdentifiersReversed` | +| `Comparator` | `SemVerComparator`, `parseComparator`, `tryParseComparator` | +| Comparator formatting, test, and intersection | `normalizeComparator`, `satisfiesComparator`, `comparatorsIntersect` | +| `Range` | `parseRange` | +| `toComparators`, `validRange` | `rangeToComparators`, `normalizeRange` | +| `maxSatisfying`, `minSatisfying`, `minVersion` | `findMaxSatisfying`, `findMinSatisfying`, `findMinimumForRange` | +| `outside`, `gtr`, `ltr` | `isOutsideRange`, `isGreaterThanRange`, `isLessThanRange` | +| `intersects`, `subset` | `rangesIntersect`, `isRangeSubset` | +| `RELEASE_TYPES` | `INCREMENT_TYPES` (also includes `release`) | `valid` returns a normalized `string | null` in node-semver, so its equivalent is `normalize`. Use `isValid` when you only need a boolean. @@ -153,18 +156,18 @@ Full package imports, minified with Rolldown: | Package | Minified | gzip | Brotli | | --------------------- | -------: | ------: | ------: | -| verkit | 18,512 B | 5,812 B | 5,297 B | +| verkit | 18,754 B | 5,874 B | 5,341 B | | [semver][node-semver] | 24,603 B | 7,356 B | 6,703 B | -| verkit reduction | 24.8% | 21.0% | 21.0% | +| verkit reduction | 23.8% | 20.1% | 20.3% | Common validation, range, comparison, increment, and coercion imports, tree-shaken and minified with Rolldown: | Package | Minified | gzip | Brotli | | --------------------- | -------: | ------: | ------: | -| verkit | 9,933 B | 3,387 B | 3,092 B | -| [semver][node-semver] | 24,756 B | 7,428 B | 6,765 B | -| verkit reduction | 59.9% | 54.4% | 54.3% | +| verkit | 9,834 B | 3,361 B | 3,086 B | +| [semver][node-semver] | 24,754 B | 7,427 B | 6,764 B | +| verkit reduction | 60.3% | 54.7% | 54.4% | Run `pnpm test:size` to reproduce the comparison. @@ -175,13 +178,13 @@ better. | Operation | verkit ops/s | semver ops/s | Faster | | ------------------------- | -----------: | -----------: | ------------ | -| Parse and normalize | 1.92M | 1.63M | verkit 1.18× | -| Compare | 1.60M | 1.13M | verkit 1.41× | -| Compare parsed versions | 12.83M | 5.93M | verkit 2.16× | -| Increment | 1.78M | 1.02M | verkit 1.76× | -| Coerce | 1.18M | 1.05M | verkit 1.13× | -| Satisfy uncached ranges | 0.07M | 0.06M | verkit 1.18× | -| Satisfy pre-parsed inputs | 5.15M | 2.28M | verkit 2.26× | +| Parse and normalize | 1.89M | 1.59M | verkit 1.19× | +| Compare | 1.58M | 1.03M | verkit 1.53× | +| Compare parsed versions | 11.70M | 5.44M | verkit 2.15× | +| Increment | 1.79M | 1.00M | verkit 1.79× | +| Coerce | 1.40M | 1.06M | verkit 1.32× | +| Satisfy uncached ranges | 81.7K | 56.3K | verkit 1.45× | +| Satisfy pre-parsed inputs | 5.34M | 2.05M | verkit 2.60× | Range benchmarks either cycle through 1,001 inputs to avoid cache hits or parse once and reuse the resulting objects. diff --git a/__snapshots__/tsnapi/index.snapshot.d.ts b/__snapshots__/tsnapi/index.snapshot.d.ts deleted file mode 100644 index 2086e90..0000000 --- a/__snapshots__/tsnapi/index.snapshot.d.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Generated by tsnapi — public API snapshot of `verkit` - */ -// #region Interfaces -export interface CoerceOptions extends VersionOptions { - includePrerelease?: boolean; - rtl?: boolean; -} -export interface IncrementOptions extends VersionOptions { - identifier?: string; - identifierBase?: 0 | 1 | false; -} -export interface RangeOptions extends VersionOptions { - includePrerelease?: boolean; -} -export interface SemVer { - major: number; - minor: number; - patch: number; - prerelease?: PrereleaseIdentifier[] | undefined; - build?: string[] | undefined; -} -export interface SemVerComparator { - operator: "" | "<" | "<=" | ">" | ">="; - options: RangeOptions; - value: string; - version: SemVer | null; -} -export interface SemVerRange { - normalized: string; - options: RangeOptions; - raw: string; - sets: SemVerComparator[][]; -} -export interface VersionOptions { - loose?: boolean; -} -// #endregion - -// #region Types -export type Comparison = -1 | 0 | 1; -export type ComparisonOperator = "" | "!=" | "!==" | "<" | "<=" | "=" | "==" | "===" | ">" | ">="; -export type IncrementType = "major" | "minor" | "patch" | "premajor" | "preminor" | "prepatch" | "prerelease" | "release"; -export type PrereleaseIdentifier = number | string; -export type RangeDirection = "<" | ">"; -export type RangeInput = SemVerRange | string; -export type TruncationType = Exclude; -export type VersionDifference = "major" | "minor" | "patch" | "premajor" | "preminor" | "prepatch" | "prerelease"; -export type VersionInput = SemVer | string; -// #endregion - -// #region Functions -export declare function clean(_: VersionInput, _?: VersionOptions): string | null; -export declare function coerce(_: number | VersionInput, _?: CoerceOptions): string | null; -export declare function comparatorsIntersect(_: string, _: string, _?: RangeOptions): boolean; -export declare function compare(_: VersionInput, _: VersionInput, _?: VersionOptions): Comparison; -export declare function compareBuild(_: VersionInput, _: VersionInput, _?: VersionOptions): Comparison; -export declare function compareIdentifiers(_: PrereleaseIdentifier, _: PrereleaseIdentifier): Comparison; -export declare function compareIdentifiersReversed(_: PrereleaseIdentifier, _: PrereleaseIdentifier): Comparison; -export declare function compareMain(_: VersionInput, _: VersionInput, _?: VersionOptions): Comparison; -export declare function comparePrerelease(_: VersionInput, _: VersionInput, _?: VersionOptions): Comparison; -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[], _: 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; -export declare function getPatch(_: VersionInput, _?: VersionOptions): number; -export declare function getPrerelease(_: VersionInput, _?: VersionOptions): PrereleaseIdentifier[] | null; -export declare function increment(_: VersionInput, _: IncrementType, _?: IncrementOptions): string | null; -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, _: 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, _: string, _?: RangeOptions): boolean; -export declare function isLessThanRange(_: VersionInput, _: RangeInput): boolean; -export declare function isNotEqual(_: VersionInput, _: VersionInput, _?: VersionOptions): boolean; -export declare function isOutsideRange(_: VersionInput, _: string, _: RangeDirection, _?: RangeOptions): boolean; -export declare function isOutsideRange(_: VersionInput, _: RangeInput, _: RangeDirection): boolean; -export declare function isPrerelease(_: VersionInput, _?: VersionOptions): boolean | null; -export declare function isRangeSubset(_: string, _: string, _?: RangeOptions): boolean; -export declare function isRangeSubset(_: RangeInput, _: RangeInput): boolean; -export declare function isStable(_: VersionInput, _?: VersionOptions): boolean | null; -export declare function isValid(_: VersionInput, _?: VersionOptions): 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(_: string, _?: RangeOptions): string | null; -export declare function normalizeRange(_: RangeInput): string | null; -export declare function parse(_: VersionInput, _?: VersionOptions): SemVer; -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[], _: 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(_: string, _?: RangeOptions): SemVerRange | null; -export declare function tryParseRange(_: RangeInput): SemVerRange | null; -// #endregion - -// #region Variables -export declare const INCREMENT_TYPES: readonly IncrementType[]; -export declare const SEMVER_SPEC_VERSION: string; -export declare const TRUNCATION_TYPES: readonly TruncationType[]; -// #endregion \ No newline at end of file diff --git a/__snapshots__/tsnapi/index.snapshot.js b/__snapshots__/tsnapi/index.snapshot.js deleted file mode 100644 index 1d9042a..0000000 --- a/__snapshots__/tsnapi/index.snapshot.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Generated by tsnapi — public API snapshot of `verkit` - */ -// #region Functions -export function clean(_, _) {} -export function coerce(_, _) {} -export function comparatorsIntersect(_, _, _) {} -export function compare(_, _, _) {} -export function compareBuild(_, _, _) {} -export function compareIdentifiers(_, _) {} -export function compareIdentifiersReversed(_, _) {} -export function compareMain(_, _, _) {} -export function comparePrerelease(_, _, _) {} -export function compareReversed(_, _, _) {} -export function compareWithOperator(_, _, _, _) {} -export function difference(_, _) {} -export function findMaxSatisfying(_, _, _) {} -export function findMinimumForRange(_, _) {} -export function findMinSatisfying(_, _, _) {} -export function getBuild(_, _) {} -export function getMajor(_, _) {} -export function getMinor(_, _) {} -export function getPatch(_, _) {} -export function getPrerelease(_, _) {} -export function increment(_, _, _) {} -export function isEqual(_, _, _) {} -export function isGreater(_, _, _) {} -export function isGreaterOrEqual(_, _, _) {} -export function isGreaterThanRange(_, _, _) {} -export function isLess(_, _, _) {} -export function isLessOrEqual(_, _, _) {} -export function isLessThanRange(_, _, _) {} -export function isNotEqual(_, _, _) {} -export function isOutsideRange(_, _, _, _) {} -export function isPrerelease(_, _) {} -export function isRangeSubset(_, _, _) {} -export function isStable(_, _) {} -export function isValid(_, _) {} -export function isValidRange(_, _) {} -export function normalize(_, _) {} -export function normalizeComparator(_, _) {} -export function normalizeFull(_, _) {} -export function normalizeRange(_, _) {} -export function parse(_, _) {} -export function parseRange(_, _) {} -export function rangesIntersect(_, _, _) {} -export function rangeToComparators(_, _) {} -export function satisfies(_, _, _) {} -export function satisfiesComparator(_, _, _) {} -export function simplifyRange(_, _, _) {} -export function sort(_, _) {} -export function sortReversed(_, _) {} -export function truncate(_, _, _) {} -export function tryParse(_, _) {} -export function tryParseRange(_, _) {} -// #endregion - -// #region Variables -export var INCREMENT_TYPES /* const */ -export var SEMVER_SPEC_VERSION /* const */ -export var TRUNCATION_TYPES /* const */ -// #endregion \ No newline at end of file diff --git a/package.json b/package.json index af6a758..db47902 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,9 @@ "sideEffects": false, "exports": { ".": "./dist/index.js", + "./comparator": "./dist/comparator.js", + "./range": "./dist/range.js", + "./version": "./dist/version.js", "./package.json": "./package.json" }, "files": [ @@ -65,7 +68,6 @@ "semver": "^7.8.5", "tsdown": "^0.22.14", "tsdown-preset-sxzz": "^0.7.0", - "tsnapi": "^1.2.0", "typescript": "~6.0.3", "vitest": "4.1.10" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f168eb..7c80730 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,9 +44,6 @@ importers: tsdown-preset-sxzz: specifier: ^0.7.0 version: 0.7.0(tsdown@0.22.14(@arethetypeswrong/core@0.18.5)(publint@0.3.23)(typescript@6.0.3)(typescript@7.0.2)) - tsnapi: - specifier: ^1.2.0 - version: 1.2.0(vitest@4.1.10(@types/node@26.1.2)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))) typescript: specifier: ~6.0.3 version: 6.0.3 @@ -190,97 +187,48 @@ packages: resolution: {integrity: sha512-XRO0zi2NIUKq2lUk3T1ecFSld1fMWRKE6naRFGkgkdeosx7IslyUKNv5Dcb5PJTja9tHJoFu0v/7yEpAkrkrTg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@oxc-parser/binding-android-arm-eabi@0.138.0': - resolution: {integrity: sha512-hSYAD+F9W2Qh8SETMqBsQRx6YHvB4z+i/i36shlC7tfdZQauMs4vf3G/EQwKOkNlN7rkTiKINvsNmQb9q2MWcQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - '@oxc-parser/binding-android-arm-eabi@0.139.0': resolution: {integrity: sha512-22EsXTA3Vc7OvrF4bfT48PFln2UbxkVgrp/Tm32qLw76Dv7SmcInfClJe6yPYamnli6HiqasnESZ5ezN+X4ybg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxc-parser/binding-android-arm64@0.138.0': - resolution: {integrity: sha512-Ns5LLTp8cVyP8DsYqD482h0HE84xiGYRgtm7g4LtTinq209NAiMF768e/8r2NHaa0UMirS5mrT1m1VwiVmBi4Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - '@oxc-parser/binding-android-arm64@0.139.0': resolution: {integrity: sha512-uASQkZV+CUQ6xXvoMzDQyfhd8OMusdv2FyCPfAi5ZDYptesapJlw7erhpGi1Lc+U8QjQV2JUrIh7d/3su2LzmQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-parser/binding-darwin-arm64@0.138.0': - resolution: {integrity: sha512-Yka0m4YhKUHBIZufafSLAeO+DUrfHPtNXBlZSj7DxshquIl41x/a+i/MbRnbOy8heuLiYU1STa6h0FAAzT7Pbw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - '@oxc-parser/binding-darwin-arm64@0.139.0': resolution: {integrity: sha512-vuQOv2WF5pZCkdSPEExul98o853M3MCR24DpWsGrUoPJu5KcnGE34kLXY2yFwBwZwT+eN1x40Tt4q6ZzlEdNUA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxc-parser/binding-darwin-x64@0.138.0': - resolution: {integrity: sha512-MWLUZZzmNRUqTWueZF27ncreaZ1wZ0gboWL2QMPxRQA2xgOmBPlGg2H9pAKJSPBlwEHcWa9TdWRiehAS+yls8w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - '@oxc-parser/binding-darwin-x64@0.139.0': resolution: {integrity: sha512-zxZB8ChS+7XactZhcyxQ292P/DNiiBaPPKYiVUlb3RC0V/hme5bokNubjcmEmbW9uTfmqLTpveAdkbe66H3pGg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxc-parser/binding-freebsd-x64@0.138.0': - resolution: {integrity: sha512-Vae5tzsrzZ/lCDVCZUMi/vzSiiHEgcOEfsyIfWOHmjZ2ji+gT+n96T757yX5/f7/7JIJuiannAHJKV5ARaF6ng==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - '@oxc-parser/binding-freebsd-x64@0.139.0': resolution: {integrity: sha512-iw2MsoCPBQwdJqywRAGsF1jEAcGoZ+DeG2LVzt5pdUvRHqajsMF46BH0rHiWoWtMwcMSia+oQYga7fHo7u7Bcw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxc-parser/binding-linux-arm-gnueabihf@0.138.0': - resolution: {integrity: sha512-qkU8wv5mYexrCw0X4DHFgxGbRScwGLIIKUkHXU7xXEiLoMnQzELak2gujxfa9GFrlEgPjbyLUDFHWm67Zs38ng==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@oxc-parser/binding-linux-arm-gnueabihf@0.139.0': resolution: {integrity: sha512-SwjD/k3Y5bwGJWOH313VyaDrAuaYg1Pe+4AAXCgUKvSO5IHkj+mZ0oFcOWmB5nTuOM3uwdT+leL8h4OnFlI5hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-musleabihf@0.138.0': - resolution: {integrity: sha512-3HgULIvoDV7h2ZfVYzxQwOSOJnAjMwYmyUBzndNuLRGgBNI549ED0P6AGmN9y2TnSvrwJ+Q8zqdxqssMnGXitA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@oxc-parser/binding-linux-arm-musleabihf@0.139.0': resolution: {integrity: sha512-qbeygDkcvailKFhphb2YGMMXsaZIynHlPtoOrcx4NZB/tRbUKraCCMbw8dPeFtEKasfS2qWh1VnReY+Lcys1zA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm64-gnu@0.138.0': - resolution: {integrity: sha512-pIonbH2p0KLCwz4CNPCi0xGqci4numpMQDCLJwLfsrEky7NUuByKDFhCjzE0E7vR3aj/lBjyMoTskHBo/qSg8g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@oxc-parser/binding-linux-arm64-gnu@0.139.0': resolution: {integrity: sha512-SrlL02KeImKlvx/9rCeopOLH7qKI3rqKKEguK6KBwVwEGLhV/A47dW4DNigf6/LFhrwoovaiayEQryjYAI86dQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -288,13 +236,6 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-arm64-musl@0.138.0': - resolution: {integrity: sha512-cT5L1Xz/5m6Ga1hD3922gLc+fePOauJZJdApPTI/2Vu0EmYo62uHG9V5Dq65hhgU9TW10oDi2840y9cGdd7BIg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - '@oxc-parser/binding-linux-arm64-musl@0.139.0': resolution: {integrity: sha512-u9e884ChAVRmIZ1jr/m46S96FoDQnruFjISLi4Y0i6Wu/JUUmIiw7+umLyXILJsPfUuqnN5BJLe23t07+Y6+IA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -302,13 +243,6 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-linux-ppc64-gnu@0.138.0': - resolution: {integrity: sha512-hKy/vvejKk3LNE/FsRbekWejLa046//TnLWtSo7ur29NIsNbSIvnOVYIirSVC7fsd6NO8UFzwDdcoZfCyBvSBA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - '@oxc-parser/binding-linux-ppc64-gnu@0.139.0': resolution: {integrity: sha512-Z9tU2b3GJfAXOdirQmz4gZQkQkVjy53i77gf91l0733MQKa/qtk73KQQE2GzDtMqim+HyjpzvemmqzBtH2IJUA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -316,13 +250,6 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-gnu@0.138.0': - resolution: {integrity: sha512-bh6tjNGq0v0b9GAMu0pTv/YpTqepCFy0TIOtQHm8+41fZwLXTaB6xiEWVUSarNCXqc5kyzYcH6EOfwW1sJxJOw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - '@oxc-parser/binding-linux-riscv64-gnu@0.139.0': resolution: {integrity: sha512-RyUbr7hzPK84YDWKs77PRYk9VBwWbsbuYsQzQWiSmLnARXTg2zntLPGfCH/1wpfUYdmGkp/6SsqTXSsYdw9Jgw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -330,13 +257,6 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-musl@0.138.0': - resolution: {integrity: sha512-HhOkddcClSTtTxY10f/mACblKcQdxWy4lYYwX12G23j+S5eiJ5y1kpo1r7kKng+2bdnCBO+lCDWOVVc9kVl9+g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - libc: [musl] - '@oxc-parser/binding-linux-riscv64-musl@0.139.0': resolution: {integrity: sha512-dcQhjtcDvtR8BgkUpt03Yz5SzxdzYvTigenIJOEsiSX6G2t6yybEMxGWjp0dOuYGror0BaqcZfTyXR58amCEig==} engines: {node: ^20.19.0 || >=22.12.0} @@ -344,13 +264,6 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-linux-s390x-gnu@0.138.0': - resolution: {integrity: sha512-5mi+wtbeJiEa4waGG88EcEGgJBBNJdDeIcayPPcrLNMXbCrgdtbb80q0Nrat7A8NglLUVzhuTAAp7K6PjmUO8Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@oxc-parser/binding-linux-s390x-gnu@0.139.0': resolution: {integrity: sha512-iuGrxysV4rGUymdKpn7bgZQ6Vix8Bi/6D/rp71HYIzphq6NKrsBhsGOYsSZte+uBFL43tXh7Xr7TM72sGliJNA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -358,13 +271,6 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-gnu@0.138.0': - resolution: {integrity: sha512-ckbq3AMI7lI8AhQtE8KdqYRmzmzwKfCU12QN/PBKXO72PfWdvvZQN0hFShDX/XRNsPqjddLmvXaQMT3zfYtNlw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - '@oxc-parser/binding-linux-x64-gnu@0.139.0': resolution: {integrity: sha512-NxJdZZyaa2JLLvNfH/iJQXfCNfKcPMylwY4ObMpBVmW4Nq+RyUpVVQXrMMelcQ6rwx3nuhF1Iga8n+eoAKGCIA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -372,13 +278,6 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-musl@0.138.0': - resolution: {integrity: sha512-JrCOzHO9BYEs5Xz5JHYBxSc/hYKxfXUj5QQb64sERSbkQot6+KEgMTOR2C9hLrhaqOui65OYcFyTTS+YxXDtnA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - '@oxc-parser/binding-linux-x64-musl@0.139.0': resolution: {integrity: sha512-ePxBvvtzISmSsJ0RIj8FNikSCn58i1jtccj7XR4U9Li4iSzhkFyYlnJ51cQTUwkairz8WMTD4SpKoot8RyTnQA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -386,67 +285,35 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-openharmony-arm64@0.138.0': - resolution: {integrity: sha512-eASMMfOOIfLHkWJRPSu8llByvVRM+c1M/lh18KjsjELM3y10+7B5iBbbrht9LdtsJXQ+mRuP/lJ7UWe3Ok3ehw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - '@oxc-parser/binding-openharmony-arm64@0.139.0': resolution: {integrity: sha512-b/c2+mPXMOxG5x16n8yf9cjor/ntQQScmYnSmLEWIWJ4rfXd5dokMxx0kliSLA+YAGq6DD3K9BWi+aFXHiiV1w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxc-parser/binding-wasm32-wasi@0.138.0': - resolution: {integrity: sha512-BnTCO87Iwc57NufXS7vcrkrmpN+daeCeYr1+/xgPT6HjwNs0lBmJYeFrcOs4WkNN8yscdd6Rc4FxWh3+59hAFw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - '@oxc-parser/binding-wasm32-wasi@0.139.0': resolution: {integrity: sha512-rNU0pO+/CVPC2qZ+xtX5R2cOje9Q75q3UkfgwUCPlplqxMv6Iffd5tMPGVMCLGnPINxPlmi0WPsW94HltbYvfQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@oxc-parser/binding-win32-arm64-msvc@0.138.0': - resolution: {integrity: sha512-+Zi47boD2wKNL0hOA47Vkwk6njMZ8sOsr4Geu/56EUtlooDh9crNOU41U6bXGS0UjC4Y72HtRA1iuB6qx1ARUw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - '@oxc-parser/binding-win32-arm64-msvc@0.139.0': resolution: {integrity: sha512-JPEmfncZfqAFiGsAN6UZSMIBqnG8wn8buywRacFOWjP+9dZwcs03SsBp4tmyjM/4l/VoH/IuThrW1Jof3OyQUw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxc-parser/binding-win32-ia32-msvc@0.138.0': - resolution: {integrity: sha512-SYcV674Wi2WuoBefUFgf0PBMNlZe5IF0YZ0TnP7DK+EusMVpEWq6iz+7r64svjAb7vjthzlas0FUCSlz8YkqYg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - '@oxc-parser/binding-win32-ia32-msvc@0.139.0': resolution: {integrity: sha512-c06dWBwEnFHof5JN7T9/ts23uATXS9czPEBO60d4xnjH2Am29Bo7OGKdVHRmcWQnw0OBxlTg9fIEXAvtcwOBfw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxc-parser/binding-win32-x64-msvc@0.138.0': - resolution: {integrity: sha512-QZplnCxS4vPe4StAVBtvD2bW3pELlidf0Ek6iQ/HHiCjbEtrs5pFZZfLAoPhKLJyDzyxoGAdic9bSIYrJYTZcg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@oxc-parser/binding-win32-x64-msvc@0.139.0': resolution: {integrity: sha512-bI3/44urQjW/D495JPXe+c9d+4Q+ONi3DvDNnzwRZYTWHZ3hAlFdmirYK4mzhTfCZiAly02EbMYir7QGU+9O2Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxc-project/types@0.138.0': - resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} - '@oxc-project/types@0.139.0': resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} @@ -1841,10 +1708,6 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - oxc-parser@0.138.0: - resolution: {integrity: sha512-c25lvfpZ2+WY1yk6NkP0X0RTQg0ZxgSVaZHDa7lt6fEe1jwZjPWkRWvTyZ1xyaM7roVJMdtRCfbhUj/d4ims3Q==} - engines: {node: ^20.19.0 || >=22.12.0} - oxc-parser@0.139.0: resolution: {integrity: sha512-cf1TKZN+zc0lwqigeyXKzKVk5+vNRe99Or2+wVJsXLdlhJgC+gsIniYDfj/ZEBzCJ8Xm21ZG6YtMbR262CcS2w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2114,15 +1977,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsnapi@1.2.0: - resolution: {integrity: sha512-0/uoggvqS+iNVMgMdLghRugAArckJo7uAkRtpEW/pblmDWC+NtIqvN0pMdbYl6ACvzIJ7AqpwfD5g1KJHT4Hyg==} - hasBin: true - peerDependencies: - vitest: '>=4.0.0' - peerDependenciesMeta: - vitest: - optional: true - type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2486,109 +2340,54 @@ snapshots: '@ota-meshi/ast-token-store@0.3.0': {} - '@oxc-parser/binding-android-arm-eabi@0.138.0': - optional: true - '@oxc-parser/binding-android-arm-eabi@0.139.0': optional: true - '@oxc-parser/binding-android-arm64@0.138.0': - optional: true - '@oxc-parser/binding-android-arm64@0.139.0': optional: true - '@oxc-parser/binding-darwin-arm64@0.138.0': - optional: true - '@oxc-parser/binding-darwin-arm64@0.139.0': optional: true - '@oxc-parser/binding-darwin-x64@0.138.0': - optional: true - '@oxc-parser/binding-darwin-x64@0.139.0': optional: true - '@oxc-parser/binding-freebsd-x64@0.138.0': - optional: true - '@oxc-parser/binding-freebsd-x64@0.139.0': optional: true - '@oxc-parser/binding-linux-arm-gnueabihf@0.138.0': - optional: true - '@oxc-parser/binding-linux-arm-gnueabihf@0.139.0': optional: true - '@oxc-parser/binding-linux-arm-musleabihf@0.138.0': - optional: true - '@oxc-parser/binding-linux-arm-musleabihf@0.139.0': optional: true - '@oxc-parser/binding-linux-arm64-gnu@0.138.0': - optional: true - '@oxc-parser/binding-linux-arm64-gnu@0.139.0': optional: true - '@oxc-parser/binding-linux-arm64-musl@0.138.0': - optional: true - '@oxc-parser/binding-linux-arm64-musl@0.139.0': optional: true - '@oxc-parser/binding-linux-ppc64-gnu@0.138.0': - optional: true - '@oxc-parser/binding-linux-ppc64-gnu@0.139.0': optional: true - '@oxc-parser/binding-linux-riscv64-gnu@0.138.0': - optional: true - '@oxc-parser/binding-linux-riscv64-gnu@0.139.0': optional: true - '@oxc-parser/binding-linux-riscv64-musl@0.138.0': - optional: true - '@oxc-parser/binding-linux-riscv64-musl@0.139.0': optional: true - '@oxc-parser/binding-linux-s390x-gnu@0.138.0': - optional: true - '@oxc-parser/binding-linux-s390x-gnu@0.139.0': optional: true - '@oxc-parser/binding-linux-x64-gnu@0.138.0': - optional: true - '@oxc-parser/binding-linux-x64-gnu@0.139.0': optional: true - '@oxc-parser/binding-linux-x64-musl@0.138.0': - optional: true - '@oxc-parser/binding-linux-x64-musl@0.139.0': optional: true - '@oxc-parser/binding-openharmony-arm64@0.138.0': - optional: true - '@oxc-parser/binding-openharmony-arm64@0.139.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.138.0': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - optional: true - '@oxc-parser/binding-wasm32-wasi@0.139.0': dependencies: '@emnapi/core': 1.11.1 @@ -2596,26 +2395,15 @@ snapshots: '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@oxc-parser/binding-win32-arm64-msvc@0.138.0': - optional: true - '@oxc-parser/binding-win32-arm64-msvc@0.139.0': optional: true - '@oxc-parser/binding-win32-ia32-msvc@0.138.0': - optional: true - '@oxc-parser/binding-win32-ia32-msvc@0.139.0': optional: true - '@oxc-parser/binding-win32-x64-msvc@0.138.0': - optional: true - '@oxc-parser/binding-win32-x64-msvc@0.139.0': optional: true - '@oxc-project/types@0.138.0': {} - '@oxc-project/types@0.139.0': {} '@oxc-project/types@0.142.0': {} @@ -4035,31 +3823,6 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - oxc-parser@0.138.0: - dependencies: - '@oxc-project/types': 0.138.0 - optionalDependencies: - '@oxc-parser/binding-android-arm-eabi': 0.138.0 - '@oxc-parser/binding-android-arm64': 0.138.0 - '@oxc-parser/binding-darwin-arm64': 0.138.0 - '@oxc-parser/binding-darwin-x64': 0.138.0 - '@oxc-parser/binding-freebsd-x64': 0.138.0 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.138.0 - '@oxc-parser/binding-linux-arm-musleabihf': 0.138.0 - '@oxc-parser/binding-linux-arm64-gnu': 0.138.0 - '@oxc-parser/binding-linux-arm64-musl': 0.138.0 - '@oxc-parser/binding-linux-ppc64-gnu': 0.138.0 - '@oxc-parser/binding-linux-riscv64-gnu': 0.138.0 - '@oxc-parser/binding-linux-riscv64-musl': 0.138.0 - '@oxc-parser/binding-linux-s390x-gnu': 0.138.0 - '@oxc-parser/binding-linux-x64-gnu': 0.138.0 - '@oxc-parser/binding-linux-x64-musl': 0.138.0 - '@oxc-parser/binding-openharmony-arm64': 0.138.0 - '@oxc-parser/binding-wasm32-wasi': 0.138.0 - '@oxc-parser/binding-win32-arm64-msvc': 0.138.0 - '@oxc-parser/binding-win32-ia32-msvc': 0.138.0 - '@oxc-parser/binding-win32-x64-msvc': 0.138.0 - oxc-parser@0.139.0: dependencies: '@oxc-project/types': 0.139.0 @@ -4318,16 +4081,6 @@ snapshots: tslib@2.8.1: optional: true - tsnapi@1.2.0(vitest@4.1.10(@types/node@26.1.2)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))): - dependencies: - '@vitest/utils': 4.1.10 - cac: 7.0.0 - magic-string: 0.30.21 - oxc-parser: 0.138.0 - tinyglobby: 0.2.17 - optionalDependencies: - vitest: 4.1.10(@types/node@26.1.2)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) - type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/src/comparator.ts b/src/comparator.ts index 401df41..5c45a73 100644 --- a/src/comparator.ts +++ b/src/comparator.ts @@ -1,33 +1,11 @@ -import { - parseComparator, - parsedComparatorsIntersect, - testComparatorVersion, -} from './internal/comparator.ts' -import type { RangeOptions, VersionInput } from './types.ts' - -export function normalizeComparator( - comparator: string, - options: RangeOptions = {}, -): string { - return parseComparator(comparator, options).value -} - -export function satisfiesComparator( - version: VersionInput, - comparator: string, - options: RangeOptions = {}, -): boolean { - return testComparatorVersion(parseComparator(comparator, options), version) -} - -export function comparatorsIntersect( - left: string, - right: string, - options: RangeOptions = {}, -): boolean { - return parsedComparatorsIntersect( - parseComparator(left, options), - parseComparator(right, options), - options, - ) -} +export { + comparatorsIntersect, + normalizeComparator, + satisfiesComparator, +} from './comparator/operations.ts' +export { parseComparator, tryParseComparator } from './comparator/parse.ts' +export type { + ComparatorInput, + RangeOptions, + SemVerComparator, +} from './comparator/types.ts' diff --git a/src/comparator/operations.ts b/src/comparator/operations.ts new file mode 100644 index 0000000..1c65fb2 --- /dev/null +++ b/src/comparator/operations.ts @@ -0,0 +1,69 @@ +import { formatComparator, parseComparator } from './parse.ts' +import { parsedComparatorsIntersect, testComparatorVersion } from './set.ts' +import type { VersionInput } from '../version/types.ts' +import type { + ComparatorInput, + RangeOptions, + SemVerComparator, +} from './types.ts' + +function resolveComparator( + comparator: ComparatorInput, + options: RangeOptions, +): SemVerComparator { + return typeof comparator === 'string' + ? parseComparator(comparator, options) + : comparator +} + +export function normalizeComparator( + comparator: string, + options?: RangeOptions, +): string +export function normalizeComparator(comparator: ComparatorInput): string +export function normalizeComparator( + comparator: ComparatorInput, + options: RangeOptions = {}, +): string { + return formatComparator(resolveComparator(comparator, options)) +} + +export function satisfiesComparator( + version: VersionInput, + comparator: string, + options?: RangeOptions, +): boolean +export function satisfiesComparator( + version: VersionInput, + comparator: ComparatorInput, +): boolean +export function satisfiesComparator( + version: VersionInput, + comparator: ComparatorInput, + options: RangeOptions = {}, +): boolean { + return testComparatorVersion(resolveComparator(comparator, options), version) +} + +export function comparatorsIntersect( + left: string, + right: string, + options?: RangeOptions, +): boolean +export function comparatorsIntersect( + left: ComparatorInput, + right: ComparatorInput, +): boolean +export function comparatorsIntersect( + left: ComparatorInput, + right: ComparatorInput, + options: RangeOptions = {}, +): boolean { + const parsedLeft = resolveComparator(left, options) + const parsedRight = resolveComparator(right, options) + return parsedComparatorsIntersect(parsedLeft, parsedRight, { + ...parsedLeft.options, + ...parsedRight.options, + ...options, + }) +} diff --git a/src/comparator/parse.ts b/src/comparator/parse.ts new file mode 100644 index 0000000..37216e8 --- /dev/null +++ b/src/comparator/parse.ts @@ -0,0 +1,73 @@ +import { formatComparableVersion, parse } from '../version/parse.ts' +import { + FULL_PLAIN, + GREATER_LESS_THAN, + LOOSE_PLAIN, + safeRegex, +} from '../version/patterns.ts' +import type { + ComparatorInput, + RangeOptions, + SemVerComparator, +} from './types.ts' + +const STRICT_COMPARATOR = safeRegex( + String.raw`^${GREATER_LESS_THAN}\s*(${FULL_PLAIN})$|^$`, +) +const LOOSE_COMPARATOR = safeRegex( + String.raw`^${GREATER_LESS_THAN}\s*(${LOOSE_PLAIN})$|^$`, +) + +export function formatComparator(comparator: SemVerComparator): string { + return comparator.version + ? `${comparator.operator}${formatComparableVersion(comparator.version)}` + : '' +} + +export function parseComparator( + comparator: string, + options?: RangeOptions, +): SemVerComparator +export function parseComparator(comparator: ComparatorInput): SemVerComparator +export function parseComparator( + comparator: ComparatorInput, + options: RangeOptions = {}, +): SemVerComparator { + if (typeof comparator !== 'string') return comparator + const normalized = comparator.trim().replaceAll(/\s+/g, ' ') + const match = normalized.match( + options.loose ? LOOSE_COMPARATOR : STRICT_COMPARATOR, + ) + if (!match) throw new TypeError(`Invalid comparator: ${normalized}`) + + const operator = ( + match[1] === '=' ? '' : match[1] || '' + ) as SemVerComparator['operator'] + const version = match[2] ? parse(match[2], options) : null + return { + operator, + options, + value: version ? `${operator}${formatComparableVersion(version)}` : '', + version, + } +} + +export function tryParseComparator( + comparator: string, + options?: RangeOptions, +): SemVerComparator | null +export function tryParseComparator( + comparator: ComparatorInput, +): SemVerComparator | null +export function tryParseComparator( + comparator: ComparatorInput, + options: RangeOptions = {}, +): SemVerComparator | null { + try { + return typeof comparator === 'string' + ? parseComparator(comparator, options) + : comparator + } catch { + return null + } +} diff --git a/src/internal/comparator.ts b/src/comparator/set.ts similarity index 68% rename from src/internal/comparator.ts rename to src/comparator/set.ts index 4b945fa..8e074b6 100644 --- a/src/internal/comparator.ts +++ b/src/comparator/set.ts @@ -1,50 +1,8 @@ -import { - FULL_PLAIN, - GREATER_LESS_THAN, - LOOSE_PLAIN, - safeRegex, -} from './patterns.ts' -import { - compareParsed, - formatComparableVersion, - parse, - tryParse, -} from './version.ts' -import type { - RangeOptions, - SemVer, - SemVerComparator, - VersionInput, -} from '../types.ts' - -const STRICT_COMPARATOR = safeRegex( - String.raw`^${GREATER_LESS_THAN}\s*(${FULL_PLAIN})$|^$`, -) -const LOOSE_COMPARATOR = safeRegex( - String.raw`^${GREATER_LESS_THAN}\s*(${LOOSE_PLAIN})$|^$`, -) - -export function parseComparator( - comparator: string, - options: RangeOptions = {}, -): SemVerComparator { - const normalized = comparator.trim().replaceAll(/\s+/g, ' ') - const match = normalized.match( - options.loose ? LOOSE_COMPARATOR : STRICT_COMPARATOR, - ) - if (!match) throw new TypeError(`Invalid comparator: ${normalized}`) - - const operator = ( - match[1] === '=' ? '' : match[1] || '' - ) as SemVerComparator['operator'] - const version = match[2] ? parse(match[2], options) : null - return { - operator, - options, - value: version ? `${operator}${formatComparableVersion(version)}` : '', - version, - } -} +import { compareParsed } from '../version/comparison.ts' +import { tryParse } from '../version/parse.ts' +import { formatComparator } from './parse.ts' +import type { SemVer, VersionInput } from '../version/types.ts' +import type { RangeOptions, SemVerComparator } from './types.ts' export function testParsedComparator( comparator: SemVerComparator, @@ -118,15 +76,17 @@ export function parsedComparatorsIntersect( return testComparatorSet([left], right.version, options) } + const leftValue = formatComparator(left) + const rightValue = formatComparator(right) if ( options.includePrerelease && - (left.value === '<0.0.0-0' || right.value === '<0.0.0-0') + (leftValue === '<0.0.0-0' || rightValue === '<0.0.0-0') ) { return false } if ( !options.includePrerelease && - (left.value.startsWith('<0.0.0') || right.value.startsWith('<0.0.0')) + (leftValue.startsWith('<0.0.0') || rightValue.startsWith('<0.0.0')) ) { return false } diff --git a/src/comparator/types.ts b/src/comparator/types.ts new file mode 100644 index 0000000..83c324b --- /dev/null +++ b/src/comparator/types.ts @@ -0,0 +1,14 @@ +import type { SemVer, VersionOptions } from '../version/types.ts' + +export interface RangeOptions extends VersionOptions { + includePrerelease?: boolean +} + +export interface SemVerComparator { + operator: '' | '<' | '<=' | '>' | '>=' + options: RangeOptions + value: string + version: SemVer | null +} + +export type ComparatorInput = SemVerComparator | string diff --git a/src/index.ts b/src/index.ts index f4c7b93..a7f53de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,83 +1,3 @@ -export { - comparatorsIntersect, - normalizeComparator, - satisfiesComparator, -} from './comparator.ts' -export { - compare, - compareBuild, - compareMain, - comparePrerelease, - compareReversed, - compareWithOperator, - isEqual, - isGreater, - isGreaterOrEqual, - isLess, - isLessOrEqual, - isNotEqual, - sort, - sortReversed, -} from './comparison.ts' -export { - INCREMENT_TYPES, - SEMVER_SPEC_VERSION, - TRUNCATION_TYPES, -} from './constants.ts' -export { - compareIdentifiers, - compareIdentifiersReversed, -} from './identifiers.ts' -export { - findMaxSatisfying, - findMinimumForRange, - findMinSatisfying, - isGreaterThanRange, - isLessThanRange, - isOutsideRange, - isRangeSubset, - isValidRange, - normalizeRange, - parseRange, - rangesIntersect, - rangeToComparators, - satisfies, - simplifyRange, - tryParseRange, -} from './range.ts' -export { - clean, - coerce, - difference, - getBuild, - getMajor, - getMinor, - getPatch, - getPrerelease, - increment, - isPrerelease, - isStable, - isValid, - normalize, - normalizeFull, - parse, - truncate, - tryParse, -} from './version.ts' -export type { RangeInput, SemVerRange } from './range.ts' -export type { - CoerceOptions, - Comparison, - ComparisonOperator, - IncrementOptions, - IncrementType, - PrereleaseIdentifier, - RangeDirection, - RangeOptions, - SemVer, - SemVerComparator, - TruncationType, - VersionDifference, - VersionInput, - VersionOptions, -} from './types.ts' +export * from './comparator.ts' +export * from './range.ts' +export * from './version.ts' diff --git a/src/internal/range-patterns.ts b/src/internal/range-patterns.ts deleted file mode 100644 index 910d7bf..0000000 --- a/src/internal/range-patterns.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { - BUILD, - FULL_PLAIN, - GREATER_LESS_THAN, - LONE_CARET, - LONE_TILDE, - LOOSE_PLAIN, - safeRegex, - XRANGE_PLAIN, - XRANGE_PLAIN_LOOSE, -} from './patterns.ts' diff --git a/src/range.ts b/src/range.ts index 37dbde8..e080e84 100644 --- a/src/range.ts +++ b/src/range.ts @@ -1,603 +1,17 @@ -import { sort } from './comparison.ts' -import { - comparatorAllowsPrerelease, - parseComparator, - testComparatorSet, - testParsedComparator, -} from './internal/comparator.ts' -import { - parsedRangesIntersect, - parseRange, - testParsedRange, - testRangeVersion, - tryParseRange, - type ParseRangeInput, - type RangeInput, - type SemVerRange, -} from './internal/range.ts' -import { - compareParsed, - formatComparableVersion, - parse, - tryParse, -} from './internal/version.ts' -import type { - RangeDirection, - RangeOptions, - SemVer, - SemVerComparator, - VersionInput, -} from './types.ts' - -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 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 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 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 as ParseRangeInput)(range, options).sets.map((set) => - set - .map((comparator) => comparator.value) - .join(' ') - .trim() - .split(' '), - ) -} - -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 as TryParseRangeInput)(range, options) - if (!parsedRange) return null - let maximum: T | null = null - let maximumParsed: SemVer | null = null - for (const version of versions) { - const parsedVersion = tryParse(version, parsedRange.options) - if (!parsedVersion || !testParsedRange(parsedRange, parsedVersion)) continue - if (!maximumParsed || compareParsed(maximumParsed, parsedVersion) < 0) { - maximum = version - maximumParsed = parsedVersion - } - } - 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 as TryParseRangeInput)(range, options) - if (!parsedRange) return null - let minimum: T | null = null - let minimumParsed: SemVer | null = null - for (const version of versions) { - const parsedVersion = tryParse(version, parsedRange.options) - if (!parsedVersion || !testParsedRange(parsedRange, parsedVersion)) continue - if (!minimumParsed || compareParsed(minimumParsed, parsedVersion) > 0) { - minimum = version - minimumParsed = parsedVersion - } - } - return minimum -} - -function nextVersionAfter(version: SemVer): SemVer { - const { prerelease } = version - const comparable = formatComparableVersion( - prerelease?.length - ? { - major: version.major, - minor: version.minor, - patch: version.patch, - prerelease: [...prerelease, 0], - } - : { - major: version.major, - minor: version.minor, - patch: version.patch + 1, - }, - ) - return parse(comparable) -} - -const ZERO_VERSION: SemVer = { major: 0, minor: 0, patch: 0 } -const ZERO_PRERELEASE_VERSION: SemVer = { - ...ZERO_VERSION, - prerelease: [0], -} - -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 as ParseRangeInput)(range, options) - if (testParsedRange(parsedRange, ZERO_VERSION)) return '0.0.0' - if (testParsedRange(parsedRange, ZERO_PRERELEASE_VERSION)) return '0.0.0-0' - - let minimum: SemVer | null = null - for (const set of parsedRange.sets) { - let setMinimum: SemVer | null = null - for (const comparator of set) { - if (!comparator.version) continue - const candidate = - comparator.operator === '>' - ? nextVersionAfter(comparator.version) - : comparator.operator === '' || comparator.operator === '>=' - ? comparator.version - : null - if ( - candidate && - (!setMinimum || compareParsed(candidate, setMinimum) > 0) - ) { - setMinimum = candidate - } - } - if (setMinimum && (!minimum || compareParsed(minimum, setMinimum) > 0)) { - minimum = setMinimum - } - } - return minimum && testParsedRange(parsedRange, minimum) - ? formatComparableVersion(minimum) - : null -} - -function compareInDirection( - left: SemVer, - right: SemVer, - direction: RangeDirection, -): boolean { - const comparison = compareParsed(left, right) - return direction === '>' ? comparison > 0 : comparison < 0 -} - -function compareOppositeOrEqual( - left: SemVer, - right: SemVer, - direction: RangeDirection, -): boolean { - const comparison = compareParsed(left, right) - return direction === '>' ? comparison <= 0 : comparison >= 0 -} - -function compareOpposite( - left: SemVer, - right: SemVer, - direction: RangeDirection, -): boolean { - const comparison = compareParsed(left, right) - 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, - direction: RangeDirection, - options: RangeOptions = {}, -): boolean { - if (direction !== '>' && direction !== '<') { - throw new TypeError('Must provide a direction of "<" or ">"') - } - const parsedRange = (parseRange as ParseRangeInput)(range, options) - const parsedVersion = parse(version, parsedRange.options) - if (testParsedRange(parsedRange, parsedVersion)) return false - - const inclusiveDirection = direction === '>' ? '>=' : '<=' - for (const set of parsedRange.sets) { - const concrete = set.map((comparator) => - comparator.version ? comparator : parseComparator('>=0.0.0'), - ) - let high = concrete[0]! - let low = concrete[0]! - for (const comparator of concrete) { - if (compareInDirection(comparator.version!, high.version!, direction)) { - high = comparator - } else if ( - compareOpposite(comparator.version!, low.version!, direction) - ) { - low = comparator - } - } - - if (high.operator === direction || high.operator === inclusiveDirection) { - return false - } - if ( - (!low.operator || low.operator === direction) && - compareOppositeOrEqual(parsedVersion, low.version!, direction) - ) { - return false - } - if ( - low.operator === inclusiveDirection && - compareOpposite(parsedVersion, low.version!, direction) - ) { - return false - } - } - 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 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 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 as ParseRangeInput)(left, options) - const parsedRight = (parseRange as ParseRangeInput)(right, options) - return parsedRangesIntersect(parsedLeft, parsedRight, { - ...parsedLeft.options, - ...parsedRight.options, - ...options, - }) -} - -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 as ParseRangeInput)(range, options) - const sorted = sort(versions, parsedRange.options) - const sets: [T, T | null][] = [] - let first: T | null = null - let previous: T | null = null - for (const version of sorted) { - if (testRangeVersion(parsedRange, version)) { - previous = version - first ??= version - } else if (previous) { - sets.push([first!, previous]) - first = null - previous = null - } - } - if (first) sets.push([first, null]) - - const simplified = sets - .map(([minimum, maximum]) => { - const minimumValue = formatComparableVersion( - parse(minimum, parsedRange.options), - ) - const maximumValue = maximum - ? formatComparableVersion(parse(maximum, parsedRange.options)) - : null - if (minimumValue === maximumValue) return minimumValue - if (!maximum && minimum === sorted[0]) return '*' - if (!maximumValue) return `>=${minimumValue}` - if (minimum === sorted[0]) return `<=${maximumValue}` - return `${minimumValue} - ${maximumValue}` - }) - .join(' || ') - const original = typeof range === 'string' ? range : range.raw - return simplified.length < original.length ? simplified : original -} - -function higherLowerBound( - left: SemVerComparator | undefined, - right: SemVerComparator, -): SemVerComparator { - if (!left) return right - const comparison = compareParsed(left.version!, right.version!) - return comparison > 0 - ? left - : comparison < 0 || (right.operator === '>' && left.operator === '>=') - ? right - : left -} - -function lowerUpperBound( - left: SemVerComparator | undefined, - right: SemVerComparator, -): SemVerComparator { - if (!left) return right - const comparison = compareParsed(left.version!, right.version!) - return comparison < 0 - ? left - : comparison > 0 || (right.operator === '<' && left.operator === '<=') - ? right - : left -} - -function simpleRangeSubset( - rawSubset: readonly SemVerComparator[], - rawSuperset: readonly SemVerComparator[], - options: RangeOptions, -): boolean | null { - let subset = rawSubset - let superset = rawSuperset - const subsetAny = subset.length === 1 && !subset[0]!.version - const supersetAny = superset.length === 1 && !superset[0]!.version - if (subsetAny) { - if (supersetAny) return true - subset = [ - parseComparator(options.includePrerelease ? '>=0.0.0-0' : '>=0.0.0'), - ] - } - if (supersetAny) { - if (options.includePrerelease) return true - superset = [parseComparator('>=0.0.0')] - } - - const equal = new Map() - let lower: SemVerComparator | undefined - let upper: SemVerComparator | undefined - for (const comparator of subset) { - if (comparator.operator === '>' || comparator.operator === '>=') { - lower = higherLowerBound(lower, comparator) - } else if (comparator.operator === '<' || comparator.operator === '<=') { - upper = lowerUpperBound(upper, comparator) - } else if (comparator.version) { - equal.set(formatComparableVersion(comparator.version), comparator.version) - } - } - if (equal.size > 1) return null - - let boundsComparison: number | undefined - if (lower && upper) { - boundsComparison = compareParsed(lower.version!, upper.version!) - if (boundsComparison > 0) return null - if ( - boundsComparison === 0 && - (lower.operator !== '>=' || upper.operator !== '<=') - ) { - return null - } - } - - for (const version of equal.values()) { - if (lower && !testParsedComparator(lower, version)) return null - if (upper && !testParsedComparator(upper, version)) return null - // Testing each superset comparator on its own would skip the prerelease rule. - return testComparatorSet(superset, version, options) - } - - let needsLowerPrerelease = - lower && !options.includePrerelease && lower.version!.prerelease?.length - ? lower.version! - : null - let needsUpperPrerelease = - upper && !options.includePrerelease && upper.version!.prerelease?.length - ? upper.version! - : null - const upperPrerelease = needsUpperPrerelease?.prerelease - if ( - upperPrerelease?.length === 1 && - upper?.operator === '<' && - upperPrerelease[0] === 0 - ) { - needsUpperPrerelease = null - } - - let hasSupersetLower = false - let hasSupersetUpper = false - for (const comparator of superset) { - hasSupersetLower ||= - comparator.operator === '>' || comparator.operator === '>=' - hasSupersetUpper ||= - comparator.operator === '<' || comparator.operator === '<=' - if (lower) { - if ( - needsLowerPrerelease && - comparatorAllowsPrerelease(comparator, needsLowerPrerelease) - ) { - needsLowerPrerelease = null - } - if (comparator.operator === '>' || comparator.operator === '>=') { - const higher = higherLowerBound(lower, comparator) - if (higher === comparator && higher !== lower) return false - } else if ( - lower.operator === '>=' && - !testParsedComparator(comparator, lower.version!) - ) { - return false - } - } - if (upper) { - if ( - needsUpperPrerelease && - comparatorAllowsPrerelease(comparator, needsUpperPrerelease) - ) { - needsUpperPrerelease = null - } - if (comparator.operator === '<' || comparator.operator === '<=') { - const lowerBound = lowerUpperBound(upper, comparator) - if (lowerBound === comparator && lowerBound !== upper) return false - } else if ( - upper.operator === '<=' && - !testParsedComparator(comparator, upper.version!) - ) { - return false - } - } - if ( - comparator.operator === '' && - (lower || upper) && - boundsComparison !== 0 - ) { - return false - } - } - - if (lower && hasSupersetUpper && !upper && boundsComparison !== 0) - return false - if (upper && hasSupersetLower && !lower && boundsComparison !== 0) - return false - 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 as ParseRangeInput)(subset, options) - const parsedSuperset = (parseRange as ParseRangeInput)(superset, options) - const effectiveOptions = { - ...parsedSubset.options, - ...parsedSuperset.options, - ...options, - } - let sawNonNull = false - - for (const subsetSet of parsedSubset.sets) { - let matched = false - for (const supersetSet of parsedSuperset.sets) { - const result = simpleRangeSubset(subsetSet, supersetSet, effectiveOptions) - sawNonNull ||= result !== null - if (result) { - matched = true - break - } - } - if (!matched && sawNonNull) return false - } - return true -} +export { + findMaxSatisfying, + findMinimumForRange, + findMinSatisfying, + isGreaterThanRange, + isLessThanRange, + isOutsideRange, + isValidRange, + normalizeRange, + rangesIntersect, + rangeToComparators, + satisfies, + simplifyRange, +} from './range/operations.ts' +export { parseRange, tryParseRange } from './range/parse.ts' +export { isRangeSubset } from './range/subset.ts' +export type { RangeDirection, RangeInput, SemVerRange } from './range/types.ts' diff --git a/src/range/operations.ts b/src/range/operations.ts new file mode 100644 index 0000000..432727a --- /dev/null +++ b/src/range/operations.ts @@ -0,0 +1,384 @@ +import { formatComparator, parseComparator } from '../comparator/parse.ts' +import { compareParsed, sort } from '../version/comparison.ts' +import { formatComparableVersion, parse, tryParse } from '../version/parse.ts' +import { + formatRange, + parsedRangesIntersect, + parseRange, + testParsedRange, + testRangeVersion, + tryParseRange, + type ParseRangeInput, +} from './parse.ts' +import type { RangeOptions } from '../comparator/types.ts' +import type { SemVer, VersionInput } from '../version/types.ts' +import type { RangeDirection, RangeInput, SemVerRange } from './types.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 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 as TryParseRangeInput)(range, options) + return parsed ? formatRange(parsed) || '*' : 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 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 as ParseRangeInput)(range, options).sets.map((set) => + set.map(formatComparator).join(' ').trim().split(' '), + ) +} + +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 as TryParseRangeInput)(range, options) + if (!parsedRange) return null + let maximum: T | null = null + let maximumParsed: SemVer | null = null + for (const version of versions) { + const parsedVersion = tryParse(version, parsedRange.options) + if (!parsedVersion || !testParsedRange(parsedRange, parsedVersion)) continue + if (!maximumParsed || compareParsed(maximumParsed, parsedVersion) < 0) { + maximum = version + maximumParsed = parsedVersion + } + } + 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 as TryParseRangeInput)(range, options) + if (!parsedRange) return null + let minimum: T | null = null + let minimumParsed: SemVer | null = null + for (const version of versions) { + const parsedVersion = tryParse(version, parsedRange.options) + if (!parsedVersion || !testParsedRange(parsedRange, parsedVersion)) continue + if (!minimumParsed || compareParsed(minimumParsed, parsedVersion) > 0) { + minimum = version + minimumParsed = parsedVersion + } + } + return minimum +} + +function nextVersionAfter(version: SemVer): SemVer { + const { prerelease } = version + return prerelease?.length + ? { + major: version.major, + minor: version.minor, + patch: version.patch, + prerelease: [...prerelease, 0], + } + : { + major: version.major, + minor: version.minor, + patch: version.patch + 1, + } +} + +export function findMinimumForRange( + range: string, + options?: RangeOptions, +): SemVer | null +export function findMinimumForRange(range: RangeInput): SemVer | null +export function findMinimumForRange( + range: RangeInput, + options: RangeOptions = {}, +): SemVer | null { + const parsedRange = (parseRange as ParseRangeInput)(range, options) + const zero: SemVer = { major: 0, minor: 0, patch: 0 } + if (testParsedRange(parsedRange, zero)) return zero + const zeroPrerelease: SemVer = { ...zero, prerelease: [0] } + if (testParsedRange(parsedRange, zeroPrerelease)) return zeroPrerelease + + let minimum: SemVer | null = null + for (const set of parsedRange.sets) { + let setMinimum: SemVer | null = null + for (const comparator of set) { + if (!comparator.version) continue + const candidate = + comparator.operator === '>' + ? nextVersionAfter(comparator.version) + : comparator.operator === '' || comparator.operator === '>=' + ? structuredClone(comparator.version) + : null + if ( + candidate && + (!setMinimum || compareParsed(candidate, setMinimum) > 0) + ) { + setMinimum = candidate + } + } + if (setMinimum && (!minimum || compareParsed(minimum, setMinimum) > 0)) { + minimum = setMinimum + } + } + return minimum && testParsedRange(parsedRange, minimum) ? minimum : null +} + +function compareInDirection( + left: SemVer, + right: SemVer, + direction: RangeDirection, +): boolean { + const comparison = compareParsed(left, right) + return direction === '>' ? comparison > 0 : comparison < 0 +} + +function compareOppositeOrEqual( + left: SemVer, + right: SemVer, + direction: RangeDirection, +): boolean { + const comparison = compareParsed(left, right) + return direction === '>' ? comparison <= 0 : comparison >= 0 +} + +function compareOpposite( + left: SemVer, + right: SemVer, + direction: RangeDirection, +): boolean { + const comparison = compareParsed(left, right) + 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, + direction: RangeDirection, + options: RangeOptions = {}, +): boolean { + if (direction !== '>' && direction !== '<') { + throw new TypeError('Must provide a direction of "<" or ">"') + } + const parsedRange = (parseRange as ParseRangeInput)(range, options) + const parsedVersion = parse(version, parsedRange.options) + if (testParsedRange(parsedRange, parsedVersion)) return false + + const inclusiveDirection = direction === '>' ? '>=' : '<=' + for (const set of parsedRange.sets) { + const concrete = set.map((comparator) => + comparator.version ? comparator : parseComparator('>=0.0.0'), + ) + let high = concrete[0]! + let low = concrete[0]! + for (const comparator of concrete) { + if (compareInDirection(comparator.version!, high.version!, direction)) { + high = comparator + } else if ( + compareOpposite(comparator.version!, low.version!, direction) + ) { + low = comparator + } + } + + if (high.operator === direction || high.operator === inclusiveDirection) { + return false + } + if ( + (!low.operator || low.operator === direction) && + compareOppositeOrEqual(parsedVersion, low.version!, direction) + ) { + return false + } + if ( + low.operator === inclusiveDirection && + compareOpposite(parsedVersion, low.version!, direction) + ) { + return false + } + } + 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 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 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 as ParseRangeInput)(left, options) + const parsedRight = (parseRange as ParseRangeInput)(right, options) + return parsedRangesIntersect(parsedLeft, parsedRight, { + ...parsedLeft.options, + ...parsedRight.options, + ...options, + }) +} + +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 as ParseRangeInput)(range, options) + const sorted = sort(versions, parsedRange.options) + const sets: [T, T | null][] = [] + let first: T | null = null + let previous: T | null = null + for (const version of sorted) { + if (testRangeVersion(parsedRange, version)) { + previous = version + first ??= version + } else if (previous) { + sets.push([first!, previous]) + first = null + previous = null + } + } + if (first) sets.push([first, null]) + + const simplified = sets + .map(([minimum, maximum]) => { + const minimumValue = formatComparableVersion( + parse(minimum, parsedRange.options), + ) + const maximumValue = maximum + ? formatComparableVersion(parse(maximum, parsedRange.options)) + : null + if (minimumValue === maximumValue) return minimumValue + if (!maximum && minimum === sorted[0]) return '*' + if (!maximumValue) return `>=${minimumValue}` + if (minimum === sorted[0]) return `<=${maximumValue}` + return `${minimumValue} - ${maximumValue}` + }) + .join(' || ') + const original = + typeof range === 'string' ? range : formatRange(parsedRange) || '*' + return simplified.length < original.length ? simplified : original +} diff --git a/src/internal/range.ts b/src/range/parse.ts similarity index 94% rename from src/internal/range.ts rename to src/range/parse.ts index 835ec96..ffe4107 100644 --- a/src/internal/range.ts +++ b/src/range/parse.ts @@ -1,8 +1,9 @@ +import { formatComparator, parseComparator } from '../comparator/parse.ts' import { - parseComparator, parsedComparatorsIntersect, testComparatorSet, -} from './comparator.ts' +} from '../comparator/set.ts' +import { tryParse } from '../version/parse.ts' import { BUILD, GREATER_LESS_THAN, @@ -12,29 +13,20 @@ import { safeRegex, XRANGE_PLAIN, XRANGE_PLAIN_LOOSE, -} from './range-patterns.ts' -import { tryParse } from './version.ts' -import type { - RangeOptions, - SemVer, - SemVerComparator, - VersionInput, -} from '../types.ts' - -export interface SemVerRange { - normalized: string - options: RangeOptions - raw: string - sets: SemVerComparator[][] -} - -export type RangeInput = SemVerRange | string +} from '../version/patterns.ts' +import type { RangeOptions, SemVerComparator } from '../comparator/types.ts' +import type { SemVer, VersionInput } from '../version/types.ts' +import type { RangeInput, SemVerRange } from './types.ts' export type ParseRangeInput = ( range: RangeInput, options?: RangeOptions, ) => SemVerRange +export function formatRange(range: SemVerRange): string { + return range.sets.map((set) => set.map(formatComparator).join(' ')).join('||') +} + const BUILD_STRIP = new RegExp(BUILD, 'g') const BUILD_SAFE = safeRegex(BUILD) const STRICT_HYPHEN = safeRegex( @@ -313,13 +305,15 @@ export function parseRange( ): SemVerRange { if (typeof range !== 'string') return range const parsedOptions = { ...options } - const raw = range.trim().replaceAll(/\s+/g, ' ') - let sets = raw + const normalizedRange = range.trim().replaceAll(/\s+/g, ' ') + let sets = normalizedRange .split('||') .map((part) => parseSimpleRange(part.trim(), parsedOptions)) .filter((set) => set.length) if (!sets.length) { - throw new TypeError(`Range contains no valid comparator sets: ${raw}`) + throw new TypeError( + `Range contains no valid comparator sets: ${normalizedRange}`, + ) } if (sets.length > 1) { @@ -333,11 +327,7 @@ export function parseRange( } return { - normalized: sets - .map((set) => set.map((comparator) => comparator.value).join(' ')) - .join('||'), options: parsedOptions, - raw, sets, } } diff --git a/src/range/subset.ts b/src/range/subset.ts new file mode 100644 index 0000000..6554a4f --- /dev/null +++ b/src/range/subset.ts @@ -0,0 +1,201 @@ +import { parseComparator } from '../comparator/parse.ts' +import { + comparatorAllowsPrerelease, + testComparatorSet, + testParsedComparator, +} from '../comparator/set.ts' +import { compareParsed } from '../version/comparison.ts' +import { formatComparableVersion } from '../version/parse.ts' +import { parseRange, type ParseRangeInput } from './parse.ts' +import type { RangeOptions, SemVerComparator } from '../comparator/types.ts' +import type { SemVer } from '../version/types.ts' +import type { RangeInput } from './types.ts' + +function higherLowerBound( + left: SemVerComparator | undefined, + right: SemVerComparator, +): SemVerComparator { + if (!left) return right + const comparison = compareParsed(left.version!, right.version!) + return comparison > 0 + ? left + : comparison < 0 || (right.operator === '>' && left.operator === '>=') + ? right + : left +} + +function lowerUpperBound( + left: SemVerComparator | undefined, + right: SemVerComparator, +): SemVerComparator { + if (!left) return right + const comparison = compareParsed(left.version!, right.version!) + return comparison < 0 + ? left + : comparison > 0 || (right.operator === '<' && left.operator === '<=') + ? right + : left +} + +function simpleRangeSubset( + rawSubset: readonly SemVerComparator[], + rawSuperset: readonly SemVerComparator[], + options: RangeOptions, +): boolean | null { + let subset = rawSubset + let superset = rawSuperset + const subsetAny = subset.length === 1 && !subset[0]!.version + const supersetAny = superset.length === 1 && !superset[0]!.version + if (subsetAny) { + if (supersetAny) return true + subset = [ + parseComparator(options.includePrerelease ? '>=0.0.0-0' : '>=0.0.0'), + ] + } + if (supersetAny) { + if (options.includePrerelease) return true + superset = [parseComparator('>=0.0.0')] + } + + const equal = new Map() + let lower: SemVerComparator | undefined + let upper: SemVerComparator | undefined + for (const comparator of subset) { + if (comparator.operator === '>' || comparator.operator === '>=') { + lower = higherLowerBound(lower, comparator) + } else if (comparator.operator === '<' || comparator.operator === '<=') { + upper = lowerUpperBound(upper, comparator) + } else if (comparator.version) { + equal.set(formatComparableVersion(comparator.version), comparator.version) + } + } + if (equal.size > 1) return null + + let boundsComparison: number | undefined + if (lower && upper) { + boundsComparison = compareParsed(lower.version!, upper.version!) + if (boundsComparison > 0) return null + if ( + boundsComparison === 0 && + (lower.operator !== '>=' || upper.operator !== '<=') + ) { + return null + } + } + + for (const version of equal.values()) { + if (lower && !testParsedComparator(lower, version)) return null + if (upper && !testParsedComparator(upper, version)) return null + // Testing each superset comparator on its own would skip the prerelease rule. + return testComparatorSet(superset, version, options) + } + + let needsLowerPrerelease = + lower && !options.includePrerelease && lower.version!.prerelease?.length + ? lower.version! + : null + let needsUpperPrerelease = + upper && !options.includePrerelease && upper.version!.prerelease?.length + ? upper.version! + : null + const upperPrerelease = needsUpperPrerelease?.prerelease + if ( + upperPrerelease?.length === 1 && + upper?.operator === '<' && + upperPrerelease[0] === 0 + ) { + needsUpperPrerelease = null + } + + let hasSupersetLower = false + let hasSupersetUpper = false + for (const comparator of superset) { + hasSupersetLower ||= + comparator.operator === '>' || comparator.operator === '>=' + hasSupersetUpper ||= + comparator.operator === '<' || comparator.operator === '<=' + if (lower) { + if ( + needsLowerPrerelease && + comparatorAllowsPrerelease(comparator, needsLowerPrerelease) + ) { + needsLowerPrerelease = null + } + if (comparator.operator === '>' || comparator.operator === '>=') { + const higher = higherLowerBound(lower, comparator) + if (higher === comparator && higher !== lower) return false + } else if ( + lower.operator === '>=' && + !testParsedComparator(comparator, lower.version!) + ) { + return false + } + } + if (upper) { + if ( + needsUpperPrerelease && + comparatorAllowsPrerelease(comparator, needsUpperPrerelease) + ) { + needsUpperPrerelease = null + } + if (comparator.operator === '<' || comparator.operator === '<=') { + const lowerBound = lowerUpperBound(upper, comparator) + if (lowerBound === comparator && lowerBound !== upper) return false + } else if ( + upper.operator === '<=' && + !testParsedComparator(comparator, upper.version!) + ) { + return false + } + } + if ( + comparator.operator === '' && + (lower || upper) && + boundsComparison !== 0 + ) { + return false + } + } + + if (lower && hasSupersetUpper && !upper && boundsComparison !== 0) + return false + if (upper && hasSupersetLower && !lower && boundsComparison !== 0) + return false + 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 as ParseRangeInput)(subset, options) + const parsedSuperset = (parseRange as ParseRangeInput)(superset, options) + const effectiveOptions = { + ...parsedSubset.options, + ...parsedSuperset.options, + ...options, + } + let sawNonNull = false + + for (const subsetSet of parsedSubset.sets) { + let matched = false + for (const supersetSet of parsedSuperset.sets) { + const result = simpleRangeSubset(subsetSet, supersetSet, effectiveOptions) + sawNonNull ||= result !== null + if (result) { + matched = true + break + } + } + if (!matched && sawNonNull) return false + } + return true +} diff --git a/src/range/types.ts b/src/range/types.ts new file mode 100644 index 0000000..7289441 --- /dev/null +++ b/src/range/types.ts @@ -0,0 +1,10 @@ +import type { RangeOptions, SemVerComparator } from '../comparator/types.ts' + +export interface SemVerRange { + options: RangeOptions + sets: SemVerComparator[][] +} + +export type RangeInput = SemVerRange | string + +export type RangeDirection = '<' | '>' diff --git a/src/version.ts b/src/version.ts index 6363567..85a4603 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,187 +1,56 @@ -import { TRUNCATION_TYPES } from './constants.ts' -import { - coerceParsedVersion, - compareMainParsed, - compareParsed, - formatComparableVersion, - formatFullVersion, - incrementParsedVersion, - parse, - tryParse, -} from './internal/version.ts' -import type { +export { + compare, + compareBuild, + compareMain, + comparePrerelease, + compareReversed, + compareWithOperator, + isEqual, + isGreater, + isGreaterOrEqual, + isLess, + isLessOrEqual, + isNotEqual, + sort, + sortReversed, +} from './version/comparison.ts' +export { + INCREMENT_TYPES, + SEMVER_SPEC_VERSION, + TRUNCATION_TYPES, +} from './version/constants.ts' +export { + compareIdentifiers, + compareIdentifiersReversed, +} from './version/identifiers.ts' +export { + clean, + coerce, + difference, + getBuild, + getMajor, + getMinor, + getPatch, + getPrerelease, + increment, + isPrerelease, + isStable, + isValid, + normalize, + normalizeFull, + truncate, +} from './version/operations.ts' +export { parse, tryParse } from './version/parse.ts' +export type { CoerceOptions, + Comparison, + ComparisonOperator, IncrementOptions, IncrementType, PrereleaseIdentifier, + SemVer, TruncationType, VersionDifference, VersionInput, VersionOptions, -} from './types.ts' - -export { parse, tryParse } - -export function isValid( - version: VersionInput, - options: VersionOptions = {}, -): boolean { - return tryParse(version, options) !== null -} - -export function isPrerelease( - version: VersionInput, - options: VersionOptions = {}, -): boolean | null { - const parsed = tryParse(version, options) - if (!parsed) return null - return !!parsed.prerelease?.length -} - -export function isStable( - version: VersionInput, - options: VersionOptions = {}, -): boolean | null { - const parsed = tryParse(version, options) - if (!parsed) return null - return !parsed.prerelease?.length -} - -export function normalizeFull( - version: VersionInput, - options: VersionOptions = {}, -): string | null { - const parsed = tryParse(version, options) - return parsed ? formatFullVersion(parsed) : null -} - -export function normalize( - version: VersionInput, - options: VersionOptions = {}, -): string | null { - const parsed = tryParse(version, options) - return parsed ? formatComparableVersion(parsed) : null -} - -export function clean( - version: VersionInput, - options: VersionOptions = {}, -): string | null { - if (typeof version !== 'string') { - return normalize(version, options) - } - return normalize(version.trim().replace(/^[=v]+/, ''), options) -} - -export function coerce( - value: number | VersionInput, - options: CoerceOptions = {}, -): string | null { - const parsed = coerceParsedVersion(value, options) - return parsed ? formatFullVersion(parsed) : null -} - -export function increment( - version: VersionInput, - release: IncrementType, - options: IncrementOptions = {}, -): string | null { - try { - return incrementParsedVersion( - parse(version, options), - release, - options.identifier, - options.identifierBase, - options.loose, - ) - } catch { - return null - } -} - -export function truncate( - version: VersionInput, - truncation: TruncationType, - options: VersionOptions = {}, -): string | null { - if (!TRUNCATION_TYPES.includes(truncation)) return null - const parsed = tryParse(version, options) - if (!parsed) return null - if (truncation.startsWith('pre')) return formatComparableVersion(parsed) - - return formatComparableVersion({ - major: parsed.major, - minor: truncation === 'major' ? 0 : parsed.minor, - patch: truncation === 'major' || truncation === 'minor' ? 0 : parsed.patch, - }) -} - -export function difference( - left: VersionInput, - right: VersionInput, -): VersionDifference | null { - const leftVersion = parse(left) - const rightVersion = parse(right) - const comparison = compareParsed(leftVersion, rightVersion) - if (comparison === 0) return null - - const high = comparison > 0 ? leftVersion : rightVersion - const low = comparison > 0 ? rightVersion : leftVersion - const highHasPrerelease = high.prerelease?.length - const lowHasPrerelease = low.prerelease?.length - if (lowHasPrerelease && !highHasPrerelease) { - if (low.patch === 0 && low.minor === 0) return 'major' - if (compareMainParsed(low, high) === 0) { - return low.minor !== 0 && low.patch === 0 ? 'minor' : 'patch' - } - } - - const prefix = highHasPrerelease ? 'pre' : '' - if (leftVersion.major !== rightVersion.major) { - return `${prefix}major` as VersionDifference - } - if (leftVersion.minor !== rightVersion.minor) { - return `${prefix}minor` as VersionDifference - } - if (leftVersion.patch !== rightVersion.patch) { - return `${prefix}patch` as VersionDifference - } - return 'prerelease' -} - -export function getMajor( - version: VersionInput, - options: VersionOptions = {}, -): number { - return parse(version, options).major -} - -export function getMinor( - version: VersionInput, - options: VersionOptions = {}, -): number { - return parse(version, options).minor -} - -export function getPatch( - version: VersionInput, - options: VersionOptions = {}, -): number { - return parse(version, options).patch -} - -export function getPrerelease( - version: VersionInput, - options: VersionOptions = {}, -): PrereleaseIdentifier[] | null { - const parsed = tryParse(version, options) - return parsed ? [...(parsed.prerelease || [])] : null -} - -export function getBuild( - version: VersionInput, - options: VersionOptions = {}, -): string[] | null { - const parsed = tryParse(version, options) - return parsed ? [...(parsed.build || [])] : null -} +} from './version/types.ts' diff --git a/src/comparison.ts b/src/version/comparison.ts similarity index 60% rename from src/comparison.ts rename to src/version/comparison.ts index 78a6e28..ce4c928 100644 --- a/src/comparison.ts +++ b/src/version/comparison.ts @@ -1,17 +1,71 @@ -import { - compareBuildParsed, - compareMainParsed, - compareParsed, - comparePrereleaseParsed, - parse, -} from './internal/version.ts' +import { compareIdentifiers } from './identifiers.ts' +import { parse } from './parse.ts' import type { Comparison, ComparisonOperator, + SemVer, VersionInput, VersionOptions, } from './types.ts' +export function compareMainParsed(left: SemVer, right: SemVer): Comparison { + return left.major === right.major + ? left.minor === right.minor + ? left.patch === right.patch + ? 0 + : left.patch < right.patch + ? -1 + : 1 + : left.minor < right.minor + ? -1 + : 1 + : left.major < right.major + ? -1 + : 1 +} + +export function comparePrereleaseParsed( + left: SemVer, + right: SemVer, +): Comparison { + const leftPrerelease = left.prerelease + const rightPrerelease = right.prerelease + if (leftPrerelease?.length && !rightPrerelease?.length) return -1 + if (!leftPrerelease?.length && rightPrerelease?.length) return 1 + if (!leftPrerelease?.length && !rightPrerelease?.length) return 0 + + for (let index = 0; ; index++) { + const leftIdentifier = leftPrerelease?.[index] + const rightIdentifier = rightPrerelease?.[index] + if (leftIdentifier === undefined && rightIdentifier === undefined) return 0 + if (rightIdentifier === undefined) return 1 + if (leftIdentifier === undefined) return -1 + if (leftIdentifier !== rightIdentifier) { + return compareIdentifiers(leftIdentifier, rightIdentifier) + } + } +} + +export function compareParsed(left: SemVer, right: SemVer): Comparison { + return compareMainParsed(left, right) || comparePrereleaseParsed(left, right) +} + +export function compareBuildParsed(left: SemVer, right: SemVer): Comparison { + const precedence = compareParsed(left, right) + if (precedence !== 0) return precedence + + for (let index = 0; ; index++) { + const leftIdentifier = left.build?.[index] + const rightIdentifier = right.build?.[index] + if (leftIdentifier === undefined && rightIdentifier === undefined) return 0 + if (rightIdentifier === undefined) return 1 + if (leftIdentifier === undefined) return -1 + if (leftIdentifier !== rightIdentifier) { + return compareIdentifiers(leftIdentifier, rightIdentifier) + } + } +} + export function compare( left: VersionInput, right: VersionInput, diff --git a/src/constants.ts b/src/version/constants.ts similarity index 100% rename from src/constants.ts rename to src/version/constants.ts diff --git a/src/identifiers.ts b/src/version/identifiers.ts similarity index 100% rename from src/identifiers.ts rename to src/version/identifiers.ts diff --git a/src/internal/version.ts b/src/version/operations.ts similarity index 58% rename from src/internal/version.ts rename to src/version/operations.ts index 962b93e..337d0bf 100644 --- a/src/internal/version.ts +++ b/src/version/operations.ts @@ -1,165 +1,124 @@ -import { MAX_LENGTH } from '../constants.ts' -import { compareIdentifiers } from '../identifiers.ts' +import { compareMainParsed, compareParsed } from './comparison.ts' +import { TRUNCATION_TYPES } from './constants.ts' +import { compareIdentifiers } from './identifiers.ts' +import { + formatComparableVersion, + formatFullVersion, + parse, + tryParse, +} from './parse.ts' import { COERCE, COERCE_FULL, - FULL_PLAIN, - LOOSE_PLAIN, PRERELEASE, PRERELEASE_LOOSE, safeRegex, } from './patterns.ts' import type { - Comparison, + CoerceOptions, + IncrementOptions, IncrementType, PrereleaseIdentifier, SemVer, + TruncationType, + VersionDifference, VersionInput, VersionOptions, -} from '../types.ts' +} from './types.ts' -const FULL = safeRegex(`^${FULL_PLAIN}$`) -const LOOSE = safeRegex(`^${LOOSE_PLAIN}$`) -const PRERELEASE_EXACT = safeRegex(`^${PRERELEASE}$`) -const PRERELEASE_LOOSE_EXACT = safeRegex(`^${PRERELEASE_LOOSE}$`) const COERCE_EXACT = safeRegex(COERCE) const COERCE_FULL_EXACT = safeRegex(COERCE_FULL) -const NUMERIC = /^\d+$/ +const PRERELEASE_EXACT = safeRegex(`^${PRERELEASE}$`) +const PRERELEASE_LOOSE_EXACT = safeRegex(`^${PRERELEASE_LOOSE}$`) -export function formatComparableVersion( - version: Pick, -): string { - const base = `${version.major}.${version.minor}.${version.patch}` - return version.prerelease?.length - ? `${base}-${version.prerelease.join('.')}` - : base +export function isValid( + version: VersionInput, + options: VersionOptions = {}, +): boolean { + return tryParse(version, options) !== null } -export function formatFullVersion( - version: Pick, -): string { - const comparable = formatComparableVersion(version) - return version.build?.length - ? `${comparable}+${version.build.join('.')}` - : comparable +export function isPrerelease( + version: VersionInput, + options: VersionOptions = {}, +): boolean | null { + const parsed = tryParse(version, options) + if (!parsed) return null + return !!parsed.prerelease?.length } -export function parse( +export function isStable( version: VersionInput, options: VersionOptions = {}, -): SemVer { - if (typeof version !== 'string') return version - - if (version.length > MAX_LENGTH) { - throw new TypeError( - `Version exceeds the maximum length of ${MAX_LENGTH} characters`, - ) - } - - const match = version.trim().match(options.loose ? LOOSE : FULL) - if (!match) throw new TypeError(`Invalid version syntax: ${version}`) - - const major = Number(match[1]) - const minor = Number(match[2]) - const patch = Number(match[3]) - if (major > Number.MAX_SAFE_INTEGER || major < 0) { - throw new TypeError(`Invalid major version: ${match[1]}`) - } - if (minor > Number.MAX_SAFE_INTEGER || minor < 0) { - throw new TypeError(`Invalid minor version: ${match[2]}`) - } - if (patch > Number.MAX_SAFE_INTEGER || patch < 0) { - throw new TypeError(`Invalid patch version: ${match[3]}`) - } - - const prerelease = match[4] - ? match[4].split('.').map((identifier) => { - if (NUMERIC.test(identifier)) { - const numeric = Number(identifier) - if (numeric >= 0 && numeric < Number.MAX_SAFE_INTEGER) { - return numeric - } - } - return identifier - }) - : undefined - const build = match[5]?.split('.') - return { - build, - major, - minor, - patch, - prerelease, - } +): boolean | null { + const parsed = tryParse(version, options) + if (!parsed) return null + return !parsed.prerelease?.length } -export function tryParse( +export function normalizeFull( version: VersionInput, options: VersionOptions = {}, -): SemVer | null { - try { - return parse(version, options) - } catch { - return null - } +): string | null { + const parsed = tryParse(version, options) + return parsed ? formatFullVersion(parsed) : null } -export function compareMainParsed(left: SemVer, right: SemVer): Comparison { - return left.major === right.major - ? left.minor === right.minor - ? left.patch === right.patch - ? 0 - : left.patch < right.patch - ? -1 - : 1 - : left.minor < right.minor - ? -1 - : 1 - : left.major < right.major - ? -1 - : 1 +export function normalize( + version: VersionInput, + options: VersionOptions = {}, +): string | null { + const parsed = tryParse(version, options) + return parsed ? formatComparableVersion(parsed) : null } -export function comparePrereleaseParsed( - left: SemVer, - right: SemVer, -): Comparison { - const leftPrerelease = left.prerelease - const rightPrerelease = right.prerelease - if (leftPrerelease?.length && !rightPrerelease?.length) return -1 - if (!leftPrerelease?.length && rightPrerelease?.length) return 1 - if (!leftPrerelease?.length && !rightPrerelease?.length) return 0 - - for (let index = 0; ; index++) { - const leftIdentifier = leftPrerelease?.[index] - const rightIdentifier = rightPrerelease?.[index] - if (leftIdentifier === undefined && rightIdentifier === undefined) return 0 - if (rightIdentifier === undefined) return 1 - if (leftIdentifier === undefined) return -1 - if (leftIdentifier !== rightIdentifier) { - return compareIdentifiers(leftIdentifier, rightIdentifier) - } +export function clean( + version: VersionInput, + options: VersionOptions = {}, +): string | null { + if (typeof version !== 'string') { + return normalize(version, options) } + return normalize(version.trim().replace(/^[=v]+/, ''), options) } -export function compareParsed(left: SemVer, right: SemVer): Comparison { - return compareMainParsed(left, right) || comparePrereleaseParsed(left, right) -} - -export function compareBuildParsed(left: SemVer, right: SemVer): Comparison { - const precedence = compareParsed(left, right) - if (precedence !== 0) return precedence +export function coerce( + value: number | VersionInput, + options: CoerceOptions = {}, +): SemVer | null { + if (typeof value === 'object') return value + const input = typeof value === 'number' ? String(value) : value + let match: RegExpExecArray | null = null - for (let index = 0; ; index++) { - const leftIdentifier = left.build?.[index] - const rightIdentifier = right.build?.[index] - if (leftIdentifier === undefined && rightIdentifier === undefined) return 0 - if (rightIdentifier === undefined) return 1 - if (leftIdentifier === undefined) return -1 - if (leftIdentifier !== rightIdentifier) { - return compareIdentifiers(leftIdentifier, rightIdentifier) + if (options.rtl) { + const source = options.includePrerelease ? COERCE_FULL : COERCE + const expression = safeRegex(source, 'g') + let next: RegExpExecArray | null + while ( + (next = expression.exec(input)) && + (!match || match.index + match[0].length !== input.length) + ) { + if ( + !match || + next.index + next[0].length !== match.index + match[0].length + ) { + match = next + } + expression.lastIndex = next.index + next[1]!.length + next[2]!.length } + } else { + match = (options.includePrerelease ? COERCE_FULL_EXACT : COERCE_EXACT).exec( + input, + ) } + if (!match) return null + + const major = match[2] + const minor = match[3] || '0' + const patch = match[4] || '0' + const prerelease = options.includePrerelease && match[5] ? `-${match[5]}` : '' + const build = options.includePrerelease && match[6] ? `+${match[6]}` : '' + return tryParse(`${major}.${minor}.${patch}${prerelease}${build}`, options) } function isPrereleasePrefix( @@ -283,7 +242,7 @@ function incrementMutable( } } -export function incrementParsedVersion( +function incrementParsedVersion( parsed: SemVer, release: IncrementType, identifier?: string, @@ -314,46 +273,107 @@ export function incrementParsedVersion( return formatComparableVersion(mutable) } -export function coerceParsedVersion( - value: number | VersionInput, - options: { - includePrerelease?: boolean - loose?: boolean - rtl?: boolean - } = {}, -): SemVer | null { - if (typeof value === 'object') return value - const input = typeof value === 'number' ? String(value) : value - if (typeof input !== 'string') return null - let match: RegExpExecArray | null = null +export function increment( + version: VersionInput, + release: IncrementType, + options: IncrementOptions = {}, +): string | null { + try { + return incrementParsedVersion( + parse(version, options), + release, + options.identifier, + options.identifierBase, + options.loose, + ) + } catch { + return null + } +} - if (options.rtl) { - const source = options.includePrerelease ? COERCE_FULL : COERCE - const expression = safeRegex(source, 'g') - let next: RegExpExecArray | null - while ( - (next = expression.exec(input)) && - (!match || match.index + match[0].length !== input.length) - ) { - if ( - !match || - next.index + next[0].length !== match.index + match[0].length - ) { - match = next - } - expression.lastIndex = next.index + next[1]!.length + next[2]!.length +export function truncate( + version: VersionInput, + truncation: TruncationType, + options: VersionOptions = {}, +): string | null { + if (!TRUNCATION_TYPES.includes(truncation)) return null + const parsed = tryParse(version, options) + if (!parsed) return null + if (truncation.startsWith('pre')) return formatComparableVersion(parsed) + + return formatComparableVersion({ + major: parsed.major, + minor: truncation === 'major' ? 0 : parsed.minor, + patch: truncation === 'major' || truncation === 'minor' ? 0 : parsed.patch, + }) +} + +export function difference( + left: VersionInput, + right: VersionInput, +): VersionDifference | null { + const leftVersion = parse(left) + const rightVersion = parse(right) + const comparison = compareParsed(leftVersion, rightVersion) + if (comparison === 0) return null + + const high = comparison > 0 ? leftVersion : rightVersion + const low = comparison > 0 ? rightVersion : leftVersion + const highHasPrerelease = high.prerelease?.length + const lowHasPrerelease = low.prerelease?.length + if (lowHasPrerelease && !highHasPrerelease) { + if (low.patch === 0 && low.minor === 0) return 'major' + if (compareMainParsed(low, high) === 0) { + return low.minor !== 0 && low.patch === 0 ? 'minor' : 'patch' } - } else { - match = (options.includePrerelease ? COERCE_FULL_EXACT : COERCE_EXACT).exec( - input, - ) } - if (!match) return null - const major = match[2] - const minor = match[3] || '0' - const patch = match[4] || '0' - const prerelease = options.includePrerelease && match[5] ? `-${match[5]}` : '' - const build = options.includePrerelease && match[6] ? `+${match[6]}` : '' - return tryParse(`${major}.${minor}.${patch}${prerelease}${build}`, options) + const prefix = highHasPrerelease ? 'pre' : '' + if (leftVersion.major !== rightVersion.major) { + return `${prefix}major` as VersionDifference + } + if (leftVersion.minor !== rightVersion.minor) { + return `${prefix}minor` as VersionDifference + } + if (leftVersion.patch !== rightVersion.patch) { + return `${prefix}patch` as VersionDifference + } + return 'prerelease' +} + +export function getMajor( + version: VersionInput, + options: VersionOptions = {}, +): number { + return parse(version, options).major +} + +export function getMinor( + version: VersionInput, + options: VersionOptions = {}, +): number { + return parse(version, options).minor +} + +export function getPatch( + version: VersionInput, + options: VersionOptions = {}, +): number { + return parse(version, options).patch +} + +export function getPrerelease( + version: VersionInput, + options: VersionOptions = {}, +): PrereleaseIdentifier[] | null { + const parsed = tryParse(version, options) + return parsed ? [...(parsed.prerelease || [])] : null +} + +export function getBuild( + version: VersionInput, + options: VersionOptions = {}, +): string[] | null { + const parsed = tryParse(version, options) + return parsed ? [...(parsed.build || [])] : null } diff --git a/src/version/parse.ts b/src/version/parse.ts new file mode 100644 index 0000000..fe8426a --- /dev/null +++ b/src/version/parse.ts @@ -0,0 +1,90 @@ +import { MAX_LENGTH } from './constants.ts' +import { FULL_PLAIN, LOOSE_PLAIN, safeRegex } from './patterns.ts' +import type { + PrereleaseIdentifier, + SemVer, + VersionInput, + VersionOptions, +} from './types.ts' + +const FULL = safeRegex(`^${FULL_PLAIN}$`) +const LOOSE = safeRegex(`^${LOOSE_PLAIN}$`) +const NUMERIC = /^\d+$/ + +export function formatComparableVersion( + version: Pick, +): string { + const base = `${version.major}.${version.minor}.${version.patch}` + return version.prerelease?.length + ? `${base}-${version.prerelease.join('.')}` + : base +} + +export function formatFullVersion( + version: Pick, +): string { + const comparable = formatComparableVersion(version) + return version.build?.length + ? `${comparable}+${version.build.join('.')}` + : comparable +} + +export function parse( + version: VersionInput, + options: VersionOptions = {}, +): SemVer { + if (typeof version !== 'string') return version + + if (version.length > MAX_LENGTH) { + throw new TypeError( + `Version exceeds the maximum length of ${MAX_LENGTH} characters`, + ) + } + + const match = version.trim().match(options.loose ? LOOSE : FULL) + if (!match) throw new TypeError(`Invalid version syntax: ${version}`) + + const major = Number(match[1]) + const minor = Number(match[2]) + const patch = Number(match[3]) + if (major > Number.MAX_SAFE_INTEGER || major < 0) { + throw new TypeError(`Invalid major version: ${match[1]}`) + } + if (minor > Number.MAX_SAFE_INTEGER || minor < 0) { + throw new TypeError(`Invalid minor version: ${match[2]}`) + } + if (patch > Number.MAX_SAFE_INTEGER || patch < 0) { + throw new TypeError(`Invalid patch version: ${match[3]}`) + } + + const prerelease = match[4] + ? match[4].split('.').map((identifier) => { + if (NUMERIC.test(identifier)) { + const numeric = Number(identifier) + if (numeric >= 0 && numeric < Number.MAX_SAFE_INTEGER) { + return numeric + } + } + return identifier + }) + : undefined + const build = match[5]?.split('.') + return { + build, + major, + minor, + patch, + prerelease, + } +} + +export function tryParse( + version: VersionInput, + options: VersionOptions = {}, +): SemVer | null { + try { + return parse(version, options) + } catch { + return null + } +} diff --git a/src/internal/patterns.ts b/src/version/patterns.ts similarity index 99% rename from src/internal/patterns.ts rename to src/version/patterns.ts index dd17342..8cab8b6 100644 --- a/src/internal/patterns.ts +++ b/src/version/patterns.ts @@ -2,7 +2,7 @@ import { MAX_LENGTH, MAX_SAFE_BUILD_LENGTH, MAX_SAFE_COMPONENT_LENGTH, -} from '../constants.ts' +} from './constants.ts' export const LETTER_DASH_NUMBER: string = '[a-zA-Z0-9-]' export const NUMERIC_IDENTIFIER: string = String.raw`0|[1-9]\d*` diff --git a/src/types.ts b/src/version/types.ts similarity index 78% rename from src/types.ts rename to src/version/types.ts index ff353c7..224a306 100644 --- a/src/types.ts +++ b/src/version/types.ts @@ -2,10 +2,6 @@ export interface VersionOptions { loose?: boolean } -export interface RangeOptions extends VersionOptions { - includePrerelease?: boolean -} - export interface CoerceOptions extends VersionOptions { includePrerelease?: boolean rtl?: boolean @@ -26,13 +22,6 @@ export interface SemVer { build?: string[] | undefined } -export interface SemVerComparator { - operator: '' | '<' | '<=' | '>' | '>=' - options: RangeOptions - value: string - version: SemVer | null -} - export type VersionInput = SemVer | string export type Comparison = -1 | 0 | 1 @@ -40,8 +29,6 @@ export type Comparison = -1 | 0 | 1 export type ComparisonOperator = '' | '!=' | '!==' | '<' | '<=' | '=' | '==' | '===' | '>' | '>=' -export type RangeDirection = '<' | '>' - export type IncrementType = | 'major' | 'minor' diff --git a/tests/comparator.test.ts b/tests/comparator.test.ts index 910cd40..4c02338 100644 --- a/tests/comparator.test.ts +++ b/tests/comparator.test.ts @@ -3,7 +3,9 @@ import { describe, expect, it } from 'vitest' import { comparatorsIntersect, normalizeComparator, + parseComparator, satisfiesComparator, + tryParseComparator, } from '../src/comparator.ts' import { parse } from '../src/version.ts' import comparatorIntersections from './fixtures/node-semver/comparator-intersection.ts' @@ -11,6 +13,28 @@ import comparatorIntersections from './fixtures/node-semver/comparator-intersect type IntersectionCase = readonly [string, string, boolean, boolean?] describe('comparators', () => { + it('parses reusable mutable comparators', () => { + const comparator = parseComparator('>=1.2.3') + + expect(comparator).toEqual({ + operator: '>=', + options: {}, + value: '>=1.2.3', + version: parse('1.2.3'), + }) + expect(parseComparator(comparator)).toBe(comparator) + expect(tryParseComparator(comparator)).toBe(comparator) + expect(tryParseComparator('not a comparator')).toBeNull() + expect(() => parseComparator('not a comparator')).toThrow(TypeError) + + comparator.version!.patch = 4 + expect(normalizeComparator(comparator)).toBe('>=1.2.4') + expect(satisfiesComparator('1.2.4', comparator)).toBe(true) + expect(comparatorsIntersect(comparator, parseComparator('<2.0.0'))).toBe( + true, + ) + }) + it('normalizes and tests strict comparators', () => { expect(normalizeComparator(' >= 1.2.3+build ')).toBe('>=1.2.3') expect(satisfiesComparator('1.2.3+other', '>=1.2.3')).toBe(true) diff --git a/tests/comparator.types.ts b/tests/comparator.types.ts new file mode 100644 index 0000000..3f22cae --- /dev/null +++ b/tests/comparator.types.ts @@ -0,0 +1,31 @@ +import { + comparatorsIntersect, + normalizeComparator, + parseComparator, + satisfiesComparator, + tryParseComparator, + type ComparatorInput, + type RangeOptions, + type SemVerComparator, +} from '../src/index.ts' + +declare const input: ComparatorInput +declare const options: RangeOptions +declare const parsed: SemVerComparator + +parseComparator(input) +tryParseComparator(input) +normalizeComparator(input) +satisfiesComparator('1.0.0', input) +comparatorsIntersect(input, parsed) + +// @ts-expect-error A parsed comparator already contains its parsing options. +parseComparator(parsed, options) +// @ts-expect-error A parsed comparator already contains its parsing options. +tryParseComparator(parsed, options) +// @ts-expect-error A parsed comparator already contains its parsing options. +normalizeComparator(parsed, options) +// @ts-expect-error A parsed comparator already contains its parsing options. +satisfiesComparator('1.0.0', parsed, options) +// @ts-expect-error Parsed comparators already contain their parsing options. +comparatorsIntersect(parsed, '>=1.0.0', options) diff --git a/tests/comparison.test.ts b/tests/comparison.test.ts index 6bb378a..10b6d9c 100644 --- a/tests/comparison.test.ts +++ b/tests/comparison.test.ts @@ -3,6 +3,8 @@ import { describe, expect, it } from 'vitest' import { compare, compareBuild, + compareIdentifiers, + compareIdentifiersReversed, compareMain, comparePrerelease, compareReversed, @@ -13,17 +15,13 @@ import { isLess, isLessOrEqual, isNotEqual, + parse, sort, sortReversed, -} from '../src/comparison.ts' -import { - compareIdentifiers, - compareIdentifiersReversed, -} from '../src/identifiers.ts' -import { parse } from '../src/version.ts' + type VersionOptions, +} from '../src/version.ts' import comparisons from './fixtures/node-semver/comparisons.ts' import equality from './fixtures/node-semver/equality.ts' -import type { VersionOptions } from '../src/types.ts' type ComparisonCase = readonly [string, string, unknown?] diff --git a/tests/fixtures/bundle-size/semver.ts b/tests/fixtures/bundle-size/semver.ts index 02acdf9..02c3cbb 100644 --- a/tests/fixtures/bundle-size/semver.ts +++ b/tests/fixtures/bundle-size/semver.ts @@ -3,12 +3,12 @@ import { coerce, compare, inc, satisfies, valid } from 'semver' export function runCommonOperations( version: string, range: string, -): readonly [string | null, boolean, number, string | null, string | null] { +): readonly [string | null, boolean, number, string | null, number | null] { return [ valid(version), satisfies(version, range), compare(version, '1.0.0'), inc(version, 'patch'), - coerce(version)?.version ?? null, + coerce(version)?.major ?? null, ] } diff --git a/tests/fixtures/bundle-size/verkit.ts b/tests/fixtures/bundle-size/verkit.ts index b446252..501579f 100644 --- a/tests/fixtures/bundle-size/verkit.ts +++ b/tests/fixtures/bundle-size/verkit.ts @@ -9,12 +9,13 @@ import { export function runCommonOperations( version: string, range: string, -): readonly [string | null, boolean, number, string | null, string | null] { +): readonly [string | null, boolean, number, string | null, number | null] { + const coerced = coerce(version) return [ normalize(version), satisfies(version, range), compare(version, '1.0.0'), increment(version, 'patch'), - coerce(version), + coerced?.major ?? null, ] } diff --git a/tests/fixtures/node-semver/invalid-versions.ts b/tests/fixtures/node-semver/invalid-versions.ts index 2b3eace..9808d1f 100644 --- a/tests/fixtures/node-semver/invalid-versions.ts +++ b/tests/fixtures/node-semver/invalid-versions.ts @@ -2,7 +2,7 @@ // none of these are semvers // [value, reason, opt] -import { MAX_LENGTH } from '../../../src/constants.ts' +import { MAX_LENGTH } from '../../../src/version/constants.ts' const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER const fixtures: readonly (readonly unknown[])[] = [ diff --git a/tests/range.test.ts b/tests/range.test.ts index ea2f7ad..5aef27e 100644 --- a/tests/range.test.ts +++ b/tests/range.test.ts @@ -17,7 +17,7 @@ import { simplifyRange, tryParseRange, } from '../src/range.ts' -import { parse } from '../src/version.ts' +import { normalize, parse } from '../src/version.ts' import rangeExclude from './fixtures/node-semver/range-exclude.ts' import rangeInclude from './fixtures/node-semver/range-include.ts' import rangeIntersections from './fixtures/node-semver/range-intersection.ts' @@ -27,7 +27,7 @@ import versionGtRange from './fixtures/node-semver/version-gt-range.ts' import versionLtRange from './fixtures/node-semver/version-lt-range.ts' import versionNotGtRange from './fixtures/node-semver/version-not-gt-range.ts' import versionNotLtRange from './fixtures/node-semver/version-not-lt-range.ts' -import type { RangeOptions } from '../src/types.ts' +import type { RangeOptions } from '../src/comparator.ts' type RangeCase = readonly [string, string, unknown?] type RangeParseCase = readonly [string, string | null, unknown?] @@ -39,12 +39,15 @@ function rangeOptions(value: unknown): RangeOptions { return value && typeof value === 'object' ? (value as RangeOptions) : {} } +function minimumValue(range: string): string | null { + const minimum = findMinimumForRange(range) + return minimum ? normalize(minimum) : null +} + describe('range parsing and satisfaction', () => { it('parses reusable SemVerRange objects', () => { const range = parseRange('^1.2.3') - expect(range.raw).toBe('^1.2.3') - expect(range.normalized).toBe('>=1.2.3 <2.0.0-0') expect(parseRange(range)).toBe(range) expect(() => parseRange('not a range')).toThrow('Invalid comparator: not') expect(tryParseRange('not a range')).toBeNull() @@ -52,7 +55,7 @@ describe('range parsing and satisfaction', () => { expect(normalizeRange(range)).toBe('>=1.2.3 <2.0.0-0') expect(satisfies('1.5.0', range)).toBe(true) expect(rangeToComparators(range)).toEqual([['>=1.2.3', '<2.0.0-0']]) - expect(findMinimumForRange(range)).toBe('1.2.3') + expect(normalize(findMinimumForRange(range)!)).toBe('1.2.3') expect(rangesIntersect(range, '>=1.5.0')).toBe(true) expect(isRangeSubset(range, '1.x')).toBe(true) }) @@ -76,20 +79,16 @@ describe('range parsing and satisfaction', () => { expect(first.sets[0]).not.toBe(second.sets[0]) expect(first.sets[0]![0]!.version).not.toBe(second.sets[0]![0]!.version) - first.raw = 'mutated' - first.normalized = '>1.2.9' first.options.includePrerelease = true first.sets[0]![0]!.operator = '>' - first.sets[0]![0]!.value = '>1.2.9' first.sets[0]![0]!.version!.patch = 9 - expect(second.raw).toBe('^1.2.3') - expect(second.normalized).toBe('>=1.2.3 <2.0.0-0') expect(second.options.includePrerelease).toBeUndefined() expect(options.includePrerelease).toBeUndefined() expect(second.sets[0]![0]!.operator).toBe('>=') expect(second.sets[0]![0]!.value).toBe('>=1.2.3') expect(second.sets[0]![0]!.version!.patch).toBe(3) + expect(normalizeRange(first)).toBe('>1.2.9 <2.0.0-0') }) it('accepts SemVer objects with optional identifier arrays', () => { @@ -150,8 +149,8 @@ describe('range sets', () => { expect(findMaxSatisfying(versions, '^1.2.0')).toBe('1.5.0') expect(findMinSatisfying(versions, '^1.2.0')).toBe('1.2.3') expect(findMaxSatisfying(versions, 'invalid')).toBeNull() - expect(findMinimumForRange('>1.2.3')).toBe('1.2.4') - expect(findMinimumForRange('>1.2.3-alpha.1')).toBe('1.2.3-alpha.1.0') + expect(minimumValue('>1.2.3')).toBe('1.2.4') + expect(minimumValue('>1.2.3-alpha.1')).toBe('1.2.3-alpha.1.0') expect(findMinimumForRange('<0.0.0')).toBeNull() }) @@ -214,10 +213,21 @@ describe('range sets', () => { ] for (const [range, expected] of cases) { - expect(findMinimumForRange(range)).toBe(expected) + expect(minimumValue(range)).toBe(expected) } }) + it('returns an independent mutable minimum version', () => { + const range = parseRange('>=1.2.3') + const comparatorVersion = range.sets[0]![0]!.version! + const minimum = findMinimumForRange(range)! + + expect(minimum).toEqual(comparatorVersion) + expect(minimum).not.toBe(comparatorVersion) + minimum.patch = 4 + expect(comparatorVersion.patch).toBe(3) + }) + it('matches every subset behavior fixture', () => { for (const row of rangeSubsets as readonly RangeSubsetCase[]) { const [subset, superset, expected, rawOptions] = row @@ -270,6 +280,10 @@ describe('range sets', () => { ) expect(simplifyRange(versions, '1 || 2 || 3')).toBe('*') expect(simplifyRange(versions, '2.1 || 2.2 || 2.3')).toBe('2.1.0 - 2.3.1') + + const parsedRange = parseRange('1.x') + parsedRange.sets = parseRange('>=3.0.0').sets + expect(simplifyRange(versions, parsedRange)).toBe('>=3.0.0') expect(versions).toEqual(original) }) }) diff --git a/tests/readme.test.ts b/tests/readme.test.ts index 599caf4..d5762cf 100644 --- a/tests/readme.test.ts +++ b/tests/readme.test.ts @@ -25,7 +25,7 @@ describe('README examples', () => { expect(normalize(version)).toBe('1.2.4-rc.1') expect(increment(version, 'minor')).toBe('1.3.0') expect(truncate(version, 'patch')).toBe('1.2.4') - expect(coerce('release 42.6.7.9', { rtl: true })).toBe('6.7.9') + expect(coerce('release 42.6.7.9', { rtl: true })?.major).toBe(6) }) it('keeps comparison and range examples executable', () => { diff --git a/tests/version.test.ts b/tests/version.test.ts index bd21aac..40bff90 100644 --- a/tests/version.test.ts +++ b/tests/version.test.ts @@ -18,20 +18,18 @@ import { parse, truncate, tryParse, + type IncrementOptions, + type IncrementType, + type PrereleaseIdentifier, + type SemVer, + type TruncationType, + type VersionDifference, + type VersionOptions, } from '../src/version.ts' import increments from './fixtures/node-semver/increments.ts' import invalidVersions from './fixtures/node-semver/invalid-versions.ts' import truncations from './fixtures/node-semver/truncations.ts' import validVersions from './fixtures/node-semver/valid-versions.ts' -import type { - IncrementOptions, - IncrementType, - PrereleaseIdentifier, - SemVer, - TruncationType, - VersionDifference, - VersionOptions, -} from '../src/types.ts' type ValidVersionCase = readonly [ string, @@ -116,7 +114,7 @@ describe('version parsing and accessors', () => { expect(normalizeFull(version)).toBe('1.2.4-beta.1+next') expect(normalize(version)).toBe('1.2.4-beta.1') expect(clean(version)).toBe('1.2.4-beta.1') - expect(coerce(version)).toBe('1.2.4-beta.1+next') + expect(coerce(version)).toBe(version) expect(increment(version, 'minor')).toBe('1.3.0') expect(truncate(version, 'patch')).toBe('1.2.4') expect(getMajor(version)).toBe(1) @@ -209,11 +207,11 @@ describe('version operations', () => { }) it('coerces left-to-right, right-to-left, and prerelease forms', () => { - expect(coerce('release 1.2.3.4')).toBe('1.2.3') - expect(coerce('release 1.2.3.4', { rtl: true })).toBe('2.3.4') - expect(coerce('v2')).toBe('2.0.0') - expect(coerce('1.2.3-rc.1+build.7', { includePrerelease: true })).toBe( - '1.2.3-rc.1+build.7', + expect(coerce('release 1.2.3.4')).toEqual(parse('1.2.3')) + expect(coerce('release 1.2.3.4', { rtl: true })).toEqual(parse('2.3.4')) + expect(coerce('v2')).toEqual(parse('2.0.0')) + expect(coerce('1.2.3-rc.1+build.7', { includePrerelease: true })).toEqual( + parse('1.2.3-rc.1+build.7'), ) expect(coerce('not a version')).toBeNull() expect(coerce('9'.repeat(17))).toBeNull() diff --git a/tsdown.config.ts b/tsdown.config.ts index 470b4ac..716ba20 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -1,9 +1,5 @@ import { lib } from 'tsdown-preset-sxzz' -import tsnapi from 'tsnapi/rolldown' -export default lib( - {}, - { - plugins: [tsnapi()], - }, -) +export default lib({ + entry: 'shallow', +})