@credentum/semver-explain 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/index.d.ts +31 -0
- package/index.js +363 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# semver-explain
|
|
2
|
+
|
|
3
|
+
Explain semver ranges in plain English. The `cronstrue` for version constraints.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { explain } from '@credentum/semver-explain';
|
|
7
|
+
|
|
8
|
+
explain('^1.2.3');
|
|
9
|
+
// => ">=1.2.3 <2.0.0 — Compatible with 1.2.3, allowing minor and patch updates"
|
|
10
|
+
|
|
11
|
+
explain('~1.2.3');
|
|
12
|
+
// => ">=1.2.3 <1.3.0 — Approximately 1.2.3, allowing only patch updates"
|
|
13
|
+
|
|
14
|
+
explain('1.2.x');
|
|
15
|
+
// => ">=1.2.0 <1.3.0 — Any patch version within 1.2"
|
|
16
|
+
|
|
17
|
+
explain('>=1.0.0 <2.0.0');
|
|
18
|
+
// => ">=1.0.0 and <2.0.0"
|
|
19
|
+
|
|
20
|
+
explain('^1.2.3 || ^2.0.0');
|
|
21
|
+
// => ">=1.2.3 <2.0.0 — Compatible with 1.2.3 | OR | >=2.0.0 <3.0.0 — Compatible with 2.0.0"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @credentum/semver-explain
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Why?
|
|
31
|
+
|
|
32
|
+
Every team using npm encounters semver ranges. The `^` and `~` operators are unintuitive — even experienced developers confuse them. The answer is always a StackOverflow search:
|
|
33
|
+
|
|
34
|
+
- ["What's the difference between tilde(~) and caret(^)?"](https://stackoverflow.com/questions/22343224) — **2.1M views**
|
|
35
|
+
- ["What does ^0.0.1 mean?"](https://stackoverflow.com/questions/22137778) — 200K+ views
|
|
36
|
+
- `semver` on npm — 498M weekly downloads, but no explain function
|
|
37
|
+
|
|
38
|
+
`cronstrue` converts cron to English (3.5M/week). No equivalent exists for semver. Until now.
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
### `explain(range: string): string`
|
|
43
|
+
|
|
44
|
+
Converts a semver range string into a human-readable English explanation.
|
|
45
|
+
|
|
46
|
+
**Supports all npm semver range syntax:**
|
|
47
|
+
|
|
48
|
+
| Input | Output |
|
|
49
|
+
|-------|--------|
|
|
50
|
+
| `1.2.3` | `1.2.3 — Exactly version 1.2.3` |
|
|
51
|
+
| `^1.2.3` | `>=1.2.3 <2.0.0 — Compatible with 1.2.3, allowing minor and patch updates` |
|
|
52
|
+
| `^0.2.3` | `>=0.2.3 <0.3.0 — Compatible with 0.2.3, allowing only patch updates` |
|
|
53
|
+
| `^0.0.3` | `>=0.0.3 <0.0.4 — Only version 0.0.3 (caret on 0.0.x is exact)` |
|
|
54
|
+
| `~1.2.3` | `>=1.2.3 <1.3.0 — Approximately 1.2.3, allowing only patch updates` |
|
|
55
|
+
| `~1.2` | `>=1.2.0 <1.3.0 — Any patch version within 1.2` |
|
|
56
|
+
| `~0.2` | `>=0.2.0 <0.3.0 — Any patch version within 0.2` |
|
|
57
|
+
| `1.x` | `>=1.0.0 <2.0.0 — Any version starting with 1` |
|
|
58
|
+
| `1.2.x` | `>=1.2.0 <1.3.0 — Any patch version within 1.2` |
|
|
59
|
+
| `*` | `Any version` |
|
|
60
|
+
| `>=1.2.3` | `>=1.2.3` |
|
|
61
|
+
| `1.2.3 - 2.3.4` | `>=1.2.3 <=2.3.4 — Between 1.2.3 and 2.3.4, inclusive` |
|
|
62
|
+
| `>=1.0.0 <2.0.0` | `>=1.0.0 and <2.0.0` |
|
|
63
|
+
| `^1 \|\| ^2` | Explains each branch separated by ` \| OR \| ` |
|
|
64
|
+
|
|
65
|
+
### `expandRange(range: string): string`
|
|
66
|
+
|
|
67
|
+
Returns only the expanded comparator form without the English description. Useful for tooling.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { expandRange } from '@credentum/semver-explain';
|
|
71
|
+
|
|
72
|
+
expandRange('^1.2.3'); // ">=1.2.3 <2.0.0"
|
|
73
|
+
expandRange('~1.2.3'); // ">=1.2.3 <1.3.0"
|
|
74
|
+
expandRange('1.2.x'); // ">=1.2.0 <1.3.0"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Zero Dependencies
|
|
78
|
+
|
|
79
|
+
Pure TypeScript. No dependency on the `semver` package. Implements range expansion from the [node-semver specification](https://github.com/npm/node-semver#ranges) directly.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semver-explain — Explain semver ranges in plain English.
|
|
3
|
+
* Zero dependencies. Pure functions.
|
|
4
|
+
*
|
|
5
|
+
* Implements range expansion per the node-semver specification:
|
|
6
|
+
* https://github.com/npm/node-semver#ranges
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Explain a semver range in plain English.
|
|
10
|
+
*
|
|
11
|
+
* Supports all npm semver range syntax:
|
|
12
|
+
* - Exact: 1.2.3
|
|
13
|
+
* - Caret: ^1.2.3
|
|
14
|
+
* - Tilde: ~1.2.3
|
|
15
|
+
* - X-ranges: 1.x, 1.2.*, *
|
|
16
|
+
* - Hyphen: 1.2.3 - 2.3.4
|
|
17
|
+
* - Comparators: >=1.0.0, <2.0.0
|
|
18
|
+
* - Compound: >=1.0.0 <2.0.0
|
|
19
|
+
* - Union: ^1 || ^2
|
|
20
|
+
*
|
|
21
|
+
* @param range - A semver range string
|
|
22
|
+
* @returns Human-readable explanation
|
|
23
|
+
*/
|
|
24
|
+
export declare function explain(range: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Return only the expanded comparator form, without the English description.
|
|
27
|
+
*
|
|
28
|
+
* @param range - A semver range string
|
|
29
|
+
* @returns Expanded comparator string (e.g. ">=1.2.3 <2.0.0")
|
|
30
|
+
*/
|
|
31
|
+
export declare function expandRange(range: string): string;
|
package/index.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* semver-explain — Explain semver ranges in plain English.
|
|
3
|
+
* Zero dependencies. Pure functions.
|
|
4
|
+
*
|
|
5
|
+
* Implements range expansion per the node-semver specification:
|
|
6
|
+
* https://github.com/npm/node-semver#ranges
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Parse a version string like "1.2.3" into a Version object.
|
|
10
|
+
* Missing components default to 0.
|
|
11
|
+
*/
|
|
12
|
+
function parseVersion(v) {
|
|
13
|
+
const parts = v.split(".");
|
|
14
|
+
return {
|
|
15
|
+
major: parseInt(parts[0], 10) || 0,
|
|
16
|
+
minor: parts.length > 1 ? parseInt(parts[1], 10) || 0 : 0,
|
|
17
|
+
patch: parts.length > 2 ? parseInt(parts[2], 10) || 0 : 0,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Format a Version back to string */
|
|
21
|
+
function fmt(v) {
|
|
22
|
+
return `${v.major}.${v.minor}.${v.patch}`;
|
|
23
|
+
}
|
|
24
|
+
/** Count how many numeric parts were provided (1, 2, or 3) */
|
|
25
|
+
function partCount(v) {
|
|
26
|
+
const stripped = v.replace(/^[v=\s]+/, "");
|
|
27
|
+
return stripped.split(".").length;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Expand a caret range (^) to its comparator bounds.
|
|
31
|
+
*
|
|
32
|
+
* Per node-semver spec:
|
|
33
|
+
* - ^1.2.3 => >=1.2.3 <2.0.0 (leftmost non-zero is major)
|
|
34
|
+
* - ^0.2.3 => >=0.2.3 <0.3.0 (leftmost non-zero is minor)
|
|
35
|
+
* - ^0.0.3 => >=0.0.3 <0.0.4 (leftmost non-zero is patch)
|
|
36
|
+
* - ^1.2.x => >=1.2.0 <2.0.0
|
|
37
|
+
* - ^0.0.x => >=0.0.0 <0.1.0
|
|
38
|
+
* - ^0.0 => >=0.0.0 <0.1.0
|
|
39
|
+
* - ^1.x => >=1.0.0 <2.0.0
|
|
40
|
+
* - ^0.x => >=0.0.0 <1.0.0
|
|
41
|
+
*/
|
|
42
|
+
function expandCaret(raw) {
|
|
43
|
+
const body = raw.replace(/^[v=\s]+/, "");
|
|
44
|
+
const parts = body.split(".");
|
|
45
|
+
const hasX = parts.some((p) => p === "x" || p === "X" || p === "*");
|
|
46
|
+
const numParts = parts.length;
|
|
47
|
+
// Replace x/X/* with 0 for parsing
|
|
48
|
+
const cleaned = parts.map((p) => p === "x" || p === "X" || p === "*" ? "0" : p);
|
|
49
|
+
const major = parseInt(cleaned[0], 10) || 0;
|
|
50
|
+
const minor = numParts > 1 ? parseInt(cleaned[1], 10) || 0 : 0;
|
|
51
|
+
const patch = numParts > 2 ? parseInt(cleaned[2], 10) || 0 : 0;
|
|
52
|
+
const lower = `${major}.${minor}.${patch}`;
|
|
53
|
+
let upper;
|
|
54
|
+
let description;
|
|
55
|
+
if (major !== 0) {
|
|
56
|
+
upper = `${major + 1}.0.0`;
|
|
57
|
+
if (minor === 0 && patch === 0) {
|
|
58
|
+
description = `Compatible with ${major}.x, allowing minor and patch updates`;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
description = `Compatible with ${lower}, allowing minor and patch updates`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (minor !== 0) {
|
|
65
|
+
upper = `0.${minor + 1}.0`;
|
|
66
|
+
description = `Compatible with ${lower}, allowing only patch updates`;
|
|
67
|
+
}
|
|
68
|
+
else if (patch !== 0 && !hasX) {
|
|
69
|
+
upper = `0.0.${patch + 1}`;
|
|
70
|
+
description = `Only version ${lower} (caret on 0.0.x is exact)`;
|
|
71
|
+
}
|
|
72
|
+
else if (hasX && numParts >= 2 && cleaned[1] === "0") {
|
|
73
|
+
// ^0.0.x or ^0.0
|
|
74
|
+
upper = "0.1.0";
|
|
75
|
+
description = `Any patch version within 0.0`;
|
|
76
|
+
}
|
|
77
|
+
else if (hasX || numParts === 1) {
|
|
78
|
+
upper = `${major + 1}.0.0`;
|
|
79
|
+
description = `Any version starting with ${major}`;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// ^0.0.0
|
|
83
|
+
upper = "0.1.0";
|
|
84
|
+
description = `Any patch version within 0.0`;
|
|
85
|
+
}
|
|
86
|
+
return { lower, upper, description };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Expand a tilde range (~) to its comparator bounds.
|
|
90
|
+
*
|
|
91
|
+
* Per node-semver spec:
|
|
92
|
+
* - ~1.2.3 => >=1.2.3 <1.3.0 (patch-level changes allowed)
|
|
93
|
+
* - ~1.2 => >=1.2.0 <1.3.0 (same as 1.2.x)
|
|
94
|
+
* - ~1 => >=1.0.0 <2.0.0 (same as 1.x)
|
|
95
|
+
* - ~0.2.3 => >=0.2.3 <0.3.0
|
|
96
|
+
*/
|
|
97
|
+
function expandTilde(raw) {
|
|
98
|
+
const body = raw.replace(/^[v=\s]+/, "");
|
|
99
|
+
const parts = body.split(".");
|
|
100
|
+
const numParts = parts.length;
|
|
101
|
+
const major = parseInt(parts[0], 10) || 0;
|
|
102
|
+
const minor = numParts > 1 ? parseInt(parts[1], 10) || 0 : 0;
|
|
103
|
+
const patch = numParts > 2 ? parseInt(parts[2], 10) || 0 : 0;
|
|
104
|
+
const lower = `${major}.${minor}.${patch}`;
|
|
105
|
+
if (numParts === 1) {
|
|
106
|
+
// ~1 => >=1.0.0 <2.0.0
|
|
107
|
+
return {
|
|
108
|
+
lower,
|
|
109
|
+
upper: `${major + 1}.0.0`,
|
|
110
|
+
description: `Any version starting with ${major}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ~1.2.3 or ~1.2 => >=1.2.x <1.3.0
|
|
114
|
+
const upper = `${major}.${minor + 1}.0`;
|
|
115
|
+
if (patch === 0) {
|
|
116
|
+
return {
|
|
117
|
+
lower,
|
|
118
|
+
upper,
|
|
119
|
+
description: `Any patch version within ${major}.${minor}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
lower,
|
|
124
|
+
upper,
|
|
125
|
+
description: `Approximately ${lower}, allowing only patch updates`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Expand an x-range (1.x, 1.2.x, *, x) to its comparator bounds.
|
|
130
|
+
*/
|
|
131
|
+
function expandXRange(raw) {
|
|
132
|
+
const body = raw.replace(/^[v=\s]+/, "").trim();
|
|
133
|
+
// Pure wildcard
|
|
134
|
+
if (body === "*" || body === "x" || body === "X" || body === "") {
|
|
135
|
+
return { lower: "", upper: "", description: "Any version" };
|
|
136
|
+
}
|
|
137
|
+
const parts = body.split(".");
|
|
138
|
+
const major = parseInt(parts[0], 10);
|
|
139
|
+
if (isNaN(major)) {
|
|
140
|
+
return { lower: "", upper: "", description: "Any version" };
|
|
141
|
+
}
|
|
142
|
+
// 1.x or 1.*
|
|
143
|
+
if (parts.length === 1 ||
|
|
144
|
+
(parts.length >= 2 &&
|
|
145
|
+
(parts[1] === "x" || parts[1] === "X" || parts[1] === "*"))) {
|
|
146
|
+
return {
|
|
147
|
+
lower: `${major}.0.0`,
|
|
148
|
+
upper: `${major + 1}.0.0`,
|
|
149
|
+
description: `Any version starting with ${major}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// 1.2.x or 1.2.*
|
|
153
|
+
const minor = parseInt(parts[1], 10) || 0;
|
|
154
|
+
if (parts.length >= 3 &&
|
|
155
|
+
(parts[2] === "x" || parts[2] === "X" || parts[2] === "*")) {
|
|
156
|
+
return {
|
|
157
|
+
lower: `${major}.${minor}.0`,
|
|
158
|
+
upper: `${major}.${minor + 1}.0`,
|
|
159
|
+
description: `Any patch version within ${major}.${minor}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Shouldn't reach here, but handle gracefully
|
|
163
|
+
return {
|
|
164
|
+
lower: `${major}.${minor}.0`,
|
|
165
|
+
upper: `${major}.${minor + 1}.0`,
|
|
166
|
+
description: `Any patch version within ${major}.${minor}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Expand a hyphen range (A - B) to its comparator bounds.
|
|
171
|
+
*
|
|
172
|
+
* Per node-semver spec:
|
|
173
|
+
* - 1.2.3 - 2.3.4 => >=1.2.3 <=2.3.4
|
|
174
|
+
* - 1.2 - 2.3.4 => >=1.2.0 <=2.3.4
|
|
175
|
+
* - 1.2.3 - 2.3 => >=1.2.3 <2.4.0
|
|
176
|
+
* - 1.2.3 - 2 => >=1.2.3 <3.0.0
|
|
177
|
+
*/
|
|
178
|
+
function expandHyphen(left, right) {
|
|
179
|
+
const lBody = left.replace(/^[v=\s]+/, "").trim();
|
|
180
|
+
const rBody = right.replace(/^[v=\s]+/, "").trim();
|
|
181
|
+
const lv = parseVersion(lBody);
|
|
182
|
+
const rv = parseVersion(rBody);
|
|
183
|
+
const rParts = partCount(rBody);
|
|
184
|
+
const lower = fmt(lv);
|
|
185
|
+
if (rParts === 3) {
|
|
186
|
+
// Full version on right: inclusive upper bound
|
|
187
|
+
return {
|
|
188
|
+
expanded: `>=${lower} <=${fmt(rv)}`,
|
|
189
|
+
description: `Between ${lower} and ${fmt(rv)}, inclusive`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// Partial version on right: exclusive upper bound on next increment
|
|
193
|
+
let upper;
|
|
194
|
+
if (rParts === 2) {
|
|
195
|
+
upper = `${rv.major}.${rv.minor + 1}.0`;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
upper = `${rv.major + 1}.0.0`;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
expanded: `>=${lower} <${upper}`,
|
|
202
|
+
description: `Between ${lower} and ${rBody}`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Check if a string is a simple comparator (>=, <=, >, <, =).
|
|
207
|
+
*/
|
|
208
|
+
function isComparator(token) {
|
|
209
|
+
return /^(>=|<=|>|<|=)\s*\d/.test(token.trim());
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Explain a single simple range (no OR unions).
|
|
213
|
+
* This handles: exact, caret, tilde, x-range, hyphen, and comparators.
|
|
214
|
+
*/
|
|
215
|
+
function explainSimple(range) {
|
|
216
|
+
const trimmed = range.trim();
|
|
217
|
+
if (trimmed === "" || trimmed === "*") {
|
|
218
|
+
return "Any version";
|
|
219
|
+
}
|
|
220
|
+
// Hyphen range: "A - B"
|
|
221
|
+
const hyphenMatch = trimmed.match(/^([v=\s]*[\dx.*X]+(?:\.[\dx.*X]+)*)\s+-\s+([v=\s]*[\dx.*X]+(?:\.[\dx.*X]+)*)$/);
|
|
222
|
+
if (hyphenMatch) {
|
|
223
|
+
const { expanded, description } = expandHyphen(hyphenMatch[1], hyphenMatch[2]);
|
|
224
|
+
return `${expanded} \u2014 ${description}`;
|
|
225
|
+
}
|
|
226
|
+
// Caret range
|
|
227
|
+
if (trimmed.startsWith("^")) {
|
|
228
|
+
const body = trimmed.slice(1);
|
|
229
|
+
const { lower, upper, description } = expandCaret(body);
|
|
230
|
+
return `>=${lower} <${upper} \u2014 ${description}`;
|
|
231
|
+
}
|
|
232
|
+
// Tilde range
|
|
233
|
+
if (trimmed.startsWith("~")) {
|
|
234
|
+
const body = trimmed.slice(1);
|
|
235
|
+
const { lower, upper, description } = expandTilde(body);
|
|
236
|
+
return `>=${lower} <${upper} \u2014 ${description}`;
|
|
237
|
+
}
|
|
238
|
+
// X-range (contains x, X, or * in version parts)
|
|
239
|
+
if (/[xX*]/.test(trimmed) || /^\d+$/.test(trimmed)) {
|
|
240
|
+
// Single number like "1" is treated as 1.x.x
|
|
241
|
+
if (/^\d+$/.test(trimmed)) {
|
|
242
|
+
const major = parseInt(trimmed, 10);
|
|
243
|
+
return `>=${major}.0.0 <${major + 1}.0.0 \u2014 Any version starting with ${major}`;
|
|
244
|
+
}
|
|
245
|
+
const { lower, upper, description } = expandXRange(trimmed);
|
|
246
|
+
if (lower === "" && upper === "") {
|
|
247
|
+
return description;
|
|
248
|
+
}
|
|
249
|
+
return `>=${lower} <${upper} \u2014 ${description}`;
|
|
250
|
+
}
|
|
251
|
+
// Compound comparators (space-separated, e.g. ">=1.0.0 <2.0.0")
|
|
252
|
+
const tokens = trimmed.split(/\s+/);
|
|
253
|
+
if (tokens.length > 1 && tokens.every((t) => isComparator(t))) {
|
|
254
|
+
return tokens.join(" and ");
|
|
255
|
+
}
|
|
256
|
+
// Single comparator (>=, <=, >, <, =)
|
|
257
|
+
if (isComparator(trimmed)) {
|
|
258
|
+
const normalized = trimmed.replace(/^=\s*/, "");
|
|
259
|
+
if (trimmed.startsWith("=")) {
|
|
260
|
+
return `${normalized} \u2014 Exactly version ${normalized}`;
|
|
261
|
+
}
|
|
262
|
+
return trimmed;
|
|
263
|
+
}
|
|
264
|
+
// Exact version (e.g. "1.2.3")
|
|
265
|
+
if (/^\d+\.\d+\.\d+/.test(trimmed)) {
|
|
266
|
+
return `${trimmed} \u2014 Exactly version ${trimmed}`;
|
|
267
|
+
}
|
|
268
|
+
// Partial version without x (e.g. "1.2")
|
|
269
|
+
if (/^\d+\.\d+$/.test(trimmed)) {
|
|
270
|
+
const major = parseInt(trimmed.split(".")[0], 10);
|
|
271
|
+
const minor = parseInt(trimmed.split(".")[1], 10);
|
|
272
|
+
return `>=${major}.${minor}.0 <${major}.${minor + 1}.0 \u2014 Any patch version within ${trimmed}`;
|
|
273
|
+
}
|
|
274
|
+
// Fallback: return as-is
|
|
275
|
+
return trimmed;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Explain a semver range in plain English.
|
|
279
|
+
*
|
|
280
|
+
* Supports all npm semver range syntax:
|
|
281
|
+
* - Exact: 1.2.3
|
|
282
|
+
* - Caret: ^1.2.3
|
|
283
|
+
* - Tilde: ~1.2.3
|
|
284
|
+
* - X-ranges: 1.x, 1.2.*, *
|
|
285
|
+
* - Hyphen: 1.2.3 - 2.3.4
|
|
286
|
+
* - Comparators: >=1.0.0, <2.0.0
|
|
287
|
+
* - Compound: >=1.0.0 <2.0.0
|
|
288
|
+
* - Union: ^1 || ^2
|
|
289
|
+
*
|
|
290
|
+
* @param range - A semver range string
|
|
291
|
+
* @returns Human-readable explanation
|
|
292
|
+
*/
|
|
293
|
+
export function explain(range) {
|
|
294
|
+
if (typeof range !== "string") {
|
|
295
|
+
return "Invalid range";
|
|
296
|
+
}
|
|
297
|
+
const trimmed = range.trim();
|
|
298
|
+
if (trimmed === "") {
|
|
299
|
+
return "Any version";
|
|
300
|
+
}
|
|
301
|
+
// Split on || for OR unions
|
|
302
|
+
const branches = trimmed.split(/\s*\|\|\s*/);
|
|
303
|
+
if (branches.length === 1) {
|
|
304
|
+
return explainSimple(branches[0]);
|
|
305
|
+
}
|
|
306
|
+
// Multiple branches: explain each, join with OR
|
|
307
|
+
return branches.map((b) => explainSimple(b)).join(" | OR | ");
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Return only the expanded comparator form, without the English description.
|
|
311
|
+
*
|
|
312
|
+
* @param range - A semver range string
|
|
313
|
+
* @returns Expanded comparator string (e.g. ">=1.2.3 <2.0.0")
|
|
314
|
+
*/
|
|
315
|
+
export function expandRange(range) {
|
|
316
|
+
if (typeof range !== "string") {
|
|
317
|
+
return "Invalid range";
|
|
318
|
+
}
|
|
319
|
+
const trimmed = range.trim();
|
|
320
|
+
if (trimmed === "" || trimmed === "*") {
|
|
321
|
+
return "*";
|
|
322
|
+
}
|
|
323
|
+
// Split on || for OR unions
|
|
324
|
+
const branches = trimmed.split(/\s*\|\|\s*/);
|
|
325
|
+
const expanded = branches.map((branch) => {
|
|
326
|
+
const b = branch.trim();
|
|
327
|
+
// Hyphen range
|
|
328
|
+
const hyphenMatch = b.match(/^([v=\s]*[\dx.*X]+(?:\.[\dx.*X]+)*)\s+-\s+([v=\s]*[\dx.*X]+(?:\.[\dx.*X]+)*)$/);
|
|
329
|
+
if (hyphenMatch) {
|
|
330
|
+
return expandHyphen(hyphenMatch[1], hyphenMatch[2]).expanded;
|
|
331
|
+
}
|
|
332
|
+
// Caret
|
|
333
|
+
if (b.startsWith("^")) {
|
|
334
|
+
const { lower, upper } = expandCaret(b.slice(1));
|
|
335
|
+
return `>=${lower} <${upper}`;
|
|
336
|
+
}
|
|
337
|
+
// Tilde
|
|
338
|
+
if (b.startsWith("~")) {
|
|
339
|
+
const { lower, upper } = expandTilde(b.slice(1));
|
|
340
|
+
return `>=${lower} <${upper}`;
|
|
341
|
+
}
|
|
342
|
+
// X-range
|
|
343
|
+
if (/[xX*]/.test(b)) {
|
|
344
|
+
const { lower, upper } = expandXRange(b);
|
|
345
|
+
if (lower === "" && upper === "")
|
|
346
|
+
return "*";
|
|
347
|
+
return `>=${lower} <${upper}`;
|
|
348
|
+
}
|
|
349
|
+
// Single number (1 => 1.x.x)
|
|
350
|
+
if (/^\d+$/.test(b)) {
|
|
351
|
+
const major = parseInt(b, 10);
|
|
352
|
+
return `>=${major}.0.0 <${major + 1}.0.0`;
|
|
353
|
+
}
|
|
354
|
+
// Partial version (1.2 => 1.2.x)
|
|
355
|
+
if (/^\d+\.\d+$/.test(b)) {
|
|
356
|
+
const [maj, min] = b.split(".").map(Number);
|
|
357
|
+
return `>=${maj}.${min}.0 <${maj}.${min + 1}.0`;
|
|
358
|
+
}
|
|
359
|
+
// Already a comparator or exact version
|
|
360
|
+
return b;
|
|
361
|
+
});
|
|
362
|
+
return expanded.join(" || ");
|
|
363
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@credentum/semver-explain",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Explain semver ranges in plain English. The cronstrue for version constraints.",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"index.d.ts"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"semver",
|
|
14
|
+
"explain",
|
|
15
|
+
"human-readable",
|
|
16
|
+
"version",
|
|
17
|
+
"range",
|
|
18
|
+
"caret",
|
|
19
|
+
"tilde",
|
|
20
|
+
"npm",
|
|
21
|
+
"package.json"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"typescript": "^5.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|