From 81789f0a70d874caaa2a25b0649dc761aa95c6a1 Mon Sep 17 00:00:00 2001 From: Prakharrdev Date: Sun, 20 Sep 2026 16:36:31 +0530 Subject: [PATCH 1/2] fix(version): apply the 0.x caret rule in satisfiesRange (#1177) --- src/utils/version.ts | 11 +++++++---- tests/utils/version-extensions.test.ts | 13 +++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/utils/version.ts b/src/utils/version.ts index 741ea6e4..7750c9b7 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -224,10 +224,13 @@ export function satisfiesRange(version: string, range: string): boolean { const target = coerceVersion(trimmedRange.slice(1)); if (!target) return false; if (compareVersions(v, target) < 0) return false; - const tMajor = majorVersion(target); - const vMajor = majorVersion(v); - if (tMajor === null || vMajor === null) return false; - return vMajor === tMajor; + const [vMaj, vMin, vPatch] = v.split(".").map(Number); + const [tMaj, tMin, tPatch] = target.split(".").map(Number); + // Caret follows the leftmost non-zero component of the target: major > 0 -> + // same major; 0.y.z -> same minor (the breaking position); 0.0.z -> exact. + if (tMaj > 0) return vMaj === tMaj; + if (tMin > 0) return vMaj === tMaj && vMin === tMin; + return vMaj === tMaj && vMin === tMin && vPatch === tPatch; } // `~` tilde: same major.minor, >= specified diff --git a/tests/utils/version-extensions.test.ts b/tests/utils/version-extensions.test.ts index 733db2b8..c1f96eeb 100644 --- a/tests/utils/version-extensions.test.ts +++ b/tests/utils/version-extensions.test.ts @@ -85,6 +85,19 @@ describe("satisfiesRange", () => { expect(satisfiesRange("2.0.0", "^1.2.0")).toBe(false); expect(satisfiesRange("1.1.0", "^1.2.0")).toBe(false); }); + it("caret on 0.x: minor is the breaking position", () => { + expect(satisfiesRange("0.25.0", "^0.25.0")).toBe(true); + expect(satisfiesRange("0.25.1", "^0.25.0")).toBe(true); + expect(satisfiesRange("0.26.0", "^0.25.0")).toBe(false); + expect(satisfiesRange("0.99.0", "^0.25.0")).toBe(false); + expect(satisfiesRange("0.2.9", "^0.2.3")).toBe(true); + expect(satisfiesRange("0.3.0", "^0.2.3")).toBe(false); + }); + it("caret on 0.0.x: patch is the breaking position", () => { + expect(satisfiesRange("0.0.3", "^0.0.3")).toBe(true); + expect(satisfiesRange("0.0.4", "^0.0.3")).toBe(false); + expect(satisfiesRange("0.1.0", "^0.0.3")).toBe(false); + }); it("tilde: same major.minor, >= specified patch", () => { expect(satisfiesRange("1.2.5", "~1.2.3")).toBe(true); expect(satisfiesRange("1.3.0", "~1.2.3")).toBe(false); From 0767d0f7a0522422ab32b6ed0ccfd82fa93591fd Mon Sep 17 00:00:00 2001 From: Prakharrdev Date: Sun, 20 Sep 2026 17:56:36 +0530 Subject: [PATCH 2/2] fix(version): honor component count for partial caret ranges in satisfiesRange --- src/utils/version.ts | 15 +++++++++------ tests/utils/version-extensions.test.ts | 12 ++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/utils/version.ts b/src/utils/version.ts index 7750c9b7..355669e2 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -219,18 +219,21 @@ export function satisfiesRange(version: string, range: string): boolean { return target !== null && compareVersions(v, target) === 0; } - // `^` caret: same major, >= specified, < next major + // `^` caret: >= floor, ceiling at the leftmost non-zero component of the + // target (npm: ^1.2.0 -> <2.0.0; ^0.2.3 -> <0.3.0; ^0.0.3 -> <0.0.4). + // Partial ranges keep their component count: ^0 admits all 0.x, ^0.0 admits 0.0.x. if (trimmedRange.startsWith("^")) { const target = coerceVersion(trimmedRange.slice(1)); if (!target) return false; if (compareVersions(v, target) < 0) return false; const [vMaj, vMin, vPatch] = v.split(".").map(Number); const [tMaj, tMin, tPatch] = target.split(".").map(Number); - // Caret follows the leftmost non-zero component of the target: major > 0 -> - // same major; 0.y.z -> same minor (the breaking position); 0.0.z -> exact. - if (tMaj > 0) return vMaj === tMaj; - if (tMin > 0) return vMaj === tMaj && vMin === tMin; - return vMaj === tMaj && vMin === tMin && vPatch === tPatch; + const parts = trimmedRange.slice(1).split("-")[0].split("."); + if (tMaj > 0) return vMaj === tMaj; // ^1, ^1.2, ^1.2.3 + if (tMin > 0) return vMaj === tMaj && vMin === tMin; // ^0.1, ^0.1.x + if (parts.length >= 3) return vMaj === tMaj && vMin === tMin && vPatch === tPatch; // ^0.0.3 exact + if (parts.length === 2) return vMaj === tMaj && vMin === tMin; // ^0.0 -> 0.0.x + return vMaj === tMaj; // ^0 -> all 0.x } // `~` tilde: same major.minor, >= specified diff --git a/tests/utils/version-extensions.test.ts b/tests/utils/version-extensions.test.ts index c1f96eeb..39a1e630 100644 --- a/tests/utils/version-extensions.test.ts +++ b/tests/utils/version-extensions.test.ts @@ -98,6 +98,18 @@ describe("satisfiesRange", () => { expect(satisfiesRange("0.0.4", "^0.0.3")).toBe(false); expect(satisfiesRange("0.1.0", "^0.0.3")).toBe(false); }); + it("caret partial forms: component count sets the ceiling", () => { + expect(satisfiesRange("0.25.0", "^0")).toBe(true); + expect(satisfiesRange("0.99.0", "^0")).toBe(true); + expect(satisfiesRange("1.0.0", "^0")).toBe(false); + expect(satisfiesRange("0.0.1", "^0.0")).toBe(true); + expect(satisfiesRange("0.0.9", "^0.0")).toBe(true); + expect(satisfiesRange("0.1.0", "^0.0")).toBe(false); + expect(satisfiesRange("1.9.9", "^1")).toBe(true); + expect(satisfiesRange("2.0.0", "^1")).toBe(false); + expect(satisfiesRange("1.9.9", "^1.2")).toBe(true); + expect(satisfiesRange("2.0.0", "^1.2")).toBe(false); + }); it("tilde: same major.minor, >= specified patch", () => { expect(satisfiesRange("1.2.5", "~1.2.3")).toBe(true); expect(satisfiesRange("1.3.0", "~1.2.3")).toBe(false);