在我的上一篇文章中,我谈到了eslintrc的配置系统是如何通过一系列小的、渐进的变化而变得比必要的更复杂。另一方面,扁平化的配置系统从一开始就被设计成在许多方面都比较简单。我们从过去六年的ESLint开发中吸取了所有的经验,想出了一个整体的配置方法,它吸收了eslintrc的优点,并与其他JavaScript相关工具处理配置的方式相结合。其结果是,希望现有的ESLint用户能感到熟悉,并且比以前的功能要强大得多。
flat config的目标
为了给flat config的变化创造条件,我们有几个目标:
- 逻辑默认--在过去的9年中,人们编写JavaScript的方式发生了很大的变化,我们希望新的配置系统能够反映我们当前的现实,而不是我们在ESLint刚发布时的生活。
- 一种方式来定义配置--我们不希望人们再有多种方式来做同样的事情。对于任何给定的项目,应该有一种方法来定义配置。
- 规则配置应该保持不变--我们觉得规则的配置方式已经很好了,所以为了让它更容易过渡到平面配置,我们不想对规则配置做任何改变。同样的
rules
,可以在平面配置中以同样的方式使用。 - 所有东西都使用本地加载- 我们对eslintrc最大的遗憾之一是以自定义的方式重新创建Node.js
require
分辨率。这是一个重要的复杂性来源,而且事后看来是不必要的。展望未来,我们希望直接利用JavaScript运行时的加载能力。 - 更好地组织顶层键- 自从ESLint发布以来,eslintrc顶层键的数量急剧增加。我们需要看看哪些键是必要的,它们之间的关系如何。
- 现有的插件应该能够工作--ESLint的生态系统充满了数百个插件。这些插件继续工作是很重要的。
- 向后兼容应该是一个优先事项--即使我们正在转移到一个新的配置系统,我们也不想把所有现有的生态系统留在后面。特别是,我们希望有办法让可共享的配置继续尽可能地工作。虽然我们知道100%的兼容性可能是不现实的,但我们想尽最大努力确保现有的可共享配置能够正常工作。
考虑到这些目标,我们想出了新的扁平化配置系统。
为linting设置逻辑默认值
当ESLint刚创建时,ECMAScript 5是JavaScript的最新版本,大多数文件都被写成 "共享一切 "的脚本或CommonJS模块(针对Node.js)。ECMAScript 6即将到来,但没有人知道它的实施速度会有多快,也没有人知道模块(ESM)最终会被如何使用。所以ESLint的默认值是假设所有文件都是ECMAScript 5。我们最终采用了ecmaVersion
解析器的配置,允许人们在准备好后选择加入ECMAScript 6。
快进到2022年:ECMAScript在不断发展,ESM是大家都在使用的标准模块格式。我们无法改变eslintrc的默认设置,否则可能会破坏很多现有的配置,但我们绝对可以用flat config进行改变。
Flat config具有以下默认值:
ecmaVersion: "latest"
为所有的JavaScript文件 - 没错,默认情况下,所有的JavaScript文件将被设置为最新版本的ECMAScript。这模仿了JavaScript运行时的工作方式,即每一次升级都意味着你选择了最新、最好的JavaScript版本。这一变化应该意味着你可能不必在配置中手动设置 ,除非你想由于运行时间的限制而强制使用以前的版本。如果有必要,你仍然可以将 一直设置到 。ecmaVersion
ecmaVersion
3
sourceType: "module"
为所有 和 文件 - 默认情况下,flat config 假设你正在编写ESM。如果不是,你可以随时将 设为 。.js
.mjs
sourceType
"script"
sourceType: "commonjs"
对于 文件 - 我们仍然处于过渡期,很多Node.js代码是用CommonJS编写的。为了支持这些用户,我们添加了一个新的 ,为该环境正确配置了一切。.cjs
sourceType
"commonjs"
- ESLint搜索
.js
、.mjs
和.cjs
文件--在eslintrc中,ESLint只在你在命令行中传递目录名时搜索.js
文件,而你需要使用--ext
标志来定义更多的文件。有了flat config,所有三个最常见的JavaScript文件名扩展名都会被自动搜索到。
我们对这些新的默认值感到非常兴奋,因为我们认为这将帮助人们更快地加入ESLint,并减少混乱。
新的配置文件:eslint.config.js
与eslintrc相比,它允许在多个位置有多个配置文件,多个配置文件格式,甚至是基于package.json
,flat config只有一个位置,用于你项目的所有配置:eslint.config.js
。通过将配置限制在一个位置和一种格式,我们可以直接利用JavaScript运行时的加载机制,并避免对配置文件进行自定义解析的需要。
当使用ESLint CLI时,它从当前工作目录中搜索eslint.config.js
,如果没有找到,将继续在目录的祖先上搜索,直到找到该文件或击中根目录。这一个eslint.config.js
文件包含了ESLint运行的所有配置信息,所以与eslintrc相比,它极大地减少了所需的磁盘访问,eslintrc必须检查从linted文件位置到根目录的每一个目录,以找到任何额外的配置文件。
此外,使用一个JavaScript文件允许我们依靠用户来加载他们的配置文件可能需要的额外信息。与其说是extends
和plugins
按名称加载东西,现在你只需在必要时使用import
和require
来引入这些附加资源。下面是一个关于eslint.config.js
文件的例子:
export default [
{
files: ["**/*.js"],
rules: {
"semi": "error",
"no-unused-vars": "error"
}
}
];
一个eslint.config.js
文件导出一个配置对象的数组。继续阅读以了解更多关于这个例子的信息。
基于全局的配置无处不在
虽然eslintrc中的overrides
键是很多复杂问题的来源,但有一点非常清楚:人们非常喜欢在他们的配置文件中通过glob模式来定义配置。因为我们想消除eslintrc的配置级联,我们必须使用glob模式来实现相同类型的配置覆盖。我们使用overrides
configs 作为 flat config 的基础。
每个 config 对象都可以有可选的files
和ignores
键,指定基于最小匹配的 glob 模式来匹配文件。一个配置对象只适用于一个文件,如果该文件名与files
中的模式相匹配(或者没有files
键,在这种情况下,它将匹配所有文件)。ignores
键从files
列表中过滤掉文件,因此你可以限制配置对象适用于哪些文件。例如,也许你的测试文件和你的源文件生活在同一个目录下,你希望一个配置对象只适用于源文件。你可以像这样做:
export default [
{
files: ["**/*.js"],
ignores: ["**/*.test.js"],
rules: {
"semi": "error",
"no-unused-vars": "error"
}
}
];
这里,配置对象将匹配所有的JavaScript文件,然后过滤掉任何以.test.js
结尾的文件。
如果你想完全忽略文件呢?你可以通过指定一个只有一个ignores
关键的配置对象来做到这一点,像这样:
export default [
{
ignores: ["**/*.test.js"]
},
{
files: ["**/*.js"],
rules: {
"semi": "error",
"no-unused-vars": "error"
}
}
]
有了这个配置,所有以.test.js
结尾的JavaScript文件将被忽略。你可以认为这相当于eslintrc中的ignorePatterns
,尽管有最小匹配模式。
再见,extends
,你好,扁平级联
虽然我们想摆脱基于目录的配置级联,但flat config实际上仍然有一个直接在你的eslint.config.js
文件中定义的扁平级联。在数组中,ESLint找到所有与被提示的文件相匹配的配置对象,并以与eslintrc相同的方式将它们合并在一起。唯一真正的区别是,合并是从数组的顶部到底部进行的,而不是使用目录结构中的文件。比如说:
export default [
{
files: ["**/*.js", "**/*.cjs"],
rules: {
"semi": "error",
"no-unused-vars": "error"
}
},
{
files: ["**/*.js"],
rules: {
"no-undef": "error",
"semi": "warn"
}
}
];
这个配置有两个配置对象,有重叠的files
模式。第一个配置对象适用于所有.js
和.cjs
文件,而第二个对象只适用于.js
文件。当对一个以.js
结尾的文件进行检查时,ESLint结合这两个配置对象来创建该文件的最终配置。因为第二个配置将semi
设置为"warn"
的严重性,它优先于第一个配置中设置的"error"
。当有冲突时,最后匹配的配置总是获胜。
这对可共享的配置意味着你可以直接把它们插入数组,而不是使用extends
,例如:
import customConfig from "eslint-config-custom";
export default [
customConfig,
{
files: ["**/*.js", "**/*.cjs"],
rules: {
"semi": "error",
"no-unused-vars": "error"
}
},
{
files: ["**/*.js"],
rules: {
"no-undef": "error",
"semi": "warn"
}
}
];
这里,customConfig
被首先插入数组中,这样它就成为这个文件的配置基础。下面的每个配置对象都建立在这个基础上,为一个给定的JavaScript文件创建最终的配置。
重新设计的语言选项
ESLint一直有一个奇怪的选项组合,影响着JavaScript的解释方式。有一个顶级的globals
键,可以修改可用的全局变量,还有ecmaVersion
和sourceType
作为parserOptions
,更不用说env
来增加更多的全局变量。也许最令人困惑的是,你必须同时设置ecmaVersion
和添加像es6
这样的环境,以启用你想要的语法并确保正确的全局变量可用。
在flat config中,我们将所有与JavaScript评估有关的键移到一个新的顶级键中,称为languageOptions
。
在flat config中设置ecmaVersion
最大的变化是,我们把ecmaVersion
从parserOptions
中移出,直接移到languageOptions
中。这更好地反映了这个键的新行为,即根据指定的ECMAScript版本来启用语法和全局变量。比如说:
export default [
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: 6
}
}
];
这个配置将ecmaVersion
降级为6
。这样做可以确保所有的ES6语法和所有的ES6全局变量都可用。(任何使用的自定义解析器仍然会收到这个值ecmaVersion
。)
在平面配置中设置sourceType
接下来,我们把sourceType
移到languageOptions
。与ecmaVersion
相似,这个键不仅影响文件的解析方式,也影响ESLint对其范围结构的评估方式。我们为ESM保留了传统的"module"
,为脚本保留了"script"
,同时还增加了"commonjs"
,它让ESLint知道它应该把文件当作CommonJS来处理(这也使得CommonJS特有的globals得以实现)。如果你使用ecmaVersion: 3
或ecmaVersion: 5
,一定要设置sourceType: script
,像这样:
export default [
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: 5,
sourceType: "script"
}
}
];
再见环境,你好globals
eslintrc中的环境提供了一组已知的globals,并且一直是用户困惑的来源。它们需要保持更新(尤其是在browser
),而这种更新需要等待ESLint的发布。另外,我们把一些额外的功能挂在环境上,使其更容易与Node.js一起工作,最后,我们弄得一团糟。
对于平面配置,我们决定完全删除env
关键。为什么?因为它不再需要了。所有我们挂在环境上用于Node.js的自定义功能现在都被sourceType: "commonjs"
,所以剩下的就是让环境来管理全局变量。对于ESLint来说,在核心部分做这些是没有意义的,所以我们把这个责任交还给了你。
几年前,我们与Sindre Sorhus合作,创建了 globals
包,它从ESLint中提取了所有的环境信息,这样它就可以被其他包使用。然后ESLint使用globals
作为其环境的来源。
有了flat config,你可以直接使用globals
包,随时更新它,以获得所有过去由环境提供的相同功能。例如,这里是你如何将浏览器球状物添加到你的配置中:
import globals from "globals";
export default [
{
files: ["**/*.js"],
languageOptions: {
globals: {
...globals.browser,
myCustomGlobal: "readonly"
}
}
}
];
languageOptions.globals
键的工作原理与eslintrc中的相同,只是现在,你可以使用JavaScript来动态地插入你想要的任何全局变量。
自定义分析器和分析器选项大多是相同的
parser
和parserOptions
键现在已经移到了languageOptions
键中,但它们的工作方式大多与eslintrc中相同,有两个具体的区别。
- 你现在可以直接在配置中插入解析器对象。
- 解析器现在可以和插件捆绑在一起,你可以为
parser
指定一个字符串值,以使用来自插件的解析器。(在下一节有更多描述)。
下面是一个使用Babel ESLint解析器的例子:
import babelParser from "@babel/eslint-parser";
export default [
{
files: ["**/*.js", "**/*.mjs"],
languageOptions: {
parser: babelParser
}
}
];
这个配置确保Babel解析器,而不是默认的,将被用来解析所有以.js
和.mjs
结尾的文件。
你也可以通过使用parserOptions
键直接将选项传递给自定义解析器,这与eslintrc中的工作方式相同:
import babelParser from "@babel/eslint-parser";
export default [
{
files: ["**/*.js", "**/*.mjs"],
languageOptions: {
parser: babelParser,
parserOptions: {
requireConfigFile: false,
babelOptions: {
babelrc: false,
configFile: false,
// your babel options
presets: ["@babel/preset-env"],
}
}
}
}
];
更加强大和可配置的插件
ESLint的优势在于个人和公司维护的插件生态系统,以定制他们的语义分析策略。因此,我们希望确保现有的插件能够继续工作而不需要修改,同时也允许插件做一些他们过去从来没有做过的事情。
表面上看,在flat config中使用一个插件与在eslintrc中使用一个插件非常相似。最大的区别是,eslintrc使用字符串,而flat configs使用对象。你不需要指定一个插件的名字,而是直接导入插件并将其放入plugins
,就像这个例子一样:
import jsdoc from "eslint-plugin-jsdoc";
export default [
{
files: ["**/*.js"],
plugins: {
jsdoc
}
rules: {
"jsdoc/require-description": "error",
"jsdoc/check-values": "error"
}
}
];
这个配置使用 eslint-plugin-jsdoc
插件,将其作为一个本地jsdoc
变量导入,然后将其插入到配置中的plugins
键中。之后,插件内的规则是使用jsdoc
命名空间来引用的。
注意:因为插件现在像其他的JavaScript模块一样被导入,所以不再严格执行插件包的名称。你不再需要把eslint-plugin-
作为你的包名的前缀......但我们希望你能这样做。
个性化的插件命名空间
由于你的配置中的插件名称现在与插件包的名称脱钩,你可以选择任何你想要的名称,就像在这个例子中:
import jsdoc from "eslint-plugin-jsdoc";
export default [
{
files: ["**/*.js"],
plugins: {
jsd: jsdoc
}
rules: {
"jsd/require-description": "error",
"jsd/check-values": "error"
}
}
];
这里,插件在配置中被命名为jsd
,所以规则也使用jsd
,以表明它们来自哪个插件。
从--rulesdir
到运行时插件
在eslintrc中,规则需要由CLI直接加载,以便在配置文件中可用。这意味着要么在一个插件中捆绑自定义规则,要么使用--rulesdir
标志来指定ESLint应该从哪个目录加载自定义规则。这两种方法都需要一些额外的工作来设置,并且经常让我们的用户感到沮丧。
有了平面配置,你可以直接在配置文件中加载自定义规则。因为插件现在是直接在配置中的对象,你可以很容易地创建只存在于你的配置文件中的运行时插件,比如说:
import myrule from "./custom-rules/myrule.js";
export default [
{
files: ["**/*.js"],
plugins: {
custom: {
rules: {
myrule
}
}
}
rules: {
"custom/myrule": "error"
}
}
];
这里,一个自定义规则被导入为myrule
,然后创建了一个名为custom
的运行时插件,将该规则作为custom/myrule
提供给配置。
因此,一旦完成向平面配置的过渡,我们将删除--rulesdir
。
插件中的自定义分析器
ESLint开发过程中的一个奇怪的工件是,解析器从来都不是插件的一部分。这是因为自定义解析器早在插件出现之前就已经存在了,而且我们从来没有回去让这两者很好地一起工作。在扁平化配置中,我们借机解决了这个问题,允许插件像暴露处理器和规则一样暴露解析器。例如,你现在可以定义一个看起来像这样的插件:
export default {
parsers: {
parserName: {
parse() { /*... */ }
}
}
}
这个插件在parsers
关键下暴露了一个叫做parserName
的分析器。然后你可以在你的配置中使用这个分析器,像这样:
import custom from "./custom-plugin.js";
export default [
{
files: ["**/*.js"],
plugins: {
custom
},
languageOptions: {
parser: "custom/parserName"
}
}
];
这个配置创建了一个名为custom
的插件命名空间。然后可以使用字符串"custom/parserName"
来访问这个自定义的分析器。
处理程序的工作方式与eslintrc类似
processor
顶层键的工作方式与eslintrc中基本相同,主要的使用情况是使用一个在插件中定义的处理器,比如说:
import markdown from "eslint-plugin-markdown";
export default [
{
files: ["**/*.md"],
plugins: {
markdown
},
processor: "markdown/markdown"
}
];
这个配置对象指定在名为"markdown"
的插件中包含一个名为"markdown"
的处理器,并将该处理器应用于所有以.md
结尾的文件。
flat config中的一个新增内容是:processor
现在也可以是一个同时包含preprocess()
和postprocess()
方法的对象。
有组织的linter选项
在eslintrc中,有几个键直接与linter的操作方式相关,即noInlineConfig
和reportUnusedDisableDirectives
。这些键已经移到了新的linterOptions
,但工作方式与eslintrc中完全相同。这里有一个例子:
export default [
{
files: ["**/*.js"],
linterOptions: {
noInlineConfig: true,
reportUnusedDisableDirectives: true
}
}
];
共享设置是完全一样的
顶层的settings
key的行为方式与eslintrc中完全相同。你可以定义一个带有键值对的对象,这些键值对应该对所有的规则都可用。这里有一个例子:
export default [
{
settings: {
sharedData: "Hello"
}
}
];
使用预定义的配置
ESLint有两个预定义的配置:
eslint:recommended
- 启用ESLint推荐大家使用的规则以避免潜在的错误eslint:all
- 启用所有ESLint提供的规则。
为了包括这些预定义的配置,你可以把字符串值插入导出的数组中,然后在随后的配置对象中对其他属性进行任何修改:
export default [
"eslint:recommended",
{
rules: {
semi: ["warn", "always"]
}
}
];
这里,eslint:recommended
预定义的配置首先被应用,然后另一个配置对象为semi
添加所需的配置。
向后兼容工具
如前所述,我们觉得需要与eslintrc有很好的向后兼容性,以缓解过渡。该 @eslint/eslintrc
包提供了一个FlatCompat
类,使得在一个平面配置文件中继续使用eslintrc风格的共享配置和设置变得容易。这里有一个例子:
import { FlatCompat } from "@eslint/eslintrc";
const compat = new FlatCompat({
baseDirectory: __dirname
});
export default [
// mimic ESLintRC-style extends
compat.extends("standard", "example"),
// mimic environments
compat.env({
es2020: true,
node: true
}),
// mimic plugins
compat.plugins("airbnb", "react"),
// translate an entire config
compat.config({
plugins: ["airbnb", "react"],
extends: "standard",
env: {
es2020: true,
node: true
},
rules: {
semi: "error"
}
})
];
使用FlatCompat
类允许你继续使用你现有的所有eslintrc文件,同时优化它们以便与flat config一起使用。我们设想这是一个必要的过渡步骤,允许生态系统慢慢地转换到flat config。
总结
团队花了很长时间来设计flat config,使它既能让现有的用户感到熟悉,又能提供新的功能,使每个人都受益。我们保持了规则、设置和处理器等内容的不变,同时将插件、语言选项和linter选项等内容扩展得更加统一。我们认为扁平化配置在这两极之间找到了一个很好的平衡,一旦新的配置系统普遍可用,你会更喜欢使用ESLint。同时,兼容性工具将允许你继续使用现有的共享配置。