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