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