authorgravatar for 132888838+arhxam@users.noreply.github.comArham Amin <132888838+arhxam@users.noreply.github.com> 2026-07-31 03:43:46+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-30 22:13:46+00:00
log063ed7120a17de65fcd5bd835b5c7c4e19536037
tree62b8b8245df82e548d4a27d209ca135b7e8b2a69
parent34b15a9acec5519425c81bdd2a10f11e9ba3d23d
signature Signed by PGP key B5690EEEBB952194

feat(shared): support shorthand (major-only) versions in the semver helpers (#5027)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

2 files changed, 31 insertions(+), 2 deletions(-)

packages/shared/src/semver.test.ts+25-1
...@@ -1,6 +1,11 @@...@@ -1,6 +1,11 @@
1import { describe, expect, it } from "vite-plus/test";1import { describe, expect, it } from "vite-plus/test";
22
3import { compareSemverVersions, normalizeSemverVersion, satisfiesSemverRange } from "./semver.ts";3import {
4 compareSemverVersions,
5 normalizeSemverVersion,
6 parseSemver,
7 satisfiesSemverRange,
8} from "./semver.ts";
49
5describe("semver helpers", () => {10describe("semver helpers", () => {
6 it("matches supported range groups", () => {11 it("matches supported range groups", () => {
...@@ -18,6 +23,25 @@ describe("semver helpers", () => {...@@ -18,6 +23,25 @@ describe("semver helpers", () => {
18 expect(normalizeSemverVersion("2.1")).toBe("2.1.0");23 expect(normalizeSemverVersion("2.1")).toBe("2.1.0");
19 });24 });
2025
26 it("normalizes and parses shorthand major-only versions", () => {
27 expect(normalizeSemverVersion("20")).toBe("20.0.0");
28 expect(normalizeSemverVersion("v18")).toBe("v18.0.0");
29 expect(normalizeSemverVersion("20-rc.1")).toBe("20.0.0-rc.1");
30 expect(parseSemver("20")).toEqual({ major: 20, minor: 0, patch: 0, prerelease: [] });
31 });
32
33 it("compares shorthand versions numerically instead of lexically", () => {
34 // Regression: "20" vs "9" previously fell back to string comparison, which
35 // ordered "20" before "9" ("2" < "9").
36 expect(compareSemverVersions("20", "9")).toBeGreaterThan(0);
37 expect(compareSemverVersions("18", "18.0.0")).toBe(0);
38 });
39
40 it("still rejects non-numeric shorthand and keeps empty input empty", () => {
41 expect(parseSemver("abc")).toBeNull();
42 expect(normalizeSemverVersion("")).toBe("");
43 });
44
21 it("compares prerelease versions before stable versions", () => {45 it("compares prerelease versions before stable versions", () => {
22 expect(compareSemverVersions("2.1.111-beta.1", "2.1.111")).toBeLessThan(0);46 expect(compareSemverVersions("2.1.111-beta.1", "2.1.111")).toBeLessThan(0);
23 });47 });
packages/shared/src/semver.ts+6-1
...@@ -17,7 +17,12 @@ export function normalizeSemverVersion(version: string): string {...@@ -17,7 +17,12 @@ export function normalizeSemverVersion(version: string): string {
17 }17 }
18 }18 }
1919
20 if (segments.length === 2) {20 // Pad shorthand versions ("20" or "20.1") up to three segments so major-only
21 // and minor-only inputs parse and compare numerically. This matches
22 // satisfiesSemverRange, which already treats a missing minor/patch as 0. The
23 // length > 0 guard keeps empty/garbage input empty (parseSemver still
24 // rejects it), and inputs with more than three segments are left untouched.
25 while (segments.length > 0 && segments.length < 3) {
21 segments.push("0");26 segments.push("0");
22 }27 }
2328