
在 我之前的文章 中,我谈到了 eslintrc 配置系统如何通过一系列小的、渐进式的更改变得比必要的更复杂。另一方面,扁平配置系统从一开始就被设计得在许多方面都更简单。我们吸取了过去六年 ESLint 开发的所有经验教训,提出了一种配置的整体方法,该方法结合了 eslintrc 的优点以及其他与 JavaScript 相关的工具处理配置的方式。结果是,对于现有的 ESLint 用户来说,它应该会感到熟悉,并且比以前可能实现的强大得多。
文档: 在 官方文档 中阅读更多关于扁平配置系统的信息。
扁平配置的目标
为了为扁平配置的更改奠定基础,我们有几个目标
- 逻辑默认值 - 人们编写 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 的默认设置,而不会潜在地破坏大量现有配置,但我们绝对可以在扁平配置中进行更改。
扁平配置具有以下默认值
- 所有 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 必须检查从 linted 文件位置到根目录的每个目录以查找任何其他配置文件。
此外,使用 JavaScript 文件使我们能够依靠用户加载其配置文件可能需要的其他信息。现在,您可以直接使用 import
和 require
来引入这些额外的资源,而不是通过名称加载 extends
和 plugins
。以下是一个 eslint.config.js
文件的示例
export default [
{
files: ["**/*.js"],
rules: {
"semi": "error",
"no-unused-vars": "error"
}
}
];
eslint.config.js
文件导出一个配置对象数组。请继续阅读以了解有关此示例的更多信息。
随处可见的基于 Glob 的配置
虽然 eslintrc 中的 overrides
键是许多复杂性的来源,但有一件事非常清楚:人们非常喜欢能够在配置文件中通过 glob 模式定义配置。因为我们想消除 eslintrc 的配置级联,所以我们必须使用 glob 模式来启用相同类型的配置覆盖。我们使用 overrides
配置作为扁平配置的基础。
每个配置对象都可以具有可选的 files
和 ignores
键,用于指定基于 minimatch 的 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
的等效项,尽管使用的是 minimatch 模式。
告别 extends
,欢迎扁平级联
虽然我们想摆脱基于目录的配置级联,但扁平配置实际上仍然在您的 eslint.config.js
文件中直接定义了扁平级联。在数组内部,ESLint 查找所有与正在 linting 的文件匹配的配置对象,并将它们合并在一起,方式与 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
文件。当 linting 以 .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 评估其作用域结构的方式。我们保留了传统的 "module"
用于 ESM 和 "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 的优势在于个人和公司维护的插件生态系统,以自定义其 linting 策略。因此,我们希望确保现有插件继续工作而无需修改,并允许插件执行过去从未能够执行的操作。
从表面上看,在扁平配置中使用插件与在 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 配置。