前端工程:ESLint - 扁平化配置系统【译】

426 阅读9分钟

最近在一些项目中看到了 eslint.config.js 配置文件,知道它是 eslint 的新的配置方式,但是实际还未使用过。就特意去官网研究了一番,并翻译(机翻+摘要+润色)了相关的博客,以便更好的理解 eslint 做出这一重大改变的前因后果。

原文来自于 eslint 官方 blog,地址:eslint.org/blog/2022/0… 作者:Nicholas C. Zakas  尼古拉斯·扎卡斯,ESLint 的创建者、独立软件开发人员、顾问、教练和作家


第一部分:背景

在 2013 年 ESLint 首次发布的时候,它的配置系统相当简单。

只需要在.eslintrc文件中定义要启用或禁用的规则,然后当一个文件被 linted 时,ESLint 将首先在同级目录中查找 .eslintrc 配置文件,如果未找到则继续沿着目录层次结构向上查找,直到到达根目录,并合并沿途找到的所有 .eslintrc 文件的配置。这个系统,我们称之为配置级联,

但是随着多年来项目的发展,配置系统变得越来越糟。于是在 2019 年,我(即 Nicholas C.Zakas)提议创建一个新的配置系统,以便在越来越复杂的 js 项目中更方便的配置 eslint。

一、增量更改导致的复杂性

回顾 eslintrc 配置系统的演变历史,每一步在当时都是合乎逻辑的做法,而且 eslint 一直采用渐进式的开发方法。

1、extends

第一个重大变化是从 JSHint 中借鉴了 extends 配置,它允许用户导入另一个 eslint 配置,然后对其进行扩充。

{
  "extends": ["./other-config.json"],
  "rules": {
    "semi": "warn"
  }
}

这个功能的引入对 eslint 有重要的意义:

首先,extends 这个想法,实际上比通过 npm 分发共享配置更早。在实现 extends 的过程中,我们意识到可共享的配置是可能的,因为 extends 中指定的文件是通过 nodejs 的 require()加载的,所以它可以加载任何内容用作要扩展的配置。

其次,eslint 最初默认启用了几个规则,但这让一部分用户成为负担,所以后来默认关闭了所有规则,但是又让没有看到任何规则的新用户感到困惑。但是有了 extends,就可以使用 esllint:recommended提供一套规则,用户可以按需使用。

事后看来,如果我们再考虑一下,就会在这一节点上删除配置级联。引入 extends 后,许多用例都实现了与级联相同的用例,而保留这两个用例的结果却是一团糟,我们将花费数年时间试图解决。

2、个人配置

有用户要求增加一个 ~/.eslintrc 配置文件,也就是说如果在文件位置的祖先中没有配置文件,将自动查找用户的个人配置文件。

3、多种配置文件格式

eslint 允许不同的配置文件格式,包括 .eslintrc.json.eslintrc.yml.eslintrc.yaml.eslintrc.js,以及 .eslintrc

事后看来,这也不是个好主意,添加 js 配置文件格式会导致它与非 js 格式不兼容,比如:某些规则需要正则表达式对象才能正确配置,在 js 中完全没问题,因为任何对象都可以传递到配置中并在规则中可用。但是对于非 js 格式的文件中无法正确配置该规则。而且没法修复。

4、可共享的配置和依赖项

eslint 早期面临的最大问题可能是 npm 决定停止在 v3 版本中自动安装 peerDependencies,因为在这之前,eslint 建议共享配置将它们所依赖的插件作为 peerDependencies 而不是常规依赖,这是实现 extends 的一个特点:使用 require()

由于可共享的配置是纯数据的,不能直接引用 Node.js 依赖项,因此 require() 不会自动将直接依赖项加载到 ESLint 解析它们的路径中。另一方面,对等依赖项只需使用 require() 即可运行,因为这些依赖项安装在正常的包查找的位置。

当 npm v3 停止默认安装对等依赖项时,所有依赖此行为的共享配置都无法正常工作。有一个长期存在的 issue,要求允许可共享的配置直接使用依赖项,但 eslintrc 的架构不允许这样做。我们基本上必须在 ESLint 内部重新创建整个 require() 功能,以解决可共享配置的设计方式。我们建议可共享配置创建一个安装后脚本来安装其对等依赖项。无论如何,都不理想。

我们添加了 --resolve-plugins-relative-to 命令行选项来尝试解决此问题,但这还不够。在我们的r Discord #help channel 中最受欢迎的帮助请求与配置文件中插件解析不当有关。

最终在 npm v7 中改回了 默认安装对等依赖项,但到那时,对 ESLint 生态系统的损害已经造成

5、root

配置级联带来的一个问题是,用户不会意识到他正在处理的项目的祖先目录中有一个配置文件,然后最终得到了意外的配置。

为了解决这个问题,引入了 root 属性,通过root:true标识根目录,避免搜索到更上级的配置文件。

6、overrides 配置

有人提出需求:从现有配置文件中提供基于 glob 的配置,由此提供了 overrides 配置项,允许用户进一此修改 eslint 正在 linting 的特定文件子集的配置。

示例:

{
  "rules": {
    "quotes": ["error", "double"]
  },
  "overrides": [
    {
      "files": ["bin/*.js", "lib/*.js"],
      "excludedFiles": "*.test.js",
      "rules": {
        "quotes": ["error", "single"]
      }
    }
  ]
}

事实证明,overrides 基于 glob 的配置是完成配置级联操作的更好方式,而且此时是消除级联的最佳时机,但是是我们没有。

7、把 extends 添加到 overrides

eslintrc 开发的最后一步是在 overrides 中添加 extends 配置,允许将额外的配置数据注入到 glob 的配置对象中

{
  "rules": {
    "quotes": ["error", "double"]
  },
  "overrides": [
    {
      "files": ["bin/*.js", "lib/*.js"],
      "excludedFiles": "*.test.js",
      "extends": ["eslint:recommended"],
      "rules": {
        "quotes": ["error", "single"]
      }
    }
  ]
}

这个做法带来了很多额外的复杂性,因为必须弄清楚如何在两个不同的配置之间合并 glob 模式,最终结果是,extendsoverrides配置内部将使用 AND 运算符来合并filesexcludedFiles

二、简化的必要性

在 2019 年新年左右,我越来越担心 eslintrc 系统的复杂性,我们也收到越来越多的关于与加载找不到其他配置文件或插件的配置文件相关的晦涩的错误消息的问题。

而且,团队集体变得害怕触及与配置系统有关的任何事情。没有人真正理解计算任何给定文件的最终配置的所有不同排列。我们陷入了许多软件项目所犯的陷阱:我们不断添加新功能,而没有退后一步,从整体上看待问题和解决方案。这导致了我们部分代码几乎无法维护。

正是这个时候,我做了一个思想实验:如果从头开始,按照现在已知的所有关于 eslint 的一切,配置系统会是什么样子?随之而来的是 ESLint 历史上最具争议的 RFC 提案。当时,团队几乎平分秋色,一派想抛弃 eslintrc 从头开始,另一派认为 eslintrc 可以通过更多的迭代来保存。最终,经过 18 个月的修改和辩论,我们决定是时候着手构建一个全新的配置系统了。

三、前进的道路

现在是 2022 年,我们终于有了在 v8.21.0 中发布的新配置系统的第一个实现,我们称为“flat config(扁平配置)”,旨在让现有的 eslint 用户感到熟悉,同时大大简化配置文件的设置过程。


第二部分:扁平配置简介

上面谈到了 eslintrc 配置系统是如何通过一系列小的、渐进式的更改变得比实际需要更加复杂。另一方面,扁平化配置系统从一开始就被设计为在许多方面更加简单。

我们吸取了过去六年的所有经验教训,提出了一种整体的配置方法,该方法充分利用了 eslintrc 的优点,并将其与其他 JavaScript 相关工具处理配置的方式相结合。目的是希望现有 ESLint 用户感到熟悉,并且比以前更强大。

为了给扁平化配置的改变做好准备,我们定下了几个目标:

  1. 合乎逻辑的默认值:在过去的九年里,人们编写 JavaScript 的方式发生了很大的变化,我们希望新的配置系统能够反映我们当前的现实,而不是 ESLint 首次发布时所处的环境(也就是2013 年)。
  2. 统一配置方式:我们不希望再有多种方式来做同样的事情,而应该有一种方法可以为任何项目定义配置。
  3. 规则配置应该保持不变:我们觉得规则的配置方式已经很好用了,所以为了更容易过渡到平面配置,我们不想对 rules 配置进行任何更改
  4. 使用原生写的加载方案:对 eslintrc 最大的遗憾之一是以自定义方式重新创建 Node.js 的 require ,事后看来是不必要的,我们希望直接利用 js 的运行时加载功能
  5. 更好的组织顶级配置:自 eslint 发布以来,eslintrc 的顶级配置数量急剧增长,我们需要评估哪些是必要的,以及它们之间的关联
  6. 保持现有的插件系统:eslint 生态中有数百个插件,一个重要的目标是保证这些插件可以继续被使用
  7. 向后兼容是一个优先事项:即使我们正在迁移到一个新的配置系统,也不想抛弃所有现有的生态系统。特别是,我们希望有办法让可共享的配置继续尽可能地紧密地工作。虽然 100% 的兼容性可能是不现实的,但我们希望尽最大努力确保现有的可共享配置能够正常工作。