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