1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | "use strict";
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | const util = require("util"),
|
13 | inquirer = require("inquirer"),
|
14 | ProgressBar = require("progress"),
|
15 | semver = require("semver"),
|
16 | autoconfig = require("./autoconfig.js"),
|
17 | ConfigFile = require("./config-file"),
|
18 | ConfigOps = require("./config-ops"),
|
19 | getSourceCodeOfFiles = require("../util/source-code-util").getSourceCodeOfFiles,
|
20 | ModuleResolver = require("../util/module-resolver"),
|
21 | npmUtil = require("../util/npm-util"),
|
22 | recConfig = require("../../conf/eslint-recommended"),
|
23 | log = require("../logging");
|
24 |
|
25 | const debug = require("debug")("eslint:config-initializer");
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 | function writeFile(config, format) {
|
39 |
|
40 |
|
41 | let extname = ".js";
|
42 |
|
43 | if (format === "YAML") {
|
44 | extname = ".yml";
|
45 | } else if (format === "JSON") {
|
46 | extname = ".json";
|
47 | }
|
48 |
|
49 | const installedESLint = config.installedESLint;
|
50 |
|
51 | delete config.installedESLint;
|
52 |
|
53 | ConfigFile.write(config, `./.eslintrc${extname}`);
|
54 | log.info(`Successfully created .eslintrc${extname} file in ${process.cwd()}`);
|
55 |
|
56 | if (installedESLint) {
|
57 | log.info("ESLint was installed locally. We recommend using this local copy instead of your globally-installed copy.");
|
58 | }
|
59 | }
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 | function getPeerDependencies(moduleName) {
|
71 | let result = getPeerDependencies.cache.get(moduleName);
|
72 |
|
73 | if (!result) {
|
74 | log.info(`Checking peerDependencies of ${moduleName}`);
|
75 |
|
76 | result = npmUtil.fetchPeerDependencies(moduleName);
|
77 | getPeerDependencies.cache.set(moduleName, result);
|
78 | }
|
79 |
|
80 | return result;
|
81 | }
|
82 | getPeerDependencies.cache = new Map();
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 | function installModules(config, installESLint) {
|
91 | const modules = {};
|
92 |
|
93 |
|
94 | if (config.plugins) {
|
95 | for (const plugin of config.plugins) {
|
96 | modules[`eslint-plugin-${plugin}`] = "latest";
|
97 | }
|
98 | }
|
99 | if (config.extends && config.extends.indexOf("eslint:") === -1) {
|
100 | const moduleName = `eslint-config-${config.extends}`;
|
101 |
|
102 | modules[moduleName] = "latest";
|
103 | Object.assign(
|
104 | modules,
|
105 | getPeerDependencies(`${moduleName}@latest`)
|
106 | );
|
107 | }
|
108 |
|
109 |
|
110 | if (Object.keys(modules).length === 0) {
|
111 | return;
|
112 | }
|
113 |
|
114 | if (installESLint === false) {
|
115 | delete modules.eslint;
|
116 | } else {
|
117 | const installStatus = npmUtil.checkDevDeps(["eslint"]);
|
118 |
|
119 |
|
120 | if (installStatus.eslint === false) {
|
121 | log.info("Local ESLint installation not found.");
|
122 | modules.eslint = modules.eslint || "latest";
|
123 | config.installedESLint = true;
|
124 | }
|
125 | }
|
126 |
|
127 |
|
128 | const modulesToInstall = Object.keys(modules).map(name => `${name}@${modules[name]}`);
|
129 |
|
130 | log.info(`Installing ${modulesToInstall.join(", ")}`);
|
131 |
|
132 | npmUtil.installSyncSaveDev(modulesToInstall);
|
133 | }
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 | function configureRules(answers, config) {
|
146 | const BAR_TOTAL = 20,
|
147 | BAR_SOURCE_CODE_TOTAL = 4,
|
148 | newConfig = Object.assign({}, config),
|
149 | disabledConfigs = {};
|
150 | let sourceCodes,
|
151 | registry;
|
152 |
|
153 |
|
154 | const bar = new ProgressBar("Determining Config: :percent [:bar] :elapseds elapsed, eta :etas ", {
|
155 | width: 30,
|
156 | total: BAR_TOTAL
|
157 | });
|
158 |
|
159 | bar.tick(0);
|
160 |
|
161 |
|
162 | const patterns = answers.patterns.split(/[\s]+/);
|
163 |
|
164 | try {
|
165 | sourceCodes = getSourceCodeOfFiles(patterns, { baseConfig: newConfig, useEslintrc: false }, total => {
|
166 | bar.tick((BAR_SOURCE_CODE_TOTAL / total));
|
167 | });
|
168 | } catch (e) {
|
169 | log.info("\n");
|
170 | throw e;
|
171 | }
|
172 | const fileQty = Object.keys(sourceCodes).length;
|
173 |
|
174 | if (fileQty === 0) {
|
175 | log.info("\n");
|
176 | throw new Error("Automatic Configuration failed. No files were able to be parsed.");
|
177 | }
|
178 |
|
179 |
|
180 | registry = new autoconfig.Registry();
|
181 | registry.populateFromCoreRules();
|
182 |
|
183 |
|
184 | registry = registry.lintSourceCode(sourceCodes, newConfig, total => {
|
185 | bar.tick((BAR_TOTAL - BAR_SOURCE_CODE_TOTAL) / total);
|
186 | });
|
187 | debug(`\nRegistry: ${util.inspect(registry.rules, { depth: null })}`);
|
188 |
|
189 |
|
190 | const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId]));
|
191 |
|
192 |
|
193 | const failingRegistry = registry.getFailingRulesRegistry();
|
194 |
|
195 | Object.keys(failingRegistry.rules).forEach(ruleId => {
|
196 |
|
197 |
|
198 | disabledConfigs[ruleId] = (recRules.indexOf(ruleId) !== -1) ? 2 : 0;
|
199 | });
|
200 |
|
201 |
|
202 | registry = registry.stripFailingConfigs();
|
203 |
|
204 | |
205 |
|
206 |
|
207 |
|
208 | const singleConfigs = registry.createConfig().rules;
|
209 |
|
210 | |
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 | const specTwoConfigs = registry.filterBySpecificity(2).createConfig().rules;
|
217 |
|
218 |
|
219 | const specThreeConfigs = registry.filterBySpecificity(3).createConfig().rules;
|
220 |
|
221 |
|
222 | const defaultConfigs = registry.filterBySpecificity(1).createConfig().rules;
|
223 |
|
224 |
|
225 | newConfig.rules = Object.assign({}, disabledConfigs, defaultConfigs, specThreeConfigs, specTwoConfigs, singleConfigs);
|
226 |
|
227 |
|
228 | bar.update(BAR_TOTAL);
|
229 |
|
230 |
|
231 | const finalRuleIds = Object.keys(newConfig.rules);
|
232 | const totalRules = finalRuleIds.length;
|
233 | const enabledRules = finalRuleIds.filter(ruleId => (newConfig.rules[ruleId] !== 0)).length;
|
234 | const resultMessage = [
|
235 | `\nEnabled ${enabledRules} out of ${totalRules}`,
|
236 | `rules based on ${fileQty}`,
|
237 | `file${(fileQty === 1) ? "." : "s."}`
|
238 | ].join(" ");
|
239 |
|
240 | log.info(resultMessage);
|
241 |
|
242 | ConfigOps.normalizeToStrings(newConfig);
|
243 | return newConfig;
|
244 | }
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 | function processAnswers(answers) {
|
252 | let config = { rules: {}, env: {} };
|
253 |
|
254 | if (answers.es6) {
|
255 | config.env.es6 = true;
|
256 | if (answers.modules) {
|
257 | config.parserOptions = config.parserOptions || {};
|
258 | config.parserOptions.sourceType = "module";
|
259 | }
|
260 | }
|
261 | if (answers.commonjs) {
|
262 | config.env.commonjs = true;
|
263 | }
|
264 | answers.env.forEach(env => {
|
265 | config.env[env] = true;
|
266 | });
|
267 | if (answers.jsx) {
|
268 | config.parserOptions = config.parserOptions || {};
|
269 | config.parserOptions.ecmaFeatures = config.parserOptions.ecmaFeatures || {};
|
270 | config.parserOptions.ecmaFeatures.jsx = true;
|
271 | if (answers.react) {
|
272 | config.plugins = ["react"];
|
273 | config.parserOptions.ecmaFeatures.experimentalObjectRestSpread = true;
|
274 | }
|
275 | }
|
276 |
|
277 | if (answers.source === "prompt") {
|
278 | config.extends = "eslint:recommended";
|
279 | config.rules.indent = ["error", answers.indent];
|
280 | config.rules.quotes = ["error", answers.quotes];
|
281 | config.rules["linebreak-style"] = ["error", answers.linebreak];
|
282 | config.rules.semi = ["error", answers.semi ? "always" : "never"];
|
283 | }
|
284 |
|
285 | installModules(config);
|
286 |
|
287 | if (answers.source === "auto") {
|
288 | config = configureRules(answers, config);
|
289 | config = autoconfig.extendFromRecommended(config);
|
290 | }
|
291 |
|
292 | ConfigOps.normalizeToStrings(config);
|
293 | return config;
|
294 | }
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 | function getConfigForStyleGuide(guide, installESLint) {
|
303 | const guides = {
|
304 | google: { extends: "google" },
|
305 | airbnb: { extends: "airbnb" },
|
306 | "airbnb-base": { extends: "airbnb-base" },
|
307 | standard: { extends: "standard" }
|
308 | };
|
309 |
|
310 | if (!guides[guide]) {
|
311 | throw new Error("You referenced an unsupported guide.");
|
312 | }
|
313 |
|
314 | installModules(guides[guide], installESLint);
|
315 |
|
316 | return guides[guide];
|
317 | }
|
318 |
|
319 |
|
320 |
|
321 |
|
322 |
|
323 | function getLocalESLintVersion() {
|
324 | try {
|
325 | const resolver = new ModuleResolver();
|
326 | const eslintPath = resolver.resolve("eslint", process.cwd());
|
327 | const eslint = require(eslintPath);
|
328 |
|
329 | return eslint.linter.version || null;
|
330 | } catch (_err) {
|
331 | return null;
|
332 | }
|
333 | }
|
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 | function getStyleGuideName(answers) {
|
341 | if (answers.styleguide === "airbnb" && !answers.airbnbReact) {
|
342 | return "airbnb-base";
|
343 | }
|
344 | return answers.styleguide;
|
345 | }
|
346 |
|
347 |
|
348 |
|
349 |
|
350 |
|
351 |
|
352 | function hasESLintVersionConflict(answers) {
|
353 |
|
354 |
|
355 | const localESLintVersion = getLocalESLintVersion();
|
356 |
|
357 | if (!localESLintVersion) {
|
358 | return false;
|
359 | }
|
360 |
|
361 |
|
362 | const configName = getStyleGuideName(answers);
|
363 | const moduleName = `eslint-config-${configName}@latest`;
|
364 | const peerDependencies = getPeerDependencies(moduleName) || {};
|
365 | const requiredESLintVersionRange = peerDependencies.eslint;
|
366 |
|
367 | if (!requiredESLintVersionRange) {
|
368 | return false;
|
369 | }
|
370 |
|
371 | answers.localESLintVersion = localESLintVersion;
|
372 | answers.requiredESLintVersionRange = requiredESLintVersionRange;
|
373 |
|
374 |
|
375 | if (semver.satisfies(localESLintVersion, requiredESLintVersionRange)) {
|
376 | answers.installESLint = false;
|
377 | return false;
|
378 | }
|
379 |
|
380 | return true;
|
381 | }
|
382 |
|
383 |
|
384 |
|
385 |
|
386 |
|
387 |
|
388 | function promptUser() {
|
389 |
|
390 | return inquirer.prompt([
|
391 | {
|
392 | type: "list",
|
393 | name: "source",
|
394 | message: "How would you like to configure ESLint?",
|
395 | default: "prompt",
|
396 | choices: [
|
397 | { name: "Answer questions about your style", value: "prompt" },
|
398 | { name: "Use a popular style guide", value: "guide" },
|
399 | { name: "Inspect your JavaScript file(s)", value: "auto" }
|
400 | ]
|
401 | },
|
402 | {
|
403 | type: "list",
|
404 | name: "styleguide",
|
405 | message: "Which style guide do you want to follow?",
|
406 | choices: [{ name: "Google", value: "google" }, { name: "Airbnb", value: "airbnb" }, { name: "Standard", value: "standard" }],
|
407 | when(answers) {
|
408 | answers.packageJsonExists = npmUtil.checkPackageJson();
|
409 | return answers.source === "guide" && answers.packageJsonExists;
|
410 | }
|
411 | },
|
412 | {
|
413 | type: "confirm",
|
414 | name: "airbnbReact",
|
415 | message: "Do you use React?",
|
416 | default: false,
|
417 | when(answers) {
|
418 | return answers.styleguide === "airbnb";
|
419 | }
|
420 | },
|
421 | {
|
422 | type: "input",
|
423 | name: "patterns",
|
424 | message: "Which file(s), path(s), or glob(s) should be examined?",
|
425 | when(answers) {
|
426 | return (answers.source === "auto");
|
427 | },
|
428 | validate(input) {
|
429 | if (input.trim().length === 0 && input.trim() !== ",") {
|
430 | return "You must tell us what code to examine. Try again.";
|
431 | }
|
432 | return true;
|
433 | }
|
434 | },
|
435 | {
|
436 | type: "list",
|
437 | name: "format",
|
438 | message: "What format do you want your config file to be in?",
|
439 | default: "JavaScript",
|
440 | choices: ["JavaScript", "YAML", "JSON"],
|
441 | when(answers) {
|
442 | return ((answers.source === "guide" && answers.packageJsonExists) || answers.source === "auto");
|
443 | }
|
444 | },
|
445 | {
|
446 | type: "confirm",
|
447 | name: "installESLint",
|
448 | message(answers) {
|
449 | const verb = semver.ltr(answers.localESLintVersion, answers.requiredESLintVersionRange)
|
450 | ? "upgrade"
|
451 | : "downgrade";
|
452 |
|
453 | return `The style guide "${answers.styleguide}" requires eslint@${answers.requiredESLintVersionRange}. You are currently using eslint@${answers.localESLintVersion}.\n Do you want to ${verb}?`;
|
454 | },
|
455 | default: true,
|
456 | when(answers) {
|
457 | return answers.source === "guide" && answers.packageJsonExists && hasESLintVersionConflict(answers);
|
458 | }
|
459 | }
|
460 | ]).then(earlyAnswers => {
|
461 |
|
462 |
|
463 | if (earlyAnswers.source === "guide") {
|
464 | if (!earlyAnswers.packageJsonExists) {
|
465 | log.info("A package.json is necessary to install plugins such as style guides. Run `npm init` to create a package.json file and try again.");
|
466 | return void 0;
|
467 | }
|
468 | if (earlyAnswers.installESLint === false && !semver.satisfies(earlyAnswers.localESLintVersion, earlyAnswers.requiredESLintVersionRange)) {
|
469 | log.info(`Note: it might not work since ESLint's version is mismatched with the ${earlyAnswers.styleguide} config.`);
|
470 | }
|
471 | if (earlyAnswers.styleguide === "airbnb" && !earlyAnswers.airbnbReact) {
|
472 | earlyAnswers.styleguide = "airbnb-base";
|
473 | }
|
474 |
|
475 | const config = getConfigForStyleGuide(earlyAnswers.styleguide, earlyAnswers.installESLint);
|
476 |
|
477 | writeFile(config, earlyAnswers.format);
|
478 |
|
479 | return void 0;
|
480 | }
|
481 |
|
482 |
|
483 | return inquirer.prompt([
|
484 | {
|
485 | type: "confirm",
|
486 | name: "es6",
|
487 | message: "Are you using ECMAScript 6 features?",
|
488 | default: false
|
489 | },
|
490 | {
|
491 | type: "confirm",
|
492 | name: "modules",
|
493 | message: "Are you using ES6 modules?",
|
494 | default: false,
|
495 | when(answers) {
|
496 | return answers.es6 === true;
|
497 | }
|
498 | },
|
499 | {
|
500 | type: "checkbox",
|
501 | name: "env",
|
502 | message: "Where will your code run?",
|
503 | default: ["browser"],
|
504 | choices: [{ name: "Browser", value: "browser" }, { name: "Node", value: "node" }]
|
505 | },
|
506 | {
|
507 | type: "confirm",
|
508 | name: "commonjs",
|
509 | message: "Do you use CommonJS?",
|
510 | default: false,
|
511 | when(answers) {
|
512 | return answers.env.some(env => env === "browser");
|
513 | }
|
514 | },
|
515 | {
|
516 | type: "confirm",
|
517 | name: "jsx",
|
518 | message: "Do you use JSX?",
|
519 | default: false
|
520 | },
|
521 | {
|
522 | type: "confirm",
|
523 | name: "react",
|
524 | message: "Do you use React?",
|
525 | default: false,
|
526 | when(answers) {
|
527 | return answers.jsx;
|
528 | }
|
529 | }
|
530 | ]).then(secondAnswers => {
|
531 |
|
532 |
|
533 | if (earlyAnswers.source === "auto") {
|
534 | const combinedAnswers = Object.assign({}, earlyAnswers, secondAnswers);
|
535 |
|
536 | const config = processAnswers(combinedAnswers);
|
537 |
|
538 | installModules(config);
|
539 | writeFile(config, earlyAnswers.format);
|
540 |
|
541 | return void 0;
|
542 | }
|
543 |
|
544 |
|
545 | return inquirer.prompt([
|
546 | {
|
547 | type: "list",
|
548 | name: "indent",
|
549 | message: "What style of indentation do you use?",
|
550 | default: "tab",
|
551 | choices: [{ name: "Tabs", value: "tab" }, { name: "Spaces", value: 4 }]
|
552 | },
|
553 | {
|
554 | type: "list",
|
555 | name: "quotes",
|
556 | message: "What quotes do you use for strings?",
|
557 | default: "double",
|
558 | choices: [{ name: "Double", value: "double" }, { name: "Single", value: "single" }]
|
559 | },
|
560 | {
|
561 | type: "list",
|
562 | name: "linebreak",
|
563 | message: "What line endings do you use?",
|
564 | default: "unix",
|
565 | choices: [{ name: "Unix", value: "unix" }, { name: "Windows", value: "windows" }]
|
566 | },
|
567 | {
|
568 | type: "confirm",
|
569 | name: "semi",
|
570 | message: "Do you require semicolons?",
|
571 | default: true
|
572 | },
|
573 | {
|
574 | type: "list",
|
575 | name: "format",
|
576 | message: "What format do you want your config file to be in?",
|
577 | default: "JavaScript",
|
578 | choices: ["JavaScript", "YAML", "JSON"]
|
579 | }
|
580 | ]).then(answers => {
|
581 | const totalAnswers = Object.assign({}, earlyAnswers, secondAnswers, answers);
|
582 |
|
583 | const config = processAnswers(totalAnswers);
|
584 |
|
585 | installModules(config);
|
586 | writeFile(config, answers.format);
|
587 | });
|
588 | });
|
589 | });
|
590 | }
|
591 |
|
592 |
|
593 |
|
594 |
|
595 |
|
596 | const init = {
|
597 | getConfigForStyleGuide,
|
598 | hasESLintVersionConflict,
|
599 | processAnswers,
|
600 | initializeConfig() {
|
601 | return promptUser();
|
602 | }
|
603 | };
|
604 |
|
605 | module.exports = init;
|