1// hoisted class for cyclic dependency 2class Range { 3 constructor (range, options) { 4 options = parseOptions(options) 5 6 if (range instanceof Range) { 7 if ( 8 range.loose === !!options.loose && 9 range.includePrerelease === !!options.includePrerelease 10 ) { 11 return range 12 } else { 13 return new Range(range.raw, options) 14 } 15 } 16 17 if (range instanceof Comparator) { 18 // just put it in the set and return 19 this.raw = range.value 20 this.set = [[range]] 21 this.format() 22 return this 23 } 24 25 this.options = options 26 this.loose = !!options.loose 27 this.includePrerelease = !!options.includePrerelease 28 29 // First reduce all whitespace as much as possible so we do not have to rely 30 // on potentially slow regexes like \s*. This is then stored and used for 31 // future error messages as well. 32 this.raw = range 33 .trim() 34 .split(/\s+/) 35 .join(' ') 36 37 // First, split on || 38 this.set = this.raw 39 .split('||') 40 // map the range to a 2d array of comparators 41 .map(r => this.parseRange(r.trim())) 42 // throw out any comparator lists that are empty 43 // this generally means that it was not a valid range, which is allowed 44 // in loose mode, but will still throw if the WHOLE range is invalid. 45 .filter(c => c.length) 46 47 if (!this.set.length) { 48 throw new TypeError(`Invalid SemVer Range: ${this.raw}`) 49 } 50 51 // if we have any that are not the null set, throw out null sets. 52 if (this.set.length > 1) { 53 // keep the first one, in case they're all null sets 54 const first = this.set[0] 55 this.set = this.set.filter(c => !isNullSet(c[0])) 56 if (this.set.length === 0) { 57 this.set = [first] 58 } else if (this.set.length > 1) { 59 // if we have any that are *, then the range is just * 60 for (const c of this.set) { 61 if (c.length === 1 && isAny(c[0])) { 62 this.set = [c] 63 break 64 } 65 } 66 } 67 } 68 69 this.format() 70 } 71 72 format () { 73 this.range = this.set 74 .map((comps) => comps.join(' ').trim()) 75 .join('||') 76 .trim() 77 return this.range 78 } 79 80 toString () { 81 return this.range 82 } 83 84 parseRange (range) { 85 // memoize range parsing for performance. 86 // this is a very hot path, and fully deterministic. 87 const memoOpts = 88 (this.options.includePrerelease && FLAG_INCLUDE_PRERELEASE) | 89 (this.options.loose && FLAG_LOOSE) 90 const memoKey = memoOpts + ':' + range 91 const cached = cache.get(memoKey) 92 if (cached) { 93 return cached 94 } 95 96 const loose = this.options.loose 97 // `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4` 98 const hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE] 99 range = range.replace(hr, hyphenReplace(this.options.includePrerelease)) 100 debug('hyphen replace', range) 101 102 // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` 103 range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace) 104 debug('comparator trim', range) 105 106 // `~ 1.2.3` => `~1.2.3` 107 range = range.replace(re[t.TILDETRIM], tildeTrimReplace) 108 debug('tilde trim', range) 109 110 // `^ 1.2.3` => `^1.2.3` 111 range = range.replace(re[t.CARETTRIM], caretTrimReplace) 112 debug('caret trim', range) 113 114 // At this point, the range is completely trimmed and 115 // ready to be split into comparators. 116 117 let rangeList = range 118 .split(' ') 119 .map(comp => parseComparator(comp, this.options)) 120 .join(' ') 121 .split(/\s+/) 122 // >=0.0.0 is equivalent to * 123 .map(comp => replaceGTE0(comp, this.options)) 124 125 if (loose) { 126 // in loose mode, throw out any that are not valid comparators 127 rangeList = rangeList.filter(comp => { 128 debug('loose invalid filter', comp, this.options) 129 return !!comp.match(re[t.COMPARATORLOOSE]) 130 }) 131 } 132 debug('range list', rangeList) 133 134 // if any comparators are the null set, then replace with JUST null set 135 // if more than one comparator, remove any * comparators 136 // also, don't include the same comparator more than once 137 const rangeMap = new Map() 138 const comparators = rangeList.map(comp => new Comparator(comp, this.options)) 139 for (const comp of comparators) { 140 if (isNullSet(comp)) { 141 return [comp] 142 } 143 rangeMap.set(comp.value, comp) 144 } 145 if (rangeMap.size > 1 && rangeMap.has('')) { 146 rangeMap.delete('') 147 } 148 149 const result = [...rangeMap.values()] 150 cache.set(memoKey, result) 151 return result 152 } 153 154 intersects (range, options) { 155 if (!(range instanceof Range)) { 156 throw new TypeError('a Range is required') 157 } 158 159 return this.set.some((thisComparators) => { 160 return ( 161 isSatisfiable(thisComparators, options) && 162 range.set.some((rangeComparators) => { 163 return ( 164 isSatisfiable(rangeComparators, options) && 165 thisComparators.every((thisComparator) => { 166 return rangeComparators.every((rangeComparator) => { 167 return thisComparator.intersects(rangeComparator, options) 168 }) 169 }) 170 ) 171 }) 172 ) 173 }) 174 } 175 176 // if ANY of the sets match ALL of its comparators, then pass 177 test (version) { 178 if (!version) { 179 return false 180 } 181 182 if (typeof version === 'string') { 183 try { 184 version = new SemVer(version, this.options) 185 } catch (er) { 186 return false 187 } 188 } 189 190 for (let i = 0; i < this.set.length; i++) { 191 if (testSet(this.set[i], version, this.options)) { 192 return true 193 } 194 } 195 return false 196 } 197} 198 199module.exports = Range 200 201const LRU = require('lru-cache') 202const cache = new LRU({ max: 1000 }) 203 204const parseOptions = require('../internal/parse-options') 205const Comparator = require('./comparator') 206const debug = require('../internal/debug') 207const SemVer = require('./semver') 208const { 209 safeRe: re, 210 t, 211 comparatorTrimReplace, 212 tildeTrimReplace, 213 caretTrimReplace, 214} = require('../internal/re') 215const { FLAG_INCLUDE_PRERELEASE, FLAG_LOOSE } = require('../internal/constants') 216 217const isNullSet = c => c.value === '<0.0.0-0' 218const isAny = c => c.value === '' 219 220// take a set of comparators and determine whether there 221// exists a version which can satisfy it 222const isSatisfiable = (comparators, options) => { 223 let result = true 224 const remainingComparators = comparators.slice() 225 let testComparator = remainingComparators.pop() 226 227 while (result && remainingComparators.length) { 228 result = remainingComparators.every((otherComparator) => { 229 return testComparator.intersects(otherComparator, options) 230 }) 231 232 testComparator = remainingComparators.pop() 233 } 234 235 return result 236} 237 238// comprised of xranges, tildes, stars, and gtlt's at this point. 239// already replaced the hyphen ranges 240// turn into a set of JUST comparators. 241const parseComparator = (comp, options) => { 242 debug('comp', comp, options) 243 comp = replaceCarets(comp, options) 244 debug('caret', comp) 245 comp = replaceTildes(comp, options) 246 debug('tildes', comp) 247 comp = replaceXRanges(comp, options) 248 debug('xrange', comp) 249 comp = replaceStars(comp, options) 250 debug('stars', comp) 251 return comp 252} 253 254const isX = id => !id || id.toLowerCase() === 'x' || id === '*' 255 256// ~, ~> --> * (any, kinda silly) 257// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0-0 258// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0-0 259// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0-0 260// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0-0 261// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0-0 262// ~0.0.1 --> >=0.0.1 <0.1.0-0 263const replaceTildes = (comp, options) => { 264 return comp 265 .trim() 266 .split(/\s+/) 267 .map((c) => replaceTilde(c, options)) 268 .join(' ') 269} 270 271const replaceTilde = (comp, options) => { 272 const r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE] 273 return comp.replace(r, (_, M, m, p, pr) => { 274 debug('tilde', comp, _, M, m, p, pr) 275 let ret 276 277 if (isX(M)) { 278 ret = '' 279 } else if (isX(m)) { 280 ret = `>=${M}.0.0 <${+M + 1}.0.0-0` 281 } else if (isX(p)) { 282 // ~1.2 == >=1.2.0 <1.3.0-0 283 ret = `>=${M}.${m}.0 <${M}.${+m + 1}.0-0` 284 } else if (pr) { 285 debug('replaceTilde pr', pr) 286 ret = `>=${M}.${m}.${p}-${pr 287 } <${M}.${+m + 1}.0-0` 288 } else { 289 // ~1.2.3 == >=1.2.3 <1.3.0-0 290 ret = `>=${M}.${m}.${p 291 } <${M}.${+m + 1}.0-0` 292 } 293 294 debug('tilde return', ret) 295 return ret 296 }) 297} 298 299// ^ --> * (any, kinda silly) 300// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0-0 301// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0-0 302// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0-0 303// ^1.2.3 --> >=1.2.3 <2.0.0-0 304// ^1.2.0 --> >=1.2.0 <2.0.0-0 305// ^0.0.1 --> >=0.0.1 <0.0.2-0 306// ^0.1.0 --> >=0.1.0 <0.2.0-0 307const replaceCarets = (comp, options) => { 308 return comp 309 .trim() 310 .split(/\s+/) 311 .map((c) => replaceCaret(c, options)) 312 .join(' ') 313} 314 315const replaceCaret = (comp, options) => { 316 debug('caret', comp, options) 317 const r = options.loose ? re[t.CARETLOOSE] : re[t.CARET] 318 const z = options.includePrerelease ? '-0' : '' 319 return comp.replace(r, (_, M, m, p, pr) => { 320 debug('caret', comp, _, M, m, p, pr) 321 let ret 322 323 if (isX(M)) { 324 ret = '' 325 } else if (isX(m)) { 326 ret = `>=${M}.0.0${z} <${+M + 1}.0.0-0` 327 } else if (isX(p)) { 328 if (M === '0') { 329 ret = `>=${M}.${m}.0${z} <${M}.${+m + 1}.0-0` 330 } else { 331 ret = `>=${M}.${m}.0${z} <${+M + 1}.0.0-0` 332 } 333 } else if (pr) { 334 debug('replaceCaret pr', pr) 335 if (M === '0') { 336 if (m === '0') { 337 ret = `>=${M}.${m}.${p}-${pr 338 } <${M}.${m}.${+p + 1}-0` 339 } else { 340 ret = `>=${M}.${m}.${p}-${pr 341 } <${M}.${+m + 1}.0-0` 342 } 343 } else { 344 ret = `>=${M}.${m}.${p}-${pr 345 } <${+M + 1}.0.0-0` 346 } 347 } else { 348 debug('no pr') 349 if (M === '0') { 350 if (m === '0') { 351 ret = `>=${M}.${m}.${p 352 }${z} <${M}.${m}.${+p + 1}-0` 353 } else { 354 ret = `>=${M}.${m}.${p 355 }${z} <${M}.${+m + 1}.0-0` 356 } 357 } else { 358 ret = `>=${M}.${m}.${p 359 } <${+M + 1}.0.0-0` 360 } 361 } 362 363 debug('caret return', ret) 364 return ret 365 }) 366} 367 368const replaceXRanges = (comp, options) => { 369 debug('replaceXRanges', comp, options) 370 return comp 371 .split(/\s+/) 372 .map((c) => replaceXRange(c, options)) 373 .join(' ') 374} 375 376const replaceXRange = (comp, options) => { 377 comp = comp.trim() 378 const r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE] 379 return comp.replace(r, (ret, gtlt, M, m, p, pr) => { 380 debug('xRange', comp, ret, gtlt, M, m, p, pr) 381 const xM = isX(M) 382 const xm = xM || isX(m) 383 const xp = xm || isX(p) 384 const anyX = xp 385 386 if (gtlt === '=' && anyX) { 387 gtlt = '' 388 } 389 390 // if we're including prereleases in the match, then we need 391 // to fix this to -0, the lowest possible prerelease value 392 pr = options.includePrerelease ? '-0' : '' 393 394 if (xM) { 395 if (gtlt === '>' || gtlt === '<') { 396 // nothing is allowed 397 ret = '<0.0.0-0' 398 } else { 399 // nothing is forbidden 400 ret = '*' 401 } 402 } else if (gtlt && anyX) { 403 // we know patch is an x, because we have any x at all. 404 // replace X with 0 405 if (xm) { 406 m = 0 407 } 408 p = 0 409 410 if (gtlt === '>') { 411 // >1 => >=2.0.0 412 // >1.2 => >=1.3.0 413 gtlt = '>=' 414 if (xm) { 415 M = +M + 1 416 m = 0 417 p = 0 418 } else { 419 m = +m + 1 420 p = 0 421 } 422 } else if (gtlt === '<=') { 423 // <=0.7.x is actually <0.8.0, since any 0.7.x should 424 // pass. Similarly, <=7.x is actually <8.0.0, etc. 425 gtlt = '<' 426 if (xm) { 427 M = +M + 1 428 } else { 429 m = +m + 1 430 } 431 } 432 433 if (gtlt === '<') { 434 pr = '-0' 435 } 436 437 ret = `${gtlt + M}.${m}.${p}${pr}` 438 } else if (xm) { 439 ret = `>=${M}.0.0${pr} <${+M + 1}.0.0-0` 440 } else if (xp) { 441 ret = `>=${M}.${m}.0${pr 442 } <${M}.${+m + 1}.0-0` 443 } 444 445 debug('xRange return', ret) 446 447 return ret 448 }) 449} 450 451// Because * is AND-ed with everything else in the comparator, 452// and '' means "any version", just remove the *s entirely. 453const replaceStars = (comp, options) => { 454 debug('replaceStars', comp, options) 455 // Looseness is ignored here. star is always as loose as it gets! 456 return comp 457 .trim() 458 .replace(re[t.STAR], '') 459} 460 461const replaceGTE0 = (comp, options) => { 462 debug('replaceGTE0', comp, options) 463 return comp 464 .trim() 465 .replace(re[options.includePrerelease ? t.GTE0PRE : t.GTE0], '') 466} 467 468// This function is passed to string.replace(re[t.HYPHENRANGE]) 469// M, m, patch, prerelease, build 470// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5 471// 1.2.3 - 3.4 => >=1.2.0 <3.5.0-0 Any 3.4.x will do 472// 1.2 - 3.4 => >=1.2.0 <3.5.0-0 473const hyphenReplace = incPr => ($0, 474 from, fM, fm, fp, fpr, fb, 475 to, tM, tm, tp, tpr, tb) => { 476 if (isX(fM)) { 477 from = '' 478 } else if (isX(fm)) { 479 from = `>=${fM}.0.0${incPr ? '-0' : ''}` 480 } else if (isX(fp)) { 481 from = `>=${fM}.${fm}.0${incPr ? '-0' : ''}` 482 } else if (fpr) { 483 from = `>=${from}` 484 } else { 485 from = `>=${from}${incPr ? '-0' : ''}` 486 } 487 488 if (isX(tM)) { 489 to = '' 490 } else if (isX(tm)) { 491 to = `<${+tM + 1}.0.0-0` 492 } else if (isX(tp)) { 493 to = `<${tM}.${+tm + 1}.0-0` 494 } else if (tpr) { 495 to = `<=${tM}.${tm}.${tp}-${tpr}` 496 } else if (incPr) { 497 to = `<${tM}.${tm}.${+tp + 1}-0` 498 } else { 499 to = `<=${to}` 500 } 501 502 return `${from} ${to}`.trim() 503} 504 505const testSet = (set, version, options) => { 506 for (let i = 0; i < set.length; i++) { 507 if (!set[i].test(version)) { 508 return false 509 } 510 } 511 512 if (version.prerelease.length && !options.includePrerelease) { 513 // Find the set of versions that are allowed to have prereleases 514 // For example, ^1.2.3-pr.1 desugars to >=1.2.3-pr.1 <2.0.0 515 // That should allow `1.2.3-pr.2` to pass. 516 // However, `1.2.4-alpha.notready` should NOT be allowed, 517 // even though it's within the range set by the comparators. 518 for (let i = 0; i < set.length; i++) { 519 debug(set[i].semver) 520 if (set[i].semver === Comparator.ANY) { 521 continue 522 } 523 524 if (set[i].semver.prerelease.length > 0) { 525 const allowed = set[i].semver 526 if (allowed.major === version.major && 527 allowed.minor === version.minor && 528 allowed.patch === version.patch) { 529 return true 530 } 531 } 532 } 533 534 // Version has a -pre, but it's not one of the ones we like. 535 return false 536 } 537 538 return true 539} 540