|
@@ -0,0 +1,519 @@
|
|
|
+// hoisted class for cyclic dependency
|
|
|
+class Range {
|
|
|
+ constructor (range, options) {
|
|
|
+ options = parseOptions(options)
|
|
|
+
|
|
|
+ if (range instanceof Range) {
|
|
|
+ if (
|
|
|
+ range.loose === !!options.loose &&
|
|
|
+ range.includePrerelease === !!options.includePrerelease
|
|
|
+ ) {
|
|
|
+ return range
|
|
|
+ } else {
|
|
|
+ return new Range(range.raw, options)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (range instanceof Comparator) {
|
|
|
+ // just put it in the set and return
|
|
|
+ this.raw = range.value
|
|
|
+ this.set = [[range]]
|
|
|
+ this.format()
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+ this.options = options
|
|
|
+ this.loose = !!options.loose
|
|
|
+ this.includePrerelease = !!options.includePrerelease
|
|
|
+
|
|
|
+ // First, split based on boolean or ||
|
|
|
+ this.raw = range
|
|
|
+ this.set = range
|
|
|
+ .split('||')
|
|
|
+ // map the range to a 2d array of comparators
|
|
|
+ .map(r => this.parseRange(r.trim()))
|
|
|
+ // throw out any comparator lists that are empty
|
|
|
+ // this generally means that it was not a valid range, which is allowed
|
|
|
+ // in loose mode, but will still throw if the WHOLE range is invalid.
|
|
|
+ .filter(c => c.length)
|
|
|
+
|
|
|
+ if (!this.set.length) {
|
|
|
+ throw new TypeError(`Invalid SemVer Range: ${range}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ // if we have any that are not the null set, throw out null sets.
|
|
|
+ if (this.set.length > 1) {
|
|
|
+ // keep the first one, in case they're all null sets
|
|
|
+ const first = this.set[0]
|
|
|
+ this.set = this.set.filter(c => !isNullSet(c[0]))
|
|
|
+ if (this.set.length === 0) {
|
|
|
+ this.set = [first]
|
|
|
+ } else if (this.set.length > 1) {
|
|
|
+ // if we have any that are *, then the range is just *
|
|
|
+ for (const c of this.set) {
|
|
|
+ if (c.length === 1 && isAny(c[0])) {
|
|
|
+ this.set = [c]
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.format()
|
|
|
+ }
|
|
|
+
|
|
|
+ format () {
|
|
|
+ this.range = this.set
|
|
|
+ .map((comps) => {
|
|
|
+ return comps.join(' ').trim()
|
|
|
+ })
|
|
|
+ .join('||')
|
|
|
+ .trim()
|
|
|
+ return this.range
|
|
|
+ }
|
|
|
+
|
|
|
+ toString () {
|
|
|
+ return this.range
|
|
|
+ }
|
|
|
+
|
|
|
+ parseRange (range) {
|
|
|
+ range = range.trim()
|
|
|
+
|
|
|
+ // memoize range parsing for performance.
|
|
|
+ // this is a very hot path, and fully deterministic.
|
|
|
+ const memoOpts = Object.keys(this.options).join(',')
|
|
|
+ const memoKey = `parseRange:${memoOpts}:${range}`
|
|
|
+ const cached = cache.get(memoKey)
|
|
|
+ if (cached) {
|
|
|
+ return cached
|
|
|
+ }
|
|
|
+
|
|
|
+ const loose = this.options.loose
|
|
|
+ // `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4`
|
|
|
+ const hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE]
|
|
|
+ range = range.replace(hr, hyphenReplace(this.options.includePrerelease))
|
|
|
+ debug('hyphen replace', range)
|
|
|
+ // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5`
|
|
|
+ range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace)
|
|
|
+ debug('comparator trim', range)
|
|
|
+
|
|
|
+ // `~ 1.2.3` => `~1.2.3`
|
|
|
+ range = range.replace(re[t.TILDETRIM], tildeTrimReplace)
|
|
|
+
|
|
|
+ // `^ 1.2.3` => `^1.2.3`
|
|
|
+ range = range.replace(re[t.CARETTRIM], caretTrimReplace)
|
|
|
+
|
|
|
+ // normalize spaces
|
|
|
+ range = range.split(/\s+/).join(' ')
|
|
|
+
|
|
|
+ // At this point, the range is completely trimmed and
|
|
|
+ // ready to be split into comparators.
|
|
|
+
|
|
|
+ let rangeList = range
|
|
|
+ .split(' ')
|
|
|
+ .map(comp => parseComparator(comp, this.options))
|
|
|
+ .join(' ')
|
|
|
+ .split(/\s+/)
|
|
|
+ // >=0.0.0 is equivalent to *
|
|
|
+ .map(comp => replaceGTE0(comp, this.options))
|
|
|
+
|
|
|
+ if (loose) {
|
|
|
+ // in loose mode, throw out any that are not valid comparators
|
|
|
+ rangeList = rangeList.filter(comp => {
|
|
|
+ debug('loose invalid filter', comp, this.options)
|
|
|
+ return !!comp.match(re[t.COMPARATORLOOSE])
|
|
|
+ })
|
|
|
+ }
|
|
|
+ debug('range list', rangeList)
|
|
|
+
|
|
|
+ // if any comparators are the null set, then replace with JUST null set
|
|
|
+ // if more than one comparator, remove any * comparators
|
|
|
+ // also, don't include the same comparator more than once
|
|
|
+ const rangeMap = new Map()
|
|
|
+ const comparators = rangeList.map(comp => new Comparator(comp, this.options))
|
|
|
+ for (const comp of comparators) {
|
|
|
+ if (isNullSet(comp)) {
|
|
|
+ return [comp]
|
|
|
+ }
|
|
|
+ rangeMap.set(comp.value, comp)
|
|
|
+ }
|
|
|
+ if (rangeMap.size > 1 && rangeMap.has('')) {
|
|
|
+ rangeMap.delete('')
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = [...rangeMap.values()]
|
|
|
+ cache.set(memoKey, result)
|
|
|
+ return result
|
|
|
+ }
|
|
|
+
|
|
|
+ intersects (range, options) {
|
|
|
+ if (!(range instanceof Range)) {
|
|
|
+ throw new TypeError('a Range is required')
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.set.some((thisComparators) => {
|
|
|
+ return (
|
|
|
+ isSatisfiable(thisComparators, options) &&
|
|
|
+ range.set.some((rangeComparators) => {
|
|
|
+ return (
|
|
|
+ isSatisfiable(rangeComparators, options) &&
|
|
|
+ thisComparators.every((thisComparator) => {
|
|
|
+ return rangeComparators.every((rangeComparator) => {
|
|
|
+ return thisComparator.intersects(rangeComparator, options)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ )
|
|
|
+ })
|
|
|
+ )
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // if ANY of the sets match ALL of its comparators, then pass
|
|
|
+ test (version) {
|
|
|
+ if (!version) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof version === 'string') {
|
|
|
+ try {
|
|
|
+ version = new SemVer(version, this.options)
|
|
|
+ } catch (er) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for (let i = 0; i < this.set.length; i++) {
|
|
|
+ if (testSet(this.set[i], version, this.options)) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ }
|
|
|
+}
|
|
|
+module.exports = Range
|
|
|
+
|
|
|
+const LRU = require('lru-cache')
|
|
|
+const cache = new LRU({ max: 1000 })
|
|
|
+
|
|
|
+const parseOptions = require('../internal/parse-options')
|
|
|
+const Comparator = require('./comparator')
|
|
|
+const debug = require('../internal/debug')
|
|
|
+const SemVer = require('./semver')
|
|
|
+const {
|
|
|
+ re,
|
|
|
+ t,
|
|
|
+ comparatorTrimReplace,
|
|
|
+ tildeTrimReplace,
|
|
|
+ caretTrimReplace,
|
|
|
+} = require('../internal/re')
|
|
|
+
|
|
|
+const isNullSet = c => c.value === '<0.0.0-0'
|
|
|
+const isAny = c => c.value === ''
|
|
|
+
|
|
|
+// take a set of comparators and determine whether there
|
|
|
+// exists a version which can satisfy it
|
|
|
+const isSatisfiable = (comparators, options) => {
|
|
|
+ let result = true
|
|
|
+ const remainingComparators = comparators.slice()
|
|
|
+ let testComparator = remainingComparators.pop()
|
|
|
+
|
|
|
+ while (result && remainingComparators.length) {
|
|
|
+ result = remainingComparators.every((otherComparator) => {
|
|
|
+ return testComparator.intersects(otherComparator, options)
|
|
|
+ })
|
|
|
+
|
|
|
+ testComparator = remainingComparators.pop()
|
|
|
+ }
|
|
|
+
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+// comprised of xranges, tildes, stars, and gtlt's at this point.
|
|
|
+// already replaced the hyphen ranges
|
|
|
+// turn into a set of JUST comparators.
|
|
|
+const parseComparator = (comp, options) => {
|
|
|
+ debug('comp', comp, options)
|
|
|
+ comp = replaceCarets(comp, options)
|
|
|
+ debug('caret', comp)
|
|
|
+ comp = replaceTildes(comp, options)
|
|
|
+ debug('tildes', comp)
|
|
|
+ comp = replaceXRanges(comp, options)
|
|
|
+ debug('xrange', comp)
|
|
|
+ comp = replaceStars(comp, options)
|
|
|
+ debug('stars', comp)
|
|
|
+ return comp
|
|
|
+}
|
|
|
+
|
|
|
+const isX = id => !id || id.toLowerCase() === 'x' || id === '*'
|
|
|
+
|
|
|
+// ~, ~> --> * (any, kinda silly)
|
|
|
+// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0-0
|
|
|
+// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0-0
|
|
|
+// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0-0
|
|
|
+// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0-0
|
|
|
+// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0-0
|
|
|
+const replaceTildes = (comp, options) =>
|
|
|
+ comp.trim().split(/\s+/).map((c) => {
|
|
|
+ return replaceTilde(c, options)
|
|
|
+ }).join(' ')
|
|
|
+
|
|
|
+const replaceTilde = (comp, options) => {
|
|
|
+ const r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE]
|
|
|
+ return comp.replace(r, (_, M, m, p, pr) => {
|
|
|
+ debug('tilde', comp, _, M, m, p, pr)
|
|
|
+ let ret
|
|
|
+
|
|
|
+ if (isX(M)) {
|
|
|
+ ret = ''
|
|
|
+ } else if (isX(m)) {
|
|
|
+ ret = `>=${M}.0.0 <${+M + 1}.0.0-0`
|
|
|
+ } else if (isX(p)) {
|
|
|
+ // ~1.2 == >=1.2.0 <1.3.0-0
|
|
|
+ ret = `>=${M}.${m}.0 <${M}.${+m + 1}.0-0`
|
|
|
+ } else if (pr) {
|
|
|
+ debug('replaceTilde pr', pr)
|
|
|
+ ret = `>=${M}.${m}.${p}-${pr
|
|
|
+ } <${M}.${+m + 1}.0-0`
|
|
|
+ } else {
|
|
|
+ // ~1.2.3 == >=1.2.3 <1.3.0-0
|
|
|
+ ret = `>=${M}.${m}.${p
|
|
|
+ } <${M}.${+m + 1}.0-0`
|
|
|
+ }
|
|
|
+
|
|
|
+ debug('tilde return', ret)
|
|
|
+ return ret
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// ^ --> * (any, kinda silly)
|
|
|
+// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0-0
|
|
|
+// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0-0
|
|
|
+// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0-0
|
|
|
+// ^1.2.3 --> >=1.2.3 <2.0.0-0
|
|
|
+// ^1.2.0 --> >=1.2.0 <2.0.0-0
|
|
|
+const replaceCarets = (comp, options) =>
|
|
|
+ comp.trim().split(/\s+/).map((c) => {
|
|
|
+ return replaceCaret(c, options)
|
|
|
+ }).join(' ')
|
|
|
+
|
|
|
+const replaceCaret = (comp, options) => {
|
|
|
+ debug('caret', comp, options)
|
|
|
+ const r = options.loose ? re[t.CARETLOOSE] : re[t.CARET]
|
|
|
+ const z = options.includePrerelease ? '-0' : ''
|
|
|
+ return comp.replace(r, (_, M, m, p, pr) => {
|
|
|
+ debug('caret', comp, _, M, m, p, pr)
|
|
|
+ let ret
|
|
|
+
|
|
|
+ if (isX(M)) {
|
|
|
+ ret = ''
|
|
|
+ } else if (isX(m)) {
|
|
|
+ ret = `>=${M}.0.0${z} <${+M + 1}.0.0-0`
|
|
|
+ } else if (isX(p)) {
|
|
|
+ if (M === '0') {
|
|
|
+ ret = `>=${M}.${m}.0${z} <${M}.${+m + 1}.0-0`
|
|
|
+ } else {
|
|
|
+ ret = `>=${M}.${m}.0${z} <${+M + 1}.0.0-0`
|
|
|
+ }
|
|
|
+ } else if (pr) {
|
|
|
+ debug('replaceCaret pr', pr)
|
|
|
+ if (M === '0') {
|
|
|
+ if (m === '0') {
|
|
|
+ ret = `>=${M}.${m}.${p}-${pr
|
|
|
+ } <${M}.${m}.${+p + 1}-0`
|
|
|
+ } else {
|
|
|
+ ret = `>=${M}.${m}.${p}-${pr
|
|
|
+ } <${M}.${+m + 1}.0-0`
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ ret = `>=${M}.${m}.${p}-${pr
|
|
|
+ } <${+M + 1}.0.0-0`
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ debug('no pr')
|
|
|
+ if (M === '0') {
|
|
|
+ if (m === '0') {
|
|
|
+ ret = `>=${M}.${m}.${p
|
|
|
+ }${z} <${M}.${m}.${+p + 1}-0`
|
|
|
+ } else {
|
|
|
+ ret = `>=${M}.${m}.${p
|
|
|
+ }${z} <${M}.${+m + 1}.0-0`
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ ret = `>=${M}.${m}.${p
|
|
|
+ } <${+M + 1}.0.0-0`
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ debug('caret return', ret)
|
|
|
+ return ret
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const replaceXRanges = (comp, options) => {
|
|
|
+ debug('replaceXRanges', comp, options)
|
|
|
+ return comp.split(/\s+/).map((c) => {
|
|
|
+ return replaceXRange(c, options)
|
|
|
+ }).join(' ')
|
|
|
+}
|
|
|
+
|
|
|
+const replaceXRange = (comp, options) => {
|
|
|
+ comp = comp.trim()
|
|
|
+ const r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE]
|
|
|
+ return comp.replace(r, (ret, gtlt, M, m, p, pr) => {
|
|
|
+ debug('xRange', comp, ret, gtlt, M, m, p, pr)
|
|
|
+ const xM = isX(M)
|
|
|
+ const xm = xM || isX(m)
|
|
|
+ const xp = xm || isX(p)
|
|
|
+ const anyX = xp
|
|
|
+
|
|
|
+ if (gtlt === '=' && anyX) {
|
|
|
+ gtlt = ''
|
|
|
+ }
|
|
|
+
|
|
|
+ // if we're including prereleases in the match, then we need
|
|
|
+ // to fix this to -0, the lowest possible prerelease value
|
|
|
+ pr = options.includePrerelease ? '-0' : ''
|
|
|
+
|
|
|
+ if (xM) {
|
|
|
+ if (gtlt === '>' || gtlt === '<') {
|
|
|
+ // nothing is allowed
|
|
|
+ ret = '<0.0.0-0'
|
|
|
+ } else {
|
|
|
+ // nothing is forbidden
|
|
|
+ ret = '*'
|
|
|
+ }
|
|
|
+ } else if (gtlt && anyX) {
|
|
|
+ // we know patch is an x, because we have any x at all.
|
|
|
+ // replace X with 0
|
|
|
+ if (xm) {
|
|
|
+ m = 0
|
|
|
+ }
|
|
|
+ p = 0
|
|
|
+
|
|
|
+ if (gtlt === '>') {
|
|
|
+ // >1 => >=2.0.0
|
|
|
+ // >1.2 => >=1.3.0
|
|
|
+ gtlt = '>='
|
|
|
+ if (xm) {
|
|
|
+ M = +M + 1
|
|
|
+ m = 0
|
|
|
+ p = 0
|
|
|
+ } else {
|
|
|
+ m = +m + 1
|
|
|
+ p = 0
|
|
|
+ }
|
|
|
+ } else if (gtlt === '<=') {
|
|
|
+ // <=0.7.x is actually <0.8.0, since any 0.7.x should
|
|
|
+ // pass. Similarly, <=7.x is actually <8.0.0, etc.
|
|
|
+ gtlt = '<'
|
|
|
+ if (xm) {
|
|
|
+ M = +M + 1
|
|
|
+ } else {
|
|
|
+ m = +m + 1
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (gtlt === '<') {
|
|
|
+ pr = '-0'
|
|
|
+ }
|
|
|
+
|
|
|
+ ret = `${gtlt + M}.${m}.${p}${pr}`
|
|
|
+ } else if (xm) {
|
|
|
+ ret = `>=${M}.0.0${pr} <${+M + 1}.0.0-0`
|
|
|
+ } else if (xp) {
|
|
|
+ ret = `>=${M}.${m}.0${pr
|
|
|
+ } <${M}.${+m + 1}.0-0`
|
|
|
+ }
|
|
|
+
|
|
|
+ debug('xRange return', ret)
|
|
|
+
|
|
|
+ return ret
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// Because * is AND-ed with everything else in the comparator,
|
|
|
+// and '' means "any version", just remove the *s entirely.
|
|
|
+const replaceStars = (comp, options) => {
|
|
|
+ debug('replaceStars', comp, options)
|
|
|
+ // Looseness is ignored here. star is always as loose as it gets!
|
|
|
+ return comp.trim().replace(re[t.STAR], '')
|
|
|
+}
|
|
|
+
|
|
|
+const replaceGTE0 = (comp, options) => {
|
|
|
+ debug('replaceGTE0', comp, options)
|
|
|
+ return comp.trim()
|
|
|
+ .replace(re[options.includePrerelease ? t.GTE0PRE : t.GTE0], '')
|
|
|
+}
|
|
|
+
|
|
|
+// This function is passed to string.replace(re[t.HYPHENRANGE])
|
|
|
+// M, m, patch, prerelease, build
|
|
|
+// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5
|
|
|
+// 1.2.3 - 3.4 => >=1.2.0 <3.5.0-0 Any 3.4.x will do
|
|
|
+// 1.2 - 3.4 => >=1.2.0 <3.5.0-0
|
|
|
+const hyphenReplace = incPr => ($0,
|
|
|
+ from, fM, fm, fp, fpr, fb,
|
|
|
+ to, tM, tm, tp, tpr, tb) => {
|
|
|
+ if (isX(fM)) {
|
|
|
+ from = ''
|
|
|
+ } else if (isX(fm)) {
|
|
|
+ from = `>=${fM}.0.0${incPr ? '-0' : ''}`
|
|
|
+ } else if (isX(fp)) {
|
|
|
+ from = `>=${fM}.${fm}.0${incPr ? '-0' : ''}`
|
|
|
+ } else if (fpr) {
|
|
|
+ from = `>=${from}`
|
|
|
+ } else {
|
|
|
+ from = `>=${from}${incPr ? '-0' : ''}`
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isX(tM)) {
|
|
|
+ to = ''
|
|
|
+ } else if (isX(tm)) {
|
|
|
+ to = `<${+tM + 1}.0.0-0`
|
|
|
+ } else if (isX(tp)) {
|
|
|
+ to = `<${tM}.${+tm + 1}.0-0`
|
|
|
+ } else if (tpr) {
|
|
|
+ to = `<=${tM}.${tm}.${tp}-${tpr}`
|
|
|
+ } else if (incPr) {
|
|
|
+ to = `<${tM}.${tm}.${+tp + 1}-0`
|
|
|
+ } else {
|
|
|
+ to = `<=${to}`
|
|
|
+ }
|
|
|
+
|
|
|
+ return (`${from} ${to}`).trim()
|
|
|
+}
|
|
|
+
|
|
|
+const testSet = (set, version, options) => {
|
|
|
+ for (let i = 0; i < set.length; i++) {
|
|
|
+ if (!set[i].test(version)) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (version.prerelease.length && !options.includePrerelease) {
|
|
|
+ // Find the set of versions that are allowed to have prereleases
|
|
|
+ // For example, ^1.2.3-pr.1 desugars to >=1.2.3-pr.1 <2.0.0
|
|
|
+ // That should allow `1.2.3-pr.2` to pass.
|
|
|
+ // However, `1.2.4-alpha.notready` should NOT be allowed,
|
|
|
+ // even though it's within the range set by the comparators.
|
|
|
+ for (let i = 0; i < set.length; i++) {
|
|
|
+ debug(set[i].semver)
|
|
|
+ if (set[i].semver === Comparator.ANY) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if (set[i].semver.prerelease.length > 0) {
|
|
|
+ const allowed = set[i].semver
|
|
|
+ if (allowed.major === version.major &&
|
|
|
+ allowed.minor === version.minor &&
|
|
|
+ allowed.patch === version.patch) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Version has a -pre, but it's not one of the ones we like.
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|