wyvrnpm 2.10.2 → 2.12.2
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/README.md +1914 -1860
- package/bin/{wyvrn.js → wyvrnpm.js} +66 -0
- package/cmake/cpp.cmake +9 -9
- package/cmake/functions.cmake +224 -224
- package/cmake/macros.cmake +284 -284
- package/package.json +3 -2
- package/src/auth.js +66 -66
- package/src/binary-dir.js +95 -0
- package/src/bootstrap/cookbook.js +196 -196
- package/src/bootstrap/detect.js +150 -150
- package/src/bootstrap/index.js +220 -220
- package/src/bootstrap/version.js +72 -72
- package/src/build/cache.js +344 -344
- package/src/build/clone.js +170 -170
- package/src/build/cmake.js +342 -342
- package/src/build/index.js +299 -297
- package/src/build/msvc-env.js +260 -260
- package/src/build/recipe.js +188 -188
- package/src/commands/add.js +141 -141
- package/src/commands/bootstrap.js +96 -96
- package/src/commands/build.js +482 -452
- package/src/commands/cache.js +189 -189
- package/src/commands/clean.js +80 -80
- package/src/commands/configure.js +92 -92
- package/src/commands/init.js +70 -70
- package/src/commands/install-skill.js +115 -115
- package/src/commands/install.js +730 -674
- package/src/commands/link.js +320 -320
- package/src/commands/profile.js +237 -237
- package/src/commands/publish.js +584 -555
- package/src/commands/show.js +252 -252
- package/src/commands/version.js +187 -0
- package/src/compat.js +273 -273
- package/src/conf/index.js +415 -391
- package/src/conf/namespaces.js +94 -94
- package/src/context.js +230 -230
- package/src/http-fetch.js +53 -53
- package/src/ignore.js +118 -118
- package/src/logger.js +122 -122
- package/src/options.js +303 -303
- package/src/settings-overrides.js +152 -152
- package/src/toolchain/deps.js +164 -164
- package/src/toolchain/index.js +212 -212
- package/src/toolchain/presets.js +356 -340
- package/src/toolchain/template.cmake +77 -77
- package/src/upload-built.js +265 -265
- package/src/version-range.js +301 -301
- package/src/zip-safe.js +52 -52
- package/src/zip-stream.js +126 -0
package/src/version-range.js
CHANGED
|
@@ -1,301 +1,301 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Version range parsing and matching for 4-part `major.minor.patch.build`
|
|
5
|
-
* versions.
|
|
6
|
-
*
|
|
7
|
-
* Accepts three range syntaxes in addition to the existing exact and
|
|
8
|
-
* `"latest"` strings:
|
|
9
|
-
*
|
|
10
|
-
* "^1.2.3.4" major-pinned : >= 1.2.3.4 < 2.0.0.0
|
|
11
|
-
* "~1.2.3.4" minor-pinned : >= 1.2.3.4 < 1.3.0.0
|
|
12
|
-
* ">=1.2.3.4 <2.0" explicit : one or more space-separated comparators
|
|
13
|
-
* among >=, >, <=, <, =
|
|
14
|
-
*
|
|
15
|
-
* `parseRange(s)` classifies the input; `matchesRange(v, parsed)`
|
|
16
|
-
* evaluates a candidate version; `pickHighestInRange(versions, parsed)`
|
|
17
|
-
* selects the best match; `intersectRanges(a, b)` combines two ranges
|
|
18
|
-
* (returns null when the intersection is empty).
|
|
19
|
-
*
|
|
20
|
-
* See claude/PLAN-VERSION-RANGES.md for the full design.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const VERSION_RE = /^\d+\.\d+\.\d+\.\d+$/;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Compare two 4-part versions numerically. Missing trailing components
|
|
27
|
-
* default to 0 so `compareVersions("1.2", "1.2.0.0") === 0`, though the
|
|
28
|
-
* parser elsewhere always emits fully-qualified 4-part strings.
|
|
29
|
-
*
|
|
30
|
-
* @param {string} a
|
|
31
|
-
* @param {string} b
|
|
32
|
-
* @returns {-1|0|1}
|
|
33
|
-
*/
|
|
34
|
-
function compareVersions(a, b) {
|
|
35
|
-
const partsOf = (s) => s.split('.').map((n) => parseInt(n, 10) || 0);
|
|
36
|
-
const pa = partsOf(a);
|
|
37
|
-
const pb = partsOf(b);
|
|
38
|
-
const len = Math.max(pa.length, pb.length);
|
|
39
|
-
for (let i = 0; i < len; i++) {
|
|
40
|
-
const x = pa[i] ?? 0;
|
|
41
|
-
const y = pb[i] ?? 0;
|
|
42
|
-
if (x < y) return -1;
|
|
43
|
-
if (x > y) return 1;
|
|
44
|
-
}
|
|
45
|
-
return 0;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function bumpMajor(v) {
|
|
49
|
-
const [maj] = v.split('.').map((n) => parseInt(n, 10) || 0);
|
|
50
|
-
return `${maj + 1}.0.0.0`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function bumpMinor(v) {
|
|
54
|
-
const [maj, min] = v.split('.').map((n) => parseInt(n, 10) || 0);
|
|
55
|
-
return `${maj}.${min + 1}.0.0`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Parse a dependency version string into a structured spec. Exact versions
|
|
60
|
-
* and `"latest"` pass through unchanged; `^` / `~` / comparator strings
|
|
61
|
-
* become `{ kind: 'range', min, max }` specs.
|
|
62
|
-
*
|
|
63
|
-
* `min` and `max` are each `{ op: '>=' | '>' | null, v: string } | null`
|
|
64
|
-
* (null means unbounded on that side — an open upper bound, etc.).
|
|
65
|
-
*
|
|
66
|
-
* Throws on malformed input with a user-friendly multi-line error that
|
|
67
|
-
* lists the supported syntaxes.
|
|
68
|
-
*
|
|
69
|
-
* @param {string} spec
|
|
70
|
-
* @returns {{ kind: 'exact', version: string }
|
|
71
|
-
* | { kind: 'tag', tag: 'latest' }
|
|
72
|
-
* | { kind: 'range', raw: string,
|
|
73
|
-
* min: { op: '>='|'>', v: string } | null,
|
|
74
|
-
* max: { op: '<='|'<', v: string } | null,
|
|
75
|
-
* exact?: string }}
|
|
76
|
-
*/
|
|
77
|
-
function parseRange(spec) {
|
|
78
|
-
if (typeof spec !== 'string' || spec.length === 0) {
|
|
79
|
-
throw new Error(malformedRangeMessage(spec, 'version spec must be a non-empty string'));
|
|
80
|
-
}
|
|
81
|
-
const s = spec.trim();
|
|
82
|
-
|
|
83
|
-
if (s === 'latest') return { kind: 'tag', tag: 'latest' };
|
|
84
|
-
|
|
85
|
-
// `linked:` is handled by resolve.js directly — not our concern.
|
|
86
|
-
if (s.startsWith('linked:')) return { kind: 'linked', raw: s };
|
|
87
|
-
|
|
88
|
-
if (VERSION_RE.test(s)) return { kind: 'exact', version: s };
|
|
89
|
-
|
|
90
|
-
// Caret / tilde — require a full 4-part version as the operand.
|
|
91
|
-
if (s[0] === '^' || s[0] === '~') {
|
|
92
|
-
const op = s[0];
|
|
93
|
-
const v = s.slice(1).trim();
|
|
94
|
-
if (!VERSION_RE.test(v)) {
|
|
95
|
-
throw new Error(malformedRangeMessage(spec, `${op} prefix requires a 4-part version`));
|
|
96
|
-
}
|
|
97
|
-
return {
|
|
98
|
-
kind: 'range',
|
|
99
|
-
raw: s,
|
|
100
|
-
min: { op: '>=', v },
|
|
101
|
-
max: { op: '<', v: op === '^' ? bumpMajor(v) : bumpMinor(v) },
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Explicit comparator form: one or more of {>=, >, <=, <, =} each followed
|
|
106
|
-
// by a 4-part version, space-separated.
|
|
107
|
-
if (/^[<>=]/.test(s)) {
|
|
108
|
-
const tokens = s.split(/\s+/);
|
|
109
|
-
let min = null;
|
|
110
|
-
let max = null;
|
|
111
|
-
let exact = null;
|
|
112
|
-
for (const tok of tokens) {
|
|
113
|
-
const m = tok.match(/^(>=|<=|>|<|=)(\d+\.\d+\.\d+\.\d+)$/);
|
|
114
|
-
if (!m) {
|
|
115
|
-
throw new Error(malformedRangeMessage(spec, `unrecognised comparator "${tok}"`));
|
|
116
|
-
}
|
|
117
|
-
const [, op, v] = m;
|
|
118
|
-
if (op === '=') {
|
|
119
|
-
if (exact && exact !== v) {
|
|
120
|
-
throw new Error(malformedRangeMessage(spec, `conflicting = comparators (${exact} vs ${v})`));
|
|
121
|
-
}
|
|
122
|
-
exact = v;
|
|
123
|
-
} else if (op === '>=' || op === '>') {
|
|
124
|
-
if (!min || compareVersions(v, min.v) > 0 || (compareVersions(v, min.v) === 0 && op === '>')) {
|
|
125
|
-
min = { op, v };
|
|
126
|
-
}
|
|
127
|
-
} else {
|
|
128
|
-
if (!max || compareVersions(v, max.v) < 0 || (compareVersions(v, max.v) === 0 && op === '<')) {
|
|
129
|
-
max = { op, v };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (exact) return { kind: 'exact', version: exact };
|
|
134
|
-
if (!min && !max) {
|
|
135
|
-
throw new Error(malformedRangeMessage(spec, 'no comparators parsed'));
|
|
136
|
-
}
|
|
137
|
-
return { kind: 'range', raw: s, min, max };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
throw new Error(malformedRangeMessage(spec, 'unknown version syntax'));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function malformedRangeMessage(spec, why) {
|
|
144
|
-
return (
|
|
145
|
-
`cannot parse dependency version ${JSON.stringify(spec)}: ${why}\n` +
|
|
146
|
-
` supported syntaxes:\n` +
|
|
147
|
-
` "1.2.3.4" exact version\n` +
|
|
148
|
-
` "latest" latest-published tag\n` +
|
|
149
|
-
` "^1.2.3.4" major-pinned range (>= 1.2.3.4 < 2.0.0.0)\n` +
|
|
150
|
-
` "~1.2.3.4" minor-pinned range (>= 1.2.3.4 < 1.3.0.0)\n` +
|
|
151
|
-
` ">=1.2.3.4 <2.0.0.0" explicit range (>=, >, <=, <, = separated by spaces)`
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Returns true if `version` satisfies every bound in `parsed`. Exact-kind
|
|
157
|
-
* specs require byte-equal; tag/linked specs always return false here
|
|
158
|
-
* because they are handled upstream.
|
|
159
|
-
*
|
|
160
|
-
* @param {string} version
|
|
161
|
-
* @param {ReturnType<typeof parseRange>} parsed
|
|
162
|
-
* @returns {boolean}
|
|
163
|
-
*/
|
|
164
|
-
function matchesRange(version, parsed) {
|
|
165
|
-
if (!parsed) return false;
|
|
166
|
-
if (parsed.kind === 'exact') return version === parsed.version;
|
|
167
|
-
if (parsed.kind !== 'range') return false;
|
|
168
|
-
if (parsed.min) {
|
|
169
|
-
const cmp = compareVersions(version, parsed.min.v);
|
|
170
|
-
if (parsed.min.op === '>=' && cmp < 0) return false;
|
|
171
|
-
if (parsed.min.op === '>' && cmp <= 0) return false;
|
|
172
|
-
}
|
|
173
|
-
if (parsed.max) {
|
|
174
|
-
const cmp = compareVersions(version, parsed.max.v);
|
|
175
|
-
if (parsed.max.op === '<=' && cmp > 0) return false;
|
|
176
|
-
if (parsed.max.op === '<' && cmp >= 0) return false;
|
|
177
|
-
}
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Pick the highest version from `publishedVersions` that satisfies
|
|
183
|
-
* `parsed`. Returns null when no version matches. Input is not mutated.
|
|
184
|
-
*
|
|
185
|
-
* @param {string[]} publishedVersions
|
|
186
|
-
* @param {ReturnType<typeof parseRange>} parsed
|
|
187
|
-
* @returns {string|null}
|
|
188
|
-
*/
|
|
189
|
-
function pickHighestInRange(publishedVersions, parsed) {
|
|
190
|
-
if (!Array.isArray(publishedVersions) || publishedVersions.length === 0) return null;
|
|
191
|
-
const matches = publishedVersions.filter((v) => matchesRange(v, parsed));
|
|
192
|
-
if (matches.length === 0) return null;
|
|
193
|
-
matches.sort(compareVersions);
|
|
194
|
-
return matches[matches.length - 1];
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Intersect two parsed range specs. Exact specs are treated as the degenerate
|
|
199
|
-
* range `[v, v]`. Returns a new range spec or null when the intersection is
|
|
200
|
-
* empty.
|
|
201
|
-
*
|
|
202
|
-
* `tag` and `linked` kinds cannot participate in intersection — the caller
|
|
203
|
-
* must resolve them to exact first. We throw rather than silently ignore.
|
|
204
|
-
*
|
|
205
|
-
* @param {ReturnType<typeof parseRange>} a
|
|
206
|
-
* @param {ReturnType<typeof parseRange>} b
|
|
207
|
-
* @returns {{ kind: 'range'|'exact', ... }|null}
|
|
208
|
-
*/
|
|
209
|
-
function intersectRanges(a, b) {
|
|
210
|
-
const liftExact = (spec) => spec.kind === 'exact'
|
|
211
|
-
? { kind: 'range', raw: spec.version, min: { op: '>=', v: spec.version }, max: { op: '<=', v: spec.version } }
|
|
212
|
-
: spec;
|
|
213
|
-
|
|
214
|
-
if (!a || !b) throw new Error('intersectRanges: null spec');
|
|
215
|
-
if (a.kind === 'tag' || a.kind === 'linked' || b.kind === 'tag' || b.kind === 'linked') {
|
|
216
|
-
throw new Error('intersectRanges: tag/linked must be resolved before intersection');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const ra = liftExact(a);
|
|
220
|
-
const rb = liftExact(b);
|
|
221
|
-
|
|
222
|
-
// Tighter min (higher value; strictly-greater beats equal under gte).
|
|
223
|
-
const tighterMin = pickTighter(ra.min, rb.min, 'min');
|
|
224
|
-
// Tighter max (lower value; strictly-less beats equal under lte).
|
|
225
|
-
const tighterMax = pickTighter(ra.max, rb.max, 'max');
|
|
226
|
-
|
|
227
|
-
if (tighterMin && tighterMax) {
|
|
228
|
-
const cmp = compareVersions(tighterMin.v, tighterMax.v);
|
|
229
|
-
if (cmp > 0) return null;
|
|
230
|
-
if (cmp === 0) {
|
|
231
|
-
// Both bounds reference the same version. Feasible only if both are
|
|
232
|
-
// inclusive; otherwise the point is excluded.
|
|
233
|
-
if (tighterMin.op !== '>=' || tighterMax.op !== '<=') return null;
|
|
234
|
-
return { kind: 'exact', version: tighterMin.v };
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return {
|
|
239
|
-
kind: 'range',
|
|
240
|
-
raw: `${ra.raw ?? 'range'} ∩ ${rb.raw ?? 'range'}`,
|
|
241
|
-
min: tighterMin,
|
|
242
|
-
max: tighterMax,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function pickTighter(a, b, kind) {
|
|
247
|
-
if (!a) return b;
|
|
248
|
-
if (!b) return a;
|
|
249
|
-
const cmp = compareVersions(a.v, b.v);
|
|
250
|
-
if (kind === 'min') {
|
|
251
|
-
if (cmp > 0) return a;
|
|
252
|
-
if (cmp < 0) return b;
|
|
253
|
-
// equal: strictly-greater is tighter than gte
|
|
254
|
-
return a.op === '>' ? a : b;
|
|
255
|
-
} else {
|
|
256
|
-
if (cmp < 0) return a;
|
|
257
|
-
if (cmp > 0) return b;
|
|
258
|
-
return a.op === '<' ? a : b;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Human-readable rendering for error messages. Exact and tag kinds pass
|
|
264
|
-
* through unchanged; ranges render as the original raw string when
|
|
265
|
-
* available, else as a comparator reconstruction.
|
|
266
|
-
*
|
|
267
|
-
* @param {ReturnType<typeof parseRange>} parsed
|
|
268
|
-
* @returns {string}
|
|
269
|
-
*/
|
|
270
|
-
function rangeToString(parsed) {
|
|
271
|
-
if (!parsed) return '(unparsed)';
|
|
272
|
-
if (parsed.kind === 'exact') return parsed.version;
|
|
273
|
-
if (parsed.kind === 'tag') return parsed.tag;
|
|
274
|
-
if (parsed.kind === 'linked') return parsed.raw;
|
|
275
|
-
if (parsed.raw && !parsed.raw.includes('∩')) return parsed.raw;
|
|
276
|
-
const parts = [];
|
|
277
|
-
if (parsed.min) parts.push(`${parsed.min.op}${parsed.min.v}`);
|
|
278
|
-
if (parsed.max) parts.push(`${parsed.max.op}${parsed.max.v}`);
|
|
279
|
-
return parts.join(' ');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Convenience predicate used by the resolver: does this spec require
|
|
284
|
-
* range-based resolution (as opposed to exact / latest / linked)?
|
|
285
|
-
*/
|
|
286
|
-
function isRangeSpec(spec) {
|
|
287
|
-
try {
|
|
288
|
-
const parsed = parseRange(spec);
|
|
289
|
-
return parsed.kind === 'range';
|
|
290
|
-
} catch { return false; }
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
module.exports = {
|
|
294
|
-
parseRange,
|
|
295
|
-
matchesRange,
|
|
296
|
-
pickHighestInRange,
|
|
297
|
-
intersectRanges,
|
|
298
|
-
rangeToString,
|
|
299
|
-
isRangeSpec,
|
|
300
|
-
compareVersions,
|
|
301
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Version range parsing and matching for 4-part `major.minor.patch.build`
|
|
5
|
+
* versions.
|
|
6
|
+
*
|
|
7
|
+
* Accepts three range syntaxes in addition to the existing exact and
|
|
8
|
+
* `"latest"` strings:
|
|
9
|
+
*
|
|
10
|
+
* "^1.2.3.4" major-pinned : >= 1.2.3.4 < 2.0.0.0
|
|
11
|
+
* "~1.2.3.4" minor-pinned : >= 1.2.3.4 < 1.3.0.0
|
|
12
|
+
* ">=1.2.3.4 <2.0" explicit : one or more space-separated comparators
|
|
13
|
+
* among >=, >, <=, <, =
|
|
14
|
+
*
|
|
15
|
+
* `parseRange(s)` classifies the input; `matchesRange(v, parsed)`
|
|
16
|
+
* evaluates a candidate version; `pickHighestInRange(versions, parsed)`
|
|
17
|
+
* selects the best match; `intersectRanges(a, b)` combines two ranges
|
|
18
|
+
* (returns null when the intersection is empty).
|
|
19
|
+
*
|
|
20
|
+
* See claude/PLAN-VERSION-RANGES.md for the full design.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const VERSION_RE = /^\d+\.\d+\.\d+\.\d+$/;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compare two 4-part versions numerically. Missing trailing components
|
|
27
|
+
* default to 0 so `compareVersions("1.2", "1.2.0.0") === 0`, though the
|
|
28
|
+
* parser elsewhere always emits fully-qualified 4-part strings.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} a
|
|
31
|
+
* @param {string} b
|
|
32
|
+
* @returns {-1|0|1}
|
|
33
|
+
*/
|
|
34
|
+
function compareVersions(a, b) {
|
|
35
|
+
const partsOf = (s) => s.split('.').map((n) => parseInt(n, 10) || 0);
|
|
36
|
+
const pa = partsOf(a);
|
|
37
|
+
const pb = partsOf(b);
|
|
38
|
+
const len = Math.max(pa.length, pb.length);
|
|
39
|
+
for (let i = 0; i < len; i++) {
|
|
40
|
+
const x = pa[i] ?? 0;
|
|
41
|
+
const y = pb[i] ?? 0;
|
|
42
|
+
if (x < y) return -1;
|
|
43
|
+
if (x > y) return 1;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function bumpMajor(v) {
|
|
49
|
+
const [maj] = v.split('.').map((n) => parseInt(n, 10) || 0);
|
|
50
|
+
return `${maj + 1}.0.0.0`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function bumpMinor(v) {
|
|
54
|
+
const [maj, min] = v.split('.').map((n) => parseInt(n, 10) || 0);
|
|
55
|
+
return `${maj}.${min + 1}.0.0`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse a dependency version string into a structured spec. Exact versions
|
|
60
|
+
* and `"latest"` pass through unchanged; `^` / `~` / comparator strings
|
|
61
|
+
* become `{ kind: 'range', min, max }` specs.
|
|
62
|
+
*
|
|
63
|
+
* `min` and `max` are each `{ op: '>=' | '>' | null, v: string } | null`
|
|
64
|
+
* (null means unbounded on that side — an open upper bound, etc.).
|
|
65
|
+
*
|
|
66
|
+
* Throws on malformed input with a user-friendly multi-line error that
|
|
67
|
+
* lists the supported syntaxes.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} spec
|
|
70
|
+
* @returns {{ kind: 'exact', version: string }
|
|
71
|
+
* | { kind: 'tag', tag: 'latest' }
|
|
72
|
+
* | { kind: 'range', raw: string,
|
|
73
|
+
* min: { op: '>='|'>', v: string } | null,
|
|
74
|
+
* max: { op: '<='|'<', v: string } | null,
|
|
75
|
+
* exact?: string }}
|
|
76
|
+
*/
|
|
77
|
+
function parseRange(spec) {
|
|
78
|
+
if (typeof spec !== 'string' || spec.length === 0) {
|
|
79
|
+
throw new Error(malformedRangeMessage(spec, 'version spec must be a non-empty string'));
|
|
80
|
+
}
|
|
81
|
+
const s = spec.trim();
|
|
82
|
+
|
|
83
|
+
if (s === 'latest') return { kind: 'tag', tag: 'latest' };
|
|
84
|
+
|
|
85
|
+
// `linked:` is handled by resolve.js directly — not our concern.
|
|
86
|
+
if (s.startsWith('linked:')) return { kind: 'linked', raw: s };
|
|
87
|
+
|
|
88
|
+
if (VERSION_RE.test(s)) return { kind: 'exact', version: s };
|
|
89
|
+
|
|
90
|
+
// Caret / tilde — require a full 4-part version as the operand.
|
|
91
|
+
if (s[0] === '^' || s[0] === '~') {
|
|
92
|
+
const op = s[0];
|
|
93
|
+
const v = s.slice(1).trim();
|
|
94
|
+
if (!VERSION_RE.test(v)) {
|
|
95
|
+
throw new Error(malformedRangeMessage(spec, `${op} prefix requires a 4-part version`));
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
kind: 'range',
|
|
99
|
+
raw: s,
|
|
100
|
+
min: { op: '>=', v },
|
|
101
|
+
max: { op: '<', v: op === '^' ? bumpMajor(v) : bumpMinor(v) },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Explicit comparator form: one or more of {>=, >, <=, <, =} each followed
|
|
106
|
+
// by a 4-part version, space-separated.
|
|
107
|
+
if (/^[<>=]/.test(s)) {
|
|
108
|
+
const tokens = s.split(/\s+/);
|
|
109
|
+
let min = null;
|
|
110
|
+
let max = null;
|
|
111
|
+
let exact = null;
|
|
112
|
+
for (const tok of tokens) {
|
|
113
|
+
const m = tok.match(/^(>=|<=|>|<|=)(\d+\.\d+\.\d+\.\d+)$/);
|
|
114
|
+
if (!m) {
|
|
115
|
+
throw new Error(malformedRangeMessage(spec, `unrecognised comparator "${tok}"`));
|
|
116
|
+
}
|
|
117
|
+
const [, op, v] = m;
|
|
118
|
+
if (op === '=') {
|
|
119
|
+
if (exact && exact !== v) {
|
|
120
|
+
throw new Error(malformedRangeMessage(spec, `conflicting = comparators (${exact} vs ${v})`));
|
|
121
|
+
}
|
|
122
|
+
exact = v;
|
|
123
|
+
} else if (op === '>=' || op === '>') {
|
|
124
|
+
if (!min || compareVersions(v, min.v) > 0 || (compareVersions(v, min.v) === 0 && op === '>')) {
|
|
125
|
+
min = { op, v };
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
if (!max || compareVersions(v, max.v) < 0 || (compareVersions(v, max.v) === 0 && op === '<')) {
|
|
129
|
+
max = { op, v };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (exact) return { kind: 'exact', version: exact };
|
|
134
|
+
if (!min && !max) {
|
|
135
|
+
throw new Error(malformedRangeMessage(spec, 'no comparators parsed'));
|
|
136
|
+
}
|
|
137
|
+
return { kind: 'range', raw: s, min, max };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
throw new Error(malformedRangeMessage(spec, 'unknown version syntax'));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function malformedRangeMessage(spec, why) {
|
|
144
|
+
return (
|
|
145
|
+
`cannot parse dependency version ${JSON.stringify(spec)}: ${why}\n` +
|
|
146
|
+
` supported syntaxes:\n` +
|
|
147
|
+
` "1.2.3.4" exact version\n` +
|
|
148
|
+
` "latest" latest-published tag\n` +
|
|
149
|
+
` "^1.2.3.4" major-pinned range (>= 1.2.3.4 < 2.0.0.0)\n` +
|
|
150
|
+
` "~1.2.3.4" minor-pinned range (>= 1.2.3.4 < 1.3.0.0)\n` +
|
|
151
|
+
` ">=1.2.3.4 <2.0.0.0" explicit range (>=, >, <=, <, = separated by spaces)`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Returns true if `version` satisfies every bound in `parsed`. Exact-kind
|
|
157
|
+
* specs require byte-equal; tag/linked specs always return false here
|
|
158
|
+
* because they are handled upstream.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} version
|
|
161
|
+
* @param {ReturnType<typeof parseRange>} parsed
|
|
162
|
+
* @returns {boolean}
|
|
163
|
+
*/
|
|
164
|
+
function matchesRange(version, parsed) {
|
|
165
|
+
if (!parsed) return false;
|
|
166
|
+
if (parsed.kind === 'exact') return version === parsed.version;
|
|
167
|
+
if (parsed.kind !== 'range') return false;
|
|
168
|
+
if (parsed.min) {
|
|
169
|
+
const cmp = compareVersions(version, parsed.min.v);
|
|
170
|
+
if (parsed.min.op === '>=' && cmp < 0) return false;
|
|
171
|
+
if (parsed.min.op === '>' && cmp <= 0) return false;
|
|
172
|
+
}
|
|
173
|
+
if (parsed.max) {
|
|
174
|
+
const cmp = compareVersions(version, parsed.max.v);
|
|
175
|
+
if (parsed.max.op === '<=' && cmp > 0) return false;
|
|
176
|
+
if (parsed.max.op === '<' && cmp >= 0) return false;
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Pick the highest version from `publishedVersions` that satisfies
|
|
183
|
+
* `parsed`. Returns null when no version matches. Input is not mutated.
|
|
184
|
+
*
|
|
185
|
+
* @param {string[]} publishedVersions
|
|
186
|
+
* @param {ReturnType<typeof parseRange>} parsed
|
|
187
|
+
* @returns {string|null}
|
|
188
|
+
*/
|
|
189
|
+
function pickHighestInRange(publishedVersions, parsed) {
|
|
190
|
+
if (!Array.isArray(publishedVersions) || publishedVersions.length === 0) return null;
|
|
191
|
+
const matches = publishedVersions.filter((v) => matchesRange(v, parsed));
|
|
192
|
+
if (matches.length === 0) return null;
|
|
193
|
+
matches.sort(compareVersions);
|
|
194
|
+
return matches[matches.length - 1];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Intersect two parsed range specs. Exact specs are treated as the degenerate
|
|
199
|
+
* range `[v, v]`. Returns a new range spec or null when the intersection is
|
|
200
|
+
* empty.
|
|
201
|
+
*
|
|
202
|
+
* `tag` and `linked` kinds cannot participate in intersection — the caller
|
|
203
|
+
* must resolve them to exact first. We throw rather than silently ignore.
|
|
204
|
+
*
|
|
205
|
+
* @param {ReturnType<typeof parseRange>} a
|
|
206
|
+
* @param {ReturnType<typeof parseRange>} b
|
|
207
|
+
* @returns {{ kind: 'range'|'exact', ... }|null}
|
|
208
|
+
*/
|
|
209
|
+
function intersectRanges(a, b) {
|
|
210
|
+
const liftExact = (spec) => spec.kind === 'exact'
|
|
211
|
+
? { kind: 'range', raw: spec.version, min: { op: '>=', v: spec.version }, max: { op: '<=', v: spec.version } }
|
|
212
|
+
: spec;
|
|
213
|
+
|
|
214
|
+
if (!a || !b) throw new Error('intersectRanges: null spec');
|
|
215
|
+
if (a.kind === 'tag' || a.kind === 'linked' || b.kind === 'tag' || b.kind === 'linked') {
|
|
216
|
+
throw new Error('intersectRanges: tag/linked must be resolved before intersection');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const ra = liftExact(a);
|
|
220
|
+
const rb = liftExact(b);
|
|
221
|
+
|
|
222
|
+
// Tighter min (higher value; strictly-greater beats equal under gte).
|
|
223
|
+
const tighterMin = pickTighter(ra.min, rb.min, 'min');
|
|
224
|
+
// Tighter max (lower value; strictly-less beats equal under lte).
|
|
225
|
+
const tighterMax = pickTighter(ra.max, rb.max, 'max');
|
|
226
|
+
|
|
227
|
+
if (tighterMin && tighterMax) {
|
|
228
|
+
const cmp = compareVersions(tighterMin.v, tighterMax.v);
|
|
229
|
+
if (cmp > 0) return null;
|
|
230
|
+
if (cmp === 0) {
|
|
231
|
+
// Both bounds reference the same version. Feasible only if both are
|
|
232
|
+
// inclusive; otherwise the point is excluded.
|
|
233
|
+
if (tighterMin.op !== '>=' || tighterMax.op !== '<=') return null;
|
|
234
|
+
return { kind: 'exact', version: tighterMin.v };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
kind: 'range',
|
|
240
|
+
raw: `${ra.raw ?? 'range'} ∩ ${rb.raw ?? 'range'}`,
|
|
241
|
+
min: tighterMin,
|
|
242
|
+
max: tighterMax,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function pickTighter(a, b, kind) {
|
|
247
|
+
if (!a) return b;
|
|
248
|
+
if (!b) return a;
|
|
249
|
+
const cmp = compareVersions(a.v, b.v);
|
|
250
|
+
if (kind === 'min') {
|
|
251
|
+
if (cmp > 0) return a;
|
|
252
|
+
if (cmp < 0) return b;
|
|
253
|
+
// equal: strictly-greater is tighter than gte
|
|
254
|
+
return a.op === '>' ? a : b;
|
|
255
|
+
} else {
|
|
256
|
+
if (cmp < 0) return a;
|
|
257
|
+
if (cmp > 0) return b;
|
|
258
|
+
return a.op === '<' ? a : b;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Human-readable rendering for error messages. Exact and tag kinds pass
|
|
264
|
+
* through unchanged; ranges render as the original raw string when
|
|
265
|
+
* available, else as a comparator reconstruction.
|
|
266
|
+
*
|
|
267
|
+
* @param {ReturnType<typeof parseRange>} parsed
|
|
268
|
+
* @returns {string}
|
|
269
|
+
*/
|
|
270
|
+
function rangeToString(parsed) {
|
|
271
|
+
if (!parsed) return '(unparsed)';
|
|
272
|
+
if (parsed.kind === 'exact') return parsed.version;
|
|
273
|
+
if (parsed.kind === 'tag') return parsed.tag;
|
|
274
|
+
if (parsed.kind === 'linked') return parsed.raw;
|
|
275
|
+
if (parsed.raw && !parsed.raw.includes('∩')) return parsed.raw;
|
|
276
|
+
const parts = [];
|
|
277
|
+
if (parsed.min) parts.push(`${parsed.min.op}${parsed.min.v}`);
|
|
278
|
+
if (parsed.max) parts.push(`${parsed.max.op}${parsed.max.v}`);
|
|
279
|
+
return parts.join(' ');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Convenience predicate used by the resolver: does this spec require
|
|
284
|
+
* range-based resolution (as opposed to exact / latest / linked)?
|
|
285
|
+
*/
|
|
286
|
+
function isRangeSpec(spec) {
|
|
287
|
+
try {
|
|
288
|
+
const parsed = parseRange(spec);
|
|
289
|
+
return parsed.kind === 'range';
|
|
290
|
+
} catch { return false; }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
parseRange,
|
|
295
|
+
matchesRange,
|
|
296
|
+
pickHighestInRange,
|
|
297
|
+
intersectRanges,
|
|
298
|
+
rangeToString,
|
|
299
|
+
isRangeSpec,
|
|
300
|
+
compareVersions,
|
|
301
|
+
};
|