ESLint 的新配置系统,第二部分:扁平配置简介

ESLint 的新配置系统,昵称扁平配置,旨在既熟悉又比原始配置系统简单得多。

我之前的文章 中,我谈到了 eslintrc 配置系统如何通过一系列小的、渐进式的更改变得比必要的更复杂。另一方面,扁平配置系统从一开始就被设计得在许多方面都更简单。我们吸取了过去六年 ESLint 开发的所有经验教训,提出了一种配置的整体方法,该方法结合了 eslintrc 的优点以及其他与 JavaScript 相关的工具处理配置的方式。结果是,对于现有的 ESLint 用户来说,它应该会感到熟悉,并且比以前可能实现的强大得多。

文档:官方文档 中阅读更多关于扁平配置系统的信息。

扁平配置的目标

为了为扁平配置的更改奠定基础,我们有几个目标

  1. 逻辑默认值 - 人们编写 JavaScript 的方式在过去九年中发生了很大变化,我们希望新的配置系统反映我们当前的现实,而不是 ESLint 首次发布时的现实。
  2. 定义配置的一种方式 - 我们不希望人们再有多种方式来做同一件事。对于任何给定的项目,应该有一种定义配置的方式。
  3. 规则配置应保持不变 - 我们认为规则的配置方式已经运行良好,因此为了更容易过渡到扁平配置,我们不想对规则配置进行任何更改。相同的 rules 键可以在扁平配置中以相同的方式使用。
  4. 对所有内容使用原生加载 - 我们对 eslintrc 最大的遗憾之一是以自定义方式重新创建了 Node.js require 解析。这是复杂性的一个重要来源,事后看来是不必要的。展望未来,我们希望直接利用 JavaScript 运行时的加载能力。
  5. 更好组织的顶级键 - 自 ESLint 发布以来,eslintrc 顶级的键的数量急剧增加。我们需要查看哪些键是必要的,以及它们彼此之间是如何关联的。
  6. 现有插件应该可以工作 - ESLint 生态系统充满了数百个插件。重要的是这些插件能够继续工作。
  7. 向后兼容性应该是优先事项 - 即使我们正在转向新的配置系统,我们也不想抛弃所有现有的生态系统。特别是,我们希望有方法让可共享配置尽可能地继续工作。虽然我们知道 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 文件使我们能够依靠用户加载其配置文件可能需要的其他信息。现在,您可以直接使用 importrequire 来引入这些额外的资源,而不是通过名称加载 extendsplugins。以下是一个 eslint.config.js 文件的示例

export default [
{
files: ["**/*.js"],
rules: {
"semi": "error",
"no-unused-vars": "error"
}
}
];

eslint.config.js 文件导出一个配置对象数组。请继续阅读以了解有关此示例的更多信息。

随处可见的基于 Glob 的配置

虽然 eslintrc 中的 overrides 键是许多复杂性的来源,但有一件事非常清楚:人们非常喜欢能够在配置文件中通过 glob 模式定义配置。因为我们想消除 eslintrc 的配置级联,所以我们必须使用 glob 模式来启用相同类型的配置覆盖。我们使用 overrides 配置作为扁平配置的基础。

每个配置对象都可以具有可选的 filesignores 键,用于指定基于 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 键,用于修改可用的全局变量,以及作为 parserOptionsecmaVersionsourceType,更不用说 env 来添加更多全局变量。也许最令人困惑的是,您必须同时设置 ecmaVersion 并添加像 es6 这样的环境,才能同时启用您想要的语法并确保正确的全局变量可用。

在扁平配置中,我们将所有与 JavaScript 求值相关的键移动到一个名为 languageOptions 的新顶级键中。

在扁平配置中设置 ecmaVersion

最大的变化是我们将 ecmaVersionparserOptions 中移出,直接移入 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: 3ecmaVersion: 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 动态插入您想要的任何全局变量。

自定义解析器和解析器选项基本相同

parserparserOptions 键现在已移动到 languageOptions 键中,但它们的工作方式与 eslintrc 中的基本相同,只有两个具体区别

  1. 您现在可以将解析器对象直接插入到配置中。
  2. 解析器现在可以与插件捆绑在一起,您可以为 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 的操作方式相关,即 noInlineConfigreportUnusedDisableDirectives。这些已移动到新的 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 配置。

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

Evolving flat config with extends
5 分钟阅读

使用 extends 进化扁平配置

您的 eslint.config.js 文件现在可以使用 extends 来简化您的配置。

ESLint v9.22.0 released
1 分钟阅读

ESLint v9.22.0 发布

我们刚刚推送了 ESLint v9.22.0,这是一个 ESLint 的次要版本升级。此版本添加了一些新功能并修复了先前版本中发现的几个错误。

ESLint v9.21.0 released
2 分钟阅读

ESLint v9.21.0 发布

我们刚刚推送了 ESLint v9.21.0,这是一个 ESLint 的次要版本升级。此版本添加了一些新功能并修复了先前版本中发现的几个错误。