UNPKG

@eslint/create-config

Version:

Utility to create ESLint config files.

320 lines (270 loc) 12.2 kB
/** * @fileoverview to generate config files. * @author 唯然<weiran.zsd@outlook.com> */ //----------------------------------------------------------------------------- // Imports //----------------------------------------------------------------------------- import process from "node:process"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { writeFile } from "node:fs/promises"; import enquirer from "enquirer"; import { isPackageTypeModule, installSyncSaveDev, fetchPeerDependencies, findPackageJson } from "./utils/npm-utils.js"; import { getShorthandName } from "./utils/naming.js"; import * as log from "./utils/logging.js"; //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- /** * Get the file extensions to lint based on the user's answers. * @param {Object} answers The answers provided by the user. * @returns {string[]} The file extensions to lint. */ function getExtensions(answers) { const extensions = ["js", "mjs", "cjs"]; if (answers.language === "typescript") { extensions.push("ts"); } if (answers.framework === "vue") { extensions.push("vue"); } if (answers.framework === "react") { extensions.push("jsx"); if (answers.language === "typescript") { extensions.push("tsx"); } } return extensions; } //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- /** * Class representing a ConfigGenerator. */ export class ConfigGenerator { /** * Create a ConfigGenerator. * @param {Object} options The options for the ConfigGenerator. * @param {string} options.cwd The current working directory. * @param {Object} options.answers The answers provided by the user. * @returns {ConfigGenerator} The ConfigGenerator instance. */ constructor(options) { this.cwd = options.cwd; this.packageJsonPath = options.packageJsonPath || findPackageJson(this.cwd); this.answers = options.answers || {}; this.result = { devDependencies: ["eslint"], configFilename: "eslint.config.js", configContent: "", installFlags: ["-D"] }; } /** * Prompt the user for input. * @returns {void} */ async prompt() { const questions = [ { type: "select", name: "purpose", message: "How would you like to use ESLint?", initial: 1, choices: [ { message: "To check syntax only", name: "syntax" }, { message: "To check syntax and find problems", name: "problems" } ] }, { type: "select", name: "moduleType", message: "What type of modules does your project use?", initial: 0, choices: [ { message: "JavaScript modules (import/export)", name: "esm" }, { message: "CommonJS (require/exports)", name: "commonjs" }, { message: "None of these", name: "script" } ] }, { type: "select", name: "framework", message: "Which framework does your project use?", initial: 0, choices: [ { message: "React", name: "react" }, { message: "Vue.js", name: "vue" }, { message: "None of these", name: "none" } ] }, { type: "select", name: "language", message: "Does your project use TypeScript?", choices: [ { message: "No", name: "javascript" }, { message: "Yes", name: "typescript" } ], initial: 0 }, { type: "multiselect", name: "env", message: "Where does your code run?", hint: "(Press <space> to select, <a> to toggle all, <i> to invert selection)", initial: 0, choices: [ { message: "Browser", name: "browser" }, { message: "Node", name: "node" } ] } ]; const answers = await enquirer.prompt(questions); Object.assign(this.answers, answers); } /** * Calculate the configuration based on the user's answers. * @returns {void} */ calc() { const isESMModule = isPackageTypeModule(this.packageJsonPath); this.result.configFilename = isESMModule ? "eslint.config.js" : "eslint.config.mjs"; this.answers.config = typeof this.answers.config === "string" ? { packageName: this.answers.config, type: "flat" } : this.answers.config; const extensions = `**/*.{${getExtensions(this.answers)}}`; let importContent = "import { defineConfig } from \"eslint/config\";\n"; const helperContent = `import path from "node:path"; import { fileURLToPath } from "node:url"; import { FlatCompat } from "@eslint/eslintrc"; import js from "@eslint/js"; // mimic CommonJS variables -- not needed if using CommonJS const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const compat = new FlatCompat({baseDirectory: __dirname, recommendedConfig: js.configs.recommended}); `; let exportContent = ""; let needCompatHelper = false; if (this.answers.moduleType === "commonjs" || this.answers.moduleType === "script") { exportContent += ` { files: ["**/*.js"], languageOptions: { sourceType: "${this.answers.moduleType}" } },\n`; } if (this.answers.env?.length > 0) { this.result.devDependencies.push("globals"); importContent += "import globals from \"globals\";\n"; const envContent = { browser: "globals: globals.browser", node: "globals: globals.node", "browser,node": "globals: {...globals.browser, ...globals.node}" }; exportContent += ` { files: ["${extensions}"], languageOptions: { ${envContent[this.answers.env.join(",")]} } },\n`; } if (this.answers.purpose === "syntax") { // no need to install any plugin } else if (this.answers.purpose === "problems") { this.result.devDependencies.push("@eslint/js"); importContent += "import js from \"@eslint/js\";\n"; exportContent += ` { files: ["${extensions}"], plugins: { js }, extends: ["js/recommended"] },\n`; } if (this.answers.language === "typescript") { this.result.devDependencies.push("typescript-eslint"); importContent += "import tseslint from \"typescript-eslint\";\n"; exportContent += " tseslint.configs.recommended,\n"; } if (this.answers.framework === "vue") { this.result.devDependencies.push("eslint-plugin-vue"); importContent += "import pluginVue from \"eslint-plugin-vue\";\n"; exportContent += " pluginVue.configs[\"flat/essential\"],\n"; // https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser if (this.answers.language === "typescript") { exportContent += " { files: [\"**/*.vue\"], languageOptions: { parserOptions: { parser: tseslint.parser } } },\n"; } } if (this.answers.framework === "react") { this.result.devDependencies.push("eslint-plugin-react"); importContent += "import pluginReact from \"eslint-plugin-react\";\n"; exportContent += " pluginReact.configs.flat.recommended,\n"; } if (this.answers.config) { const config = this.answers.config; this.result.devDependencies.push(config.packageName); // install peer dependencies - it's needed for most eslintrc-style shared configs. const peers = fetchPeerDependencies(config.packageName); if (peers !== null) { const eslintIndex = peers.findIndex(dep => (dep.startsWith("eslint@"))); if (eslintIndex === -1) { // eslint is not in the peer dependencies this.result.devDependencies.push(...peers); } else { // eslint is in the peer dependencies => overwrite eslint version this.result.devDependencies[0] = peers[eslintIndex]; peers.splice(eslintIndex, 1); this.result.devDependencies.push(...peers); } } if (config.type === "flat" || config.type === void 0) { importContent += `import config from "${config.packageName}";\n`; exportContent += " config,\n"; } else if (config.type === "eslintrc") { needCompatHelper = true; const shorthandName = getShorthandName(config.packageName, "eslint-config"); exportContent += ` compat.extends("${shorthandName}"),\n`; } } if (needCompatHelper) { this.result.devDependencies.push("@eslint/eslintrc", "@eslint/js"); } const lintFilesConfig = ` { files: ["${extensions}"] },\n`; exportContent = `${lintFilesConfig}${exportContent}`; this.result.configContent = `${importContent} ${needCompatHelper ? helperContent : ""} export default defineConfig([\n${exportContent || " {}\n"}]);`; // defaults to `[{}]` to avoid empty config warning } /** * Output the configuration. * @returns {void} */ async output() { log.info("The config that you've selected requires the following dependencies:\n"); log.info(this.result.devDependencies.join(", ")); const questions = [{ type: "toggle", name: "executeInstallation", message: "Would you like to install them now?", enabled: "Yes", disabled: "No", initial: 1 }, { type: "select", name: "packageManager", message: "Which package manager do you want to use?", initial: 0, choices: ["npm", "yarn", "pnpm", "bun"], skip() { return this.state.answers.executeInstallation === false; } }]; const { executeInstallation, packageManager } = (await enquirer.prompt(questions)); const configPath = path.join(this.cwd, this.result.configFilename); if (executeInstallation === true) { log.info("☕️Installing..."); installSyncSaveDev(this.result.devDependencies, packageManager, this.result.installFlags); await writeFile(configPath, this.result.configContent); // import("eslint") won't work in some cases. // refs: https://github.com/eslint/create-config/issues/8, https://github.com/eslint/create-config/issues/12 const eslintBin = path.join(this.packageJsonPath, "../node_modules/eslint/bin/eslint.js"); const result = spawnSync(process.execPath, [eslintBin, "--fix", "--quiet", configPath], { encoding: "utf8" }); if (result.error || result.status !== 0) { log.error("A config file was generated, but the config file itself may not follow your linting rules."); } else { log.info(`Successfully created ${configPath} file.`); } } else { await writeFile(configPath, this.result.configContent); log.info(`Successfully created ${configPath} file.`); log.warn("You will need to install the dependencies yourself."); } } }