UNPKG

6.44 kBJavaScriptView Raw
1'use strict';
2const os = require('os');
3const path = require('path');
4const arrify = require('arrify');
5const pkgConf = require('pkg-conf');
6const deepAssign = require('deep-assign');
7const multimatch = require('multimatch');
8const resolveFrom = require('resolve-from');
9const pathExists = require('path-exists');
10const parseGitignore = require('parse-gitignore');
11const globby = require('globby');
12
13const DEFAULT_IGNORE = [
14 '**/node_modules/**',
15 '**/bower_components/**',
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
27const DEFAULT_EXTENSION = [
28 'js',
29 'jsx'
30];
31
32const DEFAULT_CONFIG = {
33 useEslintrc: false,
34 cache: true,
35 cacheLocation: path.join(os.homedir() || os.tmpdir(), '.xo-cache/'),
36 baseConfig: {
37 extends: [
38 'xo',
39 path.join(__dirname, 'config/overrides.js'),
40 path.join(__dirname, 'config/plugins.js')
41 ]
42 }
43};
44
45const normalizeOpts = opts => {
46 opts = Object.assign({}, opts);
47
48 // Alias to help humans
49 [
50 'env',
51 'global',
52 'ignore',
53 'plugin',
54 'rule',
55 'setting',
56 'extend',
57 'extension'
58 ].forEach(singular => {
59 const plural = singular + 's';
60 let value = opts[plural] || opts[singular];
61
62 delete opts[singular];
63
64 if (value === undefined) {
65 return;
66 }
67
68 if (singular !== 'rule' && singular !== 'setting') {
69 value = arrify(value);
70 }
71
72 opts[plural] = value;
73 });
74
75 return opts;
76}
77
78const mergeWithPkgConf = opts => {
79 opts = Object.assign({cwd: process.cwd()}, opts);
80 const conf = pkgConf.sync('xo', {cwd: opts.cwd, skipOnFalse: true});
81 return Object.assign({}, conf, opts);
82}
83
84// Define the shape of deep properties for deepAssign
85const emptyOptions = () => ({
86 rules: {},
87 settings: {},
88 globals: [],
89 envs: [],
90 plugins: [],
91 extends: []
92});
93
94const buildConfig = opts => {
95 const config = deepAssign(
96 emptyOptions(),
97 DEFAULT_CONFIG,
98 opts
99 );
100
101 if (opts.space) {
102 const spaces = typeof opts.space === 'number' ? opts.space : 2;
103 config.rules.indent = ['error', spaces, {SwitchCase: 1}];
104
105 // Only apply if the user has the React plugin
106 if (opts.cwd && resolveFrom(opts.cwd, 'eslint-plugin-react')) {
107 config.plugins = config.plugins.concat('react');
108 config.rules['react/jsx-indent-props'] = ['error', spaces];
109 config.rules['react/jsx-indent'] = ['error', spaces];
110 }
111 }
112
113 if (opts.semicolon === false) {
114 config.rules.semi = ['error', 'never'];
115 config.rules['semi-spacing'] = ['error', {
116 before: false,
117 after: true
118 }];
119 }
120
121 if (opts.esnext !== false) {
122 config.baseConfig.extends = ['xo/esnext', path.join(__dirname, 'config/plugins.js')];
123 }
124
125 if (opts.rules) {
126 Object.assign(config.rules, opts.rules);
127 }
128
129 if (opts.settings) {
130 config.baseConfig.settings = opts.settings;
131 }
132
133 if (opts.parser) {
134 config.baseConfig.parser = opts.parser;
135 }
136
137 if (opts.extends && opts.extends.length > 0) {
138 // TODO: this logic needs to be improved, preferably use the same code as ESLint
139 // user's configs must be resolved to their absolute paths
140 const configs = opts.extends.map(name => {
141 // Don't do anything if it's a filepath
142 if (pathExists.sync(name)) {
143 return name;
144 }
145
146 // Don't do anything if it's a config from a plugin
147 if (name.startsWith('plugin:')) {
148 return name;
149 }
150
151 if (!name.includes('eslint-config-')) {
152 name = `eslint-config-${name}`;
153 }
154
155 const ret = resolveFrom(opts.cwd, name);
156
157 if (!ret) {
158 throw new Error(`Couldn't find ESLint config: ${name}`);
159 }
160
161 return ret;
162 });
163
164 config.baseConfig.extends = config.baseConfig.extends.concat(configs);
165 }
166
167 return config;
168}
169
170// Builds a list of overrides for a particular path, and a hash value.
171// The hash value is a binary representation of which elements in the `overrides` array apply to the path.
172//
173// If overrides.length === 4, and only the first and third elements apply, then our hash is: 1010 (in binary)
174const findApplicableOverrides = (path, overrides) => {
175 let hash = 0;
176 const applicable = [];
177
178 overrides.forEach(override => {
179 hash <<= 1;
180
181 if (multimatch(path, override.files).length > 0) {
182 applicable.push(override);
183 hash |= 1;
184 }
185 });
186
187 return {
188 hash,
189 applicable
190 };
191}
192
193const mergeApplicableOverrides = (baseOptions, applicableOverrides) => {
194 applicableOverrides = applicableOverrides.map(normalizeOpts);
195 const overrides = [emptyOptions(), baseOptions].concat(applicableOverrides);
196 return deepAssign.apply(null, overrides);
197}
198
199// Creates grouped sets of merged options together with the paths they apply to.
200const groupConfigs = (paths, baseOptions, overrides) => {
201 const map = {};
202 const arr = [];
203
204 paths.forEach(x => {
205 const data = findApplicableOverrides(x, overrides);
206
207 if (!map[data.hash]) {
208 const mergedOpts = mergeApplicableOverrides(baseOptions, data.applicable);
209 delete mergedOpts.files;
210
211 arr.push(map[data.hash] = {
212 opts: mergedOpts,
213 paths: []
214 });
215 }
216
217 map[data.hash].paths.push(x);
218 });
219
220 return arr;
221}
222
223const getIgnores = opts => {
224 opts.ignores = DEFAULT_IGNORE.concat(opts.ignores || []);
225 return opts;
226}
227
228const getGitIgnores = opts => globby
229 .sync('**/.gitignore', {
230 ignore: opts.ignores || [],
231 cwd: opts.cwd || process.cwd()
232 })
233 .map(pathToGitignore => {
234 const patterns = parseGitignore(pathToGitignore);
235 const base = path.dirname(pathToGitignore);
236
237 return patterns
238 .map(pattern => {
239 const negate = !pattern.startsWith('!');
240 const patternPath = negate ? pattern : pattern.substr(1);
241 return {negate, pattern: path.join(base, patternPath)};
242 })
243 .sort(pattern => pattern.negate ? 1 : -1)
244 .map(item => item.negate ? `!${item.pattern}` : item.pattern);
245 })
246 .reduce((a, b) => a.concat(b), []);
247
248const preprocess = opts => {
249 opts = mergeWithPkgConf(opts);
250 opts = normalizeOpts(opts);
251 opts = getIgnores(opts);
252 opts.extensions = DEFAULT_EXTENSION.concat(opts.extensions || []);
253
254 return opts;
255}
256
257exports.DEFAULT_IGNORE = DEFAULT_IGNORE;
258exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
259exports.mergeWithPkgConf = mergeWithPkgConf;
260exports.normalizeOpts = normalizeOpts;
261exports.buildConfig = buildConfig;
262exports.findApplicableOverrides = findApplicableOverrides;
263exports.mergeApplicableOverrides = mergeApplicableOverrides;
264exports.groupConfigs = groupConfigs;
265exports.preprocess = preprocess;
266exports.emptyOptions = emptyOptions;
267exports.getIgnores = getIgnores;
268exports.getGitIgnores = getGitIgnores;