2 * @fileoverview Config initialization wizard.
9 //------------------------------------------------------------------------------
11 //------------------------------------------------------------------------------
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");
29 const debug
= require("debug")("eslint:config-initializer");
31 //------------------------------------------------------------------------------
33 //------------------------------------------------------------------------------
35 /* istanbul ignore next: hard to test fs function */
37 * Create .eslintrc file in the current working directory
38 * @param {Object} config object that contains user's answers
39 * @param {string} format The file format to write to.
42 function writeFile(config
, format
) {
47 if (format
=== "YAML") {
49 } else if (format
=== "JSON") {
53 const installedESLint
= config
.installedESLint
;
55 delete config
.installedESLint
;
57 ConfigFile
.write(config
, `./.eslintrc${extname}`);
58 log
.info(`Successfully created .eslintrc${extname} file in ${process.cwd()}`);
60 if (installedESLint
) {
61 log
.info("ESLint was installed locally. We recommend using this local copy instead of your globally-installed copy.");
66 * Get the peer dependencies of the given module.
67 * This adds the gotten value to cache at the first time, then reuses it.
68 * In a process, this function is called twice, but `npmUtils.fetchPeerDependencies` needs to access network which is relatively slow.
69 * @param {string} moduleName The module name to get.
70 * @returns {Object} The peer dependencies of the given module.
71 * This object is the object of `peerDependencies` field of `package.json`.
72 * Returns null if npm was not found.
74 function getPeerDependencies(moduleName
) {
75 let result
= getPeerDependencies
.cache
.get(moduleName
);
78 log
.info(`Checking peerDependencies of ${moduleName}`);
80 result
= npmUtils
.fetchPeerDependencies(moduleName
);
81 getPeerDependencies
.cache
.set(moduleName
, result
);
86 getPeerDependencies
.cache
= new Map();
89 * Return necessary plugins, configs, parsers, etc. based on the config
90 * @param {Object} config config object
91 * @param {boolean} [installESLint=true] If `false` is given, it does not install eslint.
92 * @returns {string[]} An array of modules to be installed.
94 function getModulesList(config
, installESLint
) {
97 // Create a list of modules which should be installed based on config
99 for (const plugin
of config
.plugins
) {
100 const moduleName
= naming
.normalizePackageName(plugin
, "eslint-plugin");
102 modules
[moduleName
] = "latest";
105 if (config
.extends) {
106 const extendList
= Array
.isArray(config
.extends) ? config
.extends : [config
.extends];
108 for (const extend
of extendList
) {
109 if (extend
.startsWith("eslint:") || extend
.startsWith("plugin:")) {
112 const moduleName
= naming
.normalizePackageName(extend
, "eslint-config");
114 modules
[moduleName
] = "latest";
117 getPeerDependencies(`${moduleName}@latest`)
122 const parser
= config
.parser
|| (config
.parserOptions
&& config
.parserOptions
.parser
);
125 modules
[parser
] = "latest";
128 if (installESLint
=== false) {
129 delete modules
.eslint
;
131 const installStatus
= npmUtils
.checkDevDeps(["eslint"]);
133 // Mark to show messages if it's new installation of eslint.
134 if (installStatus
.eslint
=== false) {
135 log
.info("Local ESLint installation not found.");
136 modules
.eslint
= modules
.eslint
|| "latest";
137 config
.installedESLint
= true;
141 return Object
.keys(modules
).map(name
=> `${name}@${modules[name]}`);
145 * Set the `rules` of a config by examining a user's source code
147 * Note: This clones the config object and returns a new config to avoid mutating
148 * the original config parameter.
149 * @param {Object} answers answers received from enquirer
150 * @param {Object} config config object
151 * @returns {Object} config object with configured rules
153 function configureRules(answers
, config
) {
154 const BAR_TOTAL
= 20,
155 BAR_SOURCE_CODE_TOTAL
= 4,
156 newConfig
= Object
.assign({}, config
),
157 disabledConfigs
= {};
161 // Set up a progress bar, as this process can take a long time
162 const bar
= new ProgressBar("Determining Config: :percent [:bar] :elapseds elapsed, eta :etas ", {
167 bar
.tick(0); // Shows the progress bar
169 // Get the SourceCode of all chosen files
170 const patterns
= answers
.patterns
.split(/[\s]+/u);
173 sourceCodes
= getSourceCodeOfFiles(patterns
, { baseConfig
: newConfig
, useEslintrc
: false }, total
=> {
174 bar
.tick((BAR_SOURCE_CODE_TOTAL
/ total
));
180 const fileQty
= Object
.keys(sourceCodes
).length
;
184 throw new Error("Automatic Configuration failed. No files were able to be parsed.");
187 // Create a registry of rule configs
188 registry
= new autoconfig
.Registry();
189 registry
.populateFromCoreRules();
191 // Lint all files with each rule config in the registry
192 registry
= registry
.lintSourceCode(sourceCodes
, newConfig
, total
=> {
193 bar
.tick((BAR_TOTAL
- BAR_SOURCE_CODE_TOTAL
) / total
); // Subtract out ticks used at beginning
195 debug(`\nRegistry: ${util.inspect(registry.rules, { depth: null })}`);
197 // Create a list of recommended rules, because we don't want to disable them
198 const recRules
= Object
.keys(recConfig
.rules
).filter(ruleId
=> ConfigOps
.isErrorSeverity(recConfig
.rules
[ruleId
]));
200 // Find and disable rules which had no error-free configuration
201 const failingRegistry
= registry
.getFailingRulesRegistry();
203 Object
.keys(failingRegistry
.rules
).forEach(ruleId
=> {
205 // If the rule is recommended, set it to error, otherwise disable it
206 disabledConfigs
[ruleId
] = (recRules
.indexOf(ruleId
) !== -1) ? 2 : 0;
209 // Now that we know which rules to disable, strip out configs with errors
210 registry
= registry
.stripFailingConfigs();
213 * If there is only one config that results in no errors for a rule, we should use it.
214 * createConfig will only add rules that have one configuration in the registry.
216 const singleConfigs
= registry
.createConfig().rules
;
219 * The "sweet spot" for number of options in a config seems to be two (severity plus one option).
220 * Very often, a third option (usually an object) is available to address
221 * edge cases, exceptions, or unique situations. We will prefer to use a config with
222 * specificity of two.
224 const specTwoConfigs
= registry
.filterBySpecificity(2).createConfig().rules
;
226 // Maybe a specific combination using all three options works
227 const specThreeConfigs
= registry
.filterBySpecificity(3).createConfig().rules
;
229 // If all else fails, try to use the default (severity only)
230 const defaultConfigs
= registry
.filterBySpecificity(1).createConfig().rules
;
232 // Combine configs in reverse priority order (later take precedence)
233 newConfig
.rules
= Object
.assign({}, disabledConfigs
, defaultConfigs
, specThreeConfigs
, specTwoConfigs
, singleConfigs
);
235 // Make sure progress bar has finished (floating point rounding)
236 bar
.update(BAR_TOTAL
);
238 // Log out some stats to let the user know what happened
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."}`
248 log
.info(resultMessage
);
250 ConfigOps
.normalizeToStrings(newConfig
);
255 * process user's answers and create config object
256 * @param {Object} answers answers received from enquirer
257 * @returns {Object} config object
259 function processAnswers(answers
) {
267 config
.parserOptions
.ecmaVersion
= espree
.latestEcmaVersion
;
268 config
.env
.es2021
= true;
270 // set the module type
271 if (answers
.moduleType
=== "esm") {
272 config
.parserOptions
.sourceType
= "module";
273 } else if (answers
.moduleType
=== "commonjs") {
274 config
.env
.commonjs
= true;
277 // add in browser and node environments if necessary
278 answers
.env
.forEach(env
=> {
279 config
.env
[env
] = true;
282 // add in library information
283 if (answers
.framework
=== "react") {
284 config
.parserOptions
.ecmaFeatures
= {
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");
294 if (answers
.typescript
) {
295 if (answers
.framework
=== "vue") {
296 config
.parserOptions
.parser
= "@typescript-eslint/parser";
298 config
.parser
= "@typescript-eslint/parser";
301 if (Array
.isArray(config
.plugins
)) {
302 config
.plugins
.push("@typescript-eslint");
304 config
.plugins
= ["@typescript-eslint"];
308 // setup rules based on problems/style enforcement preferences
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
);
323 if (answers
.typescript
&& config
.extends.includes("eslint:recommended")) {
324 config
.extends.push("plugin:@typescript-eslint/recommended");
328 if (config
.extends.length
=== 0) {
329 delete config
.extends;
330 } else if (config
.extends.length
=== 1) {
331 config
.extends = config
.extends[0];
334 ConfigOps
.normalizeToStrings(config
);
339 * Get the version of the local ESLint.
340 * @returns {string|null} The version. If the local ESLint was not found, returns null.
342 function getLocalESLintVersion() {
344 const eslintPath
= ModuleResolver
.resolve("eslint", path
.join(process
.cwd(), "__placeholder__.js"));
345 const eslint
= require(eslintPath
);
347 return eslint
.linter
.version
|| null;
354 * Get the shareable config name of the chosen style guide.
355 * @param {Object} answers The answers object.
356 * @returns {string} The shareable config name.
358 function getStyleGuideName(answers
) {
359 if (answers
.styleguide
=== "airbnb" && answers
.framework
!== "react") {
360 return "airbnb-base";
362 return answers
.styleguide
;
366 * Check whether the local ESLint version conflicts with the required version of the chosen shareable config.
367 * @param {Object} answers The answers object.
368 * @returns {boolean} `true` if the local ESLint is found then it conflicts with the required version of the chosen shareable config.
370 function hasESLintVersionConflict(answers
) {
372 // Get the local ESLint version.
373 const localESLintVersion
= getLocalESLintVersion();
375 if (!localESLintVersion
) {
379 // Get the required range of ESLint version.
380 const configName
= getStyleGuideName(answers
);
381 const moduleName
= `eslint-config-${configName}@latest`;
382 const peerDependencies
= getPeerDependencies(moduleName
) || {};
383 const requiredESLintVersionRange
= peerDependencies
.eslint
;
385 if (!requiredESLintVersionRange
) {
389 answers
.localESLintVersion
= localESLintVersion
;
390 answers
.requiredESLintVersionRange
= requiredESLintVersionRange
;
392 // Check the version.
393 if (semver
.satisfies(localESLintVersion
, requiredESLintVersionRange
)) {
394 answers
.installESLint
= false;
403 * @param {string[]} modules Modules to be installed.
406 function installModules(modules
) {
407 log
.info(`Installing ${modules.join(", ")}`);
408 npmUtils
.installSyncSaveDev(modules
);
411 /* istanbul ignore next: no need to test enquirer */
413 * Ask user to install modules.
414 * @param {string[]} modules Array of modules to be installed.
415 * @param {boolean} packageJsonExists Indicates if package.json is existed.
416 * @returns {Promise} Answer that indicates if user wants to install.
418 function askInstallModules(modules
, packageJsonExists
) {
420 // If no modules, do nothing.
421 if (modules
.length
=== 0) {
422 return Promise
.resolve();
425 log
.info("The config that you've selected requires the following dependencies:\n");
426 log
.info(modules
.join(" "));
427 return enquirer
.prompt([
430 name
: "executeInstallation",
431 message
: "Would you like to install them now with npm?",
436 return !(modules
.length
&& packageJsonExists
);
439 return this.skipped
? null : input
;
442 ]).then(({ executeInstallation
}) => {
443 if (executeInstallation
) {
444 installModules(modules
);
449 /* istanbul ignore next: no need to test enquirer */
451 * Ask use a few questions on command prompt
452 * @returns {Promise} The promise with the result of the prompt
454 function promptUser() {
456 return enquirer
.prompt([
460 message
: "How would you like to use ESLint?",
462 // The returned number matches the name value of nth in the choices array.
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" }
473 message
: "What type of modules does your project use?",
476 { message
: "JavaScript modules (import/export)", name
: "esm" },
477 { message
: "CommonJS (require/exports)", name
: "commonjs" },
478 { message
: "None of these", name
: "none" }
484 message
: "Which framework does your project use?",
487 { message
: "React", name
: "react" },
488 { message
: "Vue.js", name
: "vue" },
489 { message
: "None of these", name
: "none" }
495 message
: "Does your project use TypeScript?",
503 message
: "Where does your code run?",
504 hint
: "(Press <space> to select, <a> to toggle all, <i> to invert selection)",
507 { message
: "Browser", name
: "browser" },
508 { message
: "Node", name
: "node" }
514 message
: "How would you like to define a style for your project?",
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" }
521 return this.state
.answers
.purpose
!== "style";
524 return this.skipped
? null : input
;
530 message
: "Which style guide do you want to follow?",
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" }
537 this.state
.answers
.packageJsonExists
= npmUtils
.checkPackageJson();
538 return !(this.state
.answers
.source
=== "guide" && this.state
.answers
.packageJsonExists
);
541 return this.skipped
? null : input
;
547 message
: "Which file(s), path(s), or glob(s) should be examined?",
549 return this.state
.answers
.source
!== "auto";
552 if (!this.skipped
&& input
.trim().length
=== 0 && input
.trim() !== ",") {
553 return "You must tell us what code to examine. Try again.";
561 message
: "What format do you want your config file to be in?",
563 choices
: ["JavaScript", "YAML", "JSON"]
567 name
: "installESLint",
569 const verb
= semver
.ltr(answers
.localESLintVersion
, answers
.requiredESLintVersionRange
)
573 return `The style guide "${answers.styleguide}" requires eslint@${answers.requiredESLintVersionRange}. You are currently using eslint@${answers.localESLintVersion}.\n Do you want to ${verb}?`;
579 return !(this.state
.answers
.source
=== "guide" && this.state
.answers
.packageJsonExists
&& hasESLintVersionConflict(this.state
.answers
));
582 return this.skipped
? null : input
;
585 ]).then(earlyAnswers
=> {
587 // early exit if no style guide is necessary
588 if (earlyAnswers
.purpose
!== "style") {
589 const config
= processAnswers(earlyAnswers
);
590 const modules
= getModulesList(config
);
592 return askInstallModules(modules
, earlyAnswers
.packageJsonExists
)
593 .then(() => writeFile(config
, earlyAnswers
.format
));
596 // early exit if you are using a style guide
597 if (earlyAnswers
.source
=== "guide") {
598 if (!earlyAnswers
.packageJsonExists
) {
599 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.");
602 if (earlyAnswers
.installESLint
=== false && !semver
.satisfies(earlyAnswers
.localESLintVersion
, earlyAnswers
.requiredESLintVersionRange
)) {
603 log
.info(`Note: it might not work since ESLint's version is mismatched with the ${earlyAnswers.styleguide} config.`);
605 if (earlyAnswers
.styleguide
=== "airbnb" && earlyAnswers
.framework
!== "react") {
606 earlyAnswers
.styleguide
= "airbnb-base";
609 const config
= processAnswers(earlyAnswers
);
611 if (Array
.isArray(config
.extends)) {
612 config
.extends.push(earlyAnswers
.styleguide
);
613 } else if (config
.extends) {
614 config
.extends = [config
.extends, earlyAnswers
.styleguide
];
616 config
.extends = [earlyAnswers
.styleguide
];
619 const modules
= getModulesList(config
);
621 return askInstallModules(modules
, earlyAnswers
.packageJsonExists
)
622 .then(() => writeFile(config
, earlyAnswers
.format
));
626 if (earlyAnswers
.source
=== "auto") {
627 const combinedAnswers
= Object
.assign({}, earlyAnswers
);
628 const config
= processAnswers(combinedAnswers
);
629 const modules
= getModulesList(config
);
631 return askInstallModules(modules
).then(() => writeFile(config
, earlyAnswers
.format
));
634 // continue with the style questions otherwise...
635 return enquirer
.prompt([
639 message
: "What style of indentation do you use?",
641 choices
: [{ message
: "Tabs", name
: "tab" }, { message
: "Spaces", name
: 4 }]
646 message
: "What quotes do you use for strings?",
648 choices
: [{ message
: "Double", name
: "double" }, { message
: "Single", name
: "single" }]
653 message
: "What line endings do you use?",
655 choices
: [{ message
: "Unix", name
: "unix" }, { message
: "Windows", name
: "windows" }]
660 message
: "Do you require semicolons?",
666 const totalAnswers
= Object
.assign({}, earlyAnswers
, answers
);
668 const config
= processAnswers(totalAnswers
);
669 const modules
= getModulesList(config
);
671 return askInstallModules(modules
).then(() => writeFile(config
, earlyAnswers
.format
));
676 //------------------------------------------------------------------------------
678 //------------------------------------------------------------------------------
682 hasESLintVersionConflict
,
685 /* istanbul ignore next */initializeConfig() {
690 module
.exports
= init
;