1 var path = require('path')
2 var e2c = require('electron-to-chromium/versions')
4 var agents = require('caniuse-lite/dist/unpacker/agents').agents
5 var region = require('caniuse-lite/dist/unpacker/region').default
7 var BrowserslistError = require('./error')
8 var env = require('./node') // Will load browser.js in webpack
10 var FLOAT_RANGE = /^\d+(\.\d+)?(-\d+(\.\d+)?)*$/
12 function normalize (versions) {
13 return versions.filter(function (version) {
14 return typeof version === 'string'
18 function nameMapper (name) {
19 return function mapName (version) {
20 return name + ' ' + version
24 function getMajor (version) {
25 return parseInt(version.split('.')[0])
28 function getMajorVersions (released, number) {
29 if (released.length === 0) return []
30 var minimum = getMajor(released[released.length - 1]) - parseInt(number) + 1
32 for (var i = released.length - 1; i >= 0; i--) {
33 if (minimum > getMajor(released[i])) break
34 selected.unshift(released[i])
39 function uniq (array) {
41 for (var i = 0; i < array.length; i++) {
42 if (filtered.indexOf(array[i]) === -1) filtered.push(array[i])
49 function fillUsage (result, name, data) {
51 result[name + ' ' + i] = data[i]
55 function generateFilter (sign, version) {
56 version = parseFloat(version)
59 return parseFloat(v) > version
61 } else if (sign === '>=') {
63 return parseFloat(v) >= version
65 } else if (sign === '<') {
67 return parseFloat(v) < version
71 return parseFloat(v) <= version
76 function compareStrings (a, b) {
82 function normalizeVersion (data, version) {
83 if (data.versions.indexOf(version) !== -1) {
85 } else if (browserslist.versionAliases[data.name][version]) {
86 return browserslist.versionAliases[data.name][version]
87 } else if (data.versions.length === 1) {
88 return data.versions[0]
94 function loadCountryStatistics (country) {
95 country = country.replace(/[^\w-]/g, '')
96 if (!browserslist.usage[country]) {
98 // eslint-disable-next-line security/detect-non-literal-require
99 var compressed = require('caniuse-lite/data/regions/' + country + '.js')
100 var data = region(compressed)
101 for (var i in data) {
102 fillUsage(usage, i, data[i])
104 browserslist.usage[country] = usage
108 function filterByYear (since) {
109 return Object.keys(agents).reduce(function (selected, name) {
110 var data = byName(name)
111 if (!data) return selected
112 var versions = Object.keys(data.releaseDate).filter(function (v) {
113 return data.releaseDate[v] >= since
115 return selected.concat(versions.map(nameMapper(data.name)))
119 function byName (name) {
120 name = name.toLowerCase()
121 name = browserslist.aliases[name] || name
122 return browserslist.data[name]
125 function checkName (name) {
126 var data = byName(name)
127 if (!data) throw new BrowserslistError('Unknown browser ' + name)
131 function resolve (queries, context) {
132 return queries.reduce(function (result, selection, index) {
133 selection = selection.trim()
134 if (selection === '') return result
136 var isExclude = selection.indexOf('not ') === 0
139 throw new BrowserslistError(
140 'Write any browsers query (for instance, `defaults`) ' +
141 'before `' + selection + '`')
143 selection = selection.slice(4)
146 for (var i = 0; i < QUERIES.length; i++) {
147 var type = QUERIES[i]
148 var match = selection.match(type.regexp)
150 var args = [context].concat(match.slice(1))
151 var array = type.select.apply(browserslist, args)
153 array = array.concat(array.map(function (j) {
154 return j.replace(/\s\d+/, ' 0')
156 return result.filter(function (j) {
157 return array.indexOf(j) === -1
160 return result.concat(array)
164 throw new BrowserslistError('Unknown browser query `' + selection + '`')
169 * Return array of browsers by selection queries.
171 * @param {(string|string[])} [queries=browserslist.defaults] Browser queries.
172 * @param {object} opts Options.
173 * @param {string} [opts.path="."] Path to processed file.
174 * It will be used to find config files.
175 * @param {string} [opts.env="development"] Processing environment.
176 * It will be used to take right
177 * queries from config file.
178 * @param {string} [opts.config] Path to config file with queries.
179 * @param {object} [opts.stats] Custom browser usage statistics
180 * for "> 1% in my stats" query.
181 * @return {string[]} Array with browser names in Can I Use.
184 * browserslist('IE >= 10, IE 8') //=> ['ie 11', 'ie 10', 'ie 8']
186 function browserslist (queries, opts) {
187 if (typeof opts === 'undefined') opts = { }
189 if (!opts.hasOwnProperty('path')) {
190 opts.path = path.resolve ? path.resolve('.') : '.'
193 if (typeof queries === 'undefined' || queries === null) {
194 var config = env.loadConfig(opts)
198 queries = browserslist.defaults
202 if (typeof queries === 'string') {
203 queries = queries.split(/,\s*/)
206 if (!Array.isArray(queries)) {
207 throw new BrowserslistError(
208 'Browser queries must be an array. Got ' + typeof queries + '.')
211 var context = { dangerousExtend: opts.dangerousExtend }
213 var stats = env.getStat(opts)
215 if ('dataByBrowser' in stats) {
216 stats = stats.dataByBrowser
218 context.customUsage = { }
219 for (var browser in stats) {
220 fillUsage(context.customUsage, browser, stats[browser])
224 var result = resolve(queries, context).map(function (i) {
225 var parts = i.split(' ')
227 var version = parts[1]
228 if (version === '0') {
229 return name + ' ' + byName(name).versions[0]
233 }).sort(function (name1, name2) {
234 name1 = name1.split(' ')
235 name2 = name2.split(' ')
236 if (name1[0] === name2[0]) {
237 if (FLOAT_RANGE.test(name1[1]) && FLOAT_RANGE.test(name2[1])) {
238 return parseFloat(name2[1]) - parseFloat(name1[1])
240 return compareStrings(name2[1], name1[1])
243 return compareStrings(name1[0], name2[0])
250 // Will be filled by Can I Use data below
251 browserslist.data = { }
252 browserslist.usage = {
257 // Default browsers query
258 browserslist.defaults = [
264 // Browser names aliases
265 browserslist.aliases = {
271 explorermobile: 'ie_mob',
272 operamini: 'op_mini',
273 operamobile: 'op_mob',
274 chromeandroid: 'and_chr',
275 firefoxandroid: 'and_ff',
280 // Aliases to work with joined versions like `ios_saf 7.0-7.1`
281 browserslist.versionAliases = { }
283 browserslist.clearCaches = env.clearCaches
284 browserslist.parseConfig = env.parseConfig
285 browserslist.readConfig = env.readConfig
286 browserslist.findConfig = env.findConfig
289 * Return browsers market coverage.
291 * @param {string[]} browsers Browsers names in Can I Use.
292 * @param {string} [country="global"] Which country statistics should be used.
294 * @return {number} Total market coverage for all selected browsers.
297 * browserslist.coverage(browserslist('> 1% in US'), 'US') //=> 83.1
299 browserslist.coverage = function (browsers, country) {
300 if (country && country !== 'global') {
301 if (country.length > 2) {
302 country = country.toLowerCase()
304 country = country.toUpperCase()
306 loadCountryStatistics(country)
311 return browsers.reduce(function (all, i) {
312 var usage = browserslist.usage[country][i]
313 if (usage === undefined) {
314 usage = browserslist.usage[country][i.replace(/ [\d.]+$/, ' 0')]
316 return all + (usage || 0)
322 regexp: /^last\s+(\d+)\s+major versions?$/i,
323 select: function (context, versions) {
324 return Object.keys(agents).reduce(function (selected, name) {
325 var data = byName(name)
326 if (!data) return selected
327 var array = getMajorVersions(data.released, versions)
329 array = array.map(nameMapper(data.name))
330 return selected.concat(array)
335 regexp: /^last\s+(\d+)\s+versions?$/i,
336 select: function (context, versions) {
337 return Object.keys(agents).reduce(function (selected, name) {
338 var data = byName(name)
339 if (!data) return selected
340 var array = data.released.slice(-versions)
342 array = array.map(nameMapper(data.name))
343 return selected.concat(array)
348 regexp: /^last\s+(\d+)\s+electron\s+major versions?$/i,
349 select: function (context, versions) {
350 var validVersions = getMajorVersions(Object.keys(e2c).reverse(), versions)
351 return validVersions.map(function (i) {
352 return 'chrome ' + e2c[i]
357 regexp: /^last\s+(\d+)\s+(\w+)\s+major versions?$/i,
358 select: function (context, versions, name) {
359 var data = checkName(name)
360 var validVersions = getMajorVersions(data.released, versions)
361 return validVersions.map(nameMapper(data.name))
365 regexp: /^last\s+(\d+)\s+electron\s+versions?$/i,
366 select: function (context, versions) {
367 return Object.keys(e2c).reverse().slice(-versions).map(function (i) {
368 return 'chrome ' + e2c[i]
373 regexp: /^last\s+(\d+)\s+(\w+)\s+versions?$/i,
374 select: function (context, versions, name) {
375 var data = checkName(name)
376 return data.released.slice(-versions).map(nameMapper(data.name))
380 regexp: /^unreleased\s+versions$/i,
381 select: function () {
382 return Object.keys(agents).reduce(function (selected, name) {
383 var data = byName(name)
384 if (!data) return selected
385 var array = data.versions.filter(function (v) {
386 return data.released.indexOf(v) === -1
389 array = array.map(nameMapper(data.name))
390 return selected.concat(array)
395 regexp: /^unreleased\s+electron\s+versions?$/i,
396 select: function () {
401 regexp: /^unreleased\s+(\w+)\s+versions?$/i,
402 select: function (context, name) {
403 var data = checkName(name)
404 return data.versions.filter(function (v) {
405 return data.released.indexOf(v) === -1
406 }).map(nameMapper(data.name))
410 regexp: /^last\s+(\d+)\s+years?$/i,
411 select: function (context, years) {
412 var date = new Date()
413 var since = date.setFullYear(date.getFullYear() - years) / 1000
415 return filterByYear(since)
419 regexp: /^since (\d+)(?:-(\d+))?(?:-(\d+))?$/i,
420 select: function (context, year, month, date) {
421 year = parseInt(year)
422 month = parseInt(month || '01') - 1
423 date = parseInt(date || '01')
424 var since = Date.UTC(year, month, date, 0, 0, 0) / 1000
426 return filterByYear(since)
430 regexp: /^(>=?|<=?)\s*(\d*\.?\d+)%$/,
431 select: function (context, sign, popularity) {
432 popularity = parseFloat(popularity)
433 var usage = browserslist.usage.global
435 return Object.keys(usage).reduce(function (result, version) {
437 if (usage[version] > popularity) {
440 } else if (sign === '<') {
441 if (usage[version] < popularity) {
444 } else if (sign === '<=') {
445 if (usage[version] <= popularity) {
448 } else if (usage[version] >= popularity) {
456 regexp: /^(>=?|<=?)\s*(\d*\.?\d+)%\s+in\s+my\s+stats$/,
457 select: function (context, sign, popularity) {
458 popularity = parseFloat(popularity)
460 if (!context.customUsage) {
461 throw new BrowserslistError('Custom usage statistics was not provided')
464 var usage = context.customUsage
466 return Object.keys(usage).reduce(function (result, version) {
468 if (usage[version] > popularity) {
471 } else if (sign === '<') {
472 if (usage[version] < popularity) {
475 } else if (sign === '<=') {
476 if (usage[version] <= popularity) {
479 } else if (usage[version] >= popularity) {
487 regexp: /^(>=?|<=?)\s*(\d*\.?\d+)%\s+in\s+((alt-)?\w\w)$/,
488 select: function (context, sign, popularity, place) {
489 popularity = parseFloat(popularity)
491 if (place.length === 2) {
492 place = place.toUpperCase()
494 place = place.toLowerCase()
497 loadCountryStatistics(place)
498 var usage = browserslist.usage[place]
500 return Object.keys(usage).reduce(function (result, version) {
502 if (usage[version] > popularity) {
505 } else if (sign === '<') {
506 if (usage[version] < popularity) {
509 } else if (sign === '<=') {
510 if (usage[version] <= popularity) {
513 } else if (usage[version] >= popularity) {
521 regexp: /^electron\s+([\d.]+)\s*-\s*([\d.]+)$/i,
522 select: function (context, from, to) {
524 throw new BrowserslistError('Unknown version ' + from + ' of electron')
527 throw new BrowserslistError('Unknown version ' + to + ' of electron')
530 from = parseFloat(from)
533 return Object.keys(e2c).filter(function (i) {
534 var parsed = parseFloat(i)
535 return parsed >= from && parsed <= to
536 }).map(function (i) {
537 return 'chrome ' + e2c[i]
542 regexp: /^(\w+)\s+([\d.]+)\s*-\s*([\d.]+)$/i,
543 select: function (context, name, from, to) {
544 var data = checkName(name)
545 from = parseFloat(normalizeVersion(data, from) || from)
546 to = parseFloat(normalizeVersion(data, to) || to)
548 function filter (v) {
549 var parsed = parseFloat(v)
550 return parsed >= from && parsed <= to
553 return data.released.filter(filter).map(nameMapper(data.name))
557 regexp: /^electron\s*(>=?|<=?)\s*([\d.]+)$/i,
558 select: function (context, sign, version) {
559 return Object.keys(e2c)
560 .filter(generateFilter(sign, version))
562 return 'chrome ' + e2c[i]
567 regexp: /^(\w+)\s*(>=?|<=?)\s*([\d.]+)$/,
568 select: function (context, name, sign, version) {
569 var data = checkName(name)
570 var alias = browserslist.versionAliases[data.name][version]
575 .filter(generateFilter(sign, version))
577 return data.name + ' ' + v
582 regexp: /^(firefox|ff|fx)\s+esr$/i,
583 select: function () {
584 return ['firefox 52']
588 regexp: /(operamini|op_mini)\s+all/i,
589 select: function () {
590 return ['op_mini all']
594 regexp: /^electron\s+([\d.]+)$/i,
595 select: function (context, version) {
596 var chrome = e2c[version]
598 throw new BrowserslistError(
599 'Unknown version ' + version + ' of electron')
601 return ['chrome ' + chrome]
605 regexp: /^(\w+)\s+(tp|[\d.]+)$/i,
606 select: function (context, name, version) {
607 if (/^tp$/i.test(version)) version = 'TP'
608 var data = checkName(name)
609 var alias = normalizeVersion(data, version)
613 if (version.indexOf('.') === -1) {
614 alias = version + '.0'
615 } else if (/\.0$/.test(version)) {
616 alias = version.replace(/\.0$/, '')
618 alias = normalizeVersion(data, alias)
622 throw new BrowserslistError(
623 'Unknown version ' + version + ' of ' + name)
626 return [data.name + ' ' + version]
630 regexp: /^extends (.+)$/i,
631 select: function (context, name) {
632 return resolve(env.loadQueries(context, name), context)
636 regexp: /^defaults$/i,
637 select: function () {
638 return browserslist(browserslist.defaults)
643 // Get and convert Can I Use data
646 for (var name in agents) {
647 var browser = agents[name]
648 browserslist.data[name] = {
650 versions: normalize(agents[name].versions),
651 released: normalize(agents[name].versions.slice(0, -3)),
652 releaseDate: agents[name].release_date
654 fillUsage(browserslist.usage.global, name, browser.usage_global)
656 browserslist.versionAliases[name] = { }
657 for (var i = 0; i < browser.versions.length; i++) {
658 var full = browser.versions[i]
661 if (full.indexOf('-') !== -1) {
662 var interval = full.split('-')
663 for (var j = 0; j < interval.length; j++) {
664 browserslist.versionAliases[name][interval[j]] = full
671 module.exports = browserslist