Skip to content

feat: add semver comparison operators#53

Merged
joalves merged 10 commits intomainfrom
feat/semver-operators
Mar 24, 2026
Merged

feat: add semver comparison operators#53
joalves merged 10 commits intomainfrom
feat/semver-operators

Conversation

@joalves
Copy link
Copy Markdown
Contributor

@joalves joalves commented Mar 23, 2026

Summary

  • Add versionCompare() method to Evaluator with full semver parsing (v prefix stripping, build metadata ignored, pre-release handling, numeric comparison, missing parts treated as 0, leading zeros tolerated)
  • Add 5 new operators: semver_eq, semver_gt, semver_gte, semver_lt, semver_lte — each delegates to versionCompare following the existing BinaryOperator pattern
  • Reject malformed inputs (empty core after normalization like "v", "+build") by returning null
  • Use string-length + lexicographic comparison for numeric identifiers to avoid parseInt precision loss beyond Number.MAX_SAFE_INTEGER
  • CI workflow now triggers on all PR events, not just opened

Bundle Size

Enabling helpers: true in @babel/plugin-transform-runtime deduplicates Babel helpers (_classCallCheck, _createClass, _slicedToArray, etc.) that were previously inlined into every module. This reduces the minified browser bundle from 118 KiB (main) to 97.7 KiB — a 17% reduction — even after adding 5 new operator modules and the versionCompare method.

Main This PR
Modules 15 operators 20 operators
absmartly.min.js 118 KiB 97.7 KiB

Backward Compatibility

Old SDKs receiving expressions with unknown semver operators will not crash. The evaluator returns null for unrecognized operators, which converts to false — users simply won't match the targeting rule. Verified this behavior across all SDKs (JS, Java, Python, Go, Ruby, PHP, Swift, .NET, Dart, C++, Rust, Scala, Elixir).

Test Plan

  • 20 unit tests for versionCompare (equal versions, major/minor/patch, numeric ordering, missing parts, pre-release, v prefix, null inputs, coercion, build metadata, pre-release+build, empty strings, undefined, empty core, leading zeros)
  • Unit tests for all 5 operator classes with afterEach cleanup and null path assertions
  • All 306 tests pass across 34 suites
  • Cross-SDK test scenarios added to cross-sdk-tests repo (9 scenarios in generate_scenarios.py)

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds SemVer-aware comparison to the evaluator: a new Evaluator.versionCompare method with SemVer parsing and comparison (core, pre-release and build metadata handling), registers five new binary operators (semver_eq, semver_gt, semver_gte, semver_lt, semver_lte) in the jsonexpr operators map, and implements those operator classes. Extensive Jest tests for versionCompare and each operator are included, alongside a small CI workflow update and test helper changes to mock versionCompare.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Poem

🐇 I nibble digits, dots and “v” with glee,
Counting cores and pre-releases for thee,
Five little hops—eq, gt, gte, lt, lte—
I sort and compare till the versions agree,
A tiny rabbit cheering SemVer harmony.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add semver comparison operators' directly and clearly describes the main change in the changeset—adding five new semver comparison operators and the underlying versionCompare functionality.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/semver-operators

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (7)
src/__tests__/jsonexpr/operators/semver_lt.test.js (2)

34-39: Consider asserting that versionCompare is called for null inputs.

The test verifies that null is returned for null inputs, but doesn't assert on the versionCompare call. Based on the operator implementation, versionCompare(null, null) should still be called (and return null). Adding this assertion would strengthen the test.

🧪 Proposed enhancement
 		it("should return null for null inputs", () => {
 			expect(operator.evaluate(evaluator, [null, null])).toBe(null);
+			expect(evaluator.versionCompare).toHaveBeenCalledWith(null, null);
 
 			evaluator.evaluate.mockClear();
 			evaluator.versionCompare.mockClear();
 		});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/jsonexpr/operators/semver_lt.test.js` around lines 34 - 39, Add
an assertion in the "should return null for null inputs" test to verify that
evaluator.versionCompare was invoked with the null inputs when
operator.evaluate(evaluator, [null, null]) is called; specifically assert
evaluator.versionCompare was called once (or called with (null, null)) after the
evaluate call and before clearing mocks (use the existing mock on
evaluator.versionCompare and evaluator.evaluate to perform the check).

7-9: Consider using beforeEach for mock clearing.

The shared evaluator with manual mockClear() at the end of each test is fragile—if a test fails before clearing, subsequent tests may be affected. Using Jest's beforeEach hook would be more robust and reduce repetition.

♻️ Proposed refactor
 	describe("evaluate", () => {
 		const evaluator = mockEvaluator();
 
+		beforeEach(() => {
+			evaluator.evaluate.mockClear();
+			evaluator.versionCompare.mockClear();
+		});
+
 		it("should return true when left version is less", () => {
 			expect(operator.evaluate(evaluator, ["1.0.0", "2.0.0"])).toBe(true);
 			expect(evaluator.versionCompare).toHaveBeenCalledWith("1.0.0", "2.0.0");
-
-			evaluator.evaluate.mockClear();
-			evaluator.versionCompare.mockClear();
 		});
 		// ... similarly for other tests
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/jsonexpr/operators/semver_lt.test.js` around lines 7 - 9, The
tests create a shared evaluator via const evaluator = mockEvaluator() and call
evaluator.mockClear() manually at each test end; replace this pattern with a
Jest beforeEach that either reassigns evaluator = mockEvaluator() or calls
evaluator.mockClear() before each test so mocks are reset even if a test fails —
update references to the evaluator created by mockEvaluator() in the
describe("evaluate", ...) block and remove per-test manual mockClear() calls to
avoid fragile cleanup.
src/__tests__/jsonexpr/operators/semver_gt.test.js (2)

34-39: Add versionCompare call assertion for null inputs.

Similar to the suggestion for semver_lt.test.js, consider asserting that versionCompare is called with (null, null) to fully document the expected behaviour.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/jsonexpr/operators/semver_gt.test.js` around lines 34 - 39, The
test for null inputs should also assert that evaluator.versionCompare was
invoked with (null, null); update the "should return null for null inputs" case
to add expect(evaluator.versionCompare).toHaveBeenCalledWith(null, null) (placed
after the operator.evaluate assertion and before mockClear calls) so the test
documents that operator.evaluate triggered versionCompare with those arguments.

7-9: Same refactoring opportunity as semver_lt.test.js.

Consider using beforeEach for mock clearing to improve test isolation and reduce repetition. The same pattern applies to other semver operator test files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/jsonexpr/operators/semver_gt.test.js` around lines 7 - 9, Tests
in describe("evaluate") repeat mock setup and lack per-test isolation; change
the const evaluator = mockEvaluator() pattern to initialize/reset the mock in a
beforeEach block: declare evaluator in the describe scope (use let evaluator)
and call evaluator = mockEvaluator() (or jest.clearAllMocks() plus re-init)
inside beforeEach so every test gets a fresh mock; apply the same change to
semver_gt.test.js and other semver operator test files following the
semver_lt.test.js refactor.
src/__tests__/jsonexpr/operators/evaluator.js (1)

21-27: Inconsistent indentation in versionCompare mock body.

The body of versionCompare (lines 22-26) lacks the leading tab indentation used elsewhere in this object (e.g., compare on lines 29+). This breaks the consistent formatting style and may trigger linter warnings.

🔧 Proposed fix
 	versionCompare: jest.fn((lhs, rhs) => {
-	const lhsStr = typeof lhs === "string" ? lhs : null;
-	const rhsStr = typeof rhs === "string" ? rhs : null;
-	if (lhsStr === null || rhsStr === null) return null;
-	return lhsStr === rhsStr ? 0 : lhsStr > rhsStr ? 1 : -1;
-}),
+		const lhsStr = typeof lhs === "string" ? lhs : null;
+		const rhsStr = typeof rhs === "string" ? rhs : null;
+		if (lhsStr === null || rhsStr === null) return null;
+		return lhsStr === rhsStr ? 0 : lhsStr > rhsStr ? 1 : -1;
+	}),
 
-	compare: jest.fn((lhs, rhs) => {
+		compare: jest.fn((lhs, rhs) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/jsonexpr/operators/evaluator.js` around lines 21 - 27, The mock
implementation for versionCompare has inconsistent indentation compared to other
operator mocks; update the body of the jest.fn for versionCompare so its inner
lines (the const declarations, null checks and return) use the same leading tab
indentation as the surrounding entries (e.g., match the indentation style used
by compare) to satisfy the linter and maintain consistent formatting for the
versionCompare jest.fn mock.
src/__tests__/jsonexpr/operators/semver_lte.test.js (2)

7-39: Consider using beforeEach/afterEach for mock clearing.

The manual mockClear() calls at the end of each test are repetitive. Using Jest's afterEach hook would be cleaner and ensure mocks are always cleared, even if a test fails mid-execution.

♻️ Suggested refactor
 	describe("evaluate", () => {
 		const evaluator = mockEvaluator();

+		afterEach(() => {
+			evaluator.evaluate.mockClear();
+			evaluator.versionCompare.mockClear();
+		});
+
 		it("should return true when left version is less", () => {
 			expect(operator.evaluate(evaluator, ["1.0.0", "2.0.0"])).toBe(true);
 			expect(evaluator.versionCompare).toHaveBeenCalledWith("1.0.0", "2.0.0");
-
-			evaluator.evaluate.mockClear();
-			evaluator.versionCompare.mockClear();
 		});
 		// ... apply same pattern to other tests
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/jsonexpr/operators/semver_lte.test.js` around lines 7 - 39,
Replace the repetitive manual evaluator.evaluate.mockClear() and
evaluator.versionCompare.mockClear() calls in each it block with a single
afterEach hook that clears those mocks; locate the test suite around
describe("evaluate", ...) and add an afterEach(() => {
evaluator.evaluate.mockClear(); evaluator.versionCompare.mockClear(); }) (or use
jest.clearAllMocks() if preferred) and remove the per-test mockClear() lines so
mocks are always cleared even on test failures.

34-39: Consider verifying versionCompare call behaviour for null inputs.

The other semver operator tests (e.g., semver_eq.test.js) verify whether versionCompare was or wasn't called when inputs are null. This test lacks that assertion, making it inconsistent with the test suite.

♻️ Suggested improvement
 		it("should return null for null inputs", () => {
 			expect(operator.evaluate(evaluator, [null, null])).toBe(null);
+			expect(evaluator.versionCompare).not.toHaveBeenCalled();

 			evaluator.evaluate.mockClear();
 			evaluator.versionCompare.mockClear();
 		});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/jsonexpr/operators/semver_lte.test.js` around lines 34 - 39,
The test semver_lte.test.js's "should return null for null inputs" currently
checks the return value but doesn't assert whether evaluator.versionCompare was
invoked; update the test for operator.evaluate(evaluator, [null, null]) to also
assert that evaluator.versionCompare was not called (e.g.,
expect(evaluator.versionCompare).not.toHaveBeenCalled()), and keep the existing
mockClear() calls for evaluator.evaluate and evaluator.versionCompare to match
the other semver operator tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/__tests__/jsonexpr/operators/evaluator.js`:
- Around line 21-27: The mock implementation for versionCompare has inconsistent
indentation compared to other operator mocks; update the body of the jest.fn for
versionCompare so its inner lines (the const declarations, null checks and
return) use the same leading tab indentation as the surrounding entries (e.g.,
match the indentation style used by compare) to satisfy the linter and maintain
consistent formatting for the versionCompare jest.fn mock.

In `@src/__tests__/jsonexpr/operators/semver_gt.test.js`:
- Around line 34-39: The test for null inputs should also assert that
evaluator.versionCompare was invoked with (null, null); update the "should
return null for null inputs" case to add
expect(evaluator.versionCompare).toHaveBeenCalledWith(null, null) (placed after
the operator.evaluate assertion and before mockClear calls) so the test
documents that operator.evaluate triggered versionCompare with those arguments.
- Around line 7-9: Tests in describe("evaluate") repeat mock setup and lack
per-test isolation; change the const evaluator = mockEvaluator() pattern to
initialize/reset the mock in a beforeEach block: declare evaluator in the
describe scope (use let evaluator) and call evaluator = mockEvaluator() (or
jest.clearAllMocks() plus re-init) inside beforeEach so every test gets a fresh
mock; apply the same change to semver_gt.test.js and other semver operator test
files following the semver_lt.test.js refactor.

In `@src/__tests__/jsonexpr/operators/semver_lt.test.js`:
- Around line 34-39: Add an assertion in the "should return null for null
inputs" test to verify that evaluator.versionCompare was invoked with the null
inputs when operator.evaluate(evaluator, [null, null]) is called; specifically
assert evaluator.versionCompare was called once (or called with (null, null))
after the evaluate call and before clearing mocks (use the existing mock on
evaluator.versionCompare and evaluator.evaluate to perform the check).
- Around line 7-9: The tests create a shared evaluator via const evaluator =
mockEvaluator() and call evaluator.mockClear() manually at each test end;
replace this pattern with a Jest beforeEach that either reassigns evaluator =
mockEvaluator() or calls evaluator.mockClear() before each test so mocks are
reset even if a test fails — update references to the evaluator created by
mockEvaluator() in the describe("evaluate", ...) block and remove per-test
manual mockClear() calls to avoid fragile cleanup.

In `@src/__tests__/jsonexpr/operators/semver_lte.test.js`:
- Around line 7-39: Replace the repetitive manual evaluator.evaluate.mockClear()
and evaluator.versionCompare.mockClear() calls in each it block with a single
afterEach hook that clears those mocks; locate the test suite around
describe("evaluate", ...) and add an afterEach(() => {
evaluator.evaluate.mockClear(); evaluator.versionCompare.mockClear(); }) (or use
jest.clearAllMocks() if preferred) and remove the per-test mockClear() lines so
mocks are always cleared even on test failures.
- Around line 34-39: The test semver_lte.test.js's "should return null for null
inputs" currently checks the return value but doesn't assert whether
evaluator.versionCompare was invoked; update the test for
operator.evaluate(evaluator, [null, null]) to also assert that
evaluator.versionCompare was not called (e.g.,
expect(evaluator.versionCompare).not.toHaveBeenCalled()), and keep the existing
mockClear() calls for evaluator.evaluate and evaluator.versionCompare to match
the other semver operator tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb6230c3-3c5f-4fb8-b977-7362a31b498b

📥 Commits

Reviewing files that changed from the base of the PR and between 47390ae and b3d6fd9.

📒 Files selected for processing (15)
  • cross-sdk-tests/test_scenarios_complete.json
  • src/__tests__/jsonexpr/evaluator.test.js
  • src/__tests__/jsonexpr/operators/evaluator.js
  • src/__tests__/jsonexpr/operators/semver_eq.test.js
  • src/__tests__/jsonexpr/operators/semver_gt.test.js
  • src/__tests__/jsonexpr/operators/semver_gte.test.js
  • src/__tests__/jsonexpr/operators/semver_lt.test.js
  • src/__tests__/jsonexpr/operators/semver_lte.test.js
  • src/jsonexpr/evaluator.ts
  • src/jsonexpr/jsonexpr.ts
  • src/jsonexpr/operators/semver_eq.ts
  • src/jsonexpr/operators/semver_gt.ts
  • src/jsonexpr/operators/semver_gte.ts
  • src/jsonexpr/operators/semver_lt.ts
  • src/jsonexpr/operators/semver_lte.ts

- Return null for empty string inputs in versionCompare
- Extract parseSemver/compareIdentifiers to module-level functions
- Add explicit return type annotation to versionCompare
- Add tests for empty string, undefined, pre-release+build metadata
- Add rhs null test for semver_eq operator
- Fix mock evaluator indentation
- Add basic semver_gt scenarios to cross-SDK tests
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/__tests__/jsonexpr/operators/evaluator.js (1)

21-26: Avoid lexicographic fallback in the SemVer mock.

On Line [25], lhsStr > rhsStr is lexicographic, so values like "1.10.0" and "1.2.0" are ordered incorrectly for semver. In this shared helper, that can hide semver-ordering issues in operator tests. Prefer a neutral default for non-equality and let each test explicitly stub ordering.

Proposed change
 		versionCompare: jest.fn((lhs, rhs) => {
 			const lhsStr = typeof lhs === "string" ? lhs : null;
 			const rhsStr = typeof rhs === "string" ? rhs : null;
 			if (lhsStr === null || rhsStr === null) return null;
-			return lhsStr === rhsStr ? 0 : lhsStr > rhsStr ? 1 : -1;
+			return lhsStr === rhsStr ? 0 : null;
 		}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/jsonexpr/operators/evaluator.js` around lines 21 - 26, The mock
for versionCompare currently falls back to lexicographic comparison (lhsStr >
rhsStr), which misorders semver like "1.10.0" vs "1.2.0"; update the jest.fn
mock for versionCompare so it only returns 0 when lhs and rhs are equal strings
and returns null for non-equal string inputs (i.e., remove the lexicographic
comparison branch), so tests must explicitly stub ordering when needed; locate
the versionCompare mock in the evaluator tests and change its implementation
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/jsonexpr/evaluator.ts`:
- Around line 4-20: The parseSemver function must reject inputs that normalize
to an empty core: after stripping leading "v"/"V" and trimming "+build"
metadata, if core === "" or the resulting parts array has no non-empty segments,
return null instead of {parts, preRelease}; also ensure the version-comparison
logic that currently compares parsed results (the block that uses parseSemver's
output) checks for a null parseSemver return and returns null immediately when
either side is null rather than proceeding to compare empty/malformed parts.
- Around line 23-33: The current numeric detection and comparison in
src/jsonexpr/evaluator.ts (variables aNum, bNum, aIsNum, bIsNum using parseInt)
loses precision for integers beyond Number.MAX_SAFE_INTEGER; replace
parseInt-based logic with a regex numeric check (e.g., /^\d+$/) to set aIsNum
and bIsNum, then when both are numeric compare by length (longer = larger) and,
if lengths equal, by lexicographic string comparison to preserve arbitrary
precision; keep the existing numeric-vs-non-numeric precedence (if aIsNum &&
!bIsNum return -1, if bIsNum && !aIsNum return 1) and fall back to the existing
string equality/lexicographic branch for non-numeric identifiers.

---

Nitpick comments:
In `@src/__tests__/jsonexpr/operators/evaluator.js`:
- Around line 21-26: The mock for versionCompare currently falls back to
lexicographic comparison (lhsStr > rhsStr), which misorders semver like "1.10.0"
vs "1.2.0"; update the jest.fn mock for versionCompare so it only returns 0 when
lhs and rhs are equal strings and returns null for non-equal string inputs
(i.e., remove the lexicographic comparison branch), so tests must explicitly
stub ordering when needed; locate the versionCompare mock in the evaluator tests
and change its implementation accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 63034ccf-5a80-4d24-a29f-05a1d128d767

📥 Commits

Reviewing files that changed from the base of the PR and between b3d6fd9 and a6f071d.

📒 Files selected for processing (5)
  • cross-sdk-tests/test_scenarios_complete.json
  • src/__tests__/jsonexpr/evaluator.test.js
  • src/__tests__/jsonexpr/operators/evaluator.js
  • src/__tests__/jsonexpr/operators/semver_eq.test.js
  • src/jsonexpr/evaluator.ts
✅ Files skipped from review due to trivial changes (2)
  • cross-sdk-tests/test_scenarios_complete.json
  • src/tests/jsonexpr/evaluator.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tests/jsonexpr/operators/semver_eq.test.js

@joalves
Copy link
Copy Markdown
Contributor Author

joalves commented Mar 24, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Reviewed: semver parsing logic, operator delegation, null propagation, test coverage, git history context, and past PR comments. 5 independent review passes found no issues scoring above the confidence threshold.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

- Reject inputs with empty core after stripping (e.g., "v", "+build")
- Use regex + string-length comparison for numeric identifiers to avoid
  large integer precision loss with parseInt
- Use nullish coalescing (??) instead of || for missing version parts
- Fix mock versionCompare to use numeric segment comparison
- Switch all operator tests from manual mockClear to afterEach
- Add versionCompare not-called assertions to all null tests
- Split null tests into separate lhs/rhs cases for gt/gte/lt/lte
- Add tests for empty-core inputs
@joalves joalves marked this pull request as ready for review March 24, 2026 10:59
Scenarios moved to the dedicated cross-sdk-tests repository.
Copy link
Copy Markdown
Contributor

@calthejuggler calthejuggler left a comment

Choose a reason for hiding this comment

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

LGTM 🙂 Just one question about what we do with invalid strings

if (a.length !== b.length) {
return a.length > b.length ? 1 : -1;
}
return a === b ? 0 : a > b ? 1 : -1;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Won't this still be a string comparison? 🤔 "12" > "7" === false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh, I figured it out 😅 They have different lengths, so "12".length > "7".length === true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct — for same-length numeric strings, lexicographic comparison gives the right answer (e.g., "12" > "07" is true). Different lengths are handled by the length check first ("12".length > "7".length). This is how we avoid parseInt precision loss for very large numbers.

}
if (aIsNum) return -1;
if (bIsNum) return 1;
return a === b ? 0 : a > b ? 1 : -1;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here 🤔 Also, if neither aIsNum or bIsNum, then do we fall back to straight string comparison?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm thinking strings like 1.02.30. Not sure what the best way to go is if they are invalid, but we should be explicit in what we choose 🙂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Leading zeros like 1.02.30 were being rejected by the strict semver regex and falling through to string comparison, causing incorrect results (e.g., versionCompare("1.02.0", "1.2.0") was returning 1 instead of 0).

Fixed in fd43a80 — now using /^\d+$/ with leading zero stripping so 02 is treated as 2. This makes us lenient with non-strict semver inputs while preserving correct numeric ordering.

For the non-numeric fallback: yes, if neither side is numeric (e.g., pre-release identifiers like "alpha" vs "beta"), we fall back to straight string comparison, which is correct per the semver spec for alphanumeric pre-release identifiers.

joalves added 4 commits March 24, 2026 15:44
Leading zeros like "1.02.30" are now treated as their numeric value
(02 == 2). Uses permissive regex /^\d+$/ with leading zero stripping
instead of strict semver regex that rejected them.
The 5 new semver operator modules + evaluator versionCompare method
add ~5.6 KiB of source which translates to ~14 KiB in the minified
bundle due to per-module webpack overhead. No bloat or unnecessary
polyfills — the increase is proportional to the code added.
Enable helpers: true in @babel/plugin-transform-runtime so Babel
helpers (_classCallCheck, _createClass, _slicedToArray, etc.) are
imported from a shared runtime instead of being inlined into every
module. Reduces minified browser bundle from 132 KiB to 97.7 KiB.

Also reverts the maxAssetSize bump since the bundle is now well
under the 128 KiB limit.
@joalves joalves merged commit 4b3f3c2 into main Mar 24, 2026
2 checks passed
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.

2 participants