@sdxc/semver 0.0.0-pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sergio Xalambrí
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # @sdxc/semver
2
+
3
+ SemVer 2.0.0 parsing, precedence ordering and range-free version comparisons.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm add @sdxc/semver
9
+ ```
10
+
11
+ `parse()` reports failures as a `Result` from [`@sdxc/result`](https://www.npmjs.com/package/@sdxc/result), which installs alongside this package.
12
+
13
+ ## Usage
14
+
15
+ ### Comparing Two Versions
16
+
17
+ ```typescript
18
+ import { satisfies } from "@sdxc/semver";
19
+
20
+ satisfies("1.9.0", "^", "1.4.2"); // true
21
+ satisfies("1.5.0", "~", "1.4.2"); // false
22
+ satisfies("2.0.0", ">", "1.99.99"); // true
23
+
24
+ satisfies("nightly", ">", "1.0.0"); // false — "nightly" is not a version
25
+ ```
26
+
27
+ ### Sorting A List Of Versions
28
+
29
+ ```typescript
30
+ import { compare } from "@sdxc/semver";
31
+
32
+ ["2026.10.1", "2026.9.4", "2026.9.30"].sort(compare);
33
+ // ["2026.9.4", "2026.9.30", "2026.10.1"]
34
+
35
+ ["1.0.0", "1.0.0-rc.1", "1.0.0-beta.11", "1.0.0-beta.2"].sort(compare);
36
+ // ["1.0.0-beta.2", "1.0.0-beta.11", "1.0.0-rc.1", "1.0.0"]
37
+ ```
38
+
39
+ ### Reading A Version's Elements
40
+
41
+ ```typescript
42
+ import { parse } from "@sdxc/semver";
43
+ import { isFailure } from "@sdxc/result";
44
+
45
+ let result = parse("v2.1.0-rc.3+build.9");
46
+
47
+ if (isFailure(result)) {
48
+ console.error(result.error.message); // Invalid version: "v2.1.0-rc.3+build.9"
49
+ return;
50
+ }
51
+
52
+ result.data; // { major: 2, minor: 1, patch: 0, prerelease: ["rc", "3"] }
53
+ ```
54
+
55
+ ## API
56
+
57
+ ### `parse(text: string): Result<SemVer, InvalidSemVerError>`
58
+
59
+ Read a version string into its elements, returning a `Success<SemVer>` or a `Failure<InvalidSemVerError>` naming the rejected text. The grammar is SemVer 2.0.0 with one addition: an optional leading `v`, as a git tag or a user agent writes it. Build metadata is dropped, since it carries no precedence.
60
+
61
+ ```typescript
62
+ parse("1.2.3"); // { status: "success", data: { major: 1, minor: 2, patch: 3, prerelease: [] } }
63
+ parse("v1.0.0-rc.1"); // prerelease: ["rc", "1"]
64
+ parse("01.2.3"); // { status: "failure", error: InvalidSemVerError } — a padded element
65
+ ```
66
+
67
+ ### `compare(a: string, b: string): number`
68
+
69
+ Order two version strings by SemVer 2.0.0 precedence, ready to hand to [`Array.prototype.sort`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). Returns a negative number when `a` comes first, a positive one when `b` does, and `0` when the two rank equally.
70
+
71
+ The ordering is total, so a list whose entries come from somewhere unchecked — a registry, a tag listing, user input — sorts in one call. Text that is not a version ranks below every version and ties with other such text, which collects those entries at the front where the caller can see them.
72
+
73
+ ```typescript
74
+ compare("1.9.0", "1.10.0"); // negative — elements compare as numbers
75
+ compare("1.0.0-rc.1", "1.0.0"); // negative — a prerelease precedes its release
76
+ compare("1.2.3+a", "1.2.3+b"); // 0 — build metadata carries no precedence
77
+ compare("latest", "0.0.0"); // negative — text that is not a version sorts first
78
+ ```
79
+
80
+ ### `satisfies(value: string, comparison: SemVerComparison, against: string): boolean`
81
+
82
+ Answer whether `value` stands in the named relation to `against`. Either side that is not a version answers `false`, so a field holding something else matches nothing instead of failing the check.
83
+
84
+ A prerelease takes part by precedence alone: `1.2.4-rc.1` satisfies `^ 1.2.3`, because it outranks `1.2.3` and shares its major. Test a release channel by comparing against the prerelease you mean — `satisfies(value, ">=", "1.2.4-rc.1")` — when that is the boundary you want.
85
+
86
+ ```typescript
87
+ satisfies("1.2.3", "=", "1.2.3"); // true
88
+ satisfies("1.4.5", "~", "1.4.2"); // true, the same minor
89
+ satisfies("1.5.0", "~", "1.4.2"); // false, a later minor
90
+ satisfies("0.3.0", "^", "0.2.3"); // false, the left-most non-zero element moved
91
+ ```
92
+
93
+ | Comparison | Holds when |
94
+ | ---------- | ---------------------------------------------------------------------- |
95
+ | `=` | The two rank equally |
96
+ | `!=` | The two rank differently |
97
+ | `<` | `value` precedes `against` |
98
+ | `<=` | `value` precedes `against` or ranks equally |
99
+ | `>` | `value` follows `against` |
100
+ | `>=` | `value` follows `against` or ranks equally |
101
+ | `~` | `value` is at least `against` and shares its major and minor |
102
+ | `^` | `value` is at least `against` and keeps its left-most non-zero element |
103
+
104
+ ### `InvalidSemVerError`
105
+
106
+ Error describing text that fails the SemVer 2.0.0 grammar. It arrives inside a `Failure` value.
107
+
108
+ - `text`: `string` - The rejected text, kept verbatim for diagnostics
109
+ - `name`: `string` - Always `"InvalidSemVerError"`
110
+ - `message`: `string` - `Invalid version: "<text>"`, quoted so whitespace and empty strings stay visible in logs
111
+
112
+ ### Types
113
+
114
+ #### `SemVer`
115
+
116
+ ```typescript
117
+ interface SemVer {
118
+ major: number;
119
+ minor: number;
120
+ patch: number;
121
+ prerelease: string[];
122
+ }
123
+ ```
124
+
125
+ A version taken apart for precedence. `prerelease` holds the dot-separated identifiers after the `-` and is empty for a release. Build metadata is absent, so two versions differing only in it are the same `SemVer`.
126
+
127
+ #### `SemVerComparison`
128
+
129
+ ```typescript
130
+ type SemVerComparison = "=" | "!=" | "<" | "<=" | ">" | ">=" | "~" | "^";
131
+ ```
132
+
133
+ The comparisons `satisfies()` accepts. It is a closed union, so an editor offers the eight as a list and a stored value round-trips through a schema.
134
+
135
+ ### Precedence Rules
136
+
137
+ Versions compare element by element: major, then minor, then patch, each as a number rather than as text, so `1.10.0` follows `1.9.0`. Build metadata is ignored throughout.
138
+
139
+ A version carrying a prerelease precedes the same version without one. Two prereleases compare identifier by identifier: numerically when both identifiers are numeric, by character otherwise, with a numeric identifier ranking below a textual one. When one list runs out while the other continues, the shorter one comes first, so `1.0.0-alpha` precedes `1.0.0-alpha.1`.
140
+
141
+ ## Pattern: Gating A Feature On A Client Version
142
+
143
+ Store the comparison and the version as data, and let an operator edit the rule without a deploy.
144
+
145
+ ```typescript
146
+ import type { SemVerComparison } from "@sdxc/semver";
147
+
148
+ import { satisfies } from "@sdxc/semver";
149
+
150
+ interface VersionRule {
151
+ comparison: SemVerComparison;
152
+ version: string;
153
+ }
154
+
155
+ function allows(rule: VersionRule, clientVersion: string): boolean {
156
+ return satisfies(clientVersion, rule.comparison, rule.version);
157
+ }
158
+
159
+ allows({ comparison: "^", version: "2.0.0" }, "2.4.1"); // true
160
+ allows({ comparison: ">=", version: "3.0.0" }, "2.4.1"); // false
161
+ ```
162
+
163
+ ## Pattern: Picking The Newest Published Version
164
+
165
+ Registries hand back whatever they hold, including placeholders and tags. Sorting with `compare()` needs no filtering pass first, because the newest version is the last entry either way.
166
+
167
+ ```typescript
168
+ import { compare } from "@sdxc/semver";
169
+
170
+ function newest(versions: string[]): string | undefined {
171
+ return [...versions].sort(compare).at(-1);
172
+ }
173
+
174
+ newest(["0.0.0-pre.1", "2026.9.4", "2026.10.1"]); // "2026.10.1"
175
+ newest(["0.0.0-pre.9", "0.0.0-pre.10"]); // "0.0.0-pre.10"
176
+ ```
177
+
178
+ ## Pattern: Deciding Whether An Upgrade Is Breaking
179
+
180
+ Parse both sides once and read the elements, when the question is about the shape of the change rather than about a threshold.
181
+
182
+ ```typescript
183
+ import { parse } from "@sdxc/semver";
184
+ import { isFailure } from "@sdxc/result";
185
+
186
+ function isBreaking(from: string, to: string): boolean {
187
+ let before = parse(from);
188
+ let after = parse(to);
189
+
190
+ if (isFailure(before) || isFailure(after)) return true;
191
+ if (before.data.major !== 0) return after.data.major !== before.data.major;
192
+
193
+ return after.data.minor !== before.data.minor;
194
+ }
195
+
196
+ isBreaking("1.4.2", "1.9.0"); // false
197
+ isBreaking("0.2.3", "0.3.0"); // true — below 1.0.0 the minor carries compatibility
198
+ ```
199
+
200
+ ## Versioning
201
+
202
+ Releases are dated rather than semantic. A version is the UTC date it was published, written `YYYY.M.D`, so `2026.9.4` is the release from 4 September 2026. At most one release goes out per day.
203
+
204
+ Those numbers say when, not what: a later date means a later release and carries no compatibility promise. Any release may change or remove an export.
205
+
206
+ Depend on one exact date, and move it when you are ready to take the change:
207
+
208
+ ```json
209
+ {
210
+ "dependencies": {
211
+ "@sdxc/semver": "2026.9.4"
212
+ }
213
+ }
214
+ ```
215
+
216
+ A caret or tilde range reads the date as major, minor and patch, so it accepts every later release in the same year. An exact version keeps the upgrade yours to schedule.
217
+
218
+ ## License
219
+
220
+ MIT
221
+
222
+ ## Author
223
+
224
+ [Sergio Xalambrí](https://sergiodxa.com)
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Total SemVer 2.0.0 ordering over version strings, so a list read from a
3
+ * registry or a tag listing sorts in one call even when some entry in it turns
4
+ * out to be something other than a version.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /**
10
+ * Order two version strings by SemVer 2.0.0 precedence, ready to hand to
11
+ * `Array.prototype.sort`. Text that is not a version ranks below every version
12
+ * and ties with other such text, which keeps the ordering total.
13
+ *
14
+ * @returns A negative number when `a` comes first, zero when the two rank equally.
15
+ *
16
+ * @example
17
+ * ["2026.10.1", "2026.9.4"].sort(compare); // ["2026.9.4", "2026.10.1"]
18
+ * @example
19
+ * compare("1.0.0-rc.1", "1.0.0"); // negative, a prerelease preceding its release
20
+ * @example
21
+ * ["1.0.0", "latest"].sort(compare); // ["latest", "1.0.0"]
22
+ */
23
+ export declare function compare(a: string, b: string): number;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Total SemVer 2.0.0 ordering over version strings, so a list read from a
3
+ * registry or a tag listing sorts in one call even when some entry in it turns
4
+ * out to be something other than a version.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import { isFailure } from "@sdxc/result";
10
+ import { parse } from "./parse.js";
11
+ import { precedence } from "./precedence.js";
12
+ /**
13
+ * Order two version strings by SemVer 2.0.0 precedence, ready to hand to
14
+ * `Array.prototype.sort`. Text that is not a version ranks below every version
15
+ * and ties with other such text, which keeps the ordering total.
16
+ *
17
+ * @returns A negative number when `a` comes first, zero when the two rank equally.
18
+ *
19
+ * @example
20
+ * ["2026.10.1", "2026.9.4"].sort(compare); // ["2026.9.4", "2026.10.1"]
21
+ * @example
22
+ * compare("1.0.0-rc.1", "1.0.0"); // negative, a prerelease preceding its release
23
+ * @example
24
+ * ["1.0.0", "latest"].sort(compare); // ["latest", "1.0.0"]
25
+ */
26
+ export function compare(a, b) {
27
+ let left = parse(a);
28
+ let right = parse(b);
29
+ if (isFailure(left))
30
+ return isFailure(right) ? 0 : -1;
31
+ if (isFailure(right))
32
+ return 1;
33
+ return precedence(left.data, right.data);
34
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Public surface of the semver package: the version shape and the comparison
3
+ * union, strict SemVer 2.0.0 reading with its error, the total precedence
4
+ * ordering, and the eight comparisons one version can be tested against another.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ export type { SemVer, SemVerComparison } from "./types.js";
10
+ export { compare } from "./compare.js";
11
+ export { InvalidSemVerError } from "./invalid-semver-error.js";
12
+ export { parse } from "./parse.js";
13
+ export { satisfies } from "./satisfies.js";
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Public surface of the semver package: the version shape and the comparison
3
+ * union, strict SemVer 2.0.0 reading with its error, the total precedence
4
+ * ordering, and the eight comparisons one version can be tested against another.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ export { compare } from "./compare.js";
10
+ export { InvalidSemVerError } from "./invalid-semver-error.js";
11
+ export { parse } from "./parse.js";
12
+ export { satisfies } from "./satisfies.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The failure value `parse()` reports for text SemVer 2.0.0 rejects. It keeps the
3
+ * offending text on the error so a version read from a registry, a tag or a user
4
+ * agent can be named in a log line.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /**
10
+ * Error describing text that fails the SemVer 2.0.0 grammar, delivered to
11
+ * callers inside a `Failure` value.
12
+ */
13
+ export declare class InvalidSemVerError extends Error {
14
+ /** The rejected text, kept verbatim for diagnostics. */
15
+ readonly text: string;
16
+ /**
17
+ * Builds an error whose message quotes the rejected text, so whitespace and
18
+ * empty strings stay visible in logs.
19
+ *
20
+ * @param text - Text that did not match the SemVer 2.0.0 grammar.
21
+ */
22
+ constructor(text: string);
23
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The failure value `parse()` reports for text SemVer 2.0.0 rejects. It keeps the
3
+ * offending text on the error so a version read from a registry, a tag or a user
4
+ * agent can be named in a log line.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /**
10
+ * Error describing text that fails the SemVer 2.0.0 grammar, delivered to
11
+ * callers inside a `Failure` value.
12
+ */
13
+ export class InvalidSemVerError extends Error {
14
+ /** The rejected text, kept verbatim for diagnostics. */
15
+ text;
16
+ /**
17
+ * Builds an error whose message quotes the rejected text, so whitespace and
18
+ * empty strings stay visible in logs.
19
+ *
20
+ * @param text - Text that did not match the SemVer 2.0.0 grammar.
21
+ */
22
+ constructor(text) {
23
+ super(`Invalid version: ${JSON.stringify(text)}`);
24
+ this.name = "InvalidSemVerError";
25
+ this.text = text;
26
+ }
27
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Strict SemVer 2.0.0 reading, the single gate every other export goes through
3
+ * so one grammar decides what counts as a version. Text arrives from registries,
4
+ * git tags and user agents, so a rejection is a `Result` failure instead of a throw.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { Result } from "@sdxc/result";
10
+ import type { SemVer } from "./types.js";
11
+ import { InvalidSemVerError } from "./invalid-semver-error.js";
12
+ /**
13
+ * Read a version string into its elements, accepting an optional leading `v` and
14
+ * dropping build metadata, which SemVer 2.0.0 excludes from precedence.
15
+ *
16
+ * @param text - Text to read, e.g. a version served by a registry.
17
+ * @returns The version's elements, or an `InvalidSemVerError` naming the rejected text.
18
+ *
19
+ * @example
20
+ * parse("v1.2.3"); // { status: "success", data: { major: 1, minor: 2, patch: 3, prerelease: [] } }
21
+ * @example
22
+ * parse("1.2"); // { status: "failure", error: InvalidSemVerError }
23
+ */
24
+ export declare function parse(text: string): Result<SemVer, InvalidSemVerError>;
package/dist/parse.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Strict SemVer 2.0.0 reading, the single gate every other export goes through
3
+ * so one grammar decides what counts as a version. Text arrives from registries,
4
+ * git tags and user agents, so a rejection is a `Result` failure instead of a throw.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import { failure, success } from "@sdxc/result";
10
+ import { InvalidSemVerError } from "./invalid-semver-error.js";
11
+ /**
12
+ * SemVer 2.0.0, with the `v` a git tag or a user agent tends to carry in front
13
+ * of it. Each core element is canonical, so a padded `01.2.3` is rejected rather
14
+ * than silently read as `1.2.3`.
15
+ */
16
+ const VERSION = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][\da-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][\da-zA-Z-]*))*))?(?:\+[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?$/;
17
+ /**
18
+ * Read a version string into its elements, accepting an optional leading `v` and
19
+ * dropping build metadata, which SemVer 2.0.0 excludes from precedence.
20
+ *
21
+ * @param text - Text to read, e.g. a version served by a registry.
22
+ * @returns The version's elements, or an `InvalidSemVerError` naming the rejected text.
23
+ *
24
+ * @example
25
+ * parse("v1.2.3"); // { status: "success", data: { major: 1, minor: 2, patch: 3, prerelease: [] } }
26
+ * @example
27
+ * parse("1.2"); // { status: "failure", error: InvalidSemVerError }
28
+ */
29
+ export function parse(text) {
30
+ let match = VERSION.exec(text);
31
+ if (match === null)
32
+ return failure(new InvalidSemVerError(text));
33
+ return success({
34
+ major: Number(match[1]),
35
+ minor: Number(match[2]),
36
+ patch: Number(match[3]),
37
+ prerelease: match[4] === undefined ? [] : match[4].split("."),
38
+ });
39
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * SemVer 2.0.0 precedence over already-parsed versions, shared by the string
3
+ * ordering and by the comparison set so both answer from one definition of which
4
+ * version comes first.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { SemVer } from "./types.js";
10
+ /**
11
+ * Order two parsed versions: the core elements first, then the prerelease, with a
12
+ * prerelease ranking below the release it leads to.
13
+ *
14
+ * @returns A negative number when `left` comes first, zero when the two rank equally.
15
+ */
16
+ export declare function precedence(left: SemVer, right: SemVer): number;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * SemVer 2.0.0 precedence over already-parsed versions, shared by the string
3
+ * ordering and by the comparison set so both answer from one definition of which
4
+ * version comes first.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /** A whole numeric identifier, which sorts by value where a textual one sorts by character. */
10
+ const NUMERIC = /^\d+$/;
11
+ /**
12
+ * Order two parsed versions: the core elements first, then the prerelease, with a
13
+ * prerelease ranking below the release it leads to.
14
+ *
15
+ * @returns A negative number when `left` comes first, zero when the two rank equally.
16
+ */
17
+ export function precedence(left, right) {
18
+ if (left.major !== right.major)
19
+ return left.major - right.major;
20
+ if (left.minor !== right.minor)
21
+ return left.minor - right.minor;
22
+ if (left.patch !== right.patch)
23
+ return left.patch - right.patch;
24
+ if (left.prerelease.length === 0 && right.prerelease.length === 0)
25
+ return 0;
26
+ if (left.prerelease.length === 0)
27
+ return 1;
28
+ if (right.prerelease.length === 0)
29
+ return -1;
30
+ return comparePrerelease(left.prerelease, right.prerelease);
31
+ }
32
+ /**
33
+ * Compare prerelease identifiers pairwise: numerically where both are numeric, by
34
+ * character otherwise, with a numeric identifier ranking below a textual one. A
35
+ * list that runs out first ranks below the one that continues.
36
+ */
37
+ function comparePrerelease(left, right) {
38
+ for (let index = 0; index < Math.max(left.length, right.length); index++) {
39
+ let one = left[index];
40
+ let other = right[index];
41
+ if (one === undefined)
42
+ return -1;
43
+ if (other === undefined)
44
+ return 1;
45
+ if (one === other)
46
+ continue;
47
+ if (NUMERIC.test(one) && NUMERIC.test(other))
48
+ return Number(one) - Number(other);
49
+ if (NUMERIC.test(one))
50
+ return -1;
51
+ if (NUMERIC.test(other))
52
+ return 1;
53
+ return one < other ? -1 : 1;
54
+ }
55
+ return 0;
56
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The eight comparisons one version can be asked to stand in against another,
3
+ * covering what a rollout rule or a compatibility check needs without the range
4
+ * grammar and its parser.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { SemVerComparison } from "./types.js";
10
+ /**
11
+ * Answer whether `value` stands in the named relation to `against`. A prerelease
12
+ * takes part by precedence alone, so `1.2.4-rc.1` satisfies `^ 1.2.3`. Either
13
+ * side that is not a version answers `false`, matching nothing rather than failing.
14
+ *
15
+ * @param value - The version being tested, e.g. one read from a request.
16
+ * @param comparison - Which relation must hold.
17
+ * @param against - The version the rule was written against.
18
+ *
19
+ * @example
20
+ * satisfies("1.4.0", "~", "1.4.2"); // false
21
+ * @example
22
+ * satisfies("1.9.0", "^", "1.4.2"); // true
23
+ * @example
24
+ * satisfies("nightly", ">", "1.0.0"); // false
25
+ */
26
+ export declare function satisfies(value: string, comparison: SemVerComparison, against: string): boolean;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The eight comparisons one version can be asked to stand in against another,
3
+ * covering what a rollout rule or a compatibility check needs without the range
4
+ * grammar and its parser.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import { isFailure } from "@sdxc/result";
10
+ import { parse } from "./parse.js";
11
+ import { precedence } from "./precedence.js";
12
+ /**
13
+ * Answer whether `value` stands in the named relation to `against`. A prerelease
14
+ * takes part by precedence alone, so `1.2.4-rc.1` satisfies `^ 1.2.3`. Either
15
+ * side that is not a version answers `false`, matching nothing rather than failing.
16
+ *
17
+ * @param value - The version being tested, e.g. one read from a request.
18
+ * @param comparison - Which relation must hold.
19
+ * @param against - The version the rule was written against.
20
+ *
21
+ * @example
22
+ * satisfies("1.4.0", "~", "1.4.2"); // false
23
+ * @example
24
+ * satisfies("1.9.0", "^", "1.4.2"); // true
25
+ * @example
26
+ * satisfies("nightly", ">", "1.0.0"); // false
27
+ */
28
+ export function satisfies(value, comparison, against) {
29
+ let left = parse(value);
30
+ let right = parse(against);
31
+ if (isFailure(left) || isFailure(right))
32
+ return false;
33
+ let order = precedence(left.data, right.data);
34
+ switch (comparison) {
35
+ case "=":
36
+ return order === 0;
37
+ case "!=":
38
+ return order !== 0;
39
+ case "<":
40
+ return order < 0;
41
+ case "<=":
42
+ return order <= 0;
43
+ case ">":
44
+ return order > 0;
45
+ case ">=":
46
+ return order >= 0;
47
+ case "~":
48
+ return order >= 0 && sharesMinor(left.data, right.data);
49
+ case "^":
50
+ return order >= 0 && sharesLeadingElement(left.data, right.data);
51
+ }
52
+ }
53
+ /** Holds when `left` sits in the same minor series `right` names, which is the width of a `~`. */
54
+ function sharesMinor(left, right) {
55
+ return left.major === right.major && left.minor === right.minor;
56
+ }
57
+ /**
58
+ * Holds when `left` keeps the element `right` stakes its compatibility on — the
59
+ * major, or for a `0.x` version the minor, or for a `0.0.x` version the patch —
60
+ * which is what makes `^0.2.3` exclude `0.3.0`.
61
+ */
62
+ function sharesLeadingElement(left, right) {
63
+ if (right.major !== 0)
64
+ return left.major === right.major;
65
+ if (right.minor !== 0)
66
+ return left.major === 0 && left.minor === right.minor;
67
+ return left.major === 0 && left.minor === 0 && left.patch === right.patch;
68
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The shapes the package speaks in: a version taken apart for precedence, and
3
+ * the closed set of comparisons one version can be asked to stand in against
4
+ * another.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ /**
10
+ * A version taken apart for precedence. Build metadata carries no precedence, so
11
+ * a parsed version keeps none of it and two versions differing only in build
12
+ * metadata rank equally.
13
+ */
14
+ export interface SemVer {
15
+ major: number;
16
+ minor: number;
17
+ patch: number;
18
+ /** The dot-separated identifiers after the `-`, empty for a release. */
19
+ prerelease: string[];
20
+ }
21
+ /**
22
+ * The comparisons available without range syntax, small enough that an editor
23
+ * can render it as a list: `~` holds within one minor, `^` within the left-most
24
+ * non-zero element.
25
+ */
26
+ export type SemVerComparison = "=" | "!=" | "<" | "<=" | ">" | ">=" | "~" | "^";
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The shapes the package speaks in: a version taken apart for precedence, and
3
+ * the closed set of comparisons one version can be asked to stand in against
4
+ * another.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@sdxc/semver",
3
+ "version": "0.0.0-pre.1",
4
+ "description": "SemVer 2.0.0 parsing, precedence ordering and range-free version comparisons",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
10
+ "dependencies": {
11
+ "@sdxc/result": "2026.9.11"
12
+ },
13
+ "gitHead": "d56f75a79171aab3be101d8fa624d51972cd6028",
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/sergiodxa/monorepo.git",
20
+ "directory": "packages/semver"
21
+ }
22
+ }