range.js 14 KB

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