1 | 'use strict';
|
2 | const os = require('os');
|
3 | const path = require('path');
|
4 | const arrify = require('arrify');
|
5 | const pkgConf = require('pkg-conf');
|
6 | const deepAssign = require('deep-assign');
|
7 | const multimatch = require('multimatch');
|
8 | const resolveFrom = require('resolve-from');
|
9 | const pathExists = require('path-exists');
|
10 | const parseGitignore = require('parse-gitignore');
|
11 | const globby = require('globby');
|
12 |
|
13 | const 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 |
|
27 | const DEFAULT_EXTENSION = [
|
28 | 'js',
|
29 | 'jsx'
|
30 | ];
|
31 |
|
32 | const 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 |
|
45 | const normalizeOpts = opts => {
|
46 | opts = Object.assign({}, opts);
|
47 |
|
48 |
|
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 |
|
78 | const 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 |
|
85 | const emptyOptions = () => ({
|
86 | rules: {},
|
87 | settings: {},
|
88 | globals: [],
|
89 | envs: [],
|
90 | plugins: [],
|
91 | extends: []
|
92 | });
|
93 |
|
94 | const 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 |
|
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 |
|
139 |
|
140 | const configs = opts.extends.map(name => {
|
141 |
|
142 | if (pathExists.sync(name)) {
|
143 | return name;
|
144 | }
|
145 |
|
146 |
|
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 |
|
171 |
|
172 |
|
173 |
|
174 | const 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 |
|
193 | const mergeApplicableOverrides = (baseOptions, applicableOverrides) => {
|
194 | applicableOverrides = applicableOverrides.map(normalizeOpts);
|
195 | const overrides = [emptyOptions(), baseOptions].concat(applicableOverrides);
|
196 | return deepAssign.apply(null, overrides);
|
197 | }
|
198 |
|
199 |
|
200 | const 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 |
|
223 | const getIgnores = opts => {
|
224 | opts.ignores = DEFAULT_IGNORE.concat(opts.ignores || []);
|
225 | return opts;
|
226 | }
|
227 |
|
228 | const 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 |
|
248 | const 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 |
|
257 | exports.DEFAULT_IGNORE = DEFAULT_IGNORE;
|
258 | exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
259 | exports.mergeWithPkgConf = mergeWithPkgConf;
|
260 | exports.normalizeOpts = normalizeOpts;
|
261 | exports.buildConfig = buildConfig;
|
262 | exports.findApplicableOverrides = findApplicableOverrides;
|
263 | exports.mergeApplicableOverrides = mergeApplicableOverrides;
|
264 | exports.groupConfigs = groupConfigs;
|
265 | exports.preprocess = preprocess;
|
266 | exports.emptyOptions = emptyOptions;
|
267 | exports.getIgnores = getIgnores;
|
268 | exports.getGitIgnores = getGitIgnores;
|