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