index.js 17 KB

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