From 0d2b9a6dfa544f7ab084425eafc90a90aa14bcae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=AF=E7=84=B6?= Date: Sun, 9 Jan 2022 18:34:51 +0800 Subject: [PATCH] feat: move `eslint --init` to @eslint/create-config (#15150) * feat: move eslint --init to @eslint/create-config fixes #14768, fixes #15159 Update: rm eslint auto config refs: https://github.com/aladdin-add/rfcs/blob/bdc12aa062750d837e5a3bbbf2f6e5e3a98da388/designs/2021-init-command-eslint-cli/README.md#1-remove-eslint-auto-config Update: mv eslint --init to another package moved `lib/init/config-rule` to tools, as it is also used by some others refs: https://github.com/aladdin-add/rfcs/blob/bdc12aa062750d837e5a3bbbf2f6e5e3a98da388/designs/2021-init-command-eslint-cli/README.md#2-move-eslint---init-related-files-to-a-separate-repo chore: fix imports to make test passing todo: use the new eslint api fix: use the new eslint api (async) fix: remove espree from deps chore: fix a failing test fix: a failing test chore: cleanup TODOs fix: allow to use local-installed eslint wip: fix one-var chore: lib => esm chore: tests => esm todo: proxyquire => td chore: update deps to latest fix: should write a file through fs when a ${fileType} path is passed replaced proxyquire & sinon => td fix: should include a newline character at EOF chore: add testdouble --wip-- [skip ci] chore: remove package @eslint/create-eslint feat: update npm --init to run `npm init @eslint/config` docs: update getting-started Update README.md Update getting-started.md chore: rm init fixtures fix: `npm init @eslint/config` output chore: rm unused files chore: rm unused deps Update bin/eslint.js Co-authored-by: Brandon Mills chore: fix typo * docs: rm mentioned `--init` * chore: fixtures/autoconfig/* * chore: add config-rule unit tests * docs: update some `eslint --init` usage --- README.md | 4 +- bin/eslint.js | 8 +- docs/developer-guide/architecture.md | 1 - docs/user-guide/command-line-interface.md | 2 +- .../configuring/configuration-files.md | 2 +- docs/user-guide/getting-started.md | 10 +- docs/user-guide/migrating-from-jscs.md | 2 +- lib/init/autoconfig.js | 351 --------- lib/init/config-file.js | 144 ---- lib/init/config-initializer.js | 709 ------------------ lib/init/npm-utils.js | 179 ----- lib/init/source-code-utils.js | 110 --- messages/no-config-found.js | 2 +- package.json | 5 +- .../autoconfig/source-with-comments.js | 6 - tests/fixtures/autoconfig/source.js | 3 - .../config-initializer/lib/doubleQuotes.js | 1 - .../config-initializer/lib/no-semi.js | 1 - .../new-es-features/new-es-features.js | 3 - .../parse-error/parse-error.js | 1 - .../config-initializer/singleQuotes.js | 1 - .../config-initializer/tests/console-log.js | 1 - .../config-initializer/tests/doubleQuotes.js | 1 - tests/lib/init/autoconfig.js | 385 ---------- tests/lib/init/config-file.js | 159 ---- tests/lib/init/config-initializer.js | 577 -------------- tests/lib/init/npm-utils.js | 228 ------ tests/lib/init/source-code-utils.js | 250 ------ tests/{lib/init => tools}/config-rule.js | 6 +- tests/tools/eslint-fuzzer.js | 2 +- {lib/init => tools}/config-rule.js | 2 +- tools/eslint-fuzzer.js | 2 +- 32 files changed, 26 insertions(+), 3132 deletions(-) delete mode 100644 lib/init/autoconfig.js delete mode 100644 lib/init/config-file.js delete mode 100644 lib/init/config-initializer.js delete mode 100644 lib/init/npm-utils.js delete mode 100644 lib/init/source-code-utils.js delete mode 100644 tests/fixtures/autoconfig/source-with-comments.js delete mode 100644 tests/fixtures/autoconfig/source.js delete mode 100644 tests/fixtures/config-initializer/lib/doubleQuotes.js delete mode 100644 tests/fixtures/config-initializer/lib/no-semi.js delete mode 100644 tests/fixtures/config-initializer/new-es-features/new-es-features.js delete mode 100644 tests/fixtures/config-initializer/parse-error/parse-error.js delete mode 100644 tests/fixtures/config-initializer/singleQuotes.js delete mode 100644 tests/fixtures/config-initializer/tests/console-log.js delete mode 100644 tests/fixtures/config-initializer/tests/doubleQuotes.js delete mode 100644 tests/lib/init/autoconfig.js delete mode 100644 tests/lib/init/config-file.js delete mode 100644 tests/lib/init/config-initializer.js delete mode 100644 tests/lib/init/npm-utils.js delete mode 100644 tests/lib/init/source-code-utils.js rename tests/{lib/init => tools}/config-rule.js (98%) rename {lib/init => tools}/config-rule.js (99%) diff --git a/README.md b/README.md index 1f98fa25198..6c6a855ec89 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ $ npm install eslint --save-dev You should then set up a configuration file: ```sh -$ ./node_modules/.bin/eslint --init +$ npm init @eslint/config ``` After that, you can run ESLint on any file or directory like this: @@ -65,7 +65,7 @@ $ ./node_modules/.bin/eslint yourfile.js ## Configuration -After running `eslint --init`, you'll have a `.eslintrc` file in your directory. In it, you'll see some rules configured like this: +After running `npm init @eslint/config`, you'll have a `.eslintrc` file in your directory. In it, you'll see some rules configured like this: ```json { diff --git a/bin/eslint.js b/bin/eslint.js index 6b05356b9db..d00a870c089 100755 --- a/bin/eslint.js +++ b/bin/eslint.js @@ -124,7 +124,13 @@ ${message}`); // Call the config initializer if `--init` is present. if (process.argv.includes("--init")) { - await require("../lib/init/config-initializer").initializeConfig(); + + // `eslint --init` has been moved to `@eslint/create-config` + console.warn("You can also run this command directly using 'npm init @eslint/config'."); + + const spawn = require("cross-spawn"); + + spawn.sync("npm", ["init", "@eslint/config"], { encoding: "utf8", stdio: "inherit" }); return; } diff --git a/docs/developer-guide/architecture.md b/docs/developer-guide/architecture.md index 6531c28391f..37261abcc3d 100644 --- a/docs/developer-guide/architecture.md +++ b/docs/developer-guide/architecture.md @@ -7,7 +7,6 @@ At a high level, there are a few key parts to ESLint: * `bin/eslint.js` - this is the file that actually gets executed with the command line utility. It's a dumb wrapper that does nothing more than bootstrap ESLint, passing the command line arguments to `cli`. This is intentionally small so as not to require heavy testing. * `lib/api.js` - this is the entry point of `require("eslint")`. This file exposes an object that contains public classes `Linter`, `ESLint`, `RuleTester`, and `SourceCode`. * `lib/cli.js` - this is the heart of the ESLint CLI. It takes an array of arguments and then uses `eslint` to execute the commands. By keeping this as a separate utility, it allows others to effectively call ESLint from within another Node.js program as if it were done on the command line. The main call is `cli.execute()`. This is also the part that does all the file reading, directory traversing, input, and output. -* `lib/init/` - this module contains `--init` functionality that set up a configuration file for end users. * `lib/cli-engine/` - this module is `CLIEngine` class that finds source code files and configuration files then does code verifying with the `Linter` class. This includes the loading logic of configuration files, parsers, plugins, and formatters. * `lib/linter/` - this module is the core `Linter` class that does code verifying based on configuration options. This file does no file I/O and does not interact with the `console` at all. For other Node.js programs that have JavaScript text to verify, they would be able to use this interface directly. * `lib/rule-tester/` - this module is `RuleTester` class that is a wrapper around Mocha so that rules can be unit tested. This class lets us write consistently formatted tests for each rule that is implemented and be confident that each of the rules work. The RuleTester interface was modeled after Mocha and works with Mocha's global testing methods. RuleTester can also be modified to work with other testing frameworks. diff --git a/docs/user-guide/command-line-interface.md b/docs/user-guide/command-line-interface.md index 6f350d48597..d30931bf048 100644 --- a/docs/user-guide/command-line-interface.md +++ b/docs/user-guide/command-line-interface.md @@ -457,7 +457,7 @@ Example: #### `--init` -This option will start config initialization wizard. It's designed to help new users quickly create .eslintrc file by answering a few questions, choosing a popular style guide, or inspecting your source files and attempting to automatically generate a suitable configuration. +This option will run `npm init @eslint/config` to start config initialization wizard. It's designed to help new users quickly create .eslintrc file by answering a few questions, choosing a popular style guide. The resulting configuration file will be created in the current directory. diff --git a/docs/user-guide/configuring/configuration-files.md b/docs/user-guide/configuring/configuration-files.md index 3156a99e312..cc1d5bf3c5f 100644 --- a/docs/user-guide/configuring/configuration-files.md +++ b/docs/user-guide/configuring/configuration-files.md @@ -232,7 +232,7 @@ A [sharable configuration](https://eslint.org/docs/developer-guide/shareable-con The `extends` property value can omit the `eslint-config-` prefix of the package name. -The `eslint --init` command can create a configuration so you can extend a popular style guide (for example, `eslint-config-standard`). +The `npm init @eslint/config` command can create a configuration so you can extend a popular style guide (for example, `eslint-config-standard`). Example of a configuration file in YAML format: diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md index cf808ddc022..1cd016b6647 100644 --- a/docs/user-guide/getting-started.md +++ b/docs/user-guide/getting-started.md @@ -20,17 +20,17 @@ npm install eslint --save-dev yarn add eslint --dev ``` -You should then set up a configuration file, and the easiest way to do that is to use the `--init` flag: +You should then set up a configuration file, and the easiest way to do that is: ```sh -$ npx eslint --init +$ npm init @eslint/config # or -$ yarn run eslint --init +$ yarn create @eslint/config ``` -**Note:** `--init` assumes you have a `package.json` file already. If you don't, make sure to run `npm init` or `yarn init` beforehand. +**Note:** `npm init @eslint/config` assumes you have a `package.json` file already. If you don't, make sure to run `npm init` or `yarn init` beforehand. After that, you can run ESLint on any file or directory like this: @@ -48,7 +48,7 @@ It is also possible to install ESLint globally rather than locally (using `npm i **Note:** If you are coming from a version before 1.0.0 please see the [migration guide](migrating-to-1.0.0.md). -After running `eslint --init`, you'll have a `.eslintrc.{js,yml,json}` file in your directory. In it, you'll see some rules configured like this: +After running `npm init @eslint/config`, you'll have a `.eslintrc.{js,yml,json}` file in your directory. In it, you'll see some rules configured like this: ```json { diff --git a/docs/user-guide/migrating-from-jscs.md b/docs/user-guide/migrating-from-jscs.md index e05ee70d625..b48e9f9fe07 100644 --- a/docs/user-guide/migrating-from-jscs.md +++ b/docs/user-guide/migrating-from-jscs.md @@ -42,7 +42,7 @@ $ polyjuice --jscs .jscsrc.json ./foo/.jscsrc.json > .eslintrc.json If you don't want to convert your JSCS configuration directly into an ESLint configuration, then you can use ESLint's built-in wizard to get you started. Just run: ```sh -$ eslint --init +$ npm init @eslint/config ``` You'll be guided through a series of questions that will help you setup a basic configuration file to get you started. diff --git a/lib/init/autoconfig.js b/lib/init/autoconfig.js deleted file mode 100644 index ea2523421c2..00000000000 --- a/lib/init/autoconfig.js +++ /dev/null @@ -1,351 +0,0 @@ -/** - * @fileoverview Used for creating a suggested configuration based on project code. - * @author Ian VanSchooten - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const equal = require("fast-deep-equal"), - recConfig = require("../../conf/eslint-recommended"), - { - Legacy: { - ConfigOps - } - } = require("@eslint/eslintrc"), - { Linter } = require("../linter"), - configRule = require("./config-rule"); - -const debug = require("debug")("eslint:autoconfig"); -const linter = new Linter(); - -//------------------------------------------------------------------------------ -// Data -//------------------------------------------------------------------------------ - -const MAX_CONFIG_COMBINATIONS = 17, // 16 combinations + 1 for severity only - RECOMMENDED_CONFIG_NAME = "eslint:recommended"; - -//------------------------------------------------------------------------------ -// Private -//------------------------------------------------------------------------------ - -/** - * Information about a rule configuration, in the context of a Registry. - * @typedef {Object} registryItem - * @property {ruleConfig} config A valid configuration for the rule - * @property {number} specificity The number of elements in the ruleConfig array - * @property {number} errorCount The number of errors encountered when linting with the config - */ - -/** - * This callback is used to measure execution status in a progress bar - * @callback progressCallback - * @param {number} The total number of times the callback will be called. - */ - -/** - * Create registryItems for rules - * @param {rulesConfig} rulesConfig Hash of rule names and arrays of ruleConfig items - * @returns {Object} registryItems for each rule in provided rulesConfig - */ -function makeRegistryItems(rulesConfig) { - return Object.keys(rulesConfig).reduce((accumulator, ruleId) => { - accumulator[ruleId] = rulesConfig[ruleId].map(config => ({ - config, - specificity: config.length || 1, - errorCount: void 0 - })); - return accumulator; - }, {}); -} - -/** - * Creates an object in which to store rule configs and error counts - * - * Unless a rulesConfig is provided at construction, the registry will not contain - * any rules, only methods. This will be useful for building up registries manually. - * - * Registry class - */ -class Registry { - - /** - * @param {rulesConfig} [rulesConfig] Hash of rule names and arrays of possible configurations - */ - constructor(rulesConfig) { - this.rules = (rulesConfig) ? makeRegistryItems(rulesConfig) : {}; - } - - /** - * Populate the registry with core rule configs. - * - * It will set the registry's `rule` property to an object having rule names - * as keys and an array of registryItems as values. - * @returns {void} - */ - populateFromCoreRules() { - const rulesConfig = configRule.createCoreRuleConfigs(/* noDeprecated = */ true); - - this.rules = makeRegistryItems(rulesConfig); - } - - /** - * Creates sets of rule configurations which can be used for linting - * and initializes registry errors to zero for those configurations (side effect). - * - * This combines as many rules together as possible, such that the first sets - * in the array will have the highest number of rules configured, and later sets - * will have fewer and fewer, as not all rules have the same number of possible - * configurations. - * - * The length of the returned array will be <= MAX_CONFIG_COMBINATIONS. - * @returns {Object[]} "rules" configurations to use for linting - */ - buildRuleSets() { - let idx = 0; - const ruleIds = Object.keys(this.rules), - ruleSets = []; - - /** - * Add a rule configuration from the registry to the ruleSets - * - * This is broken out into its own function so that it doesn't need to be - * created inside of the while loop. - * @param {string} rule The ruleId to add. - * @returns {void} - */ - const addRuleToRuleSet = function(rule) { - - /* - * This check ensures that there is a rule configuration and that - * it has fewer than the max combinations allowed. - * If it has too many configs, we will only use the most basic of - * the possible configurations. - */ - const hasFewCombos = (this.rules[rule].length <= MAX_CONFIG_COMBINATIONS); - - if (this.rules[rule][idx] && (hasFewCombos || this.rules[rule][idx].specificity <= 2)) { - - /* - * If the rule has too many possible combinations, only take - * simple ones, avoiding objects. - */ - if (!hasFewCombos && typeof this.rules[rule][idx].config[1] === "object") { - return; - } - - ruleSets[idx] = ruleSets[idx] || {}; - ruleSets[idx][rule] = this.rules[rule][idx].config; - - /* - * Initialize errorCount to zero, since this is a config which - * will be linted. - */ - this.rules[rule][idx].errorCount = 0; - } - }.bind(this); - - while (ruleSets.length === idx) { - ruleIds.forEach(addRuleToRuleSet); - idx += 1; - } - - return ruleSets; - } - - /** - * Remove all items from the registry with a non-zero number of errors - * - * Note: this also removes rule configurations which were not linted - * (meaning, they have an undefined errorCount). - * @returns {void} - */ - stripFailingConfigs() { - const ruleIds = Object.keys(this.rules), - newRegistry = new Registry(); - - newRegistry.rules = Object.assign({}, this.rules); - ruleIds.forEach(ruleId => { - const errorFreeItems = newRegistry.rules[ruleId].filter(registryItem => (registryItem.errorCount === 0)); - - if (errorFreeItems.length > 0) { - newRegistry.rules[ruleId] = errorFreeItems; - } else { - delete newRegistry.rules[ruleId]; - } - }); - - return newRegistry; - } - - /** - * Removes rule configurations which were not included in a ruleSet - * @returns {void} - */ - stripExtraConfigs() { - const ruleIds = Object.keys(this.rules), - newRegistry = new Registry(); - - newRegistry.rules = Object.assign({}, this.rules); - ruleIds.forEach(ruleId => { - newRegistry.rules[ruleId] = newRegistry.rules[ruleId].filter(registryItem => (typeof registryItem.errorCount !== "undefined")); - }); - - return newRegistry; - } - - /** - * Creates a registry of rules which had no error-free configs. - * The new registry is intended to be analyzed to determine whether its rules - * should be disabled or set to warning. - * @returns {Registry} A registry of failing rules. - */ - getFailingRulesRegistry() { - const ruleIds = Object.keys(this.rules), - failingRegistry = new Registry(); - - ruleIds.forEach(ruleId => { - const failingConfigs = this.rules[ruleId].filter(registryItem => (registryItem.errorCount > 0)); - - if (failingConfigs && failingConfigs.length === this.rules[ruleId].length) { - failingRegistry.rules[ruleId] = failingConfigs; - } - }); - - return failingRegistry; - } - - /** - * Create an eslint config for any rules which only have one configuration - * in the registry. - * @returns {Object} An eslint config with rules section populated - */ - createConfig() { - const ruleIds = Object.keys(this.rules), - config = { rules: {} }; - - ruleIds.forEach(ruleId => { - if (this.rules[ruleId].length === 1) { - config.rules[ruleId] = this.rules[ruleId][0].config; - } - }); - - return config; - } - - /** - * Return a cloned registry containing only configs with a desired specificity - * @param {number} specificity Only keep configs with this specificity - * @returns {Registry} A registry of rules - */ - filterBySpecificity(specificity) { - const ruleIds = Object.keys(this.rules), - newRegistry = new Registry(); - - newRegistry.rules = Object.assign({}, this.rules); - ruleIds.forEach(ruleId => { - newRegistry.rules[ruleId] = this.rules[ruleId].filter(registryItem => (registryItem.specificity === specificity)); - }); - - return newRegistry; - } - - /** - * Lint SourceCodes against all configurations in the registry, and record results - * @param {Object[]} sourceCodes SourceCode objects for each filename - * @param {Object} config ESLint config object - * @param {progressCallback} [cb] Optional callback for reporting execution status - * @returns {Registry} New registry with errorCount populated - */ - lintSourceCode(sourceCodes, config, cb) { - let lintedRegistry = new Registry(); - - lintedRegistry.rules = Object.assign({}, this.rules); - - const ruleSets = lintedRegistry.buildRuleSets(); - - lintedRegistry = lintedRegistry.stripExtraConfigs(); - - debug("Linting with all possible rule combinations"); - - const filenames = Object.keys(sourceCodes); - const totalFilesLinting = filenames.length * ruleSets.length; - - filenames.forEach(filename => { - debug(`Linting file: ${filename}`); - - let ruleSetIdx = 0; - - ruleSets.forEach(ruleSet => { - const lintConfig = Object.assign({}, config, { rules: ruleSet }); - const lintResults = linter.verify(sourceCodes[filename], lintConfig); - - lintResults.forEach(result => { - - /* - * It is possible that the error is from a configuration comment - * in a linted file, in which case there may not be a config - * set in this ruleSetIdx. - * (https://github.com/eslint/eslint/issues/5992) - * (https://github.com/eslint/eslint/issues/7860) - */ - if ( - lintedRegistry.rules[result.ruleId] && - lintedRegistry.rules[result.ruleId][ruleSetIdx] - ) { - lintedRegistry.rules[result.ruleId][ruleSetIdx].errorCount += 1; - } - }); - - ruleSetIdx += 1; - - if (cb) { - cb(totalFilesLinting); // eslint-disable-line node/callback-return -- End of function - } - }); - - // Deallocate for GC - sourceCodes[filename] = null; - }); - - return lintedRegistry; - } -} - -/** - * Extract rule configuration into eslint:recommended where possible. - * - * This will return a new config with `["extends": [ ..., "eslint:recommended"]` and - * only the rules which have configurations different from the recommended config. - * @param {Object} config config object - * @returns {Object} config object using `"extends": ["eslint:recommended"]` - */ -function extendFromRecommended(config) { - const newConfig = Object.assign({}, config); - - ConfigOps.normalizeToStrings(newConfig); - - const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId])); - - recRules.forEach(ruleId => { - if (equal(recConfig.rules[ruleId], newConfig.rules[ruleId])) { - delete newConfig.rules[ruleId]; - } - }); - newConfig.extends.unshift(RECOMMENDED_CONFIG_NAME); - return newConfig; -} - - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -module.exports = { - Registry, - extendFromRecommended -}; diff --git a/lib/init/config-file.js b/lib/init/config-file.js deleted file mode 100644 index 9eb10fab3a4..00000000000 --- a/lib/init/config-file.js +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @fileoverview Helper to locate and load configuration files. - * @author Nicholas C. Zakas - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const fs = require("fs"), - path = require("path"), - stringify = require("json-stable-stringify-without-jsonify"); - -const debug = require("debug")("eslint:config-file"); - -//------------------------------------------------------------------------------ -// 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 - */ -function writeYAMLConfigFile(config, filePath) { - debug(`Writing YAML config file: ${filePath}`); - - // lazy load YAML to improve performance when not used - const yaml = require("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 - */ -function writeJSConfigFile(config, filePath) { - debug(`Writing JS config file: ${filePath}`); - - let contentToWrite; - const stringifiedContent = `module.exports = ${stringify(config, { cmp: sortByKey, space: 4 })};\n`; - - try { - const { CLIEngine } = require("../cli-engine"); - const linter = new CLIEngine({ - baseConfig: config, - fix: true, - useEslintrc: false - }); - const report = linter.executeOnText(stringifiedContent); - - contentToWrite = report.results[0].output || stringifiedContent; - } catch (e) { - debug("Error linting JavaScript config file, writing unlinted version"); - const errorMessage = e.message; - - contentToWrite = stringifiedContent; - e.message = "An error occurred while generating your JavaScript config file. "; - e.message += "A config file was still generated, but the config file itself may not follow your linting rules."; - e.message += `\nError: ${errorMessage}`; - throw e; - } finally { - fs.writeFileSync(filePath, contentToWrite, "utf8"); - } -} - -/** - * 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 - */ -function write(config, filePath) { - switch (path.extname(filePath)) { - case ".js": - case ".cjs": - writeJSConfigFile(config, filePath); - break; - - case ".json": - writeJSONConfigFile(config, filePath); - break; - - case ".yaml": - case ".yml": - writeYAMLConfigFile(config, filePath); - break; - - default: - throw new Error("Can't write to unknown file type."); - } -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -module.exports = { - write -}; diff --git a/lib/init/config-initializer.js b/lib/init/config-initializer.js deleted file mode 100644 index 3c244b7bcc0..00000000000 --- a/lib/init/config-initializer.js +++ /dev/null @@ -1,709 +0,0 @@ -/** - * @fileoverview Config initialization wizard. - * @author Ilya Volodin - */ - - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const util = require("util"), - path = require("path"), - fs = require("fs"), - enquirer = require("enquirer"), - ProgressBar = require("progress"), - semver = require("semver"), - espree = require("espree"), - recConfig = require("../../conf/eslint-recommended"), - { - Legacy: { - ConfigOps, - naming - } - } = require("@eslint/eslintrc"), - log = require("../shared/logging"), - ModuleResolver = require("../shared/relative-module-resolver"), - autoconfig = require("./autoconfig.js"), - ConfigFile = require("./config-file"), - npmUtils = require("./npm-utils"), - { getSourceCodeOfFiles } = require("./source-code-utils"); - -const debug = require("debug")("eslint:config-initializer"); - -//------------------------------------------------------------------------------ -// Private -//------------------------------------------------------------------------------ - -/* 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} - */ -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") { - const pkgJSONPath = npmUtils.findPackageJson(); - - if (pkgJSONPath) { - const pkgJSONContents = JSON.parse(fs.readFileSync(pkgJSONPath, "utf8")); - - if (pkgJSONContents.type === "module") { - extname = ".cjs"; - } - } - } - - const installedESLint = config.installedESLint; - - delete config.installedESLint; - - ConfigFile.write(config, `./.eslintrc${extname}`); - log.info(`Successfully created .eslintrc${extname} file in ${process.cwd()}`); - - if (installedESLint) { - log.info("ESLint was installed locally. We recommend using this local copy instead of your globally-installed copy."); - } -} - -/** - * 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) { - log.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"; - } - } - if (config.extends) { - const extendList = Array.isArray(config.extends) ? config.extends : [config.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) { - log.info("Local ESLint installation not found."); - modules.eslint = modules.eslint || "latest"; - config.installedESLint = true; - } - } - - return Object.keys(modules).map(name => `${name}@${modules[name]}`); -} - -/** - * Set the `rules` of a config by examining a user's source code - * - * Note: This clones the config object and returns a new config to avoid mutating - * the original config parameter. - * @param {Object} answers answers received from enquirer - * @param {Object} config config object - * @throws {Error} If source code retrieval fails or source code file count is 0. - * @returns {Object} config object with configured rules - */ -function configureRules(answers, config) { - const BAR_TOTAL = 20, - BAR_SOURCE_CODE_TOTAL = 4, - newConfig = Object.assign({}, config), - disabledConfigs = {}; - let sourceCodes, - registry; - - // Set up a progress bar, as this process can take a long time - const bar = new ProgressBar("Determining Config: :percent [:bar] :elapseds elapsed, eta :etas ", { - width: 30, - total: BAR_TOTAL - }); - - bar.tick(0); // Shows the progress bar - - // Get the SourceCode of all chosen files - const patterns = answers.patterns.split(/[\s]+/u); - - try { - sourceCodes = getSourceCodeOfFiles(patterns, { baseConfig: newConfig, useEslintrc: false }, total => { - bar.tick((BAR_SOURCE_CODE_TOTAL / total)); - }); - } catch (e) { - log.info("\n"); - throw e; - } - const fileQty = Object.keys(sourceCodes).length; - - if (fileQty === 0) { - log.info("\n"); - throw new Error("Automatic Configuration failed. No files were able to be parsed."); - } - - // Create a registry of rule configs - registry = new autoconfig.Registry(); - registry.populateFromCoreRules(); - - // Lint all files with each rule config in the registry - registry = registry.lintSourceCode(sourceCodes, newConfig, total => { - bar.tick((BAR_TOTAL - BAR_SOURCE_CODE_TOTAL) / total); // Subtract out ticks used at beginning - }); - debug(`\nRegistry: ${util.inspect(registry.rules, { depth: null })}`); - - // Create a list of recommended rules, because we don't want to disable them - const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId])); - - // Find and disable rules which had no error-free configuration - const failingRegistry = registry.getFailingRulesRegistry(); - - Object.keys(failingRegistry.rules).forEach(ruleId => { - - // If the rule is recommended, set it to error, otherwise disable it - disabledConfigs[ruleId] = (recRules.indexOf(ruleId) !== -1) ? 2 : 0; - }); - - // Now that we know which rules to disable, strip out configs with errors - registry = registry.stripFailingConfigs(); - - /* - * If there is only one config that results in no errors for a rule, we should use it. - * createConfig will only add rules that have one configuration in the registry. - */ - const singleConfigs = registry.createConfig().rules; - - /* - * The "sweet spot" for number of options in a config seems to be two (severity plus one option). - * Very often, a third option (usually an object) is available to address - * edge cases, exceptions, or unique situations. We will prefer to use a config with - * specificity of two. - */ - const specTwoConfigs = registry.filterBySpecificity(2).createConfig().rules; - - // Maybe a specific combination using all three options works - const specThreeConfigs = registry.filterBySpecificity(3).createConfig().rules; - - // If all else fails, try to use the default (severity only) - const defaultConfigs = registry.filterBySpecificity(1).createConfig().rules; - - // Combine configs in reverse priority order (later take precedence) - newConfig.rules = Object.assign({}, disabledConfigs, defaultConfigs, specThreeConfigs, specTwoConfigs, singleConfigs); - - // Make sure progress bar has finished (floating point rounding) - bar.update(BAR_TOTAL); - - // Log out some stats to let the user know what happened - const finalRuleIds = Object.keys(newConfig.rules); - const totalRules = finalRuleIds.length; - const enabledRules = finalRuleIds.filter(ruleId => (newConfig.rules[ruleId] !== 0)).length; - const resultMessage = [ - `\nEnabled ${enabledRules} out of ${totalRules}`, - `rules based on ${fileQty}`, - `file${(fileQty === 1) ? "." : "s."}` - ].join(" "); - - log.info(resultMessage); - - ConfigOps.normalizeToStrings(newConfig); - return newConfig; -} - -/** - * process user's answers and create config object - * @param {Object} answers answers received from enquirer - * @returns {Object} config object - */ -function processAnswers(answers) { - let config = { - rules: {}, - env: {}, - parserOptions: {}, - extends: [] - }; - - config.parserOptions.ecmaVersion = espree.latestEcmaVersion; - config.env.es2021 = 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; - }); - - // add in library information - if (answers.framework === "react") { - config.parserOptions.ecmaFeatures = { - jsx: true - }; - config.plugins = ["react"]; - config.extends.push("plugin:react/recommended"); - } else if (answers.framework === "vue") { - config.plugins = ["vue"]; - config.extends.push("plugin:vue/essential"); - } - - if (answers.typescript) { - if (answers.framework === "vue") { - config.parserOptions.parser = "@typescript-eslint/parser"; - } else { - config.parser = "@typescript-eslint/parser"; - } - - if (Array.isArray(config.plugins)) { - config.plugins.push("@typescript-eslint"); - } else { - config.plugins = ["@typescript-eslint"]; - } - } - - // 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"]; - } else if (answers.source === "auto") { - config = configureRules(answers, config); - config = autoconfig.extendFromRecommended(config); - } - } - if (answers.typescript && config.extends.includes("eslint:recommended")) { - config.extends.push("plugin:@typescript-eslint/recommended"); - } - - // normalize extends - if (config.extends.length === 0) { - delete config.extends; - } else if (config.extends.length === 1) { - config.extends = config.extends[0]; - } - - 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 eslintPath = ModuleResolver.resolve("eslint", path.join(process.cwd(), "__placeholder__.js")); - const eslint = require(eslintPath); - - return eslint.linter.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. - * @returns {void} - */ -function installModules(modules) { - log.info(`Installing ${modules.join(", ")}`); - npmUtils.installSyncSaveDev(modules); -} - -/* istanbul ignore next: no need to test enquirer */ -/** - * Ask user to install modules. - * @param {string[]} modules Array of modules to be installed. - * @param {boolean} packageJsonExists Indicates if package.json is existed. - * @returns {Promise} Answer that indicates if user wants to install. - */ -function askInstallModules(modules, packageJsonExists) { - - // If no modules, do nothing. - if (modules.length === 0) { - return Promise.resolve(); - } - - log.info("The config that you've selected requires the following dependencies:\n"); - log.info(modules.join(" ")); - return enquirer.prompt([ - { - type: "toggle", - name: "executeInstallation", - message: "Would you like to install them now with npm?", - enabled: "Yes", - disabled: "No", - initial: 1, - skip() { - return !(modules.length && packageJsonExists); - }, - result(input) { - return this.skipped ? null : input; - } - } - ]).then(({ executeInstallation }) => { - if (executeInstallation) { - installModules(modules); - } - }); -} - -/* 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 - */ -function promptUser() { - - 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 - }, - { - 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" }, - { message: "Inspect your JavaScript file(s)", name: "auto" } - ], - 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: [ - { message: "Airbnb: https://github.com/airbnb/javascript", name: "airbnb" }, - { message: "Standard: https://github.com/standard/standard", name: "standard" }, - { message: "Google: https://github.com/google/eslint-config-google", name: "google" }, - { message: "XO: https://github.com/xojs/eslint-config-xo", name: "xo" } - ], - skip() { - this.state.answers.packageJsonExists = npmUtils.checkPackageJson(); - return !(this.state.answers.source === "guide" && this.state.answers.packageJsonExists); - }, - result(input) { - return this.skipped ? null : input; - } - }, - { - type: "input", - name: "patterns", - message: "Which file(s), path(s), or glob(s) should be examined?", - skip() { - return this.state.answers.source !== "auto"; - }, - validate(input) { - if (!this.skipped && input.trim().length === 0 && input.trim() !== ",") { - return "You must tell us what code to examine. Try again."; - } - return true; - } - }, - { - 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" && this.state.answers.packageJsonExists && 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, earlyAnswers.packageJsonExists) - .then(() => writeFile(config, earlyAnswers.format)); - } - - // early exit if you are using a style guide - if (earlyAnswers.source === "guide") { - if (!earlyAnswers.packageJsonExists) { - log.info("A package.json is necessary to install plugins such as style guides. Run `npm init` to create a package.json file and try again."); - return void 0; - } - if (earlyAnswers.installESLint === false && !semver.satisfies(earlyAnswers.localESLintVersion, earlyAnswers.requiredESLintVersionRange)) { - log.info(`Note: it might not work since ESLint's version is mismatched with the ${earlyAnswers.styleguide} config.`); - } - if (earlyAnswers.styleguide === "airbnb" && earlyAnswers.framework !== "react") { - earlyAnswers.styleguide = "airbnb-base"; - } - - const config = processAnswers(earlyAnswers); - - if (Array.isArray(config.extends)) { - config.extends.push(earlyAnswers.styleguide); - } else if (config.extends) { - config.extends = [config.extends, earlyAnswers.styleguide]; - } else { - config.extends = [earlyAnswers.styleguide]; - } - - const modules = getModulesList(config); - - return askInstallModules(modules, earlyAnswers.packageJsonExists) - .then(() => writeFile(config, earlyAnswers.format)); - - } - - if (earlyAnswers.source === "auto") { - const combinedAnswers = Object.assign({}, earlyAnswers); - const config = processAnswers(combinedAnswers); - 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)); - }); - }); -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -const init = { - getModulesList, - hasESLintVersionConflict, - installModules, - processAnswers, - writeFile, - /* istanbul ignore next */initializeConfig() { - return promptUser(); - } -}; - -module.exports = init; diff --git a/lib/init/npm-utils.js b/lib/init/npm-utils.js deleted file mode 100644 index 4a8efe964f3..00000000000 --- a/lib/init/npm-utils.js +++ /dev/null @@ -1,179 +0,0 @@ -/** - * @fileoverview Utility for executing npm commands. - * @author Ian VanSchooten - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const fs = require("fs"), - spawn = require("cross-spawn"), - path = require("path"), - log = require("../shared/logging"); - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Find the closest package.json file, starting at process.cwd (by default), - * and working up to root. - * @param {string} [startDir=process.cwd()] Starting directory - * @returns {string} Absolute path to closest package.json file - */ -function findPackageJson(startDir) { - let dir = path.resolve(startDir || process.cwd()); - - do { - const pkgFile = path.join(dir, "package.json"); - - if (!fs.existsSync(pkgFile) || !fs.statSync(pkgFile).isFile()) { - dir = path.join(dir, ".."); - continue; - } - return pkgFile; - } while (dir !== path.resolve(dir, "..")); - return null; -} - -//------------------------------------------------------------------------------ -// Private -//------------------------------------------------------------------------------ - -/** - * Install node modules synchronously and save to devDependencies in package.json - * @param {string|string[]} packages Node module or modules to install - * @returns {void} - */ -function installSyncSaveDev(packages) { - const packageList = Array.isArray(packages) ? packages : [packages]; - const npmProcess = spawn.sync("npm", ["i", "--save-dev"].concat(packageList), { stdio: "inherit" }); - const error = npmProcess.error; - - if (error && error.code === "ENOENT") { - const pluralS = packageList.length > 1 ? "s" : ""; - - log.error(`Could not execute npm. Please install the following package${pluralS} with a package manager of your choice: ${packageList.join(", ")}`); - } -} - -/** - * Fetch `peerDependencies` of the given package by `npm show` command. - * @param {string} packageName The package name to fetch peerDependencies. - * @returns {Object} Gotten peerDependencies. Returns null if npm was not found. - */ -function fetchPeerDependencies(packageName) { - const npmProcess = spawn.sync( - "npm", - ["show", "--json", packageName, "peerDependencies"], - { encoding: "utf8" } - ); - - const error = npmProcess.error; - - if (error && error.code === "ENOENT") { - return null; - } - const fetchedText = npmProcess.stdout.trim(); - - return JSON.parse(fetchedText || "{}"); - - -} - -/** - * Check whether node modules are include in a project's package.json. - * @param {string[]} packages Array of node module names - * @param {Object} opt Options Object - * @param {boolean} opt.dependencies Set to true to check for direct dependencies - * @param {boolean} opt.devDependencies Set to true to check for development dependencies - * @param {boolean} opt.startdir Directory to begin searching from - * @throws {Error} If cannot find valid `package.json` file. - * @returns {Object} An object whose keys are the module names - * and values are booleans indicating installation. - */ -function check(packages, opt) { - const deps = new Set(); - const pkgJson = (opt) ? findPackageJson(opt.startDir) : findPackageJson(); - let fileJson; - - if (!pkgJson) { - throw new Error("Could not find a package.json file. Run 'npm init' to create one."); - } - - try { - fileJson = JSON.parse(fs.readFileSync(pkgJson, "utf8")); - } catch (e) { - const error = new Error(e); - - error.messageTemplate = "failed-to-read-json"; - error.messageData = { - path: pkgJson, - message: e.message - }; - throw error; - } - - ["dependencies", "devDependencies"].forEach(key => { - if (opt[key] && typeof fileJson[key] === "object") { - Object.keys(fileJson[key]).forEach(dep => deps.add(dep)); - } - }); - - return packages.reduce((status, pkg) => { - status[pkg] = deps.has(pkg); - return status; - }, {}); -} - -/** - * Check whether node modules are included in the dependencies of a project's - * package.json. - * - * Convenience wrapper around check(). - * @param {string[]} packages Array of node modules to check. - * @param {string} rootDir The directory containing a package.json - * @returns {Object} An object whose keys are the module names - * and values are booleans indicating installation. - */ -function checkDeps(packages, rootDir) { - return check(packages, { dependencies: true, startDir: rootDir }); -} - -/** - * Check whether node modules are included in the devDependencies of a project's - * package.json. - * - * Convenience wrapper around check(). - * @param {string[]} packages Array of node modules to check. - * @returns {Object} An object whose keys are the module names - * and values are booleans indicating installation. - */ -function checkDevDeps(packages) { - return check(packages, { devDependencies: true }); -} - -/** - * Check whether package.json is found in current path. - * @param {string} [startDir] Starting directory - * @returns {boolean} Whether a package.json is found in current path. - */ -function checkPackageJson(startDir) { - return !!findPackageJson(startDir); -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -module.exports = { - installSyncSaveDev, - fetchPeerDependencies, - findPackageJson, - checkDeps, - checkDevDeps, - checkPackageJson -}; diff --git a/lib/init/source-code-utils.js b/lib/init/source-code-utils.js deleted file mode 100644 index 08c20e5d56e..00000000000 --- a/lib/init/source-code-utils.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @fileoverview Tools for obtaining SourceCode objects. - * @author Ian VanSchooten - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const { CLIEngine } = require("../cli-engine"); - -/* - * This is used for: - * - * 1. Enumerate target file because we have not expose such a API on `CLIEngine` - * (https://github.com/eslint/eslint/issues/11222). - * 2. Create `SourceCode` instances. Because we don't have any function which - * instantiate `SourceCode` so it needs to take the created `SourceCode` - * instance out after linting. - * - * TODO1: Expose the API that enumerates target files. - * TODO2: Extract the creation logic of `SourceCode` from `Linter` class. - */ -const { getCLIEngineInternalSlots } = require("../cli-engine/cli-engine"); // eslint-disable-line node/no-restricted-require -- Todo - -const debug = require("debug")("eslint:source-code-utils"); - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Get the SourceCode object for a single file - * @param {string} filename The fully resolved filename to get SourceCode from. - * @param {Object} engine A CLIEngine. - * @throws {Error} Upon fatal errors from execution. - * @returns {Array} Array of the SourceCode object representing the file - * and fatal error message. - */ -function getSourceCodeOfFile(filename, engine) { - debug("getting sourceCode of", filename); - const results = engine.executeOnFiles([filename]); - - if (results && results.results[0] && results.results[0].messages[0] && results.results[0].messages[0].fatal) { - const msg = results.results[0].messages[0]; - - throw new Error(`(${filename}:${msg.line}:${msg.column}) ${msg.message}`); - } - - // TODO: extract the logic that creates source code objects to `SourceCode#parse(text, options)` or something like. - const { linter } = getCLIEngineInternalSlots(engine); - const sourceCode = linter.getSourceCode(); - - return sourceCode; -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - - -/** - * This callback is used to measure execution status in a progress bar - * @callback progressCallback - * @param {number} The total number of times the callback will be called. - */ - -/** - * Gets the SourceCode of a single file, or set of files. - * @param {string[]|string} patterns A filename, directory name, or glob, or an array of them - * @param {Object} options A CLIEngine options object. If not provided, the default cli options will be used. - * @param {progressCallback} callback Callback for reporting execution status - * @returns {Object} The SourceCode of all processed files. - */ -function getSourceCodeOfFiles(patterns, options, callback) { - const sourceCodes = {}; - const globPatternsList = typeof patterns === "string" ? [patterns] : patterns; - const engine = new CLIEngine({ ...options, rules: {} }); - - // TODO: make file iteration as a public API and use it. - const { fileEnumerator } = getCLIEngineInternalSlots(engine); - const filenames = - Array.from(fileEnumerator.iterateFiles(globPatternsList)) - .filter(entry => !entry.ignored) - .map(entry => entry.filePath); - - if (filenames.length === 0) { - debug(`Did not find any files matching pattern(s): ${globPatternsList}`); - } - - filenames.forEach(filename => { - const sourceCode = getSourceCodeOfFile(filename, engine); - - if (sourceCode) { - debug("got sourceCode of", filename); - sourceCodes[filename] = sourceCode; - } - if (callback) { - callback(filenames.length); // eslint-disable-line node/callback-return -- End of function - } - }); - - return sourceCodes; -} - -module.exports = { - getSourceCodeOfFiles -}; diff --git a/messages/no-config-found.js b/messages/no-config-found.js index 1042143f9fd..c2f7ac73b40 100644 --- a/messages/no-config-found.js +++ b/messages/no-config-found.js @@ -6,7 +6,7 @@ module.exports = function(it) { return ` ESLint couldn't find a configuration file. To set up a configuration file for this project, please run: - eslint --init + npm init @eslint/config ESLint looked for configuration files in ${directoryPath} and its ancestors. If it found none, it then looked in your home directory. diff --git a/package.json b/package.json index 948d8d959ec..3cb6cea4249 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", - "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.1.0", "eslint-utils": "^3.0.0", @@ -78,9 +77,7 @@ "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", - "progress": "^2.0.0", "regexpp": "^3.2.0", - "semver": "^7.2.1", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0", @@ -125,10 +122,12 @@ "node-polyfill-webpack-plugin": "^1.0.3", "npm-license": "^0.3.3", "nyc": "^15.0.1", + "progress": "^2.0.3", "proxyquire": "^2.0.1", "puppeteer": "^9.1.1", "recast": "^0.20.4", "regenerator-runtime": "^0.13.2", + "semver": "^7.3.5", "shelljs": "^0.8.2", "sinon": "^11.0.0", "temp": "^0.9.0", diff --git a/tests/fixtures/autoconfig/source-with-comments.js b/tests/fixtures/autoconfig/source-with-comments.js deleted file mode 100644 index d2e3bf9a665..00000000000 --- a/tests/fixtures/autoconfig/source-with-comments.js +++ /dev/null @@ -1,6 +0,0 @@ -/* eslint semi: [2, "never"] */ -/* eslint import/extensions: 2 */ - -var foo = 42; -var baz = "baz"; -var bar = '"no-escape"'; diff --git a/tests/fixtures/autoconfig/source.js b/tests/fixtures/autoconfig/source.js deleted file mode 100644 index 0c126ba6d15..00000000000 --- a/tests/fixtures/autoconfig/source.js +++ /dev/null @@ -1,3 +0,0 @@ -var foo = 42; -var baz = "baz"; -var bar = '"no-escape"'; diff --git a/tests/fixtures/config-initializer/lib/doubleQuotes.js b/tests/fixtures/config-initializer/lib/doubleQuotes.js deleted file mode 100644 index 1cd4a2abbe2..00000000000 --- 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 bf10b1cfd19..00000000000 --- 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 c3e03fc32ca..00000000000 --- 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 90bd920ceff..00000000000 --- 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 db1e01feda6..00000000000 --- 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 929e82a146d..00000000000 --- 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 1cd4a2abbe2..00000000000 --- a/tests/fixtures/config-initializer/tests/doubleQuotes.js +++ /dev/null @@ -1 +0,0 @@ -var foo = "doubleQuotes"; diff --git a/tests/lib/init/autoconfig.js b/tests/lib/init/autoconfig.js deleted file mode 100644 index 3db6a63645f..00000000000 --- a/tests/lib/init/autoconfig.js +++ /dev/null @@ -1,385 +0,0 @@ -/** - * @fileoverview Used for creating a suggested configuration based on project code. - * @author Ian VanSchooten - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - autoconfig = require("../../../lib/init/autoconfig"), - sourceCodeUtils = require("../../../lib/init/source-code-utils"), - baseDefaultOptions = require("../../../conf/default-cli-options"), - recommendedConfig = require("../../conf/eslint-recommended"); - -const defaultOptions = Object.assign({}, baseDefaultOptions, { cwd: process.cwd() }); - -//------------------------------------------------------------------------------ -// Data -//------------------------------------------------------------------------------ - -const SOURCE_CODE_FIXTURE_FILENAME = "./tests/fixtures/autoconfig/source.js"; -const CONFIG_COMMENTS_FILENAME = "./tests/fixtures/autoconfig/source-with-comments.js"; -const SEVERITY = 2; - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -const rulesConfig = { - semi: [SEVERITY, [SEVERITY, "always"], [SEVERITY, "never"]], - "semi-spacing": [SEVERITY, - [SEVERITY, { before: true, after: true }], - [SEVERITY, { before: true, after: false }], - [SEVERITY, { before: false, after: true }], - [SEVERITY, { before: false, after: false }] - ], - quotes: [SEVERITY, - [SEVERITY, "single"], - [SEVERITY, "double"], - [SEVERITY, "backtick"], - [SEVERITY, "single", "avoid-escape"], - [SEVERITY, "double", "avoid-escape"], - [SEVERITY, "backtick", "avoid-escape"]] -}; - -const errorRulesConfig = { - "no-unused-vars": [SEVERITY], - "semi-spacing": [SEVERITY, - [SEVERITY, { before: true, after: true }], - [SEVERITY, { before: true, after: false }], - [SEVERITY, { before: false, after: true }], - [SEVERITY, { before: false, after: false }] - ] -}; - -describe("autoconfig", () => { - - describe("Registry", () => { - - it("should set up a registry for rules in a provided rulesConfig", () => { - const expectedRules = Object.keys(rulesConfig); - const registry = new autoconfig.Registry(rulesConfig); - - assert.strictEqual(Object.keys(registry.rules).length, 3); - assert.sameMembers(Object.keys(registry.rules), expectedRules); - assert.isArray(registry.rules.semi); - assert.isArray(registry.rules["semi-spacing"]); - assert.isArray(registry.rules.quotes); - assert.lengthOf(registry.rules.semi, 3); - assert.lengthOf(registry.rules["semi-spacing"], 5); - assert.lengthOf(registry.rules.quotes, 7); - }); - - it("should not have any rules if constructed without a config argument", () => { - const registry = new autoconfig.Registry(); - - assert.isObject(registry.rules); - assert.lengthOf(Object.keys(registry.rules), 0); - }); - - it("should create registryItems for each rule with the proper keys", () => { - const registry = new autoconfig.Registry(rulesConfig); - - assert.isObject(registry.rules.semi[0]); - assert.isObject(registry.rules["semi-spacing"][0]); - assert.isObject(registry.rules.quotes[0]); - assert.property(registry.rules.semi[0], "config"); - assert.property(registry.rules.semi[0], "specificity"); - assert.property(registry.rules.semi[0], "errorCount"); - }); - - it("should populate the config property correctly", () => { - const registry = new autoconfig.Registry(rulesConfig); - - assert.strictEqual(registry.rules.quotes[0].config, SEVERITY); - assert.deepStrictEqual(registry.rules.quotes[1].config, [SEVERITY, "single"]); - assert.deepStrictEqual(registry.rules.quotes[2].config, [SEVERITY, "double"]); - assert.deepStrictEqual(registry.rules.quotes[3].config, [SEVERITY, "backtick"]); - assert.deepStrictEqual(registry.rules.quotes[4].config, [SEVERITY, "single", "avoid-escape"]); - assert.deepStrictEqual(registry.rules.quotes[5].config, [SEVERITY, "double", "avoid-escape"]); - assert.deepStrictEqual(registry.rules.quotes[6].config, [SEVERITY, "backtick", "avoid-escape"]); - }); - - it("should assign the correct specificity", () => { - const registry = new autoconfig.Registry(rulesConfig); - - assert.strictEqual(registry.rules.quotes[0].specificity, 1); - assert.strictEqual(registry.rules.quotes[1].specificity, 2); - assert.strictEqual(registry.rules.quotes[6].specificity, 3); - }); - - it("should initially leave the errorCount as undefined", () => { - const registry = new autoconfig.Registry(rulesConfig); - - assert.isUndefined(registry.rules.quotes[0].errorCount); - assert.isUndefined(registry.rules.quotes[1].errorCount); - assert.isUndefined(registry.rules.quotes[6].errorCount); - }); - - describe("populateFromCoreRules()", () => { - - it("should add core rules to registry", () => { - const registry = new autoconfig.Registry(); - - registry.populateFromCoreRules(); - const finalRuleCount = Object.keys(registry.rules).length; - - assert(finalRuleCount > 0); - assert.include(Object.keys(registry.rules), "eqeqeq"); - }); - - it("should not add deprecated rules", () => { - const registry = new autoconfig.Registry(); - - registry.populateFromCoreRules(); - - const { rules } = registry; - - assert.notProperty(rules, "id-blacklist"); - assert.notProperty(rules, "no-negated-in-lhs"); - assert.notProperty(rules, "no-process-exit"); - assert.notProperty(rules, "no-spaced-func"); - assert.notProperty(rules, "prefer-reflect"); - }); - - it("should not add duplicate rules", () => { - const registry = new autoconfig.Registry(rulesConfig); - - registry.populateFromCoreRules(); - const semiCount = Object.keys(registry.rules).filter(ruleId => ruleId === "semi").length; - - assert.strictEqual(semiCount, 1); - }); - }); - - describe("buildRuleSets()", () => { - let ruleSets; - - beforeEach(() => { - const registry = new autoconfig.Registry(rulesConfig); - - ruleSets = registry.buildRuleSets(); - }); - - it("should create an array of rule configuration sets", () => { - assert.isArray(ruleSets); - }); - - it("should include configs for each rule (at least for the first set)", () => { - assert.sameMembers(Object.keys(ruleSets[0]), ["semi", "semi-spacing", "quotes"]); - }); - - it("should create the first set from default rule configs (severity only)", () => { - assert.deepStrictEqual(ruleSets[0], { semi: SEVERITY, "semi-spacing": SEVERITY, quotes: SEVERITY }); - }); - - it("should create as many ruleSets as the highest number of configs in a rule", () => { - - // `quotes` has 7 possible configurations - assert.lengthOf(ruleSets, 7); - }); - }); - - describe("lintSourceCode()", () => { - let registry; - - beforeEach(() => { - const config = { ignore: false }; - const sourceCode = sourceCodeUtils.getSourceCodeOfFiles(SOURCE_CODE_FIXTURE_FILENAME, config); - - registry = new autoconfig.Registry(rulesConfig); - registry = registry.lintSourceCode(sourceCode, defaultOptions); - }); - - it("should populate the errorCount of all registryItems", () => { - const expectedRules = ["semi", "semi-spacing", "quotes"]; - - assert.sameMembers(Object.keys(registry.rules), expectedRules); - expectedRules.forEach(ruleId => { - assert(registry.rules[ruleId].length > 0); - registry.rules[ruleId].forEach(conf => { - assert.isNumber(conf.errorCount); - }); - }); - }); - - it("should correctly set the error count of configurations", () => { - assert.strictEqual(registry.rules.semi[0].config, SEVERITY); - assert.strictEqual(registry.rules.semi[0].errorCount, 0); - assert.deepStrictEqual(registry.rules.semi[2].config, [SEVERITY, "never"]); - assert.strictEqual(registry.rules.semi[2].errorCount, 3); - }); - - it("should respect inline eslint config comments (and not crash when they make linting errors)", () => { - const config = { ignore: false }; - const sourceCode = sourceCodeUtils.getSourceCodeOfFiles(CONFIG_COMMENTS_FILENAME, config); - const expectedRegistry = [ - { config: 2, specificity: 1, errorCount: 3 }, - { config: [2, "always"], specificity: 2, errorCount: 3 }, - { config: [2, "never"], specificity: 2, errorCount: 3 } - ]; - - registry = new autoconfig.Registry(rulesConfig); - registry = registry.lintSourceCode(sourceCode, defaultOptions); - - assert.deepStrictEqual(registry.rules.semi, expectedRegistry); - }); - }); - - describe("stripFailingConfigs()", () => { - let registry; - - beforeEach(() => { - const config = { ignore: false }; - const sourceCode = sourceCodeUtils.getSourceCodeOfFiles(SOURCE_CODE_FIXTURE_FILENAME, config); - - registry = new autoconfig.Registry(rulesConfig); - registry = registry.lintSourceCode(sourceCode, defaultOptions); - registry = registry.stripFailingConfigs(); - }); - - it("should remove all registryItems with a non-zero errorCount", () => { - assert.lengthOf(registry.rules.semi, 2); - assert.lengthOf(registry.rules["semi-spacing"], 3); - assert.lengthOf(registry.rules.quotes, 1); - registry.rules.semi.forEach(registryItem => { - assert.strictEqual(registryItem.errorCount, 0); - }); - registry.rules["semi-spacing"].forEach(registryItem => { - assert.strictEqual(registryItem.errorCount, 0); - }); - registry.rules.quotes.forEach(registryItem => { - assert.strictEqual(registryItem.errorCount, 0); - }); - }); - }); - - describe("getFailingRulesRegistry()", () => { - let failingRegistry; - - beforeEach(() => { - const config = { ignore: false }; - const sourceCode = sourceCodeUtils.getSourceCodeOfFiles(SOURCE_CODE_FIXTURE_FILENAME, config); - let registry = new autoconfig.Registry(errorRulesConfig); - - registry = registry.lintSourceCode(sourceCode, defaultOptions); - failingRegistry = registry.getFailingRulesRegistry(); - }); - - it("should return a registry with no registryItems with an errorCount of zero", () => { - const failingRules = Object.keys(failingRegistry.rules); - - assert.deepStrictEqual(failingRules, ["no-unused-vars"]); - assert.lengthOf(failingRegistry.rules["no-unused-vars"], 1); - assert(failingRegistry.rules["no-unused-vars"][0].errorCount > 0); - }); - }); - - describe("createConfig()", () => { - let createdConfig; - - beforeEach(() => { - const config = { ignore: false }; - const sourceCode = sourceCodeUtils.getSourceCodeOfFiles(SOURCE_CODE_FIXTURE_FILENAME, config); - let registry = new autoconfig.Registry(rulesConfig); - - registry = registry.lintSourceCode(sourceCode, defaultOptions); - registry = registry.stripFailingConfigs(); - createdConfig = registry.createConfig(); - }); - - it("should create a config with a rules property", () => { - assert.property(createdConfig, "rules"); - }); - - it("should add rules which have only one registryItem to the config", () => { - const configuredRules = Object.keys(createdConfig.rules); - - assert.deepStrictEqual(configuredRules, ["quotes"]); - }); - - it("should set the configuration of the rule to the registryItem's `config` value", () => { - assert.deepStrictEqual(createdConfig.rules.quotes, [2, "double", "avoid-escape"]); - }); - - it("should not care how many errors the config has", () => { - const config = { ignore: false }; - const sourceCode = sourceCodeUtils.getSourceCodeOfFiles(SOURCE_CODE_FIXTURE_FILENAME, config); - let registry = new autoconfig.Registry(errorRulesConfig); - - registry = registry.lintSourceCode(sourceCode, defaultOptions); - const failingRegistry = registry.getFailingRulesRegistry(); - - createdConfig = failingRegistry.createConfig(); - const configuredRules = Object.keys(createdConfig.rules); - - assert.deepStrictEqual(configuredRules, ["no-unused-vars"]); - }); - }); - - describe("filterBySpecificity()", () => { - let registry; - - beforeEach(() => { - registry = new autoconfig.Registry(rulesConfig); - }); - - it("should return a registry where all configs have a desired specificity", () => { - const filteredRegistry1 = registry.filterBySpecificity(1); - const filteredRegistry2 = registry.filterBySpecificity(2); - const filteredRegistry3 = registry.filterBySpecificity(3); - - assert.lengthOf(filteredRegistry1.rules.semi, 1); - assert.lengthOf(filteredRegistry1.rules["semi-spacing"], 1); - assert.lengthOf(filteredRegistry1.rules.quotes, 1); - assert.lengthOf(filteredRegistry2.rules.semi, 2); - assert.lengthOf(filteredRegistry2.rules["semi-spacing"], 4); - assert.lengthOf(filteredRegistry2.rules.quotes, 3); - assert.lengthOf(filteredRegistry3.rules.quotes, 3); - }); - }); - }); - - describe("extendFromRecommended()", () => { - it("should return a configuration which has `extends` key with Array type value", () => { - const oldConfig = { extends: [], rules: {} }; - const newConfig = autoconfig.extendFromRecommended(oldConfig); - - assert.exists(newConfig.extends); - assert.isArray(newConfig.extends); - }); - - it("should return a configuration which has array property `extends`", () => { - const oldConfig = { extends: [], rules: {} }; - const newConfig = autoconfig.extendFromRecommended(oldConfig); - - assert.include(newConfig.extends, "eslint:recommended"); - }); - - it("should return a configuration which preserves the previous extending configurations", () => { - const oldConfig = { extends: ["previous:configuration1", "previous:configuration2"], rules: {} }; - const newConfig = autoconfig.extendFromRecommended(oldConfig); - - assert.includeMembers(newConfig.extends, oldConfig.extends); - }); - - it("should return a configuration which has `eslint:recommended` at the first of `extends`", () => { - const oldConfig = { extends: ["previous:configuration1", "previous:configuration2"], rules: {} }; - const newConfig = autoconfig.extendFromRecommended(oldConfig); - const [firstExtendInNewConfig] = newConfig.extends; - - assert.strictEqual(firstExtendInNewConfig, "eslint:recommended"); - }); - - it("should return a configuration which not includes rules configured in `eslint:recommended`", () => { - const oldConfig = { extends: [], rules: { ...recommendedConfig.rules } }; - const newConfig = autoconfig.extendFromRecommended(oldConfig); - - assert.notInclude(newConfig.rules, oldConfig.rules); - }); - }); -}); diff --git a/tests/lib/init/config-file.js b/tests/lib/init/config-file.js deleted file mode 100644 index 50a9af1d507..00000000000 --- a/tests/lib/init/config-file.js +++ /dev/null @@ -1,159 +0,0 @@ -/** - * @fileoverview Tests for ConfigFile - * @author Nicholas C. Zakas - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - sinon = require("sinon"), - path = require("path"), - yaml = require("js-yaml"), - espree = require("espree"), - ConfigFile = require("../../../lib/init/config-file"), - { CLIEngine } = require("../../../lib/cli-engine"); - -const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); - -//------------------------------------------------------------------------------ -// 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) { - 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`, () => { - const fakeFS = { - writeFileSync: () => {} - }; - - sinon.mock(fakeFS).expects("writeFileSync").withExactArgs( - filename, - sinon.match(value => !!validate(value)), - "utf8" - ); - - const StubbedConfigFile = proxyquire("../../../lib/init/config-file", { - fs: fakeFS - }); - - StubbedConfigFile.write(config, filename); - }); - - it("should include a newline character at EOF", () => { - const fakeFS = { - writeFileSync: () => {} - }; - - sinon.mock(fakeFS).expects("writeFileSync").withExactArgs( - filename, - sinon.match(value => value.endsWith("\n")), - "utf8" - ); - - const StubbedConfigFile = proxyquire("../../../lib/init/config-file", { - fs: fakeFS - }); - - StubbedConfigFile.write(config, filename); - }); - }); - - it("should make sure js config files match linting rules", () => { - const fakeFS = { - writeFileSync: () => {} - }; - - const singleQuoteConfig = { - rules: { - quotes: [2, "single"] - } - }; - - sinon.mock(fakeFS).expects("writeFileSync").withExactArgs( - "test-config.js", - sinon.match(value => !value.includes("\"")), - "utf8" - ); - - const StubbedConfigFile = proxyquire("../../../lib/init/config-file", { - fs: fakeFS - }); - - StubbedConfigFile.write(singleQuoteConfig, "test-config.js"); - }); - - it("should still write a js config file even if linting fails", () => { - const fakeFS = { - writeFileSync: () => {} - }; - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ - baseConfig: config, - fix: true, - useEslintrc: false - })); - - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnText").throws(); - - sinon.mock(fakeFS).expects("writeFileSync").once(); - - const StubbedConfigFile = proxyquire("../../../lib/init/config-file", { - fs: fakeFS, - "../cli-engine": { CLIEngine: fakeCLIEngine } - }); - - assert.throws(() => { - StubbedConfigFile.write(config, "test-config.js"); - }); - }); - - it("should throw error if file extension is not valid", () => { - assert.throws(() => { - ConfigFile.write({}, getFixturePath("yaml/.eslintrc.class")); - }, /write to unknown file type/u); - }); - }); -}); diff --git a/tests/lib/init/config-initializer.js b/tests/lib/init/config-initializer.js deleted file mode 100644 index 81e4e52faaa..00000000000 --- a/tests/lib/init/config-initializer.js +++ /dev/null @@ -1,577 +0,0 @@ -/** - * @fileoverview Tests for configInitializer. - * @author Ilya Volodin - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - fs = require("fs"), - path = require("path"), - os = require("os"), - sinon = require("sinon"), - sh = require("shelljs"), - espree = require("espree"), - autoconfig = require("../../../lib/init/autoconfig"), - npmUtils = require("../../../lib/init/npm-utils"); - -const originalDir = process.cwd(); -const proxyquire = require("proxyquire").noPreserveCache(); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -let answers = {}; -let pkgJSONContents = {}; -let pkgJSONPath = ""; - -describe("configInitializer", () => { - - let fixtureDir, - npmCheckStub, - npmInstallStub, - npmFetchPeerDependenciesStub, - init, - localESLintVersion = null; - - const log = { - info: sinon.spy(), - error: sinon.spy() - }; - const requireStubs = { - "../shared/logging": log, - "../shared/relative-module-resolver": { - resolve() { - if (localESLintVersion) { - return `local-eslint-${localESLintVersion}`; - } - throw new Error("Cannot find module"); - } - }, - "local-eslint-3.18.0": { linter: { version: "3.18.0" }, "@noCallThru": true }, - "local-eslint-3.19.0": { linter: { version: "3.19.0" }, "@noCallThru": true }, - "local-eslint-4.0.0": { linter: { version: "4.0.0" }, "@noCallThru": true } - }; - - /** - * Returns the path inside of the fixture directory. - * @param {...string} args file path segments. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFixturePath(...args) { - const filepath = path.join(fixtureDir, ...args); - - try { - return fs.realpathSync(filepath); - } catch { - return filepath; - } - } - - // copy into clean area so as not to get "infected" by this project's .eslintrc files - before(() => { - fixtureDir = path.join(os.tmpdir(), "eslint/fixtures/config-initializer"); - sh.mkdir("-p", fixtureDir); - sh.cp("-r", "./tests/fixtures/config-initializer/.", fixtureDir); - fixtureDir = fs.realpathSync(fixtureDir); - }); - - beforeEach(() => { - npmInstallStub = sinon.stub(npmUtils, "installSyncSaveDev"); - npmCheckStub = sinon.stub(npmUtils, "checkDevDeps").callsFake(packages => packages.reduce((status, pkg) => { - status[pkg] = false; - return status; - }, {})); - npmFetchPeerDependenciesStub = sinon - .stub(npmUtils, "fetchPeerDependencies") - .returns({ - eslint: "^3.19.0", - "eslint-plugin-jsx-a11y": "^5.0.1", - "eslint-plugin-import": "^2.2.0", - "eslint-plugin-react": "^7.0.1" - }); - init = proxyquire("../../../lib/init/config-initializer", requireStubs); - }); - - afterEach(() => { - log.info.resetHistory(); - log.error.resetHistory(); - npmInstallStub.restore(); - npmCheckStub.restore(); - npmFetchPeerDependenciesStub.restore(); - }); - - 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 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, espree.latestEcmaVersion); - 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.ecmaFeatures.jsx, true); - assert.strictEqual(config.parserOptions.ecmaVersion, espree.latestEcmaVersion); - assert.deepStrictEqual(config.plugins, ["react"]); - }); - - it("should enable vue plugin", () => { - answers.framework = "vue"; - const config = init.processAnswers(answers); - - assert.strictEqual(config.parserOptions.ecmaVersion, espree.latestEcmaVersion); - assert.deepStrictEqual(config.plugins, ["vue"]); - assert.deepStrictEqual(config.extends, ["eslint:recommended", "plugin:vue/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.deepStrictEqual(config.extends, ["eslint:recommended", "plugin:vue/essential", "plugin:@typescript-eslint/recommended"]); - assert.strictEqual(config.parserOptions.parser, "@typescript-eslint/parser"); - assert.deepStrictEqual(config.plugins, ["vue", "@typescript-eslint"]); - }); - - 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); - }); - }); - - 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 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)", () => { - describe("if local ESLint is not found,", () => { - before(() => { - localESLintVersion = null; - }); - - it("should return false.", () => { - const result = init.hasESLintVersionConflict({ styleguide: "airbnb" }); - - assert.strictEqual(result, false); - }); - }); - - describe("if local ESLint is 3.19.0,", () => { - before(() => { - localESLintVersion = "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(() => { - localESLintVersion = "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(() => { - localESLintVersion = "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/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("auto", () => { - const completeSpy = sinon.spy(); - let config; - - before(() => { - const patterns = [ - getFixturePath("lib"), - getFixturePath("tests") - ].join(" "); - - answers = { - purpose: "style", - source: "auto", - patterns, - env: ["browser"], - format: "JSON" - }; - - sinon.stub(console, "log"); // necessary to replace, because of progress bar - - process.chdir(fixtureDir); - config = init.processAnswers(answers); - sinon.restore(); - }); - - after(() => { - sinon.restore(); - }); - - afterEach(() => { - process.chdir(originalDir); - sinon.restore(); - }); - - it("should create a config", () => { - assert.isTrue(completeSpy.notCalled); - assert.ok(config); - }); - - it("should create the config based on examined files", () => { - assert.deepStrictEqual(config.rules.quotes, ["error", "double"]); - assert.strictEqual(config.rules.semi, "off"); - }); - - it("should extend and not disable recommended rules", () => { - assert.strictEqual(config.extends, "eslint:recommended"); - assert.notProperty(config.rules, "no-debugger"); - }); - - it("should not include deprecated rules", () => { - assert.notProperty(config.rules, "id-blacklist"); - assert.notProperty(config.rules, "no-negated-in-lhs"); - assert.notProperty(config.rules, "no-process-exit"); - assert.notProperty(config.rules, "no-spaced-func"); - assert.notProperty(config.rules, "prefer-reflect"); - }); - - it("should support new ES features if using later ES version", () => { - const filename = getFixturePath("new-es-features"); - - answers.patterns = filename; - answers.ecmaVersion = 2017; - process.chdir(fixtureDir); - config = init.processAnswers(answers); - }); - - it("should throw on fatal parsing error", () => { - const filename = getFixturePath("parse-error"); - - sinon.stub(autoconfig, "extendFromRecommended"); - answers.patterns = filename; - process.chdir(fixtureDir); - assert.throws(() => { - config = init.processAnswers(answers); - }, "Parsing error: Unexpected token ;"); - }); - - it("should throw if no files are matched from patterns", () => { - sinon.stub(autoconfig, "extendFromRecommended"); - answers.patterns = "not-a-real-filename"; - process.chdir(fixtureDir); - assert.throws(() => { - config = init.processAnswers(answers); - }, "No files matching 'not-a-real-filename' were found."); - }); - }); - }); - - 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", () => { - answers.format = "JavaScript"; - - const config = init.processAnswers(answers); - const filePath = path.resolve(fixtureDir, ".eslintrc.js"); - - 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.yml", () => { - answers.format = "YAML"; - - const config = init.processAnswers(answers); - const filePath = path.resolve(fixtureDir, ".eslintrc.yml"); - - fs.writeFileSync(pkgJSONPath, JSON.stringify(pkgJSONContents)); - - 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", () => { - 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"); - - 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'", () => { - 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"); - - init.writeFile(config, answers.format); - - assert.isTrue(fs.existsSync(filePath)); - - fs.unlinkSync(filePath); - fs.unlinkSync(pkgJSONPath); - }); - }); -}); diff --git a/tests/lib/init/npm-utils.js b/tests/lib/init/npm-utils.js deleted file mode 100644 index 8465796a367..00000000000 --- a/tests/lib/init/npm-utils.js +++ /dev/null @@ -1,228 +0,0 @@ -/** - * @fileoverview Tests for rule fixer. - * @author Ian VanSchooten - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const - assert = require("chai").assert, - spawn = require("cross-spawn"), - sinon = require("sinon"), - npmUtils = require("../../../lib/init/npm-utils"), - log = require("../../../lib/shared/logging"), - { defineInMemoryFs } = require("../../_utils"); - -const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Import `npm-utils` with the in-memory file system. - * @param {Object} files The file definitions. - * @returns {Object} `npm-utils`. - */ -function requireNpmUtilsWithInMemoryFileSystem(files) { - const fs = defineInMemoryFs({ files }); - - return proxyquire("../../../lib/init/npm-utils", { fs }); -} - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("npmUtils", () => { - afterEach(() => { - sinon.verifyAndRestore(); - }); - - describe("checkDevDeps()", () => { - let installStatus; - - before(() => { - installStatus = npmUtils.checkDevDeps(["debug", "mocha", "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); - }); - - it("should not find non-dependencies", () => { - assert.isFalse(installStatus.notarealpackage); - }); - - it("should not find nested dependencies", () => { - assert.isFalse(installStatus.jshint); - }); - - it("should return false for a single, non-existent package", () => { - installStatus = npmUtils.checkDevDeps(["notarealpackage"]); - assert.isFalse(installStatus.notarealpackage); - }); - - it("should handle missing devDependencies key", () => { - const stubbedNpmUtils = requireNpmUtilsWithInMemoryFileSystem({ - "package.json": JSON.stringify({ private: true, dependencies: {} }) - }); - - // Should not throw. - stubbedNpmUtils.checkDevDeps(["some-package"]); - }); - - it("should throw with message when parsing invalid package.json", () => { - const stubbedNpmUtils = requireNpmUtilsWithInMemoryFileSystem({ - "package.json": "{ \"not: \"valid json\" }" - }); - - assert.throws(() => { - try { - stubbedNpmUtils.checkDevDeps(["some-package"]); - } catch (error) { - assert.strictEqual(error.messageTemplate, "failed-to-read-json"); - throw error; - } - }, "SyntaxError: Unexpected token v"); - }); - }); - - describe("checkDeps()", () => { - let installStatus; - - before(() => { - installStatus = npmUtils.checkDeps(["debug", "mocha", "notarealpackage", "jshint"]); - }); - - it("should find a direct dependency of the project", () => { - assert.isTrue(installStatus.debug); - }); - - it("should not find a dev dependency of the project", () => { - assert.isFalse(installStatus.mocha); - }); - - it("should not find non-dependencies", () => { - assert.isFalse(installStatus.notarealpackage); - }); - - it("should not find nested dependencies", () => { - assert.isFalse(installStatus.jshint); - }); - - it("should return false for a single, non-existent package", () => { - installStatus = npmUtils.checkDeps(["notarealpackage"]); - assert.isFalse(installStatus.notarealpackage); - }); - - it("should throw if no package.json can be found", () => { - assert.throws(() => { - installStatus = npmUtils.checkDeps(["notarealpackage"], "/fakepath"); - }, "Could not find a package.json file"); - }); - - it("should handle missing dependencies key", () => { - const stubbedNpmUtils = requireNpmUtilsWithInMemoryFileSystem({ - "package.json": JSON.stringify({ private: true, devDependencies: {} }) - }); - - // Should not throw. - stubbedNpmUtils.checkDeps(["some-package"]); - }); - - it("should throw with message when parsing invalid package.json", () => { - const stubbedNpmUtils = requireNpmUtilsWithInMemoryFileSystem({ - "package.json": "{ \"not: \"valid json\" }" - }); - - assert.throws(() => { - try { - stubbedNpmUtils.checkDeps(["some-package"]); - } catch (error) { - assert.strictEqual(error.messageTemplate, "failed-to-read-json"); - throw error; - } - }, "SyntaxError: Unexpected token v"); - }); - }); - - describe("checkPackageJson()", () => { - it("should return true if package.json exists", () => { - const stubbedNpmUtils = requireNpmUtilsWithInMemoryFileSystem({ - "package.json": "{ \"file\": \"contents\" }" - }); - - assert.strictEqual(stubbedNpmUtils.checkPackageJson(), true); - }); - - it("should return false if package.json does not exist", () => { - const stubbedNpmUtils = requireNpmUtilsWithInMemoryFileSystem({}); - - assert.strictEqual(stubbedNpmUtils.checkPackageJson(), false); - }); - }); - - describe("installSyncSaveDev()", () => { - it("should invoke npm to install a single desired package", () => { - const stub = sinon.stub(spawn, "sync").returns({ stdout: "" }); - - npmUtils.installSyncSaveDev("desired-package"); - assert(stub.calledOnce); - assert.strictEqual(stub.firstCall.args[0], "npm"); - assert.deepStrictEqual(stub.firstCall.args[1], ["i", "--save-dev", "desired-package"]); - stub.restore(); - }); - - it("should accept an array of packages to install", () => { - const stub = sinon.stub(spawn, "sync").returns({ stdout: "" }); - - npmUtils.installSyncSaveDev(["first-package", "second-package"]); - assert(stub.calledOnce); - assert.strictEqual(stub.firstCall.args[0], "npm"); - assert.deepStrictEqual(stub.firstCall.args[1], ["i", "--save-dev", "first-package", "second-package"]); - stub.restore(); - }); - - it("should log an error message if npm throws ENOENT error", () => { - const logErrorStub = sinon.stub(log, "error"); - const npmUtilsStub = sinon.stub(spawn, "sync").returns({ error: { code: "ENOENT" } }); - - npmUtils.installSyncSaveDev("some-package"); - - assert(logErrorStub.calledOnce); - - logErrorStub.restore(); - npmUtilsStub.restore(); - }); - }); - - describe("fetchPeerDependencies()", () => { - it("should execute 'npm show --json peerDependencies' command", () => { - const stub = sinon.stub(spawn, "sync").returns({ stdout: "" }); - - npmUtils.fetchPeerDependencies("desired-package"); - assert(stub.calledOnce); - assert.strictEqual(stub.firstCall.args[0], "npm"); - assert.deepStrictEqual(stub.firstCall.args[1], ["show", "--json", "desired-package", "peerDependencies"]); - stub.restore(); - }); - - it("should return null if npm throws ENOENT error", () => { - const stub = sinon.stub(spawn, "sync").returns({ error: { code: "ENOENT" } }); - - const peerDependencies = npmUtils.fetchPeerDependencies("desired-package"); - - assert.isNull(peerDependencies); - - stub.restore(); - }); - }); -}); diff --git a/tests/lib/init/source-code-utils.js b/tests/lib/init/source-code-utils.js deleted file mode 100644 index 994d23d3168..00000000000 --- a/tests/lib/init/source-code-utils.js +++ /dev/null @@ -1,250 +0,0 @@ -/** - * @fileoverview Tests for source-code-util. - * @author Ian VanSchooten - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const path = require("path"), - fs = require("fs"), - os = require("os"), - assert = require("chai").assert, - sinon = require("sinon"), - sh = require("shelljs"), - { SourceCode } = require("../../../lib/source-code"); - -const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); -const originalDir = process.cwd(); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("SourceCodeUtil", () => { - - let fixtureDir, - getSourceCodeOfFiles; - - /** - * Returns the path inside of the fixture directory. - * @param {...string} args file path segments. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFixturePath(...args) { - let filepath = path.join(fixtureDir, ...args); - - try { - filepath = fs.realpathSync(filepath); - return filepath; - } catch { - return filepath; - } - } - - const log = { - info: sinon.spy(), - error: sinon.spy() - }; - const requireStubs = { - "../logging": log - }; - - // copy into clean area so as not to get "infected" by this project's .eslintrc files - before(() => { - fixtureDir = `${os.tmpdir()}/eslint/fixtures/source-code-util`; - sh.mkdir("-p", fixtureDir); - sh.cp("-r", "./tests/fixtures/source-code-util/.", fixtureDir); - fixtureDir = fs.realpathSync(fixtureDir); - }); - - beforeEach(() => { - getSourceCodeOfFiles = proxyquire("../../../lib/init/source-code-utils", requireStubs).getSourceCodeOfFiles; - }); - - afterEach(() => { - log.info.resetHistory(); - log.error.resetHistory(); - }); - - after(() => { - sh.rm("-r", fixtureDir); - }); - - describe("getSourceCodeOfFiles()", () => { - - it("should handle single string filename arguments", () => { - const filename = getFixturePath("foo.js"); - const sourceCode = getSourceCodeOfFiles(filename, { cwd: fixtureDir }); - - assert.isObject(sourceCode); - }); - - it("should accept an array of string filenames", () => { - const fooFilename = getFixturePath("foo.js"); - const barFilename = getFixturePath("bar.js"); - const sourceCode = getSourceCodeOfFiles([fooFilename, barFilename], { cwd: fixtureDir }); - - assert.isObject(sourceCode); - }); - - it("should accept a glob argument", () => { - const glob = getFixturePath("*.js"); - const filename = getFixturePath("foo.js"); - const sourceCode = getSourceCodeOfFiles(glob, { cwd: fixtureDir }); - - assert.isObject(sourceCode); - assert.property(sourceCode, filename); - }); - - it("should accept a relative filename", () => { - const filename = "foo.js"; - const sourceCode = getSourceCodeOfFiles(filename, { cwd: fixtureDir }); - - assert.isObject(sourceCode); - assert.property(sourceCode, getFixturePath(filename)); - }); - - it("should accept a relative path to a file in a parent directory", () => { - const filename = "../foo.js"; - const sourceCode = getSourceCodeOfFiles(filename, { cwd: getFixturePath("nested") }); - - assert.isObject(sourceCode); - assert.property(sourceCode, getFixturePath("foo.js")); - }); - - it("should accept a callback", () => { - const filename = getFixturePath("foo.js"); - const spy = sinon.spy(); - - process.chdir(fixtureDir); - getSourceCodeOfFiles(filename, {}, spy); - process.chdir(originalDir); - assert(spy.calledOnce); - }); - - it("should call the callback with total number of files being processed", () => { - const filename = getFixturePath("foo.js"); - const spy = sinon.spy(); - - process.chdir(fixtureDir); - getSourceCodeOfFiles(filename, {}, spy); - process.chdir(originalDir); - assert.strictEqual(spy.firstCall.args[0], 1); - }); - - it("should create an object with located filenames as keys", () => { - const fooFilename = getFixturePath("foo.js"); - const barFilename = getFixturePath("bar.js"); - const sourceCode = getSourceCodeOfFiles([fooFilename, barFilename], { cwd: fixtureDir }); - - assert.property(sourceCode, fooFilename); - assert.property(sourceCode, barFilename); - }); - - it("should should not include non-existent filenames in results", () => { - const filename = getFixturePath("missing.js"); - - assert.throws(() => { - getSourceCodeOfFiles(filename, { cwd: fixtureDir }); - }, `No files matching '${filename}' were found.`); - }); - - it("should throw for files with parsing errors", () => { - const filename = getFixturePath("parse-error", "parse-error.js"); - - assert.throw(() => { - getSourceCodeOfFiles(filename, { cwd: fixtureDir }); - }, /Parsing error: Unexpected token ;/u); - - }); - - it("should obtain the sourceCode of a file", () => { - const filename = getFixturePath("foo.js"); - const sourceCode = getSourceCodeOfFiles(filename, { cwd: fixtureDir }); - - assert.isObject(sourceCode); - assert.instanceOf(sourceCode[filename], SourceCode); - }); - - it("should obtain the sourceCode of JSX files", () => { - const filename = getFixturePath("jsx", "foo.jsx"); - const options = { - cwd: fixtureDir, - parserOptions: { - ecmaFeatures: { - jsx: true - } - } - }; - const sourceCode = getSourceCodeOfFiles(filename, options); - - assert.isObject(sourceCode); - assert.instanceOf(sourceCode[filename], SourceCode); - }); - - it("should honor .eslintignore files by default", () => { - const glob = getFixturePath("*.js"); - const unignoredFilename = getFixturePath("foo.js"); - const ignoredFilename = getFixturePath("ignored.js"); - const sourceCode = getSourceCodeOfFiles(glob, { cwd: fixtureDir }); - - assert.property(sourceCode, unignoredFilename); - assert.notProperty(sourceCode, ignoredFilename); - }); - - it("should obtain the sourceCode of all files in a specified folder", () => { - const folder = getFixturePath("nested"); - const fooFile = getFixturePath("nested/foo.js"); - const barFile = getFixturePath("nested/bar.js"); - const sourceCode = getSourceCodeOfFiles(folder, { cwd: fixtureDir }); - - assert.strictEqual(Object.keys(sourceCode).length, 2); - assert.instanceOf(sourceCode[fooFile], SourceCode); - assert.instanceOf(sourceCode[barFile], SourceCode); - }); - - it("should accept cli options", () => { - const pattern = getFixturePath("ext"); - const abcFile = getFixturePath("ext/foo.abc"); - const cliOptions = { extensions: [".abc"], cwd: fixtureDir }; - const sourceCode = getSourceCodeOfFiles(pattern, cliOptions); - - assert.strictEqual(Object.keys(sourceCode).length, 1); - assert.instanceOf(sourceCode[abcFile], SourceCode); - }); - - it("should execute the callback function, if provided", () => { - const callback = sinon.spy(); - const filename = getFixturePath("foo.js"); - - getSourceCodeOfFiles(filename, { cwd: fixtureDir }, callback); - assert(callback.calledOnce); - }); - - it("should execute callback function once per file", () => { - const callback = sinon.spy(); - const fooFilename = getFixturePath("foo.js"); - const barFilename = getFixturePath("bar.js"); - - getSourceCodeOfFiles([fooFilename, barFilename], { cwd: fixtureDir }, callback); - assert.strictEqual(callback.callCount, 2); - }); - - it("should call callback function with total number of files with sourceCode", () => { - const callback = sinon.spy(); - const firstFn = getFixturePath("foo.js"); - const secondFn = getFixturePath("bar.js"); - const thirdFn = getFixturePath("nested/foo.js"); - - getSourceCodeOfFiles([firstFn, secondFn, thirdFn], { cwd: fixtureDir }, callback); - assert(callback.calledWith(3)); - }); - - }); - -}); diff --git a/tests/lib/init/config-rule.js b/tests/tools/config-rule.js similarity index 98% rename from tests/lib/init/config-rule.js rename to tests/tools/config-rule.js index 425317dd2c8..c91f464c20b 100644 --- a/tests/lib/init/config-rule.js +++ b/tests/tools/config-rule.js @@ -10,9 +10,9 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, - ConfigRule = require("../../../lib/init/config-rule"), - builtInRules = require("../../../lib/rules"), - schema = require("../../fixtures/config-rule/schemas"); + ConfigRule = require("../../tools/config-rule"), + builtInRules = require("../../lib/rules"), + schema = require("../fixtures/config-rule/schemas"); //------------------------------------------------------------------------------ // Tests diff --git a/tests/tools/eslint-fuzzer.js b/tests/tools/eslint-fuzzer.js index 4723befa4e2..1c894b99595 100644 --- a/tests/tools/eslint-fuzzer.js +++ b/tests/tools/eslint-fuzzer.js @@ -8,7 +8,7 @@ const assert = require("chai").assert; const eslint = require("../.."); const espree = require("espree"); const sinon = require("sinon"); -const configRule = require("../../lib/init/config-rule"); +const configRule = require("../../tools/config-rule"); //------------------------------------------------------------------------------ // Tests diff --git a/lib/init/config-rule.js b/tools/config-rule.js similarity index 99% rename from lib/init/config-rule.js rename to tools/config-rule.js index 131e84a60c5..91e7eaef5a4 100644 --- a/lib/init/config-rule.js +++ b/tools/config-rule.js @@ -9,7 +9,7 @@ // Requirements //------------------------------------------------------------------------------ -const builtInRules = require("../rules"); +const builtInRules = require("../lib/rules"); //------------------------------------------------------------------------------ // Helpers diff --git a/tools/eslint-fuzzer.js b/tools/eslint-fuzzer.js index 6c5ed9b1bcb..d951ad3377c 100644 --- a/tools/eslint-fuzzer.js +++ b/tools/eslint-fuzzer.js @@ -13,7 +13,7 @@ const assert = require("assert"); const eslump = require("eslump"); const espree = require("espree"); const SourceCodeFixer = require("../lib/linter/source-code-fixer"); -const ruleConfigs = require("../lib/init/config-rule").createCoreRuleConfigs(true); +const ruleConfigs = require("./config-rule").createCoreRuleConfigs(true); const sampleMinimizer = require("./code-sample-minimizer"); //------------------------------------------------------------------------------