Skip to content

fix: return prerelease of 0.0.0 from minVersion when it is the minimum - #936

Open
thisisshivamtiwari wants to merge 3 commits into
npm:mainfrom
thisisshivamtiwari:fix/min-version-zero-prerelease
Open

thisisshivamtiwari wants to merge 3 commits into
npm:mainfrom
thisisshivamtiwari:fix/min-version-zero-prerelease

Conversation

@thisisshivamtiwari

Copy link
Copy Markdown

Fixes #934

minVersion checked 0.0.0 before 0.0.0-0 and returned it as soon as it matched. Since every prerelease of 0.0.0 sorts below 0.0.0, that shortcut returned the wrong answer whenever a 0.0.0 prerelease also matches:

semver.minVersion('>=0.0.0-alpha <1.0.0').version              // was '0.0.0', now '0.0.0-alpha'
semver.minVersion('>=0.0.0-0').version                          // was '0.0.0', now '0.0.0-0'
semver.minVersion('*', { includePrerelease: true }).version     // was '0.0.0', now '0.0.0-0'

Changes in ranges/min-version.js:

  • Check 0.0.0-0 first, since it is the lowest possible version.
  • If 0.0.0 matches, still run the normal lower-bound loop and return its result only when it matches and is below 0.0.0. Otherwise return 0.0.0 as before. This keeps every existing result: when the loop finds nothing lower, the answer is still 0.0.0.
  • Skip ANY comparators in the loop. Before this change the loop never saw them, because any range containing * matched 0.0.0 and returned early.

Without includePrerelease, * and <2 still return 0.0.0, which matches the expectation in the issue thread.

Tests

  • Added cases to test/ranges/min-version.js for 0.0.0 prerelease lower bounds (>=, >, in a union, and an impossible prerelease set next to <1 that must still fall back to 0.0.0), plus includePrerelease cases. The new cases fail on main and pass with this change.
  • The test helper now prints the options object with JSON.stringify so the new includePrerelease cases have readable names.
  • npm test (tap with 100% coverage, then eslint and template-oss-check) passes locally.

🤖 Generated with Claude Code

minVersion tried 0.0.0 before 0.0.0-0 and returned it as soon as it
matched, so a lower bound such as >=0.0.0-alpha, or any range with
includePrerelease, gave 0.0.0 instead of the lower prerelease.

Fixes npm#934
@thisisshivamtiwari
thisisshivamtiwari requested a review from a team as a code owner October 29, 2026 02:52
@jeffkl
jeffkl requested a balanced review from Copilot October 29, 2026 15:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Candidate validation occurs too late, allowing an impossible OR branch to mask a valid prerelease minimum and return an incorrect fallback.

1 open issue
What changed in this PR

This PR addresses #934 by updating minVersion to consider matching prereleases below 0.0.0, rather than returning the stable version immediately.

Changes:

  • Check 0.0.0-0 first and consider prerelease lower bounds before falling back to 0.0.0.
  • Skip wildcard comparators during lower-bound calculation.
  • Add prerelease and includePrerelease regression cases, with readable options in test names.
File Description
test/​ranges/​min-version.js Adds regression cases and improves test labels for options objects.
ranges/​min-version.js Revises minimum-version selection and handles wildcard comparators.

Review effort: Balanced

Comment thread ranges/min-version.js Resolved
A set whose lower bound fails its own upper bounds, such as
>=0.0.0-alpha <0.0.0-alpha, could win the minimum across a union and
then fail the final range check, hiding another set's valid minimum.
Check each set's candidate against that set before comparing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jeffkl
jeffkl previously approved these changes Oct 29, 2026
Comment thread ranges/min-version.js Outdated
if (setMin && (!minver || gt(minver, setMin))) {
// A set whose own lower bound fails its upper bounds (e.g. `>=1.0.0 <1.0.0`) has no minimum; skipping it
// keeps it from hiding the minimum of another set in the union.
if (setMin && new Range(comparators.join(' '), range.options).test(setMin) &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now returns null for a range with a valid minimum:

const range = '>=1.0.1-0 <1.0.0 || >1.0.0 <1.0.1-alpha'
semver.minVersion(range) // null; previously 1.0.1-0
semver.satisfies('1.0.1-0', range) // true

The generated 1.0.1 candidate fails the upper bound, but 1.0.1-0 satisfies the second branch. Please preserve this minimum and add a regression test.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, confirmed: this was a regression from my change (before it, this range gave 1.0.1-0). Fixed in 791b290. When a > bound's release fails its set, the first prerelease of that release (X.Y.Z-0) is now tried first, so 1.0.1-0 is kept. Regression tests added for the default, loose and includePrerelease options. Side effect: minVersion('>1.0.0', { includePrerelease: true }) now returns 1.0.1-0, the actual lowest match, and has a test. npm test passes with 100% coverage.

Comment thread ranges/min-version.js
}

if (minver && range.test(minver)) {
if (minver && range.test(minver) && (!zeroMatches || gt(zero, minver))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid long prerelease bounds can now return an invalid version or throw:

const range = '>0.0.0-' + 'a'.repeat(249)
const result = semver.minVersion(range)
semver.valid(result.version) // null: the result has 257 characters
semver.minVersion(range, { includePrerelease: true }) // throws

This input previously returned 0.0.0 without throwing. Please handle the version-length limit when generating the successor and add boundary tests for default, loose, and includePrerelease options.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, confirmed: with a bound at the 256-character limit, the pushed .0 produced an over-length version, and with loose it also threw. Fixed in 791b290. The successor prerelease is generated only if it fits within MAX_LENGTH. Otherwise the release itself is used, as before this PR (>0.0.0-<249 a's> gives 0.0.0). Boundary tests cover lengths 255 and 256 under the default, loose and includePrerelease options.

Per review:
- A '>' bound's release candidate can fail its set while the first
  prerelease of that version passes (>1.0.0 <1.0.1-alpha allows 1.0.1-0),
  so try the prerelease first.
- The next prerelease of a bound at the 256-character limit cannot be
  written; use the release instead, as minVersion did before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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] minVersion returns 0.0.0 when a 0.0.0 prerelease is the true minimum

4 participants