diff --git a/src/utils/version.ts b/src/utils/version.ts index 741ea6e4..355669e2 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -219,15 +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 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); + 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 733db2b8..39a1e630 100644 --- a/tests/utils/version-extensions.test.ts +++ b/tests/utils/version-extensions.test.ts @@ -85,6 +85,31 @@ 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("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);