From e06f70c94b95be95a7ff13d8e92455d567f06bd5 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 13 Nov 2024 14:13:34 -0500 Subject: [PATCH 01/14] feat: Flat config extends --- designs/2024-config-extends/README.md | 1148 +++++++++++++++++++++++++ 1 file changed, 1148 insertions(+) create mode 100644 designs/2024-config-extends/README.md diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md new file mode 100644 index 00000000..6a9f971d --- /dev/null +++ b/designs/2024-config-extends/README.md @@ -0,0 +1,1148 @@ +- Start Date: 2024-11-13 +- RFC PR: (todo) +- Authors: Nicholas C. Zakas (@nzakas) +- repo: eslint/eslint + +# Flat Config Extends + +## Summary + +This proposal provides a way to implement `extends` in flat config objects. The `extends` key was an integral part of the eslintrc configuration format and was not included in flat config initially. + +## Motivation + +One of the goals of flat config was to greatly simplify the creation of config files. We eliminated a lot of the extraneous features, grouped related keys together, and settled on a single, flat array with config objects as the overall format. The intent was to turn ESLint configuration into just an array of objects that could easily be manipulated in a variety of ways, limited only by what the JavaScript language enabled. + +Now, several months after the general availability of flat config, some common complaints are emerging. + +### Loss of standarized way to expose configs + +First, it's difficult to know how a plugin exports configurations. A configuration can be an object or an array, and it may be accessible via a `configs` key or via a module specifier. Further, typescript-eslint actually [recommends](https://typescript-eslint.io/packages/typescript-eslint/#flat-config-extends) the use of their own utility to create configs, which [confuses users](https://github.com/typescript-eslint/typescript-eslint/issues/8496). That means you can have multiple ways of importing a config into your own. Here's an example: + +```js +import js from "@eslint/js"; +import tailwind from "eslint-plugin-tailwindcss"; +import reactPlugin from "eslint-plugin-react"; +import eslintPluginImportX from 'eslint-plugin-import-x' + +export default [ + js.configs.recommended, + ...tailwind.configs["flat/recommended"], + ...reactPlugin.configs.flat.recommended, + eslintPluginImportX.flatConfigs.recommended, +]; +``` + +### Confusion over `...` syntax + +As discussed in the previous section, configs may be objects or arrays, and the latter requires the use of the spread syntax (`...`) to include its contents into the overall array. We've found that JavaScript beginners don't know or understand spread syntax, making this distinction between objects and arrays even more confusing. + +### Difficult extending configs for specific file patterns + +Further adding to the confusion over objects and arrays is how to take an existing config and apply it to a subset of files. This explicitly requires you to know if something is an object or array because how you extend it varies depending on the type. For objects, you do this: + +```js +// eslint.config.js +import js from "@eslint/js"; + +export default [ + { + ...js.configs.recommended, + files: ["**/src/safe/*.js"] + } +]; +``` + +For arrays you do this: + +```js +// eslint.config.js +import exampleConfigs from "eslint-config-example"; + +export default [ + ...exampleConfigs.map(config => ({ + ...config, + files: ["**/src/safe/*.js"] + })), + + // your modifications + { + rules: { + "no-unused-vars": "warn" + } + } +]; +``` + +In both cases, users need to use the spread syntax, and in both cases, just glancing at the config object doesn't really tell you what exactly is happening. It's a lot of syntax for what used to be a single statement in eslintrc. + +## Goals + +There are two goals with this design: + +1. **Make it easier to configure ESLint.** For users, we want to reduce unnecessarily boilerplate and guesswork as much as possible. +1. **Encourage plugins to use `configs`.** For plugin authors, we want to encourage the use of a consistent entrypoint, `configs`, where users can find predefined configurations. + +## Detailed Design + +Design Summary: + +1. Allow arrays in config arrays (in addition to objects) +1. Introduce an `extends` key in config objects + +### Allow Arrays in Config Arrays + +TODO + +### Introduce an `extends` Key + +The `extends` key is an array that may contain other configurations. When used, ESLint will expand the config object into multiple config objects in order to create a flat structure to evaluate. + +#### Extending Objects + +You can pass one or more objects in `extends`, like this: + +```js +import js from "@eslint/js"; +import example from "eslint-plugin-example"; + +export default [ + + { + files: ["**/src/*.js"], + extends: [js.configs.recommended, example.configs.recommended] + } + +]; +``` + +Assuming these config objects do not contain `files` or `ignores`, ESLint will convert this config into the following: + +```js +import js from "@eslint/js"; +import example from "eslint-plugin-example"; + +export default [ + { + ...js.configs.recommended + files: ["**/src/*.js"], + }, + { + ...example.configs.recommended + files: ["**/src/*.js"], + }, +]; +``` + +If the objects in `extends` contain `files` or `ignores`, then ESLint will merge those values with the values found in the config using `extends`. For example: + +```js +import globals from "globals"; + +const config1 = { + name: "config1", + files: ["**/*.cjs"], + languageOptions: { + sourceType: "commonjs", + globals: { + ...globals.node + } + } +}; + +const config2 = { + name: "config2", + files: ["**/*.js"], + ignores: ["**/tests"], + rules: { + "no-console": "error" + } +}; + + +export default [ + + { + name: "myconfig", + files: ["**/src/*.js"], + extends: [config1, config2], + rules: { + semi: "error" + } + } + +]; +``` + +Here, the `files` keys will be combined and the `ignores` key will be inherited, resulting in a final config that looks like this: + +```js + +export default [ + + // combined files + { + name: "myconfig > config1", + files: ["**/src/*.js", "**/*.cjs"], + languageOptions: { + sourceType: "commonjs", + globals: { + ...globals.node + } + } + }, + + // combined files and inherited ignores + { + name: "myconfig > config2", + files: ["**/src/*.js", "**/*.js"], + ignores: ["**/tests"], + rules: { + "no-console": "error" + } + }, + + // original config + { + name: "myconfig", + files: ["**/src/*.js"], + rules: { + semi: "error" + } + } + +]; +``` + +Note that the `name` key is generated for calculated config objects so that it indicates the inheritance from another config that doesn't exist in the final representation of the config array. + +#### Extending Arrays + +Arrays can also be used in `extends` (to eliminate the guesswork of what type a config is). When evaluating `extends`, ESLint internally calls `.flat()` on the array and then processes the config objects as discussed in the previous example. Consider the following: + +```js +import js from "@eslint/js"; +import example from "eslint-plugin-example"; + +export default [ + + { + files: ["**/src/*.js"], + extends: [ + [js.configs.recommended, example.configs.recommended] + ] + } + +]; +``` + +This is equivalent to the following: + +```js +import js from "@eslint/js"; +import example from "eslint-plugin-example"; + +export default [ + + { + files: ["**/src/*.js"], + extends: [js.configs.recommended, example.configs.recommended] + } + +]; +``` + +The extended objects are evaluated in the same order and result in the same final config. + +#### Extending Named Configs + +While the previous examples are an improvement over current situation, we can go a step further and restore the use of named configs by allowing strings inside of the `extends` array. When provided, the string must refer to a config contained in a plugin. For example: + +```js +import js from "@eslint/js"; + +export default [ + + { + plugins: { + js + }, + files: ["**/src/*.js"], + extends: ["js/recommended"] + } + +]; +``` + +Here, `js/recommended` refers to the plugin defined as `js`. Internally, ESLint will look up the plugin with the name `js`, looks at the `configs` key, and retrieve the `recommended` key as a replacement for the string `"js/recommended"`. + +This has several advantages: + +1. It gives ESLint knowledge of where the config is coming from. When passing in an object or an array, ESLint doesn't know the config's origin. With more information, we can give better error messages when things go wrong. +1. The config is guaranteed to have a name (the string in `extends`) that can be used for debugging purposes. +1. It encourages plugin authors to use the `configs` key on their plugin in order to allow this usage. +1. It allows ESLint to modify configs before they are used (see below). + + + + + +### The `eslint.config.js` File + +The `eslint.config.js` file is a JavaScript file (there is no JSON or YAML equivalent) that exports an object: + +```js +module.exports = { + name: "name", + files: ["*.js"], + ignores: ["*.test.js"], + settings: {}, + languageOptions: { + ecmaVersion: 2020, + sourceType: "module", + globals: {}, + parser: object || "string", + parserOptions: {}, + } + linterOptions: { + reportUnusedDisableDirectives: "string" + }, + processor: object || "string", + plugins: {} + rules: {} +}; +``` + +The following keys are new to the `eslint.config.js` format: + +* `name` - Specifies the name of the config object. This is helpful for printing out debugging information and, while not required, is recommended for that reason. +* `files` - Determines the glob file patterns that this configuration applies to. These patterns can be negated by prefixing them with `!`, which effectively mimics the behavior of `excludedFiles` in `.eslintrc`. +* `ignores` - Determines the files that should not be linted using ESLint. The files specified by this array of glob patterns are subtracted from the files specified in `files`. If there is no `files` key, then `ignores` acts the same as `ignorePatterns` in `.eslintrc` files; if there is a `files` key then `ignores` acts like `excludedFiles` in `.eslintrc`. + +The following keys are specified the same as in `.eslintrc` files: + +* `settings` +* `rules` + +The following keys are specified differently than in `.eslintrc` files: + +* `plugins` - an object mapping plugin names to implementations. This replaces the `plugins` key in `.eslintrc` files and the `--rulesdir` option. +* `processor` - an object or string in `eslint.config.js` files (a string in `.eslintrc`) +* `languageOptions` - top-level grouping for all options that affect how JavaScript is interpreted by ESLint. + * `ecmaVersion` - sets the JavaScript version ESLint should use for these files. This value is copied into `parserOptions` if not already present. + * `sourceType` - sets the source type of the JavaScript code to parse. One of `module`, `script`, or `commonjs`. + * `parser` - an object or string in `eslint.config.js` files (a string in `.eslintrc`) + * `parserOptions` - an object specifying any additional parameters to be passed to the parser. + * `globals` - any additional global variables to add. +* `linterOptions` - an object for linter-specific settings + * `reportUnusedDisableDirectives` - new location for the same option name. + +Each of these keys used to require one or more strings specifying module(s) to load in `.eslintrc`. In `eslint.config.js`, these are all objects or strings (referencing an object in a plugin), requiring users to manually specify the objects to use. + +The following keys are invalid in `eslint.config.js`: + +* `extends` - replaced by config arrays +* `env` - responsibility of the user +* `overrides` - responsibility of the user +* `root` - always considered `true` + +Each of these keys represent different ways of augmenting how configuration is calculated and all of that responsibility now falls on the user. + +#### Extending Another Config + +Extending another config is accomplished by returning an array as the value of `module.exports`. Configs that come later in the array are merged with configs that come earlier in the array. For example: + +```js +module.exports = [ + require("eslint-config-standard"), + { + files: ["*.js"], + rules: { + semi: ["error", "always"] + } + } +]; +``` + +This config extends `eslint-config-standard` because that package is included first in the array. You can add multiple configs into the array to extend from multiple configs, such as: + +```js +module.exports = [ + require("eslint-config-standard"), + require("@me/eslint-config"), + { + files: ["*.js"], + rules: { + semi: ["error", "always"] + } + } +]; +``` + +Each item in a config array can be a config array. For example, this is a valid config array and equivalent to the previous example: + +```js +module.exports = [ + [ + require("eslint-config-standard"), + require("@me/eslint-config") + ], + { + files: ["*.js"], + rules: { + semi: ["error", "always"] + } + } +]; +``` + +A config array is always flattened before being evaluated, so even though this example is a two-dimensional config array, it will be evaluated as if it were a one-dimensional config array. + +When using a config array, only one config object must have a `files` key (config arrays where no objects contain `files` will result in an error). If a config in the config array does not contain `files` or `ignores`, then that config is merged into every config with a `files` pattern. For example: + +```js +module.exports = [ + { + languageOptions: { + globals: { + Foo: true + } + } + }, + { + files: ["*.js"], + rules: { + semi: ["error", "always"] + } + }, + { + files: ["*.mjs"], + rules: { + semi: ["error", "never"] + } + } +]; +``` + +In this example, the first config in the array defines a global variable of `Foo`. That global variable is merged into the other two configs in the array automatically because there is no `files` or `ignores` specifying when it should be used. The first config matches zero files on its own and would be invalid if it was the only config in the config array. + +#### Extending From `eslint:recommended` and `eslint:all` + +Both `eslint:recommended` and `eslint:all` can be represented as strings in a config array. For example: + +```js +module.exports = [ + "eslint:recommended", + require("eslint-config-standard"), + require("@me/eslint-config"), + { + files: ["*.js"], + rules: { + semi: ["error", "always"] + } + } +]; +``` + +This config first extends `eslint:recommended` and then continues on to extend other configs. + +#### Setting the Name of Shareable Configs + +For shareable configs, specifying a `name` property for each config they export helps ESLint to output more useful error messages if there is a problem. The `name` property is a string that will be used to identify configs to help users resolve problems. For example, if you are creating `eslint-config-example`, then you can specify a `name` property to reflect that: + +```js +module.exports = { + name: "eslint-config-example", + + // other info here +}; +``` + +It's recommended that the shareable config provide a unique name for each config that is exported. + +#### Disambiguating Shareable Configs With Common Dependencies + +Today, shareable configs that depend on plugin rules must specify the plugin as a peer dependency and then either provide a script to install those dependencies or ask the user to install them manually. + +With this design, shareable configs can specify plugins as direct dependencies that will be automatically installed with the shareable config, improving the user experience of complex shareable configs. This means it's possible for multiple shareable configs to depend on the same plugin and, in theory, depend on different versions of the same plugin. In general, npm will handle this directly, installing the correct plugin version at the correct level for each shareable config to `require()`. For example, suppose there are two shareable configs, `eslint-config-a` and `eslint-config-b` that both rely on the `eslint-plugin-example` plugin, but the former relies on 1.0.0 and the latter relies on 2.0.0. npm will install those plugins like this: + +``` +your-project +├── eslint.config.js +└── node_modules + ├── eslint + ├── eslint-config-a + | └── node_modules + | └── eslint-plugin-example@1.0.0 + └── eslint-config-b + └── node_modules + └── eslint-plugin-example@2.0.0 +``` + +The problem comes when the shareable configs try to use the default namespace of `eslint-plugin-example` for its rules, such as: + +```js +const example = require("eslint-plugin-example"); + +module.exports = { + plugins: { + example + } +}; +``` + +If both shareable configs do this, and the user tries to use both shareable configs, an error will be thrown when the configs are normalized because the plugin namespace `example` can only be assigned once. + +To work around this problem, the end user can create a separate namespace for the same plugin so that it doesn't conflict with an existing plugin namespace from a shareable config. For example, suppose you want to use `eslint-config-first`, and that has an `example` plugin namespace defined. You'd also like to use `eslint-config-second`, which also has an `example` plugin namespace defined. Trying to use both shareable configs will throw an error because a plugin namespace cannot be defined twice. You can still use both shareable configs by creating a new config from `eslint-config-second` that uses a different namespace. For example: + +```js +// get the config you want to extend +const configToExtend = require("eslint-config-second"); + +// create a new copy (NOTE: probably best to do this with @eslint/config somehow) +const compatConfig = Object.create(configToExtend); +compatConfig.plugins["compat::example"] = require("eslint-plugin-example"); +delete compatConfig.plugins.example; + +// include in config +module.exports = [ + + require("eslint-config-first"); + compatConfig, + { + // overrides here + } +]; +``` + +#### Referencing Plugin Rules + +The `plugins` key in `.eslintrc` was an array of strings indicating the plugins to load, allowing you to specify processors, rules, etc., by referencing the name of the plugin. It's no longer necessary to indicate the plugins to load because that is done directly in the `eslint.config.js` file. For example, consider this `.eslintrc` file: + +```yaml +plugins: + - react + +rules: + react/jsx-uses-react: error +``` + +This file tells ESLint to load `eslint-plugin-react` and then configure a rule from that plugin. The `react/` is automatically preprended to the rule by ESLint for easy reference. + +In `eslint.config.js`, the same configuration is achieved using a `plugins` key: + +```js +const react = require("eslint-plugin-react"); + +module.exports = { + files: ["*.js"], + plugins: { + react + }, + rules: { + "react/jsx-uses-react": "error" + } +}; +``` + +Here, it is the `plugins` that assigns the name `react` to the rules from `eslint-plugin-react`. The reference to `react/` in a rule will always look up that value in the `plugins` key. + +**Note:** If a config is merged with another config that already has the same `plugins` namespace defined and the namespace doesn't refer to the same rules object, then an error is thrown. In this case, if a config already has a `react` namespace, then attempting to combine with another config that has a `react` namespace that contains different rules will throw an error. This is to ensure the meaning of `namespace/rule` remains consistent. + +#### Plugins Specifying Their Own Namespaces + +Rules imported from a plugin must be assigned a namespace using `plugins`, which puts the responsibility for that namespace on the config file user. Plugins can define their own namespace for rules in two ways. (Note that plugins will not be required to define their own namespaces.) + +First, a plugin can export a recommended configuration to place in a config array. For example, a plugin called `eslint-plugin-example`, might define a config like this: + +```js +module.exports = { + configs: { + recommended: { + plugins: { + example: { + rules: { + rule1: require("./rules/rule1") + } + } + } + } + } +}; +``` + +Then, inside of a user config, the plugin's recommended config can be loaded: + +```js +module.exports = [ + require("eslint-plugin-example").configs.recommended, + { + rules: { + "example/rule1": "error" + } + } +]; +``` + +The user config in this example now inherits the `plugins` from the plugin's recommended config, automatically adding in the rules with their preferred namespace. (Note that the user config can't have another `plugins` namespace called `example` without an error being thrown.) + +The second way for plugins to specify their preferred namespace is to export a `plugin` key directly that users can include their own config. This is what it would look like in the plugin: + +```js +exports.plugin = { + example: { + rules: { + rule1: require("./rules/rule1") + } + } +}; +``` + +Then, inside of a user config, the plugin's `plugin` can be included directly`: + +```js +module.exports = { + plugins: { + ...require("eslint-plugin-example").plugin + }, + rules: { + "example/rule1": "error" + } +}; +``` + +This example imports the `plugin` from a plugin directly into the same section in the user config. + +#### Referencing Parsers and Processors + +In `.eslintrc`, the `parser` and `processor` keys required strings to be specified, such as: + +```yaml +plugins: ["markdown"] +parser: "babel-eslint" +processor: "markdown/markdown" +``` + +In `eslint.config.js`, there are two options. First, you can pass references directly into these keys: + +```js +module.exports = { + files: ["*.js"], + languageOptions: { + parser: require("babel-eslint"), + }, + processor: require("eslint-plugin-markdown").processors.markdown +}; +``` + +Second, you can use a string to specify an object to load from a plugin, such as: + +```js +module.exports = { + plugins: { + markdown: require("eslint-plugin-markdown"), + babel: require("eslint-plugin-babel") + }, + files: ["*.js"], + languageOptions: { + parser: "babel/eslint-parser", + }, + processor: "markdown/markdown" +}; +``` + +In this example, `"babel/eslint-parser"` loads the parser defined in the `eslint-plugin-babel` plugin and `"markdown/markdown"` loads the processor from the `eslint-plugin-markdown` plugin. Note that the behavior for `parser` is different than with `.eslintrc` in that the string **must** represent a parser defined in a plugin. + +The benefit to this approach of specifying parsers and processors is that it uses the builtin Node.js module resolution system or allows users to specify their own. There is never a question of where the modules will be resolved from. + +**Note:** This example requires that `eslint-plugin-babel` publishes a `parsers` property, such as: + +```js +module.exports = { + parsers: { + "eslint-parser": require("./some-file.js") + } +} +``` + +This is a new feature of plugins introduced with this RFC. + +#### Applying an Environment + +Unlike with `.eslintrc` files, there is no `env` key in `eslint.config.js`. For different ECMA versions, ESLint will automatically add in the required globals. For example: + +```js +const globals = require("globals"); + +module.exports = { + files: ["*.js"], + languageOptions: { + ecmaVersion: 2020 + } +}; +``` + +Because the `languageOptions.ecmaVersion` property is now a linter-level option instead of a parser-level option, ESLint will automatically add in all of the globals for ES2020 without requiring the user to do anything else. + +Similarly, when `sourceType` is `"commonjs"`, ESLint will automatically add the `require`, `exports`, and `module` global variables (as well as set `parserOptions.ecmaFeatures.globalReturn` to `true`). In this case, ESLint will pass a `sourceType` of `"script"` as part of `parserOptions` because parsers don't support `"commonjs"` for `sourceType`. + +For other globals, ssers can mimic the behavior of `env` by assigning directly to the `globals` key: + +```js +const globals = require("globals"); + +module.exports = { + files: ["*.js"], + languageOptions: { + globals: { + MyGlobal: true, + ...globals.browser + } + } +}; +``` + +This effectively duplicates the use of `env: { browser: true }` in ESLint. + +**Note:** This would allow us to stop shipping environments in ESLint. We could just tell people to use `globals` in their config and allow them to specify which version of `globals` they want to use. + +#### Overriding Configuration Based on File Patterns + +Whereas `.eslintrc` had an `overrides` key that made a hierarchical structure, the `eslint.config.js` file does not have any such hierarchy. Instead, users can return an array of configs that should be used. For example, consider this `.eslintrc` config: + +```yaml +plugins: ["react"] +rules: + react/jsx-uses-react: error + semi: error + +overrides: + - files: "*.md" + plugins: ["markdown"], + processor: "markdown/markdown" +``` + +This can be written in `eslint.config.js` as an array of two configs: + +```js +module.exports = [ + { + files: "*.js", + plugins: { + react: require("eslint-plugin-react"), + }, + rules: { + "react/jsx-uses-react": "error", + semi: "error" + } + }, + { + files: "*.md", + processor: require("eslint-plugin-markdown").processors.markdown + } +]; +``` + +When ESLint uses this config, it will check each `files` pattern to determine which configs apply. Any config with a `files` pattern matching the file to lint will be extracted and used (if multiple configs match, then those configs are merged to determine the final config to use). In this way, returning an array acts exactly the same as the array in `overrides`. + +#### Using AND patterns for files + +If any entry in the `files` key is an array, then all of the patterns must match in order for a filename to be considered a match. For example: + +```js +module.exports = [ + { + files: [ ["*.test.*", "*.js"] ], + rules: { + semi: ["error", "always"] + } + } +]; +``` + +Here, the `files` key specifies two glob patterns. The filename `foo.test.js` would match because it matches both patterns whereas the filename `foo.js` would not match because it only matches one of the glob patterns. + +**Note:** This feature is primarily intended for backwards compatibility with eslintrc's ability to specify `extends` in an `overrides` block. + +#### Ignoring files + +With `eslint.config.js`, there are three ways that files and directories can be ignored: + +1. **Defaults** - by default, ESLint will ignore `node_modules` and `.git` directories only. This is different from the current behavior where ESLint ignores `node_modules` and all files/directories beginning with a dot (`.`). +2. **.eslintignore** - the regular ESLint ignore file. +3. **eslint.config.js** - patterns specified in `ignores` keys when `files` is not specified. (See details below.) + +Anytime `ignores` appears in a config object without `files`, then the `ignores` patterns acts like the `ignorePatterns` key in `.eslintrc` in that the patterns are excluded from all searches before any other matching is done. For example: + +```js +module.exports = [{ + ignores: "web_modules" +}]; +``` + +Here, the directory `web_modules` will be ignored as if it were defined in an `.eslintignore` file. The `web_modules` directory will be excluded from the glob pattern used to determine which files ESLint will run against. + +The `--no-ignore` flag will disable `eslint.config.js` and `.eslintignore` ignore patterns while leaving the default ignore patterns in place. + +### Replacing `--ext` + +The `--ext` flag is currently used to pass in one or more file extensions that ESLint should search for when a directory without a glob pattern is passed on the command line, such as: + +```bash +eslint src/ --ext .js,.jsx +``` + +This curently searches all subdirectories of `src/` for files with extensions matching `.js` or `.jsx`. + +This proposal removes `--ext` by allowing the same information to be passed in a config. For example, the following config achieves the same result: + +```js +const fs = require("fs"); + +module.exports = { + files: ["*.js", "*.jsx"], +}; +``` + +ESLint could then be run with this command: + +```bash +eslint src/ +``` + +When evaluating the `files` array in the config, ESLint will end up searching for `src/**/*.js` and `src/**/*.jsx`. (More information about file resolution is included later this proposal.) + +Additionally, ESLint can be run without specifying anything on the command line, relying just on what's in `eslint.config.js` to determine what to lint: + +```bash +eslint +``` + +This will go into the `eslint.config.js` and use all of the `files` glob patterns to determine which files to lint. + +### Replacing `--rulesdir` + +In order to recreate the functionality of `--rulesdir`, a user would need to create a new entry in `plugins` and then specify the rules from a directory. This can be accomplished using the [`requireindex`](https://npmjs.com/package/requireindex) npm package: + +```js +const requireIndex = require("requireindex"); + +module.exports = { + plugins: { + custom: { + rules: requireIndex("./custom-rules") + } + }, + rules: { + "custom/my-rule": "error" + } +}; +``` + +The `requireIndex()` method returns an object where the keys are the rule IDs (based on the filenames found in the directory) and the values are the rule objects. Unlike today, rules loaded from a local directory must have a namespace just like plugin rules (`custom` in this example). + +### Function Configs + +Some users may need information from ESLint to determine the correct configuration to use. To allow for that, `module.exports` may also be a function that returns an object, such as: + +```js +module.exports = (context) => { + + // do something + + return { + files: ["*.js"], + rules: { + "semi": ["error", "always"] + } + }; + +}; +``` + +The `context` object has the following members: + +* `name` - the name of the application being used +* `version` - the version of ESLint being used +* `cwd` - the current working directory for ESLint (might be different than `process.cwd()` but always matches `CLIEngine.options.cwd`, see https://github.com/eslint/eslint/issues/11218) + +This information allows users to make logical decisions about how the config should be constructed. + +A configuration function may return an object or an array of objects. An error is thrown if any other type of value is returned. + +#### Including Function Configs in an Array + +A function config can be used anywhere a config object or a config array is valid. That means you can insert a function config as a config array member: + +```js +module.exports = [ + (context) => someObject, + require("eslint-config-myconfig") +]; +``` + +Each function config in an array will be executed with a `context` object when ESLint evaluates the configuration file. This also means that shareable configs can export a function instead of an object or array. + +**Note:** If a function config inside of a config array happens to return an array, then those config array items are flattened as with any array-in-array situation. + +### The `@eslint/eslintrc` Utility + +To allow for backwards compatibility with existing configs and plugins, an `@eslint/eslintrc` utility is provided. The package exports the following classes: + +* `ESLintRCCompat` - a class to help convert `.eslintrc`-style configs into the correct format. + + +```js +class ESLintRCCompat { + + constructor(baseDir) {} + + config(configObjct) {} + plugins(...pluginNames) {} + extends(...sharedConfigNames) {} + env(envObject) {} + +} +``` + +#### Importing existing configs + +The `ESLintRCCompat#extends()` function allows users to specify an existing `.eslintrc` config location in the same format that used in the `.eslintrc` `extends` key. Users can pass in a filename, a shareable config name, or a plugin config name and have it converted automatically into the correct format. For example: + +```js +const { ESLintRCCompat } = require("@eslint/eslintrc"); + +const eslintrc = new ESLintRCCompat(__dirname); + +module.exports = [ + "eslint:recommended", + + // load a file + eslintrc.extends("./.eslintrc.yml"), + + // load eslint-config-standard + eslintrc.extends("standard"), + + // load eslint-plugin-vue/recommended + eslintrc.extends("plugin:vue/recommended"), + + // or multiple at once + eslintrc.extends("./.eslintrc.yml", "standard", "plugin:vue/recommended") + +]; +``` + +#### Translating config objects + +The `ESLintRCCompat#config()` methods allows users to pass in a `.eslintrc`-style config and get back a config object that works with `eslint.config.js`. For example: + +```js +const { ESLintRCCompat } = require("@eslint/eslintrc"); + +const eslintrc = new ESLintRCCompat(__dirname); + +module.exports = [ + "eslint:recommended", + + eslintrc.config({ + env: { + node: true + }, + root: true + }); +]; +``` + +#### Including plugins + +The `ESLintRCCompat#plugins()` method allows users to automatically load a plugin's rules and processors without separately assigning a namespace. For example: + +```js +const { ESLintRCCompat } = require("@eslint/eslintrc"); + +const eslintrc = new ESLintRCCompat(__dirname); + +module.exports = [ + "eslint:recommended", + + // add in eslint-plugin-vue and eslint-plugin-example + eslintrc.plugins("vue", "example") +]; +``` + +This example includes both `eslint-plugin-vue` and `eslint-plugin-example` so that all of the rules are available with the correct namespace and processors are automatically hooked up to the correct `files` pattern. + +#### Applying environments + +The `ESLintRCCompat#env()` method allows users to specify an `env` settings as they would in an `.eslintrc`-style config and have globals automatically added. For example: + +```js +const { ESLintRCCompat } = require("@eslint/eslintrc"); + +const eslintrc = new ESLintRCCompat(__dirname); + +module.exports = [ + "eslint:recommended", + + // load node environment + eslintrc.env({ + node: true + }) +]; +``` + +### Configuration Location Resolution + +When ESLint is executed, the following steps are taken to find the `eslint.config.js` file to use: + +1. If the `-c` flag is used then the specified configuration file is used. There is no further search performed. +1. Otherwise: + 1. Look for `eslint.config.js` in the current working directory. If found, stop searching and use that file. + 1. If not found, search up the directory hierarchy looking for `eslint.config.js`. + 1. If a `eslint.config.js` file is found at any point, stop searching and use that file. + 1. If `/` is reached without finding `eslint.config.js`, then stop searching and output a "no configuration found" error. + +This approach will allow running ESLint from within a subdirectory of a project and get the same result as when ESLint is run from the project's root directory (the one where `eslint.config.js` is found). + +Some of the key differences from the way ESLint's configuration resolution works today are: + +1. There is no automatic search for `eslint.config.js` in the user's home directory. Users wanting this functionality can either pass a home directory file using `-c` or manually read in that file from their `eslint.config.js` file. +1. Once a `eslint.config.js` file is found, there is no more searching for any further config files. +1. There is no automatic merging of config files using either `extends` or `overrides`. +1. When `-c` is passed on the command line, there is no search performed. + +### File Pattern Resolution + +Because there are file patterns included in `eslint.config.js`, this requires a change to how ESLint decides which files to lint. The process for determining which files to lint is: + +1. When a filename is passed directly (such as `eslint foo.js`): + 1. ESLint checks to see if there is one or more configs where the `files` pattern matches the file that was passed in and does not match the `ignores` pattern. The pattern is evaluated by prepending the directory in which `eslint.config.js` was found to each pattern in `files`. All configs that match `files` and not `ignores` are merged (with the last matching config taking precedence over others). If no config is found, then the file is ignored with an appropriate warning. + 1. If a matching config is found, then the `ignores` pattern is tested against the filename. If it's a match, then the file is ignored. Otherwise, the file is linted. +1. When a glob pattern is passed directly (such as `eslint src/*.js`): + 1. ESLint expands the glob pattern to get a list of files. + 1. Each file is checked individually as in step 1. +1. When a directory is passed directly (such as `eslint src`): + 1. The directory is converted into a glob pattern by appending `**/*` to the directory (such as `src` becomes `src/**/*`). + 1. The glob pattern is checked as in step 2. +1. When a relative directory is passed directly (such as `eslint .`): + 1. The relative directory pattern is resolved to a full directory name. + 1. The glob pattern is checked as in step 3. + +**Note:** ESLint will continue to ignore `node_modules` by default. + +### Rename `--no-eslintrc` to `--no-config-file` and `useEslintrc` to `useConfigFile` + +Because the config filename has changed, it makes sense to change the command line `--no-eslintrc` flag to a more generic name, `--no-config-file` and change `CLIEngine`'s `useEslintrc` option to `useConfigFile`. In the short term, to avoid a breaking change, these pairs of names can be aliased to each other. + +### Implementation Details + +The implementation of this feature requires the following changes: + +1. Create a new `ESLintConfigArray` class to manage configs. +1. Create a `--no-config-file` CLI option and alias it to `--no-eslintrc` for backwards compatibility. +1. Create a `useConfigFile` option for `CLIEngine`. Alias `useEslintrc` to this option for backwards compatibility. +1. In `CLIEngine#executeOnFiles()`: + 1. Check for existence of `eslint.config.js`, and if found, opt-in to new behavior. + 1. Create a `ESLintConfigArray` to hold the configuration information and to determine which files to lint (in conjunction with already-existing `globUtils`) + 1. Rename the private functions `processText()` and `processFiles()` to `legacyProcessText()` and `legacyProcessFiles()`; create new versions with the new functionality named `processText()` and `processFiles()`. Use the appropriate functions based on whether or not the user has opted-in. + 1. Update `Linter#verify()` to check for objects on keys that now support objects instead of strings (like `parser`) and add a `disableEnv` property to the options to indicate that environments should not be honored. +1. At a later point, we will be able to remove a lot of the existing configuration utilities. + +#### The `ESLintConfigArray` Class + +The `ESLintConfigArray` class is the primary new class for handling the configuration change defined in this proposal. + +```js +class ESLintConfigArray extends Array { + + // normalize the current ConfigArray + async normalize(context) {} + + // get a single config for the given filename + getConfig(filename) {} + + // get the file patterns to search for + get files() {} + + // get the "ignore file" values + get ignores() {} +} +``` + +In this class, "normalize" means that all functions are called and replaced with their results, the array has been flattened, and configs without `files` keys have been merged into configs that do have `files` keys for easier calculation. + +## Documentation + +This will require extensive documentation changes and an introductory blog post. + +At a minimum, these pages will have to be updated (and rewritten): + +* https://eslint.org/docs/user-guide/getting-started#configuration +* https://eslint.org/docs/user-guide/configuring +* https://eslint.org/docs/developer-guide/working-with-plugins#configs-in-plugins +* https://eslint.org/docs/developer-guide/shareable-configs + +## Drawbacks + +As with any significant change, there are some significant drawbacks: + +1. We'd need to do a phased rollout (see Backwards Compatibility Analysis) to minimize impact on users. That means maintaining two configuration systems for a while, increasing maintenance overhead and complexity of the application. +1. Getting everyone to convert to `eslint.config.js` format would be a significant stress on the community that could cause some resentment. +1. We can no longer enforce naming conventions for plugins and shareable configs. This may make it more difficult for users to find compatible npm packages. +1. Creating configuration files will be more complicated. +1. The `--print-config` option becomes less useful because we can't output objects in a meaningful way. +1. People depending on environments may find this change particularly painful. + +## Backwards Compatibility Analysis + +The intent of this proposal is to replace the current `.eslintrc` format, but can be implemented incrementally so as to cause as little disturbance to ESLint users as possible. + +In the first phase, I envision this: + +1. Extract all current `.eslintrc` functionality into `@eslint/eslintrc` package. Change ESLint to depend on `@eslint/eslintrc` package. +1. `eslint.config.js` can be implemented alongside `.eslintrc`. +1. If `eslint.config.js` is found, then: + 1. All `.eslintrc` files are ignored. + 1. `--rulesdir` is ignored. + 1. `--env` is ignored. + 1. `--ext` is ignored. + 1. `eslint-env` config comments are ignored. +1. If `eslint.config.js` is not found, then fall back to the current behavior. +1. Switch ESLint itself to use `eslint.config.js` as a way to test and ensure compatibility with existing shareable configs in `.eslintrc` format. + +This keeps the current behavior for the majority of users while allowing some users to test out the new functionality. Also, `-c` could not be used with `eslint.config.js` in this phase. + +In the second phase (and in a major release), ESLint will emit deprecation warnings whenever the original functionality is used but will still honor them so long as `eslint.config.js` is not found. In this phase, we will work with several high-profile plugins and shareable configs to convert their packages into the new format. We will use this to find the remaining compatibility issues. + +In the third phase (and in another major release), `eslint.config.js` becomes the official way to configure ESLint. If no `eslint.config.js` file is found, ESLint will still search for a `.eslintrc` file, and if found, print an error message information the user that the configuration file format has changed. + +So while this is intended to be a breaking change, it will be introduced over the course of three major releases in order to give users ample time to transition. + +## Alternatives + +While there are no alternatives that cover all of the functionality in this RFC, there are alternatives designed to address various parts. + +* Both https://github.com/eslint/rfcs/pull/7 and https://github.com/eslint/rfcs/pull/5 specify an alternative method for resolving plugin locations in configs. This attempts to solve the problem with bundling plugins with configs (https://github.com/eslint/eslint/issues/3458). These proposals have the benefit of working within the current configuration system and requiring very few changes from users. +* It is possible to come up with a solution to https://github.com/eslint/eslint/issues/8813 that makes use of `extends`, though this would get a bit complicated if an extended config in an `overrides` section also has `overrides`. +* We could switch the current configuration system over so that all config files are considered to implicitly have `root:true`. This could dramatically simplify configuration searching and merging. + +## Open Questions + +1. **Should `files` and `ignores` be merged (as in this proposal) or just dropped completely?** I can see an argument for dropping them completely, but I also know that some folks share configs with multiple different `files` patterns in an array that is meant to be used together. + +## Frequently Asked Questions + +## Related Discussions + +* https://github.com/eslint/rfcs/pull/7 +* https://github.com/eslint/rfcs/pull/5 +* https://github.com/eslint/eslint/issues/3458 +* https://github.com/eslint/eslint/issues/6732 +* https://github.com/eslint/eslint/issues/8813 +* https://github.com/eslint/eslint/issues/9192 +* https://github.com/eslint/eslint/issues/9897 +* https://github.com/eslint/eslint/issues/10125 +* https://github.com/eslint/eslint/issues/10643 +* https://github.com/eslint/eslint/issues/10891 +* https://github.com/eslint/eslint/issues/11223 +* https://github.com/eslint/rfcs/pull/55 From bdf0a35d038312604569c0a3b5fef527ee072bd9 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 14 Nov 2024 16:57:04 -0500 Subject: [PATCH 02/14] feat: Flat config extends --- designs/2024-config-extends/README.md | 913 ++++---------------------- 1 file changed, 132 insertions(+), 781 deletions(-) diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md index 6a9f971d..0bb6b8bd 100644 --- a/designs/2024-config-extends/README.md +++ b/designs/2024-config-extends/README.md @@ -92,7 +92,9 @@ Design Summary: ### Allow Arrays in Config Arrays -TODO +This was part of the original RFC but was disabled over concerns about the complexity involved with importing configs from packages and having no idea whether it was an object, an array, or an array with a mix of objects and arrays. This mattered most when users needed to use `map()` to apply different `files` or `ignores` to configs, but with `extends`, that concern goes away. + +Support for nested arrays is already in `ConfigArray`, it's just disabled. ### Introduce an `extends` Key @@ -283,866 +285,215 @@ This has several advantages: 1. It encourages plugin authors to use the `configs` key on their plugin in order to allow this usage. 1. It allows ESLint to modify configs before they are used (see below). +#### Reassignable Plugin Configs +When using named configs, this gives ESLint the opportunity to inspect and change the config before applying it. This means we have the opportunity to improve the plugin config experience. +Right now, bundling a config in a plugin is a bit of a messy process for two reasons: +1. You need to include the plugin in the config directly, which means that you can't just have an single object literal defined as the plugin. You need to define the plugin first and then add the configs with the plugin reference second. +1. You don't actually know what namespace a user will assign the plugin to, so while you might assume someone will import `eslint-plugin-import` as `import` and you then refer to rules from the plugin as `import/rule`, there is nothing preventing the user from assigning the plugin to a different name. In that case, the user won't know how to override rule options or assign different languages from the plugin. -### The `eslint.config.js` File - -The `eslint.config.js` file is a JavaScript file (there is no JSON or YAML equivalent) that exports an object: - -```js -module.exports = { - name: "name", - files: ["*.js"], - ignores: ["*.test.js"], - settings: {}, - languageOptions: { - ecmaVersion: 2020, - sourceType: "module", - globals: {}, - parser: object || "string", - parserOptions: {}, - } - linterOptions: { - reportUnusedDisableDirectives: "string" - }, - processor: object || "string", - plugins: {} - rules: {} -}; -``` - -The following keys are new to the `eslint.config.js` format: - -* `name` - Specifies the name of the config object. This is helpful for printing out debugging information and, while not required, is recommended for that reason. -* `files` - Determines the glob file patterns that this configuration applies to. These patterns can be negated by prefixing them with `!`, which effectively mimics the behavior of `excludedFiles` in `.eslintrc`. -* `ignores` - Determines the files that should not be linted using ESLint. The files specified by this array of glob patterns are subtracted from the files specified in `files`. If there is no `files` key, then `ignores` acts the same as `ignorePatterns` in `.eslintrc` files; if there is a `files` key then `ignores` acts like `excludedFiles` in `.eslintrc`. - -The following keys are specified the same as in `.eslintrc` files: - -* `settings` -* `rules` - -The following keys are specified differently than in `.eslintrc` files: - -* `plugins` - an object mapping plugin names to implementations. This replaces the `plugins` key in `.eslintrc` files and the `--rulesdir` option. -* `processor` - an object or string in `eslint.config.js` files (a string in `.eslintrc`) -* `languageOptions` - top-level grouping for all options that affect how JavaScript is interpreted by ESLint. - * `ecmaVersion` - sets the JavaScript version ESLint should use for these files. This value is copied into `parserOptions` if not already present. - * `sourceType` - sets the source type of the JavaScript code to parse. One of `module`, `script`, or `commonjs`. - * `parser` - an object or string in `eslint.config.js` files (a string in `.eslintrc`) - * `parserOptions` - an object specifying any additional parameters to be passed to the parser. - * `globals` - any additional global variables to add. -* `linterOptions` - an object for linter-specific settings - * `reportUnusedDisableDirectives` - new location for the same option name. - -Each of these keys used to require one or more strings specifying module(s) to load in `.eslintrc`. In `eslint.config.js`, these are all objects or strings (referencing an object in a plugin), requiring users to manually specify the objects to use. - -The following keys are invalid in `eslint.config.js`: - -* `extends` - replaced by config arrays -* `env` - responsibility of the user -* `overrides` - responsibility of the user -* `root` - always considered `true` - -Each of these keys represent different ways of augmenting how configuration is calculated and all of that responsibility now falls on the user. - -#### Extending Another Config - -Extending another config is accomplished by returning an array as the value of `module.exports`. Configs that come later in the array are merged with configs that come earlier in the array. For example: - -```js -module.exports = [ - require("eslint-config-standard"), - { - files: ["*.js"], - rules: { - semi: ["error", "always"] - } - } -]; -``` - -This config extends `eslint-config-standard` because that package is included first in the array. You can add multiple configs into the array to extend from multiple configs, such as: - -```js -module.exports = [ - require("eslint-config-standard"), - require("@me/eslint-config"), - { - files: ["*.js"], - rules: { - semi: ["error", "always"] - } - } -]; -``` - -Each item in a config array can be a config array. For example, this is a valid config array and equivalent to the previous example: - -```js -module.exports = [ - [ - require("eslint-config-standard"), - require("@me/eslint-config") - ], - { - files: ["*.js"], - rules: { - semi: ["error", "always"] - } - } -]; -``` - -A config array is always flattened before being evaluated, so even though this example is a two-dimensional config array, it will be evaluated as if it were a one-dimensional config array. - -When using a config array, only one config object must have a `files` key (config arrays where no objects contain `files` will result in an error). If a config in the config array does not contain `files` or `ignores`, then that config is merged into every config with a `files` pattern. For example: - -```js -module.exports = [ - { - languageOptions: { - globals: { - Foo: true - } - } - }, - { - files: ["*.js"], - rules: { - semi: ["error", "always"] - } - }, - { - files: ["*.mjs"], - rules: { - semi: ["error", "never"] - } - } -]; -``` - -In this example, the first config in the array defines a global variable of `Foo`. That global variable is merged into the other two configs in the array automatically because there is no `files` or `ignores` specifying when it should be used. The first config matches zero files on its own and would be invalid if it was the only config in the config array. - -#### Extending From `eslint:recommended` and `eslint:all` - -Both `eslint:recommended` and `eslint:all` can be represented as strings in a config array. For example: - -```js -module.exports = [ - "eslint:recommended", - require("eslint-config-standard"), - require("@me/eslint-config"), - { - files: ["*.js"], - rules: { - semi: ["error", "always"] - } - } -]; -``` - -This config first extends `eslint:recommended` and then continues on to extend other configs. - -#### Setting the Name of Shareable Configs - -For shareable configs, specifying a `name` property for each config they export helps ESLint to output more useful error messages if there is a problem. The `name` property is a string that will be used to identify configs to help users resolve problems. For example, if you are creating `eslint-config-example`, then you can specify a `name` property to reflect that: - -```js -module.exports = { - name: "eslint-config-example", - - // other info here -}; -``` - -It's recommended that the shareable config provide a unique name for each config that is exported. - -#### Disambiguating Shareable Configs With Common Dependencies - -Today, shareable configs that depend on plugin rules must specify the plugin as a peer dependency and then either provide a script to install those dependencies or ask the user to install them manually. - -With this design, shareable configs can specify plugins as direct dependencies that will be automatically installed with the shareable config, improving the user experience of complex shareable configs. This means it's possible for multiple shareable configs to depend on the same plugin and, in theory, depend on different versions of the same plugin. In general, npm will handle this directly, installing the correct plugin version at the correct level for each shareable config to `require()`. For example, suppose there are two shareable configs, `eslint-config-a` and `eslint-config-b` that both rely on the `eslint-plugin-example` plugin, but the former relies on 1.0.0 and the latter relies on 2.0.0. npm will install those plugins like this: - -``` -your-project -├── eslint.config.js -└── node_modules - ├── eslint - ├── eslint-config-a - | └── node_modules - | └── eslint-plugin-example@1.0.0 - └── eslint-config-b - └── node_modules - └── eslint-plugin-example@2.0.0 -``` - -The problem comes when the shareable configs try to use the default namespace of `eslint-plugin-example` for its rules, such as: +Here's an actual example from [`@eslint/json`](https://github.com/eslint/json): ```js -const example = require("eslint-plugin-example"); +const plugin = { + meta: { + name: "@eslint/json", + version: "0.6.0", // x-release-please-version + }, + languages: { + json: new JSONLanguage({ mode: "json" }), + jsonc: new JSONLanguage({ mode: "jsonc" }), + json5: new JSONLanguage({ mode: "json5" }), + }, + rules: { + "no-duplicate-keys": noDuplicateKeys, + "no-empty-keys": noEmptyKeys, + }, -module.exports = { - plugins: { - example - } + // can't include the plugin here because it must reference `plugin` + configs: {}, }; -``` - -If both shareable configs do this, and the user tries to use both shareable configs, an error will be thrown when the configs are normalized because the plugin namespace `example` can only be assigned once. - -To work around this problem, the end user can create a separate namespace for the same plugin so that it doesn't conflict with an existing plugin namespace from a shareable config. For example, suppose you want to use `eslint-config-first`, and that has an `example` plugin namespace defined. You'd also like to use `eslint-config-second`, which also has an `example` plugin namespace defined. Trying to use both shareable configs will throw an error because a plugin namespace cannot be defined twice. You can still use both shareable configs by creating a new config from `eslint-config-second` that uses a different namespace. For example: - -```js -// get the config you want to extend -const configToExtend = require("eslint-config-second"); -// create a new copy (NOTE: probably best to do this with @eslint/config somehow) -const compatConfig = Object.create(configToExtend); -compatConfig.plugins["compat::example"] = require("eslint-plugin-example"); -delete compatConfig.plugins.example; +Object.assign(plugin.configs, { + recommended: { -// include in config -module.exports = [ + // now we can reference plugin + plugins: { json: plugin }, + rules: { + "json/no-duplicate-keys": "error", + "json/no-empty-keys": "error", + }, + }, +}); - require("eslint-config-first"); - compatConfig, - { - // overrides here - } -]; +export default plugin; ``` -#### Referencing Plugin Rules - -The `plugins` key in `.eslintrc` was an array of strings indicating the plugins to load, allowing you to specify processors, rules, etc., by referencing the name of the plugin. It's no longer necessary to indicate the plugins to load because that is done directly in the `eslint.config.js` file. For example, consider this `.eslintrc` file: +Here, we are hardcoding the namespace `json` even though that might not be the namespace that the user assigns to this plugin. This is something we can now address with the use of `extends` because we have the ability to alter the config before inserting it. -```yaml -plugins: - - react - -rules: - react/jsx-uses-react: error -``` - -This file tells ESLint to load `eslint-plugin-react` and then configure a rule from that plugin. The `react/` is automatically preprended to the rule by ESLint for easy reference. - -In `eslint.config.js`, the same configuration is achieved using a `plugins` key: +Instead of using a hardcoded plugin namespace, plugins can instead use `#` to indicate that they'd like to have the plugin itself included and referenced using the namespace the user assigned. For example, we could rewrite the JSON plugin like this: ```js -const react = require("eslint-plugin-react"); - -module.exports = { - files: ["*.js"], - plugins: { - react - }, - rules: { - "react/jsx-uses-react": "error" - } -}; -``` - -Here, it is the `plugins` that assigns the name `react` to the rules from `eslint-plugin-react`. The reference to `react/` in a rule will always look up that value in the `plugins` key. - -**Note:** If a config is merged with another config that already has the same `plugins` namespace defined and the namespace doesn't refer to the same rules object, then an error is thrown. In this case, if a config already has a `react` namespace, then attempting to combine with another config that has a `react` namespace that contains different rules will throw an error. This is to ensure the meaning of `namespace/rule` remains consistent. +export default { + meta: { + name: "@eslint/json", + version: "0.6.0", // x-release-please-version + }, + languages: { + json: new JSONLanguage({ mode: "json" }), + jsonc: new JSONLanguage({ mode: "jsonc" }), + json5: new JSONLanguage({ mode: "json5" }), + }, + rules: { + "no-duplicate-keys": noDuplicateKeys, + "no-empty-keys": noEmptyKeys, + }, -#### Plugins Specifying Their Own Namespaces - -Rules imported from a plugin must be assigned a namespace using `plugins`, which puts the responsibility for that namespace on the config file user. Plugins can define their own namespace for rules in two ways. (Note that plugins will not be required to define their own namespaces.) - -First, a plugin can export a recommended configuration to place in a config array. For example, a plugin called `eslint-plugin-example`, might define a config like this: - -```js -module.exports = { - configs: { + configs: { recommended: { - plugins: { - example: { - rules: { - rule1: require("./rules/rule1") - } - } - } - } - } -}; -``` - -Then, inside of a user config, the plugin's recommended config can be loaded: - -```js -module.exports = [ - require("eslint-plugin-example").configs.recommended, - { - rules: { - "example/rule1": "error" - } - } -]; -``` - -The user config in this example now inherits the `plugins` from the plugin's recommended config, automatically adding in the rules with their preferred namespace. (Note that the user config can't have another `plugins` namespace called `example` without an error being thrown.) - -The second way for plugins to specify their preferred namespace is to export a `plugin` key directly that users can include their own config. This is what it would look like in the plugin: - -```js -exports.plugin = { - example: { - rules: { - rule1: require("./rules/rule1") - } - } -}; -``` - -Then, inside of a user config, the plugin's `plugin` can be included directly`: - -```js -module.exports = { - plugins: { - ...require("eslint-plugin-example").plugin - }, - rules: { - "example/rule1": "error" - } -}; -``` - -This example imports the `plugin` from a plugin directly into the same section in the user config. - -#### Referencing Parsers and Processors - -In `.eslintrc`, the `parser` and `processor` keys required strings to be specified, such as: - -```yaml -plugins: ["markdown"] -parser: "babel-eslint" -processor: "markdown/markdown" -``` - -In `eslint.config.js`, there are two options. First, you can pass references directly into these keys: - -```js -module.exports = { - files: ["*.js"], - languageOptions: { - parser: require("babel-eslint"), - }, - processor: require("eslint-plugin-markdown").processors.markdown -}; -``` - -Second, you can use a string to specify an object to load from a plugin, such as: - -```js -module.exports = { - plugins: { - markdown: require("eslint-plugin-markdown"), - babel: require("eslint-plugin-babel") - }, - files: ["*.js"], - languageOptions: { - parser: "babel/eslint-parser", + plugins: { "#": null }, + rules: { + "#/no-duplicate-keys": "error", + "#/no-empty-keys": "error", + }, + }, }, - processor: "markdown/markdown" -}; -``` - -In this example, `"babel/eslint-parser"` loads the parser defined in the `eslint-plugin-babel` plugin and `"markdown/markdown"` loads the processor from the `eslint-plugin-markdown` plugin. Note that the behavior for `parser` is different than with `.eslintrc` in that the string **must** represent a parser defined in a plugin. - -The benefit to this approach of specifying parsers and processors is that it uses the builtin Node.js module resolution system or allows users to specify their own. There is never a question of where the modules will be resolved from. - -**Note:** This example requires that `eslint-plugin-babel` publishes a `parsers` property, such as: - -```js -module.exports = { - parsers: { - "eslint-parser": require("./some-file.js") - } -} -``` - -This is a new feature of plugins introduced with this RFC. - -#### Applying an Environment - -Unlike with `.eslintrc` files, there is no `env` key in `eslint.config.js`. For different ECMA versions, ESLint will automatically add in the required globals. For example: - -```js -const globals = require("globals"); - -module.exports = { - files: ["*.js"], - languageOptions: { - ecmaVersion: 2020 - } }; ``` -Because the `languageOptions.ecmaVersion` property is now a linter-level option instead of a parser-level option, ESLint will automatically add in all of the globals for ES2020 without requiring the user to do anything else. - -Similarly, when `sourceType` is `"commonjs"`, ESLint will automatically add the `require`, `exports`, and `module` global variables (as well as set `parserOptions.ecmaFeatures.globalReturn` to `true`). In this case, ESLint will pass a `sourceType` of `"script"` as part of `parserOptions` because parsers don't support `"commonjs"` for `sourceType`. - -For other globals, ssers can mimic the behavior of `env` by assigning directly to the `globals` key: +In an `eslint.config.js` file: ```js -const globals = require("globals"); - -module.exports = { - files: ["*.js"], - languageOptions: { - globals: { - MyGlobal: true, - ...globals.browser - } - } -}; -``` - -This effectively duplicates the use of `env: { browser: true }` in ESLint. +import json from "@eslint/json"; -**Note:** This would allow us to stop shipping environments in ESLint. We could just tell people to use `globals` in their config and allow them to specify which version of `globals` they want to use. - -#### Overriding Configuration Based on File Patterns - -Whereas `.eslintrc` had an `overrides` key that made a hierarchical structure, the `eslint.config.js` file does not have any such hierarchy. Instead, users can return an array of configs that should be used. For example, consider this `.eslintrc` config: - -```yaml -plugins: ["react"] -rules: - react/jsx-uses-react: error - semi: error - -overrides: - - files: "*.md" - plugins: ["markdown"], - processor: "markdown/markdown" -``` - -This can be written in `eslint.config.js` as an array of two configs: - -```js -module.exports = [ +export default [ { - files: "*.js", plugins: { - react: require("eslint-plugin-react"), + j: json }, + files: ["**/*.json"], + extends: ["j/recommended"], rules: { - "react/jsx-uses-react": "error", - semi: "error" - } - }, - { - files: "*.md", - processor: require("eslint-plugin-markdown").processors.markdown - } -]; -``` - -When ESLint uses this config, it will check each `files` pattern to determine which configs apply. Any config with a `files` pattern matching the file to lint will be extracted and used (if multiple configs match, then those configs are merged to determine the final config to use). In this way, returning an array acts exactly the same as the array in `overrides`. - -#### Using AND patterns for files - -If any entry in the `files` key is an array, then all of the patterns must match in order for a filename to be considered a match. For example: - -```js -module.exports = [ - { - files: [ ["*.test.*", "*.js"] ], - rules: { - semi: ["error", "always"] + "j/no-empty-keys": "warn" } } ]; ``` -Here, the `files` key specifies two glob patterns. The filename `foo.test.js` would match because it matches both patterns whereas the filename `foo.js` would not match because it only matches one of the glob patterns. - -**Note:** This feature is primarily intended for backwards compatibility with eslintrc's ability to specify `extends` in an `overrides` block. - -#### Ignoring files - -With `eslint.config.js`, there are three ways that files and directories can be ignored: - -1. **Defaults** - by default, ESLint will ignore `node_modules` and `.git` directories only. This is different from the current behavior where ESLint ignores `node_modules` and all files/directories beginning with a dot (`.`). -2. **.eslintignore** - the regular ESLint ignore file. -3. **eslint.config.js** - patterns specified in `ignores` keys when `files` is not specified. (See details below.) - -Anytime `ignores` appears in a config object without `files`, then the `ignores` patterns acts like the `ignorePatterns` key in `.eslintrc` in that the patterns are excluded from all searches before any other matching is done. For example: - -```js -module.exports = [{ - ignores: "web_modules" -}]; -``` - -Here, the directory `web_modules` will be ignored as if it were defined in an `.eslintignore` file. The `web_modules` directory will be excluded from the glob pattern used to determine which files ESLint will run against. - -The `--no-ignore` flag will disable `eslint.config.js` and `.eslintignore` ignore patterns while leaving the default ignore patterns in place. - -### Replacing `--ext` - -The `--ext` flag is currently used to pass in one or more file extensions that ESLint should search for when a directory without a glob pattern is passed on the command line, such as: - -```bash -eslint src/ --ext .js,.jsx -``` - -This curently searches all subdirectories of `src/` for files with extensions matching `.js` or `.jsx`. - -This proposal removes `--ext` by allowing the same information to be passed in a config. For example, the following config achieves the same result: - -```js -const fs = require("fs"); - -module.exports = { - files: ["*.js", "*.jsx"], -}; -``` - -ESLint could then be run with this command: - -```bash -eslint src/ -``` - -When evaluating the `files` array in the config, ESLint will end up searching for `src/**/*.js` and `src/**/*.jsx`. (More information about file resolution is included later this proposal.) - -Additionally, ESLint can be run without specifying anything on the command line, relying just on what's in `eslint.config.js` to determine what to lint: - -```bash -eslint -``` - -This will go into the `eslint.config.js` and use all of the `files` glob patterns to determine which files to lint. - -### Replacing `--rulesdir` - -In order to recreate the functionality of `--rulesdir`, a user would need to create a new entry in `plugins` and then specify the rules from a directory. This can be accomplished using the [`requireindex`](https://npmjs.com/package/requireindex) npm package: - -```js -const requireIndex = require("requireindex"); - -module.exports = { - plugins: { - custom: { - rules: requireIndex("./custom-rules") - } - }, - rules: { - "custom/my-rule": "error" - } -}; -``` - -The `requireIndex()` method returns an object where the keys are the rule IDs (based on the filenames found in the directory) and the values are the rule objects. Unlike today, rules loaded from a local directory must have a namespace just like plugin rules (`custom` in this example). - -### Function Configs - -Some users may need information from ESLint to determine the correct configuration to use. To allow for that, `module.exports` may also be a function that returns an object, such as: - -```js -module.exports = (context) => { - - // do something - - return { - files: ["*.js"], - rules: { - "semi": ["error", "always"] - } - }; - -}; -``` - -The `context` object has the following members: - -* `name` - the name of the application being used -* `version` - the version of ESLint being used -* `cwd` - the current working directory for ESLint (might be different than `process.cwd()` but always matches `CLIEngine.options.cwd`, see https://github.com/eslint/eslint/issues/11218) +Here, the user has assigned the namespace `j` to the JSON plugin. When ESLint loads this config, it sees `extends` with a value of `"j/recommended"`. It looks up the plugin referenced by `j` and then the config named `"recommended"`. It then sees that there's a plugin entry for `"#"` and replaces that with an entry for `j` that points to the JSON plugin. The next step is to look through the config for all the `#` references and replace that with `j` so the references are correct. -This information allows users to make logical decisions about how the config should be constructed. - -A configuration function may return an object or an array of objects. An error is thrown if any other type of value is returned. - -#### Including Function Configs in an Array - -A function config can be used anywhere a config object or a config array is valid. That means you can insert a function config as a config array member: - -```js -module.exports = [ - (context) => someObject, - require("eslint-config-myconfig") -]; -``` - -Each function config in an array will be executed with a `context` object when ESLint evaluates the configuration file. This also means that shareable configs can export a function instead of an object or array. +### Implementation Details -**Note:** If a function config inside of a config array happens to return an array, then those config array items are flattened as with any array-in-array situation. +The implementation of this feature requires the following changes: -### The `@eslint/eslintrc` Utility +1. Enable nested arrays in `FlatConfigArray` +1. Create a new `ConfigExtender` class that encapsulates the functionality for extending configs. +1. Update `FlatConfigArray` to use `ConfigExtender` inside of the `normalize()` and `normalizeSync()` methods. -To allow for backwards compatibility with existing configs and plugins, an `@eslint/eslintrc` utility is provided. The package exports the following classes: +#### Enable Nested Arrays in `FlatConfigArray` -* `ESLintRCCompat` - a class to help convert `.eslintrc`-style configs into the correct format. +Pass [`extraConfigTypes: ["array"]`](https://github.com/eslint/rewrite/blob/a957ee351c27ac1bf22966768cf8aac8c12ce0d2/packages/config-array/src/config-array.js#L572) to [`super()`](https://github.com/eslint/eslint/blob/fd33f1315ac59b1b3828dbab8e1e056a1585eff0/lib/config/flat-config-array.js#L95-L98) to enable nested arrays. +#### The `ConfigExtender` Class -```js -class ESLintRCCompat { - - constructor(baseDir) {} - - config(configObjct) {} - plugins(...pluginNames) {} - extends(...sharedConfigNames) {} - env(envObject) {} +The `ConfigExtender` class is the primary new class for handling the `extends` key in config objects. +```ts +interface ConfigExtender { + evaluate(config: ConfigObject): Array; } ``` -#### Importing existing configs - -The `ESLintRCCompat#extends()` function allows users to specify an existing `.eslintrc` config location in the same format that used in the `.eslintrc` `extends` key. Users can pass in a filename, a shareable config name, or a plugin config name and have it converted automatically into the correct format. For example: - -```js -const { ESLintRCCompat } = require("@eslint/eslintrc"); - -const eslintrc = new ESLintRCCompat(__dirname); - -module.exports = [ - "eslint:recommended", - - // load a file - eslintrc.extends("./.eslintrc.yml"), - - // load eslint-config-standard - eslintrc.extends("standard"), - - // load eslint-plugin-vue/recommended - eslintrc.extends("plugin:vue/recommended"), - - // or multiple at once - eslintrc.extends("./.eslintrc.yml", "standard", "plugin:vue/recommended") - -]; -``` - -#### Translating config objects - -The `ESLintRCCompat#config()` methods allows users to pass in a `.eslintrc`-style config and get back a config object that works with `eslint.config.js`. For example: - -```js -const { ESLintRCCompat } = require("@eslint/eslintrc"); - -const eslintrc = new ESLintRCCompat(__dirname); - -module.exports = [ - "eslint:recommended", - - eslintrc.config({ - env: { - node: true - }, - root: true - }); -]; -``` - -#### Including plugins - -The `ESLintRCCompat#plugins()` method allows users to automatically load a plugin's rules and processors without separately assigning a namespace. For example: - -```js -const { ESLintRCCompat } = require("@eslint/eslintrc"); - -const eslintrc = new ESLintRCCompat(__dirname); - -module.exports = [ - "eslint:recommended", - - // add in eslint-plugin-vue and eslint-plugin-example - eslintrc.plugins("vue", "example") -]; -``` - -This example includes both `eslint-plugin-vue` and `eslint-plugin-example` so that all of the rules are available with the correct namespace and processors are automatically hooked up to the correct `files` pattern. +The `ConfigExtender#evaluate()` method accepts a single config object and returns an array of compatible config objects. If the config object doesn't contain `extends` then it just passes that object back in an array. -#### Applying environments +#### Update `FlatConfigArray` to use `ConfigExtender` -The `ESLintRCCompat#env()` method allows users to specify an `env` settings as they would in an `.eslintrc`-style config and have globals automatically added. For example: +The [`normalize()`](https://github.com/eslint/eslint/blob/fd33f1315ac59b1b3828dbab8e1e056a1585eff0/lib/config/flat-config-array.js#L141) and [`normalizeSync()`](https://github.com/eslint/eslint/blob/fd33f1315ac59b1b3828dbab8e1e056a1585eff0/lib/config/flat-config-array.js#L160) methods will be updated to use a `ConfigExtender` instance to evaluate any `extends` keys. To do this, we'll need to evaluate each object for `extends` before allowing calling the superclass method. Here's an example of what `normalize()` will look like: ```js -const { ESLintRCCompat } = require("@eslint/eslintrc"); - -const eslintrc = new ESLintRCCompat(__dirname); - -module.exports = [ - "eslint:recommended", - - // load node environment - eslintrc.env({ - node: true - }) -]; -``` - -### Configuration Location Resolution - -When ESLint is executed, the following steps are taken to find the `eslint.config.js` file to use: - -1. If the `-c` flag is used then the specified configuration file is used. There is no further search performed. -1. Otherwise: - 1. Look for `eslint.config.js` in the current working directory. If found, stop searching and use that file. - 1. If not found, search up the directory hierarchy looking for `eslint.config.js`. - 1. If a `eslint.config.js` file is found at any point, stop searching and use that file. - 1. If `/` is reached without finding `eslint.config.js`, then stop searching and output a "no configuration found" error. - -This approach will allow running ESLint from within a subdirectory of a project and get the same result as when ESLint is run from the project's root directory (the one where `eslint.config.js` is found). - -Some of the key differences from the way ESLint's configuration resolution works today are: - -1. There is no automatic search for `eslint.config.js` in the user's home directory. Users wanting this functionality can either pass a home directory file using `-c` or manually read in that file from their `eslint.config.js` file. -1. Once a `eslint.config.js` file is found, there is no more searching for any further config files. -1. There is no automatic merging of config files using either `extends` or `overrides`. -1. When `-c` is passed on the command line, there is no search performed. - -### File Pattern Resolution - -Because there are file patterns included in `eslint.config.js`, this requires a change to how ESLint decides which files to lint. The process for determining which files to lint is: - -1. When a filename is passed directly (such as `eslint foo.js`): - 1. ESLint checks to see if there is one or more configs where the `files` pattern matches the file that was passed in and does not match the `ignores` pattern. The pattern is evaluated by prepending the directory in which `eslint.config.js` was found to each pattern in `files`. All configs that match `files` and not `ignores` are merged (with the last matching config taking precedence over others). If no config is found, then the file is ignored with an appropriate warning. - 1. If a matching config is found, then the `ignores` pattern is tested against the filename. If it's a match, then the file is ignored. Otherwise, the file is linted. -1. When a glob pattern is passed directly (such as `eslint src/*.js`): - 1. ESLint expands the glob pattern to get a list of files. - 1. Each file is checked individually as in step 1. -1. When a directory is passed directly (such as `eslint src`): - 1. The directory is converted into a glob pattern by appending `**/*` to the directory (such as `src` becomes `src/**/*`). - 1. The glob pattern is checked as in step 2. -1. When a relative directory is passed directly (such as `eslint .`): - 1. The relative directory pattern is resolved to a full directory name. - 1. The glob pattern is checked as in step 3. - -**Note:** ESLint will continue to ignore `node_modules` by default. -### Rename `--no-eslintrc` to `--no-config-file` and `useEslintrc` to `useConfigFile` +const extender = new ConfigExtender(); -Because the config filename has changed, it makes sense to change the command line `--no-eslintrc` flag to a more generic name, `--no-config-file` and change `CLIEngine`'s `useEslintrc` option to `useConfigFile`. In the short term, to avoid a breaking change, these pairs of names can be aliased to each other. +class FlatConfigArray { -### Implementation Details - -The implementation of this feature requires the following changes: + // snip -1. Create a new `ESLintConfigArray` class to manage configs. -1. Create a `--no-config-file` CLI option and alias it to `--no-eslintrc` for backwards compatibility. -1. Create a `useConfigFile` option for `CLIEngine`. Alias `useEslintrc` to this option for backwards compatibility. -1. In `CLIEngine#executeOnFiles()`: - 1. Check for existence of `eslint.config.js`, and if found, opt-in to new behavior. - 1. Create a `ESLintConfigArray` to hold the configuration information and to determine which files to lint (in conjunction with already-existing `globUtils`) - 1. Rename the private functions `processText()` and `processFiles()` to `legacyProcessText()` and `legacyProcessFiles()`; create new versions with the new functionality named `processText()` and `processFiles()`. Use the appropriate functions based on whether or not the user has opted-in. - 1. Update `Linter#verify()` to check for objects on keys that now support objects instead of strings (like `parser`) and add a `disableEnv` property to the options to indicate that environments should not be honored. -1. At a later point, we will be able to remove a lot of the existing configuration utilities. + normalize(context) { -#### The `ESLintConfigArray` Class - -The `ESLintConfigArray` class is the primary new class for handling the configuration change defined in this proposal. - -```js -class ESLintConfigArray extends Array { - - // normalize the current ConfigArray - async normalize(context) {} + // make sure not to make any changes if already normalized + if (this.isNormalized()) { + throw new Error("..."); + } - // get a single config for the given filename - getConfig(filename) {} + // replace each element with an array + this.forEach((element, i) => { + const configs = Array.isArray(element) ? element : [element]; + this[i] = configs.flat().map(config => extender.evaluate(config)) + }); + + // proceed as usual + return super.normalize(context) + .catch(error => { + if (error.name === "ConfigError") { + throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]); + } - // get the file patterns to search for - get files() {} + throw error; - // get the "ignore file" values - get ignores() {} + }); + } } ``` -In this class, "normalize" means that all functions are called and replaced with their results, the array has been flattened, and configs without `files` keys have been merged into configs that do have `files` keys for easier calculation. - ## Documentation -This will require extensive documentation changes and an introductory blog post. +This will require documentation changes and an introductory blog post. -At a minimum, these pages will have to be updated (and rewritten): +At a minimum, these pages will have to be updated: -* https://eslint.org/docs/user-guide/getting-started#configuration -* https://eslint.org/docs/user-guide/configuring -* https://eslint.org/docs/developer-guide/working-with-plugins#configs-in-plugins -* https://eslint.org/docs/developer-guide/shareable-configs +* https://eslint.org/docs/latest/use/getting-started#configuration +* https://eslint.org/docs/latest/use/configure/configuration-files +* https://eslint.org/docs/latest/extend/plugins#configs-in-plugins +* https://eslint.org/docs/latest/use/configure/combine-configs ## Drawbacks -As with any significant change, there are some significant drawbacks: - -1. We'd need to do a phased rollout (see Backwards Compatibility Analysis) to minimize impact on users. That means maintaining two configuration systems for a while, increasing maintenance overhead and complexity of the application. -1. Getting everyone to convert to `eslint.config.js` format would be a significant stress on the community that could cause some resentment. -1. We can no longer enforce naming conventions for plugins and shareable configs. This may make it more difficult for users to find compatible npm packages. -1. Creating configuration files will be more complicated. -1. The `--print-config` option becomes less useful because we can't output objects in a meaningful way. -1. People depending on environments may find this change particularly painful. +1. Introducing a new key to flat config at this point means that users of ESLint v8 won't be able to use it. Even though v8 is EOL, we had made an attempt to provide forward compatibility to make people's transition easier. +1. While this may reduce complexity for users, it increases the complexity of config evaluation inside of ESLint. This necessarily means a performance cost that we won't be able to quantify until implementation. +1. Users will have to know which version of ESLint supports `extends` in order to use it. There really isn't an easy way to feature test this capability other than to inspect the version of the current ESLint package. ## Backwards Compatibility Analysis -The intent of this proposal is to replace the current `.eslintrc` format, but can be implemented incrementally so as to cause as little disturbance to ESLint users as possible. - -In the first phase, I envision this: - -1. Extract all current `.eslintrc` functionality into `@eslint/eslintrc` package. Change ESLint to depend on `@eslint/eslintrc` package. -1. `eslint.config.js` can be implemented alongside `.eslintrc`. -1. If `eslint.config.js` is found, then: - 1. All `.eslintrc` files are ignored. - 1. `--rulesdir` is ignored. - 1. `--env` is ignored. - 1. `--ext` is ignored. - 1. `eslint-env` config comments are ignored. -1. If `eslint.config.js` is not found, then fall back to the current behavior. -1. Switch ESLint itself to use `eslint.config.js` as a way to test and ensure compatibility with existing shareable configs in `.eslintrc` format. +This proposal is additive and does not affect the way existing configurations are evaluated. -This keeps the current behavior for the majority of users while allowing some users to test out the new functionality. Also, `-c` could not be used with `eslint.config.js` in this phase. +## Alternatives -In the second phase (and in a major release), ESLint will emit deprecation warnings whenever the original functionality is used but will still honor them so long as `eslint.config.js` is not found. In this phase, we will work with several high-profile plugins and shareable configs to convert their packages into the new format. We will use this to find the remaining compatibility issues. +1. **A utility package.** Instead of including `extends` in flat config itself, we could create a utility package that provides a method to accomplish the same thing. This has the advantage that ESLint v8 users could also use this utility package, but the downside that it would require users to install Yet Another Package to do something that many feel should be available out of the box. +1. **A utility function.** A variation of the previous approach is to export a function from the `eslint` package that people could use to extend configs. This has the advantage that it would be easier to feature test for but the downside that it still requires an extra step to use `extends`. -In the third phase (and in another major release), `eslint.config.js` becomes the official way to configure ESLint. If no `eslint.config.js` file is found, ESLint will still search for a `.eslintrc` file, and if found, print an error message information the user that the configuration file format has changed. +## Open Questions -So while this is intended to be a breaking change, it will be introduced over the course of three major releases in order to give users ample time to transition. +1. **Should `files` and `ignores` be merged (as in this proposal) or just dropped completely?** I can see an argument for dropping them completely, but I also know that some folks share configs with multiple different `files` patterns in an array that is meant to be used together. +1. **Does the `#` pattern in plugin configs cause any unintended side effects?** The string `"#"` would be an invalid package name, so it's unlikely to cause any naming collisions. +1. **Should `extends` be allowed in plugin configs?** This RFC does not allow `extends` in plugin configs for simplicity. Allowing this would mean the possibility of nested `extends`, which may be desirable but also more challenging to implement. Further, if plugins export configs with `extends`, then that automatically means those configs cannot be used in any earlier versions of ESLint. However, not allowing it also means having different schemas for user-defined and plugin configs, which is a significant downside. -## Alternatives +## Frequently Asked Questions -While there are no alternatives that cover all of the functionality in this RFC, there are alternatives designed to address various parts. +### Why is `"#"` assigned as `null` in the example? -* Both https://github.com/eslint/rfcs/pull/7 and https://github.com/eslint/rfcs/pull/5 specify an alternative method for resolving plugin locations in configs. This attempts to solve the problem with bundling plugins with configs (https://github.com/eslint/eslint/issues/3458). These proposals have the benefit of working within the current configuration system and requiring very few changes from users. -* It is possible to come up with a solution to https://github.com/eslint/eslint/issues/8813 that makes use of `extends`, though this would get a bit complicated if an extended config in an `overrides` section also has `overrides`. -* We could switch the current configuration system over so that all config files are considered to implicitly have `root:true`. This could dramatically simplify configuration searching and merging. +This allows us to very quickly check `plugin.plugins["#"]` to see if there's a `"#"` that needs evaluating. The value actually doesn't matter because it will be replaced, so `null` seemed like a nice shorthand. Without the `"#"` entry in `plugins`, we'd need to search through `rules`, `processor`, `language`, and potentially other future keys to see if `"#"` was referenced. I considered that initially, but I think being explicit is a better idea. -## Open Questions +### Why is this functionality added to `FlatConfigArray` instead of `@eslint/config-array` -1. **Should `files` and `ignores` be merged (as in this proposal) or just dropped completely?** I can see an argument for dropping them completely, but I also know that some folks share configs with multiple different `files` patterns in an array that is meant to be used together. +In order to support named configs, we need the concept of a plugin. The generic `ConfigArray` class has no concept of plugins, which means the functionality needs to live in `FlatConfigArray` in some way. There may be an argument for supporting `extends` with just objects and arrays in `ConfigArray`, with `FlatConfigArray` overriding that to support named configs, but that would increase the complexity of implementation. -## Frequently Asked Questions +If we end up not supporting named configs, then we can revisit this decision. ## Related Discussions -* https://github.com/eslint/rfcs/pull/7 -* https://github.com/eslint/rfcs/pull/5 -* https://github.com/eslint/eslint/issues/3458 -* https://github.com/eslint/eslint/issues/6732 -* https://github.com/eslint/eslint/issues/8813 -* https://github.com/eslint/eslint/issues/9192 -* https://github.com/eslint/eslint/issues/9897 -* https://github.com/eslint/eslint/issues/10125 -* https://github.com/eslint/eslint/issues/10643 -* https://github.com/eslint/eslint/issues/10891 -* https://github.com/eslint/eslint/issues/11223 -* https://github.com/eslint/rfcs/pull/55 +* https://github.com/eslint/eslint/discussions/16960#discussioncomment-11147407 +* https://github.com/eslint/eslint/discussions/16960#discussioncomment-11160226 +* https://github.com/eslint/eslint/issues/18040 +* https://github.com/eslint/eslint/issues/18752 From 839861cbc0ffa99e3184b13003d20afdc1d99548 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 14 Nov 2024 16:58:56 -0500 Subject: [PATCH 03/14] Add PR URL --- designs/2024-config-extends/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md index 0bb6b8bd..91d4ce1d 100644 --- a/designs/2024-config-extends/README.md +++ b/designs/2024-config-extends/README.md @@ -1,5 +1,5 @@ - Start Date: 2024-11-13 -- RFC PR: (todo) +- RFC PR: https://github.com/eslint/rfcs/pull/126 - Authors: Nicholas C. Zakas (@nzakas) - repo: eslint/eslint From d953622a881db850030fe443d3d32938225b7792 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 19 Nov 2024 11:02:58 -0500 Subject: [PATCH 04/14] Change files merging behavior to intersection --- designs/2024-config-extends/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md index 91d4ce1d..df0f8901 100644 --- a/designs/2024-config-extends/README.md +++ b/designs/2024-config-extends/README.md @@ -136,7 +136,7 @@ export default [ ]; ``` -If the objects in `extends` contain `files` or `ignores`, then ESLint will merge those values with the values found in the config using `extends`. For example: +If the objects in `extends` contain `files` or `ignores`, then ESLint will merge those values with the values found in the config using `extends` such that they intersect. For example: ```js import globals from "globals"; @@ -185,7 +185,7 @@ export default [ // combined files { name: "myconfig > config1", - files: ["**/src/*.js", "**/*.cjs"], + files: [["**/src/*.js", "**/*.cjs"]], languageOptions: { sourceType: "commonjs", globals: { @@ -197,7 +197,7 @@ export default [ // combined files and inherited ignores { name: "myconfig > config2", - files: ["**/src/*.js", "**/*.js"], + files: [["**/src/*.js", "**/*.js"]], ignores: ["**/tests"], rules: { "no-console": "error" From b47b7210ec8e4c4268e5198800427d004779eeab Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 19 Nov 2024 11:16:59 -0500 Subject: [PATCH 05/14] Clarify flat(Infinity) --- designs/2024-config-extends/README.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md index df0f8901..0e27b6e3 100644 --- a/designs/2024-config-extends/README.md +++ b/designs/2024-config-extends/README.md @@ -136,14 +136,14 @@ export default [ ]; ``` -If the objects in `extends` contain `files` or `ignores`, then ESLint will merge those values with the values found in the config using `extends` such that they intersect. For example: +If the objects in `extends` contain `files`, then ESLint will intersect those values (AND operation); if the object in `extends` contains `ignores`, then ESLint will merge those values (OR operation). For example: ```js import globals from "globals"; const config1 = { name: "config1", - files: ["**/*.cjs"], + files: ["**/*.cjs.js"], languageOptions: { sourceType: "commonjs", globals: { @@ -167,6 +167,7 @@ export default [ { name: "myconfig", files: ["**/src/*.js"], + ignores: ["**/*.test.js"] extends: [config1, config2], rules: { semi: "error" @@ -176,16 +177,17 @@ export default [ ]; ``` -Here, the `files` keys will be combined and the `ignores` key will be inherited, resulting in a final config that looks like this: +Here, the `files` keys will be combined and the `ignores` key will be merged, resulting in a final config that looks like this: ```js export default [ - // combined files + // intersected files and original ignores { name: "myconfig > config1", - files: [["**/src/*.js", "**/*.cjs"]], + files: [["**/src/*.js", "**/*.cjs.js"]], + ignores: ["**/*.test.js"], languageOptions: { sourceType: "commonjs", globals: { @@ -194,11 +196,11 @@ export default [ } }, - // combined files and inherited ignores + // intersected files and merged ignores { name: "myconfig > config2", files: [["**/src/*.js", "**/*.js"]], - ignores: ["**/tests"], + ignores: ["**/*.test.js", "**/tests"], rules: { "no-console": "error" } @@ -208,6 +210,7 @@ export default [ { name: "myconfig", files: ["**/src/*.js"], + ignores: ["**/*.test.js"] rules: { semi: "error" } @@ -216,11 +219,15 @@ export default [ ]; ``` -Note that the `name` key is generated for calculated config objects so that it indicates the inheritance from another config that doesn't exist in the final representation of the config array. +Notes: + +1. The `files` and `ignores` values from the base config always come first in the calculated `files` and `ignores`. If there's not a match, then there's no point in continuing on to use the patterns from the extended configs. +1. The `name` key is generated for calculated config objects so that it indicates the inheritance from another config. The extended configs don't exist in the final representation of the config array. +1. These behaviors differ from the [typescript-eslint extends helper](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/src/config-helper.ts). #### Extending Arrays -Arrays can also be used in `extends` (to eliminate the guesswork of what type a config is). When evaluating `extends`, ESLint internally calls `.flat()` on the array and then processes the config objects as discussed in the previous example. Consider the following: +Arrays can also be used in `extends` (to eliminate the guesswork of what type a config is). When evaluating `extends`, ESLint internally calls `.flat(Infinity)` on the array and then processes the config objects as discussed in the previous example. Consider the following: ```js import js from "@eslint/js"; @@ -475,7 +482,6 @@ This proposal is additive and does not affect the way existing configurations ar ## Open Questions -1. **Should `files` and `ignores` be merged (as in this proposal) or just dropped completely?** I can see an argument for dropping them completely, but I also know that some folks share configs with multiple different `files` patterns in an array that is meant to be used together. 1. **Does the `#` pattern in plugin configs cause any unintended side effects?** The string `"#"` would be an invalid package name, so it's unlikely to cause any naming collisions. 1. **Should `extends` be allowed in plugin configs?** This RFC does not allow `extends` in plugin configs for simplicity. Allowing this would mean the possibility of nested `extends`, which may be desirable but also more challenging to implement. Further, if plugins export configs with `extends`, then that automatically means those configs cannot be used in any earlier versions of ESLint. However, not allowing it also means having different schemas for user-defined and plugin configs, which is a significant downside. From 5b1264f6616d02df0b7d1d9d01e76c17dd8a951b Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Thu, 21 Nov 2024 12:00:29 -0500 Subject: [PATCH 06/14] Add note about more complex files patterns --- designs/2024-config-extends/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md index 0e27b6e3..a7269cca 100644 --- a/designs/2024-config-extends/README.md +++ b/designs/2024-config-extends/README.md @@ -219,6 +219,32 @@ export default [ ]; ``` +When an extended config and the base config both have multiple `files` entries, then the result is a `files` entry containing all combinations. For example: + +```js +export default [ + { + files: ["src/**", "lib/**"], + extends: [{ files: ["**/*.js", "**/*.mjs"] }] + } +]; +``` + +This config would be calculated as the following: + +```js +export default [ + { + files: [ + ["src/**", "**/*.js"], + ["lib/**", "**/*.js"], + ["src/**", "**/*.mjs"], + ["lib/**", "**/*.mjs"] + ] + } +]; +``` + Notes: 1. The `files` and `ignores` values from the base config always come first in the calculated `files` and `ignores`. If there's not a match, then there's no point in continuing on to use the patterns from the extended configs. From 98d404ec0a67ab9d771c0c0db1c5b6d06417446d Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 25 Nov 2024 11:44:44 -0500 Subject: [PATCH 07/14] Update named config proposal --- designs/2024-config-extends/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md index a7269cca..b6eb94e3 100644 --- a/designs/2024-config-extends/README.md +++ b/designs/2024-config-extends/README.md @@ -366,12 +366,13 @@ export default plugin; Here, we are hardcoding the namespace `json` even though that might not be the namespace that the user assigns to this plugin. This is something we can now address with the use of `extends` because we have the ability to alter the config before inserting it. -Instead of using a hardcoded plugin namespace, plugins can instead use `#` to indicate that they'd like to have the plugin itself included and referenced using the namespace the user assigned. For example, we could rewrite the JSON plugin like this: +Instead of requiring plugin configs to include the plugin in the config, we can have the plugin define a preferred namespace to indicate that any rules, processors, and plugins containing that namespace need to be rewritten using the namespace the user assigned. For example, we could rewrite the JSON plugin like this: ```js export default { meta: { name: "@eslint/json", + namespace: "json", version: "0.6.0", // x-release-please-version }, languages: { @@ -386,10 +387,9 @@ export default { configs: { recommended: { - plugins: { "#": null }, rules: { - "#/no-duplicate-keys": "error", - "#/no-empty-keys": "error", + "json/no-duplicate-keys": "error", + "json/no-empty-keys": "error", }, }, }, @@ -415,7 +415,7 @@ export default [ ]; ``` -Here, the user has assigned the namespace `j` to the JSON plugin. When ESLint loads this config, it sees `extends` with a value of `"j/recommended"`. It looks up the plugin referenced by `j` and then the config named `"recommended"`. It then sees that there's a plugin entry for `"#"` and replaces that with an entry for `j` that points to the JSON plugin. The next step is to look through the config for all the `#` references and replace that with `j` so the references are correct. +Here, the user has assigned the namespace `j` to the JSON plugin. When ESLint loads this config, it sees `extends` with a value of `"j/recommended"`. It looks up the plugin referenced by `j` and then the config named `"recommended"`. It then inserts an entry for `j` in `plugins` that references the plugin. The next step is to look through the config for all the `json` references and replace that with `j` so the references are correct (which we can determine by looking at `meta.namespace`). ### Implementation Details From d56e2d70451ebe6f7922a959bdaf2eac19c499a0 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 25 Nov 2024 14:08:28 -0500 Subject: [PATCH 08/14] Remove unnecessary questions --- designs/2024-config-extends/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md index b6eb94e3..5ae071b0 100644 --- a/designs/2024-config-extends/README.md +++ b/designs/2024-config-extends/README.md @@ -508,14 +508,10 @@ This proposal is additive and does not affect the way existing configurations ar ## Open Questions -1. **Does the `#` pattern in plugin configs cause any unintended side effects?** The string `"#"` would be an invalid package name, so it's unlikely to cause any naming collisions. 1. **Should `extends` be allowed in plugin configs?** This RFC does not allow `extends` in plugin configs for simplicity. Allowing this would mean the possibility of nested `extends`, which may be desirable but also more challenging to implement. Further, if plugins export configs with `extends`, then that automatically means those configs cannot be used in any earlier versions of ESLint. However, not allowing it also means having different schemas for user-defined and plugin configs, which is a significant downside. ## Frequently Asked Questions -### Why is `"#"` assigned as `null` in the example? - -This allows us to very quickly check `plugin.plugins["#"]` to see if there's a `"#"` that needs evaluating. The value actually doesn't matter because it will be replaced, so `null` seemed like a nice shorthand. Without the `"#"` entry in `plugins`, we'd need to search through `rules`, `processor`, `language`, and potentially other future keys to see if `"#"` was referenced. I considered that initially, but I think being explicit is a better idea. ### Why is this functionality added to `FlatConfigArray` instead of `@eslint/config-array` From 194da838d2f6247933e5ce448bb29498279ff827 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 27 Nov 2024 11:50:38 -0500 Subject: [PATCH 09/14] Add FAQs about meta.namespace --- designs/2024-config-extends/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md index 5ae071b0..861b0b62 100644 --- a/designs/2024-config-extends/README.md +++ b/designs/2024-config-extends/README.md @@ -512,13 +512,20 @@ This proposal is additive and does not affect the way existing configurations ar ## Frequently Asked Questions - ### Why is this functionality added to `FlatConfigArray` instead of `@eslint/config-array` In order to support named configs, we need the concept of a plugin. The generic `ConfigArray` class has no concept of plugins, which means the functionality needs to live in `FlatConfigArray` in some way. There may be an argument for supporting `extends` with just objects and arrays in `ConfigArray`, with `FlatConfigArray` overriding that to support named configs, but that would increase the complexity of implementation. If we end up not supporting named configs, then we can revisit this decision. +### What will happen for plugins that don't define `meta.namespace`? + +They will be treated the same as they are now. + +### Will plugins eventually be required to define `meta.namespace`? + +No. This is a feature we want to encourage but will not require. We want all existing plugins to continue working. + ## Related Discussions * https://github.com/eslint/eslint/discussions/16960#discussioncomment-11147407 From 641ea073b5fea48d9d064da8855ac01fb4aad8cc Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 27 Nov 2024 12:26:26 -0500 Subject: [PATCH 10/14] Explain differences between tsconfig and this RFC --- designs/2024-config-extends/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md index 861b0b62..2feb2326 100644 --- a/designs/2024-config-extends/README.md +++ b/designs/2024-config-extends/README.md @@ -249,7 +249,7 @@ Notes: 1. The `files` and `ignores` values from the base config always come first in the calculated `files` and `ignores`. If there's not a match, then there's no point in continuing on to use the patterns from the extended configs. 1. The `name` key is generated for calculated config objects so that it indicates the inheritance from another config. The extended configs don't exist in the final representation of the config array. -1. These behaviors differ from the [typescript-eslint extends helper](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/src/config-helper.ts). +1. These behaviors differ from the [typescript-eslint extends helper](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/src/config-helper.ts). The `tsconfig` helper overwrites the `files` and `ignores` fields on any config contained in `extends` whereas this proposal creates combination `files` and `ignores`, merging the base config with the extended configs. #### Extending Arrays From a4fd18ce03120deece7dd65ee124077c5835461b Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 2 Dec 2024 11:29:45 -0500 Subject: [PATCH 11/14] Fix tsconfig -> tseslint.config --- designs/2024-config-extends/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md index 2feb2326..57720384 100644 --- a/designs/2024-config-extends/README.md +++ b/designs/2024-config-extends/README.md @@ -249,7 +249,7 @@ Notes: 1. The `files` and `ignores` values from the base config always come first in the calculated `files` and `ignores`. If there's not a match, then there's no point in continuing on to use the patterns from the extended configs. 1. The `name` key is generated for calculated config objects so that it indicates the inheritance from another config. The extended configs don't exist in the final representation of the config array. -1. These behaviors differ from the [typescript-eslint extends helper](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/src/config-helper.ts). The `tsconfig` helper overwrites the `files` and `ignores` fields on any config contained in `extends` whereas this proposal creates combination `files` and `ignores`, merging the base config with the extended configs. +1. These behaviors differ from the [typescript-eslint extends helper](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/src/config-helper.ts). The `tseslint.config()` helper overwrites the `files` and `ignores` fields on any config contained in `extends` whereas this proposal creates combination `files` and `ignores`, merging the base config with the extended configs. #### Extending Arrays From 2ab7f006f9e63d8c6d75997933ce2b60abdb6c69 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Mon, 2 Dec 2024 11:58:31 -0500 Subject: [PATCH 12/14] Update alternatives --- designs/2024-config-extends/README.md | 83 ++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md index 57720384..20a98fe4 100644 --- a/designs/2024-config-extends/README.md +++ b/designs/2024-config-extends/README.md @@ -503,8 +503,87 @@ This proposal is additive and does not affect the way existing configurations ar ## Alternatives -1. **A utility package.** Instead of including `extends` in flat config itself, we could create a utility package that provides a method to accomplish the same thing. This has the advantage that ESLint v8 users could also use this utility package, but the downside that it would require users to install Yet Another Package to do something that many feel should be available out of the box. -1. **A utility function.** A variation of the previous approach is to export a function from the `eslint` package that people could use to extend configs. This has the advantage that it would be easier to feature test for but the downside that it still requires an extra step to use `extends`. + +### A `defineConfig()` function + +One alternative is to create a `defineConfig()` function that is exported from the `eslint` package. All of the functionality described in this RFC could be implemented in that function, leaving both `ConfigArray` and `FlatConfigArray` in their current state without the need for changes. + +```js +import globals from "globals"; +import { defineConfig } from "eslint"; + +const config1 = { + name: "config1", + files: ["**/*.cjs.js"], + languageOptions: { + sourceType: "commonjs", + globals: { + ...globals.node + } + } +}; + +const config2 = { + name: "config2", + files: ["**/*.js"], + ignores: ["**/tests"], + rules: { + "no-console": "error" + } +}; + + +export default defineConfig([ + + { + name: "myconfig", + files: ["**/src/*.js"], + ignores: ["**/*.test.js"] + extends: [config1, config2], + rules: { + semi: "error" + } + } + +]); +``` + + +Benefits to this approach: + +1. **No changes to `FlatConfigArray` or `ConfigArray`.** All of the changes are contained within the `defineConfig()` function. +1. **Config Inspector just works.** There would be no additional changes necessary for Config Inspector as it would still receive a flat array. +1. **Familiar approach.** Many other tools support a `defineConfig()` function, so this won't seem unusual to ESLint users. Examples include: [Rollup](https://rollupjs.org/command-line-interface/#config-intellisense), [Astro](https://docs.astro.build/en/guides/configuring-astro/#the-astro-config-file), [Vite](https://vite.dev/config/#config-intellisense), [Nuxt](https://nuxt.com/docs/getting-started/configuration). +1. **Type intellisense support.** Most other tools are using `defineConfig()` for intellisense support in editors. We've had [two](https://github.com/eslint/eslint/issues/16874) [requests](https://github.com/eslint/eslint/issues/14249) to add this intellisense support in the past. +1. **Ability to distribute in a separate package.** If we want to provide this functionality to earlier versions of ESLint that support flat config, we can do so by including in a separate package (maybe `@eslint/config`), then `eslint` could rely on that package to transparently pass `defineConfig` to users. +1. **Cleaner file than using a utility function.** This allows a more JSON-like appear to the overall configuration file, which makes it easier to read than if we provided a utility function such as `extends()`, with which users must intermix that with regular objects where necessary. + +Downsides of this approach: + +1. **Different config object formats depending on where used.** The `extends` key would only be valid for the argument to `defineConfig()` and not as part of the standard config object interface. Perhaps `tseslint.config()` points to that not being a problem, but it is still worth consideration. +1. **Tying `defineConfig` to the `eslint` version.** If we encourage people to use `defineConfig` from `eslint`, then any changes or fixes will mean typing the config file to a particular version of `eslint`. (Or else encouraging people to use an `@eslint/config` package all the time, which I think isn't user-friendly.) +1. **Config Inspector doesn't have deep insights into `extends`.** Because Config Inspector will just receive a flat array, it would have insights into how `extends` is being used. Perhaps it could use the name of calculated configs to approximate that information, or else maybe we need to add some kind of symbol property to the result objects to provide this information. + +### An `extend()` function + +As [mentioned by Kirk Waiblinger](https://github.com/eslint/rfcs/pull/126/files#r1860258048), we could also do a smaller version of the `defineConfig` approach by just defining an `extend()` function that users could use whenever they want to extend a config, such as: + +```js +import { extend } from "eslint"; +import js from "@eslint/js"; +import example from "eslint-plugin-example"; + +export default [ + extend( + { + files: ["**/src/*.js"], + }, + [js.configs.recommended, example.configs.recommended], + ), +]; +``` + +This has similar upsides and downsides to `defineConfig`, with the additional downside that this moves the config further away from the JSON-like visual that makes it easier to scan config files. ## Open Questions From 4c213d057012d5325702a294040e719ba527f440 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 10 Dec 2024 11:26:27 -0500 Subject: [PATCH 13/14] Rewrite as defineConfig --- designs/2024-config-extends/README.md | 283 +++++++++++++++----------- 1 file changed, 169 insertions(+), 114 deletions(-) diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md index 20a98fe4..98a9c26c 100644 --- a/designs/2024-config-extends/README.md +++ b/designs/2024-config-extends/README.md @@ -87,14 +87,95 @@ There are two goals with this design: Design Summary: +1. Introduce a `defineConfig()` function 1. Allow arrays in config arrays (in addition to objects) 1. Introduce an `extends` key in config objects +### Introduce a `defineConfig()` Function + +Rather than changing either `FlatConfigArray` or `ConfigArray`, we'll create a `defineConfig()` function that will contain all of the functionality described in this RFC. Many other tools support a `defineConfig()` function, so this won't seem unusual to ESLint users. Examples include: [Rollup](https://rollupjs.org/command-line-interface/#config-intellisense), [Astro](https://docs.astro.build/en/guides/configuring-astro/#the-astro-config-file), [Vite](https://vite.dev/config/#config-intellisense), [Nuxt](https://nuxt.com/docs/getting-started/configuration). While those tools use `defineConfig()` primarily as a means to support editor Intellisense, we will also use it an abstraction layer between the base flat config behavior and the desired new behavior. + +The `defineConfig()` function will be defined in a new `@eslint/config` package contained in the [rewrite](https://github.com/eslint/rewrite) repository. The `eslint` package will depend on `@eslint/config` and export `defineConfig()` directly, so users of ESLint versions that support `defineConfig()` natively will not have to install a separate package. For older versions of ESLint, they'll still be able to use `defineConfig()` by manually installing the `@eslint/config` package. + +The `defineConfig()` function can accept both objects and arrays, and will flatten its arguments to create a final, flat array. As a rough sketch: + +```js +export function defineConfig(...configs) { + + const finalConfigs = configs.map(processConfig); + + return finalConfigs.flat(Infinity); +} +``` + +The intent is to allow both of the following uses: + +```js +// use case 1: array +import { defineConfig } from "eslint"; +import js from "@eslint/js"; + +export default defineConfig([ + { + ...js.configs.recommended, + files: ["**/src/safe/*.js"] + }, + { + files: ["**/*.cjs"], + languageOptions: { + sourceType: "commonjs" + } + } +]); +``` + +```js +// use case 2: multiple arguments +import { defineConfig } from "eslint"; +import js from "@eslint/js"; + +export default defineConfig( + { + ...js.configs.recommended, + files: ["**/src/safe/*.js"] + }, + { + files: ["**/*.cjs"], + languageOptions: { + sourceType: "commonjs" + } + } +); +``` + ### Allow Arrays in Config Arrays -This was part of the original RFC but was disabled over concerns about the complexity involved with importing configs from packages and having no idea whether it was an object, an array, or an array with a mix of objects and arrays. This mattered most when users needed to use `map()` to apply different `files` or `ignores` to configs, but with `extends`, that concern goes away. +This was part of the [original flat config RFC](https://github.com/eslint/rfcs/tree/main/designs/2019-config-simplification#extending-another-config) but was disabled over concerns about the complexity involved with importing configs from packages and having no idea whether it was an object, an array, or an array with a mix of objects and arrays. This mattered most when users needed to use `map()` to apply different `files` or `ignores` to configs, but with `extends`, that concern goes away. + +The flattening logic will live in `defineConfig()` rather than turning on flattening with `ConfigArray`. This allows users of older ESLint versions to also have access to this functionality. The first example in this RFC can be rewritten as: + +```js +import { defineConfig } from "eslint"; +import js from "@eslint/js"; +import tailwind from "eslint-plugin-tailwindcss"; +import reactPlugin from "eslint-plugin-react"; +import eslintPluginImportX from 'eslint-plugin-import-x' -Support for nested arrays is already in `ConfigArray`, it's just disabled. +export default defineConfig([ + + // object + js.configs.recommended, + + // array + tailwind.configs["flat/recommended"], + + // array + reactPlugin.configs.flat.recommended, + + // object + eslintPluginImportX.flatConfigs.recommended, +]); +``` ### Introduce an `extends` Key @@ -105,26 +186,28 @@ The `extends` key is an array that may contain other configurations. When used, You can pass one or more objects in `extends`, like this: ```js +import { defineConfig } from "eslint"; import js from "@eslint/js"; import example from "eslint-plugin-example"; -export default [ +export default defineConfig([ { files: ["**/src/*.js"], extends: [js.configs.recommended, example.configs.recommended] } -]; +]); ``` Assuming these config objects do not contain `files` or `ignores`, ESLint will convert this config into the following: ```js +import { defineConfig } from "eslint"; import js from "@eslint/js"; import example from "eslint-plugin-example"; -export default [ +export default defineConfig([ { ...js.configs.recommended files: ["**/src/*.js"], @@ -133,12 +216,13 @@ export default [ ...example.configs.recommended files: ["**/src/*.js"], }, -]; +]); ``` If the objects in `extends` contain `files`, then ESLint will intersect those values (AND operation); if the object in `extends` contains `ignores`, then ESLint will merge those values (OR operation). For example: ```js +import { defineConfig } from "eslint"; import globals from "globals"; const config1 = { @@ -162,7 +246,7 @@ const config2 = { }; -export default [ +export default defineConfig([ { name: "myconfig", @@ -174,7 +258,7 @@ export default [ } } -]; +]); ``` Here, the `files` keys will be combined and the `ignores` key will be merged, resulting in a final config that looks like this: @@ -222,12 +306,14 @@ export default [ When an extended config and the base config both have multiple `files` entries, then the result is a `files` entry containing all combinations. For example: ```js -export default [ +import { defineConfig } from "eslint"; + +export default defineConfig([ { files: ["src/**", "lib/**"], extends: [{ files: ["**/*.js", "**/*.mjs"] }] } -]; +]); ``` This config would be calculated as the following: @@ -251,15 +337,17 @@ Notes: 1. The `name` key is generated for calculated config objects so that it indicates the inheritance from another config. The extended configs don't exist in the final representation of the config array. 1. These behaviors differ from the [typescript-eslint extends helper](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/src/config-helper.ts). The `tseslint.config()` helper overwrites the `files` and `ignores` fields on any config contained in `extends` whereas this proposal creates combination `files` and `ignores`, merging the base config with the extended configs. + #### Extending Arrays Arrays can also be used in `extends` (to eliminate the guesswork of what type a config is). When evaluating `extends`, ESLint internally calls `.flat(Infinity)` on the array and then processes the config objects as discussed in the previous example. Consider the following: ```js +import { defineConfig } from "eslint"; import js from "@eslint/js"; import example from "eslint-plugin-example"; -export default [ +export default defineConfig([ { files: ["**/src/*.js"], @@ -268,48 +356,71 @@ export default [ ] } -]; +]); ``` This is equivalent to the following: ```js +import { defineConfig } from "eslint"; import js from "@eslint/js"; import example from "eslint-plugin-example"; -export default [ +export default defineConfig([ { files: ["**/src/*.js"], extends: [js.configs.recommended, example.configs.recommended] } -]; +]); ``` The extended objects are evaluated in the same order and result in the same final config. #### Extending Named Configs -While the previous examples are an improvement over current situation, we can go a step further and restore the use of named configs by allowing strings inside of the `extends` array. When provided, the string must refer to a config contained in a plugin. For example: +While the previous examples are an improvement over current situation, we can go a step further and restore the use of named configs by allowing strings inside of the `extends` array. In eslintrc, you could do something like this: + +```json +{ + "plugins": [ + "react" + ], + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "rules": { + "react/no-set-state": "off" + } +} +``` + +The `plugin:` prefix was necessary because plugin configs were implemented after shared configs, and we needed a way to differentiate. The special `"eslint:recommended"` string was necessary to differentiate it from both shareable configs and plugin configs. This made the eslintrc version of `extends` a bit messy. + +With flat config, however, we can fix that by omitting the `plugin:` prefix and we no longer need `"eslint:recommended"`. (Packages that only export a single config can be placed directly into the `extends` array.) The same config written using this proposal looks like this: ```js +import { defineConfig } from "eslint"; import js from "@eslint/js"; +import react from "eslint-plugin-react"; -export default [ - +export default defineConfig([ { plugins: { - js + js, + react }, - files: ["**/src/*.js"], - extends: ["js/recommended"] + extends: ["js/recommended", "react/recommended"], + rules: { + "react/no-set-state": "off" + } } - -]; +]); ``` -Here, `js/recommended` refers to the plugin defined as `js`. Internally, ESLint will look up the plugin with the name `js`, looks at the `configs` key, and retrieve the `recommended` key as a replacement for the string `"js/recommended"`. +Here, `"js/recommended"` refers to the plugin defined as `js`. Internally, ESLint will look up the plugin with the name `js`, looks at the `configs` key, and retrieve the `recommended` key as a replacement for the string `"js/recommended"`. The same process occurs for `"react/recommended"`. This has several advantages: @@ -399,9 +510,10 @@ export default { In an `eslint.config.js` file: ```js +import { defineConfig } from "eslint"; import json from "@eslint/json"; -export default [ +export default defineConfig([ { plugins: { j: json @@ -412,73 +524,12 @@ export default [ "j/no-empty-keys": "warn" } } -]; -``` - -Here, the user has assigned the namespace `j` to the JSON plugin. When ESLint loads this config, it sees `extends` with a value of `"j/recommended"`. It looks up the plugin referenced by `j` and then the config named `"recommended"`. It then inserts an entry for `j` in `plugins` that references the plugin. The next step is to look through the config for all the `json` references and replace that with `j` so the references are correct (which we can determine by looking at `meta.namespace`). - -### Implementation Details - -The implementation of this feature requires the following changes: - -1. Enable nested arrays in `FlatConfigArray` -1. Create a new `ConfigExtender` class that encapsulates the functionality for extending configs. -1. Update `FlatConfigArray` to use `ConfigExtender` inside of the `normalize()` and `normalizeSync()` methods. - -#### Enable Nested Arrays in `FlatConfigArray` - -Pass [`extraConfigTypes: ["array"]`](https://github.com/eslint/rewrite/blob/a957ee351c27ac1bf22966768cf8aac8c12ce0d2/packages/config-array/src/config-array.js#L572) to [`super()`](https://github.com/eslint/eslint/blob/fd33f1315ac59b1b3828dbab8e1e056a1585eff0/lib/config/flat-config-array.js#L95-L98) to enable nested arrays. - -#### The `ConfigExtender` Class - -The `ConfigExtender` class is the primary new class for handling the `extends` key in config objects. - -```ts -interface ConfigExtender { - evaluate(config: ConfigObject): Array; -} +]); ``` -The `ConfigExtender#evaluate()` method accepts a single config object and returns an array of compatible config objects. If the config object doesn't contain `extends` then it just passes that object back in an array. - -#### Update `FlatConfigArray` to use `ConfigExtender` - -The [`normalize()`](https://github.com/eslint/eslint/blob/fd33f1315ac59b1b3828dbab8e1e056a1585eff0/lib/config/flat-config-array.js#L141) and [`normalizeSync()`](https://github.com/eslint/eslint/blob/fd33f1315ac59b1b3828dbab8e1e056a1585eff0/lib/config/flat-config-array.js#L160) methods will be updated to use a `ConfigExtender` instance to evaluate any `extends` keys. To do this, we'll need to evaluate each object for `extends` before allowing calling the superclass method. Here's an example of what `normalize()` will look like: - -```js - -const extender = new ConfigExtender(); - -class FlatConfigArray { - - // snip - - normalize(context) { - - // make sure not to make any changes if already normalized - if (this.isNormalized()) { - throw new Error("..."); - } +Here, the user has assigned the namespace `j` to the JSON plugin. When ESLint loads this config, it sees `extends` with a value of `"j/recommended"`. It looks up the plugin referenced by `j` and then the config named `"recommended"`. It then inserts an entry for `j` in `plugins` that references the plugin. The next step is to look through the config for all the `json` references and replace that with `j` so the references are correct (which we can determine by looking at `meta.namespace`). - // replace each element with an array - this.forEach((element, i) => { - const configs = Array.isArray(element) ? element : [element]; - this[i] = configs.flat().map(config => extender.evaluate(config)) - }); - - // proceed as usual - return super.normalize(context) - .catch(error => { - if (error.name === "ConfigError") { - throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]); - } - - throw error; - - }); - } -} -``` +**Note:** The `meta.namespace` value is also valuable outside of this use case. If we ever run into a case where someone has used `json/` as a prefix for a rule, language, or processor, but has only ever namespaced the plugin as `j`, we can then search through registered plugins to see if the JSON plugin is included elsewhere. This gives us the opportunity to let the user know of their error with instructions on how to fix it. ## Documentation @@ -495,22 +546,24 @@ At a minimum, these pages will have to be updated: 1. Introducing a new key to flat config at this point means that users of ESLint v8 won't be able to use it. Even though v8 is EOL, we had made an attempt to provide forward compatibility to make people's transition easier. 1. While this may reduce complexity for users, it increases the complexity of config evaluation inside of ESLint. This necessarily means a performance cost that we won't be able to quantify until implementation. -1. Users will have to know which version of ESLint supports `extends` in order to use it. There really isn't an easy way to feature test this capability other than to inspect the version of the current ESLint package. +1. Users will have to know which version of ESLint supports `defineConfig` in order to use it. There really isn't an easy way to feature test this capability other than to inspect the version of the current ESLint package. +1. We'll be introducing a difference in the expected config object format from what ESLint uses internally. Specifically, only `defineConfig()` arguments will support `extends`, so anyone who chooses not to use `defineConfig()` might be confused as to why `extends` doesn't work. +1. We'll create another package to maintain and need to coordinate releases with the `eslint` package. ## Backwards Compatibility Analysis -This proposal is additive and does not affect the way existing configurations are evaluated. +With `defineConfig()` available in the `@eslint/config` package, users of flat config in ESLint v8.x and earlier v9.x will be able to use this functionality even though they'll need to manually install the package. -## Alternatives +There are no breaking changes in this proposal. +## Alternatives -### A `defineConfig()` function +### Enable `extends` and Flattening in ESLint Directly -One alternative is to create a `defineConfig()` function that is exported from the `eslint` package. All of the functionality described in this RFC could be implemented in that function, leaving both `ConfigArray` and `FlatConfigArray` in their current state without the need for changes. +The original proposal for this RFC was to add `extends` into the implementation of `FlatConfigArray` (or alternatively in `ConfigArray`). This approach would allow writing a config file such as this: ```js import globals from "globals"; -import { defineConfig } from "eslint"; const config1 = { name: "config1", @@ -533,7 +586,7 @@ const config2 = { }; -export default defineConfig([ +export default [ { name: "myconfig", @@ -545,28 +598,20 @@ export default defineConfig([ } } -]); +]; ``` +The benefit of this approach is that everything is still controlled inside of ESLint itself. There's nothing additional to learn as a user aside from using the `extends` key. -Benefits to this approach: +However, there were several identified problems with this approach: -1. **No changes to `FlatConfigArray` or `ConfigArray`.** All of the changes are contained within the `defineConfig()` function. -1. **Config Inspector just works.** There would be no additional changes necessary for Config Inspector as it would still receive a flat array. -1. **Familiar approach.** Many other tools support a `defineConfig()` function, so this won't seem unusual to ESLint users. Examples include: [Rollup](https://rollupjs.org/command-line-interface/#config-intellisense), [Astro](https://docs.astro.build/en/guides/configuring-astro/#the-astro-config-file), [Vite](https://vite.dev/config/#config-intellisense), [Nuxt](https://nuxt.com/docs/getting-started/configuration). -1. **Type intellisense support.** Most other tools are using `defineConfig()` for intellisense support in editors. We've had [two](https://github.com/eslint/eslint/issues/16874) [requests](https://github.com/eslint/eslint/issues/14249) to add this intellisense support in the past. -1. **Ability to distribute in a separate package.** If we want to provide this functionality to earlier versions of ESLint that support flat config, we can do so by including in a separate package (maybe `@eslint/config`), then `eslint` could rely on that package to transparently pass `defineConfig` to users. -1. **Cleaner file than using a utility function.** This allows a more JSON-like appear to the overall configuration file, which makes it easier to read than if we provided a utility function such as `extends()`, with which users must intermix that with regular objects where necessary. - -Downsides of this approach: - -1. **Different config object formats depending on where used.** The `extends` key would only be valid for the argument to `defineConfig()` and not as part of the standard config object interface. Perhaps `tseslint.config()` points to that not being a problem, but it is still worth consideration. -1. **Tying `defineConfig` to the `eslint` version.** If we encourage people to use `defineConfig` from `eslint`, then any changes or fixes will mean typing the config file to a particular version of `eslint`. (Or else encouraging people to use an `@eslint/config` package all the time, which I think isn't user-friendly.) -1. **Config Inspector doesn't have deep insights into `extends`.** Because Config Inspector will just receive a flat array, it would have insights into how `extends` is being used. Perhaps it could use the name of calculated configs to approximate that information, or else maybe we need to add some kind of symbol property to the result objects to provide this information. +1. **Older ESLint versions locked out.** Users would have to upgrade to the latest ESLint version to use `extends`, meaning that folks who were still on ESLint v8.x or earlier v9.x would not be able to use this functionality at all. +1. **Ecosystem compatibility.** There was a danger that plugins could export configs with `extends`, which users in earlier ESLint versions would attempt to use and get an error. Because support for `extends` would be tied to a specific ESLint version, that made it more difficult to create a config that would work in all ESLint versions that support flat config. +1. **Config Inspector compatibility.** The original plan to implement the functionality in `FlatConfigArray` posed a problem because `FlatConfigArray` is not exported from the `eslint` package. We would either have to export `FlatConfigArray` or implement the functionality in `ConfigArray`. However, `ConfigArray`, being a generic base class, has no concept of plugins, so we'd either have to introduce that concept or not implemented name config extends. There were a lot of different tradeoffs to consider here. ### An `extend()` function -As [mentioned by Kirk Waiblinger](https://github.com/eslint/rfcs/pull/126/files#r1860258048), we could also do a smaller version of the `defineConfig` approach by just defining an `extend()` function that users could use whenever they want to extend a config, such as: +As [mentioned by Kirk Waiblinger](https://github.com/eslint/rfcs/pull/126/files#r1860258048), we could also do a smaller version of `defineConfig()` approach by just defining an `extend()` function that users could use whenever they want to extend a config, such as: ```js import { extend } from "eslint"; @@ -580,6 +625,12 @@ export default [ }, [js.configs.recommended, example.configs.recommended], ), + { + files: ["**/*.cjs"], + languageOptions: { + sourceType: "commonjs" + } + } ]; ``` @@ -587,15 +638,19 @@ This has similar upsides and downsides to `defineConfig`, with the additional do ## Open Questions -1. **Should `extends` be allowed in plugin configs?** This RFC does not allow `extends` in plugin configs for simplicity. Allowing this would mean the possibility of nested `extends`, which may be desirable but also more challenging to implement. Further, if plugins export configs with `extends`, then that automatically means those configs cannot be used in any earlier versions of ESLint. However, not allowing it also means having different schemas for user-defined and plugin configs, which is a significant downside. +1. **Is `@eslint/config` the right package name?** We could name it `@eslint/define-config`, but that seems like a name that implies `defineConfig()` will be the only export for that package. Using `@eslint/config` seems like it would give us the flexibility to add different kinds of helpers in the future without needing to create a separate package for each. I'm still unsure if this is the correct choice. +1. **Is the `>` character a good representation of "extends" in `name`?** Is that unique enough? Should we use the string `"extends"` instead? Or something else? +1. **How should Config Inspector show extended configs?** Right now, Config Inspector would receive the already-flattened config array. It could infer from the `>` in config names which configs were related to one another, but should how would that work when a config didn't have a name? Should we maybe provide additional data in the form of a symbol property config objects that Config Inspector can read to determine the relationship between config objects? And would Config Inspector alter its view in some way to show this relationship? ## Frequently Asked Questions -### Why is this functionality added to `FlatConfigArray` instead of `@eslint/config-array` +### Why are named configs a part of this RFC instead of a separate one? -In order to support named configs, we need the concept of a plugin. The generic `ConfigArray` class has no concept of plugins, which means the functionality needs to live in `FlatConfigArray` in some way. There may be an argument for supporting `extends` with just objects and arrays in `ConfigArray`, with `FlatConfigArray` overriding that to support named configs, but that would increase the complexity of implementation. +Named configs are included here for three reasons: -If we end up not supporting named configs, then we can revisit this decision. +1. It helps to normalize the application of extended configs. +1. It encourages plugins to export configs on the `configs` key. +1. It directly affects how this proposal is implemented. ### What will happen for plugins that don't define `meta.namespace`? From 9f4a892c1ef3f4803c61eaf4ccf0653de84b78f7 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Tue, 10 Dec 2024 14:46:23 -0500 Subject: [PATCH 14/14] More details on merging behavior and FAQs --- designs/2024-config-extends/README.md | 294 +++++++++++++++++++++----- 1 file changed, 244 insertions(+), 50 deletions(-) diff --git a/designs/2024-config-extends/README.md b/designs/2024-config-extends/README.md index 98a9c26c..0c5d5ce7 100644 --- a/designs/2024-config-extends/README.md +++ b/designs/2024-config-extends/README.md @@ -83,6 +83,7 @@ There are two goals with this design: 1. **Make it easier to configure ESLint.** For users, we want to reduce unnecessarily boilerplate and guesswork as much as possible. 1. **Encourage plugins to use `configs`.** For plugin authors, we want to encourage the use of a consistent entrypoint, `configs`, where users can find predefined configurations. + ## Detailed Design Design Summary: @@ -91,6 +92,12 @@ Design Summary: 1. Allow arrays in config arrays (in addition to objects) 1. Introduce an `extends` key in config objects +### Definitions + +* **Base Object** - an object containing the `extends` key. +* **Extended Object** - an object that is a member of an `extends` array. +* **Generated Object** - an object that results from applying an extended object to a base object. + ### Introduce a `defineConfig()` Function Rather than changing either `FlatConfigArray` or `ConfigArray`, we'll create a `defineConfig()` function that will contain all of the functionality described in this RFC. Many other tools support a `defineConfig()` function, so this won't seem unusual to ESLint users. Examples include: [Rollup](https://rollupjs.org/command-line-interface/#config-intellisense), [Astro](https://docs.astro.build/en/guides/configuring-astro/#the-astro-config-file), [Vite](https://vite.dev/config/#config-intellisense), [Nuxt](https://nuxt.com/docs/getting-started/configuration). While those tools use `defineConfig()` primarily as a means to support editor Intellisense, we will also use it an abstraction layer between the base flat config behavior and the desired new behavior. @@ -200,106 +207,177 @@ export default defineConfig([ ]); ``` -Assuming these config objects do not contain `files` or `ignores`, ESLint will convert this config into the following: +How these configs are merged depends on what `files` and `ignores` are present on both the base object and the extended objects. + +##### Base Objects and Extended Objects without `files` or `ignores` + +If the base object and the extended objects don't have any `files` or `ignores`, then the objects are placed directly into the config array with the extended objects first and the base object last. For example: ```js import { defineConfig } from "eslint"; -import js from "@eslint/js"; -import example from "eslint-plugin-example"; + +const config1 = { + rules: { + semi: "error"; + } +}; + +const config2 = { + rules: { + "prefer-const": "error" + } +}; export default defineConfig([ { - ...js.configs.recommended - files: ["**/src/*.js"], + extends: [config1, config2], + rules: { + "no-console": "error" + } + } +]); +``` + +This config would result in the following array: + +```js +export default [ + { + rules: { + semi: "error"; + } }, { - ...example.configs.recommended - files: ["**/src/*.js"], + rules: { + "prefer-const": "error" + } }, + { + rules: { + "no-console": "error" + } + } +]; +``` + +##### Base Object with `files` and `ignores` and Extended Objects without `files` or `ignores` + +When the base object has `files` and/or `ignores` and the extended objects do not, the extended objects are added with the same `files` and `ignores` as the base object. For example: + +```js +import { defineConfig } from "eslint"; + +const config1 = { + rules: { + semi: "error"; + } +}; + +const config2 = { + rules: { + "prefer-const": "error" + } +}; + +export default defineConfig([ + { + files: ["src/*.js"], + ignores: ["src/__tests/**/*.js"], + extends: [config1, config2], + rules: { + "no-console": "error" + } + } ]); ``` +This config would result in the following array: + +```js +export default [ + { + files: ["src/*.js"], + ignores: ["src/__tests/**/*.js"], + rules: { + semi: "error"; + } + }, + { + files: ["src/*.js"], + ignores: ["src/__tests/**/*.js"], + rules: { + "prefer-const": "error" + } + }, + { + files: ["src/*.js"], + ignores: ["src/__tests/**/*.js"], + rules: { + "no-console": "error" + } + } +]; +``` + +##### Base Object with `files` and `ignores` and Extended Object with `files` or `ignores` + If the objects in `extends` contain `files`, then ESLint will intersect those values (AND operation); if the object in `extends` contains `ignores`, then ESLint will merge those values (OR operation). For example: ```js import { defineConfig } from "eslint"; -import globals from "globals"; const config1 = { - name: "config1", files: ["**/*.cjs.js"], - languageOptions: { - sourceType: "commonjs", - globals: { - ...globals.node - } + rules: { + semi: "error"; } }; const config2 = { - name: "config2", files: ["**/*.js"], - ignores: ["**/tests"], + ignores: ["**/tests/*.js"], rules: { - "no-console": "error" + "prefer-const": "error" } }; - export default defineConfig([ - { - name: "myconfig", - files: ["**/src/*.js"], - ignores: ["**/*.test.js"] + files: ["src/*.js"], + ignores: ["src/__tests/**/*.js"], extends: [config1, config2], rules: { - semi: "error" + "no-console": "error" } } - ]); ``` -Here, the `files` keys will be combined and the `ignores` key will be merged, resulting in a final config that looks like this: +This config would result in the following array: ```js - export default [ - - // intersected files and original ignores { - name: "myconfig > config1", - files: [["**/src/*.js", "**/*.cjs.js"]], - ignores: ["**/*.test.js"], - languageOptions: { - sourceType: "commonjs", - globals: { - ...globals.node - } + files: [["src/*.js"], ["**/*.cjs.js"]], + ignores: ["src/__tests/**/*.js"], + rules: { + semi: "error"; } }, - - // intersected files and merged ignores { - name: "myconfig > config2", - files: [["**/src/*.js", "**/*.js"]], - ignores: ["**/*.test.js", "**/tests"], + files: [["src/*.js"], ["**/*.js"]] + ignores: ["src/__tests/**/*.js", "**/tests/**/*.js"], rules: { - "no-console": "error" + "prefer-const": "error" } }, - - // original config { - name: "myconfig", - files: ["**/src/*.js"], - ignores: ["**/*.test.js"] + files: ["src/*.js"], + ignores: ["src/__tests/**/*.js"], rules: { - semi: "error" + "no-console": "error" } } - ]; ``` @@ -334,9 +412,119 @@ export default [ Notes: 1. The `files` and `ignores` values from the base config always come first in the calculated `files` and `ignores`. If there's not a match, then there's no point in continuing on to use the patterns from the extended configs. -1. The `name` key is generated for calculated config objects so that it indicates the inheritance from another config. The extended configs don't exist in the final representation of the config array. 1. These behaviors differ from the [typescript-eslint extends helper](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/src/config-helper.ts). The `tseslint.config()` helper overwrites the `files` and `ignores` fields on any config contained in `extends` whereas this proposal creates combination `files` and `ignores`, merging the base config with the extended configs. +##### Extended Object with Only `ignores` + +When a base config extends an object with only `ignores` (global ignores), the extended object `ignores` is placed in a new object with just `ignores`. For example: + +```js +import { defineConfig } from "eslint"; + +const config1 = { + ignores: ["build"] +}; + +const config2 = { + ignores: ["dist"] +}; + +export default defineConfig([ + { + files: ["src/*.js"], + ignores: ["src/__tests/**/*.js"], + extends: [config1, config2], + rules: { + "no-console": "error" + } + } +]); +``` + +This config would result in the following array: + +```js +export default [ + { + ignores: ["build"] + }, + { + ignores: ["dist"] + }, + { + files: ["src/*.js"], + ignores: ["src/__tests/**/*.js"], + rules: { + "no-console": "error" + } + } +]; +``` + +In short, global ignores are always inherited without any modification. + +##### Calculating Config Names + +To provide for easier debugging, each generated object will have a `name` key that is the concatenation of the base object name and the extended object name, separated by a `>`. For example: + +```js +import { defineConfig } from "eslint"; + +const config1 = { + name: "config1", + rules: { + semi: "error"; + } +}; + +const config2 = { + name: "config2", + rules: { + "prefer-const": "error" + } +}; + +export default defineConfig([ + { + name: "base config", + extends: [config1, config2], + rules: { + "no-console": "error" + } + } +]); +``` + +This config would result in the following array: + +```js +export default [ + { + name: "base config > config1", + rules: { + semi: "error"; + } + }, + { + name: "base config > config2", + rules: { + "prefer-const": "error" + } + }, + { + name: "base config", + rules: { + "no-console": "error" + } + } +]; +``` + +Notes: + +1. If the base config doesn't have a `name` key, then it will be assigned a name in the format `"user config N"` where `N` is the index in the array. If the base object is, itself, contained in another array, then `N` will be the concatenation of the array indices from each parent array, such as `1,5,2` that would allow you to walk the arrays to find the object. +1. If an extended config is specified as a string name, then that is the name that will be used. Otherwise, its `name` will be used. +1. If an extended config doesn't have a name, it will be assigned a name of `"extended config N"` where `N` is the index in the `extends` array. Similar to base objects, if an extended object is contained in a nested array then `N` represents the concatenation of array indices starting from the `extends` array. #### Extending Arrays @@ -652,6 +840,12 @@ Named configs are included here for three reasons: 1. It encourages plugins to export configs on the `configs` key. 1. It directly affects how this proposal is implemented. +### Why is `files` intersected with extended objects? + +This matches the behavior of the eslintrc `overrides.extends` behavior, and it ensures that extending arrays results in predictable behavior. + +The alternative, replacing `files` completely with the base config values, means that you can't safely extend an array of config objects that might only intend to match specific file patterns. Blindly overwriting `files` can lead to hard-to-detect errors in this case. + ### What will happen for plugins that don't define `meta.namespace`? They will be treated the same as they are now.