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  }