在我的 上一篇文章 中,我谈到了 eslintrc 配置系统是如何通过一系列小的增量更改变得比必要的更复杂。另一方面,扁平化配置系统从一开始就被设计成在许多方面都更简单。我们借鉴了之前六年 ESLint 开发的所有经验教训,提出了一种整体的配置方法,将 eslintrc 的优点与其他 JavaScript 相关工具的配置方式相结合。结果是,希望对现有的 ESLint 用户来说感觉很熟悉,并且比以前的功能强大得多。
文档:在 官方文档 中阅读有关扁平化配置系统的更多信息。
扁平化配置的目标
为了为扁平化配置中的更改奠定基础,我们有几个目标
- 逻辑默认值 - 人们编写 JavaScript 的方式在过去九年中发生了很大变化,我们希望新的配置系统反映我们当前的现实,而不是 ESLint 首次发布时的现实。
- 定义配置的一种方式 - 我们不再希望人们有多种方法来做同一件事。应该有一种方法来为任何给定的项目定义配置。
- 规则配置应保持不变 - 我们认为规则的配置方式已经可以正常工作了,因此为了便于过渡到扁平化配置,我们不想对规则配置进行任何更改。相同的
rules
键可以在扁平化配置中以相同的方式使用。 - 对所有内容使用原生加载 - 我们对 eslintrc 最大的遗憾之一是,以自定义的方式重新创建了 Node.js 的
require
解析。这是一个重要的复杂性来源,事后看来,这是不必要的。展望未来,我们希望直接利用 JavaScript 运行时的加载功能。 - 更好地组织顶级键 - 自 ESLint 发布以来,eslintrc 顶级键的数量急剧增加。我们需要查看哪些键是必要的以及它们之间是如何关联的。
- 现有的插件应该可以工作 - ESLint 生态系统充满了数百个插件。重要的是这些插件能够继续工作。
- 向后兼容性应优先考虑 - 即使我们正在转向新的配置系统,我们也不希望抛弃所有现有的生态系统。特别是,我们希望找到方法让可共享配置尽可能地继续工作。虽然我们知道 100% 的兼容性可能是不现实的,但我们希望尽最大努力确保现有的可共享配置能够工作。
考虑到这些目标,我们提出了新的扁平化配置系统。
为代码检查设置逻辑默认值
在 ESLint 首次创建时,ECMAScript 5 是 JavaScript 的最新版本,大多数文件都是作为“共享一切”脚本或 CommonJS 模块(用于 Node.js)编写的。ECMAScript 6 即将到来,但没有人知道它将以多快的速度实施或模块(ESM)最终将如何使用。因此,ESLint 的默认设置是假设所有文件都是 ECMAScript 5。我们最终使用 ecmaVersion
解析器配置来允许人们在准备好时选择加入 ECMAScript 6。
快进到 2022 年:ECMAScript 不断发展,ESM 是每个人都在使用的标准模块格式。我们无法真正更改 eslintrc 的默认设置,而不会可能破坏许多现有的配置,但我们绝对可以在扁平化配置中进行更改。
扁平化配置具有以下默认值
- 所有 JavaScript 文件的
ecmaVersion: "latest"
- 没错,默认情况下,所有 JavaScript 文件都将设置为 ECMAScript 的最新版本。这模仿了 JavaScript 运行时的工作方式,即每次升级都意味着您选择加入 JavaScript 的最新最佳版本。此更改应该意味着您可能不必在配置中手动设置ecmaVersion
,除非您由于运行时限制而希望强制执行以前的版本。如果需要,您仍然可以将ecmaVersion
设置为低至3
。 - 所有
.js
和.mjs
文件的sourceType: "module"
- 默认情况下,扁平化配置假定您正在编写 ESM。如果不是,您始终可以将sourceType
设置回"script"
。 .cjs
文件的sourceType: "commonjs"
- 我们仍然处于过渡期,许多 Node.js 代码是用 CommonJS 编写的。为了支持这些用户,我们添加了一个新的sourceType
为"commonjs"
,它为该环境正确配置了所有内容。- ESLint 搜索
.js
、.mjs
和.cjs
文件 - 使用 eslintrc 时,当您在命令行上传递目录名称时,ESLint 仅搜索.js
文件,并且您需要使用--ext
标志来定义更多。使用扁平化配置时,会自动搜索所有三种最常见的 JavaScript 文件名扩展名。
我们对这些新的默认值感到非常兴奋,因为我们认为这将帮助人们更快、更轻松地上手 ESLint。
新的配置文件:eslint.config.js
与 eslintrc 允许在多个位置使用多个配置文件、多种配置文件格式甚至基于 package.json
的配置不同,扁平化配置只有一个位置用于您项目的所有配置:eslint.config.js
文件。通过将配置限制在一个位置和一种格式,我们可以直接利用 JavaScript 运行时的加载机制,避免需要自定义解析配置文件。
当使用 ESLint CLI 时,它会从当前工作目录搜索 eslint.config.js
,如果未找到,则会继续向上搜索目录的祖先,直到找到文件或到达根目录。那一个 eslint.config.js
文件包含该次 ESLint 运行的所有配置信息,因此与 eslintrc 相比,它大大减少了所需的磁盘访问,eslintrc 必须从被检查的文件位置到根目录检查每个目录以获取任何其他配置文件。
此外,使用 JavaScript 文件使我们能够依靠用户加载配置文件可能需要的其他信息。现在,您可以根据需要使用 import
和 require
来引入这些其他资源,而不是使用 extends
和 plugins
按名称加载。以下是一个 eslint.config.js
文件的示例
export default [
{
files: ["**/*.js"],
rules: {
"semi": "error",
"no-unused-vars": "error"
}
}
];
一个 eslint.config.js
文件导出一个配置对象数组。继续阅读以了解有关此示例的更多信息。
全局基于通配符的配置
虽然 eslintrc 中的 overrides
键是许多复杂性的来源,但有一点非常清楚:人们真的很喜欢能够在配置文件中通过通配符模式定义配置。因为我们希望消除 eslintrc 的配置级联,所以我们必须使用通配符模式来启用相同类型的配置覆盖。我们使用 overrides
配置作为扁平化配置的基础。
每个配置对象都可以具有可选的 files
和 ignores
键,用于指定基于 minimatch 的通配符模式以匹配文件。只有当文件名与 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
的等效项,尽管使用的是 minimatch 模式。
告别 extends
,你好扁平化级联
虽然我们希望摆脱基于目录的配置级联,但扁平化配置实际上仍然在您的 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
键,以及作为 parserOptions
的 ecmaVersion
和 sourceType
,更不用说添加更多全局变量的 env
了。也许最令人困惑的是,您必须同时设置 ecmaVersion
并添加像 es6
这样的环境来启用您想要的语法并确保可用的正确全局变量。
在扁平化配置中,我们将所有与 JavaScript 评估相关的键移动到一个名为 languageOptions
的新顶级键中。
在扁平化配置中设置 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 特定的全局变量)。如果您使用的是ecmaVersion: 3
或ecmaVersion: 5
,请务必设置sourceType: script
,如下所示
export default [
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: 5,
sourceType: "script"
}
}
];
告别环境,你好globals
eslintrc 中的环境提供了一组已知的全局变量,并且一直是用户困惑的根源。它们需要保持最新(尤其是在browser
的情况下),并且该更新需要等待 ESLint 版本发布。此外,我们还在环境中挂钩了一些附加功能,以便更轻松地使用 Node.js,最终,我们搞砸了。
对于扁平化配置,我们决定完全删除env
键。为什么?因为它不再需要了。我们挂钩到环境以用于 Node.js 的所有自定义功能现在都由sourceType: "commonjs"
涵盖,因此环境剩下的唯一作用就是管理全局变量。ESLint 在核心部分执行此操作没有意义,因此我们将此责任交还给您。
几年前,我们与 Sindre Sorhus 合作创建了globals
包,该包从 ESLint 中提取了所有环境信息,以便其他包可以使用它。然后 ESLint 使用globals
作为其环境的来源。
使用扁平化配置,您可以直接使用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 的优势在于个人和公司维护的插件生态系统,这些插件可以自定义其 lint 策略。因此,我们希望确保现有插件在无需修改的情况下继续工作,并允许插件执行过去无法执行的操作。
从表面上看,在扁平化配置中使用插件看起来与在 eslintrc 中使用插件非常相似。最大的区别在于 eslintrc 使用字符串,而扁平化配置使用对象。您无需指定插件的名称,而是直接导入插件并将其放入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
。
处理器的工作方式与 eslintrc 类似
顶级processor
键的工作方式与 eslintrc 中基本相同,主要用例是使用插件中定义的处理器,例如
import markdown from "eslint-plugin-markdown";
export default [
{
files: ["**/*.md"],
plugins: {
markdown
},
processor: "markdown/markdown"
}
];
此配置对象指定插件"markdown"
中包含一个名为"markdown"
的处理器,并将该处理器应用于所有以.md
结尾的文件。
扁平化配置中增加的一点是,processor
现在还可以是一个包含preprocess()
和postprocess()
方法的对象。
组织的 linter 选项
在 eslintrc 中,有一些键直接关系到 linter 的操作方式,即noInlineConfig
和reportUnusedDisableDirectives
。这些已移到新的linterOptions
键中,但工作方式与 eslintrc 中完全相同。这是一个示例
export default [
{
files: ["**/*.js"],
linterOptions: {
noInlineConfig: true,
reportUnusedDisableDirectives: true
}
}
];
共享设置完全相同
顶级settings
键的行为方式与 eslintrc 中完全相同。您可以定义一个包含键值对的对象,这些键值对应可用于所有规则。这是一个示例
export default [
{
settings: {
sharedData: "Hello"
}
}
];
使用预定义配置
ESLint 为 JavaScript 提供了两种预定义配置
js.configs.recommended
- 启用 ESLint 建议每个人都使用的规则,以避免潜在错误js.configs.all
- 启用与 ESLint 一起提供的全部规则
要包含这些预定义配置,请安装@eslint/js
包,然后在后续配置对象中对其他属性进行任何修改
import js from "@eslint/js";
export default [
js.configs.recommended,
{
rules: {
semi: ["warn", "always"]
}
}
];
在此,首先应用eslint:recommended
预定义配置,然后另一个配置对象添加semi
所需的配置。
向后兼容实用程序
如前所述,我们认为需要与 eslintrc 保持相当程度的向后兼容性,以便于过渡。@eslint/eslintrc
包提供了一个FlatCompat
类,使您可以轻松地在扁平化配置文件中继续使用 eslintrc 样式的共享配置和设置。这是一个示例
import { FlatCompat } from "@eslint/eslintrc";
import path from "path";
import { fileURLToPath } from "url";
// mimic CommonJS variables -- not needed if using CommonJS
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
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 文件,同时针对扁平化配置进行优化。我们认为这是必要的过渡步骤,以允许生态系统缓慢地转换为扁平化配置。
结论
团队花费了很长时间来设计扁平化配置,使其既对现有用户感到熟悉,又提供对每个人都有益的新功能。我们保留了规则、设置和处理器等内容,同时扩展了插件、语言选项和 linter 选项等内容,使其更加统一。我们认为扁平化配置在这两个方面之间找到了良好的平衡,并且一旦新的配置系统普遍可用,您将更享受使用 ESLint。同时,兼容性实用程序将允许您继续使用现有的共享配置。
在本博文系列的下一部分中,您将了解如何立即开始使用扁平化配置。
更新 (2024-08-12):更新了 JavaScript 的预定义 ESLint 配置。