ESLint v8.x 已于 2024 年 10 月 5 日结束生命周期,不再维护。 升级 或考虑 长期支持选项

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% 的兼容性可能是不现实的,但我们希望尽最大努力确保现有的可共享配置能够工作。

考虑到这些目标,我们提出了新的扁平化配置系统。

设置代码检查的逻辑默认值

ESLint 最初创建时,ECMAScript 5 是 JavaScript 的最新版本,大多数文件都是作为“共享一切”脚本或 CommonJS 模块(用于 Node.js)编写的。ECMAScript 6 即将问世,但没有人知道它将以多快的速度实施或模块(ESM)将如何最终使用。因此,ESLint 的默认设置是假设所有文件都是 ECMAScript 5。我们最终使用了 ecmaVersion 解析器配置,以便人们在准备好时可以选择加入 ECMAScript 6。

快进到 2022 年:ECMAScript 不断发展,ESM 是每个人都在使用的标准模块格式。我们无法真正更改 eslintrc 的默认设置,而不会可能破坏许多现有的配置,但我们绝对可以对扁平化配置进行更改。

扁平化配置具有以下默认值

  • ecmaVersion: "latest" 用于所有 JavaScript 文件 - 没错,默认情况下,所有 JavaScript 文件都将设置为 ECMAScript 的最新版本。这模仿了 JavaScript 运行时的工作方式,因为每次升级都意味着您正在选择加入 JavaScript 的最新版本。此更改应该意味着您可能不必在配置中手动设置 ecmaVersion,除非您由于运行时约束而希望强制执行以前的版本。如果需要,您仍然可以将 ecmaVersion 设置为低至 3
  • sourceType: "module" 用于所有 .js.mjs 文件 - 默认情况下,扁平化配置假设您正在编写 ESM。如果不是,您始终可以将 sourceType 设置回 "script"
  • sourceType: "commonjs" 用于 .cjs 文件 - 我们仍处于过渡期,许多 Node.js 代码都是用 CommonJS 编写的。为了支持这些用户,我们添加了一个新的 sourceType"commonjs",它为该环境正确配置了所有内容。
  • ESLint 搜索 .js.mjs.cjs 文件 - 使用 eslintrc 时,当您在命令行上传递目录名称时,ESLint 仅搜索 .js 文件,并且您需要使用 --ext 标志来定义更多文件。使用扁平化配置时,将自动搜索所有三种最常见的 JavaScript 文件名扩展名。

我们对这些新的默认值感到非常兴奋,因为我们认为这将帮助人们更快、更轻松地上手 ESLint。

新的配置文件:eslint.config.js

与 eslintrc 相比,eslintrc 允许在多个位置使用多个配置文件、多种配置文件格式,甚至基于 package.json 的配置,而扁平化配置只有一个位置用于您项目的所有配置:eslint.config.js 文件。通过将配置限制在一个位置和一种格式,我们可以直接利用 JavaScript 运行时的加载机制,避免需要自定义解析配置文件。

当使用 ESLint CLI 时,它会在当前工作目录中搜索 eslint.config.js,如果未找到,则会继续向上搜索目录的祖先,直到找到该文件或到达根目录。该 eslint.config.js 文件包含该次 ESLint 运行的所有配置信息,因此与 eslintrc 相比,它显着减少了所需的磁盘访问次数,eslintrc 必须检查从被检查文件位置到根目录的每个目录以查找任何其他配置文件。

此外,使用 JavaScript 文件使我们能够依靠用户加载配置文件可能需要的其他信息。您现在可以使用 importrequire 根据需要引入这些额外资源,而不是使用 extendsplugins 按名称加载它们。以下是 eslint.config.js 文件的一个示例

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

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

全局基于通配符的配置

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

每个配置对象都可以具有可选的 filesignores 键,用于指定基于 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 键,以及作为 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 如何评估其作用域结构。 我们保留了 ESM 的传统 "module" 和脚本的 "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 的优势在于个人和公司维护的插件生态系统,这些插件可以自定义其 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() 方法的对象。

组织好的 lint 器选项

在 eslintrc 中,有一些键直接与 lint 器的操作方式相关,即 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 文件,同时针对扁平化配置对其进行优化。 我们认为这是必要的过渡步骤,以允许生态系统缓慢地转换为扁平化配置。

结论

团队花费了很长时间设计扁平化配置,使其既对现有用户感到熟悉,又提供对所有人都有益的新功能。 我们保留了规则、设置和处理器等内容,同时扩展了插件、语言选项和 lint 器选项等内容,使其更加统一。 我们认为扁平化配置在这两个方面之间找到了良好的平衡,并且一旦新的配置系统普遍可用,您将更喜欢使用 ESLint。 同时,兼容性实用程序将允许您继续使用现有的共享配置。

在本博文系列的下一部分中,您将了解如何从今天开始使用扁平化配置。

更新 (2024-08-12):更新了 JavaScript 的预定义 ESLint 配置。

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

ESLint v9.16.0 released
阅读 2 分钟

ESLint v9.16.0 发布

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

ESLint v9.15.0 released
阅读 2 分钟

ESLint v9.15.0 发布

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

ESLint v9.14.0 released
阅读 2 分钟

ESLint v9.14.0 发布

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