diff --git a/.npmrc b/.npmrc index 9cf94950..30866b50 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ -package-lock=false \ No newline at end of file +package-lock=false +node-options=--loader=esmock diff --git a/README.md b/README.md index 9e2e1dd0..bce04e27 100644 --- a/README.md +++ b/README.md @@ -21,22 +21,12 @@ npx @eslint/create-config If you want to use a specific shareable config that is hosted on npm, you can use the `--config` option and specify the package name: ```bash -# use `eslint-config-semistandard` shared config - -# npm 7+ -npm init @eslint/config -- --config semistandard - -# or (`eslint-config` prefix is optional) -npm init @eslint/config -- --config eslint-config-semistandard - -# ⚠️ npm 6.x no extra double-dash: -npm init @eslint/config --config semistandard +# use `eslint-config-standard` shared config +npm init @eslint/config -- --config eslint-config-standard ``` -The `--config` flag also supports passing in arrays: +To use an eslintrc-style (legacy) shared config: ```bash -npm init @eslint/config -- --config semistandard,standard -# or -npm init @eslint/config -- --config semistandard --config standard +npm init @eslint/config -- --eslintrc --config eslint-config-standard ``` diff --git a/bin/create-config.js b/bin/create-config.js old mode 100644 new mode 100755 index 66ff533b..f385b205 --- a/bin/create-config.js +++ b/bin/create-config.js @@ -1,10 +1,39 @@ #!/usr/bin/env node /** - * @fileoverview Main CLI that is run via the eslint command. - * @author Nicholas C. Zakas + * @fileoverview Main CLI that is run via the `npm init @eslint/config` command. + * @author 唯然 */ -/* eslint no-console:off -- CLI */ -import { initializeConfig } from "../lib/init/config-initializer.js"; -initializeConfig(); +import { ConfigGenerator } from "../lib/config-generator.js"; +import { findPackageJson } from "../lib/utils/npm-utils.js"; +import process from "process"; + + +const cwd = process.cwd(); +const packageJsonPath = findPackageJson(cwd); + +if (packageJsonPath === null) { + throw new Error("A package.json file is necessary to initialize ESLint. Run `npm init` to create a package.json file and try again."); +} + +const argv = process.argv; +const sharedConfigIndex = process.argv.indexOf("--config"); + +if (sharedConfigIndex === -1) { + const generator = new ConfigGenerator({ cwd, packageJsonPath }); + + await generator.prompt(); + generator.calc(); + await generator.output(); +} else { + + // passed "--config" + const packageName = argv[sharedConfigIndex + 1]; + const type = argv.includes("--eslintrc") ? "eslintrc" : "flat"; + const answers = { purpose: "style", moduleType: "module", styleguide: { packageName, type } }; + const generator = new ConfigGenerator({ cwd, packageJsonPath, answers }); + + generator.calc(); + await generator.output(); +} diff --git a/eslint.config.js b/eslint.config.js index a8293635..b1544b43 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,4 @@ import eslintConfigESLint from "eslint-config-eslint"; -import globals from "globals"; export default [ { @@ -8,13 +7,5 @@ export default [ "tests/fixtures/" ] }, - ...eslintConfigESLint, - { - files: ["tests/**"], - languageOptions: { - globals: { - ...globals.mocha - } - } - } + ...eslintConfigESLint ]; diff --git a/lib/config-generator.js b/lib/config-generator.js new file mode 100644 index 00000000..bdbdc676 --- /dev/null +++ b/lib/config-generator.js @@ -0,0 +1,278 @@ +/** + * @fileoverview to generate config files. + * @author 唯然 + */ +import process from "process"; +import path from "path"; +import { spawnSync } from "child_process"; +import { writeFile } from "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"; + +// TODO: need to specify the package version - they may export flat configs in the future. +const jsStyleGuides = [ + { message: "Airbnb: https://github.com/airbnb/javascript", name: "airbnb", value: { packageName: "eslint-config-airbnb", type: "eslintrc" } }, + { message: "Standard: https://github.com/standard/standard", name: "standard", value: { packageName: "eslint-config-standard", type: "eslintrc" } }, + { message: "XO: https://github.com/xojs/eslint-config-xo", name: "xo", value: { packageName: "eslint-config-xo", type: "eslintrc" } } +]; +const tsStyleGuides = [ + { message: "Standard: https://github.com/standard/eslint-config-standard-with-typescript", name: "standard", value: { packageName: "eslint-config-standard-with-typescript", type: "eslintrc" } }, + { message: "XO: https://github.com/xojs/eslint-config-xo-typescript", name: "xo", value: { packageName: "eslint-config-xo-typescript", type: "eslintrc" } } +]; + +/** + * 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: "" + }; + } + + /** + * 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" }, + { message: "To check syntax, find problems, and enforce code style", name: "style" } + ] + }, + { + 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 to select, to toggle all, 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); + + if (answers.purpose === "style") { + const choices = this.answers.language === "javascript" ? jsStyleGuides : tsStyleGuides; + const styleguideAnswer = await enquirer.prompt({ + type: "select", + name: "styleguide", + message: "Which style guide do you want to follow?", + choices, + result: choice => choices.find(it => it.name === choice).value + }); + + Object.assign(this.answers, styleguideAnswer); + } + } + + /** + * 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"; + + let importContent = ""; + const helperContent = `import path from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; +import pluginJs 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: pluginJs.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 += ` {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 pluginJs from \"@eslint/js\";\n"; + exportContent += " pluginJs.configs.recommended,\n"; + } else if (this.answers.purpose === "style") { + const styleguide = typeof this.answers.styleguide === "string" + ? { packageName: this.answers.styleguide, type: "flat" } + : this.answers.styleguide; + + this.result.devDependencies.push(styleguide.packageName); + + // install peer dependencies - it's needed for most eslintrc-style shared configs. + const peers = fetchPeerDependencies(styleguide.packageName); + + if (peers !== null) { + this.result.devDependencies.push(...peers); + } + + if (styleguide.type === "flat" || styleguide.type === void 0) { + importContent += `import styleGuide from "${styleguide.packageName}";\n`; + exportContent += " ...[].concat(styleGuide),\n"; + } else if (styleguide.type === "eslintrc") { + needCompatHelper = true; + + const shorthandName = getShorthandName(styleguide.packageName, "eslint-config"); + + exportContent += ` ...compat.extends("${shorthandName}"),\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"; + } + + if (this.answers.framework === "react") { + this.result.devDependencies.push("eslint-plugin-react"); + importContent += "import pluginReactConfig from \"eslint-plugin-react/configs/recommended.js\";\n"; + exportContent += " pluginReactConfig,\n"; + } + + if (needCompatHelper) { + this.result.devDependencies.push("@eslint/eslintrc", "@eslint/js"); + } + this.result.configContent = `${importContent} +${needCompatHelper ? helperContent : ""} +export default [\n${exportContent}];`; + } + + /** + * 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 installDevDeps = (await enquirer.prompt({ + type: "toggle", + name: "executeInstallation", + message: "Would you like to install them now?", + enabled: "Yes", + disabled: "No", + initial: 1 + })).executeInstallation; + + const configPath = path.join(this.cwd, this.result.configFilename); + + if (installDevDeps === true) { + const packageManager = (await enquirer.prompt({ + type: "select", + name: "packageManager", + message: "Which package manager do you want to use?", + initial: 0, + choices: ["npm", "yarn", "pnpm", "bun"] + })).packageManager; + + log.info("☕️Installing..."); + installSyncSaveDev(this.result.devDependencies, packageManager); + 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."); + } + } +} diff --git a/lib/init/config-file.js b/lib/init/config-file.js deleted file mode 100644 index e8ca0399..00000000 --- a/lib/init/config-file.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - * @fileoverview Helper to locate and load configuration files. - * @author Nicholas C. Zakas - */ - - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -import fs from "fs"; -import path from "path"; -import stringify from "json-stable-stringify-without-jsonify"; -import debugEsm from "debug"; -import spawn from "cross-spawn"; -import * as log from "../shared/logging.js"; - -const debug = debugEsm("eslint:config-file"); -const cwd = process.cwd(); - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Determines sort order for object keys for json-stable-stringify - * - * see: https://github.com/samn/json-stable-stringify#cmp - * @param {Object} a The first comparison object ({key: akey, value: avalue}) - * @param {Object} b The second comparison object ({key: bkey, value: bvalue}) - * @returns {number} 1 or -1, used in stringify cmp method - */ -function sortByKey(a, b) { - return a.key > b.key ? 1 : -1; -} - -//------------------------------------------------------------------------------ -// Private -//------------------------------------------------------------------------------ - -/** - * Writes a configuration file in JSON format. - * @param {Object} config The configuration object to write. - * @param {string} filePath The filename to write to. - * @returns {void} - * @private - */ -function writeJSONConfigFile(config, filePath) { - debug(`Writing JSON config file: ${filePath}`); - - const content = `${stringify(config, { cmp: sortByKey, space: 4 })}\n`; - - fs.writeFileSync(filePath, content, "utf8"); -} - -/** - * Writes a configuration file in YAML format. - * @param {Object} config The configuration object to write. - * @param {string} filePath The filename to write to. - * @returns {void} - * @private - */ -async function writeYAMLConfigFile(config, filePath) { - debug(`Writing YAML config file: ${filePath}`); - - // lazy load YAML to improve performance when not used - const yaml = await import("js-yaml"); - - const content = yaml.dump(config, { sortKeys: true }); - - fs.writeFileSync(filePath, content, "utf8"); -} - -/** - * Writes a configuration file in JavaScript format. - * @param {Object} config The configuration object to write. - * @param {string} filePath The filename to write to. - * @throws {Error} If an error occurs linting the config file contents. - * @returns {void} - * @private - */ -async function writeJSConfigFile(config, filePath) { - debug(`Writing JS config file: ${filePath}`); - - const stringifiedContent = `module.exports = ${stringify(config, { cmp: sortByKey, space: 4 })}\n`; - - fs.writeFileSync(filePath, stringifiedContent, "utf8"); - - // 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(cwd, "./node_modules/.bin/eslint"); - const result = spawn.sync(eslintBin, ["--fix", "--quiet", filePath], { 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."); - } -} - -/** - * Writes a configuration file. - * @param {Object} config The configuration object to write. - * @param {string} filePath The filename to write to. - * @returns {void} - * @throws {Error} When an unknown file type is specified. - * @private - */ -async function write(config, filePath) { - switch (path.extname(filePath)) { - case ".js": - case ".cjs": - await writeJSConfigFile(config, filePath); - break; - - case ".json": - writeJSONConfigFile(config, filePath); - break; - - case ".yaml": - case ".yml": - await writeYAMLConfigFile(config, filePath); - break; - - default: - throw new Error("Can't write to unknown file type."); - } -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -export { - write -}; diff --git a/lib/init/config-initializer.js b/lib/init/config-initializer.js deleted file mode 100644 index bbf12dbc..00000000 --- a/lib/init/config-initializer.js +++ /dev/null @@ -1,631 +0,0 @@ -/** - * @fileoverview Config initialization wizard. - * @author Ilya Volodin - */ - - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -import path from "path"; -import fs from "fs"; -import enquirer from "enquirer"; -import semver from "semver"; -import { Legacy } from "@eslint/eslintrc"; -import { info } from "../shared/logging.js"; -import * as ConfigFile from "./config-file.js"; -import * as npmUtils from "./npm-utils.js"; -import mri from "mri"; - -const { ConfigOps, naming, ModuleResolver } = Legacy; - -//------------------------------------------------------------------------------ -// Private -//------------------------------------------------------------------------------ - -/** - * check if the package.type === "module" - * @returns {boolean} return true if the package.type === "module" - */ -function isPackageTypeModule() { - const pkgJSONPath = npmUtils.findPackageJson(); - - if (pkgJSONPath) { - const pkgJSONContents = JSON.parse(fs.readFileSync(pkgJSONPath, "utf8")); - - if (pkgJSONContents.type === "module") { - return true; - } - } - - return false; -} - -/* istanbul ignore next: hard to test fs function */ -/** - * Create .eslintrc file in the current working directory - * @param {Object} config object that contains user's answers - * @param {string} format The file format to write to. - * @returns {void} - */ -async function writeFile(config, format) { - - // default is .js - let extname = ".js"; - - if (format === "YAML") { - extname = ".yml"; - } else if (format === "JSON") { - extname = ".json"; - } else if (format === "JavaScript") { - if (isPackageTypeModule()) { - extname = ".cjs"; - } - } - - delete config.installedESLint; - - await ConfigFile.write(config, `./.eslintrc${extname}`); - info(`Successfully created .eslintrc${extname} file in ${process.cwd()}`); -} - -/** - * Get the peer dependencies of the given module. - * This adds the gotten value to cache at the first time, then reuses it. - * In a process, this function is called twice, but `npmUtils.fetchPeerDependencies` needs to access network which is relatively slow. - * @param {string} moduleName The module name to get. - * @returns {Object} The peer dependencies of the given module. - * This object is the object of `peerDependencies` field of `package.json`. - * Returns null if npm was not found. - */ -function getPeerDependencies(moduleName) { - let result = getPeerDependencies.cache.get(moduleName); - - if (!result) { - info(`Checking peerDependencies of ${moduleName}`); - - result = npmUtils.fetchPeerDependencies(moduleName); - getPeerDependencies.cache.set(moduleName, result); - } - - return result; -} -getPeerDependencies.cache = new Map(); - -/** - * Return necessary plugins, configs, parsers, etc. based on the config - * @param {Object} config config object - * @param {boolean} [installESLint=true] If `false` is given, it does not install eslint. - * @returns {string[]} An array of modules to be installed. - */ -function getModulesList(config, installESLint) { - const modules = {}; - - // Create a list of modules which should be installed based on config - if (config.plugins) { - for (const plugin of config.plugins) { - const moduleName = naming.normalizePackageName(plugin, "eslint-plugin"); - - modules[moduleName] = "latest"; - } - } - - const extendList = []; - const overrides = config.overrides || []; - - for (const item of [config, ...overrides]) { - if (typeof item.extends === "string") { - extendList.push(item.extends); - } else if (Array.isArray(item.extends)) { - extendList.push(...item.extends); - } - } - - for (const extend of extendList) { - if (extend.startsWith("eslint:") || extend.startsWith("plugin:")) { - continue; - } - const moduleName = naming.normalizePackageName(extend, "eslint-config"); - - modules[moduleName] = "latest"; - Object.assign( - modules, - getPeerDependencies(`${moduleName}@latest`) - ); - } - - const parser = config.parser || (config.parserOptions && config.parserOptions.parser); - - if (parser) { - modules[parser] = "latest"; - } - - if (installESLint === false) { - delete modules.eslint; - } else { - const installStatus = npmUtils.checkDevDeps(["eslint"]); - - // Mark to show messages if it's new installation of eslint. - if (installStatus.eslint === false) { - info("Local ESLint installation not found."); - modules.eslint = modules.eslint || "latest"; - config.installedESLint = true; - } - } - - return Object.keys(modules).map(name => `${name}@${modules[name]}`); -} - -/** - * process user's answers and create config object - * @param {Object} answers answers received from enquirer - * @returns {Object} config object - */ -function processAnswers(answers) { - const config = { - rules: {}, - env: {}, - parserOptions: {}, - extends: [], - plugins: [], - overrides: [] - }; - - config.parserOptions.ecmaVersion = "latest"; - config.env.es2021 = true; - - if (answers.format === "JavaScript") { - config.overrides.push({ - files: [".eslintrc.{js,cjs}"], - parserOptions: { sourceType: "script" }, - env: { node: true } - }); - } - - // set the module type - if (answers.moduleType === "esm") { - config.parserOptions.sourceType = "module"; - } else if (answers.moduleType === "commonjs") { - config.env.commonjs = true; - } - - // add in browser and node environments if necessary - answers.env.forEach(env => { - config.env[env] = true; - }); - - // if answers.source == "guide", the ts supports should be in the shared config. - if (answers.typescript && answers.source !== "guide") { - - // .vue files should be parsed by vue-eslint-parser. - // https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser - if (answers.framework === "vue") { - config.parserOptions.parser = "@typescript-eslint/parser"; - } else { - config.parser = "@typescript-eslint/parser"; - } - - config.plugins.push("@typescript-eslint"); - } - - // set config.extends based the selected guide - if (answers.source === "guide") { - if (answers.styleguide === "airbnb" && answers.framework !== "react") { - config.extends.push("airbnb-base"); - } else if (answers.styleguide === "xo-typescript") { - config.extends.push("xo"); - config.overrides.push({ - files: ["*.ts", "*.tsx"], - extends: ["xo-typescript"] - }); - } else { - config.extends.push(answers.styleguide); - } - } - - // setup rules based on problems/style enforcement preferences - if (answers.purpose === "problems") { - config.extends.unshift("eslint:recommended"); - } else if (answers.purpose === "style") { - if (answers.source === "prompt") { - config.extends.unshift("eslint:recommended"); - config.rules.indent = ["error", answers.indent]; - config.rules.quotes = ["error", answers.quotes]; - config.rules["linebreak-style"] = ["error", answers.linebreak]; - config.rules.semi = ["error", answers.semi ? "always" : "never"]; - } - } - if (answers.typescript && config.extends.includes("eslint:recommended")) { - config.extends.push("plugin:@typescript-eslint/recommended"); - } - - // add in library information - // The configuration of the framework plugins should be placed at the end of extends. - if (answers.framework === "react" && answers.styleguide !== "airbnb") { - config.plugins.push("react"); - config.extends.push("plugin:react/recommended"); - } else if (answers.framework === "vue") { - config.plugins.push("vue"); - config.extends.push("plugin:vue/vue3-essential"); - } - - // normalize extends - if (config.extends.length === 0) { - delete config.extends; - } else if (config.extends.length === 1) { - config.extends = config.extends[0]; - } - if (config.overrides.length === 0) { - delete config.overrides; - } - if (config.plugins.length === 0) { - delete config.plugins; - } - - ConfigOps.normalizeToStrings(config); - return config; -} - -/** - * Get the version of the local ESLint. - * @returns {string|null} The version. If the local ESLint was not found, returns null. - */ -function getLocalESLintVersion() { - try { - const eslintPkgPath = path.join(ModuleResolver.resolve("eslint/package.json", path.join(process.cwd(), "__placeholder__.js"))); - const eslintPkg = JSON.parse(fs.readFileSync(eslintPkgPath, "utf8")); - - return eslintPkg.version || null; - } catch { - return null; - } -} - -/** - * Get the shareable config name of the chosen style guide. - * @param {Object} answers The answers object. - * @returns {string} The shareable config name. - */ -function getStyleGuideName(answers) { - if (answers.styleguide === "airbnb" && answers.framework !== "react") { - return "airbnb-base"; - } - return answers.styleguide; -} - -/** - * Check whether the local ESLint version conflicts with the required version of the chosen shareable config. - * @param {Object} answers The answers object. - * @returns {boolean} `true` if the local ESLint is found then it conflicts with the required version of the chosen shareable config. - */ -function hasESLintVersionConflict(answers) { - - // Get the local ESLint version. - const localESLintVersion = getLocalESLintVersion(); - - if (!localESLintVersion) { - return false; - } - - // Get the required range of ESLint version. - const configName = getStyleGuideName(answers); - const moduleName = `eslint-config-${configName}@latest`; - const peerDependencies = getPeerDependencies(moduleName) || {}; - const requiredESLintVersionRange = peerDependencies.eslint; - - if (!requiredESLintVersionRange) { - return false; - } - - answers.localESLintVersion = localESLintVersion; - answers.requiredESLintVersionRange = requiredESLintVersionRange; - - // Check the version. - if (semver.satisfies(localESLintVersion, requiredESLintVersionRange)) { - answers.installESLint = false; - return false; - } - - return true; -} - -/** - * Install modules. - * @param {string[]} modules Modules to be installed. - * @param {string} packageManager Package manager to use for installation. - * @returns {void} - */ -function installModules(modules, packageManager) { - info(`Installing ${modules.join(", ")}`); - npmUtils.installSyncSaveDev(modules, packageManager); -} - -/* istanbul ignore next: no need to test enquirer */ -/** - * Ask user to install modules. - * @param {string[]} modules Array of modules to be installed. - * @returns {Promise} Answer that indicates if user wants to install. - */ -function askInstallModules(modules) { - - // If no modules, do nothing. - if (modules.length === 0) { - return Promise.resolve(); - } - - info("The config that you've selected requires the following dependencies:\n"); - info(modules.join(" ")); - return enquirer.prompt([ - { - type: "toggle", - name: "executeInstallation", - message: "Would you like to install them now?", - enabled: "Yes", - disabled: "No", - initial: 1, - skip() { - return !modules.length; - }, - result(input) { - return this.skipped ? null : input; - } - }, - { - 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; - } - } - ]).then(({ executeInstallation, packageManager }) => { - if (executeInstallation) { - installModules(modules, packageManager); - } - }); -} - -/* istanbul ignore next: no need to test enquirer */ -/** - * Ask use a few questions on command prompt - * @returns {Promise} The promise with the result of the prompt - * @throws {Error} If `package.json` file doesn't exist. - */ -function promptUser() { - const packageJsonExists = npmUtils.checkPackageJson(); - - if (!packageJsonExists) { - throw new Error("A package.json file is necessary to initialize ESLint. Run `npm init` to create a package.json file and try again."); - } - - const styleGuides = []; - - return enquirer.prompt([ - { - type: "select", - name: "purpose", - message: "How would you like to use ESLint?", - - // The returned number matches the name value of nth in the choices array. - initial: 1, - choices: [ - { message: "To check syntax only", name: "syntax" }, - { message: "To check syntax and find problems", name: "problems" }, - { message: "To check syntax, find problems, and enforce code style", name: "style" } - ] - }, - { - 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: "none" } - ] - }, - { - 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: "toggle", - name: "typescript", - message: "Does your project use TypeScript?", - enabled: "Yes", - disabled: "No", - initial: 0, - result(val) { - if (val) { - - // remove airbnb/google javascript style guide, as they do not support ts - styleGuides.push( - { message: "Standard: https://github.com/standard/eslint-config-standard-with-typescript", name: "standard-with-typescript" }, - { message: "XO: https://github.com/xojs/eslint-config-xo-typescript", name: "xo-typescript" } - ); - } else { - styleGuides.push( - { message: "Airbnb: https://github.com/airbnb/javascript", name: "airbnb" }, - { message: "Standard: https://github.com/standard/standard", name: "standard" }, - { message: "XO: https://github.com/xojs/eslint-config-xo", name: "xo" } - ); - } - return val; - } - }, - { - type: "multiselect", - name: "env", - message: "Where does your code run?", - hint: "(Press to select, to toggle all, to invert selection)", - initial: 0, - choices: [ - { message: "Browser", name: "browser" }, - { message: "Node", name: "node" } - ] - }, - { - type: "select", - name: "source", - message: "How would you like to define a style for your project?", - choices: [ - { message: "Use a popular style guide", name: "guide" }, - { message: "Answer questions about your style", name: "prompt" } - ], - skip() { - return this.state.answers.purpose !== "style"; - }, - result(input) { - return this.skipped ? null : input; - } - }, - { - type: "select", - name: "styleguide", - message: "Which style guide do you want to follow?", - choices: styleGuides, - skip() { - return this.state.answers.source !== "guide"; - }, - result(input) { - return this.skipped ? null : input; - } - }, - { - type: "select", - name: "format", - message: "What format do you want your config file to be in?", - initial: 0, - choices: ["JavaScript", "YAML", "JSON"] - }, - { - type: "toggle", - name: "installESLint", - message() { - const { answers } = this.state; - const verb = semver.ltr(answers.localESLintVersion, answers.requiredESLintVersionRange) - ? "upgrade" - : "downgrade"; - - return `The style guide "${answers.styleguide}" requires eslint@${answers.requiredESLintVersionRange}. You are currently using eslint@${answers.localESLintVersion}.\n Do you want to ${verb}?`; - }, - enabled: "Yes", - disabled: "No", - initial: 1, - skip() { - return !(this.state.answers.source === "guide" && hasESLintVersionConflict(this.state.answers)); - }, - result(input) { - return this.skipped ? null : input; - } - } - ]).then(earlyAnswers => { - - // early exit if no style guide is necessary - if (earlyAnswers.purpose !== "style") { - const config = processAnswers(earlyAnswers); - const modules = getModulesList(config); - - return askInstallModules(modules) - .then(() => writeFile(config, earlyAnswers.format)); - } - - // early exit if you are using a style guide - if (earlyAnswers.source === "guide") { - if (earlyAnswers.installESLint === false && !semver.satisfies(earlyAnswers.localESLintVersion, earlyAnswers.requiredESLintVersionRange)) { - info(`Note: it might not work since ESLint's version is mismatched with the ${earlyAnswers.styleguide} config.`); - } - - const config = processAnswers(earlyAnswers); - const modules = getModulesList(config); - - return askInstallModules(modules) - .then(() => writeFile(config, earlyAnswers.format)); - - } - - // continue with the style questions otherwise... - return enquirer.prompt([ - { - type: "select", - name: "indent", - message: "What style of indentation do you use?", - initial: 0, - choices: [{ message: "Tabs", name: "tab" }, { message: "Spaces", name: 4 }] - }, - { - type: "select", - name: "quotes", - message: "What quotes do you use for strings?", - initial: 0, - choices: [{ message: "Double", name: "double" }, { message: "Single", name: "single" }] - }, - { - type: "select", - name: "linebreak", - message: "What line endings do you use?", - initial: 0, - choices: [{ message: "Unix", name: "unix" }, { message: "Windows", name: "windows" }] - }, - { - type: "toggle", - name: "semi", - message: "Do you require semicolons?", - enabled: "Yes", - disabled: "No", - initial: 1 - } - ]).then(answers => { - const totalAnswers = Object.assign({}, earlyAnswers, answers); - - const config = processAnswers(totalAnswers); - const modules = getModulesList(config); - - return askInstallModules(modules).then(() => writeFile(config, earlyAnswers.format)); - }); - }); -} - -/* istanbul ignore next */ -/** - * an wrapper for promptUser - * @returns {void} - */ -function initializeConfig() { - const argv = mri(process.argv.slice(2)); - - if (argv.config) { - const config = { - extends: typeof argv.config === "string" ? argv.config.split(",") : argv.config - }; - const modules = getModulesList(config); - - return askInstallModules(modules).then(() => writeFile(config, "JavaScript")); - } - - return promptUser(); -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -export { - getModulesList, - hasESLintVersionConflict, - installModules, - processAnswers, - writeFile, - initializeConfig -}; diff --git a/lib/shared/logging.js b/lib/utils/logging.js similarity index 100% rename from lib/shared/logging.js rename to lib/utils/logging.js diff --git a/lib/utils/naming.js b/lib/utils/naming.js new file mode 100644 index 00000000..da66b21b --- /dev/null +++ b/lib/utils/naming.js @@ -0,0 +1,98 @@ +/** + * Do not change! + * the file was copied from @eslint/eslintrc, to avoid the dependency on it. + * @fileoverview Common helpers for naming of plugins, formatters and configs + */ + +const NAMESPACE_REGEX = /^@.*\//iu; + +/** + * Brings package name to correct format based on prefix + * @param {string} name The name of the package. + * @param {string} prefix Can be either "eslint-plugin", "eslint-config" or "eslint-formatter" + * @returns {string} Normalized name of the package + * @private + */ +function normalizePackageName(name, prefix) { + let normalizedName = name; + + /** + * On Windows, name can come in with Windows slashes instead of Unix slashes. + * Normalize to Unix first to avoid errors later on. + * https://github.com/eslint/eslint/issues/5644 + */ + if (normalizedName.includes("\\")) { + normalizedName = normalizedName.replace(/\\/gu, "/"); + } + + if (normalizedName.charAt(0) === "@") { + + /** + * it's a scoped package + * package name is the prefix, or just a username + */ + const scopedPackageShortcutRegex = new RegExp(`^(@[^/]+)(?:/(?:${prefix})?)?$`, "u"), + scopedPackageNameRegex = new RegExp(`^${prefix}(-|$)`, "u"); + + if (scopedPackageShortcutRegex.test(normalizedName)) { + normalizedName = normalizedName.replace(scopedPackageShortcutRegex, `$1/${prefix}`); + } else if (!scopedPackageNameRegex.test(normalizedName.split("/")[1])) { + + /** + * for scoped packages, insert the prefix after the first / unless + * the path is already @scope/eslint or @scope/eslint-xxx-yyy + */ + normalizedName = normalizedName.replace(/^@([^/]+)\/(.*)$/u, `@$1/${prefix}-$2`); + } + } else if (!normalizedName.startsWith(`${prefix}-`)) { + normalizedName = `${prefix}-${normalizedName}`; + } + + return normalizedName; +} + +/** + * Removes the prefix from a fullname. + * @param {string} fullname The term which may have the prefix. + * @param {string} prefix The prefix to remove. + * @returns {string} The term without prefix. + */ +function getShorthandName(fullname, prefix) { + if (fullname[0] === "@") { + let matchResult = new RegExp(`^(@[^/]+)/${prefix}$`, "u").exec(fullname); + + if (matchResult) { + return matchResult[1]; + } + + matchResult = new RegExp(`^(@[^/]+)/${prefix}-(.+)$`, "u").exec(fullname); + if (matchResult) { + return `${matchResult[1]}/${matchResult[2]}`; + } + } else if (fullname.startsWith(`${prefix}-`)) { + return fullname.slice(prefix.length + 1); + } + + return fullname; +} + +/** + * Gets the scope (namespace) of a term. + * @param {string} term The term which may have the namespace. + * @returns {string} The namespace of the term if it has one. + */ +function getNamespaceFromTerm(term) { + const match = term.match(NAMESPACE_REGEX); + + return match ? match[0] : ""; +} + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +export { + normalizePackageName, + getShorthandName, + getNamespaceFromTerm +}; diff --git a/lib/init/npm-utils.js b/lib/utils/npm-utils.js similarity index 87% rename from lib/init/npm-utils.js rename to lib/utils/npm-utils.js index cfce2172..f8a4dd2c 100644 --- a/lib/init/npm-utils.js +++ b/lib/utils/npm-utils.js @@ -12,7 +12,7 @@ import fs from "fs"; import spawn from "cross-spawn"; import path from "path"; -import * as log from "../shared/logging.js"; +import * as log from "./logging.js"; //------------------------------------------------------------------------------ // Helpers @@ -77,13 +77,20 @@ function fetchPeerDependencies(packageName) { const error = npmProcess.error; if (error && error.code === "ENOENT") { + + // TODO: should throw an error instead of returning null return null; } const fetchedText = npmProcess.stdout.trim(); - return JSON.parse(fetchedText || "{}"); + const peers = JSON.parse(fetchedText || "{}"); + const dependencies = []; + Object.keys(peers).forEach(pkgName => { + dependencies.push(`${pkgName}@${peers[pkgName]}`); + }); + return dependencies; } /** @@ -155,6 +162,25 @@ function checkPackageJson(startDir) { return !!findPackageJson(startDir); } +/** + * check if the package.type === "module" + * @param {string} pkgJSONPath path to package.json + * @returns {boolean} return true if the package.type === "module" + */ +function isPackageTypeModule(pkgJSONPath) { + + if (pkgJSONPath) { + const pkgJSONContents = JSON.parse(fs.readFileSync(pkgJSONPath, "utf8")); + + if (pkgJSONContents.type === "module") { + return true; + } + } + + return false; +} + + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -165,5 +191,6 @@ export { findPackageJson, checkDeps, checkDevDeps, - checkPackageJson + checkPackageJson, + isPackageTypeModule }; diff --git a/package.json b/package.json index 33b58b29..ed23e694 100644 --- a/package.json +++ b/package.json @@ -29,43 +29,30 @@ "access": "public" }, "scripts": { - "lint": "eslint . --report-unused-disable-directives", + "lint": "eslint .", "release:generate:latest": "eslint-generate-release", "release:generate:alpha": "eslint-generate-prerelease alpha", "release:generate:beta": "eslint-generate-prerelease beta", "release:generate:rc": "eslint-generate-prerelease rc", "release:publish": "eslint-publish-release", - "test": "c8 mocha \"tests/init/**/*.js\"" - }, - "mocha": { - "loader": "esmock", - "ui": "bdd", - "timeout": 10000 + "test": "vitest run --coverage", + "test:snapshots": "vitest run snapshots", + "test:snapshots:update": "vitest -u run snapshots" }, "dependencies": { - "@eslint/eslintrc": "^1.0.3", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "enquirer": "^2.3.5", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "mri": "^1.2.0", - "semver": "^7.2.1" + "enquirer": "^2.3.5" }, "devDependencies": { - "c8": "^7.10.0", - "chai": "^4.3.4", - "eslint": "^8.48.0", + "@vitest/coverage-v8": "^1.3.1", + "eslint": "^8.56.0", "eslint-config-eslint": "^9.0.0", "eslint-release": "^3.2.0", "esmock": "^2.5.8", - "espree": "^9.0.0", - "globals": "^13.21.0", "lint-staged": "^12.1.2", "memfs": "^3.4.0", - "mocha": "^9.1.3", - "shelljs": "^0.8.4", "sinon": "^12.0.1", + "vitest": "^1.1.1", "yorkie": "^2.0.0" }, "engines": { diff --git a/tests/__snapshots__/problems-commonjs-none-javascript b/tests/__snapshots__/problems-commonjs-none-javascript new file mode 100644 index 00000000..be19a4ba --- /dev/null +++ b/tests/__snapshots__/problems-commonjs-none-javascript @@ -0,0 +1,17 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-commonjs-none-typescript b/tests/__snapshots__/problems-commonjs-none-typescript new file mode 100644 index 00000000..00a021df --- /dev/null +++ b/tests/__snapshots__/problems-commonjs-none-typescript @@ -0,0 +1,20 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "typescript-eslint", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-commonjs-react-javascript b/tests/__snapshots__/problems-commonjs-react-javascript new file mode 100644 index 00000000..bf035eaf --- /dev/null +++ b/tests/__snapshots__/problems-commonjs-react-javascript @@ -0,0 +1,20 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + pluginReactConfig, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "eslint-plugin-react", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-commonjs-react-typescript b/tests/__snapshots__/problems-commonjs-react-typescript new file mode 100644 index 00000000..efb4c318 --- /dev/null +++ b/tests/__snapshots__/problems-commonjs-react-typescript @@ -0,0 +1,23 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; +import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + pluginReactConfig, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "typescript-eslint", + "eslint-plugin-react", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-commonjs-vue-javascript b/tests/__snapshots__/problems-commonjs-vue-javascript new file mode 100644 index 00000000..8b58d41b --- /dev/null +++ b/tests/__snapshots__/problems-commonjs-vue-javascript @@ -0,0 +1,20 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import pluginVue from "eslint-plugin-vue"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + ...pluginVue.configs["flat/essential"], +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "eslint-plugin-vue", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-commonjs-vue-typescript b/tests/__snapshots__/problems-commonjs-vue-typescript new file mode 100644 index 00000000..fbf258bb --- /dev/null +++ b/tests/__snapshots__/problems-commonjs-vue-typescript @@ -0,0 +1,23 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; +import pluginVue from "eslint-plugin-vue"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + ...pluginVue.configs["flat/essential"], +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "typescript-eslint", + "eslint-plugin-vue", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-esm-none-javascript b/tests/__snapshots__/problems-esm-none-javascript new file mode 100644 index 00000000..feeaec46 --- /dev/null +++ b/tests/__snapshots__/problems-esm-none-javascript @@ -0,0 +1,16 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; + + +export default [ + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-esm-none-typescript b/tests/__snapshots__/problems-esm-none-typescript new file mode 100644 index 00000000..0ff96769 --- /dev/null +++ b/tests/__snapshots__/problems-esm-none-typescript @@ -0,0 +1,19 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + + +export default [ + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "typescript-eslint", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-esm-react-javascript b/tests/__snapshots__/problems-esm-react-javascript new file mode 100644 index 00000000..a529a49e --- /dev/null +++ b/tests/__snapshots__/problems-esm-react-javascript @@ -0,0 +1,19 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; + + +export default [ + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + pluginReactConfig, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "eslint-plugin-react", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-esm-react-typescript b/tests/__snapshots__/problems-esm-react-typescript new file mode 100644 index 00000000..295b1efd --- /dev/null +++ b/tests/__snapshots__/problems-esm-react-typescript @@ -0,0 +1,22 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; +import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; + + +export default [ + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + pluginReactConfig, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "typescript-eslint", + "eslint-plugin-react", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-esm-vue-javascript b/tests/__snapshots__/problems-esm-vue-javascript new file mode 100644 index 00000000..da40a1b1 --- /dev/null +++ b/tests/__snapshots__/problems-esm-vue-javascript @@ -0,0 +1,19 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import pluginVue from "eslint-plugin-vue"; + + +export default [ + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + ...pluginVue.configs["flat/essential"], +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "eslint-plugin-vue", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-esm-vue-typescript b/tests/__snapshots__/problems-esm-vue-typescript new file mode 100644 index 00000000..3b82eaab --- /dev/null +++ b/tests/__snapshots__/problems-esm-vue-typescript @@ -0,0 +1,22 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; +import pluginVue from "eslint-plugin-vue"; + + +export default [ + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + ...pluginVue.configs["flat/essential"], +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "typescript-eslint", + "eslint-plugin-vue", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-script-none-javascript b/tests/__snapshots__/problems-script-none-javascript new file mode 100644 index 00000000..96840e00 --- /dev/null +++ b/tests/__snapshots__/problems-script-none-javascript @@ -0,0 +1,17 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-script-none-typescript b/tests/__snapshots__/problems-script-none-typescript new file mode 100644 index 00000000..d17bee0f --- /dev/null +++ b/tests/__snapshots__/problems-script-none-typescript @@ -0,0 +1,20 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "typescript-eslint", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-script-react-javascript b/tests/__snapshots__/problems-script-react-javascript new file mode 100644 index 00000000..ed1e9bca --- /dev/null +++ b/tests/__snapshots__/problems-script-react-javascript @@ -0,0 +1,20 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + pluginReactConfig, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "eslint-plugin-react", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-script-react-typescript b/tests/__snapshots__/problems-script-react-typescript new file mode 100644 index 00000000..b35aafeb --- /dev/null +++ b/tests/__snapshots__/problems-script-react-typescript @@ -0,0 +1,23 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; +import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + pluginReactConfig, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "typescript-eslint", + "eslint-plugin-react", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-script-vue-javascript b/tests/__snapshots__/problems-script-vue-javascript new file mode 100644 index 00000000..4c0fb51a --- /dev/null +++ b/tests/__snapshots__/problems-script-vue-javascript @@ -0,0 +1,20 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import pluginVue from "eslint-plugin-vue"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + ...pluginVue.configs["flat/essential"], +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "eslint-plugin-vue", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/problems-script-vue-typescript b/tests/__snapshots__/problems-script-vue-typescript new file mode 100644 index 00000000..9244df2e --- /dev/null +++ b/tests/__snapshots__/problems-script-vue-typescript @@ -0,0 +1,23 @@ +{ + "configContent": "import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; +import pluginVue from "eslint-plugin-vue"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + ...pluginVue.configs["flat/essential"], +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "@eslint/js", + "typescript-eslint", + "eslint-plugin-vue", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/style-esm-none-xo-javascript b/tests/__snapshots__/style-esm-none-xo-javascript new file mode 100644 index 00000000..f0a0acf5 --- /dev/null +++ b/tests/__snapshots__/style-esm-none-xo-javascript @@ -0,0 +1,27 @@ +{ + "configContent": "import globals from "globals"; + +import path from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; +import pluginJs 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: pluginJs.configs.recommended}); + +export default [ + {languageOptions: { globals: globals.node }}, + ...compat.extends("xo"), +];", + "configFilename": "eslint.config.mjs", + "devDependencies": [ + "eslint", + "globals", + "eslint-config-xo", + "eslint@>=8.56.0", + "@eslint/eslintrc", + "@eslint/js", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/style-esm-none-xo-typescript b/tests/__snapshots__/style-esm-none-xo-typescript new file mode 100644 index 00000000..7040761a --- /dev/null +++ b/tests/__snapshots__/style-esm-none-xo-typescript @@ -0,0 +1,33 @@ +{ + "configContent": "import globals from "globals"; +import tseslint from "typescript-eslint"; + +import path from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; +import pluginJs 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: pluginJs.configs.recommended}); + +export default [ + {languageOptions: { globals: globals.browser }}, + ...compat.extends("xo-typescript"), + ...tseslint.configs.recommended, +];", + "configFilename": "eslint.config.mjs", + "devDependencies": [ + "eslint", + "globals", + "eslint-config-xo-typescript", + "@typescript-eslint/eslint-plugin@>=7.0.2", + "@typescript-eslint/parser@>=7.0.2", + "eslint@>=8.56.0", + "typescript@>=5.0.0", + "typescript-eslint", + "@eslint/eslintrc", + "@eslint/js", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-commonjs-none-javascript b/tests/__snapshots__/syntax-commonjs-none-javascript new file mode 100644 index 00000000..c92284f0 --- /dev/null +++ b/tests/__snapshots__/syntax-commonjs-none-javascript @@ -0,0 +1,14 @@ +{ + "configContent": "import globals from "globals"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-commonjs-none-typescript b/tests/__snapshots__/syntax-commonjs-none-typescript new file mode 100644 index 00000000..7f3d2be4 --- /dev/null +++ b/tests/__snapshots__/syntax-commonjs-none-typescript @@ -0,0 +1,17 @@ +{ + "configContent": "import globals from "globals"; +import tseslint from "typescript-eslint"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + ...tseslint.configs.recommended, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "typescript-eslint", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-commonjs-react-javascript b/tests/__snapshots__/syntax-commonjs-react-javascript new file mode 100644 index 00000000..c99a6ffa --- /dev/null +++ b/tests/__snapshots__/syntax-commonjs-react-javascript @@ -0,0 +1,17 @@ +{ + "configContent": "import globals from "globals"; +import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginReactConfig, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "eslint-plugin-react", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-commonjs-react-typescript b/tests/__snapshots__/syntax-commonjs-react-typescript new file mode 100644 index 00000000..80e932ed --- /dev/null +++ b/tests/__snapshots__/syntax-commonjs-react-typescript @@ -0,0 +1,20 @@ +{ + "configContent": "import globals from "globals"; +import tseslint from "typescript-eslint"; +import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + ...tseslint.configs.recommended, + pluginReactConfig, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "typescript-eslint", + "eslint-plugin-react", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-commonjs-vue-javascript b/tests/__snapshots__/syntax-commonjs-vue-javascript new file mode 100644 index 00000000..478b344c --- /dev/null +++ b/tests/__snapshots__/syntax-commonjs-vue-javascript @@ -0,0 +1,17 @@ +{ + "configContent": "import globals from "globals"; +import pluginVue from "eslint-plugin-vue"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + ...pluginVue.configs["flat/essential"], +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "eslint-plugin-vue", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-commonjs-vue-typescript b/tests/__snapshots__/syntax-commonjs-vue-typescript new file mode 100644 index 00000000..34a56352 --- /dev/null +++ b/tests/__snapshots__/syntax-commonjs-vue-typescript @@ -0,0 +1,20 @@ +{ + "configContent": "import globals from "globals"; +import tseslint from "typescript-eslint"; +import pluginVue from "eslint-plugin-vue"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + ...tseslint.configs.recommended, + ...pluginVue.configs["flat/essential"], +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "typescript-eslint", + "eslint-plugin-vue", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-esm-none-javascript b/tests/__snapshots__/syntax-esm-none-javascript new file mode 100644 index 00000000..2da23401 --- /dev/null +++ b/tests/__snapshots__/syntax-esm-none-javascript @@ -0,0 +1,13 @@ +{ + "configContent": "import globals from "globals"; + + +export default [ + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-esm-none-typescript b/tests/__snapshots__/syntax-esm-none-typescript new file mode 100644 index 00000000..e9f51f11 --- /dev/null +++ b/tests/__snapshots__/syntax-esm-none-typescript @@ -0,0 +1,16 @@ +{ + "configContent": "import globals from "globals"; +import tseslint from "typescript-eslint"; + + +export default [ + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + ...tseslint.configs.recommended, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "typescript-eslint", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-esm-react-javascript b/tests/__snapshots__/syntax-esm-react-javascript new file mode 100644 index 00000000..9a6bdf16 --- /dev/null +++ b/tests/__snapshots__/syntax-esm-react-javascript @@ -0,0 +1,16 @@ +{ + "configContent": "import globals from "globals"; +import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; + + +export default [ + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginReactConfig, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "eslint-plugin-react", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-esm-react-typescript b/tests/__snapshots__/syntax-esm-react-typescript new file mode 100644 index 00000000..42213310 --- /dev/null +++ b/tests/__snapshots__/syntax-esm-react-typescript @@ -0,0 +1,19 @@ +{ + "configContent": "import globals from "globals"; +import tseslint from "typescript-eslint"; +import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; + + +export default [ + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + ...tseslint.configs.recommended, + pluginReactConfig, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "typescript-eslint", + "eslint-plugin-react", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-esm-vue-javascript b/tests/__snapshots__/syntax-esm-vue-javascript new file mode 100644 index 00000000..60f0964e --- /dev/null +++ b/tests/__snapshots__/syntax-esm-vue-javascript @@ -0,0 +1,16 @@ +{ + "configContent": "import globals from "globals"; +import pluginVue from "eslint-plugin-vue"; + + +export default [ + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + ...pluginVue.configs["flat/essential"], +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "eslint-plugin-vue", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-esm-vue-typescript b/tests/__snapshots__/syntax-esm-vue-typescript new file mode 100644 index 00000000..e909753e --- /dev/null +++ b/tests/__snapshots__/syntax-esm-vue-typescript @@ -0,0 +1,19 @@ +{ + "configContent": "import globals from "globals"; +import tseslint from "typescript-eslint"; +import pluginVue from "eslint-plugin-vue"; + + +export default [ + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + ...tseslint.configs.recommended, + ...pluginVue.configs["flat/essential"], +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "typescript-eslint", + "eslint-plugin-vue", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-script-none-javascript b/tests/__snapshots__/syntax-script-none-javascript new file mode 100644 index 00000000..0e7bc5de --- /dev/null +++ b/tests/__snapshots__/syntax-script-none-javascript @@ -0,0 +1,14 @@ +{ + "configContent": "import globals from "globals"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-script-none-typescript b/tests/__snapshots__/syntax-script-none-typescript new file mode 100644 index 00000000..917262d4 --- /dev/null +++ b/tests/__snapshots__/syntax-script-none-typescript @@ -0,0 +1,17 @@ +{ + "configContent": "import globals from "globals"; +import tseslint from "typescript-eslint"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + ...tseslint.configs.recommended, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "typescript-eslint", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-script-react-javascript b/tests/__snapshots__/syntax-script-react-javascript new file mode 100644 index 00000000..733b3b8d --- /dev/null +++ b/tests/__snapshots__/syntax-script-react-javascript @@ -0,0 +1,17 @@ +{ + "configContent": "import globals from "globals"; +import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginReactConfig, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "eslint-plugin-react", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-script-react-typescript b/tests/__snapshots__/syntax-script-react-typescript new file mode 100644 index 00000000..475ab3f9 --- /dev/null +++ b/tests/__snapshots__/syntax-script-react-typescript @@ -0,0 +1,20 @@ +{ + "configContent": "import globals from "globals"; +import tseslint from "typescript-eslint"; +import pluginReactConfig from "eslint-plugin-react/configs/recommended.js"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + ...tseslint.configs.recommended, + pluginReactConfig, +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "typescript-eslint", + "eslint-plugin-react", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-script-vue-javascript b/tests/__snapshots__/syntax-script-vue-javascript new file mode 100644 index 00000000..03928128 --- /dev/null +++ b/tests/__snapshots__/syntax-script-vue-javascript @@ -0,0 +1,17 @@ +{ + "configContent": "import globals from "globals"; +import pluginVue from "eslint-plugin-vue"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + ...pluginVue.configs["flat/essential"], +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "eslint-plugin-vue", + ], +} \ No newline at end of file diff --git a/tests/__snapshots__/syntax-script-vue-typescript b/tests/__snapshots__/syntax-script-vue-typescript new file mode 100644 index 00000000..c15aa12a --- /dev/null +++ b/tests/__snapshots__/syntax-script-vue-typescript @@ -0,0 +1,20 @@ +{ + "configContent": "import globals from "globals"; +import tseslint from "typescript-eslint"; +import pluginVue from "eslint-plugin-vue"; + + +export default [ + {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + ...tseslint.configs.recommended, + ...pluginVue.configs["flat/essential"], +];", + "configFilename": "eslint.config.js", + "devDependencies": [ + "eslint", + "globals", + "typescript-eslint", + "eslint-plugin-vue", + ], +} \ No newline at end of file diff --git a/tests/config-snapshots.spec.js b/tests/config-snapshots.spec.js new file mode 100644 index 00000000..f5f74bd5 --- /dev/null +++ b/tests/config-snapshots.spec.js @@ -0,0 +1,108 @@ +/** + * @fileoverview snapshot tests for config-generator.js + * run `npm run test:snapshots` to assert snapshots + * run `npm run test:snapshots:update` to update snapshots - you need + * to check the changes manually (see the diff) and make sure it's correct. + * + * @author 唯然 + */ +import { ConfigGenerator } from "../lib/config-generator.js"; +import { expect, describe, test } from "vitest"; +import { fileURLToPath } from "node:url"; +import { join } from "path"; + +const __filename = fileURLToPath(import.meta.url); // eslint-disable-line no-underscore-dangle -- commonjs convention + +describe("generate config for esm projects", () => { + const esmProjectDir = join(__filename, "../fixtures/esm-project"); + const choices = { + purpose: ["syntax", "problems"], + moduleType: ["esm", "commonjs", "script"], + framework: ["react", "vue", "none"], + language: ["javascript", "typescript"], + env: ["browser", "node"] + }; + + const inputs = []; + + // generate all possible combinations + for (let i = 0; i < choices.purpose.length; i++) { + for (let j = 0; j < choices.moduleType.length; j++) { + for (let k = 0; k < choices.framework.length; k++) { + for (let m = 0; m < choices.language.length; m++) { + inputs.push({ + name: `${choices.purpose[i]}-${choices.moduleType[j]}-${choices.framework[k]}-${choices.language[m]}`, + answers: { + purpose: choices.purpose[i], + moduleType: choices.moduleType[j], + framework: choices.framework[k], + language: choices.language[m], + env: ["browser", "node"] + } + }); + } + } + } + } + + inputs.forEach(item => { + test(`${item.name}`, () => { + const generator = new ConfigGenerator({ cwd: esmProjectDir, answers: item.answers }); + + generator.calc(); + + expect(generator.result.configFilename).toBe("eslint.config.js"); + expect(generator.packageJsonPath).toBe(join(esmProjectDir, "./package.json")); + expect(generator.result).toMatchFileSnapshot(`./__snapshots__/${item.name}`); + }); + }); + + test("sub dir", () => { + const sub = join(__filename, "../fixtures/esm-project/sub"); + const generator = new ConfigGenerator({ cwd: sub, answers: { purpose: "problems", moduleType: "esm", framework: "none", language: "javascript", env: ["node"] } }); + + generator.calc(); + + expect(generator.result.configFilename).toBe("eslint.config.js"); + expect(generator.packageJsonPath).toBe(join(esmProjectDir, "./package.json")); + }); + +}); + +describe("generate config for cjs projects", () => { + const cjsProjectDir = join(__filename, "../fixtures/cjs-project"); + const inputs = [{ + name: "style-esm-none-xo-javascript", + answers: { + purpose: "style", + moduleType: "esm", + framework: "none", + language: "javascript", + env: ["node"], + styleguide: { packageName: "eslint-config-xo", type: "eslintrc" } + } + }, + { + name: "style-esm-none-xo-typescript", + answers: { + purpose: "style", + moduleType: "esm", + framework: "none", + language: "typescript", + env: ["browser"], + styleguide: { packageName: "eslint-config-xo-typescript", type: "eslintrc" } + } + }]; + + inputs.forEach(item => { + test(`${item.name}`, () => { + const generator = new ConfigGenerator({ cwd: cjsProjectDir, answers: item.answers }); + + generator.calc(); + + expect(generator.result.configFilename).toBe("eslint.config.mjs"); + expect(generator.packageJsonPath).toBe(join(cjsProjectDir, "./package.json")); + expect(generator.result).toMatchFileSnapshot(`./__snapshots__/${item.name}`); + }); + }); +}); diff --git a/tests/fixtures/cjs-project/package.json b/tests/fixtures/cjs-project/package.json new file mode 100644 index 00000000..a483e4c8 --- /dev/null +++ b/tests/fixtures/cjs-project/package.json @@ -0,0 +1,6 @@ +{ + "name": "cjs-project", + "private": true, + "type": "commonjs", + "version": "1.0.0" +} diff --git a/tests/fixtures/config-initializer/lib/doubleQuotes.js b/tests/fixtures/config-initializer/lib/doubleQuotes.js deleted file mode 100644 index 3c1ce5e4..00000000 --- a/tests/fixtures/config-initializer/lib/doubleQuotes.js +++ /dev/null @@ -1 +0,0 @@ -var foo = 'doubleQuotes'; diff --git a/tests/fixtures/config-initializer/lib/no-semi.js b/tests/fixtures/config-initializer/lib/no-semi.js deleted file mode 100644 index 050ea54a..00000000 --- a/tests/fixtures/config-initializer/lib/no-semi.js +++ /dev/null @@ -1 +0,0 @@ -var name = 'ESLint'; diff --git a/tests/fixtures/config-initializer/new-es-features/new-es-features.js b/tests/fixtures/config-initializer/new-es-features/new-es-features.js deleted file mode 100644 index de6686df..00000000 --- a/tests/fixtures/config-initializer/new-es-features/new-es-features.js +++ /dev/null @@ -1,3 +0,0 @@ -async function fn() { - await Promise.resolve(); -} diff --git a/tests/fixtures/config-initializer/parse-error/parse-error.js b/tests/fixtures/config-initializer/parse-error/parse-error.js deleted file mode 100644 index 90bd920c..00000000 --- a/tests/fixtures/config-initializer/parse-error/parse-error.js +++ /dev/null @@ -1 +0,0 @@ -+; diff --git a/tests/fixtures/config-initializer/singleQuotes.js b/tests/fixtures/config-initializer/singleQuotes.js deleted file mode 100644 index db1e01fe..00000000 --- a/tests/fixtures/config-initializer/singleQuotes.js +++ /dev/null @@ -1 +0,0 @@ -var foo = 'singleQuotes'; diff --git a/tests/fixtures/config-initializer/tests/console-log.js b/tests/fixtures/config-initializer/tests/console-log.js deleted file mode 100644 index 53b32b51..00000000 --- a/tests/fixtures/config-initializer/tests/console-log.js +++ /dev/null @@ -1 +0,0 @@ -console.log('I\'m a log'); diff --git a/tests/fixtures/config-initializer/tests/doubleQuotes.js b/tests/fixtures/config-initializer/tests/doubleQuotes.js deleted file mode 100644 index 3c1ce5e4..00000000 --- a/tests/fixtures/config-initializer/tests/doubleQuotes.js +++ /dev/null @@ -1 +0,0 @@ -var foo = 'doubleQuotes'; diff --git a/tests/fixtures/config-rule/schemas.js b/tests/fixtures/config-rule/schemas.js deleted file mode 100644 index 9984fe72..00000000 --- a/tests/fixtures/config-rule/schemas.js +++ /dev/null @@ -1,118 +0,0 @@ -export const schemas = { - enum: [{ enum: ['always', 'never'] }], - - objectWithEnum: [{ - type: 'object', - properties: { - enumProperty: { - enum: ['always', 'never'] - } - }, - additionalProperties: false - }], - - objectWithMultipleEnums: [{ - type: 'object', - properties: { - firstEnum: { - enum: ['always', 'never'] - }, - anotherEnum: { - enum: ['var', 'let', 'const'] - } - }, - additionalProperties: false - }], - - objectWithBool: [{ - type: 'object', - properties: { - boolProperty: { - type: 'boolean' - } - }, - additionalProperties: false - }], - - objectWithMultipleBools: [{ - type: 'object', - properties: { - firstBool: { - type: 'boolean' - }, - anotherBool: { - type: 'boolean' - } - }, - additionalProperties: false - }], - - mixedEnumObject: [{ - enum: ['always', 'never'] - }, - { - type: 'object', - properties: { - singleValue: { - type: 'boolean' - } - }, - additionalProperties: false - }], - - mixedEnumObjectWithNothing: [{ - enum: ['always', 'never'] - }, - { - type: 'object', - properties: {}, - additionalProperties: false - }], - - mixedObjectWithNothingEnum: [{ - type: 'object', - properties: {}, - additionalProperties: false - }, - { - enum: ['always', 'never'] - }], - - mixedStringEnum: [{ - type: 'string' - }, - { - enum: ['always', 'never'] - }], - - oneOf: [{ - oneOf: [ - { - enum: ['before', 'after', 'both', 'neither'] - }, - { - type: 'object', - properties: { - before: { type: 'boolean' }, - after: { type: 'boolean' } - }, - additionalProperties: false - } - ] - }], - - nestedObjects: [{ - type: 'object', - properties: { - prefer: { - type: 'object', - properties: { - nestedProperty: { - type: 'boolean' - } - } - } - } - }] - -}; diff --git a/tests/fixtures/eslint/lib/api.js b/tests/fixtures/eslint/lib/api.js deleted file mode 100644 index edc25f33..00000000 --- a/tests/fixtures/eslint/lib/api.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @fileoverview the file is to mock local-installed eslint for testing - */ -"use strict"; -const _module = require("module"); -const path = require("path"); - -const { createRequire } = _module; -const root = path.resolve(__dirname, "../../../../../../"); - -const realEslintPath = createRequire(root).resolve("eslint"); -module.exports = require(realEslintPath); diff --git a/tests/fixtures/eslint/package.json b/tests/fixtures/eslint/package.json deleted file mode 100644 index ffa290cc..00000000 --- a/tests/fixtures/eslint/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "eslint", - "version": "3.18.0", - "main": "./lib/api.js" -} \ No newline at end of file diff --git a/tests/fixtures/esm-project/package.json b/tests/fixtures/esm-project/package.json new file mode 100644 index 00000000..af755b3b --- /dev/null +++ b/tests/fixtures/esm-project/package.json @@ -0,0 +1,6 @@ +{ + "name": "esm-project", + "private": true, + "type": "module", + "version": "1.0.0" +} diff --git a/tests/fixtures/esm-project/sub/foo.js b/tests/fixtures/esm-project/sub/foo.js new file mode 100644 index 00000000..e69de29b diff --git a/tests/init/config-file.js b/tests/init/config-file.js deleted file mode 100644 index 64947291..00000000 --- a/tests/init/config-file.js +++ /dev/null @@ -1,145 +0,0 @@ -/** - * @fileoverview Tests for ConfigFile - * @author Nicholas C. Zakas - */ - - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -import sinon from "sinon"; -import path from "path"; -import yaml from "js-yaml"; -import * as espree from "espree"; -import * as ConfigFile from "../../lib/init/config-file.js"; -import nodeAssert from "assert"; -import esmock from "esmock"; - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Helper function get easily get a path in the fixtures directory. - * @param {string} filepath The path to find in the fixtures directory. - * @returns {string} Full path in the fixtures directory. - * @private - */ -function getFixturePath(filepath) { - const dirname = path.dirname(new URL(import.meta.url).pathname); - - return path.resolve(dirname, "../../fixtures/config-file", filepath); -} - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("ConfigFile", () => { - describe("write()", () => { - let config; - - beforeEach(() => { - config = { - env: { - browser: true, - node: true - }, - rules: { - quotes: 2, - semi: 1 - } - }; - }); - - afterEach(() => { - sinon.verifyAndRestore(); - }); - - [ - ["JavaScript", "foo.js", espree.parse], - ["JSON", "bar.json", JSON.parse], - ["YAML", "foo.yaml", yaml.load], - ["YML", "foo.yml", yaml.load] - ].forEach(([fileType, filename, validate]) => { - - it(`should write a file through fs when a ${fileType} path is passed`, async () => { - const fakeFS = { - writeFileSync() {} - }; - - sinon.mock(fakeFS).expects("writeFileSync").withExactArgs( - filename, - sinon.match(value => !!validate(value)), - "utf8" - ); - - const StubbedConfigFile = await esmock("../../lib/init/config-file.js", { - fs: fakeFS - }); - - await StubbedConfigFile.write(config, filename); - }); - - it("should include a newline character at EOF", async () => { - const fakeFS = { - writeFileSync() {} - }; - - sinon.mock(fakeFS).expects("writeFileSync").withExactArgs( - filename, - sinon.match(value => value.endsWith("\n")), - "utf8" - ); - - const StubbedConfigFile = await esmock("../../lib/init/config-file.js", { - fs: fakeFS - }); - - await StubbedConfigFile.write(config, filename); - }); - }); - - it("should run 'eslint --fix' to make sure js config files match linting rules", async () => { - const fakeFS = { - writeFileSync() {} - }; - - const singleQuoteConfig = { - rules: { - quotes: [2, "single"] - } - }; - - sinon.mock(fakeFS).expects("writeFileSync").withExactArgs( - "test-config.js", - sinon.match.string, - "utf8" - ); - - const syncStub = sinon.fake(); - const StubbedConfigFile = await esmock("../../lib/init/config-file.js", { - fs: fakeFS, - "cross-spawn": { - default: { - sync: syncStub - } - } - }); - - StubbedConfigFile.write(singleQuoteConfig, "test-config.js"); - nodeAssert(syncStub.called); - nodeAssert(syncStub.calledWith( - sinon.match("eslint"), - sinon.match.array.contains(["--fix"]) - )); - }); - - it("should throw error if file extension is not valid", () => { - nodeAssert.rejects(async () => { - await ConfigFile.write({}, getFixturePath("yaml/.eslintrc.class")); - }, /write to unknown file type/u); - }); - }); -}); diff --git a/tests/init/config-initializer.js b/tests/init/config-initializer.js deleted file mode 100644 index 18261b2d..00000000 --- a/tests/init/config-initializer.js +++ /dev/null @@ -1,544 +0,0 @@ -/** - * @fileoverview Tests for configInitializer. - * @author Ilya Volodin - */ - - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -import chai from "chai"; -import fs from "fs"; -import path from "path"; -import sinon from "sinon"; -import sh from "shelljs"; -import esmock from "esmock"; -import { fileURLToPath } from "url"; -import * as npmUtils from "../../lib/init/npm-utils.js"; -import { defineInMemoryFs } from "../_utils/in-memory-fs.js"; - -const originalDir = process.cwd(); -const { assert } = chai; - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -let fixtureDir; -let localInstalledEslintDir; - -/** - * change local installed eslint version in fixtures - * @param {string|null} version installed eslint version, null => not installed - * @returns {void} - */ -function setLocalInstalledEslint(version) { - const eslintPkgPath = path.join(localInstalledEslintDir, "./package.json"); - let pkg = JSON.parse(fs.readFileSync(eslintPkgPath, "utf8")); - - if (version) { - pkg.version = version; - } else { - pkg = {}; - } - - fs.writeFileSync(eslintPkgPath, JSON.stringify(pkg, null, 2), "utf8"); -} - - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -let answers = {}; -let pkgJSONContents = {}; -let pkgJSONPath = ""; - -describe("configInitializer", () => { - - let npmCheckStub; - let npmInstallStub; - let npmFetchPeerDependenciesStub; - let init; - let log; - - - // copy into clean area so as not to get "infected" by this project's .eslintrc files - before(() => { - const __filename = fileURLToPath(import.meta.url); // eslint-disable-line no-underscore-dangle -- Conventional - - fixtureDir = path.join(__filename, "../../../tmp/eslint/fixtures/config-initializer"); - localInstalledEslintDir = path.join(fixtureDir, "./node_modules/eslint"); - sh.mkdir("-p", localInstalledEslintDir); - sh.cp("-r", "./tests/fixtures/config-initializer/.", fixtureDir); - sh.cp("-r", "./tests/fixtures/eslint/.", localInstalledEslintDir); - fixtureDir = fs.realpathSync(fixtureDir); - }); - - beforeEach(async () => { - log = { - info: sinon.spy(), - error: sinon.spy() - }; - - npmInstallStub = sinon.stub(); - npmCheckStub = sinon.fake(packages => packages.reduce((status, pkg) => { - status[pkg] = false; - return status; - }, {})); - npmFetchPeerDependenciesStub = sinon.fake(() => ({ - eslint: "^3.19.0", - "eslint-plugin-jsx-a11y": "^5.0.1", - "eslint-plugin-import": "^2.2.0", - "eslint-plugin-react": "^7.0.1" - })); - - const requireStubs = { - "../../lib/shared/logging.js": log, - "../../lib/init/npm-utils.js": { - ...npmUtils, - installSyncSaveDev: npmInstallStub, - checkDevDeps: npmCheckStub, - fetchPeerDependencies: npmFetchPeerDependenciesStub - } - }; - - init = await esmock("../../lib/init/config-initializer.js", requireStubs, {}); - }); - - afterEach(() => { - log.info.resetHistory(); - log.error.resetHistory(); - npmInstallStub.resetHistory(); - npmCheckStub.resetHistory(); - npmFetchPeerDependenciesStub.resetHistory(); - }); - - after(() => { - sh.rm("-r", fixtureDir); - }); - - describe("processAnswers()", () => { - - describe("prompt", () => { - - beforeEach(() => { - answers = { - purpose: "style", - source: "prompt", - extendDefault: true, - indent: 2, - quotes: "single", - linebreak: "unix", - semi: true, - moduleType: "esm", - es6Globals: true, - env: ["browser"], - format: "JSON" - }; - }); - it("should throw error with message when no package.json", async () => { - const requireStubs = { - "../../lib/shared/logging.js": log, - "../../lib/init/npm-utils.js": { - ...await esmock("../../lib/init/npm-utils.js", { fs: defineInMemoryFs({}) }) - } - }; - - - init = await esmock("../../lib/init/config-initializer.js", requireStubs, { }); - - assert.throws(() => { - init.initializeConfig(); - }, "A package.json file is necessary to initialize ESLint. Run `npm init` to create a package.json file and try again."); - }); - - it("should create default config", () => { - const config = init.processAnswers(answers); - - assert.deepStrictEqual(config.rules.indent, ["error", 2]); - assert.deepStrictEqual(config.rules.quotes, ["error", "single"]); - assert.deepStrictEqual(config.rules["linebreak-style"], ["error", "unix"]); - assert.deepStrictEqual(config.rules.semi, ["error", "always"]); - assert.strictEqual(config.env.es2021, true); - assert.strictEqual(config.parserOptions.ecmaVersion, "latest"); - assert.strictEqual(config.parserOptions.sourceType, "module"); - assert.strictEqual(config.env.browser, true); - assert.strictEqual(config.extends, "eslint:recommended"); - }); - - it("should disable semi", () => { - answers.semi = false; - const config = init.processAnswers(answers); - - assert.deepStrictEqual(config.rules.semi, ["error", "never"]); - }); - - it("should enable react plugin", () => { - answers.framework = "react"; - const config = init.processAnswers(answers); - - assert.strictEqual(config.parserOptions.ecmaVersion, "latest"); - assert.deepStrictEqual(config.plugins, ["react"]); - assert.include(config.extends, "plugin:react/recommended"); - }); - - it("should enable vue plugin", () => { - answers.framework = "vue"; - const config = init.processAnswers(answers); - - assert.strictEqual(config.parserOptions.ecmaVersion, "latest"); - assert.deepStrictEqual(config.plugins, ["vue"]); - assert.deepStrictEqual(config.extends, ["eslint:recommended", "plugin:vue/vue3-essential"]); - }); - - it("should enable typescript parser and plugin", () => { - answers.typescript = true; - const config = init.processAnswers(answers); - - assert.strictEqual(config.parser, "@typescript-eslint/parser"); - assert.deepStrictEqual(config.plugins, ["@typescript-eslint"]); - assert.deepStrictEqual(config.extends, ["eslint:recommended", "plugin:@typescript-eslint/recommended"]); - }); - - it("should enable typescript parser and plugin with vue", () => { - answers.framework = "vue"; - answers.typescript = true; - const config = init.processAnswers(answers); - - assert.include(config.parserOptions, { parser: "@typescript-eslint/parser" }); - assert.deepStrictEqual(config.extends, ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:vue/vue3-essential"]); - assert.deepStrictEqual(config.plugins, ["@typescript-eslint", "vue"]); - }); - - it("should extend eslint:recommended", () => { - const config = init.processAnswers(answers); - - assert.strictEqual(config.extends, "eslint:recommended"); - }); - - it("should not use commonjs by default", () => { - const config = init.processAnswers(answers); - - assert.isUndefined(config.env.commonjs); - }); - - it("should use commonjs when set", () => { - answers.moduleType = "commonjs"; - const config = init.processAnswers(answers); - - assert.isTrue(config.env.commonjs); - }); - - it("should set commonjs config for `.eslintrc.cjs` in esm projects", () => { - answers.moduleType = "esm"; - answers.format = "JavaScript"; - const config = init.processAnswers(answers); - - assert.isArray(config.overrides, "should have overrides config"); - assert.strictEqual(config.overrides.length, 1); - assert.deepStrictEqual(config.overrides[0].parserOptions, { sourceType: "script" }); - assert.deepStrictEqual(config.overrides[0].env, { node: true }); - }); - - }); - - describe("guide", () => { - it("should support the google style guide", () => { - const config = { extends: "google" }; - const modules = init.getModulesList(config); - - assert.deepStrictEqual(config, { extends: "google", installedESLint: true }); - assert.include(modules, "eslint-config-google@latest"); - }); - - it("should support the airbnb style guide", () => { - const config = { extends: "airbnb" }; - const modules = init.getModulesList(config); - - assert.deepStrictEqual(config, { extends: "airbnb", installedESLint: true }); - assert.include(modules, "eslint-config-airbnb@latest"); - }); - - it("should support the airbnb base style guide", () => { - const config = { extends: "airbnb-base" }; - const modules = init.getModulesList(config); - - assert.deepStrictEqual(config, { extends: "airbnb-base", installedESLint: true }); - assert.include(modules, "eslint-config-airbnb-base@latest"); - }); - - it("should support the standard style guide", () => { - const config = { extends: "standard" }; - const modules = init.getModulesList(config); - - assert.deepStrictEqual(config, { extends: "standard", installedESLint: true }); - assert.include(modules, "eslint-config-standard@latest"); - }); - - it("should support the xo style guide", () => { - const config = { extends: "xo" }; - const modules = init.getModulesList(config); - - assert.deepStrictEqual(config, { extends: "xo", installedESLint: true }); - assert.include(modules, "eslint-config-xo@latest"); - }); - - it("should support the xo-typescript style guide", () => { - const config = { extends: "xo", overrides: [{ files: ["*.ts"], extends: ["xo-typescript"] }] }; - const modules = init.getModulesList(config); - - assert.deepStrictEqual(config.extends, "xo"); - assert.deepStrictEqual(config.overrides[0].extends[0], "xo-typescript"); - assert.strictEqual(config.installedESLint, true); - assert.include(modules, "eslint-config-xo@latest"); - assert.include(modules, "eslint-config-xo-typescript@latest"); - }); - - it("should install required sharable config", () => { - const config = { extends: "google" }; - - init.installModules(init.getModulesList(config)); - assert(npmInstallStub.calledOnce); - assert(npmInstallStub.firstCall.args[0].some(name => name.startsWith("eslint-config-google@"))); - }); - - it("should install ESLint if not installed locally", () => { - const config = { extends: "google" }; - - init.installModules(init.getModulesList(config)); - assert(npmInstallStub.calledOnce); - assert(npmInstallStub.firstCall.args[0].some(name => name.startsWith("eslint@"))); - }); - - it("should install peerDependencies of the sharable config", () => { - const config = { extends: "airbnb" }; - - init.installModules(init.getModulesList(config)); - - assert(npmFetchPeerDependenciesStub.calledOnce); - assert(npmFetchPeerDependenciesStub.firstCall.args[0] === "eslint-config-airbnb@latest"); - assert(npmInstallStub.calledOnce); - assert.deepStrictEqual( - npmInstallStub.firstCall.args[0], - [ - "eslint-config-airbnb@latest", - "eslint@^3.19.0", - "eslint-plugin-jsx-a11y@^5.0.1", - "eslint-plugin-import@^2.2.0", - "eslint-plugin-react@^7.0.1" - ] - ); - }); - - describe('hasESLintVersionConflict (Note: peerDependencies always `eslint: "^3.19.0"` by stubs)', () => { - - before(() => { - - // FIX: not sure why it was changed somewhere??? - process.chdir(fixtureDir); - }); - - describe("if local ESLint is not found,", () => { - before(() => { - setLocalInstalledEslint(null); - }); - - it("should return false.", () => { - const result = init.hasESLintVersionConflict({ styleguide: "airbnb" }); - - assert.strictEqual(result, false); - }); - }); - - describe("if local ESLint is 3.19.0,", () => { - before(() => { - setLocalInstalledEslint("3.19.0"); - }); - - it("should return false.", () => { - const result = init.hasESLintVersionConflict({ styleguide: "airbnb" }); - - assert.strictEqual(result, false); - }); - }); - - describe("if local ESLint is 4.0.0,", () => { - before(() => { - setLocalInstalledEslint("4.0.0"); - }); - - it("should return true.", () => { - const result = init.hasESLintVersionConflict({ styleguide: "airbnb" }); - - assert.strictEqual(result, true); - }); - }); - - describe("if local ESLint is 3.18.0,", () => { - before(() => { - setLocalInstalledEslint("3.18.0"); - }); - - it("should return true.", () => { - const result = init.hasESLintVersionConflict({ styleguide: "airbnb" }); - - assert.strictEqual(result, true); - }); - }); - }); - - it("should support the standard style guide with Vue.js", () => { - const config = { - plugins: ["vue"], - extends: ["plugin:vue/vue3-essential", "standard"] - }; - const modules = init.getModulesList(config); - - assert.include(modules, "eslint-plugin-vue@latest"); - assert.include(modules, "eslint-config-standard@latest"); - }); - - it("should support custom parser", () => { - const config = { - parser: "@typescript-eslint/parser" - }; - const modules = init.getModulesList(config); - - assert.include(modules, "@typescript-eslint/parser@latest"); - }); - - it("should support custom parser with Vue.js", () => { - const config = { - - // We should declare the parser at `parserOptions` when using with `eslint-plugin-vue`. - parserOptions: { - parser: "@typescript-eslint/parser" - } - }; - const modules = init.getModulesList(config); - - assert.include(modules, "@typescript-eslint/parser@latest"); - }); - }); - - }); - - describe("writeFile()", () => { - - beforeEach(() => { - answers = { - purpose: "style", - source: "prompt", - extendDefault: true, - indent: 2, - quotes: "single", - linebreak: "unix", - semi: true, - moduleType: "esm", - es6Globals: true, - env: ["browser"], - format: "JSON" - }; - - pkgJSONContents = { - name: "config-initializer", - version: "1.0.0" - }; - - process.chdir(fixtureDir); - - pkgJSONPath = path.resolve(fixtureDir, "package.json"); - }); - - afterEach(() => { - process.chdir(originalDir); - }); - - it("should create .eslintrc.json", () => { - const config = init.processAnswers(answers); - const filePath = path.resolve(fixtureDir, ".eslintrc.json"); - - fs.writeFileSync(pkgJSONPath, JSON.stringify(pkgJSONContents)); - - init.writeFile(config, answers.format); - - assert.isTrue(fs.existsSync(filePath)); - - fs.unlinkSync(filePath); - fs.unlinkSync(pkgJSONPath); - }); - - it("should create .eslintrc.js", async () => { - answers.format = "JavaScript"; - - const config = init.processAnswers(answers); - const filePath = path.resolve(fixtureDir, ".eslintrc.js"); - - fs.writeFileSync(pkgJSONPath, JSON.stringify(pkgJSONContents)); - - await init.writeFile(config, answers.format); - - assert.isTrue(fs.existsSync(filePath)); - - fs.unlinkSync(filePath); - fs.unlinkSync(pkgJSONPath); - }); - - it("should create .eslintrc.yml", async () => { - answers.format = "YAML"; - - const config = init.processAnswers(answers); - const filePath = path.resolve(fixtureDir, ".eslintrc.yml"); - - fs.writeFileSync(pkgJSONPath, JSON.stringify(pkgJSONContents)); - - await init.writeFile(config, answers.format); - - assert.isTrue(fs.existsSync(filePath)); - - fs.unlinkSync(filePath); - fs.unlinkSync(pkgJSONPath); - }); - - // For https://github.com/eslint/eslint/issues/14137 - it("should create .eslintrc.cjs", async () => { - answers.format = "JavaScript"; - - // create package.json with "type": "module" - pkgJSONContents.type = "module"; - - fs.writeFileSync(pkgJSONPath, JSON.stringify(pkgJSONContents)); - - const config = init.processAnswers(answers); - const filePath = path.resolve(fixtureDir, ".eslintrc.cjs"); - - await init.writeFile(config, answers.format); - - assert.isTrue(fs.existsSync(filePath)); - - fs.unlinkSync(filePath); - fs.unlinkSync(pkgJSONPath); - }); - - it("should create .eslintrc.json even with type: 'module'", async () => { - answers.format = "JSON"; - - // create package.json with "type": "module" - pkgJSONContents.type = "module"; - - fs.writeFileSync(pkgJSONPath, JSON.stringify(pkgJSONContents)); - - const config = init.processAnswers(answers); - const filePath = path.resolve(fixtureDir, ".eslintrc.json"); - - await init.writeFile(config, answers.format); - - assert.isTrue(fs.existsSync(filePath)); - - fs.unlinkSync(filePath); - fs.unlinkSync(pkgJSONPath); - }); - }); -}); diff --git a/tests/init/npm-utils.js b/tests/utils/npm-utils.spec.js similarity index 91% rename from tests/init/npm-utils.js rename to tests/utils/npm-utils.spec.js index d7b72ca1..1cb89837 100644 --- a/tests/init/npm-utils.js +++ b/tests/utils/npm-utils.spec.js @@ -1,14 +1,12 @@ /** - * @fileoverview Tests for rule fixer. - * @author Ian VanSchooten + * @fileoverview Tests for npm-utils. + * @author Ian VanSchooten, 唯然 */ - //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ -import chai from "chai"; import spawn from "cross-spawn"; import sinon from "sinon"; import { @@ -16,11 +14,10 @@ import { fetchPeerDependencies, checkDeps, checkDevDeps -} from "../../lib/init/npm-utils.js"; +} from "../../lib/utils/npm-utils.js"; import { defineInMemoryFs } from "../_utils/in-memory-fs.js"; import esmock from "esmock"; - -const { assert } = chai; +import { assert, describe, afterEach, it } from "vitest"; //------------------------------------------------------------------------------ // Helpers @@ -34,7 +31,7 @@ const { assert } = chai; async function requireNpmUtilsWithInMemoryFileSystem(files) { const fs = defineInMemoryFs({ files }); - return await esmock("../../lib/init/npm-utils.js", { fs }); + return await esmock("../../lib/utils/npm-utils.js", { fs }); } //------------------------------------------------------------------------------ @@ -47,18 +44,14 @@ describe("npmUtils", () => { }); describe("checkDevDeps()", () => { - let installStatus; - - before(() => { - installStatus = checkDevDeps(["debug", "mocha", "notarealpackage", "jshint"]); - }); + let installStatus = checkDevDeps(["debug", "eslint", "notarealpackage", "jshint"]); it("should not find a direct dependency of the project", () => { assert.isFalse(installStatus.debug); }); it("should find a dev dependency of the project", () => { - assert.isTrue(installStatus.mocha); + assert.isTrue(installStatus.eslint); }); it("should not find non-dependencies", () => { @@ -95,14 +88,10 @@ describe("npmUtils", () => { }); describe("checkDeps()", () => { - let installStatus; - - before(() => { - installStatus = checkDeps(["debug", "mocha", "notarealpackage", "jshint"]); - }); + let installStatus = checkDeps(["enquirer", "mocha", "notarealpackage", "jshint"]); it("should find a direct dependency of the project", () => { - assert.isTrue(installStatus.debug); + assert.isTrue(installStatus.enquirer); }); it("should not find a dev dependency of the project", () => { @@ -200,8 +189,8 @@ describe("npmUtils", () => { const logErrorStub = sinon.spy(); const npmUtilsStub = sinon.stub(spawn, "sync").returns({ error: { code: "ENOENT" } }); - const { installSyncSaveDev: stubinstallSyncSaveDev } = await esmock("../../lib/init/npm-utils.js", { - "../../lib/shared/logging.js": { + const { installSyncSaveDev: stubinstallSyncSaveDev } = await esmock("../../lib/utils/npm-utils.js", { + "../../lib/utils/logging.js": { error: logErrorStub } }); diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 00000000..149d1eb6 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + coverage: { + include: ["lib/**/*"], + reporter: ["text", "html"] + } + } +});