@sdxc/semver
SemVer 2.0.0 parsing, precedence ordering and range-free version comparisons
- Installs with
- @sdxc/result
- Source
- packages/semver
SemVer 2.0.0 parsing, precedence ordering and range-free version comparisons.
Installation
npm add @sdxc/semver
parse() reports failures as a Result from @sdxc/result, which installs alongside this package.
Usage
Comparing Two Versions
import { satisfies } from "@sdxc/semver";
satisfies("1.9.0", "^", "1.4.2"); // true
satisfies("1.5.0", "~", "1.4.2"); // false
satisfies("2.0.0", ">", "1.99.99"); // true
satisfies("nightly", ">", "1.0.0"); // false — "nightly" is not a version
Sorting A List Of Versions
import { compare } from "@sdxc/semver";
["2026.10.1", "2026.9.4", "2026.9.30"].sort(compare);
// ["2026.9.4", "2026.9.30", "2026.10.1"]
["1.0.0", "1.0.0-rc.1", "1.0.0-beta.11", "1.0.0-beta.2"].sort(compare);
// ["1.0.0-beta.2", "1.0.0-beta.11", "1.0.0-rc.1", "1.0.0"]
Reading A Version's Elements
import { parse } from "@sdxc/semver";
import { isFailure } from "@sdxc/result";
let result = parse("v2.1.0-rc.3+build.9");
if (isFailure(result)) {
console.error(result.error.message); // Invalid version: "v2.1.0-rc.3+build.9"
return;
}
result.data; // { major: 2, minor: 1, patch: 0, prerelease: ["rc", "3"] }
API
parse(text: string): Result<SemVer, InvalidSemVerError>
Read a version string into its elements, returning a Success<SemVer> or a Failure<InvalidSemVerError> naming the rejected text. The grammar is SemVer 2.0.0 with one addition: an optional leading v, as a git tag or a user agent writes it. Build metadata is dropped, since it carries no precedence.
parse("1.2.3"); // { status: "success", data: { major: 1, minor: 2, patch: 3, prerelease: [] } }
parse("v1.0.0-rc.1"); // prerelease: ["rc", "1"]
parse("01.2.3"); // { status: "failure", error: InvalidSemVerError } — a padded element
compare(a: string, b: string): number
Order two version strings by SemVer 2.0.0 precedence, ready to hand to Array.prototype.sort. Returns a negative number when a comes first, a positive one when b does, and 0 when the two rank equally.
The ordering is total, so a list whose entries come from somewhere unchecked — a registry, a tag listing, user input — sorts in one call. Text that is not a version ranks below every version and ties with other such text, which collects those entries at the front where the caller can see them.
compare("1.9.0", "1.10.0"); // negative — elements compare as numbers
compare("1.0.0-rc.1", "1.0.0"); // negative — a prerelease precedes its release
compare("1.2.3+a", "1.2.3+b"); // 0 — build metadata carries no precedence
compare("latest", "0.0.0"); // negative — text that is not a version sorts first
satisfies(value: string, comparison: SemVerComparison, against: string): boolean
Answer whether value stands in the named relation to against. Either side that is not a version answers false, so a field holding something else matches nothing instead of failing the check.
A prerelease takes part by precedence alone: 1.2.4-rc.1 satisfies ^ 1.2.3, because it outranks 1.2.3 and shares its major. Test a release channel by comparing against the prerelease you mean — satisfies(value, ">=", "1.2.4-rc.1") — when that is the boundary you want.
satisfies("1.2.3", "=", "1.2.3"); // true
satisfies("1.4.5", "~", "1.4.2"); // true, the same minor
satisfies("1.5.0", "~", "1.4.2"); // false, a later minor
satisfies("0.3.0", "^", "0.2.3"); // false, the left-most non-zero element moved
| Comparison | Holds when |
|---|---|
= | The two rank equally |
!= | The two rank differently |
< | value precedes against |
<= | value precedes against or ranks equally |
> | value follows against |
>= | value follows against or ranks equally |
~ | value is at least against and shares its major and minor |
^ | value is at least against and keeps its left-most non-zero element |
InvalidSemVerError
Error describing text that fails the SemVer 2.0.0 grammar. It arrives inside a Failure value.
text:string- The rejected text, kept verbatim for diagnosticsname:string- Always"InvalidSemVerError"message:string-Invalid version: "<text>", quoted so whitespace and empty strings stay visible in logs
Types
SemVer
interface SemVer {
major: number;
minor: number;
patch: number;
prerelease: string[];
}
A version taken apart for precedence. prerelease holds the dot-separated identifiers after the - and is empty for a release. Build metadata is absent, so two versions differing only in it are the same SemVer.
SemVerComparison
type SemVerComparison = "=" | "!=" | "<" | "<=" | ">" | ">=" | "~" | "^";
The comparisons satisfies() accepts. It is a closed union, so an editor offers the eight as a list and a stored value round-trips through a schema.
Precedence Rules
Versions compare element by element: major, then minor, then patch, each as a number rather than as text, so 1.10.0 follows 1.9.0. Build metadata is ignored throughout.
A version carrying a prerelease precedes the same version without one. Two prereleases compare identifier by identifier: numerically when both identifiers are numeric, by character otherwise, with a numeric identifier ranking below a textual one. When one list runs out while the other continues, the shorter one comes first, so 1.0.0-alpha precedes 1.0.0-alpha.1.
Pattern: Gating A Feature On A Client Version
Store the comparison and the version as data, and let an operator edit the rule without a deploy.
import type { SemVerComparison } from "@sdxc/semver";
import { satisfies } from "@sdxc/semver";
interface VersionRule {
comparison: SemVerComparison;
version: string;
}
function allows(rule: VersionRule, clientVersion: string): boolean {
return satisfies(clientVersion, rule.comparison, rule.version);
}
allows({ comparison: "^", version: "2.0.0" }, "2.4.1"); // true
allows({ comparison: ">=", version: "3.0.0" }, "2.4.1"); // false
Pattern: Picking The Newest Published Version
Registries hand back whatever they hold, including placeholders and tags. Sorting with compare() needs no filtering pass first, because the newest version is the last entry either way.
import { compare } from "@sdxc/semver";
function newest(versions: string[]): string | undefined {
return [...versions].sort(compare).at(-1);
}
newest(["0.0.0-pre.1", "2026.9.4", "2026.10.1"]); // "2026.10.1"
newest(["0.0.0-pre.9", "0.0.0-pre.10"]); // "0.0.0-pre.10"
Pattern: Deciding Whether An Upgrade Is Breaking
Parse both sides once and read the elements, when the question is about the shape of the change rather than about a threshold.
import { parse } from "@sdxc/semver";
import { isFailure } from "@sdxc/result";
function isBreaking(from: string, to: string): boolean {
let before = parse(from);
let after = parse(to);
if (isFailure(before) || isFailure(after)) return true;
if (before.data.major !== 0) return after.data.major !== before.data.major;
return after.data.minor !== before.data.minor;
}
isBreaking("1.4.2", "1.9.0"); // false
isBreaking("0.2.3", "0.3.0"); // true — below 1.0.0 the minor carries compatibility