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