Skip to content

fix: include prereleases in tilde range lower bound with includePrerelease#878

Open
chatman-media wants to merge 1 commit into
npm:mainfrom
chatman-media:fix/tilde-include-prerelease-lower-bound
Open

fix: include prereleases in tilde range lower bound with includePrerelease#878
chatman-media wants to merge 1 commit into
npm:mainfrom
chatman-media:fix/tilde-include-prerelease-lower-bound

Conversation

@chatman-media

Copy link
Copy Markdown

Closes #512

Problem

A tilde range like `~1.2` is documented in the README as equivalent to the `1.2.x` x-range:

`~1.2` := `>=1.2.0 <1.(2+1).0` := `>=1.2.0 <1.3.0-0` (Same as `1.2.x`)

With `{ includePrerelease: true }`, both the x-range and the caret operator lower-bound their open forms at `-0` (the lowest possible prerelease) so that prereleases like `1.2.0-rc` match. The tilde operator did not, leaving its lower bound at `>=1.2.0`:

const semver = require("semver")
const opts = { includePrerelease: true }

semver.satisfies("1.2.0-rc", "1.2.*", opts)  // true
semver.satisfies("1.2.0-rc", "^1.2.*", opts) // true
semver.satisfies("1.2.0-rc", "~1.2.*", opts) // false  <-- bug

new semver.Range("1.2.*",  opts).range // ">=1.2.0-0 <1.3.0-0"
new semver.Range("^1.2.*", opts).range // ">=1.2.0-0 <2.0.0-0"
new semver.Range("~1.2.*", opts).range // ">=1.2.0 <1.3.0-0"   <-- lower bound missing -0

This makes `~1.2.*` inconsistent with both its documented x-range equivalent and the caret operator. Reported in #512 (and the same root cause as #510 / #736 discussion around tilde + prerelease).

Fix

`replaceCaret` and `replaceXRange` already compute a lower-bound suffix:

const z = options.includePrerelease ? "-0" : ""

and apply it to their open forms. `replaceTilde` was the only one of the three that did not. This change applies the same suffix to the open tilde forms (`~1`, `~1.x`, `~1.2`, `~1.2.x`).

After the fix, `~1.2.` desugars to `>=1.2.0-0 <1.3.0-0` — identical to the `1.2.` x-range it is documented to equal.

Scope / what is intentionally unchanged

  • Default behavior (no `includePrerelease`) is untouched — no `-0` is added, so `~1.2.*` stays `>=1.2.0 <1.3.0-0`.
  • Fully-specified tildes keep their exact lower bound, matching the caret operator: `~1.2.3` with `includePrerelease` stays `>=1.2.3 <1.3.0-0` (so `1.2.3-rc`, which sorts before the `1.2.3` release, correctly does not match — same as `^1.2.3`).
  • Explicit-prerelease tildes (`~1.2.3-beta`) are preserved as-is.

Tests

Added failing-then-passing fixtures:

  • `test/fixtures/range-parse.js` — desugaring of `~1`, `~1.x`, `~1.2`, `~1.2.x`, `~0.0` under `includePrerelease`, plus `~1.2.3` / `~1.2.3-beta.4` to lock in that they do not change.
  • `test/fixtures/range-include.js` — `~1.1` / `~1.1.x` / `~2` / `~2.x` now satisfy the same prereleases as the equivalent x-ranges.

Full suite (`npm test`) and `eslint` pass.

…lease

A tilde range like ~1.2 is documented as equivalent to the 1.2.x
x-range (>=1.2.0 <1.3.0-0). With { includePrerelease: true }, the
x-range and caret operators lower-bound at -0 (e.g. 1.2.x and ^1.2.*
become >=1.2.0-0 ...) so prereleases such as 1.2.0-rc match, but the
tilde operator did not, leaving its lower bound at >=1.2.0. This made
~1.2.* inconsistent with both its documented x-range equivalent and
the caret operator.

Apply the same -0 lower-bound suffix used by replaceCaret/replaceXRange
to the open tilde forms (~1, ~1.x, ~1.2, ~1.2.x). Fully-specified
tildes (~1.2.3) and explicit-prerelease tildes (~1.2.3-beta) keep their
exact lower bound, matching caret semantics. Default behavior (without
includePrerelease) is unchanged.

Closes npm#512
@chatman-media chatman-media requested a review from a team as a code owner June 18, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Tilde Range Not Equivalent to X-Range when Including Prerelease

1 participant