UNPKG

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