@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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -0
  3. package/index.js +486 -0
  4. 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
+ }