UNPKG

14 kBJavaScriptView Raw
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, 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}
185module.exports = Range
186
187const LRU = require('lru-cache')
188const cache = new LRU({ max: 1000 })
189
190const parseOptions = require('../internal/parse-options')
191const Comparator = require('./comparator')
192const debug = require('../internal/debug')
193const SemVer = require('./semver')
194const {
195 re,
196 t,
197 comparatorTrimReplace,
198 tildeTrimReplace,
199 caretTrimReplace
200} = require('../internal/re')
201
202const isNullSet = c => c.value === '<0.0.0-0'
203const isAny = c => c.value === ''
204
205// take a set of comparators and determine whether there
206// exists a version which can satisfy it
207const 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.
226const 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
239const 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
247const replaceTildes = (comp, options) =>
248 comp.trim().split(/\s+/).map((comp) => {
249 return replaceTilde(comp, options)
250 }).join(' ')
251
252const 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
286const replaceCarets = (comp, options) =>
287 comp.trim().split(/\s+/).map((comp) => {
288 return replaceCaret(comp, options)
289 }).join(' ')
290
291const 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
344const replaceXRanges = (comp, options) => {
345 debug('replaceXRanges', comp, options)
346 return comp.split(/\s+/).map((comp) => {
347 return replaceXRange(comp, options)
348 }).join(' ')
349}
350
351const 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.
427const 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
433const 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
444const 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
476const 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}