- Start Date: 2019-02-25
- RFC PR: #13
- Authors: Toru Nagashima (@mysticatea)
This proposal improves our configuration files. This changes the architecture of configuration files to maintain our codebase easier and make enhancing easier.
This RFC fixes two bugs I found while I make a PoC. I guess we don't want to make surprised behaviors on purpose.
- (link) Even if unused dependencies have some errors, ESLint doesn't throw it. (fixes eslint/eslint#11396)
- (link) The configuration of
overrides
in shareable configs no longer overwrites user settings in.eslintrc
files. (see details)
This RFC includes four enhancements.
- Additional Lint Targets (major)
- Array Config (minor)
extends
inoverrides
(minor)- Plugin Naming (minor)
I made the enhancements in my PoC in order to confirm this architecture change is effective to maintain our codebase easier and make enhancing easier. Therefore, the enhancements are not required for this proposal.
However, I'd like to add the enhancements with this because those will solve some important pain of the ecosystem.
- The codebase about configuration files is complicated. It has made us hard to maintain and enhance the configuration system.
- The current process determines target files at first. Next, it finds configuration files on the directory where each target file exists and that ancestors. This "files-then-config" order prevents adding some enhancements such as
--ext
functionality to our configuration file system. - Several good ideas have been born in #9 and #14. We can simplify the logic about configuration files by the ideas.
Therefore, the goal of this RFC is to simplify our codebase by some architecture changes in order to maintain our codebase easier and make enhancing easier.
Then some enhancements that the simplification gives would solve the important pains of the ecosystem.
(✨ in this section means the enhancement point for additional enhancement proposals.)
Proof of Concept (implementation): https://github.com/eslint/eslint/tree/proof-of-concept/config-array-in-eslintrc
- It simplifies the internal structure of configuration files by an array.
- The config object owns loaded plugins and parsers rather than registers those to somewhere.
- It changes the processing order to "config-then-files" from "files-then-config".
- It restructures the files about CLIEngine and lookup.
✨ When it reads configuration files, it flattens the extends
property and overrides
property of the configuration. This is the following order:
- The loaded configurations of
extends
property. - The implicit configuration for file extension processors of
plugins
property. - The configuration except
extends
property andoverrides
property. - The configurations of
overrides
property.
For some details:
-
If it cannot load a configuration of
extends
property, it throws an error immediately. -
If a loaded configuration of
extends
property hasextends
property oroverrides
property, it flattens those recursively.ℹ️ User-facing change:
The configuration ofoverrides
in shareable configs no longer overwrites user settings in.eslintrc
files. (see details) -
If the configuration has
plugins
property and the plugins have file extension processors, it yields config array element for those to apply the file extension processors to matched files automatically. -
✨ If a configuration of
overrides
property hasextends
property oroverrides
property, it throws an error. -
For duplicated settings, a later element in the array has precedence over an earlier element in the array.
💡 Example:
{
"extends": ["eslint:recommended", "plugin:node/recommended"],
"rules": { ... },
"overrides": [
{
"files": ["*.ts"],
"rules": { ... },
}
]
} [
// extends
{
"name": ".eslintrc.json » eslint:recommended",
"filePath": "node_modules/eslint/conf/eslint-recommended.js",
"rules": { ... }
},
{
"name": ".eslintrc.json » plugin:node/recommended",
"filePath": "node_modules/eslint-plugin-node/lib/index.js",
"env": { ... },
"parserOptions": { ... },
"plugins": { ... },
"rules": { ... }
},
// main
{
"name": ".eslintrc.json",
"filePath": ".eslintrc.json",
"rules": { ... }
},
// overrides
{
"name": ".eslintrc.json#overrides[0]",
"filePath": ".eslintrc.json",
"matchFile": { "includes": ["*.ts"], "excludes": null },
"rules": { ... }
}
] |
The loading logic of configuration files is complicated because it has complicated relationships between the config, plugins, parsers, and environments.
The main reason is registration. The loading logic has side-effects that register loaded plugins to the plugin manager, and plugins have side-effects that register rules and environments to other managers.
The codebase gets pretty simple by the removal of the registration. Instead, the internal structure of configuration owns loaded plugins and parsers ✨.
Surprisingly, now the return value of ConfigArrayFactory.loadFile(filePath)
has all needed information to check files. Previously, we also needed information that was registered somewhere.
💡 Example:
[
{
"name": ".eslintrc.json » plugin:@typescript-eslint/recommended",
"filePath": "node_modules/@typescript-eslint/eslint-plugin/dist/index.js",
// Config array element owns the loaded parser.
"parser": {
"definition": { ... }, // the parser implementation.
"id": "@typescript-eslint/parser",
"filePath": "node_modules/@typescript-eslint/parser/dist/index.js",
"importerPath": "node_modules/@typescript-eslint/eslint-plugin/dist/index.js"
},
"parserOptions": {
"sourceType": "module"
},
"plugins": {
// Config array element owns the loaded plugins.
"@typescript-eslint": {
"definition": { ... }, // the plugin implementation.
"id": "@typescript-eslint",
"filePath": "node_modules/@typescript-eslint/eslint-plugin/dist/index.js",
"importerPath": "node_modules/@typescript-eslint/eslint-plugin/dist/index.js"
}
},
}
] |
If arbitrary errors happen while loading a plugin or a parser, the config array stores the error information rather than throws it. Because the plugin or the parser might not be used finally.
When ConfigArray#extractConfig(filePath)
method extracted configuration for a file, if the final configuration contains the error, it throws the error. Here, "extract" means merge the elements in the config array as filtering it by files
property and excludedFiles
property.
ℹ️ User-facing change: Even if unused dependencies have some errors, ESLint doesn't throw it. (fixes eslint/eslint#11396) |
💡 Example:
[
{
"name": ".eslintrc.json » plugin:@typescript-eslint/recommended",
"filePath": "node_modules/@typescript-eslint/eslint-plugin/dist/index.js",
// Config array element owns the loaded plugins.
"parser": {
"error": Error, // an error object (maybe "Module Not Found").
"id": "@typescript-eslint/parser",
"importerPath": "node_modules/@typescript-eslint/eslint-plugin/dist/index.js"
},
"parserOptions": {
"sourceType": "module"
},
"plugins": {
// Config array element owns the loaded plugins.
"@typescript-eslint": {
"definition": { ... },
"id": "@typescript-eslint",
"filePath": "node_modules/@typescript-eslint/eslint-plugin/dist/index.js",
"importerPath": "node_modules/@typescript-eslint/eslint-plugin/dist/index.js"
}
},
}
] |
If Linter#verify
received a ConfigArray
object, it requires options.filename
as well. The Linter
object calls ConfigArray#extractConfig(filePath)
method and set needed parser, rules, and environments up. If the options.filename
was /path/to/<INPUT>.js
, it gives each rule only <INPUT>
part.
📝 Note: Now we can implement #3 in
|
Currently, first it finds target files by globs, next it finds configs for each target file. Therefore, we could not change target files by configuration files because of this ordering.
This proposal changes that ordering.
- When the file enumerator entered into a directory,
- It finds
.eslintrc.*
file on the directory.- If found, it concatenates the found configuration to the parent configuration (just
Array#concat
).
- If found, it concatenates the found configuration to the parent configuration (just
- It enumerates files on the directory.
- ✨ If a file is a regular file and matched to the current criteria, yields the pair of the file and the current configuration.
- If a file is a directory, enters into the directory (go step 1).
- It finds
📝 Note: As a side effect, the file enumerator reuses configuration instances naturally without a special cache logic. |
As the result, we can change target files by settings of configuration files.
It's premature optimization if we moved a logic only one functionality is using to the shared utility directory. The shared utility directory is similar to global variables, so it makes hard to know who use the utilities.
This proposal moves some utility files to the directory of lookup functionality.
We should be able to understand the lookup logic only the files in lib/lookup.
This core proposal needs migration guide because of a breaking change by a bug fix.
- If people was affected by the
overrides
order change, they have to modify their config file.
Additional enhancements need documents as well:
This is a large change. Currently we have multiple pull requests around configuration files, this will conflict with those pull requests severely. And this will be a large change for tests. Our tests really depend on internal structures seriously.
Most cases work fine as is.
It can work fine as is.
CLIEngine
has a Map<string, Plugin>
for the added plugin, and it gives FileEnumerator
(then ConfigArrayFactory.load*
methods) the map, and the loading logic uses the map to load plugins.
It can work fine as is.
Only if a given config
has extractConfig
method, Linter
does the setup process for the config. Otherwise, it works the same as currently.
See this paragraph also.
It can work fine as is.
The ConfigArray
has all information to distinguish if the config was changed since the last time.
Currently, overrides
is applied after all overrides
properties are merged. This means that the overrides
in a shareable config priors to the setting in your .eslintrc
.
extends:
- foo
rules:
eqeqeq: error # silently ignored if the "foo" has the "eqeqeq" setting in "overrides".
After this proposal, the setting in your .eslintrc
priors to the setting in shareable configs always.
This is a breaking change, but I think this is a bug fix.
An additional enhancement have a breaking change.
- #9 is the alternative. But double duplicate features cause confusion for the ecosystem. For newcomers, a mix of articles about two config systems makes hard to understand ESLint. For non-English users, the official document is far.
- #14
- #9
- #7
- #5
- #3
- eslint/eslint#3458
- eslint/eslint#6732
- eslint/eslint#8813
- eslint/eslint#9505
- eslint/eslint#9897
- eslint/eslint#10125
- eslint/eslint#10643
- eslint/eslint#10891
- eslint/eslint#11223
- eslint/eslint#11396
Especially, this proposal is inspired by the discussion on #9 and #14.