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