ESLint 9.0 配置打平(flat)改造

发表于 2024-12-09
ESLint

ESlint 升级到 9.0 之后配置文件名由.eslintrc.js 更改为 eslint.config.js ,并且配置项由原本的嵌套对象结构更改为了打平数组结构,废弃了原本的 extendsoverride 等组合式字段。姑且称这种新模式为 flat mode

https://eslint.org/docs/latest/use/configure/configuration-files

之前写过一篇文章 ESLint 配置最佳实践,内容包括 eslint 的语言配置、与 prettier 的协同、IDE 的格式化能力等。来到 9.0 之后配置大改,因此单独更新一文专门记录配置的调整。

前置的思考

虽然我是「极左派」(会及时把软件或者系统升级到最新以尝鲜),但对于依赖包而言,在真正动手之前还是需要改动的 ROI 有几分底气。

关于性能

9.0 之后 ESlint 的配置检索只限定于指定名称的文件,不再从 package.json等内部读取。所以配置性能读取可以说有质的提升。包括 extends一些隐晦的别名写法实际上也会降低配置文件路径的精确度,而需要多次遍历。

关于工程集成性

工程集成性其实分为两个方面:多语种和复用性。

在这之前可以说 ESlint 是专为 Javascript 设计的。一方面 Javascript 本身比较能“整活”,ES5、ES6 等语法迭代太快都快成两种语言了,更别说当今又是 Typescript 的天下,混合着 React TSX 等多种框架特有语法简直欲仙欲死; 另一方面对于工程而言 JSON、YAML、SVG、CSS 等多种格式的文件也有了不同程度的 Lint 要求。

在大型的 Monorepo 工程下,不同类型的项目(webApp、node、unit test)等不同性质的项目对 Lint 的要求也有所不同。 extendsoverride配置是“继承覆盖”的思路,相对繁琐的多。

各类插件的兼容性

自己写 rule 是不可能自己写的,用 ESlint 还是以配置市面上已有的插件为主。

那么问题来了,几个主流插件是否支持扁平化配置呢?

✅ eslint-plugin-prettier

Prettier 作为代码风格的基石,充满了老一辈的艺术家从容,从 5.1.0 版本起 eslint-plugin-prettier 包中增加一个 recommended 导出支持扁平配置

20260105220402045.png

别忘记了使用它需要安装 prettier, eslint-config-prettier, eslint-plugin-prettier 三个包

import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
 
export default [
  eslintPluginPrettierRecommended,
  {
    rules: {
      'prettier/prettier': [
        // custom rules
      ]
    }
  }
]

❌ @typescript-eslint/eslint-plugin

https://typescript-eslint.io/getting-started/legacy-eslint-setup

使用 @typescript-eslint/parser 直接配置 parser 的时代已经过去了,parser 等语言相关的配置项都被收到了 languageOptions 里面。所以 @typescript-eslint/parser 搭配 @typescript-eslint/eslint-plugin 的配置方式已经不兼容。

✅ typescript-eslint@v8

https://typescript-eslint.io/getting-started/

早在 v7 版本typescript-eslint 就开始有意收紧各类依赖的开放程度来改善各种版本配置的兼容性,在官方博客中的描述为「Into the Future」。当 ESlint 迈向 9.0 时,v8 版本的 typescript-eslint 用一个 breaking change 告别了过去繁琐且充满不确定性的配置。

至此 Typescript 的 ESlint 配置可以简化为:

import eslintJs from '@eslint/js';
import tsEslint from 'typescript-eslint';
 
export default [
  eslintJs.configs.recommended,
  ...tsEslint.configs.recommended,
]

✅ eslint-plugin-import-x 与 eslint-plugin-import

截止到目前两者都已经支持了 ESlint 9。

要吐槽的是 eslint-plugin-import 动作要慢的很多,可以看到相关的 issues 在 23 年就已经提出,而 eslint-plugin-import 在 24 年的 9 月才发布 2.30.0 版本支持,对它的持续维护性和活跃度感到担忧。

我在升级的时候它还没有支持计划,迫不得已我只能换成 eslint-plugin-import-x,作为更轻量级的平替用起来差别不大、体感不明显:

import eslintPluginImportX from 'eslint-plugin-import-x';
 
export default [
  eslintPluginImportX.flatConfigs.recommended,
  eslintPluginImportX.flatConfigs.typescript,
]

✅ eslint-plugin-jsonc

我把它用于 package.json 中依赖的自动排序,Monorepo 仓库中的永远的神。我们只需要用它 prettier 的插件即可

import eslintPluginJsonc from 'eslint-plugin-jsonc';
export default [
  ...eslintPluginJsonc.configs['flat/prettier'],
]

按照语言/功能性分类

基于扁平化的设计以及官方的 files、languageOptions 等资源,按照语言来分类是一件非常自然而言的事情,除了 prettier 本身是功能性插件以外(JS 和 TS 都会用到)

- prettier        # 公共功能性插件
- javascript
- typescript
- package-json

export default config[] 约定

可以发现上文在讲插件时,有的插件导出是一个对象需要这样使用 [someConfig];有的插件导出是一个数组这样使用[...someConfig]

没有统一的导出约定也会极大提高使用成本,所以内部封装的插件统一是导出数组。

configMerge

该函数的主要功能是为所有的数组添加上 files 选项,保证不同规则之间的隔离

const configMerge = (configs, config) => {
  return configs.map((conf) => ({ ...conf, ...config }));
};

比如最终的 tsLintConfig 如下:

const config = [
  eslintJs.configs.recommended,
  ...tsEslint.configs.recommended,
  ...tsEslint.configs.stylistic,
  ...eslintPrettier,
  eslintPluginImportX.flatConfigs.recommended,
  eslintPluginImportX.flatConfigs.typescript,
]
 
export default configMerge(config, {
  files: ['**/*.{ts,tsx}'],
})

后言

从历史包袱和活跃性而言,JS 和 PHP 是何其相似的两兄弟,成也萧何败也萧何。

工具链的统一规范对一门语言重要性不言而喻。抛开其他因素不谈,ESlint 9 有勇气甩开历史包袱重构是值得点赞的,而且社区的插件能这么快响应是我意料之外的(点名批评 import-js),我以为会像 rollup 一样先烂一阵子。