ESLint 的新配置系统,第 1 部分:背景

ESLint 的配置系统在 2013 年刚开始时相当简单。自那时起,它变得越来越复杂,现在是时候进行一些改变了。

当 ESLint 于 2013 年首次发布时,其配置系统相当简单。你可以在一个 .eslintrc 文件中定义你想启用或禁用的规则。当一个文件被 lint 时,ESLint 会首先在该文件所在的同一目录中查找 .eslintrc 文件,然后继续向上遍历目录层次直到达到根目录,并合并沿途找到的所有 .eslintrc 文件中的配置。这个系统,我们称之为配置级联,使你可以轻松地为特定目录覆盖规则,这是 JSHint 无法做到的。你还可以在 package.json 中的 eslintConfig 键中添加更多配置。

🌐 When ESLint was first released in 2013, the config system was fairly simple. You could define the rules you wanted to enable or disable in a .eslintrc file. When a file was linted, ESLint would first look in the same directory as that file for a .eslintrc file and then continue up the directory hierarchy until reaching the root, merging configurations from all the .eslintrc files found along the way. This system, which we called the configuration cascade, allowed you to easily override rules for particular directories, something that JSHint wasn’t capable of doing. You could also add more configuration in the eslintConfig key inside of package.json.

然而,多年来,配置系统已经变得杂乱无章。因此,在2019年,我提出创建一个新的配置系统,以便在 JavaScript 项目越来越复杂的世界中,更容易配置 ESLint。新配置系统的很大一部分已经合并到主分支中,因此现在是开始了解未来如何配置 ESLint 的时候了。但为了做到这一点,回顾一下过去,看看我们是如何走到当前状态的,这会很有帮助。

🌐 Over the years, however, the config system grew into an unwieldy mess. That’s why in 2019 I proposed creating a new config system to make it easier to configure ESLint in a world where JavaScript projects are growing increasingly complex. A significant portion of the new config system has been merged into the main branch, and so it’s time to start learning about how you will configure ESLint in the future. But in order to do that, it’s helpful to take a look back and see how we got into the current state of things.

逐步变化导致最大复杂性

🌐 Incremental changes leading to maximum complexity

回顾当前的配置系统(称为 eslintrc 系统)的演变过程,每一步对于当时的情况都是合乎逻辑的。ESLint 一直采用渐进式开发的方法,我们会寻找改进现有功能的方法,而不是抛弃一切重新开始。eslintrc 系统也不例外。

🌐 Looking back at how the current config system (called the eslintrc system) evolved, every step made logical sense for where we were at the time. ESLint has always operated on an incremental approach to development where we look at ways to improve what we already have instead of throwing things away to start over. The eslintrc system was no different.

extends

🌐 The extends key

eslintrc 的第一次重大变化是引入了 extends 键。extends 键是从 JSHint 借来的,它允许用户导入另一个配置然后进行增强,例如:

🌐 The first significant change to eslintrc was with the introduction of the extends key. The extends key, borrowed lovingly from JSHint, allowed users to import another configuration and then augment it, for example:

{
  "extends": ["./other-config.json"],
  "rules": {
    "semi": "warn"
  }
}

所以假设 ./other-config.json 有一些配置数据,你可以导入它,然后在上面添加你自己的 rules 设置。从多个方面来看,这对 ESLint 是一个很大的进步。

🌐 So assuming ./other-config.json had some configuration data, you could import that and then add your own rules settings on top of it. This turned out to be a great step forward for ESLint for a number of reasons.

首先,extends 实际上早于可以通过 npm 分发的可共享配置的想法。在实现 extends 的过程中,我们意识到可共享配置是可行的。extends 中指定的文件是通过 Node.js 的 require() 函数加载的,所以任何 Node.js 可以通过该函数加载的东西也都可以作为可扩展的配置使用。

🌐 First, extends actually preceded the idea of shareable configs that could be distributed via npm. It was during the implementation of extends that we realized shareable configs were possible. The files specified in extends were loaded via the Node.js require() function, so anything Node.js could load through that function could also work as a config to extend from.

其次,extends 使我们能够实现 eslint:recommended,这是我们认为每个人都应启用的一组规则。最初 ESLint 默认启用了几条规则,但这对用户来说成为了一种负担。因此我们改为默认关闭所有规则,这对新用户来说也很困惑,因为他们看不到任何规则。添加 eslint:recommended 使我们能够明确表示你包含了一些我们推荐的规则,但如果你不想使用,也可以将它们移除。

🌐 Second, extends allowed us to implement eslint:recommended, the set of rules that we felt were important for everyone to enable. Originally ESLint had several rules enabled by default, but that became a burden for users. So we switched to having all rules off by default, which also was confusing for new users who didn’t see any rules. Adding eslint:recommended allowed us to make it explicit that you were including a bunch of rules we recommended but you could remove them if you didn’t want to.

事后看来,如果我们当时多考虑一下,我们本应该在这一点上移除配置级联。引入 extends 启用了许多与级联相同的用例,而同时保留两者结果是一场混乱,我们将花费数年时间去尝试修复。

🌐 In hindsight, if we had thought things through a little more, we would have removed the configuration cascade at this point. Introducing extends enabled a lot of the same use cases as the cascade, and keeping both turned out to be a mess that we would spend years trying to fix.

个人配置

🌐 Personal configs

当人们要求我们在 ~/.eslintrc 添加个人配置文件的功能时,复杂性的下一层被加入了。因此我们增加了一个额外的检查:如果在文件位置的上级目录中没有找到配置文件,那么我们会自动查找个人配置文件。

🌐 The next layer of complexity was added when people requested that we add the ability to have a personal config file at ~/.eslintrc. So we added an additional check: if we didn’t find a config file in the ancestry of the file location, then we would automatically look for a personal config file.

多种配置文件格式

🌐 Multiple config file formats

作为重构的一部分,我们发现允许使用不同的配置文件格式是很简单的。我们不必强制大家使用非标准的 .eslintrc 文件,而是可以将 JSON 格式正式化为 .eslintrc.json,同时还可以增加对 YAML(.eslintrc.yml.eslintrc.yaml)和 JavaScript(.eslintrc.js)的支持。为了向后兼容,我们继续支持 .eslintrc,因为保留这部分代码非常简单。

🌐 As part of a refactor, we discovered that it would be trivial to allow different config file formats. Instead of forcing everyone to use a nonstandard .eslintrc file, we could formalize the JSON format as .eslintrc.json and also add support for YAML (.eslintrc.yml or .eslintrc.yaml) and JavaScript (.eslintrc.js). For backwards compatibility we continued to support .eslintrc because it was a trivial amount of code to keep around.

事后看来,这也被证明并不是一个好主意。添加 JavaScript 配置文件格式导致它与非 JS 格式之间出现了不兼容:任何 JavaScript 对象都可以传入配置并在规则中使用。由于我们没有正确验证配置以完全匹配非 JS 格式,我们最终导致某些规则需要传入正则表达式对象才能正确配置。虽然这在 JS 配置文件格式下可以工作,但规则无法在非 JS 配置文件中正确配置。不幸的是,由于插件规则依赖于此功能,我们无法回头修复它而不破坏现有功能。

🌐 This also turned out, in hindsight, to not be a great idea. Adding a JavaScript config file format created an incompatibility between it and the non-JS formats: any JavaScript object could be passed into the config and available in rules. Because we didn’t properly validate the config to exactly match the non-JS formats, we ended up with some rules requiring regular expression objects to be passed in to be properly configured. While this could work in the JS config file format, the rules could not be properly configured in non-JS config files. Unfortunately, because plugin rules depended on this functionality, we couldn’t go back and fix it without breaking things.

可共享的配置和依赖

🌐 Shareable configs and dependencies

也许我们在早期面临的最大问题是当 npm 决定在 v3 中停止安装 peer 依赖时。在此之前,我们建议可共享配置将其所依赖的任何插件作为 peer 依赖而不是常规依赖包含。这是 extends 实现方式的一个怪癖:使用 require()

🌐 Perhaps the biggest problem we faced early on was when npm decided to stop installing peer dependencies in v3. Prior to this point, we had recommended that shareable configs include any plugins they depended on as peer dependencies rather than regular dependencies. This was a quirk of the way that extends was implemented: using require().

因为可共享的配置只是数据而已,不能直接引用 Node.js 依赖,require() 不会自动将直接依赖加载到 ESLint 可解析的路径中。另一方面,同行依赖工作得非常好,只需使用 require(),因为它们安装在常规包查找可用的位置。

🌐 Because shareable configs were data-only and couldn’t directly reference Node.js dependencies, require() would not automatically load direct dependencies into the path for ESLint to resolve them. Peer dependencies, on the other hand, worked perfectly by just using require() because those were installed in a location where the normal package lookup worked.

当 npm v3 停止默认安装对等依赖时,所有依赖此行为的共享配置都无法正常工作。有一个长期问题请求允许可共享配置直接使用依赖,但 eslintrc 的架构根本不允许这样做。我们实际上不得不在 ESLint 内部重新创建整个 require() 功能,以规避共享配置的设计方式。我们建议共享配置创建一个安装后脚本来安装它们的对等依赖。无论如何,这都不是理想的方案。

🌐 When npm v3 stopped installing peer dependencies by default, all of the shared configs relying on this behavior stopped working correctly. There is a long-running issue requesting that shareable configs be allowed to use dependencies directly, but the architecture of eslintrc just didn’t allow for it. We would have essentially had to recreate the entire require() functionality inside of ESLint to work around the way shareable configs were designed. We recommended that shareable configs create a post-install script to install their peer dependencies instead. Not ideal by any stretch of the imagination.

我们添加了 --resolve-plugins-relative-to 命令行选项以尝试解决这个问题,但这还不够。我们在 Discord #help 通道 中收到的最热门求助请求,都是关于配置文件中插件解析不正确的问题。

🌐 We added the --resolve-plugins-relative-to command line option to try and help with this problem, but it wasn’t enough. The most popular requests for help in our Discord #help channel have to do with improper resolution of plugins from config files.

npm 最终在 v7 中又改回默认安装 peer 依赖,但到那时 ESLint 生态系统已经受到了影响。

🌐 npm eventually changed back to installing peer dependencies by default in v7, but by that point the damage on the ESLint ecosystem had been done.

root

🌐 The root key

随着时间的推移,配置级联继续给用户带来问题。最常见的情况是,人们不会意识到他们在正在工作的项目的上级目录中有一个配置文件。这会造成混乱,因为他们会得到看似自己没有配置的 ESLint 设置。

🌐 As time went on, the config cascade continued to cause problems for users. Most frequently, people wouldn’t realize that they had a config file in an ancestor directory of the project they were working on. This would create confusion because they would be getting ESLint settings that they seemingly hadn’t configured.

为了帮助解决这个问题,我们为配置文件引入了 root 属性。当在配置中指定 root: true 时,不会继续向上搜索祖级目录中的配置文件。这减少了一些混淆,我们最终在通过旧的 --init 命令由 ESLint 生成的配置中自动包含了 root: true,以帮助用户以最少的混淆开始使用。

🌐 To help with this problem we introduced the root property for configuration files. When root: true is specified in a config, the search for further config files doesn’t proceed to ancestor directories. This stopped a bit of the confusion and we ended up automatically including root: true in configs that ESLint generated via the old --init command to help users start off with the least amount of confusion.

overrides

🌐 The overrides key

ESLint 继续收到关于更强大方式配置项目的请求。具体来说,有人请求在现有配置文件中提供基于 glob 的配置。这导致创建了一个 overrides 键,它可以让你进一步修改 ESLint 对特定子集文件的配置。下面是一个示例:

🌐 ESLint continued to receive requests for more powerful ways to configure their projects. More specifically, there were requests to provide glob-based configs from within existing config files. This led to the creation of an overrides key that would let you further modify configurations for a specific subset of files that ESLint was linting. Here’s an example:

{
  "rules": {
    "quotes": ["error", "double"]
  },

  "overrides": [
    {
      "files": ["bin/*.js", "lib/*.js"],
      "excludedFiles": "*.test.js",
      "rules": {
        "quotes": ["error", "single"]
      }
    }
  ]
}

在这种情况下,binlib 中的 JavaScript 文件更倾向使用单引号,而不是其他地方普遍使用的双引号。

🌐 In this case, the JavaScript files in bin and lib prefer single quotes instead of the double quotes preferred everywhere else.

overrides 键配合基于通配符的配置,结果证明是实现配置层叠所尝试做的事情的一个更好方式。回头看,这本来是尝试移除层叠的完美时机……但我们没有。而且复杂性并没有就此停止。

🌐 The overrides key with glob-based configuration turned out to be a much better way of accomplishing what the configuration cascade was attempting to do. Once again, in hindsight, this would have been the perfect time to try and remove the cascade…but we didn’t. And the complexity didn’t stop there.

extends 添加到 overrides

🌐 Adding extends to overrides

eslintrc 开发的最后一步是将 extends 键添加到 overrides 配置中,允许用户将额外的配置数据注入到基于 glob 的配置对象中,如下所示:

🌐 The last step of eslintrc development was to add the extends key to overrides configurations, allowing users to inject additional config data into a glob-based config object, like this:

{
  "rules": {
    "quotes": ["error", "double"]
  },

  "overrides": [
    {
      "files": ["bin/*.js", "lib/*.js"],
      "excludedFiles": "*.test.js",
      "extends": ["eslint:recommended"],
      "rules": {
        "quotes": ["error", "single"]
      }
    }
  ]
}

这个新增功能也引入了很多额外的复杂性,因为我们必须弄清楚如何在两个不同的配置之间合并 glob 模式。最终结果是,overrides 配置中的 extends 会使用 AND 运算符来合并 filesexcludedFiles。如果你不确定这到底意味着什么,你并不孤单。即使对我们来说,这也很令人困惑。

🌐 This addition also introduced a lot of additional complexity because we had to figure out how to merge glob patterns between two different configs. The end result was that extends inside of an overrides config would use an AND operator to merge files and excludedFiles. If you’re not sure what exactly that means, you’re not alone. It’s confusing even to us.

简化的需求

🌐 The need for simplification

在2019年新年前后,我开始越来越关注eslintrc系统的复杂性。我们收到越来越多关于加载配置文件的晦涩错误信息的问题,这些配置文件无法找到其他配置文件或插件。此外,团队整体上开始害怕触碰与配置系统相关的任何东西。没有人真正理解计算任意文件最终配置的各种不同组合方式。我们陷入了许多软件项目都会遇到的陷阱:我们不断添加新功能,却没有退一步从整体上审视问题(和解决方案)。这导致了我们代码库中几乎无法维护的部分。

🌐 Around the new year of 2019, I was getting more concerned about the complexity of the eslintrc system. We were getting more and more questions about obscure error messages related to loading config files that couldn’t find other config files or plugins. Additionally, the team was collectively becoming afraid of touching anything to do with the config system. No one really understood all of the different permutations around calculating the final config for any given file. We had fallen into the trap that many software projects do: we kept adding new features without taking a a step back to look at the problem (and solution) holistically. This had led to an almost unmaintainable part of our codebase.

就在那个时候,我做了一个思维实验:如果我从零开始建立配置系统,今天,知道我现在所知道的关于 ESLint 的一切,它会是什么样子?接下来就是 ESLint 历史上最具争议的 RFC 提案。当时,团队几乎平分秋色,一部分人希望摒弃 eslintrc 从头开始,另一部分人则认为 eslintrc 可以通过多次迭代得到保留。最终,经过 18 个月的修订和讨论,我们决定是时候启动一个全新的配置系统,考虑到今天的实际情况来构建。

🌐 It was at this time that I did a thought experiment: what would the config system look like if I started from scratch, today, knowing everything that I now know about ESLint? What followed was the most contentious RFC proposal in the history of ESLint. At the time, the team was almost evenly split between those who wanted to throw away eslintrc and start from scratch and those who felt that eslintrc could be saved with more iterations. Ultimately, after 18 months of revisions and debate, we decided that it was time to embark on an entirely new config system built with today’s reality in mind.

前进的道路

🌐 The path forward

现在是2022年,我们终于在 v8.21.0 中发布了新配置系统的第一个实现。这个新系统,我们昵称为“平面配置”,旨在让现有的 ESLint 用户感觉熟悉,同时大大简化配置文件的设置过程。平面配置尚未通过 CLI 提供,因为我们还在处理错误并收集反馈,但对于直接使用 API 的开发者来说,它已经可用。我将在本系列的 第2部分 中讨论平面配置的设计。

🌐 It’s now 2022 and we finally have the first implementation of the new config system released in v8.21.0. The new system, which we’ve nicknamed “flat config,” is designed to feel familiar to existing ESLint users while dramatically simplifying the process of setting up a config file. Flat config isn’t available through the CLI yet as we continue to work on bugs and gathering feedback, but it is available to developers who use the API directly. I’ll be discussing the design of flat config in part 2 of this series.

最新的 ESLint 新闻、案例研究、教程和资源。

ESLint v10.3.0 发布
1 min read

ESLint v10.3.0 发布

我们刚刚发布了 ESLint v10.3.0,这是 ESLint 的一次小版本升级。此版本添加了一些新功能,并修复了上一版本中发现的几个错误。

ESLint v10.2.1 发布
1 min read

ESLint v10.2.1 发布

我们刚刚发布了 ESLint v10.2.1,这是 ESLint 的一个补丁版本升级。本次发布修复了上一版本中发现的几个错误。

ESLint v10.2.0 发布
2 min read

ESLint v10.2.0 发布

我们刚刚发布了 ESLint v10.2.0,这是 ESLint 的一次小版本升级。此版本添加了一些新功能,并修复了上一版本中发现的几个错误。