@veda-hq/pariksha 0.5.13 → 0.5.17

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.
@@ -0,0 +1,350 @@
1
+ // pnpm-settings: pnpm settings live where pnpm reads them, and took effect.
2
+ //
3
+ // pnpm 11 stopped reading the `pnpm` field of package.json, silently
4
+ // (pnpm/pnpm#11536): an `overrides` or `patchedDependencies` left there gives
5
+ // a green install in which every security pin has reverted and every patch
6
+ // has stopped applying. A green install proves nothing, so this reads the
7
+ // evidence out of the resolved lockfile:
8
+ //
9
+ // 1. package.json carries no pnpm setting pnpm 11 ignores;
10
+ // 2. every override in pnpm-workspace.yaml is in the lockfile, and back;
11
+ // 3. every patch is in the lockfile and back, its file exists, and its
12
+ // sha256 is the one the lockfile records;
13
+ // 4. no resolved version sits inside a range an override excludes (or, for
14
+ // a bare-name override, outside the range it forces);
15
+ // 5. every `minimumReleaseAgeExclude` entry is an exact version the lockfile
16
+ // still resolves, under a comment that says why and `EXPIRES:` when, and
17
+ // that expiry has not passed. The config's own scopes are standing
18
+ // entries, exempt.
19
+ //
20
+ // Version ranges are read by semver-range.ts: a form it does not read (`^`,
21
+ // `~`, `1.x`, a hyphen range) is reported as unchecked, never passed.
22
+ // pnpm 12 writes the lockfile as two YAML documents, the package manager's
23
+ // first: the last one with content is the project's.
24
+ import { createHash } from 'node:crypto';
25
+ import { existsSync, readFileSync } from 'node:fs';
26
+ import { join } from 'node:path';
27
+ import { CORE_SCHEMA, load, loadAll } from 'js-yaml';
28
+ import { satisfies, validRange, validVersion } from "./semver-range.js";
29
+ export const DEFAULT_PNPM_CONFIG = { id: 'pnpm-settings', ownScopes: [] };
30
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
31
+ /** `{ "id"?: "<label>", "ownScopes"?: ["@scope/*", …] }`; absent keys take the defaults, anything else throws. */
32
+ export const parsePnpmConfig = (text) => {
33
+ let raw;
34
+ try {
35
+ raw = JSON.parse(text);
36
+ }
37
+ catch (e) {
38
+ throw new Error(`not JSON: ${e.message}`);
39
+ }
40
+ if (!isRecord(raw))
41
+ throw new Error('the config must be a JSON object');
42
+ const unknown = Object.keys(raw).find((k) => k !== 'id' && k !== 'ownScopes');
43
+ if (unknown !== undefined)
44
+ throw new Error(`unknown key "${unknown}"`);
45
+ // Absent takes the default; a present `null` is refused like any other wrong value.
46
+ const id = 'id' in raw ? raw['id'] : DEFAULT_PNPM_CONFIG.id;
47
+ if (typeof id !== 'string' || id.trim() === '')
48
+ throw new Error('"id" must be a non-empty string');
49
+ const ownScopes = 'ownScopes' in raw ? raw['ownScopes'] : [];
50
+ if (!Array.isArray(ownScopes) || ownScopes.some((s) => typeof s !== 'string' || s === ''))
51
+ throw new Error('"ownScopes" must be a list of entries');
52
+ return { id, ownScopes: ownScopes };
53
+ };
54
+ /** pnpm 11 reads none of these from package.json. Leaving one there disarms it. */
55
+ export const IGNORED_IN_V11 = [
56
+ 'overrides',
57
+ 'patchedDependencies',
58
+ 'auditConfig',
59
+ 'onlyBuiltDependencies',
60
+ 'onlyBuiltDependenciesFile',
61
+ 'neverBuiltDependencies',
62
+ 'ignoredBuiltDependencies',
63
+ 'ignoreDepScripts',
64
+ 'packageExtensions',
65
+ 'peerDependencyRules',
66
+ 'allowedDeprecatedVersions',
67
+ 'updateConfig',
68
+ 'allowBuilds',
69
+ ];
70
+ /** A lockfile key as name and version, split on the last `@` (scoped names hold one too). */
71
+ export const splitNameVersion = (key) => {
72
+ const at = key.lastIndexOf('@');
73
+ if (at <= 0)
74
+ return null;
75
+ return { name: key.slice(0, at), version: key.slice(at + 1) };
76
+ };
77
+ /** The project document: the last one with content. Every document must still parse. */
78
+ const projectDocument = (text) => {
79
+ const filled = loadAll(text, null, { schema: CORE_SCHEMA }).filter((value) => value !== null && value !== undefined);
80
+ return (filled.at(-1) ?? {});
81
+ };
82
+ /** What the lockfile records: overrides, patches (pnpm 10's `{ hash, path }` or pnpm 11's bare hash) and resolved versions. */
83
+ export const readLockfileFacts = (text) => {
84
+ const doc = projectDocument(text);
85
+ const resolvedVersions = new Map();
86
+ for (const key of Object.keys(doc.packages ?? {})) {
87
+ const parsed = splitNameVersion(key);
88
+ if (!parsed)
89
+ continue;
90
+ resolvedVersions.set(parsed.name, [...(resolvedVersions.get(parsed.name) ?? []), parsed.version]);
91
+ }
92
+ const patchedDependencies = {};
93
+ for (const [key, entry] of Object.entries(doc.patchedDependencies ?? {})) {
94
+ // String(), not a typeof check: an all-digit hash parses as a number.
95
+ patchedDependencies[key] = entry !== null && typeof entry === 'object' ? { ...entry } : { hash: String(entry) };
96
+ }
97
+ return { overrides: doc.overrides ?? {}, patchedDependencies, resolvedVersions };
98
+ };
99
+ /**
100
+ * The comment block right above `minimumReleaseAgeExclude:`, the decision
101
+ * record YAML parsing drops. A blank line or a setting ends it, so a comment
102
+ * on another key is never read as this one's.
103
+ */
104
+ export const releaseAgeExclusionComment = (workspaceText) => {
105
+ const lines = workspaceText.split('\n');
106
+ const at = lines.findIndex((line) => /^minimumReleaseAgeExclude\s*:/.test(line));
107
+ if (at === -1)
108
+ return '';
109
+ const collected = [];
110
+ for (let i = at - 1; i >= 0; i--) {
111
+ const line = (lines[i] ?? '').trim();
112
+ if (!line.startsWith('#'))
113
+ break;
114
+ collected.unshift(line.replace(/^#\s?/, ''));
115
+ }
116
+ return collected.join('\n');
117
+ };
118
+ /** A date, optionally a UTC time: `2026-09-02`, `2026-09-02T18:54Z`, `2026-09-02T18:54:00.000Z`. */
119
+ const ISO_8601_DATE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?Z)?$/;
120
+ /**
121
+ * `EXPIRES: <ISO-8601>` from a line that opens with it (a mention mid-sentence
122
+ * records nothing), its trailing punctuation dropped. A value that is not an
123
+ * ISO-8601 date, or names a day its month lacks, is invalid.
124
+ */
125
+ export const parseExclusionExpiry = (comment) => {
126
+ const match = /^[ \t]*EXPIRES[ \t]*:[ \t]*(\S*)/im.exec(comment);
127
+ if (match === null)
128
+ return { kind: 'absent' };
129
+ const raw = (match[1] ?? '').replace(/[.,;:]+$/, '');
130
+ const at = new Date(raw);
131
+ if (!ISO_8601_DATE.test(raw) || Number.isNaN(at.getTime()) || at.toISOString().slice(0, 10) !== raw.slice(0, 10))
132
+ return { kind: 'invalid', raw };
133
+ return { kind: 'at', at, raw };
134
+ };
135
+ /** An override or exclusion selector as name and range: `tar@<=7.5.20`, `@babel/core@>=7 <8`; a bare name targets `*`. */
136
+ export const parseSelector = (selector) => {
137
+ const at = selector.lastIndexOf('@');
138
+ if (at <= 0)
139
+ return { name: selector, range: '*' };
140
+ return { name: selector.slice(0, at), range: selector.slice(at + 1) };
141
+ };
142
+ /** Check 5: cooling-off exceptions are scoped to one version, still resolved, documented and unexpired. */
143
+ export const checkReleaseAgeExclusions = (workspaceText, declared, lock, ownScopes, now) => {
144
+ const failures = [];
145
+ if (declared === undefined || declared === null)
146
+ return { failures, count: 0 };
147
+ if (!Array.isArray(declared)) {
148
+ return {
149
+ failures: [
150
+ `minimumReleaseAgeExclude must be a list of \`name@version\` strings, but it is ` +
151
+ `\`${typeof declared}\`. pnpm cannot apply it in this shape, so the exception it ` +
152
+ `was meant to record is not in force either.`,
153
+ ],
154
+ count: 0,
155
+ };
156
+ }
157
+ const exceptions = declared.filter((entry) => !(typeof entry === 'string' && ownScopes.includes(entry)));
158
+ // An empty list is the post-expiry steady state, not something to document.
159
+ if (exceptions.length === 0)
160
+ return { failures, count: 0 };
161
+ const comment = releaseAgeExclusionComment(workspaceText);
162
+ if (comment === '') {
163
+ failures.push(`minimumReleaseAgeExclude declares ${exceptions.length} cooling-off exception(s) with no ` +
164
+ `comment above the block. pnpm writes entries here on its own when ` +
165
+ `minimumReleaseAgeStrict is false, so an undocumented entry is a supply-chain ` +
166
+ `exception nobody took. Record why it was taken, what took it, and when it expires.`);
167
+ }
168
+ else {
169
+ const expiry = parseExclusionExpiry(comment);
170
+ if (expiry.kind === 'absent') {
171
+ failures.push(`the comment above minimumReleaseAgeExclude does not say when the exception expires. ` +
172
+ `Add an \`EXPIRES: <YYYY-MM-DD…>\` line — an exception with no end date is a ` +
173
+ `permanent hole in the cooling-off.`);
174
+ }
175
+ else if (expiry.kind === 'invalid') {
176
+ failures.push(`the EXPIRES value above minimumReleaseAgeExclude (\`${expiry.raw}\`) is not a date this ` +
177
+ `check can read, so the expiry cannot be enforced and the exception is open-ended in ` +
178
+ `practice. Write an ISO-8601 UTC instant, e.g. \`EXPIRES: 2026-09-02T18:54Z\`.`);
179
+ }
180
+ else if (expiry.at.getTime() <= now) {
181
+ failures.push(`the minimumReleaseAgeExclude exception expired at ${expiry.at.toISOString()} and is ` +
182
+ `still in force for ${exceptions.map((e) => `\`${String(e)}\``).join(', ')}. ` +
183
+ `The pinned versions have aged past minimumReleaseAge, so pnpm will not re-add them: ` +
184
+ `exception expired; delete the entries.`);
185
+ }
186
+ }
187
+ for (const entry of exceptions) {
188
+ if (typeof entry !== 'string') {
189
+ failures.push(`minimumReleaseAgeExclude entry \`${String(entry)}\` is not a string, so pnpm cannot ` + `match it against a package.`);
190
+ continue;
191
+ }
192
+ const { name, range } = parseSelector(entry);
193
+ if (!validVersion(range)) {
194
+ failures.push(`minimumReleaseAgeExclude entry \`${entry}\` is not pinned to an exact version. ` +
195
+ `Scoped this way it exempts versions nobody has reviewed from the cooling-off — ` +
196
+ `write \`${name}@<exact version>\`.`);
197
+ continue;
198
+ }
199
+ const resolved = lock.resolvedVersions.get(name) ?? [];
200
+ // Peer-suffixed keys, `1.2.3(react@19.0.0)`: the base version counts.
201
+ if (!resolved.some((version) => version.split('(')[0] === range)) {
202
+ failures.push(`minimumReleaseAgeExclude entry \`${entry}\` is stale: the lockfile no longer resolves ` +
203
+ `${name}@${range}. The exception has outlived the pin it was taken for — delete the line.`);
204
+ }
205
+ }
206
+ return { failures, count: exceptions.length };
207
+ };
208
+ const stopped = (failures, notes) => ({ failures, notes, overrideCount: 0, patchCount: 0, versionsChecked: 0, releaseAgeExclusionCount: 0 });
209
+ /** Every check on the repository at `root`; `now` decides whether an expiry has passed. */
210
+ export const checkPnpmSettings = (root, config, now = Date.now()) => {
211
+ const failures = [];
212
+ const notes = [];
213
+ const pkgPath = join(root, 'package.json');
214
+ const wsPath = join(root, 'pnpm-workspace.yaml');
215
+ const lockPath = join(root, 'pnpm-lock.yaml');
216
+ for (const p of [pkgPath, wsPath, lockPath]) {
217
+ // Fail closed: a missing input is not a pass.
218
+ if (!existsSync(p))
219
+ return stopped([`required file missing: ${p}`], notes);
220
+ }
221
+ // 1. package.json carries no setting pnpm 11 ignores.
222
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
223
+ const stranded = IGNORED_IN_V11.filter((k) => pkg.pnpm?.[k] !== undefined);
224
+ if (stranded.length > 0) {
225
+ failures.push(`package.json#pnpm still declares ${stranded.map((s) => `\`${s}\``).join(', ')}. ` +
226
+ `pnpm 11 does not read these and will not warn. Move them to pnpm-workspace.yaml. ` +
227
+ `See https://github.com/pnpm/pnpm/issues/11536`);
228
+ }
229
+ let ws;
230
+ let wsText;
231
+ try {
232
+ wsText = readFileSync(wsPath, 'utf8');
233
+ ws = (load(wsText, { schema: CORE_SCHEMA }) ?? {});
234
+ }
235
+ catch (error) {
236
+ failures.push(`pnpm-workspace.yaml is not valid YAML, so no setting in it is being applied: ${String(error)}`);
237
+ return stopped(failures, notes);
238
+ }
239
+ let lock;
240
+ try {
241
+ lock = readLockfileFacts(readFileSync(lockPath, 'utf8'));
242
+ }
243
+ catch (error) {
244
+ failures.push(`pnpm-lock.yaml could not be parsed, so override and patch parity cannot be verified: ${String(error)}`);
245
+ return stopped(failures, notes);
246
+ }
247
+ // 2. Override parity, workspace and lockfile.
248
+ const declared = ws.overrides ?? {};
249
+ const declaredKeys = Object.keys(declared);
250
+ const lockedKeys = Object.keys(lock.overrides);
251
+ if (declaredKeys.length > 0 && lockedKeys.length === 0) {
252
+ failures.push(`pnpm-workspace.yaml declares ${declaredKeys.length} overrides but pnpm-lock.yaml records none. ` +
253
+ `This is the signature of pnpm ignoring the settings source entirely. ` +
254
+ `Re-run \`pnpm install --lockfile-only\` and confirm the lockfile's overrides: block is populated.`);
255
+ }
256
+ for (const [selector, target] of Object.entries(declared)) {
257
+ if (!(selector in lock.overrides)) {
258
+ failures.push(`override \`${selector}\` is declared in pnpm-workspace.yaml but absent from the lockfile.`);
259
+ }
260
+ else if (lock.overrides[selector] !== target) {
261
+ failures.push(`override \`${selector}\` disagrees: workspace says \`${target}\`, lockfile says \`${lock.overrides[selector]}\`.`);
262
+ }
263
+ }
264
+ for (const selector of lockedKeys) {
265
+ if (!(selector in declared))
266
+ failures.push(`override \`${selector}\` is in the lockfile but no longer declared in pnpm-workspace.yaml.`);
267
+ }
268
+ // 3. Patch parity, and the patch files exist and are the ones applied.
269
+ const declaredPatches = ws.patchedDependencies ?? {};
270
+ for (const [key, patchPath] of Object.entries(declaredPatches)) {
271
+ const record = lock.patchedDependencies[key];
272
+ if (!(key in lock.patchedDependencies) || record === undefined) {
273
+ failures.push(`patch \`${key}\` is declared in pnpm-workspace.yaml but absent from the lockfile — it is not being applied.`);
274
+ continue;
275
+ }
276
+ // pnpm 10 recorded a path, pnpm 11 only the hash: compare a path only when there is one.
277
+ if (record.path !== undefined && record.path !== patchPath) {
278
+ failures.push(`patch \`${key}\` path disagrees: workspace says \`${patchPath}\`, lockfile says \`${record.path}\`.`);
279
+ }
280
+ const absolutePatch = join(root, patchPath);
281
+ if (!existsSync(absolutePatch)) {
282
+ failures.push(`patch file for \`${key}\` does not exist on disk: ${patchPath}`);
283
+ continue;
284
+ }
285
+ if (record.hash !== undefined) {
286
+ const actual = createHash('sha256').update(readFileSync(absolutePatch)).digest('hex');
287
+ if (actual !== record.hash) {
288
+ failures.push(`patch \`${key}\` content does not match the lockfile: ${patchPath} hashes to ` +
289
+ `${actual.slice(0, 16)}… but the lockfile recorded ${record.hash.slice(0, 16)}…. ` +
290
+ `Re-run \`pnpm patch-commit\` so the applied patch and the committed file agree.`);
291
+ }
292
+ }
293
+ }
294
+ for (const key of Object.keys(lock.patchedDependencies)) {
295
+ if (!(key in declaredPatches))
296
+ failures.push(`patch \`${key}\` is in the lockfile but no longer declared in pnpm-workspace.yaml.`);
297
+ }
298
+ // 4. Each override took effect on every resolved version. `tar@<=7.5.20`
299
+ // names the vulnerable range, so no resolved version may be inside it;
300
+ // bare `tar` forces every version to its target, which each must satisfy.
301
+ let versionsChecked = 0;
302
+ for (const selector of declaredKeys) {
303
+ const { name, range } = parseSelector(selector);
304
+ const resolved = lock.resolvedVersions.get(name);
305
+ if (!resolved || resolved.length === 0)
306
+ continue;
307
+ const isBareName = !selector.slice(1).includes('@');
308
+ const assertion = isBareName ? declared[selector] : range;
309
+ if (!validRange(assertion)) {
310
+ notes.push(`override \`${selector}\` has no parseable semver range (\`${assertion}\`); ` + `version compliance not checked.`);
311
+ continue;
312
+ }
313
+ for (const version of resolved) {
314
+ const base = version.split('(')[0] ?? '';
315
+ if (!validRange(base))
316
+ continue;
317
+ versionsChecked++;
318
+ const inAssertedRange = satisfies(base, assertion);
319
+ if (isBareName && !inAssertedRange) {
320
+ failures.push(`override \`${selector}\` did not take effect: ${name}@${base} is resolved, ` + `but the override forces every version to \`${assertion}\`.`);
321
+ }
322
+ else if (!isBareName && inAssertedRange) {
323
+ failures.push(`override \`${selector}\` did not take effect: ${name}@${base} is still resolved, ` + `and it falls inside the excluded range \`${assertion}\`.`);
324
+ }
325
+ }
326
+ }
327
+ // 5. Cooling-off exceptions are scoped, live and decided.
328
+ const exclusions = checkReleaseAgeExclusions(wsText, ws.minimumReleaseAgeExclude, lock, config.ownScopes, now);
329
+ failures.push(...exclusions.failures);
330
+ return { failures, notes, overrideCount: declaredKeys.length, patchCount: Object.keys(declaredPatches).length, versionsChecked, releaseAgeExclusionCount: exclusions.count };
331
+ };
332
+ /** The report as the original printed it: notes and failures on stderr, the clean line on stdout. */
333
+ export const formatParity = (report, id) => {
334
+ const err = [];
335
+ if (report.notes.length > 0) {
336
+ err.push(`⚠️ ${id}: ${report.notes.length} selector(s) not version-checked:`, ...report.notes.map((n) => ` - ${n}`));
337
+ }
338
+ if (report.failures.length > 0) {
339
+ err.push(`\n❌ ${id}: pnpm settings parity FAILED (${report.failures.length} problem(s))\n`, ...report.failures.map((f) => ` • ${f}\n`));
340
+ err.push('A successful `pnpm install` is not evidence here — pnpm 11 drops these settings without warning.\n');
341
+ return { ok: false, out: [], err };
342
+ }
343
+ const extra = `${report.releaseAgeExclusionCount > 0 ? `, ${report.releaseAgeExclusionCount} documented cooling-off exception(s)` : ''}` +
344
+ `${report.notes.length > 0 ? `, ${report.notes.length} selector(s) unchecked` : ''}`;
345
+ return {
346
+ ok: true,
347
+ out: [`✅ ${id}: pnpm settings parity OK — ${report.overrideCount} override(s), ${report.patchCount} patch(es), ${report.versionsChecked} resolved version(s) verified${extra}.`],
348
+ err,
349
+ };
350
+ };
@@ -0,0 +1,320 @@
1
+ // Ported from semver 7.8.1 (npm `semver`, https://github.com/npm/node-semver,
2
+ // ISC licence): `classes/range.js` (Range, parseComparator, replaceCarets,
3
+ // replaceTildes, replaceXRanges, replaceStars, replaceGTE0, hyphenReplace,
4
+ // testSet), with the parts of `classes/comparator.js`, `classes/semver.js`,
5
+ // `internal/re.js`, `internal/identifiers.js` and `internal/constants.js` it
6
+ // uses. 7.8.1 is the version the replaced check ran on; later releases changed
7
+ // tilde and caret lower bounds under includePrerelease, so this is not to be
8
+ // "updated" to them without the check's owner. Its licence:
9
+ //
10
+ // The ISC License
11
+ //
12
+ // Copyright (c) Isaac Z. Schlueter and Contributors
13
+ //
14
+ // Permission to use, copy, modify, and/or distribute this software for any
15
+ // purpose with or without fee is hereby granted, provided that the above
16
+ // copyright notice and this permission notice appear in all copies.
17
+ //
18
+ // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
19
+ // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
20
+ // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
21
+ // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
22
+ // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
23
+ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
24
+ // IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
25
+ //
26
+ // A semver range matcher: the part of the `semver` package (strict mode)
27
+ // that pnpm-settings needs, `satisfies` with `includePrerelease: true`,
28
+ // `validRange` and `valid`, ported step for step so it reads every range the
29
+ // way `semver` does.
30
+ //
31
+ // A range is comparator sets joined by `||` (any set matches), each set
32
+ // comparators joined by spaces (every one matches). Before testing, `semver`
33
+ // rewrites the sugar into plain comparators, and so does this: hyphen ranges
34
+ // (`1.2.3 - 2.3`), carets (`^1.2.3`), tildes (`~1.2.3`, `~>1.2`), partial and
35
+ // x-range versions (`1`, `1.2`, `1.x`, `<8`, `>=0.9`), and stars (`*`).
36
+ // Build metadata is ignored. With includePrerelease a prerelease is just a
37
+ // version that sorts before its release, so `7.5.20-beta.1` is inside
38
+ // `<=7.5.20`, and a partial lower bound starts at `-0` (`>=1.2` is
39
+ // `>=1.2.0-0`).
40
+ //
41
+ // Not read: `semver`'s loose mode, which pnpm-settings never asks for.
42
+ /** semver's MAX_LENGTH: a longer version string is not one. */
43
+ const MAX_LENGTH = 256;
44
+ const MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6;
45
+ const LETTER_DASH_NUMBER = '[a-zA-Z0-9-]';
46
+ /** semver's makeSafeRegex: bounds its greedy repeats, which changes what a long input matches. */
47
+ const bounded = (source) => {
48
+ let out = source;
49
+ for (const [token, max] of [['\\s', 1], ['\\d', MAX_LENGTH], [LETTER_DASH_NUMBER, MAX_SAFE_BUILD_LENGTH]]) {
50
+ out = out.split(`${token}*`).join(`${token}{0,${max}}`).split(`${token}+`).join(`${token}{1,${max}}`);
51
+ }
52
+ return out;
53
+ };
54
+ const re = (source, flags) => new RegExp(bounded(source), flags);
55
+ const NUMERIC = '0|[1-9]\\d*';
56
+ const NUMERIC_LOOSE = '\\d+';
57
+ const NON_NUMERIC = `\\d*[a-zA-Z-]${LETTER_DASH_NUMBER}*`;
58
+ const MAIN = `(${NUMERIC})\\.(${NUMERIC})\\.(${NUMERIC})`;
59
+ const MAIN_LOOSE = `(${NUMERIC_LOOSE})\\.(${NUMERIC_LOOSE})\\.(${NUMERIC_LOOSE})`;
60
+ const PRERELEASE_ID = `(?:${NON_NUMERIC}|${NUMERIC})`;
61
+ const PRERELEASE_ID_LOOSE = `(?:${NON_NUMERIC}|${NUMERIC_LOOSE})`;
62
+ const PRERELEASE = `(?:-(${PRERELEASE_ID}(?:\\.${PRERELEASE_ID})*))`;
63
+ const PRERELEASE_LOOSE = `(?:-?(${PRERELEASE_ID_LOOSE}(?:\\.${PRERELEASE_ID_LOOSE})*))`;
64
+ const BUILD_ID = `${LETTER_DASH_NUMBER}+`;
65
+ const BUILD = `(?:\\+(${BUILD_ID}(?:\\.${BUILD_ID})*))`;
66
+ const FULL_PLAIN = `v?${MAIN}${PRERELEASE}?${BUILD}?`;
67
+ const LOOSE_PLAIN = `[v=\\s]*${MAIN_LOOSE}${PRERELEASE_LOOSE}?${BUILD}?`;
68
+ const GTLT = '((?:<|>)?=?)';
69
+ const X_ID = `${NUMERIC}|x|X|\\*`;
70
+ const X_PLAIN = `[v=\\s]*(${X_ID})(?:\\.(${X_ID})(?:\\.(${X_ID})(?:${PRERELEASE})?${BUILD}?)?)?`;
71
+ /** A version, as `new SemVer` reads one (after trimming). */
72
+ const FULL = re(`^${FULL_PLAIN}$`);
73
+ /** Build metadata anywhere in a set: `semver` strips it, with the unbounded pattern, before anything else. */
74
+ const BUILD_STRIP = new RegExp(BUILD, 'g');
75
+ const BUILD_ONE = re(BUILD);
76
+ const HYPHEN_RANGE = re(`^\\s*(${X_PLAIN})\\s+-\\s+(${X_PLAIN})\\s*$`);
77
+ /** `> 1.2.3` is `>1.2.3`. */
78
+ const COMPARATOR_TRIM = re(`(\\s*)${GTLT}\\s*(${LOOSE_PLAIN}|${X_PLAIN})`, 'g');
79
+ const TILDE_TRIM = re('(\\s*)(?:~>?)\\s+', 'g');
80
+ const CARET_TRIM = re('(\\s*)(?:\\^)\\s+', 'g');
81
+ const TILDE = re(`^(?:~>?)${X_PLAIN}$`);
82
+ const CARET = re(`^(?:\\^)${X_PLAIN}$`);
83
+ const X_RANGE = re(`^${GTLT}\\s*${X_PLAIN}$`);
84
+ const STAR = re('(<|>)?=?\\s*\\*');
85
+ /** `>=0.0.0-0` is every version, so `semver` drops it. */
86
+ const GTE0_PRERELEASE = re('^\\s*>=\\s*0\\.0\\.0-0\\s*$');
87
+ const COMPARATOR = re(`^${GTLT}\\s*(${FULL_PLAIN})$|^$`);
88
+ /** A full version, or undefined; trimmed first, as `semver` does. */
89
+ export const parseVersion = (text) => {
90
+ if (typeof text !== 'string' || text.length > MAX_LENGTH)
91
+ return undefined;
92
+ const m = FULL.exec(text.trim());
93
+ if (m === null)
94
+ return undefined;
95
+ const main = [Number(m[1]), Number(m[2]), Number(m[3])];
96
+ if (main.some((n) => n > Number.MAX_SAFE_INTEGER))
97
+ return undefined;
98
+ return { main, prerelease: m[4] === undefined ? [] : m[4].split('.') };
99
+ };
100
+ /** `semver.valid`: whether `text` is a full version. */
101
+ export const validVersion = (text) => parseVersion(text) !== undefined;
102
+ const NUMERIC_ID = /^[0-9]+$/;
103
+ /** semver's compareIdentifiers: numbers numerically and before words, words in ASCII order. */
104
+ export const compareIdentifiers = (a, b) => {
105
+ const aNum = NUMERIC_ID.test(a);
106
+ const bNum = NUMERIC_ID.test(b);
107
+ if (aNum && bNum) {
108
+ const [x, y] = [Number(a), Number(b)];
109
+ return x === y ? 0 : x < y ? -1 : 1;
110
+ }
111
+ if (a === b)
112
+ return 0;
113
+ if (aNum)
114
+ return -1;
115
+ if (bNum)
116
+ return 1;
117
+ return a < b ? -1 : 1;
118
+ };
119
+ /** Semver precedence; build metadata plays no part. */
120
+ export const compareVersions = (a, b) => {
121
+ for (let i = 0; i < 3; i++) {
122
+ const c = compareIdentifiers(String(a.main[i]), String(b.main[i]));
123
+ if (c !== 0)
124
+ return c;
125
+ }
126
+ if (a.prerelease.length > 0 && b.prerelease.length === 0)
127
+ return -1;
128
+ if (a.prerelease.length === 0 && b.prerelease.length > 0)
129
+ return 1;
130
+ for (let i = 0;; i++) {
131
+ const x = a.prerelease[i];
132
+ const y = b.prerelease[i];
133
+ if (x === undefined && y === undefined)
134
+ return 0;
135
+ if (y === undefined)
136
+ return 1;
137
+ if (x === undefined)
138
+ return -1;
139
+ if (x !== y)
140
+ return compareIdentifiers(x, y);
141
+ }
142
+ };
143
+ /** semver's isX: a missing part, `x`, `X` or `*`. */
144
+ const isX = (id) => id === undefined || id.toLowerCase() === 'x' || id === '*';
145
+ const next = (id) => Number(id) + 1;
146
+ /** `1.2 - 3.4` is `>=1.2.0-0 <3.5.0-0`; a full upper bound `3.4.5` is `<3.4.6-0`. */
147
+ const hyphenReplace = (...g) => {
148
+ const [, fromText, fM, fm, fp, fpr, , toText, tM, tm, tp, tpr] = g;
149
+ let from;
150
+ if (isX(fM))
151
+ from = '';
152
+ else if (isX(fm))
153
+ from = `>=${fM}.0.0-0`;
154
+ else if (isX(fp))
155
+ from = `>=${fM}.${fm}.0-0`;
156
+ else if (fpr)
157
+ from = `>=${fromText}`;
158
+ else
159
+ from = `>=${fromText}-0`;
160
+ let to;
161
+ if (isX(tM))
162
+ to = '';
163
+ else if (isX(tm))
164
+ to = `<${next(tM)}.0.0-0`;
165
+ else if (isX(tp))
166
+ to = `<${tM}.${next(tm)}.0-0`;
167
+ else if (tpr)
168
+ to = `<=${tM}.${tm}.${tp}-${tpr}`;
169
+ else
170
+ to = `<${tM}.${tm}.${next(tp)}-0`;
171
+ return `${from} ${to}`.trim();
172
+ };
173
+ /** `^1.2.3` is `>=1.2.3-0 <2.0.0-0`; `^0.1.2` stops at `0.2.0-0`, `^0.0.1` at `0.0.2-0`. */
174
+ const replaceCaret = (comp) => comp.replace(CARET, (...g) => {
175
+ const [, M, m, p, pr] = g;
176
+ if (isX(M))
177
+ return '';
178
+ if (isX(m))
179
+ return `>=${M}.0.0-0 <${next(M)}.0.0-0`;
180
+ if (isX(p))
181
+ return M === '0' ? `>=${M}.${m}.0-0 <${M}.${next(m)}.0-0` : `>=${M}.${m}.0-0 <${next(M)}.0.0-0`;
182
+ if (pr) {
183
+ if (M === '0')
184
+ return m === '0' ? `>=${M}.${m}.${p}-${pr} <${M}.${m}.${next(p)}-0` : `>=${M}.${m}.${p}-${pr} <${M}.${next(m)}.0-0`;
185
+ return `>=${M}.${m}.${p}-${pr} <${next(M)}.0.0-0`;
186
+ }
187
+ if (M === '0')
188
+ return m === '0' ? `>=${M}.${m}.${p}-0 <${M}.${m}.${next(p)}-0` : `>=${M}.${m}.${p}-0 <${M}.${next(m)}.0-0`;
189
+ return `>=${M}.${m}.${p} <${next(M)}.0.0-0`;
190
+ });
191
+ /** `~1.2.3` is `>=1.2.3 <1.3.0-0`; `~1` is `>=1.0.0 <2.0.0-0`. No `-0` on the lower bound, as in `semver`. */
192
+ const replaceTilde = (comp) => comp.replace(TILDE, (...g) => {
193
+ const [, M, m, p, pr] = g;
194
+ if (isX(M))
195
+ return '';
196
+ if (isX(m))
197
+ return `>=${M}.0.0 <${next(M)}.0.0-0`;
198
+ if (isX(p))
199
+ return `>=${M}.${m}.0 <${M}.${next(m)}.0-0`;
200
+ if (pr)
201
+ return `>=${M}.${m}.${p}-${pr} <${M}.${next(m)}.0-0`;
202
+ return `>=${M}.${m}.${p} <${M}.${next(m)}.0-0`;
203
+ });
204
+ /** `<8` is `<8.0.0-0`, `>1.2` is `>=1.3.0-0`, `1.2` is `>=1.2.0-0 <1.3.0-0`, `>*` is nothing. */
205
+ const replaceXRange = (comp) => comp.trim().replace(X_RANGE, (whole, gtltText, MText, mText, pText) => {
206
+ let gtlt = gtltText;
207
+ let [M, m, p] = [MText, mText, pText];
208
+ const xM = isX(MText);
209
+ const xm = xM || isX(mText);
210
+ const xp = xm || isX(pText);
211
+ if (gtlt === '=' && xp)
212
+ gtlt = '';
213
+ // includePrerelease: every bound starts at the lowest prerelease.
214
+ const pr = '-0';
215
+ if (xM)
216
+ return gtlt === '>' || gtlt === '<' ? '<0.0.0-0' : '*';
217
+ if (gtlt && xp) {
218
+ if (xm)
219
+ m = 0;
220
+ p = 0;
221
+ if (gtlt === '>') {
222
+ gtlt = '>=';
223
+ if (xm) {
224
+ M = next(M);
225
+ m = 0;
226
+ }
227
+ else
228
+ m = next(m);
229
+ p = 0;
230
+ }
231
+ else if (gtlt === '<=') {
232
+ gtlt = '<';
233
+ if (xm)
234
+ M = next(M);
235
+ else
236
+ m = next(m);
237
+ }
238
+ return `${gtlt}${M}.${m}.${p}${pr}`;
239
+ }
240
+ if (xm)
241
+ return `>=${M}.0.0${pr} <${next(M)}.0.0-0`;
242
+ if (xp)
243
+ return `>=${M}.${m}.0${pr} <${M}.${next(m)}.0-0`;
244
+ return whole;
245
+ });
246
+ /** semver's parseComparator: one space-free piece of a set to plain comparators. */
247
+ const desugar = (piece) => {
248
+ let comp = piece.replace(BUILD_ONE, '');
249
+ comp = comp.trim().split(/\s+/).map(replaceCaret).join(' ');
250
+ comp = comp.trim().split(/\s+/).map(replaceTilde).join(' ');
251
+ comp = comp.split(/\s+/).map(replaceXRange).join(' ');
252
+ return comp.trim().replace(STAR, '');
253
+ };
254
+ /** semver's Comparator constructor, or undefined where it throws. */
255
+ const comparator = (text) => {
256
+ const m = COMPARATOR.exec(text.trim().split(/\s+/).join(' '));
257
+ if (m === null)
258
+ return undefined;
259
+ const operator = (m[1] === undefined || m[1] === '=' ? '' : m[1]);
260
+ if (!m[2])
261
+ return { operator };
262
+ const version = parseVersion(m[2]);
263
+ return version === undefined ? undefined : { operator, version };
264
+ };
265
+ /** One `||` part as plain comparators, or undefined where `semver` throws. */
266
+ const parseSet = (part) => {
267
+ let text = part.replace(BUILD_STRIP, '');
268
+ text = text.replace(HYPHEN_RANGE, hyphenReplace);
269
+ text = text.replace(COMPARATOR_TRIM, '$1$2$3');
270
+ text = text.replace(TILDE_TRIM, '$1~');
271
+ text = text.replace(CARET_TRIM, '$1^');
272
+ const pieces = text
273
+ .split(' ')
274
+ .map(desugar)
275
+ .join(' ')
276
+ .split(/\s+/)
277
+ .map((c) => c.trim().replace(GTE0_PRERELEASE, ''));
278
+ const set = pieces.map(comparator);
279
+ return set.every((c) => c !== undefined) ? set : undefined;
280
+ };
281
+ /** The range's comparator sets, or undefined when `semver` refuses it. */
282
+ export const parseRange = (range) => {
283
+ if (typeof range !== 'string')
284
+ return undefined;
285
+ const sets = [];
286
+ for (const part of range.trim().replace(/\s+/g, ' ').split('||')) {
287
+ const set = parseSet(part.trim());
288
+ if (set === undefined)
289
+ return undefined;
290
+ sets.push(set);
291
+ }
292
+ return sets;
293
+ };
294
+ /** `semver.validRange`, as a yes or no. */
295
+ export const validRange = (range) => parseRange(range) !== undefined;
296
+ const test = (c, v) => {
297
+ if (c.version === undefined)
298
+ return true;
299
+ const order = compareVersions(v, c.version);
300
+ switch (c.operator) {
301
+ case '<':
302
+ return order < 0;
303
+ case '<=':
304
+ return order <= 0;
305
+ case '>':
306
+ return order > 0;
307
+ case '>=':
308
+ return order >= 0;
309
+ default:
310
+ return order === 0;
311
+ }
312
+ };
313
+ /** `semver.satisfies(version, range, { includePrerelease: true })`; false when either does not parse. */
314
+ export const satisfies = (version, range) => {
315
+ const sets = parseRange(range);
316
+ const v = parseVersion(version);
317
+ if (sets === undefined || v === undefined)
318
+ return false;
319
+ return sets.some((set) => set.every((c) => test(c, v)));
320
+ };