• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts {
3    // https://semver.org/#spec-item-2
4    // > A normal version number MUST take the form X.Y.Z where X, Y, and Z are non-negative
5    // > integers, and MUST NOT contain leading zeroes. X is the major version, Y is the minor
6    // > version, and Z is the patch version. Each element MUST increase numerically.
7    //
8    // NOTE: We differ here in that we allow X and X.Y, with missing parts having the default
9    // value of `0`.
10    const versionRegExp = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*)(?:\-([a-z0-9-.]+))?(?:\+([a-z0-9-.]+))?)?)?$/i;
11
12    // https://semver.org/#spec-item-9
13    // > A pre-release version MAY be denoted by appending a hyphen and a series of dot separated
14    // > identifiers immediately following the patch version. Identifiers MUST comprise only ASCII
15    // > alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers
16    // > MUST NOT include leading zeroes.
17    const prereleaseRegExp = /^(?:0|[1-9]\d*|[a-z-][a-z0-9-]*)(?:\.(?:0|[1-9]\d*|[a-z-][a-z0-9-]*))*$/i;
18    const prereleasePartRegExp = /^(?:0|[1-9]\d*|[a-z-][a-z0-9-]*)$/i;
19
20    // https://semver.org/#spec-item-10
21    // > Build metadata MAY be denoted by appending a plus sign and a series of dot separated
22    // > identifiers immediately following the patch or pre-release version. Identifiers MUST
23    // > comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty.
24    const buildRegExp = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i;
25    const buildPartRegExp = /^[a-z0-9-]+$/i;
26
27    // https://semver.org/#spec-item-9
28    // > Numeric identifiers MUST NOT include leading zeroes.
29    const numericIdentifierRegExp = /^(0|[1-9]\d*)$/;
30
31    /**
32     * Describes a precise semantic version number, https://semver.org
33     */
34    export class Version {
35        static readonly zero = new Version(0, 0, 0, ["0"]);
36
37        readonly major: number;
38        readonly minor: number;
39        readonly patch: number;
40        readonly prerelease: readonly string[];
41        readonly build: readonly string[];
42
43        constructor(text: string);
44        constructor(major: number, minor?: number, patch?: number, prerelease?: string | readonly string[], build?: string | readonly string[]);
45        constructor(major: number | string, minor = 0, patch = 0, prerelease: string | readonly string[] = "", build: string | readonly string[] = "") {
46            if (typeof major === "string") {
47                const result = Debug.checkDefined(tryParseComponents(major), "Invalid version");
48                ({ major, minor, patch, prerelease, build } = result);
49            }
50
51            Debug.assert(major >= 0, "Invalid argument: major");
52            Debug.assert(minor >= 0, "Invalid argument: minor");
53            Debug.assert(patch >= 0, "Invalid argument: patch");
54
55            const prereleaseArray = prerelease ? isArray(prerelease) ? prerelease : prerelease.split(".") : emptyArray;
56            const buildArray = build ? isArray(build) ? build : build.split(".") : emptyArray;
57
58            Debug.assert(every(prereleaseArray, s => prereleasePartRegExp.test(s)), "Invalid argument: prerelease");
59            Debug.assert(every(buildArray, s => buildPartRegExp.test(s)), "Invalid argument: build");
60
61            this.major = major;
62            this.minor = minor;
63            this.patch = patch;
64            this.prerelease = prereleaseArray;
65            this.build = buildArray;
66        }
67
68        static tryParse(text: string) {
69            const result = tryParseComponents(text);
70            if (!result) return undefined;
71
72            const { major, minor, patch, prerelease, build } = result;
73            return new Version(major, minor, patch, prerelease, build);
74        }
75
76        compareTo(other: Version | undefined) {
77            // https://semver.org/#spec-item-11
78            // > Precedence is determined by the first difference when comparing each of these
79            // > identifiers from left to right as follows: Major, minor, and patch versions are
80            // > always compared numerically.
81            //
82            // https://semver.org/#spec-item-11
83            // > Precedence for two pre-release versions with the same major, minor, and patch version
84            // > MUST be determined by comparing each dot separated identifier from left to right until
85            // > a difference is found [...]
86            //
87            // https://semver.org/#spec-item-11
88            // > Build metadata does not figure into precedence
89            if (this === other) return Comparison.EqualTo;
90            if (other === undefined) return Comparison.GreaterThan;
91            return compareValues(this.major, other.major)
92                || compareValues(this.minor, other.minor)
93                || compareValues(this.patch, other.patch)
94                || comparePrereleaseIdentifiers(this.prerelease, other.prerelease);
95        }
96
97        increment(field: "major" | "minor" | "patch") {
98            switch (field) {
99                case "major": return new Version(this.major + 1, 0, 0);
100                case "minor": return new Version(this.major, this.minor + 1, 0);
101                case "patch": return new Version(this.major, this.minor, this.patch + 1);
102                default: return Debug.assertNever(field);
103            }
104        }
105
106        with(fields: { major?: number, minor?: number, patch?: number, prerelease?: string | readonly string[], build?: string | readonly string[] }) {
107            const {
108                major = this.major,
109                minor = this.minor,
110                patch = this.patch,
111                prerelease = this.prerelease,
112                build = this.build
113            } = fields;
114            return new Version(major, minor, patch, prerelease, build);
115        }
116
117        toString() {
118            let result = `${this.major}.${this.minor}.${this.patch}`;
119            if (some(this.prerelease)) result += `-${this.prerelease.join(".")}`;
120            if (some(this.build)) result += `+${this.build.join(".")}`;
121            return result;
122        }
123    }
124
125    function tryParseComponents(text: string) {
126        const match = versionRegExp.exec(text);
127        if (!match) return undefined;
128
129        const [, major, minor = "0", patch = "0", prerelease = "", build = ""] = match;
130        if (prerelease && !prereleaseRegExp.test(prerelease)) return undefined;
131        if (build && !buildRegExp.test(build)) return undefined;
132        return {
133            major: parseInt(major, 10),
134            minor: parseInt(minor, 10),
135            patch: parseInt(patch, 10),
136            prerelease,
137            build
138        };
139    }
140
141    function comparePrereleaseIdentifiers(left: readonly string[], right: readonly string[]) {
142        // https://semver.org/#spec-item-11
143        // > When major, minor, and patch are equal, a pre-release version has lower precedence
144        // > than a normal version.
145        if (left === right) return Comparison.EqualTo;
146        if (left.length === 0) return right.length === 0 ? Comparison.EqualTo : Comparison.GreaterThan;
147        if (right.length === 0) return Comparison.LessThan;
148
149        // https://semver.org/#spec-item-11
150        // > Precedence for two pre-release versions with the same major, minor, and patch version
151        // > MUST be determined by comparing each dot separated identifier from left to right until
152        // > a difference is found [...]
153        const length = Math.min(left.length, right.length);
154        for (let i = 0; i < length; i++) {
155            const leftIdentifier = left[i];
156            const rightIdentifier = right[i];
157            if (leftIdentifier === rightIdentifier) continue;
158
159            const leftIsNumeric = numericIdentifierRegExp.test(leftIdentifier);
160            const rightIsNumeric = numericIdentifierRegExp.test(rightIdentifier);
161            if (leftIsNumeric || rightIsNumeric) {
162                // https://semver.org/#spec-item-11
163                // > Numeric identifiers always have lower precedence than non-numeric identifiers.
164                if (leftIsNumeric !== rightIsNumeric) return leftIsNumeric ? Comparison.LessThan : Comparison.GreaterThan;
165
166                // https://semver.org/#spec-item-11
167                // > identifiers consisting of only digits are compared numerically
168                const result = compareValues(+leftIdentifier, +rightIdentifier);
169                if (result) return result;
170            }
171            else {
172                // https://semver.org/#spec-item-11
173                // > identifiers with letters or hyphens are compared lexically in ASCII sort order.
174                const result = compareStringsCaseSensitive(leftIdentifier, rightIdentifier);
175                if (result) return result;
176            }
177        }
178
179        // https://semver.org/#spec-item-11
180        // > A larger set of pre-release fields has a higher precedence than a smaller set, if all
181        // > of the preceding identifiers are equal.
182        return compareValues(left.length, right.length);
183    }
184
185    /**
186     * Describes a semantic version range, per https://github.com/npm/node-semver#ranges
187     */
188    export class VersionRange {
189        private _alternatives: readonly (readonly Comparator[])[];
190
191        constructor(spec: string) {
192            this._alternatives = spec ? Debug.checkDefined(parseRange(spec), "Invalid range spec.") : emptyArray;
193        }
194
195        static tryParse(text: string) {
196            const sets = parseRange(text);
197            if (sets) {
198                const range = new VersionRange("");
199                range._alternatives = sets;
200                return range;
201            }
202            return undefined;
203        }
204
205        /**
206         * Tests whether a version matches the range. This is equivalent to `satisfies(version, range, { includePrerelease: true })`.
207         * in `node-semver`.
208         */
209        test(version: Version | string) {
210            if (typeof version === "string") version = new Version(version);
211            return testDisjunction(version, this._alternatives);
212        }
213
214        toString() {
215            return formatDisjunction(this._alternatives);
216        }
217    }
218
219    interface Comparator {
220        readonly operator: "<" | "<=" | ">" | ">=" | "=";
221        readonly operand: Version;
222    }
223
224    // https://github.com/npm/node-semver#range-grammar
225    //
226    // range-set    ::= range ( logical-or range ) *
227    // range        ::= hyphen | simple ( ' ' simple ) * | ''
228    // logical-or   ::= ( ' ' ) * '||' ( ' ' ) *
229    const logicalOrRegExp = /\|\|/g;
230    const whitespaceRegExp = /\s+/g;
231
232    // https://github.com/npm/node-semver#range-grammar
233    //
234    // partial      ::= xr ( '.' xr ( '.' xr qualifier ? )? )?
235    // xr           ::= 'x' | 'X' | '*' | nr
236    // nr           ::= '0' | ['1'-'9'] ( ['0'-'9'] ) *
237    // qualifier    ::= ( '-' pre )? ( '+' build )?
238    // pre          ::= parts
239    // build        ::= parts
240    // parts        ::= part ( '.' part ) *
241    // part         ::= nr | [-0-9A-Za-z]+
242    const partialRegExp = /^([xX*0]|[1-9]\d*)(?:\.([xX*0]|[1-9]\d*)(?:\.([xX*0]|[1-9]\d*)(?:-([a-z0-9-.]+))?(?:\+([a-z0-9-.]+))?)?)?$/i;
243
244    // https://github.com/npm/node-semver#range-grammar
245    //
246    // hyphen       ::= partial ' - ' partial
247    const hyphenRegExp = /^\s*([a-z0-9-+.*]+)\s+-\s+([a-z0-9-+.*]+)\s*$/i;
248
249    // https://github.com/npm/node-semver#range-grammar
250    //
251    // simple       ::= primitive | partial | tilde | caret
252    // primitive    ::= ( '<' | '>' | '>=' | '<=' | '=' ) partial
253    // tilde        ::= '~' partial
254    // caret        ::= '^' partial
255    const rangeRegExp = /^(~|\^|<|<=|>|>=|=)?\s*([a-z0-9-+.*]+)$/i;
256
257    function parseRange(text: string) {
258        const alternatives: Comparator[][] = [];
259        for (let range of trimString(text).split(logicalOrRegExp)) {
260            if (!range) continue;
261            const comparators: Comparator[] = [];
262            range = trimString(range);
263            const match = hyphenRegExp.exec(range);
264            if (match) {
265                if (!parseHyphen(match[1], match[2], comparators)) return undefined;
266            }
267            else {
268                for (const simple of range.split(whitespaceRegExp)) {
269                    const match = rangeRegExp.exec(trimString(simple));
270                    if (!match || !parseComparator(match[1], match[2], comparators)) return undefined;
271                }
272            }
273            alternatives.push(comparators);
274        }
275        return alternatives;
276    }
277
278    function parsePartial(text: string) {
279        const match = partialRegExp.exec(text);
280        if (!match) return undefined;
281
282        const [, major, minor = "*", patch = "*", prerelease, build] = match;
283        const version = new Version(
284            isWildcard(major) ? 0 : parseInt(major, 10),
285            isWildcard(major) || isWildcard(minor) ? 0 : parseInt(minor, 10),
286            isWildcard(major) || isWildcard(minor) || isWildcard(patch) ? 0 : parseInt(patch, 10),
287            prerelease,
288            build);
289
290        return { version, major, minor, patch };
291    }
292
293    function parseHyphen(left: string, right: string, comparators: Comparator[]) {
294        const leftResult = parsePartial(left);
295        if (!leftResult) return false;
296
297        const rightResult = parsePartial(right);
298        if (!rightResult) return false;
299
300        if (!isWildcard(leftResult.major)) {
301            comparators.push(createComparator(">=", leftResult.version));
302        }
303
304        if (!isWildcard(rightResult.major)) {
305            comparators.push(
306                isWildcard(rightResult.minor) ? createComparator("<", rightResult.version.increment("major")) :
307                isWildcard(rightResult.patch) ? createComparator("<", rightResult.version.increment("minor")) :
308                createComparator("<=", rightResult.version));
309        }
310
311        return true;
312    }
313
314    function parseComparator(operator: string, text: string, comparators: Comparator[]) {
315        const result = parsePartial(text);
316        if (!result) return false;
317
318        const { version, major, minor, patch } = result;
319        if (!isWildcard(major)) {
320            switch (operator) {
321                case "~":
322                    comparators.push(createComparator(">=", version));
323                    comparators.push(createComparator("<", version.increment(
324                        isWildcard(minor) ? "major" :
325                        "minor")));
326                    break;
327                case "^":
328                    comparators.push(createComparator(">=", version));
329                    comparators.push(createComparator("<", version.increment(
330                        version.major > 0 || isWildcard(minor) ? "major" :
331                        version.minor > 0 || isWildcard(patch) ? "minor" :
332                        "patch")));
333                    break;
334                case "<":
335                case ">=":
336                    comparators.push(
337                        isWildcard(minor) || isWildcard(patch) ? createComparator(operator, version.with({ prerelease: "0" })) :
338                        createComparator(operator, version));
339                    break;
340                case "<=":
341                case ">":
342                    comparators.push(
343                        isWildcard(minor) ? createComparator(operator === "<=" ? "<" : ">=", version.increment("major").with({ prerelease: "0" })) :
344                        isWildcard(patch) ? createComparator(operator === "<=" ? "<" : ">=", version.increment("minor").with({ prerelease: "0" })) :
345                        createComparator(operator, version));
346                    break;
347                case "=":
348                case undefined:
349                    if (isWildcard(minor) || isWildcard(patch)) {
350                        comparators.push(createComparator(">=", version.with({ prerelease: "0" })));
351                        comparators.push(createComparator("<", version.increment(isWildcard(minor) ? "major" : "minor").with({ prerelease: "0" })));
352                    }
353                    else {
354                        comparators.push(createComparator("=", version));
355                    }
356                    break;
357                default:
358                    // unrecognized
359                    return false;
360            }
361        }
362        else if (operator === "<" || operator === ">") {
363            comparators.push(createComparator("<", Version.zero));
364        }
365
366        return true;
367    }
368
369    function isWildcard(part: string) {
370        return part === "*" || part === "x" || part === "X";
371    }
372
373    function createComparator(operator: Comparator["operator"], operand: Version) {
374        return { operator, operand };
375    }
376
377    function testDisjunction(version: Version, alternatives: readonly (readonly Comparator[])[]) {
378        // an empty disjunction is treated as "*" (all versions)
379        if (alternatives.length === 0) return true;
380        for (const alternative of alternatives) {
381            if (testAlternative(version, alternative)) return true;
382        }
383        return false;
384    }
385
386    function testAlternative(version: Version, comparators: readonly Comparator[]) {
387        for (const comparator of comparators) {
388            if (!testComparator(version, comparator.operator, comparator.operand)) return false;
389        }
390        return true;
391    }
392
393    function testComparator(version: Version, operator: Comparator["operator"], operand: Version) {
394        const cmp = version.compareTo(operand);
395        switch (operator) {
396            case "<": return cmp < 0;
397            case "<=": return cmp <= 0;
398            case ">": return cmp > 0;
399            case ">=": return cmp >= 0;
400            case "=": return cmp === 0;
401            default: return Debug.assertNever(operator);
402        }
403    }
404
405    function formatDisjunction(alternatives: readonly (readonly Comparator[])[]) {
406        return map(alternatives, formatAlternative).join(" || ") || "*";
407    }
408
409    function formatAlternative(comparators: readonly Comparator[]) {
410        return map(comparators, formatComparator).join(" ");
411    }
412
413    function formatComparator(comparator: Comparator) {
414        return `${comparator.operator}${comparator.operand}`;
415    }
416}