@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 +21 -0
- package/README.md +224 -0
- package/dist/compare.d.ts +23 -0
- package/dist/compare.js +34 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/invalid-semver-error.d.ts +23 -0
- package/dist/invalid-semver-error.js +27 -0
- package/dist/parse.d.ts +24 -0
- package/dist/parse.js +39 -0
- package/dist/precedence.d.ts +16 -0
- package/dist/precedence.js +56 -0
- package/dist/satisfies.d.ts +26 -0
- package/dist/satisfies.js +68 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.js +8 -0
- package/package.json +22 -0
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;
|
package/dist/compare.js
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/parse.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|