index.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. var path = require('path');
  2. var e2c = require('electron-to-chromium/versions');
  3. var fs = require('fs');
  4. var caniuse = require('caniuse-db/data.json').agents;
  5. Object.keys(caniuse).forEach(function (key) {
  6. caniuse[key].versions = caniuse[key].versions.filter(Boolean);
  7. });
  8. var FLOAT_RANGE = /^\d+(\.\d+)?(-\d+(\.\d+)?)*$/;
  9. var IS_SECTION = /^\s*\[(.+)\]\s*$/;
  10. function uniq(array) {
  11. var filtered = [];
  12. for ( var i = 0; i < array.length; i++ ) {
  13. if ( filtered.indexOf(array[i]) === -1 ) filtered.push(array[i]);
  14. }
  15. return filtered;
  16. }
  17. function BrowserslistError(message) {
  18. this.name = 'BrowserslistError';
  19. this.message = message || '';
  20. this.browserslist = true;
  21. if ( Error.captureStackTrace ) {
  22. Error.captureStackTrace(this, BrowserslistError);
  23. }
  24. }
  25. BrowserslistError.prototype = Error.prototype;
  26. // Helpers
  27. function error(name) {
  28. throw new BrowserslistError(name);
  29. }
  30. function fillUsage(result, name, data) {
  31. for ( var i in data ) {
  32. result[name + ' ' + i] = data[i];
  33. }
  34. }
  35. function isFile(file) {
  36. return fs.existsSync(file) && fs.statSync(file).isFile();
  37. }
  38. function eachParent(file, callback) {
  39. if ( !fs.readFileSync || !fs.existsSync || !fs.statSync ) {
  40. /* istanbul ignore next */
  41. return undefined;
  42. }
  43. if ( file === false ) return undefined;
  44. if ( typeof file === 'undefined' ) file = '.';
  45. var dirs = path.resolve(file).split(path.sep);
  46. while ( dirs.length ) {
  47. var result = callback(dirs.join(path.sep));
  48. if (typeof result !== 'undefined') return result;
  49. dirs.pop();
  50. }
  51. return undefined;
  52. }
  53. function getStat(opts) {
  54. if ( opts.stats ) {
  55. return opts.stats;
  56. } else if ( process.env.BROWSERSLIST_STATS ) {
  57. return process.env.BROWSERSLIST_STATS;
  58. } else {
  59. return eachParent(opts.path, function (dir) {
  60. var file = path.join(dir, 'browserslist-stats.json');
  61. if ( isFile(file) ) {
  62. return file;
  63. }
  64. });
  65. }
  66. }
  67. function parsePackage(file) {
  68. var config = JSON.parse(fs.readFileSync(file)).browserslist;
  69. if ( typeof config === 'object' && config.length ) {
  70. config = { defaults: config };
  71. }
  72. return config;
  73. }
  74. function pickEnv(config, opts) {
  75. if ( typeof config !== 'object' ) return config;
  76. var env;
  77. if ( typeof opts.env === 'string' ) {
  78. env = opts.env;
  79. } else if ( typeof process.env.BROWSERSLIST_ENV === 'string' ) {
  80. env = process.env.BROWSERSLIST_ENV;
  81. } else if ( typeof process.env.NODE_ENV === 'string' ) {
  82. env = process.env.NODE_ENV;
  83. } else {
  84. env = 'development';
  85. }
  86. return config[env] || config.defaults;
  87. }
  88. function generateFilter(sign, version) {
  89. version = parseFloat(version);
  90. if ( sign === '>' ) {
  91. return function (v) {
  92. return parseFloat(v) > version;
  93. };
  94. } else if ( sign === '>=' ) {
  95. return function (v) {
  96. return parseFloat(v) >= version;
  97. };
  98. } else if ( sign === '<' ) {
  99. return function (v) {
  100. return parseFloat(v) < version;
  101. };
  102. } else if ( sign === '<=' ) {
  103. return function (v) {
  104. return parseFloat(v) <= version;
  105. };
  106. }
  107. }
  108. /**
  109. * Return array of browsers by selection queries.
  110. *
  111. * @param {string[]} queries Browser queries.
  112. * @param {object} opts Options.
  113. * @param {string} [opts.path="."] Path to processed file.
  114. * It will be used to find config files.
  115. * @param {string} [opts.env="development"] Processing environment.
  116. * It will be used to take right
  117. * queries from config file.
  118. * @param {string} [opts.config] Path to config file with queries.
  119. * @param {object} [opts.stats] Custom browser usage statistics
  120. * for "> 1% in my stats" query.
  121. * @return {string[]} Array with browser names in Can I Use.
  122. *
  123. * @example
  124. * browserslist('IE >= 10, IE 8') //=> ['ie 11', 'ie 10', 'ie 8']
  125. */
  126. var browserslist = function (queries, opts) {
  127. if ( typeof opts === 'undefined' ) opts = { };
  128. if ( typeof queries === 'undefined' || queries === null ) {
  129. if ( process.env.BROWSERSLIST ) {
  130. queries = process.env.BROWSERSLIST;
  131. } else if ( opts.config || process.env.BROWSERSLIST_CONFIG ) {
  132. var file = opts.config || process.env.BROWSERSLIST_CONFIG;
  133. queries = pickEnv(browserslist.readConfig(file), opts);
  134. } else {
  135. queries = pickEnv(browserslist.findConfig(opts.path), opts);
  136. }
  137. }
  138. if ( typeof queries === 'undefined' || queries === null ) {
  139. queries = browserslist.defaults;
  140. }
  141. if ( typeof queries === 'string' ) {
  142. queries = queries.split(/,\s*/);
  143. }
  144. var context = { };
  145. var stats = getStat(opts);
  146. if ( stats ) {
  147. if ( typeof stats === 'string' ) {
  148. try {
  149. stats = JSON.parse(fs.readFileSync(stats));
  150. } catch (e) {
  151. error('Can\'t read ' + stats);
  152. }
  153. }
  154. if ( 'dataByBrowser' in stats ) {
  155. stats = stats.dataByBrowser;
  156. }
  157. context.customUsage = { };
  158. for ( var browser in stats ) {
  159. fillUsage(context.customUsage, browser, stats[browser]);
  160. }
  161. }
  162. var result = [];
  163. queries.forEach(function (selection) {
  164. if ( selection.trim() === '' ) return;
  165. var exclude = selection.indexOf('not ') === 0;
  166. if ( exclude ) selection = selection.slice(4);
  167. for ( var i in browserslist.queries ) {
  168. var type = browserslist.queries[i];
  169. var match = selection.match(type.regexp);
  170. if ( match ) {
  171. var args = [context].concat(match.slice(1));
  172. var array = type.select.apply(browserslist, args);
  173. if ( exclude ) {
  174. result = result.filter(function (j) {
  175. return array.indexOf(j) === -1;
  176. });
  177. } else {
  178. result = result.concat(array);
  179. }
  180. return;
  181. }
  182. }
  183. error('Unknown browser query `' + selection + '`');
  184. });
  185. result = result.map(function (i) {
  186. var parts = i.split(' ');
  187. var name = parts[0];
  188. var version = parts[1];
  189. if ( version === '0' ) {
  190. return name + ' ' + browserslist.byName(name).versions[0];
  191. } else {
  192. return i;
  193. }
  194. }).sort(function (name1, name2) {
  195. name1 = name1.split(' ');
  196. name2 = name2.split(' ');
  197. if ( name1[0] === name2[0] ) {
  198. if ( FLOAT_RANGE.test(name1[1]) && FLOAT_RANGE.test(name2[1]) ) {
  199. return parseFloat(name2[1]) - parseFloat(name1[1]);
  200. } else {
  201. return name2[1].localeCompare(name1[1]);
  202. }
  203. } else {
  204. return name1[0].localeCompare(name2[0]);
  205. }
  206. });
  207. return uniq(result);
  208. };
  209. var normalizeVersion = function (data, version) {
  210. if ( data.versions.indexOf(version) !== -1 ) {
  211. return version;
  212. } else {
  213. return browserslist.versionAliases[data.name][version];
  214. }
  215. };
  216. var loadCountryStatistics = function (country) {
  217. if ( !browserslist.usage[country] ) {
  218. var usage = { };
  219. var data = require(
  220. 'caniuse-db/region-usage-json/' + country + '.json');
  221. for ( var i in data.data ) {
  222. fillUsage(usage, i, data.data[i]);
  223. }
  224. browserslist.usage[country] = usage;
  225. }
  226. };
  227. // Will be filled by Can I Use data below
  228. browserslist.data = { };
  229. browserslist.usage = {
  230. global: { },
  231. custom: null
  232. };
  233. // Default browsers query
  234. browserslist.defaults = [
  235. '> 1%',
  236. 'last 2 versions',
  237. 'Firefox ESR'
  238. ];
  239. // What browsers will be used in `last n version` query
  240. browserslist.major = [
  241. 'safari', 'opera', 'ios_saf', 'ie_mob', 'ie', 'edge', 'firefox', 'chrome'
  242. ];
  243. // Browser names aliases
  244. browserslist.aliases = {
  245. fx: 'firefox',
  246. ff: 'firefox',
  247. ios: 'ios_saf',
  248. explorer: 'ie',
  249. blackberry: 'bb',
  250. explorermobile: 'ie_mob',
  251. operamini: 'op_mini',
  252. operamobile: 'op_mob',
  253. chromeandroid: 'and_chr',
  254. firefoxandroid: 'and_ff',
  255. ucandroid: 'and_uc'
  256. };
  257. // Aliases to work with joined versions like `ios_saf 7.0-7.1`
  258. browserslist.versionAliases = { };
  259. // Get browser data by alias or case insensitive name
  260. browserslist.byName = function (name) {
  261. name = name.toLowerCase();
  262. name = browserslist.aliases[name] || name;
  263. return browserslist.data[name];
  264. };
  265. // Get browser data by alias or case insensitive name and throw error
  266. // on unknown browser
  267. browserslist.checkName = function (name) {
  268. var data = browserslist.byName(name);
  269. if ( !data ) error('Unknown browser ' + name);
  270. return data;
  271. };
  272. // Read and parse config
  273. browserslist.readConfig = function (file) {
  274. if ( !fs.existsSync(file) || !fs.statSync(file).isFile() ) {
  275. error('Can\'t read ' + file + ' config');
  276. }
  277. return browserslist.parseConfig(fs.readFileSync(file));
  278. };
  279. // Find config, read file and parse it
  280. browserslist.findConfig = function (from) {
  281. return eachParent(from, function (dir) {
  282. var config = path.join(dir, 'browserslist');
  283. var pkg = path.join(dir, 'package.json');
  284. var pkgBrowserslist;
  285. if ( isFile(pkg) ) {
  286. try {
  287. pkgBrowserslist = parsePackage(pkg);
  288. } catch (e) {
  289. console.warn(
  290. '[Browserslist] Could not parse ' + pkg + '. ' +
  291. 'Ignoring it.');
  292. }
  293. }
  294. if ( isFile(config) && pkgBrowserslist ) {
  295. error(
  296. dir + ' contains both browserslist ' +
  297. 'and package.json with browsers');
  298. } else if ( isFile(config) ) {
  299. return browserslist.readConfig(config);
  300. } else if ( pkgBrowserslist ) {
  301. return pkgBrowserslist;
  302. }
  303. });
  304. };
  305. /**
  306. * Return browsers market coverage.
  307. *
  308. * @param {string[]} browsers Browsers names in Can I Use.
  309. * @param {string} [country="global"] Which country statistics should be used.
  310. *
  311. * @return {number} Total market coverage for all selected browsers.
  312. *
  313. * @example
  314. * browserslist.coverage(browserslist('> 1% in US'), 'US') //=> 83.1
  315. */
  316. browserslist.coverage = function (browsers, country) {
  317. if ( country && country !== 'global') {
  318. country = country.toUpperCase();
  319. loadCountryStatistics(country);
  320. } else {
  321. country = 'global';
  322. }
  323. return browsers.reduce(function (all, i) {
  324. var usage = browserslist.usage[country][i];
  325. if ( usage === undefined ) {
  326. usage = browserslist.usage[country][i.replace(/ [\d.]+$/, ' 0')];
  327. }
  328. return all + (usage || 0);
  329. }, 0);
  330. };
  331. // Return array of queries from config content
  332. browserslist.parseConfig = function (string) {
  333. var result = { defaults: [] };
  334. var section = 'defaults';
  335. string.toString()
  336. .replace(/#[^\n]*/g, '')
  337. .split(/\n/)
  338. .map(function (line) {
  339. return line.trim();
  340. })
  341. .filter(function (line) {
  342. return line !== '';
  343. })
  344. .forEach(function (line) {
  345. if ( IS_SECTION.test(line) ) {
  346. section = line.match(IS_SECTION)[1].trim();
  347. result[section] = result[section] || [];
  348. } else {
  349. result[section].push(line);
  350. }
  351. });
  352. return result;
  353. };
  354. browserslist.queries = {
  355. lastVersions: {
  356. regexp: /^last\s+(\d+)\s+versions?$/i,
  357. select: function (context, versions) {
  358. var selected = [];
  359. browserslist.major.forEach(function (name) {
  360. var data = browserslist.byName(name);
  361. if ( !data ) return;
  362. var array = data.released.slice(-versions);
  363. array = array.map(function (v) {
  364. return data.name + ' ' + v;
  365. });
  366. selected = selected.concat(array);
  367. });
  368. return selected;
  369. }
  370. },
  371. lastByBrowser: {
  372. regexp: /^last\s+(\d+)\s+(\w+)\s+versions?$/i,
  373. select: function (context, versions, name) {
  374. var data = browserslist.checkName(name);
  375. return data.released.slice(-versions).map(function (v) {
  376. return data.name + ' ' + v;
  377. });
  378. }
  379. },
  380. globalStatistics: {
  381. regexp: /^>\s*(\d*\.?\d+)%$/,
  382. select: function (context, popularity) {
  383. popularity = parseFloat(popularity);
  384. var result = [];
  385. for ( var version in browserslist.usage.global ) {
  386. if ( browserslist.usage.global[version] > popularity ) {
  387. result.push(version);
  388. }
  389. }
  390. return result;
  391. }
  392. },
  393. customStatistics: {
  394. regexp: /^>\s*(\d*\.?\d+)%\s+in\s+my\s+stats$/,
  395. select: function (context, popularity) {
  396. popularity = parseFloat(popularity);
  397. var result = [];
  398. if ( !context.customUsage ) {
  399. error('Custom usage statistics was not provided');
  400. }
  401. for ( var version in context.customUsage ) {
  402. if ( context.customUsage[version] > popularity ) {
  403. result.push(version);
  404. }
  405. }
  406. return result;
  407. }
  408. },
  409. countryStatistics: {
  410. regexp: /^>\s*(\d*\.?\d+)%\s+in\s+(\w\w)$/,
  411. select: function (context, popularity, country) {
  412. popularity = parseFloat(popularity);
  413. country = country.toUpperCase();
  414. var result = [];
  415. loadCountryStatistics(country);
  416. var usage = browserslist.usage[country];
  417. for ( var version in usage ) {
  418. if ( usage[version] > popularity ) {
  419. result.push(version);
  420. }
  421. }
  422. return result;
  423. }
  424. },
  425. electronRange: {
  426. regexp: /^electron\s+([\d\.]+)\s*-\s*([\d\.]+)$/i,
  427. select: function (context, from, to) {
  428. if ( !e2c[from] ) error('Unknown version ' + from + ' of electron');
  429. if ( !e2c[to] ) error('Unknown version ' + to + ' of electron');
  430. from = parseFloat(from);
  431. to = parseFloat(to);
  432. return Object.keys(e2c).filter(function (i) {
  433. var parsed = parseFloat(i);
  434. return parsed >= from && parsed <= to;
  435. }).map(function (i) {
  436. return 'chrome ' + e2c[i];
  437. });
  438. }
  439. },
  440. range: {
  441. regexp: /^(\w+)\s+([\d\.]+)\s*-\s*([\d\.]+)$/i,
  442. select: function (context, name, from, to) {
  443. var data = browserslist.checkName(name);
  444. from = parseFloat(normalizeVersion(data, from) || from);
  445. to = parseFloat(normalizeVersion(data, to) || to);
  446. var filter = function (v) {
  447. var parsed = parseFloat(v);
  448. return parsed >= from && parsed <= to;
  449. };
  450. return data.released.filter(filter).map(function (v) {
  451. return data.name + ' ' + v;
  452. });
  453. }
  454. },
  455. electronVersions: {
  456. regexp: /^electron\s*(>=?|<=?)\s*([\d\.]+)$/i,
  457. select: function (context, sign, version) {
  458. return Object.keys(e2c)
  459. .filter(generateFilter(sign, version))
  460. .map(function (i) {
  461. return 'chrome ' + e2c[i];
  462. });
  463. }
  464. },
  465. versions: {
  466. regexp: /^(\w+)\s*(>=?|<=?)\s*([\d\.]+)$/,
  467. select: function (context, name, sign, version) {
  468. var data = browserslist.checkName(name);
  469. var alias = normalizeVersion(data, version);
  470. if ( alias ) {
  471. version = alias;
  472. }
  473. return data.released
  474. .filter(generateFilter(sign, version))
  475. .map(function (v) {
  476. return data.name + ' ' + v;
  477. });
  478. }
  479. },
  480. esr: {
  481. regexp: /^(firefox|ff|fx)\s+esr$/i,
  482. select: function () {
  483. return ['firefox 45'];
  484. }
  485. },
  486. opMini: {
  487. regexp: /(operamini|op_mini)\s+all/i,
  488. select: function () {
  489. return ['op_mini all'];
  490. }
  491. },
  492. electron: {
  493. regexp: /^electron\s+([\d\.]+)$/i,
  494. select: function (context, version) {
  495. var chrome = e2c[version];
  496. if ( !chrome ) error('Unknown version ' + version + ' of electron');
  497. return ['chrome ' + chrome];
  498. }
  499. },
  500. direct: {
  501. regexp: /^(\w+)\s+(tp|[\d\.]+)$/i,
  502. select: function (context, name, version) {
  503. if ( /tp/i.test(version) ) version = 'TP';
  504. var data = browserslist.checkName(name);
  505. var alias = normalizeVersion(data, version);
  506. if ( alias ) {
  507. version = alias;
  508. } else {
  509. if ( version.indexOf('.') === -1 ) {
  510. alias = version + '.0';
  511. } else if ( /\.0$/.test(version) ) {
  512. alias = version.replace(/\.0$/, '');
  513. }
  514. alias = normalizeVersion(data, alias);
  515. if ( alias ) {
  516. version = alias;
  517. } else {
  518. error('Unknown version ' + version + ' of ' + name);
  519. }
  520. }
  521. return [data.name + ' ' + version];
  522. }
  523. },
  524. defaults: {
  525. regexp: /^defaults$/i,
  526. select: function () {
  527. return browserslist(browserslist.defaults);
  528. }
  529. }
  530. };
  531. // Get and convert Can I Use data
  532. (function () {
  533. for ( var name in caniuse ) {
  534. var browser = caniuse[name];
  535. browserslist.data[name] = {
  536. name: name,
  537. versions: browser.versions,
  538. released: browser.versions
  539. };
  540. fillUsage(browserslist.usage.global, name, browser.usage_global);
  541. browserslist.versionAliases[name] = { };
  542. for ( var i = 0; i < browser.versions.length; i++ ) {
  543. var full = browser.versions[i];
  544. if ( full.indexOf('-') !== -1 ) {
  545. var interval = full.split('-');
  546. for ( var j = 0; j < interval.length; j++ ) {
  547. browserslist.versionAliases[name][interval[j]] = full;
  548. }
  549. }
  550. }
  551. }
  552. }());
  553. module.exports = browserslist;