index.js 19 KB

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