2 * @fileoverview Config initialization wizard.
9 //------------------------------------------------------------------------------
11 //------------------------------------------------------------------------------
13 const util
= require("util"),
14 path
= require("path"),
15 inquirer
= require("inquirer"),
16 ProgressBar
= require("progress"),
17 semver
= require("semver"),
18 espree
= require("espree"),
19 recConfig
= require("../../conf/eslint-recommended"),
20 ConfigOps
= require("../shared/config-ops"),
21 log
= require("../shared/logging"),
22 naming
= require("../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 inquirer
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 inquirer
257 * @returns {Object} config object
259 function processAnswers(answers
) {
267 config
.parserOptions
.ecmaVersion
= espree
.latestEcmaVersion
;
268 config
.env
.es2020
= 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/eslint-recommended");
325 config
.extends.push("plugin:@typescript-eslint/recommended");
329 if (config
.extends.length
=== 0) {
330 delete config
.extends;
331 } else if (config
.extends.length
=== 1) {
332 config
.extends = config
.extends[0];
335 ConfigOps
.normalizeToStrings(config
);
340 * Get the version of the local ESLint.
341 * @returns {string|null} The version. If the local ESLint was not found, returns null.
343 function getLocalESLintVersion() {
345 const eslintPath
= ModuleResolver
.resolve("eslint", path
.join(process
.cwd(), "__placeholder__.js"));
346 const eslint
= require(eslintPath
);
348 return eslint
.linter
.version
|| null;
355 * Get the shareable config name of the chosen style guide.
356 * @param {Object} answers The answers object.
357 * @returns {string} The shareable config name.
359 function getStyleGuideName(answers
) {
360 if (answers
.styleguide
=== "airbnb" && answers
.framework
!== "react") {
361 return "airbnb-base";
363 return answers
.styleguide
;
367 * Check whether the local ESLint version conflicts with the required version of the chosen shareable config.
368 * @param {Object} answers The answers object.
369 * @returns {boolean} `true` if the local ESLint is found then it conflicts with the required version of the chosen shareable config.
371 function hasESLintVersionConflict(answers
) {
373 // Get the local ESLint version.
374 const localESLintVersion
= getLocalESLintVersion();
376 if (!localESLintVersion
) {
380 // Get the required range of ESLint version.
381 const configName
= getStyleGuideName(answers
);
382 const moduleName
= `eslint-config-${configName}@latest`;
383 const peerDependencies
= getPeerDependencies(moduleName
) || {};
384 const requiredESLintVersionRange
= peerDependencies
.eslint
;
386 if (!requiredESLintVersionRange
) {
390 answers
.localESLintVersion
= localESLintVersion
;
391 answers
.requiredESLintVersionRange
= requiredESLintVersionRange
;
393 // Check the version.
394 if (semver
.satisfies(localESLintVersion
, requiredESLintVersionRange
)) {
395 answers
.installESLint
= false;
404 * @param {string[]} modules Modules to be installed.
407 function installModules(modules
) {
408 log
.info(`Installing ${modules.join(", ")}`);
409 npmUtils
.installSyncSaveDev(modules
);
412 /* istanbul ignore next: no need to test inquirer */
414 * Ask user to install modules.
415 * @param {string[]} modules Array of modules to be installed.
416 * @param {boolean} packageJsonExists Indicates if package.json is existed.
417 * @returns {Promise} Answer that indicates if user wants to install.
419 function askInstallModules(modules
, packageJsonExists
) {
421 // If no modules, do nothing.
422 if (modules
.length
=== 0) {
423 return Promise
.resolve();
426 log
.info("The config that you've selected requires the following dependencies:\n");
427 log
.info(modules
.join(" "));
428 return inquirer
.prompt([
431 name
: "executeInstallation",
432 message
: "Would you like to install them now with npm?",
435 return modules
.length
&& packageJsonExists
;
438 ]).then(({ executeInstallation
}) => {
439 if (executeInstallation
) {
440 installModules(modules
);
445 /* istanbul ignore next: no need to test inquirer */
447 * Ask use a few questions on command prompt
448 * @returns {Promise} The promise with the result of the prompt
450 function promptUser() {
452 return inquirer
.prompt([
456 message
: "How would you like to use ESLint?",
459 { name
: "To check syntax only", value
: "syntax" },
460 { name
: "To check syntax and find problems", value
: "problems" },
461 { name
: "To check syntax, find problems, and enforce code style", value
: "style" }
467 message
: "What type of modules does your project use?",
470 { name
: "JavaScript modules (import/export)", value
: "esm" },
471 { name
: "CommonJS (require/exports)", value
: "commonjs" },
472 { name
: "None of these", value
: "none" }
478 message
: "Which framework does your project use?",
481 { name
: "React", value
: "react" },
482 { name
: "Vue.js", value
: "vue" },
483 { name
: "None of these", value
: "none" }
489 message
: "Does your project use TypeScript?",
495 message
: "Where does your code run?",
496 default: ["browser"],
498 { name
: "Browser", value
: "browser" },
499 { name
: "Node", value
: "node" }
505 message
: "How would you like to define a style for your project?",
508 { name
: "Use a popular style guide", value
: "guide" },
509 { name
: "Answer questions about your style", value
: "prompt" },
510 { name
: "Inspect your JavaScript file(s)", value
: "auto" }
513 return answers
.purpose
=== "style";
519 message
: "Which style guide do you want to follow?",
521 { name
: "Airbnb: https://github.com/airbnb/javascript", value
: "airbnb" },
522 { name
: "Standard: https://github.com/standard/standard", value
: "standard" },
523 { name
: "Google: https://github.com/google/eslint-config-google", value
: "google" }
526 answers
.packageJsonExists
= npmUtils
.checkPackageJson();
527 return answers
.source
=== "guide" && answers
.packageJsonExists
;
533 message
: "Which file(s), path(s), or glob(s) should be examined?",
535 return (answers
.source
=== "auto");
538 if (input
.trim().length
=== 0 && input
.trim() !== ",") {
539 return "You must tell us what code to examine. Try again.";
547 message
: "What format do you want your config file to be in?",
548 default: "JavaScript",
549 choices
: ["JavaScript", "YAML", "JSON"]
553 name
: "installESLint",
555 const verb
= semver
.ltr(answers
.localESLintVersion
, answers
.requiredESLintVersionRange
)
559 return `The style guide "${answers.styleguide}" requires eslint@${answers.requiredESLintVersionRange}. You are currently using eslint@${answers.localESLintVersion}.\n Do you want to ${verb}?`;
563 return answers
.source
=== "guide" && answers
.packageJsonExists
&& hasESLintVersionConflict(answers
);
566 ]).then(earlyAnswers
=> {
568 // early exit if no style guide is necessary
569 if (earlyAnswers
.purpose
!== "style") {
570 const config
= processAnswers(earlyAnswers
);
571 const modules
= getModulesList(config
);
573 return askInstallModules(modules
, earlyAnswers
.packageJsonExists
)
574 .then(() => writeFile(config
, earlyAnswers
.format
));
577 // early exit if you are using a style guide
578 if (earlyAnswers
.source
=== "guide") {
579 if (!earlyAnswers
.packageJsonExists
) {
580 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.");
583 if (earlyAnswers
.installESLint
=== false && !semver
.satisfies(earlyAnswers
.localESLintVersion
, earlyAnswers
.requiredESLintVersionRange
)) {
584 log
.info(`Note: it might not work since ESLint's version is mismatched with the ${earlyAnswers.styleguide} config.`);
586 if (earlyAnswers
.styleguide
=== "airbnb" && earlyAnswers
.framework
!== "react") {
587 earlyAnswers
.styleguide
= "airbnb-base";
590 const config
= processAnswers(earlyAnswers
);
592 if (Array
.isArray(config
.extends)) {
593 config
.extends.push(earlyAnswers
.styleguide
);
594 } else if (config
.extends) {
595 config
.extends = [config
.extends, earlyAnswers
.styleguide
];
597 config
.extends = [earlyAnswers
.styleguide
];
600 const modules
= getModulesList(config
);
602 return askInstallModules(modules
, earlyAnswers
.packageJsonExists
)
603 .then(() => writeFile(config
, earlyAnswers
.format
));
607 if (earlyAnswers
.source
=== "auto") {
608 const combinedAnswers
= Object
.assign({}, earlyAnswers
);
609 const config
= processAnswers(combinedAnswers
);
610 const modules
= getModulesList(config
);
612 return askInstallModules(modules
).then(() => writeFile(config
, earlyAnswers
.format
));
615 // continue with the style questions otherwise...
616 return inquirer
.prompt([
620 message
: "What style of indentation do you use?",
622 choices
: [{ name
: "Tabs", value
: "tab" }, { name
: "Spaces", value
: 4 }]
627 message
: "What quotes do you use for strings?",
629 choices
: [{ name
: "Double", value
: "double" }, { name
: "Single", value
: "single" }]
634 message
: "What line endings do you use?",
636 choices
: [{ name
: "Unix", value
: "unix" }, { name
: "Windows", value
: "windows" }]
641 message
: "Do you require semicolons?",
645 const totalAnswers
= Object
.assign({}, earlyAnswers
, answers
);
647 const config
= processAnswers(totalAnswers
);
648 const modules
= getModulesList(config
);
650 return askInstallModules(modules
).then(() => writeFile(config
, earlyAnswers
.format
));
655 //------------------------------------------------------------------------------
657 //------------------------------------------------------------------------------
661 hasESLintVersionConflict
,
664 /* istanbul ignore next */initializeConfig() {
669 module
.exports
= init
;