UNPKG

20.9 kBJavaScriptView Raw
1var path = require('path')
2var e2c = require('electron-to-chromium/versions')
3
4var agents = require('caniuse-lite/dist/unpacker/agents').agents
5
6var BrowserslistError = require('./error')
7var env = require('./node') // Will load browser.js in webpack
8
9var FLOAT_RANGE = /^\d+(\.\d+)?(-\d+(\.\d+)?)*$/
10
11function normalize (versions) {
12 return versions.filter(function (version) {
13 return typeof version === 'string'
14 })
15}
16
17function nameMapper (name) {
18 return function mapName (version) {
19 return name + ' ' + version
20 }
21}
22
23function getMajor (version) {
24 return parseInt(version.split('.')[0])
25}
26
27function getMajorVersions (released, number) {
28 if (released.length === 0) return []
29 var minimum = getMajor(released[released.length - 1]) - parseInt(number) + 1
30 var selected = []
31 for (var i = released.length - 1; i >= 0; i--) {
32 if (minimum > getMajor(released[i])) break
33 selected.unshift(released[i])
34 }
35 return selected
36}
37
38function uniq (array) {
39 var filtered = []
40 for (var i = 0; i < array.length; i++) {
41 if (filtered.indexOf(array[i]) === -1) filtered.push(array[i])
42 }
43 return filtered
44}
45
46// Helpers
47
48function fillUsage (result, name, data) {
49 for (var i in data) {
50 result[name + ' ' + i] = data[i]
51 }
52}
53
54function generateFilter (sign, version) {
55 version = parseFloat(version)
56 if (sign === '>') {
57 return function (v) {
58 return parseFloat(v) > version
59 }
60 } else if (sign === '>=') {
61 return function (v) {
62 return parseFloat(v) >= version
63 }
64 } else if (sign === '<') {
65 return function (v) {
66 return parseFloat(v) < version
67 }
68 } else {
69 return function (v) {
70 return parseFloat(v) <= version
71 }
72 }
73}
74
75function compareStrings (a, b) {
76 if (a < b) return -1
77 if (a > b) return +1
78 return 0
79}
80
81function normalizeVersion (data, version) {
82 if (data.versions.indexOf(version) !== -1) {
83 return version
84 } else if (browserslist.versionAliases[data.name][version]) {
85 return browserslist.versionAliases[data.name][version]
86 } else if (data.versions.length === 1) {
87 return data.versions[0]
88 } else {
89 return false
90 }
91}
92
93function filterByYear (since) {
94 return Object.keys(agents).reduce(function (selected, name) {
95 var data = byName(name)
96 if (!data) return selected
97 var versions = Object.keys(data.releaseDate).filter(function (v) {
98 return data.releaseDate[v] >= since
99 })
100 return selected.concat(versions.map(nameMapper(data.name)))
101 }, [])
102}
103
104function byName (name) {
105 name = name.toLowerCase()
106 name = browserslist.aliases[name] || name
107 return browserslist.data[name]
108}
109
110function checkName (name) {
111 var data = byName(name)
112 if (!data) throw new BrowserslistError('Unknown browser ' + name)
113 return data
114}
115
116function unknownQuery (query) {
117 return new BrowserslistError('Unknown browser query `' + query + '`')
118}
119
120function resolve (queries, context) {
121 return queries.reduce(function (result, selection, index) {
122 selection = selection.trim()
123 if (selection === '') return result
124
125 var isExclude = selection.indexOf('not ') === 0
126 if (isExclude) {
127 if (index === 0) {
128 throw new BrowserslistError(
129 'Write any browsers query (for instance, `defaults`) ' +
130 'before `' + selection + '`')
131 }
132 selection = selection.slice(4)
133 }
134
135 for (var i = 0; i < QUERIES.length; i++) {
136 var type = QUERIES[i]
137 var match = selection.match(type.regexp)
138 if (match) {
139 var args = [context].concat(match.slice(1))
140 var array = type.select.apply(browserslist, args)
141 if (isExclude) {
142 array = array.concat(array.map(function (j) {
143 return j.replace(/\s\d+/, ' 0')
144 }))
145 return result.filter(function (j) {
146 return array.indexOf(j) === -1
147 })
148 }
149 return result.concat(array)
150 }
151 }
152
153 throw unknownQuery(selection)
154 }, [])
155}
156
157/**
158 * Return array of browsers by selection queries.
159 *
160 * @param {(string|string[])} [queries=browserslist.defaults] Browser queries.
161 * @param {object} [opts] Options.
162 * @param {string} [opts.path="."] Path to processed file.
163 * It will be used to find config files.
164 * @param {string} [opts.env="production"] Processing environment.
165 * It will be used to take right
166 * queries from config file.
167 * @param {string} [opts.config] Path to config file with queries.
168 * @param {object} [opts.stats] Custom browser usage statistics
169 * for "> 1% in my stats" query.
170 * @param {boolean} [opts.ignoreUnknownVersions=false] Do not throw on unknown
171 * version in direct query.
172 * @param {boolean} [opts.dangerousExtend] Disable security checks
173 * for extend query.
174 * @return {string[]} Array with browser names in Can I Use.
175 *
176 * @example
177 * browserslist('IE >= 10, IE 8') //=> ['ie 11', 'ie 10', 'ie 8']
178 */
179function browserslist (queries, opts) {
180 if (typeof opts === 'undefined') opts = { }
181
182 if (typeof opts.path === 'undefined') {
183 opts.path = path.resolve ? path.resolve('.') : '.'
184 }
185
186 if (typeof queries === 'undefined' || queries === null) {
187 var config = browserslist.loadConfig(opts)
188 if (config) {
189 queries = config
190 } else {
191 queries = browserslist.defaults
192 }
193 }
194
195 if (typeof queries === 'string') {
196 queries = queries.split(/,\s*/)
197 }
198
199 if (!Array.isArray(queries)) {
200 throw new BrowserslistError(
201 'Browser queries must be an array. Got ' + typeof queries + '.')
202 }
203
204 var context = {
205 ignoreUnknownVersions: opts.ignoreUnknownVersions,
206 dangerousExtend: opts.dangerousExtend
207 }
208
209 var stats = env.getStat(opts)
210 if (stats) {
211 context.customUsage = { }
212 for (var browser in stats) {
213 fillUsage(context.customUsage, browser, stats[browser])
214 }
215 }
216
217 var result = resolve(queries, context).map(function (i) {
218 var parts = i.split(' ')
219 var name = parts[0]
220 var version = parts[1]
221 if (version === '0') {
222 return name + ' ' + byName(name).versions[0]
223 } else {
224 return i
225 }
226 }).sort(function (name1, name2) {
227 name1 = name1.split(' ')
228 name2 = name2.split(' ')
229 if (name1[0] === name2[0]) {
230 if (FLOAT_RANGE.test(name1[1]) && FLOAT_RANGE.test(name2[1])) {
231 return parseFloat(name2[1]) - parseFloat(name1[1])
232 } else {
233 return compareStrings(name2[1], name1[1])
234 }
235 } else {
236 return compareStrings(name1[0], name2[0])
237 }
238 })
239
240 return uniq(result)
241}
242
243// Will be filled by Can I Use data below
244browserslist.data = { }
245browserslist.usage = {
246 global: { },
247 custom: null
248}
249
250// Default browsers query
251browserslist.defaults = [
252 '> 0.5%',
253 'last 2 versions',
254 'Firefox ESR',
255 'not dead'
256]
257
258// Browser names aliases
259browserslist.aliases = {
260 fx: 'firefox',
261 ff: 'firefox',
262 ios: 'ios_saf',
263 explorer: 'ie',
264 blackberry: 'bb',
265 explorermobile: 'ie_mob',
266 operamini: 'op_mini',
267 operamobile: 'op_mob',
268 chromeandroid: 'and_chr',
269 firefoxandroid: 'and_ff',
270 ucandroid: 'and_uc',
271 qqandroid: 'and_qq'
272}
273
274// Aliases to work with joined versions like `ios_saf 7.0-7.1`
275browserslist.versionAliases = { }
276
277browserslist.clearCaches = env.clearCaches
278browserslist.parseConfig = env.parseConfig
279browserslist.readConfig = env.readConfig
280browserslist.findConfig = env.findConfig
281browserslist.loadConfig = env.loadConfig
282
283/**
284 * Return browsers market coverage.
285 *
286 * @param {string[]} browsers Browsers names in Can I Use.
287 * @param {string|object} [stats="global"] Which statistics should be used.
288 * Country code or custom statistics.
289 * Pass `"my stats"` to load statistics
290 * from Browserslist files.
291 *
292 * @return {number} Total market coverage for all selected browsers.
293 *
294 * @example
295 * browserslist.coverage(browserslist('> 1% in US'), 'US') //=> 83.1
296 */
297browserslist.coverage = function (browsers, stats) {
298 var data
299 if (typeof stats === 'undefined') {
300 data = browserslist.usage.global
301 } else if (stats === 'my stats') {
302 var opts = {}
303 opts.path = path.resolve ? path.resolve('.') : '.'
304 var customStats = env.getStat(opts)
305 if (!customStats) {
306 throw new BrowserslistError('Custom usage statistics was not provided')
307 }
308 data = {}
309 for (var browser in customStats) {
310 fillUsage(data, browser, customStats[browser])
311 }
312 } else if (typeof stats === 'string') {
313 if (stats.length > 2) {
314 stats = stats.toLowerCase()
315 } else {
316 stats = stats.toUpperCase()
317 }
318 env.loadCountry(browserslist.usage, stats)
319 data = browserslist.usage[stats]
320 } else {
321 if ('dataByBrowser' in stats) {
322 stats = stats.dataByBrowser
323 }
324 data = { }
325 for (var name in stats) {
326 for (var version in stats[name]) {
327 data[name + ' ' + version] = stats[name][version]
328 }
329 }
330 }
331
332 return browsers.reduce(function (all, i) {
333 var usage = data[i]
334 if (usage === undefined) {
335 usage = data[i.replace(/ [\d.]+$/, ' 0')]
336 }
337 return all + (usage || 0)
338 }, 0)
339}
340
341var QUERIES = [
342 {
343 regexp: /^last\s+(\d+)\s+major versions?$/i,
344 select: function (context, versions) {
345 return Object.keys(agents).reduce(function (selected, name) {
346 var data = byName(name)
347 if (!data) return selected
348 var array = getMajorVersions(data.released, versions)
349
350 array = array.map(nameMapper(data.name))
351 return selected.concat(array)
352 }, [])
353 }
354 },
355 {
356 regexp: /^last\s+(\d+)\s+versions?$/i,
357 select: function (context, versions) {
358 return Object.keys(agents).reduce(function (selected, name) {
359 var data = byName(name)
360 if (!data) return selected
361 var array = data.released.slice(-versions)
362
363 array = array.map(nameMapper(data.name))
364 return selected.concat(array)
365 }, [])
366 }
367 },
368 {
369 regexp: /^last\s+(\d+)\s+electron\s+major versions?$/i,
370 select: function (context, versions) {
371 var validVersions = getMajorVersions(Object.keys(e2c).reverse(), versions)
372 return validVersions.map(function (i) {
373 return 'chrome ' + e2c[i]
374 })
375 }
376 },
377 {
378 regexp: /^last\s+(\d+)\s+(\w+)\s+major versions?$/i,
379 select: function (context, versions, name) {
380 var data = checkName(name)
381 var validVersions = getMajorVersions(data.released, versions)
382 return validVersions.map(nameMapper(data.name))
383 }
384 },
385 {
386 regexp: /^last\s+(\d+)\s+electron\s+versions?$/i,
387 select: function (context, versions) {
388 return Object.keys(e2c).reverse().slice(-versions).map(function (i) {
389 return 'chrome ' + e2c[i]
390 })
391 }
392 },
393 {
394 regexp: /^last\s+(\d+)\s+(\w+)\s+versions?$/i,
395 select: function (context, versions, name) {
396 var data = checkName(name)
397 return data.released.slice(-versions).map(nameMapper(data.name))
398 }
399 },
400 {
401 regexp: /^unreleased\s+versions$/i,
402 select: function () {
403 return Object.keys(agents).reduce(function (selected, name) {
404 var data = byName(name)
405 if (!data) return selected
406 var array = data.versions.filter(function (v) {
407 return data.released.indexOf(v) === -1
408 })
409
410 array = array.map(nameMapper(data.name))
411 return selected.concat(array)
412 }, [])
413 }
414 },
415 {
416 regexp: /^unreleased\s+electron\s+versions?$/i,
417 select: function () {
418 return []
419 }
420 },
421 {
422 regexp: /^unreleased\s+(\w+)\s+versions?$/i,
423 select: function (context, name) {
424 var data = checkName(name)
425 return data.versions.filter(function (v) {
426 return data.released.indexOf(v) === -1
427 }).map(nameMapper(data.name))
428 }
429 },
430 {
431 regexp: /^last\s+(\d+)\s+years?$/i,
432 select: function (context, years) {
433 var date = new Date()
434 var since = date.setFullYear(date.getFullYear() - years) / 1000
435
436 return filterByYear(since)
437 }
438 },
439 {
440 regexp: /^since (\d+)(?:-(\d+))?(?:-(\d+))?$/i,
441 select: function (context, year, month, date) {
442 year = parseInt(year)
443 month = parseInt(month || '01') - 1
444 date = parseInt(date || '01')
445 var since = Date.UTC(year, month, date, 0, 0, 0) / 1000
446
447 return filterByYear(since)
448 }
449 },
450 {
451 regexp: /^(>=?|<=?)\s*(\d*\.?\d+)%$/,
452 select: function (context, sign, popularity) {
453 popularity = parseFloat(popularity)
454 var usage = browserslist.usage.global
455
456 return Object.keys(usage).reduce(function (result, version) {
457 if (sign === '>') {
458 if (usage[version] > popularity) {
459 result.push(version)
460 }
461 } else if (sign === '<') {
462 if (usage[version] < popularity) {
463 result.push(version)
464 }
465 } else if (sign === '<=') {
466 if (usage[version] <= popularity) {
467 result.push(version)
468 }
469 } else if (usage[version] >= popularity) {
470 result.push(version)
471 }
472 return result
473 }, [])
474 }
475 },
476 {
477 regexp: /^(>=?|<=?)\s*(\d*\.?\d+)%\s+in\s+my\s+stats$/,
478 select: function (context, sign, popularity) {
479 popularity = parseFloat(popularity)
480
481 if (!context.customUsage) {
482 throw new BrowserslistError('Custom usage statistics was not provided')
483 }
484
485 var usage = context.customUsage
486
487 return Object.keys(usage).reduce(function (result, version) {
488 if (sign === '>') {
489 if (usage[version] > popularity) {
490 result.push(version)
491 }
492 } else if (sign === '<') {
493 if (usage[version] < popularity) {
494 result.push(version)
495 }
496 } else if (sign === '<=') {
497 if (usage[version] <= popularity) {
498 result.push(version)
499 }
500 } else if (usage[version] >= popularity) {
501 result.push(version)
502 }
503 return result
504 }, [])
505 }
506 },
507 {
508 regexp: /^(>=?|<=?)\s*(\d*\.?\d+)%\s+in\s+((alt-)?\w\w)$/,
509 select: function (context, sign, popularity, place) {
510 popularity = parseFloat(popularity)
511
512 if (place.length === 2) {
513 place = place.toUpperCase()
514 } else {
515 place = place.toLowerCase()
516 }
517
518 env.loadCountry(browserslist.usage, place)
519 var usage = browserslist.usage[place]
520
521 return Object.keys(usage).reduce(function (result, version) {
522 if (sign === '>') {
523 if (usage[version] > popularity) {
524 result.push(version)
525 }
526 } else if (sign === '<') {
527 if (usage[version] < popularity) {
528 result.push(version)
529 }
530 } else if (sign === '<=') {
531 if (usage[version] <= popularity) {
532 result.push(version)
533 }
534 } else if (usage[version] >= popularity) {
535 result.push(version)
536 }
537 return result
538 }, [])
539 }
540 },
541 {
542 regexp: /^cover\s+(\d*\.?\d+)%(\s+in\s+(my\s+stats|(alt-)?\w\w))?$/,
543 select: function (context, coverage, statMode) {
544 coverage = parseFloat(coverage)
545
546 var usage = browserslist.usage.global
547 if (statMode) {
548 if (statMode.match(/^\s+in\s+my\s+stats$/)) {
549 if (!context.customUsage) {
550 throw new BrowserslistError(
551 'Custom usage statistics was not provided'
552 )
553 }
554 usage = context.customUsage
555 } else {
556 var match = statMode.match(/\s+in\s+((alt-)?\w\w)/)
557 var place = match[1]
558 if (place.length === 2) {
559 place = place.toUpperCase()
560 } else {
561 place = place.toLowerCase()
562 }
563 env.loadCountry(browserslist.usage, place)
564 usage = browserslist.usage[place]
565 }
566 }
567
568 var versions = Object.keys(usage).sort(function (a, b) {
569 return usage[b] - usage[a]
570 })
571
572 var coveraged = 0
573 var result = []
574 var version
575 for (var i = 0; i <= versions.length; i++) {
576 version = versions[i]
577 if (usage[version] === 0) break
578
579 coveraged += usage[version]
580 result.push(version)
581 if (coveraged >= coverage) break
582 }
583
584 return result
585 }
586 },
587 {
588 regexp: /^electron\s+([\d.]+)\s*-\s*([\d.]+)$/i,
589 select: function (context, from, to) {
590 if (!e2c[from]) {
591 throw new BrowserslistError('Unknown version ' + from + ' of electron')
592 }
593 if (!e2c[to]) {
594 throw new BrowserslistError('Unknown version ' + to + ' of electron')
595 }
596
597 from = parseFloat(from)
598 to = parseFloat(to)
599
600 return Object.keys(e2c).filter(function (i) {
601 var parsed = parseFloat(i)
602 return parsed >= from && parsed <= to
603 }).map(function (i) {
604 return 'chrome ' + e2c[i]
605 })
606 }
607 },
608 {
609 regexp: /^(\w+)\s+([\d.]+)\s*-\s*([\d.]+)$/i,
610 select: function (context, name, from, to) {
611 var data = checkName(name)
612 from = parseFloat(normalizeVersion(data, from) || from)
613 to = parseFloat(normalizeVersion(data, to) || to)
614
615 function filter (v) {
616 var parsed = parseFloat(v)
617 return parsed >= from && parsed <= to
618 }
619
620 return data.released.filter(filter).map(nameMapper(data.name))
621 }
622 },
623 {
624 regexp: /^electron\s*(>=?|<=?)\s*([\d.]+)$/i,
625 select: function (context, sign, version) {
626 return Object.keys(e2c)
627 .filter(generateFilter(sign, version))
628 .map(function (i) {
629 return 'chrome ' + e2c[i]
630 })
631 }
632 },
633 {
634 regexp: /^(\w+)\s*(>=?|<=?)\s*([\d.]+)$/,
635 select: function (context, name, sign, version) {
636 var data = checkName(name)
637 var alias = browserslist.versionAliases[data.name][version]
638 if (alias) {
639 version = alias
640 }
641 return data.released
642 .filter(generateFilter(sign, version))
643 .map(function (v) {
644 return data.name + ' ' + v
645 })
646 }
647 },
648 {
649 regexp: /^(firefox|ff|fx)\s+esr$/i,
650 select: function () {
651 return ['firefox 52']
652 }
653 },
654 {
655 regexp: /(operamini|op_mini)\s+all/i,
656 select: function () {
657 return ['op_mini all']
658 }
659 },
660 {
661 regexp: /^electron\s+([\d.]+)$/i,
662 select: function (context, version) {
663 var chrome = e2c[version]
664 if (!chrome) {
665 throw new BrowserslistError(
666 'Unknown version ' + version + ' of electron')
667 }
668 return ['chrome ' + chrome]
669 }
670 },
671 {
672 regexp: /^(\w+)\s+(tp|[\d.]+)$/i,
673 select: function (context, name, version) {
674 if (/^tp$/i.test(version)) version = 'TP'
675 var data = checkName(name)
676 var alias = normalizeVersion(data, version)
677 if (alias) {
678 version = alias
679 } else {
680 if (version.indexOf('.') === -1) {
681 alias = version + '.0'
682 } else {
683 alias = version.replace(/\.0$/, '')
684 }
685 alias = normalizeVersion(data, alias)
686 if (alias) {
687 version = alias
688 } else if (context.ignoreUnknownVersions) {
689 return []
690 } else {
691 throw new BrowserslistError(
692 'Unknown version ' + version + ' of ' + name)
693 }
694 }
695 return [data.name + ' ' + version]
696 }
697 },
698 {
699 regexp: /^extends (.+)$/i,
700 select: function (context, name) {
701 return resolve(env.loadQueries(context, name), context)
702 }
703 },
704 {
705 regexp: /^defaults$/i,
706 select: function () {
707 return browserslist(browserslist.defaults)
708 }
709 },
710 {
711 regexp: /^dead$/i,
712 select: function () {
713 return ['ie 10', 'ie_mob 10', 'bb 10', 'bb 7']
714 }
715 },
716 {
717 regexp: /^(\w+)$/i,
718 select: function (context, name) {
719 if (byName(name)) {
720 throw new BrowserslistError(
721 'Specify versions in Browserslist query for browser ' + name)
722 } else {
723 throw unknownQuery(name)
724 }
725 }
726 }
727];
728
729// Get and convert Can I Use data
730
731(function () {
732 for (var name in agents) {
733 var browser = agents[name]
734 browserslist.data[name] = {
735 name: name,
736 versions: normalize(agents[name].versions),
737 released: normalize(agents[name].versions.slice(0, -3)),
738 releaseDate: agents[name].release_date
739 }
740 fillUsage(browserslist.usage.global, name, browser.usage_global)
741
742 browserslist.versionAliases[name] = { }
743 for (var i = 0; i < browser.versions.length; i++) {
744 var full = browser.versions[i]
745 if (!full) continue
746
747 if (full.indexOf('-') !== -1) {
748 var interval = full.split('-')
749 for (var j = 0; j < interval.length; j++) {
750 browserslist.versionAliases[name][interval[j]] = full
751 }
752 }
753 }
754 }
755}())
756
757module.exports = browserslist