options-manager.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. 'use strict';
  2. const os = require('os');
  3. const path = require('path');
  4. const arrify = require('arrify');
  5. const mergeWith = require('lodash.mergewith');
  6. const multimatch = require('multimatch');
  7. const pathExists = require('path-exists');
  8. const pkgConf = require('pkg-conf');
  9. const resolveFrom = require('resolve-from');
  10. const prettier = require('prettier');
  11. const semver = require('semver');
  12. const DEFAULT_IGNORE = [
  13. '**/node_modules/**',
  14. '**/bower_components/**',
  15. 'flow-typed/**',
  16. 'coverage/**',
  17. '{tmp,temp}/**',
  18. '**/*.min.js',
  19. '**/bundle.js',
  20. 'fixture{-*,}.{js,jsx}',
  21. 'fixture{s,}/**',
  22. '{test,tests,spec,__tests__}/fixture{s,}/**',
  23. 'vendor/**',
  24. 'dist/**'
  25. ];
  26. const DEFAULT_EXTENSION = [
  27. 'js',
  28. 'jsx'
  29. ];
  30. const DEFAULT_CONFIG = {
  31. useEslintrc: false,
  32. cache: true,
  33. cacheLocation: path.join(os.homedir() || os.tmpdir(), '.xo-cache/'),
  34. baseConfig: {
  35. extends: [
  36. 'xo',
  37. path.join(__dirname, '../config/overrides.js'),
  38. path.join(__dirname, '../config/plugins.js')
  39. ]
  40. }
  41. };
  42. /**
  43. * Define the rules that are enabled only for specific version of Node, based on `engines.node` in package.json or the `node-version` option.
  44. *
  45. * The keys are rule names and the values are an Object with a valid semver (`4.0.0` is valid `4` is not) as keys and the rule configuration as values.
  46. * Each entry define the rule configuration and the minimum Node version for which to set it.
  47. * The entry with the highest version that is compliant with the `engines.node`/`node-version` range will be used.
  48. *
  49. * @type {Object}
  50. *
  51. * @example
  52. * ```javascript
  53. * {
  54. * 'plugin/rule': {
  55. * '6.0.0': ['error', {prop: 'node-6-conf'}],
  56. * '8.0.0': ['error', {prop: 'node-8-conf'}]
  57. * }
  58. * }
  59. *```
  60. * With `engines.node` set to `>=4` the rule `plugin/rule` will not be used.
  61. * With `engines.node` set to `>=6` the rule `plugin/rule` will be used with the config `{prop: 'node-6-conf'}`.
  62. * With `engines.node` set to `>=8` the rule `plugin/rule` will be used with the config `{prop: 'node-8-conf'}`.
  63. */
  64. const ENGINE_RULES = {
  65. 'promise/prefer-await-to-then': {
  66. '7.6.0': 'error'
  67. }
  68. };
  69. // Keep the same behaviour in mergeWith as deepAssign
  70. const mergeFn = (prev, val) => {
  71. if (Array.isArray(prev) && Array.isArray(val)) {
  72. return val;
  73. }
  74. };
  75. const normalizeOpts = opts => {
  76. opts = Object.assign({}, opts);
  77. // Aliases for humans
  78. const aliases = [
  79. 'env',
  80. 'global',
  81. 'ignore',
  82. 'plugin',
  83. 'rule',
  84. 'setting',
  85. 'extend',
  86. 'extension'
  87. ];
  88. for (const singular of aliases) {
  89. const plural = singular + 's';
  90. let value = opts[plural] || opts[singular];
  91. delete opts[singular];
  92. if (value === undefined) {
  93. continue;
  94. }
  95. if (singular !== 'rule' && singular !== 'setting') {
  96. value = arrify(value);
  97. }
  98. opts[plural] = value;
  99. }
  100. return opts;
  101. };
  102. const mergeWithPkgConf = opts => {
  103. opts = Object.assign({cwd: process.cwd()}, opts);
  104. opts.cwd = path.resolve(opts.cwd);
  105. const conf = pkgConf.sync('xo', {cwd: opts.cwd, skipOnFalse: true});
  106. const engines = pkgConf.sync('engines', {cwd: opts.cwd});
  107. return Object.assign({}, conf, {engines}, opts);
  108. };
  109. const normalizeSpaces = opts => typeof opts.space === 'number' ? opts.space : 2;
  110. const mergeWithPrettierConf = (opts, prettierOpts) => {
  111. if ((opts.semicolon === true && prettierOpts.semi === false) ||
  112. (opts.semicolon === false && prettierOpts.semi === true)) {
  113. throw new Error(`The Prettier config \`semi\` is ${prettierOpts.semi} while XO \`semicolon\` is ${opts.semicolon}`);
  114. }
  115. if (((opts.space === true || typeof opts.space === 'number') && prettierOpts.useTabs === true) ||
  116. ((opts.space === false) && prettierOpts.useTabs === false)) {
  117. throw new Error(`The Prettier config \`useTabs\` is ${prettierOpts.useTabs} while XO \`space\` is ${opts.space}`);
  118. }
  119. if (typeof opts.space === 'number' && typeof prettierOpts.tabWidth === 'number' && opts.space !== prettierOpts.tabWidth) {
  120. throw new Error(`The Prettier config \`tabWidth\` is ${prettierOpts.tabWidth} while XO \`space\` is ${opts.space}`);
  121. }
  122. return mergeWith(
  123. {},
  124. {
  125. singleQuote: true,
  126. bracketSpacing: false,
  127. jsxBracketSameLine: false,
  128. trailingComma: 'none',
  129. tabWidth: normalizeSpaces(opts),
  130. useTabs: !opts.space,
  131. semi: opts.semicolon !== false
  132. },
  133. prettierOpts,
  134. mergeFn
  135. );
  136. };
  137. // Define the shape of deep properties for mergeWith
  138. const emptyOptions = () => ({
  139. rules: {},
  140. settings: {},
  141. globals: [],
  142. envs: [],
  143. plugins: [],
  144. extends: []
  145. });
  146. const buildConfig = opts => {
  147. const config = mergeWith(
  148. emptyOptions(),
  149. DEFAULT_CONFIG,
  150. opts,
  151. mergeFn
  152. );
  153. const spaces = normalizeSpaces(opts);
  154. if (opts.engines && opts.engines.node && semver.validRange(opts.engines.node)) {
  155. for (const rule of Object.keys(ENGINE_RULES)) {
  156. // Use the rule value for the highest version that is lower or equal to the oldest version of Node.js supported
  157. for (const minVersion of Object.keys(ENGINE_RULES[rule]).sort(semver.compare)) {
  158. if (!semver.intersects(opts.engines.node, `<${minVersion}`)) {
  159. config.rules[rule] = ENGINE_RULES[rule][minVersion];
  160. }
  161. }
  162. }
  163. }
  164. if (opts.space && !opts.prettier) {
  165. config.rules.indent = ['error', spaces, {SwitchCase: 1}];
  166. // Only apply if the user has the React plugin
  167. if (opts.cwd && resolveFrom.silent(opts.cwd, 'eslint-plugin-react')) {
  168. config.plugins = config.plugins.concat('react');
  169. config.rules['react/jsx-indent-props'] = ['error', spaces];
  170. config.rules['react/jsx-indent'] = ['error', spaces];
  171. }
  172. }
  173. if (opts.semicolon === false && !opts.prettier) {
  174. config.rules.semi = ['error', 'never'];
  175. config.rules['semi-spacing'] = ['error', {
  176. before: false,
  177. after: true
  178. }];
  179. }
  180. if (opts.esnext !== false) {
  181. config.baseConfig.extends = [
  182. 'xo/esnext',
  183. path.join(__dirname, '../config/plugins.js')
  184. ];
  185. }
  186. if (opts.rules) {
  187. Object.assign(config.rules, opts.rules);
  188. }
  189. if (opts.settings) {
  190. config.baseConfig.settings = opts.settings;
  191. }
  192. if (opts.parser) {
  193. config.baseConfig.parser = opts.parser;
  194. }
  195. if (opts.extends && opts.extends.length > 0) {
  196. // TODO: This logic needs to be improved, preferably use the same code as ESLint
  197. // user's configs must be resolved to their absolute paths
  198. const configs = opts.extends.map(name => {
  199. // Don't do anything if it's a filepath
  200. if (pathExists.sync(name)) {
  201. return name;
  202. }
  203. // Don't do anything if it's a config from a plugin
  204. if (name.startsWith('plugin:')) {
  205. return name;
  206. }
  207. if (!name.includes('eslint-config-')) {
  208. name = `eslint-config-${name}`;
  209. }
  210. const ret = resolveFrom(opts.cwd, name);
  211. if (!ret) {
  212. throw new Error(`Couldn't find ESLint config: ${name}`);
  213. }
  214. return ret;
  215. });
  216. config.baseConfig.extends = config.baseConfig.extends.concat(configs);
  217. }
  218. // If the user sets the `prettier` options then add the `prettier` plugin and config
  219. if (opts.prettier) {
  220. // Disable formatting rules conflicting with Prettier
  221. config.rules['unicorn/number-literal-case'] = 'off';
  222. // The prettier plugin uses Prettier to format the code with `--fix`
  223. config.plugins = config.plugins.concat('prettier');
  224. // The prettier config overrides ESLint stylistic rules that are handled by Prettier
  225. config.baseConfig.extends = config.baseConfig.extends.concat('prettier');
  226. // The `prettier/prettier` rule reports errors if the code is not formatted in accordance to Prettier
  227. config.rules['prettier/prettier'] = [
  228. 'error', mergeWithPrettierConf(opts, prettier.resolveConfig.sync(opts.cwd || process.cwd()) || {})
  229. ];
  230. // If the user has the React, Flowtype or Standard plugin, add the corresponding Prettier rule overrides
  231. // See https://github.com/prettier/eslint-config-prettier for the list of plugins overrrides
  232. for (const override of ['react', 'flowtype', 'standard']) {
  233. if (opts.cwd && resolveFrom.silent(opts.cwd, `eslint-plugin-${override}`)) {
  234. config.baseConfig.extends = config.baseConfig.extends.concat(`prettier/${override}`);
  235. }
  236. }
  237. }
  238. return config;
  239. };
  240. // Builds a list of overrides for a particular path, and a hash value.
  241. // The hash value is a binary representation of which elements in the `overrides` array apply to the path.
  242. //
  243. // If overrides.length === 4, and only the first and third elements apply, then our hash is: 1010 (in binary)
  244. const findApplicableOverrides = (path, overrides) => {
  245. let hash = 0;
  246. const applicable = [];
  247. for (const override of overrides) {
  248. hash <<= 1;
  249. if (multimatch(path, override.files).length > 0) {
  250. applicable.push(override);
  251. hash |= 1;
  252. }
  253. }
  254. return {
  255. hash,
  256. applicable
  257. };
  258. };
  259. const mergeApplicableOverrides = (baseOptions, applicableOverrides) => {
  260. applicableOverrides = applicableOverrides.map(override => normalizeOpts(override));
  261. const overrides = [emptyOptions(), baseOptions].concat(applicableOverrides, mergeFn);
  262. return mergeWith(...overrides);
  263. };
  264. // Creates grouped sets of merged options together with the paths they apply to.
  265. const groupConfigs = (paths, baseOptions, overrides) => {
  266. const map = {};
  267. const arr = [];
  268. for (const x of paths) {
  269. const data = findApplicableOverrides(x, overrides);
  270. if (!map[data.hash]) {
  271. const mergedOpts = mergeApplicableOverrides(baseOptions, data.applicable);
  272. delete mergedOpts.files;
  273. arr.push(map[data.hash] = {
  274. opts: mergedOpts,
  275. paths: []
  276. });
  277. }
  278. map[data.hash].paths.push(x);
  279. }
  280. return arr;
  281. };
  282. const getIgnores = opts => {
  283. opts.ignores = DEFAULT_IGNORE.concat(opts.ignores || []);
  284. return opts;
  285. };
  286. const preprocess = opts => {
  287. opts = mergeWithPkgConf(opts);
  288. opts = normalizeOpts(opts);
  289. opts = getIgnores(opts);
  290. opts.extensions = DEFAULT_EXTENSION.concat(opts.extensions || []);
  291. return opts;
  292. };
  293. module.exports.DEFAULT_IGNORE = DEFAULT_IGNORE;
  294. module.exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
  295. module.exports.mergeWithPkgConf = mergeWithPkgConf;
  296. module.exports.mergeWithPrettierConf = mergeWithPrettierConf;
  297. module.exports.normalizeOpts = normalizeOpts;
  298. module.exports.buildConfig = buildConfig;
  299. module.exports.findApplicableOverrides = findApplicableOverrides;
  300. module.exports.mergeApplicableOverrides = mergeApplicableOverrides;
  301. module.exports.groupConfigs = groupConfigs;
  302. module.exports.preprocess = preprocess;
  303. module.exports.emptyOptions = emptyOptions;
  304. module.exports.getIgnores = getIgnores;