Semver Comparator

Two rules cause nearly every semver surprise. A prerelease sorts before the release it leads to, so 1.0.0-rc.1 is older than 1.0.0 even though it is longer and looks later. And a caret range below 1.0.0 pins the minor version rather than the major, so ^0.2.3 refuses 0.3.0 while ^1.2.3 happily takes 1.3.0. Both are in the specification, both are correct, and both are worth checking rather than assuming.

Supported: exact, ^, ~, >, >=, <, <=, x wildcards, hyphen ranges, spaces for AND, || for OR.
Semver Comparator — Precedence, Prereleases and RangesBuildFigure

The precedence rules, in the order they are applied

Comparison walks four steps and stops at the first difference. Major, then minor, then patch, each compared as a number rather than as text — which is why 1.10.0 is newer than 1.9.0 even though the string sorts the other way. If all three match, the prerelease field decides, and if there is no prerelease field on either side the versions have equal precedence.

Build metadata, the part after a plus sign, is never consulted. Two versions that differ only there are the same version for ordering purposes. That is not a rounding-off in this implementation; it is stated in the specification, and it is why build metadata is a poor place to put anything you expect a tool to act on.

Prereleases sort backwards from how they read

A prerelease tag makes a version lower than the same numbers without one. So 1.0.0-alpha, 1.0.0-beta and 1.0.0-rc.1 all come before 1.0.0. Read as English this feels wrong, because the tagged version has more in it; read as a release process it is obvious, because those builds exist on the way to 1.0.0 rather than after it.

Between two prereleases, the dot-separated identifiers are compared left to right by three rules that are easy to state and easy to forget:

CaseRuleResult
Both identifiers are all digitsCompare as numbersbeta.2 is older than beta.11
Both contain a letter or hyphenCompare by ASCIIalpha is older than beta
One numeric, one notNumeric always sorts loweralpha.1 is older than alpha.beta
One list is a prefix of the otherThe longer list winsalpha is older than alpha.1

The numeric rule is the one that bites in practice. A pipeline that produces build tags as plain text will happily order beta.11 before beta.2, because string comparison puts 1 before 2. Semantic version comparison does not, and the difference only shows up on the eleventh build.

Caret ranges change meaning below 1.0.0

A caret range allows changes that do not modify the leftmost non-zero number. Above 1.0.0 that is the major, so ^1.2.3 means at least 1.2.3 and anything up to but excluding 2.0.0 — new minors and patches are in. At 0.x the leftmost non-zero number is the minor, so ^0.2.3 means at least 0.2.3 and anything below 0.3.0. A 0.3.0 release is refused.

This is the single most misread rule in dependency ranges, and it is worth understanding rather than memorising. Semantic versioning says the major number carries the compatibility promise — and it also says that anything below 1.0.0 has made no promise at all. So the caret shifts down a level and pins the minor instead, on the theory that a pre-1.0.0 project changes its interface at minor bumps. The consequence is that a lockfile-free install of a 0.x dependency will never pick up a new minor version, which surprises people who expected the caret to behave the way it does at 1.x.

It goes one further at 0.0.x. There ^0.0.3 allows only 0.0.3 itself, since the leftmost non-zero number is the patch. Ranges of that shape are effectively pins.

Why an upper bound is written with a trailing -0

The expansion above writes the top of a caret range as <2.0.0-0 rather than <2.0.0. The reason follows from the prerelease rule: 2.0.0-alpha sorts below 2.0.0, so a bound of <2.0.0 would technically admit it. Nobody writing ^1.2.3 wants an alpha of the next major installed. Appending the lowest possible prerelease identifier closes the gap, and the same trick appears in tilde and wildcard expansions for the same reason.

That bound is also marked as generated rather than as something you wrote, which matters for the next rule.

Prereleases and ranges: an opt-in relationship

By default a version carrying a prerelease tag satisfies a range only if some comparator you wrote mentions a prerelease on the same major, minor and patch. So >=1.2.3-alpha will match 1.2.3-beta, but ^1.2.3 will not match 1.3.0-beta even though 1.3.0-beta sits inside the numeric bounds. The rationale is that you opt into prerelease builds of a specific version deliberately; you do not get them by accident from a range written months earlier. The checkbox above turns the rule off so you can see both answers, which is the fastest way to work out whether a surprising install came from this rule or from something else.

Choosing the next number

The comparison side of this page answers what is newer. Deciding what to release next is a separate judgement with a short rule: anything that breaks an existing caller is a major bump, anything that adds capability without breaking is a minor, anything else is a patch — and below 1.0.0 none of those promises are being made yet. The changelog formatter is the practical companion here, since a Breaking entry in the release notes and a major bump in the version are the same decision written twice. For the repository around it, the gitignore builder covers what should not be tagged in the first place, and the JSON formatter is useful when reading a lockfile by hand.

Questions people ask

Why is 1.0.0-alpha older than 1.0.0?

Because a prerelease tag denotes a build on the way to that version, not a build after it. The specification states that a version with a prerelease has lower precedence than the same three numbers without one, so the entire alpha, beta and release candidate sequence sorts below the release. It also means you cannot get back above 1.0.0 by adding more identifiers: 1.0.0-zzz is still older than 1.0.0.

Does ^0.2.3 really refuse 0.3.0?

Yes. The caret pins the leftmost non-zero number, and below 1.0.0 that is the minor rather than the major. The expansion is at least 0.2.3 and below 0.3.0. Test it above and the comparator table shows exactly which bound fails. If you want new minors of a 0.x dependency you have to say so explicitly, with something like >=0.2.3 <1.0.0, and accept that a 0.x project has made no compatibility promise across that gap.

What happens to the build metadata after the plus sign?

It is parsed, displayed and then ignored for ordering. Two versions differing only in build metadata have equal precedence, which the sort table marks explicitly. If your build system needs to distinguish those two, the distinguishing information has to live somewhere a comparison actually looks at, which in practice means a prerelease identifier rather than build metadata.

Which range implementation does this match?

It implements the range operators as they are commonly documented for JavaScript package ranges: caret, tilde, comparison operators, x wildcards, hyphen ranges, space for AND and double pipe for OR, plus the prerelease opt-in rule. Other ecosystems use different syntax with different semantics for similar-looking expressions, so treat a result here as authoritative for that family and as a strong hint elsewhere. Compound forms not listed above are reported as unparsed rather than approximated.

Can I paste a whole dependency list?

The sort box takes one version per line and reports which lines it could not parse, so a column of versions pasted out of a lockfile works. A full dependency file with names and ranges will not, because each line has to be a single version. Split the names off first, or test the interesting ranges one at a time in the range box, where the expansion and the pass or fail of each comparator are shown.

Is anything sent anywhere?

No. Parsing, comparing and sorting all run in this tab in JavaScript, with no request made and nothing stored. Internal package names and unreleased version numbers are the usual input, which is reason enough to say so plainly.

Related