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}