Skip to content
Source
Sign inSign up
Open History to see the last commit

semver.ts

227 lines6.7 KB

1interface ParsedSemver {
2 readonly major: number;
3 readonly minor: number;
4 readonly patch: number;
5 readonly prerelease: ReadonlyArray<string>;
6}
7​
8const SEMVER_NUMBER_SEGMENT = /^\d+$/;
9​
10export function normalizeSemverVersion(version: string): string {
11 const [versionWithoutBuildMetadata = ""] = version.trim().split("+", 1);
12 const [main, ...prereleaseParts] = versionWithoutBuildMetadata.split("-");
13 const prerelease = prereleaseParts.join("-");
14 const segments: string[] = [];
15 for (const segment of (main ?? "").split(".")) {
16 const trimmed = segment.trim();
17 if (trimmed.length > 0) {
18 segments.push(trimmed);
19 }
20 }
21​
22 while (segments.length > 0 && segments.length < 3) {
23 segments.push("0");
24 }
25​
26 return prerelease ? `${segments.join(".")}-${prerelease}` : segments.join(".");
28​
29export function parseSemver(value: string): ParsedSemver | null {
30 const normalized = normalizeSemverVersion(value).replace(/^v/, "");
31 const [main = "", ...prereleaseParts] = normalized.split("-");
32 const prerelease = prereleaseParts.join("-");
33 const segments = main.split(".");
34 if (segments.length !== 3) {
35 return null;
36 }
37​
38 const [majorSegment, minorSegment, patchSegment] = segments;
39 if (majorSegment === undefined || minorSegment === undefined || patchSegment === undefined) {
40 return null;
41 }
42 if (
43 !SEMVER_NUMBER_SEGMENT.test(majorSegment) ||
44 !SEMVER_NUMBER_SEGMENT.test(minorSegment) ||
45 !SEMVER_NUMBER_SEGMENT.test(patchSegment)
46 ) {
47 return null;
48 }
49​
50 const major = Number.parseInt(majorSegment, 10);
51 const minor = Number.parseInt(minorSegment, 10);
52 const patch = Number.parseInt(patchSegment, 10);
53 if (![major, minor, patch].every(Number.isInteger)) {
54 return null;
55 }
56​
57 return {
58 major,
59 minor,
60 patch,
61 prerelease: parsePrereleaseSegments(prerelease),
62 };
64​
65function parsePrereleaseSegments(prerelease: string | undefined): ReadonlyArray<string> {
66 if (prerelease === undefined) {
67 return [];
68 }
69 const segments: string[] = [];
70 for (const segment of prerelease.split(".")) {
71 const trimmed = segment.trim();
72 if (trimmed.length > 0) {
73 segments.push(trimmed);
74 }
75 }
76 return segments;
78​
79function comparePrereleaseIdentifier(left: string, right: string): number {
80 const leftNumeric = SEMVER_NUMBER_SEGMENT.test(left);
81 const rightNumeric = SEMVER_NUMBER_SEGMENT.test(right);
82​
83 if (leftNumeric && rightNumeric) {
84 return Number.parseInt(left, 10) - Number.parseInt(right, 10);
85 }
86 if (leftNumeric) {
87 return -1;
88 }
89 if (rightNumeric) {
90 return 1;
91 }
92 return left.localeCompare(right);
94​
95export function compareSemverVersions(left: string, right: string): number {
96 const parsedLeft = parseSemver(left);
97 const parsedRight = parseSemver(right);
98 if (!parsedLeft || !parsedRight) {
99 return left.localeCompare(right);
100 }
101​
102 if (parsedLeft.major !== parsedRight.major) {
103 return parsedLeft.major - parsedRight.major;
104 }
105 if (parsedLeft.minor !== parsedRight.minor) {
106 return parsedLeft.minor - parsedRight.minor;
107 }
108 if (parsedLeft.patch !== parsedRight.patch) {
109 return parsedLeft.patch - parsedRight.patch;
110 }
111​
112 if (parsedLeft.prerelease.length === 0 && parsedRight.prerelease.length === 0) {
113 return 0;
114 }
115 if (parsedLeft.prerelease.length === 0) {
116 return 1;
117 }
118 if (parsedRight.prerelease.length === 0) {
119 return -1;
120 }
121​
122 const length = Math.max(parsedLeft.prerelease.length, parsedRight.prerelease.length);
123 for (let index = 0; index < length; index += 1) {
124 const leftIdentifier = parsedLeft.prerelease[index];
125 const rightIdentifier = parsedRight.prerelease[index];
126 if (leftIdentifier === undefined) {
127 return -1;
128 }
129 if (rightIdentifier === undefined) {
130 return 1;
131 }
132 const comparison = comparePrereleaseIdentifier(leftIdentifier, rightIdentifier);
133 if (comparison !== 0) {
134 return comparison;
135 }
136 }
137​
138 return 0;
140​
141/** Keep plain JavaScript: SSH startup stringifies this for remote Node versions without known TS support. */
142export const satisfiesSemverRange: (rawVersion: string, range: string) => boolean =
143 function satisfiesSemverRange(rawVersion, range) {
144 const normalizedVersion = String(rawVersion).trim().replace(/^v/, "");
145 const versionMatch = normalizedVersion.match(
146 /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-[0-9A-Za-z.-]+)?$/,
147 );
148 if (!versionMatch) {
149 return false;
150 }
151​
152 const version = {
153 major: Number(versionMatch[1]),
154 minor: Number(versionMatch[2] || 0),
155 patch: Number(versionMatch[3] || 0),
156 };
157​
158 return range.split("||").some((group) => {
159 const comparators = group
160 .trim()
161 .replace(/(\^|>=|>|<=|<|=)\s+(?=v?\d)/g, "$1")
162 .split(/\s+/)
163 .filter(Boolean);
164 if (comparators.length === 0) {
165 return false;
166 }
167 return comparators.every((comparator) => {
168 const match = comparator.trim().match(/^(\^|>=|>|<=|<|=)?\s*v?(\d+(?:\.\d+){0,2})$/);
169 if (!match) {
170 return false;
171 }
172 const targetVersion = match[2];
173 if (targetVersion === undefined) {
174 return false;
175 }
176 const targetMatch = targetVersion.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
177 if (!targetMatch) {
178 return false;
179 }
180 const target = {
181 major: Number(targetMatch[1]),
182 minor: Number(targetMatch[2] || 0),
183 patch: Number(targetMatch[3] || 0),
184 };
185 const compared =
186 version.major !== target.major
187 ? version.major > target.major
188 ? 1
189 : -1
190 : version.minor !== target.minor
191 ? version.minor > target.minor
192 ? 1
193 : -1
194 : version.patch !== target.patch
195 ? version.patch > target.patch
196 ? 1
197 : -1
198 : 0;
199 const operator = match[1] || "=";
200 switch (operator) {
201 case "^":
202 if (compared < 0) {
203 return false;
204 }
205 if (target.major > 0 || targetMatch[2] === undefined) {
206 return version.major === target.major;
207 }
208 if (target.minor > 0 || targetMatch[3] === undefined) {
209 return version.major === 0 && version.minor === target.minor;
210 }
211 return version.major === 0 && version.minor === 0 && version.patch === target.patch;
212 case ">=":
213 return compared >= 0;
214 case ">":
215 return compared > 0;
216 case "<=":
217 return compared <= 0;
218 case "<":
219 return compared < 0;
220 case "=":
221 return compared === 0;
222 default:
223 return false;
224 }
225 });
226 });
227 };