@verifyhash/semver-lite 0.1.0
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 +21 -0
- package/README.md +177 -0
- package/index.js +486 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 <OWNER-NAME PLACEHOLDER>
|
|
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,177 @@
|
|
|
1
|
+
# semver-lite
|
|
2
|
+
|
|
3
|
+
<!-- publish-prep -->
|
|
4
|
+
> **TODO (owner): pick the final npm name/scope before publishing.**
|
|
5
|
+
> The npm name is **not** finalized. `package.json` uses the placeholder
|
|
6
|
+
> `PLACEHOLDER-semver-lite` — replace it with the real published name/scope
|
|
7
|
+
> before any `npm publish`. Graduation to a package registry is a
|
|
8
|
+
> **needs-human** decision, not something this project does on its own.
|
|
9
|
+
|
|
10
|
+
A tiny, **zero-dependency, zero-network** Node.js library for **Semantic
|
|
11
|
+
Versioning 2.0.0**: parse a version string, compare two versions by the exact
|
|
12
|
+
SemVer precedence rules (including the fiddly pre-release ordering), and test
|
|
13
|
+
whether a version satisfies a range using a **documented, practical subset** of
|
|
14
|
+
npm-style range syntax.
|
|
15
|
+
|
|
16
|
+
It is a single CommonJS file (`index.js`) with no dependencies, no install step,
|
|
17
|
+
and no I/O. Drop it into any project.
|
|
18
|
+
|
|
19
|
+
Spec reference: <https://semver.org/spec/v2.0.0.html>
|
|
20
|
+
|
|
21
|
+
## Who it's for
|
|
22
|
+
|
|
23
|
+
JavaScript/Node developers who need dependency- or version-logic — "is `1.4.2`
|
|
24
|
+
inside `^1.2.0`?", "which of these tags is newest?", "does this pre-release sort
|
|
25
|
+
before the release?" — **without** pulling in the full [`semver`](https://www.npmjs.com/package/semver)
|
|
26
|
+
package and its transitive footprint. Good for build scripts, CLIs, plugin
|
|
27
|
+
loaders, changelog tooling, and anywhere you want auditable, single-file version
|
|
28
|
+
math.
|
|
29
|
+
|
|
30
|
+
If you need the *complete* node-semver range grammar (coercion, `x` in the
|
|
31
|
+
middle of comparators, full prerelease-range semantics, `loose` mode, etc.), use
|
|
32
|
+
the real `semver` package. This library deliberately covers the common 95%.
|
|
33
|
+
|
|
34
|
+
## Install / use
|
|
35
|
+
|
|
36
|
+
No install — copy `index.js`, or `require` it directly:
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
const semver = require('./index.js');
|
|
40
|
+
|
|
41
|
+
semver.parse('1.2.3-rc.1+build.5');
|
|
42
|
+
// { major: 1, minor: 2, patch: 3,
|
|
43
|
+
// prerelease: ['rc', 1], build: ['build', '5'],
|
|
44
|
+
// version: '1.2.3-rc.1' }
|
|
45
|
+
|
|
46
|
+
semver.gt('1.0.0', '1.0.0-rc.1'); // true (release > prerelease)
|
|
47
|
+
semver.lt('1.0.0-alpha.1', '1.0.0-alpha.beta'); // true (numeric < alphanumeric)
|
|
48
|
+
semver.eq('1.0.0+build.1', '1.0.0+build.9'); // true (build metadata ignored)
|
|
49
|
+
|
|
50
|
+
semver.satisfies('1.2.5', '^1.2.0'); // true
|
|
51
|
+
semver.satisfies('2.0.0', '^1.2.0'); // false
|
|
52
|
+
semver.sort(['1.0.0', '1.0.0-rc.1', '1.0.0-alpha']);
|
|
53
|
+
// ['1.0.0-alpha', '1.0.0-rc.1', '1.0.0']
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
### `parse(v) → object | null`
|
|
59
|
+
|
|
60
|
+
Parses a SemVer 2.0.0 string. **Returns `null` on invalid input — it does not
|
|
61
|
+
throw.** (Comparison functions below *do* throw on invalid input; see each.)
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
{
|
|
67
|
+
major: Number,
|
|
68
|
+
minor: Number,
|
|
69
|
+
patch: Number,
|
|
70
|
+
prerelease: Array<string|number>, // numeric identifiers are Numbers
|
|
71
|
+
build: Array<string>, // always strings; ignored for precedence
|
|
72
|
+
version: String // "major.minor.patch[-prerelease]" (no build)
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Rejected (returns `null`): missing components (`1`, `1.2`), a leading `v`
|
|
77
|
+
(`v1.2.3`), leading zeros in any numeric field (`01.2.3`, `1.2.3-01`), empty
|
|
78
|
+
identifiers (`1.2.3-`, `1.0.0-a..b`), and non-strings. Surrounding whitespace is
|
|
79
|
+
trimmed.
|
|
80
|
+
|
|
81
|
+
### `valid(v) → boolean`
|
|
82
|
+
|
|
83
|
+
`true` iff `parse(v)` succeeds.
|
|
84
|
+
|
|
85
|
+
### `compare(a, b) → -1 | 0 | 1`
|
|
86
|
+
|
|
87
|
+
Compares by SemVer precedence. **Throws `TypeError` if either argument is not a
|
|
88
|
+
valid version.** Precedence rules implemented (SemVer §11):
|
|
89
|
+
|
|
90
|
+
1. Compare `major`, then `minor`, then `patch` numerically.
|
|
91
|
+
2. A version **with** a pre-release has **lower** precedence than the same
|
|
92
|
+
version **without** one (`1.0.0-rc.1 < 1.0.0`).
|
|
93
|
+
3. Pre-release identifiers are compared left-to-right:
|
|
94
|
+
- numeric identifiers are compared numerically (`beta.2 < beta.11`);
|
|
95
|
+
- a numeric identifier always has **lower** precedence than an alphanumeric
|
|
96
|
+
one (`alpha.1 < alpha.beta`);
|
|
97
|
+
- alphanumeric identifiers are compared by ASCII sort order;
|
|
98
|
+
- if all shared identifiers are equal, the version with **more** fields has
|
|
99
|
+
higher precedence (`alpha < alpha.1`).
|
|
100
|
+
4. **Build metadata is ignored** entirely (`1.0.0+a` == `1.0.0+b`).
|
|
101
|
+
|
|
102
|
+
### `gt`, `lt`, `gte`, `lte`, `eq`, `neq` `(a, b) → boolean`
|
|
103
|
+
|
|
104
|
+
Thin wrappers over `compare`; same throwing behaviour.
|
|
105
|
+
|
|
106
|
+
### `sort(list) → array`
|
|
107
|
+
|
|
108
|
+
Returns a **new** array sorted ascending by precedence.
|
|
109
|
+
|
|
110
|
+
### `satisfies(version, range) → boolean`
|
|
111
|
+
|
|
112
|
+
Returns `true` iff `version` matches `range`. **Never throws for a bad
|
|
113
|
+
`version`** (returns `false`); a *malformed range token* throws `TypeError`.
|
|
114
|
+
|
|
115
|
+
## Supported range syntax
|
|
116
|
+
|
|
117
|
+
| Form | Example | Expands to |
|
|
118
|
+
| --------------- | -------------------- | ----------------------------- |
|
|
119
|
+
| exact | `1.2.3` | `=1.2.3` |
|
|
120
|
+
| equals | `=1.2.3` | `=1.2.3` |
|
|
121
|
+
| comparators | `>1.2.3` `>=1.2.3` `<2.0.0` `<=1.2.3` | as written |
|
|
122
|
+
| caret | `^1.2.3` | `>=1.2.3 <2.0.0` |
|
|
123
|
+
| caret (0.x) | `^0.2.3` | `>=0.2.3 <0.3.0` |
|
|
124
|
+
| caret (0.0.x) | `^0.0.3` | `>=0.0.3 <0.0.4` |
|
|
125
|
+
| tilde | `~1.2.3` | `>=1.2.3 <1.3.0` |
|
|
126
|
+
| tilde (partial) | `~1.2` / `~1` | `>=1.2.0 <1.3.0` / `>=1.0.0 <2.0.0` |
|
|
127
|
+
| x-range | `1.2.x` `1.x` `1.2.*` `*` `1` | e.g. `1.2.x` → `>=1.2.0 <1.3.0` |
|
|
128
|
+
| hyphen range | `1.2.3 - 2.3.4` | `>=1.2.3 <=2.3.4` |
|
|
129
|
+
| AND (space) | `>=1.2.0 <2.0.0` | intersection |
|
|
130
|
+
| OR (`\|\|`) | `^1.0.0 \|\| ^2.0.0` | union |
|
|
131
|
+
|
|
132
|
+
Partial operands are handled the node-semver way: `>1` → `>=2.0.0`,
|
|
133
|
+
`<=1.2` → `<1.3.0`, and a partial upper hyphen bound like `1.2.3 - 2.3` becomes
|
|
134
|
+
`<2.4.0`.
|
|
135
|
+
|
|
136
|
+
### Pre-release handling in ranges
|
|
137
|
+
|
|
138
|
+
A version carrying a pre-release tag (e.g. `1.2.3-beta.2`) satisfies a range
|
|
139
|
+
**only if** some comparator in the matched set names the *same*
|
|
140
|
+
`major.minor.patch` tuple **and itself has a pre-release**. So
|
|
141
|
+
`1.2.3-beta.2` satisfies `>=1.2.3-beta.1 <2.0.0` but **not** `^1.0.0`. This
|
|
142
|
+
mirrors npm's default (non-`includePrerelease`) behaviour.
|
|
143
|
+
|
|
144
|
+
## NOT supported (use the full `semver` package instead)
|
|
145
|
+
|
|
146
|
+
- `includePrerelease` mode and advanced pre-release range edge cases beyond the
|
|
147
|
+
single documented rule above.
|
|
148
|
+
- Version **coercion** / `loose` parsing (e.g. `v1`, `1.2.3.4`, `=v1.2`).
|
|
149
|
+
- `x`/`*` in the *middle* of a version (`1.x.3`) — only trailing wildcards.
|
|
150
|
+
- Comparators glued to carets/tildes in one token, or ranges relying on
|
|
151
|
+
operator precedence quirks.
|
|
152
|
+
- Any I/O, registry lookups, or `dist-tags`.
|
|
153
|
+
|
|
154
|
+
If your input might use those, reach for [`semver`](https://www.npmjs.com/package/semver).
|
|
155
|
+
|
|
156
|
+
## Running the tests
|
|
157
|
+
|
|
158
|
+
One command, no framework, only Node's built-in `assert`:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
node test/index.test.js
|
|
162
|
+
# or
|
|
163
|
+
npm test
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The suite covers: `parse` (valid, prerelease/build, leading-zero rejection,
|
|
167
|
+
non-string input), the full **SemVer spec precedence chain**
|
|
168
|
+
(`1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 <
|
|
169
|
+
1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0`), **build-metadata-ignored** equality,
|
|
170
|
+
invalid-input handling, and `satisfies` with **both true and false** cases for
|
|
171
|
+
every supported range form (exact, `^`, `~`, x-range, comparators, hyphen, and
|
|
172
|
+
`||`).
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT — see `LICENSE`. (Owner: replace the copyright placeholder before
|
|
177
|
+
publishing.)
|
package/index.js
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* semver-lite — a zero-dependency SemVer 2.0.0 parser, comparator, and a
|
|
5
|
+
* practical subset of npm-style range matching.
|
|
6
|
+
*
|
|
7
|
+
* No network, no I/O, no dependencies. Pure functions over strings.
|
|
8
|
+
* Spec reference: https://semver.org/spec/v2.0.0.html
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// parse
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
// SemVer 2.0.0 core grammar.
|
|
16
|
+
// <major>.<minor>.<patch>[-<prerelease>][+<build>]
|
|
17
|
+
// - numeric identifiers: 0 or a non-zero digit followed by digits (no leading zero)
|
|
18
|
+
// - prerelease identifiers: alphanumerics + hyphen; numeric ones must not have
|
|
19
|
+
// leading zeros; identifiers must not be empty
|
|
20
|
+
// - build identifiers: alphanumerics + hyphen; leading zeros allowed; must not
|
|
21
|
+
// be empty
|
|
22
|
+
const NUM = '0|[1-9]\\d*';
|
|
23
|
+
const PRE_ID = '(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)';
|
|
24
|
+
const BUILD_ID = '[0-9a-zA-Z-]+';
|
|
25
|
+
const SEMVER_RE = new RegExp(
|
|
26
|
+
'^' +
|
|
27
|
+
`(${NUM})\\.(${NUM})\\.(${NUM})` +
|
|
28
|
+
`(?:-(${PRE_ID}(?:\\.${PRE_ID})*))?` +
|
|
29
|
+
`(?:\\+(${BUILD_ID}(?:\\.${BUILD_ID})*))?` +
|
|
30
|
+
'$'
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a SemVer 2.0.0 string.
|
|
35
|
+
*
|
|
36
|
+
* Returns `null` on invalid input (it does NOT throw). Callers who want a
|
|
37
|
+
* throwing variant can do `parse(v) || (() => { throw ... })()`.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} v
|
|
40
|
+
* @returns {{major:number,minor:number,patch:number,prerelease:Array<string|number>,build:string[],version:string}|null}
|
|
41
|
+
*/
|
|
42
|
+
function parse(v) {
|
|
43
|
+
if (typeof v !== 'string') return null;
|
|
44
|
+
const str = v.trim();
|
|
45
|
+
const m = SEMVER_RE.exec(str);
|
|
46
|
+
if (!m) return null;
|
|
47
|
+
|
|
48
|
+
const [, major, minor, patch, pre, build] = m;
|
|
49
|
+
const prerelease =
|
|
50
|
+
pre === undefined
|
|
51
|
+
? []
|
|
52
|
+
: pre.split('.').map((id) => (/^\d+$/.test(id) ? Number(id) : id));
|
|
53
|
+
const buildMeta = build === undefined ? [] : build.split('.');
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
major: Number(major),
|
|
57
|
+
minor: Number(minor),
|
|
58
|
+
patch: Number(patch),
|
|
59
|
+
prerelease,
|
|
60
|
+
build: buildMeta,
|
|
61
|
+
version: `${major}.${minor}.${patch}${pre ? '-' + pre : ''}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Internal: parse or throw a TypeError. */
|
|
66
|
+
function mustParse(v) {
|
|
67
|
+
const p = parse(v);
|
|
68
|
+
if (!p) throw new TypeError(`Invalid SemVer version: ${JSON.stringify(v)}`);
|
|
69
|
+
return p;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Whether a string is a valid SemVer 2.0.0 version. */
|
|
73
|
+
function valid(v) {
|
|
74
|
+
return parse(v) !== null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// compare / precedence
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/** Compare two numbers, returning -1 / 0 / 1. */
|
|
82
|
+
function cmpNum(a, b) {
|
|
83
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Compare prerelease identifier lists per SemVer §11.4.
|
|
88
|
+
* Precondition: called only when both major.minor.patch are equal.
|
|
89
|
+
*/
|
|
90
|
+
function comparePrerelease(a, b) {
|
|
91
|
+
// A version WITHOUT prerelease has HIGHER precedence than one WITH.
|
|
92
|
+
if (a.length === 0 && b.length === 0) return 0;
|
|
93
|
+
if (a.length === 0) return 1; // a is release, b is prerelease -> a > b
|
|
94
|
+
if (b.length === 0) return -1;
|
|
95
|
+
|
|
96
|
+
const len = Math.min(a.length, b.length);
|
|
97
|
+
for (let i = 0; i < len; i++) {
|
|
98
|
+
const ai = a[i];
|
|
99
|
+
const bi = b[i];
|
|
100
|
+
const aNum = typeof ai === 'number';
|
|
101
|
+
const bNum = typeof bi === 'number';
|
|
102
|
+
if (aNum && bNum) {
|
|
103
|
+
const c = cmpNum(ai, bi);
|
|
104
|
+
if (c !== 0) return c;
|
|
105
|
+
} else if (aNum && !bNum) {
|
|
106
|
+
// numeric identifiers always have LOWER precedence than alphanumeric
|
|
107
|
+
return -1;
|
|
108
|
+
} else if (!aNum && bNum) {
|
|
109
|
+
return 1;
|
|
110
|
+
} else {
|
|
111
|
+
// both alphanumeric: ASCII lexical order
|
|
112
|
+
if (ai < bi) return -1;
|
|
113
|
+
if (ai > bi) return 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// All shared identifiers equal: the larger set has higher precedence.
|
|
117
|
+
return cmpNum(a.length, b.length);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Compare two versions by SemVer precedence. Build metadata is IGNORED.
|
|
122
|
+
* @returns {number} -1 if a<b, 0 if equal precedence, 1 if a>b
|
|
123
|
+
*/
|
|
124
|
+
function compare(a, b) {
|
|
125
|
+
const pa = mustParse(a);
|
|
126
|
+
const pb = mustParse(b);
|
|
127
|
+
return (
|
|
128
|
+
cmpNum(pa.major, pb.major) ||
|
|
129
|
+
cmpNum(pa.minor, pb.minor) ||
|
|
130
|
+
cmpNum(pa.patch, pb.patch) ||
|
|
131
|
+
comparePrerelease(pa.prerelease, pb.prerelease)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function gt(a, b) {
|
|
136
|
+
return compare(a, b) > 0;
|
|
137
|
+
}
|
|
138
|
+
function lt(a, b) {
|
|
139
|
+
return compare(a, b) < 0;
|
|
140
|
+
}
|
|
141
|
+
function gte(a, b) {
|
|
142
|
+
return compare(a, b) >= 0;
|
|
143
|
+
}
|
|
144
|
+
function lte(a, b) {
|
|
145
|
+
return compare(a, b) <= 0;
|
|
146
|
+
}
|
|
147
|
+
function eq(a, b) {
|
|
148
|
+
return compare(a, b) === 0;
|
|
149
|
+
}
|
|
150
|
+
function neq(a, b) {
|
|
151
|
+
return compare(a, b) !== 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Sort a list of versions ascending by precedence (returns a new array). */
|
|
155
|
+
function sort(list) {
|
|
156
|
+
return [...list].sort(compare);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// satisfies (range matching)
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
//
|
|
163
|
+
// Supported (documented) subset — see README:
|
|
164
|
+
// - exact: 1.2.3 (also 1.2.3-alpha)
|
|
165
|
+
// - =: =1.2.3
|
|
166
|
+
// - comparators: >1.2.3 >=1.2.3 <2.0.0 <=1.2.3
|
|
167
|
+
// - caret: ^1.2.3 ^0.2.3 ^0.0.3 (and ^1.2.x style partials)
|
|
168
|
+
// - tilde: ~1.2.3 ~1.2 ~1
|
|
169
|
+
// - x-ranges: 1.2.x 1.x 1.2.* * 1
|
|
170
|
+
// - hyphen: 1.2.3 - 2.3.4
|
|
171
|
+
// - OR unions: A || B
|
|
172
|
+
// - whitespace-joined AND: ">=1.2.0 <2.0.0"
|
|
173
|
+
//
|
|
174
|
+
// A prerelease version (e.g. 1.2.3-beta) only satisfies a comparator set when
|
|
175
|
+
// at least one comparator in that set names the SAME major.minor.patch tuple
|
|
176
|
+
// with a prerelease — matching npm's behaviour, and documented as such.
|
|
177
|
+
|
|
178
|
+
const XR = '[xX*]';
|
|
179
|
+
// A single partial version like "1", "1.2", "1.2.3", "1.2.x", "*", with
|
|
180
|
+
// optional prerelease/build on a full triple.
|
|
181
|
+
const PARTIAL_RE = new RegExp(
|
|
182
|
+
'^[v=\\s]*' +
|
|
183
|
+
`(${NUM}|${XR})` +
|
|
184
|
+
`(?:\\.(${NUM}|${XR}))?` +
|
|
185
|
+
`(?:\\.(${NUM}|${XR}))?` +
|
|
186
|
+
`(?:-(${PRE_ID}(?:\\.${PRE_ID})*))?` +
|
|
187
|
+
`(?:\\+(${BUILD_ID}(?:\\.${BUILD_ID})*))?` +
|
|
188
|
+
'$'
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
function isX(part) {
|
|
192
|
+
return part === undefined || part === '*' || part === 'x' || part === 'X';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Parse a partial version into {major,minor,patch,pre} with nulls for x/missing. */
|
|
196
|
+
function parsePartial(str) {
|
|
197
|
+
const m = PARTIAL_RE.exec(str.trim());
|
|
198
|
+
if (!m) return null;
|
|
199
|
+
const [, M, mi, p, pre] = m;
|
|
200
|
+
return {
|
|
201
|
+
major: isX(M) ? null : Number(M),
|
|
202
|
+
minor: isX(mi) ? null : Number(mi),
|
|
203
|
+
patch: isX(p) ? null : Number(p),
|
|
204
|
+
pre: pre === undefined ? null : pre,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Build a comparator object {op, version(parsed)}. */
|
|
209
|
+
function comparator(op, versionStr) {
|
|
210
|
+
return { op, v: mustParse(versionStr) };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Expand a caret partial into a comparator set.
|
|
215
|
+
* ^1.2.3 -> >=1.2.3 <2.0.0
|
|
216
|
+
* ^0.2.3 -> >=0.2.3 <0.3.0
|
|
217
|
+
* ^0.0.3 -> >=0.0.3 <0.0.4
|
|
218
|
+
* ^1.2 -> >=1.2.0 <2.0.0
|
|
219
|
+
* ^0 -> >=0.0.0 <1.0.0
|
|
220
|
+
*/
|
|
221
|
+
function expandCaret(part, preStr) {
|
|
222
|
+
if (part.major === null) return [comparator('>=', '0.0.0')]; // ^* -> any
|
|
223
|
+
const min = `${part.major}.${part.minor || 0}.${part.patch || 0}${
|
|
224
|
+
preStr ? '-' + preStr : ''
|
|
225
|
+
}`;
|
|
226
|
+
let upper;
|
|
227
|
+
if (part.major > 0 || part.minor === null) {
|
|
228
|
+
upper = `${part.major + 1}.0.0`;
|
|
229
|
+
} else if (part.minor > 0 || part.patch === null) {
|
|
230
|
+
upper = `0.${part.minor + 1}.0`;
|
|
231
|
+
} else {
|
|
232
|
+
upper = `0.0.${part.patch + 1}`;
|
|
233
|
+
}
|
|
234
|
+
return [comparator('>=', min), comparator('<', upper)];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Expand a tilde partial into a comparator set.
|
|
239
|
+
* ~1.2.3 -> >=1.2.3 <1.3.0
|
|
240
|
+
* ~1.2 -> >=1.2.0 <1.3.0
|
|
241
|
+
* ~1 -> >=1.0.0 <2.0.0
|
|
242
|
+
*/
|
|
243
|
+
function expandTilde(part, preStr) {
|
|
244
|
+
if (part.major === null) return [comparator('>=', '0.0.0')];
|
|
245
|
+
const min = `${part.major}.${part.minor || 0}.${part.patch || 0}${
|
|
246
|
+
preStr ? '-' + preStr : ''
|
|
247
|
+
}`;
|
|
248
|
+
let upper;
|
|
249
|
+
if (part.minor === null) {
|
|
250
|
+
upper = `${part.major + 1}.0.0`;
|
|
251
|
+
} else {
|
|
252
|
+
upper = `${part.major}.${part.minor + 1}.0`;
|
|
253
|
+
}
|
|
254
|
+
return [comparator('>=', min), comparator('<', upper)];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Expand an x-range / bare partial into a comparator set.
|
|
259
|
+
* * -> >=0.0.0
|
|
260
|
+
* 1 -> >=1.0.0 <2.0.0
|
|
261
|
+
* 1.2 -> >=1.2.0 <1.3.0
|
|
262
|
+
* 1.2.3 -> =1.2.3 (exact; handled as >= & <= same)
|
|
263
|
+
*/
|
|
264
|
+
function expandXRange(part) {
|
|
265
|
+
if (part.major === null) return [comparator('>=', '0.0.0')];
|
|
266
|
+
if (part.minor === null) {
|
|
267
|
+
return [
|
|
268
|
+
comparator('>=', `${part.major}.0.0`),
|
|
269
|
+
comparator('<', `${part.major + 1}.0.0`),
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
if (part.patch === null) {
|
|
273
|
+
return [
|
|
274
|
+
comparator('>=', `${part.major}.${part.minor}.0`),
|
|
275
|
+
comparator('<', `${part.major}.${part.minor + 1}.0`),
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
// Fully specified: exact match (respecting any prerelease).
|
|
279
|
+
const exact = `${part.major}.${part.minor}.${part.patch}${
|
|
280
|
+
part.pre ? '-' + part.pre : ''
|
|
281
|
+
}`;
|
|
282
|
+
return [comparator('=', exact)];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Expand a comparator token like ">=1.2", ">1", "<2.0.0" where the operand may
|
|
287
|
+
* be partial. npm semantics: for inequality ops, x/missing parts default in a
|
|
288
|
+
* way that keeps the bound meaningful.
|
|
289
|
+
*/
|
|
290
|
+
function expandComparatorToken(op, part) {
|
|
291
|
+
// For >, >=, <, <= with partials, fill missing with 0 for the lower-ish
|
|
292
|
+
// interpretation used by node-semver.
|
|
293
|
+
const major = part.major === null ? 0 : part.major;
|
|
294
|
+
const minor = part.minor === null ? 0 : part.minor;
|
|
295
|
+
const patch = part.patch === null ? 0 : part.patch;
|
|
296
|
+
const preStr = part.pre ? '-' + part.pre : '';
|
|
297
|
+
|
|
298
|
+
// If everything is x (e.g. ">=*"), treat as >=0.0.0 or an always-open bound.
|
|
299
|
+
if (part.major === null) {
|
|
300
|
+
if (op === '<' || op === '<=') return [comparator('<', '0.0.0')]; // <* matches nothing
|
|
301
|
+
return [comparator('>=', '0.0.0')];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Handle partials for < and > with npm's rounding.
|
|
305
|
+
if (op === '>' && part.minor === null) {
|
|
306
|
+
// >1 -> >=2.0.0
|
|
307
|
+
return [comparator('>=', `${major + 1}.0.0`)];
|
|
308
|
+
}
|
|
309
|
+
if (op === '>' && part.patch === null) {
|
|
310
|
+
// >1.2 -> >=1.3.0
|
|
311
|
+
return [comparator('>=', `${major}.${minor + 1}.0`)];
|
|
312
|
+
}
|
|
313
|
+
if (op === '<=' && part.minor === null) {
|
|
314
|
+
// <=1 -> <2.0.0
|
|
315
|
+
return [comparator('<', `${major + 1}.0.0`)];
|
|
316
|
+
}
|
|
317
|
+
if (op === '<=' && part.patch === null) {
|
|
318
|
+
// <=1.2 -> <1.3.0
|
|
319
|
+
return [comparator('<', `${major}.${minor + 1}.0`)];
|
|
320
|
+
}
|
|
321
|
+
return [comparator(op, `${major}.${minor}.${patch}${preStr}`)];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Parse one whitespace-separated space of a range into a comparator set. */
|
|
325
|
+
function parseComparatorSet(setStr) {
|
|
326
|
+
const trimmed = setStr.trim();
|
|
327
|
+
if (trimmed === '') return [comparator('>=', '0.0.0')]; // empty == "*"
|
|
328
|
+
|
|
329
|
+
// Hyphen range: "1.2.3 - 2.3.4"
|
|
330
|
+
const hyphen = /^\s*(.+?)\s+-\s+(.+?)\s*$/.exec(trimmed);
|
|
331
|
+
if (hyphen) {
|
|
332
|
+
return expandHyphen(hyphen[1], hyphen[2]);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Split on whitespace into individual tokens (AND).
|
|
336
|
+
const tokens = trimmed.split(/\s+/);
|
|
337
|
+
const comps = [];
|
|
338
|
+
for (const tok of tokens) {
|
|
339
|
+
comps.push(...parseToken(tok));
|
|
340
|
+
}
|
|
341
|
+
return comps;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function expandHyphen(loStr, hiStr) {
|
|
345
|
+
const lo = parsePartial(loStr);
|
|
346
|
+
const hi = parsePartial(hiStr);
|
|
347
|
+
if (!lo || !hi) throw new TypeError(`Invalid hyphen range: ${loStr} - ${hiStr}`);
|
|
348
|
+
const comps = [];
|
|
349
|
+
|
|
350
|
+
// Lower bound: partials fill with 0.
|
|
351
|
+
const loMajor = lo.major === null ? 0 : lo.major;
|
|
352
|
+
const loMinor = lo.minor === null ? 0 : lo.minor;
|
|
353
|
+
const loPatch = lo.patch === null ? 0 : lo.patch;
|
|
354
|
+
comps.push(
|
|
355
|
+
comparator(
|
|
356
|
+
'>=',
|
|
357
|
+
`${loMajor}.${loMinor}.${loPatch}${lo.pre ? '-' + lo.pre : ''}`
|
|
358
|
+
)
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// Upper bound: partials become an exclusive next bound.
|
|
362
|
+
if (hi.major === null) {
|
|
363
|
+
// "x - *" -> no upper bound
|
|
364
|
+
} else if (hi.minor === null) {
|
|
365
|
+
comps.push(comparator('<', `${hi.major + 1}.0.0`));
|
|
366
|
+
} else if (hi.patch === null) {
|
|
367
|
+
comps.push(comparator('<', `${hi.major}.${hi.minor + 1}.0`));
|
|
368
|
+
} else {
|
|
369
|
+
comps.push(
|
|
370
|
+
comparator('<=', `${hi.major}.${hi.minor}.${hi.patch}${hi.pre ? '-' + hi.pre : ''}`)
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return comps;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Parse a single AND token (^, ~, comparator, x-range, exact). */
|
|
377
|
+
function parseToken(tok) {
|
|
378
|
+
let s = tok.trim();
|
|
379
|
+
if (s === '') return [comparator('>=', '0.0.0')];
|
|
380
|
+
|
|
381
|
+
// Caret
|
|
382
|
+
if (s[0] === '^') {
|
|
383
|
+
const part = parsePartial(s.slice(1));
|
|
384
|
+
if (!part) throw new TypeError(`Invalid caret range: ${tok}`);
|
|
385
|
+
return expandCaret(part, part.pre);
|
|
386
|
+
}
|
|
387
|
+
// Tilde
|
|
388
|
+
if (s[0] === '~') {
|
|
389
|
+
// Support "~>" as an alias of "~" (rubygems-ism sometimes seen)
|
|
390
|
+
const body = s[1] === '>' ? s.slice(2) : s.slice(1);
|
|
391
|
+
const part = parsePartial(body);
|
|
392
|
+
if (!part) throw new TypeError(`Invalid tilde range: ${tok}`);
|
|
393
|
+
return expandTilde(part, part.pre);
|
|
394
|
+
}
|
|
395
|
+
// Comparators
|
|
396
|
+
const m = /^(>=|<=|>|<|=)(.*)$/.exec(s);
|
|
397
|
+
if (m) {
|
|
398
|
+
const op = m[1];
|
|
399
|
+
const part = parsePartial(m[2]);
|
|
400
|
+
if (!part) throw new TypeError(`Invalid comparator: ${tok}`);
|
|
401
|
+
if (op === '=') {
|
|
402
|
+
// "=1.2" behaves as an x-range expansion of the partial.
|
|
403
|
+
return expandXRange(part);
|
|
404
|
+
}
|
|
405
|
+
return expandComparatorToken(op, part);
|
|
406
|
+
}
|
|
407
|
+
// Bare partial / x-range / exact
|
|
408
|
+
const part = parsePartial(s);
|
|
409
|
+
if (!part) throw new TypeError(`Invalid range token: ${tok}`);
|
|
410
|
+
return expandXRange(part);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Test a single comparator against a parsed version. */
|
|
414
|
+
function testComparator(comp, version) {
|
|
415
|
+
const c = compare(version.version, comp.v.version);
|
|
416
|
+
switch (comp.op) {
|
|
417
|
+
case '>':
|
|
418
|
+
return c > 0;
|
|
419
|
+
case '>=':
|
|
420
|
+
return c >= 0;
|
|
421
|
+
case '<':
|
|
422
|
+
return c < 0;
|
|
423
|
+
case '<=':
|
|
424
|
+
return c <= 0;
|
|
425
|
+
case '=':
|
|
426
|
+
return c === 0;
|
|
427
|
+
default:
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Does `version` satisfy `range`?
|
|
434
|
+
*
|
|
435
|
+
* @param {string} version
|
|
436
|
+
* @param {string} range
|
|
437
|
+
* @returns {boolean}
|
|
438
|
+
*/
|
|
439
|
+
function satisfies(version, range) {
|
|
440
|
+
const v = parse(version);
|
|
441
|
+
if (!v) return false;
|
|
442
|
+
if (typeof range !== 'string') return false;
|
|
443
|
+
|
|
444
|
+
const unions = range.split('||');
|
|
445
|
+
for (const union of unions) {
|
|
446
|
+
const comps = parseComparatorSet(union);
|
|
447
|
+
|
|
448
|
+
// Prerelease gating (npm semantics): a version with a prerelease tag only
|
|
449
|
+
// matches if some comparator in this set is for the SAME [major,minor,patch]
|
|
450
|
+
// AND itself carries a prerelease.
|
|
451
|
+
if (v.prerelease.length > 0) {
|
|
452
|
+
const allowed = comps.some(
|
|
453
|
+
(comp) =>
|
|
454
|
+
comp.v.prerelease.length > 0 &&
|
|
455
|
+
comp.v.major === v.major &&
|
|
456
|
+
comp.v.minor === v.minor &&
|
|
457
|
+
comp.v.patch === v.patch
|
|
458
|
+
);
|
|
459
|
+
if (!allowed) continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
let ok = true;
|
|
463
|
+
for (const comp of comps) {
|
|
464
|
+
if (!testComparator(comp, v)) {
|
|
465
|
+
ok = false;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (ok) return true;
|
|
470
|
+
}
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
module.exports = {
|
|
475
|
+
parse,
|
|
476
|
+
valid,
|
|
477
|
+
compare,
|
|
478
|
+
gt,
|
|
479
|
+
lt,
|
|
480
|
+
gte,
|
|
481
|
+
lte,
|
|
482
|
+
eq,
|
|
483
|
+
neq,
|
|
484
|
+
sort,
|
|
485
|
+
satisfies,
|
|
486
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@verifyhash/semver-lite",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-dependency SemVer 2.0.0 parse, precedence compare, and a documented practical subset of npm-style range matching for Node.js.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"semver",
|
|
7
|
+
"semver2",
|
|
8
|
+
"version",
|
|
9
|
+
"parse",
|
|
10
|
+
"compare",
|
|
11
|
+
"precedence",
|
|
12
|
+
"satisfies",
|
|
13
|
+
"range",
|
|
14
|
+
"caret",
|
|
15
|
+
"tilde"
|
|
16
|
+
],
|
|
17
|
+
"main": "index.js",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node test/index.test.js"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"files": [
|
|
23
|
+
"index.js",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/verifyhash/libs.git",
|
|
32
|
+
"directory": "semver-lite"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/verifyhash/libs/tree/main/semver-lite#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/verifyhash/libs/issues"
|
|
37
|
+
}
|
|
38
|
+
}
|