当ESLint在2013年首次发布时,配置系统是相当简单的。你可以在一个.eslintrc
文件中定义你想启用或禁用的规则。当一个文件被加载时,ESLint将首先在与该文件相同的目录中寻找一个.eslintrc
文件,然后继续在目录层次中寻找,直到到达根目录,合并沿途发现的所有.eslintrc
文件的配置。这个系统,我们称之为配置级联,允许你轻松覆盖特定目录的规则,这是JSHint无法做到的。你还可以在package.json
内的eslintConfig
键中添加更多的配置。
然而,多年来,配置系统发展成了一个不方便的混乱。这就是为什么我在2019年提议创建一个新的配置系统,以便在一个JavaScript项目日益复杂的世界中更容易配置ESLint。新的配置系统的很大一部分已经被合并到主分支中,所以现在是时候开始学习你在未来如何配置ESLint了。但为了做到这一点,回顾一下,看看我们是如何进入目前的状态的,是很有帮助的。
渐进式的变化导致了最大的复杂性
回顾目前的配置系统(称为eslintrc系统)是如何演变的,每一步都对我们当时的情况有逻辑意义。ESLint一直采用增量开发的方法,我们寻找方法来改善我们已经拥有的东西,而不是把东西扔掉重新开始。eslintrc系统也不例外。
extends
键
eslintrc的第一个重大变化是引入了extends
。extends
键,从JSHint中亲切地借来的,允许用户导入另一个配置,然后对其进行增强,例如:
{
"extends": ["./other-config.json"],
"rules": {
"semi": "warn"
}
}
因此,假设./other-config.json
有一些配置数据,你可以导入它,然后在上面添加你自己的rules
设置。这被证明是ESLint的一大进步,原因有很多。
首先,extends
实际上早于可共享配置的想法,可以通过npm发布。正是在实现extends
的过程中,我们意识到可共享的配置是可能的。extends
中指定的文件是通过Node.js的require()
函数加载的,所以任何Node.js可以通过该函数加载的东西也可以作为一个配置来扩展。
其次,extends
允许我们实现eslint:recommended
,这套规则我们认为对每个人都很重要,可以启用。最初ESLint默认启用了一些规则,但这成为了用户的负担。所以我们改成了默认关闭所有规则,这对于没有看到任何规则的新用户来说也是一种困惑。添加eslint:recommended
,使我们能够明确指出,你包括了我们推荐的一堆规则,但如果你不愿意,你可以删除它们。
事后看来,如果我们当时考虑得更周全一些,我们就会在这一点上删除配置级联。引入extends
,实现了很多与级联相同的使用情况,而保留这两者则变成了一个混乱的局面,我们将花费数年时间来修复。
个人配置
当人们要求我们在~/.eslintrc
,增加拥有个人配置文件的能力时,又增加了下一层的复杂性。所以我们增加了一个额外的检查:如果我们在文件位置的祖先中没有找到一个配置文件,那么我们将自动寻找一个个人配置文件。
多种配置文件格式
作为重构的一部分,我们发现允许不同的配置文件格式是微不足道的。与其强迫每个人使用非标准的.eslintrc
文件,我们可以将JSON格式正式化为.eslintrc.json
,同时增加对YAML (.eslintrc.yml
或.eslintrc.yaml
) 和 JavaScript (.eslintrc.js
) 的支持。为了向后兼容,我们继续支持.eslintrc
,因为它是一个微不足道的代码量,需要保留。
事后看来,这也不是一个好主意。添加一个JavaScript配置文件格式,在它和非JS格式之间产生了不兼容性:任何JavaScript对象都可以被传入配置,并在规则中可用。因为我们没有正确地验证配置,使其与非JS格式完全匹配,我们最终发现一些规则需要传递正则表达式对象才能正确配置。虽然这在JS配置文件格式中可以工作,但在非JS配置文件中无法正确配置规则。不幸的是,由于插件规则依赖于这个功能,我们无法在不破坏事情的情况下回去修复它。
可共享的配置和依赖性
也许我们早期面临的最大问题是npm决定在v3中停止安装对等依赖。 在这之前,我们曾建议可共享配置将它们所依赖的任何插件作为对等依赖而不是普通依赖。这是extends
实现方式的一个怪癖:使用require()
。
因为可共享的配置是纯数据的,不能直接引用Node.js的依赖关系,所以require()
不会自动将直接依赖关系加载到ESLint的路径中去解决它们。另一方面,对等的依赖,只需使用require()
,就可以完美地工作,因为这些被安装在一个正常的软件包查找工作的位置上。
当npm v3停止默认安装对等依赖时,所有依赖这种行为的共享配置都不能正常工作。有一个长期存在的问题,要求允许可共享配置直接使用依赖,但eslintrc的架构不允许这样做。我们基本上不得不在ESLint中重新创建整个require()
功能,以解决可共享配置的设计方式。我们建议可共享的配置创建一个安装后的脚本来代替安装他们的对等依赖。无论怎么说,这都不是理想的做法。
我们添加了--resolve-plugins-relative-to
命令行选项,试图帮助解决这个问题,但这还不够。在我们的Discord #help 频道中,最受欢迎的帮助请求是关于从配置文件中不正确地解决插件的问题。
npm最终在v7中改回了默认安装对等依赖,但那时对ESLint生态系统的破坏已经完成。
root
关键
随着时间的推移,配置级联继续给用户带来问题。最常见的情况是,人们不会意识到他们有一个配置文件在他们正在工作的项目的祖先目录中。这将造成混乱,因为他们会得到他们似乎没有配置过的ESLint设置。
为了帮助解决这个问题,我们为配置文件引入了root
属性。当root: true
在配置中被指定时,对进一步的配置文件的搜索不会继续到祖先的目录。这阻止了一些混乱,我们最终在ESLint通过旧的--init
命令生成的配置文件中自动包括了root: true
,以帮助用户在开始时尽量减少混乱。
overrides
密钥
ESLint继续收到要求以更强大的方式来配置他们的项目。更具体地说,有人要求在现有的配置文件中提供基于球的配置。这导致了overrides
密钥的产生,它可以让你进一步修改 ESLint 正在处理的特定文件子集的配置。这里有一个例子:
{
"rules": {
"quotes": ["error", "double"]
},
"overrides": [
{
"files": ["bin/*.js", "lib/*.js"],
"excludedFiles": "*.test.js",
"rules": {
"quotes": ["error", "single"]
}
}
]
}
在这种情况下,bin
和lib
中的 JavaScript 文件喜欢单引号,而不是其他地方喜欢的双引号。
overrides
键和基于glob的配置被证明是完成配置级联所要做的事情的一个更好的方法。再一次,事后看来,这本来是尝试删除级联的最佳时机......但我们没有这样做。复杂性并没有就此停止。
添加extends
到overrides
eslintrc开发的最后一步是在overrides
配置中添加extends
键,允许用户在基于glob的配置对象中注入额外的配置数据,就像这样:
{
"rules": {
"quotes": ["error", "double"]
},
"overrides": [
{
"files": ["bin/*.js", "lib/*.js"],
"excludedFiles": "*.test.js",
"extends": ["eslint:recommended"],
"rules": {
"quotes": ["error", "single"]
}
}
]
}
这一添加也带来了很多额外的复杂性,因为我们必须弄清楚如何在两个不同的配置之间合并glob模式。最终的结果是,在overrides
配置中的extends
将使用一个 AND 运算符来合并files
和excludedFiles
。如果你不清楚这到底是什么意思,你并不孤单。甚至对我们来说,这也是令人困惑的。
简化的需要
在2019年新年前后,我对eslintrc系统的复杂性越来越关注。我们收到越来越多的问题,涉及到与加载配置文件有关的晦涩难懂的错误信息,无法找到其他配置文件或插件。此外,团队集体开始害怕接触任何与配置系统有关的东西。没有人真正理解围绕计算任何给定文件的最终配置的所有不同的排列组合。我们陷入了许多软件项目的陷阱:我们不断地增加新的功能,却没有退一步全面地看待问题(和解决方案)。这导致了我们的代码库中几乎没有可维护的部分。
就在这个时候,我做了一个思想实验:如果我今天从头开始,了解我现在对ESLint的一切,那么配置系统会是什么样子?接下来是ESLint历史上最有争议的RFC提案。当时,团队中几乎平分秋色,有人想扔掉eslintrc,从头开始,有人认为eslintrc可以通过更多的迭代来保存。最终,经过18个月的修改和辩论,我们决定是时候着手建立一个全新的配置系统,并考虑到当今的现实。
前进的道路
现在是2022年,我们终于有了在v8.21.0中发布的新配置系统的第一个实现。这个新系统,我们昵称为 "扁平配置",旨在让现有的ESLint用户感到熟悉,同时极大地简化了设置配置文件的过程。由于我们在继续解决错误和收集反馈,扁平配置还不能通过CLI使用,但它对直接使用API的开发者是可用的。我将在本系列的第二部分中讨论扁平配置的设计。