把一个跑了三年的项目从 ESLint 8 升到 9,第一次 npx eslint . 直接吐出一堆红字,说找不到配置文件。项目根目录明明躺着 .eslintrc.json,ESLint 就是当没看见。这不是 bug,是 9.0 把默认配置系统整个换掉了。
ESLint 9.0.0 在 2024 年 4 月正式发布,是这个工具历史上改动最大的一次。Flat Config 从可选变成默认,一批格式化程序被踢出核心包,推荐规则集调整,规则 API 也做了破坏性改革。这篇把这些变化逐条拆开,配上 8 和 9 的对比表格,让你知道升级过程中每个报错对应的是哪一项改动。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- Node.js 版本要求变化,以及为什么 19.x 被排除在外
- Flat Config 的结构、字段含义与和 eslintrc 的对应关系
- 老项目怎么用
FlatCompat过渡,什么时候该彻底重写配置 - 移除的格式化程序、规则和 context 方法的完整清单
- 规则默认行为的几处静默变化,最容易在升级后误伤代码
- 新增的
--stats、--inspect-config和loadESLint()怎么用 - 写插件的人需要改哪些地方
- 一份可以照着走的六步升级流程
# 一、先确认 Node 版本
升级第一件事不是装 ESLint,是看 Node。
ESLint 9 不再支持 Node.js v18.18.0 以下的版本,也不再支持 v19.x。最低要求是 v18.18.0 或更高。
# 检查Node.js版本
node -v
# 如果版本过低,需要升级Node.js
nvm install 20
nvm use 20
v19 被整个排除掉这件事经常有人问。原因是 Node 的奇数版本号是非 LTS 的短期发布线,19.x 早就过了维护期,ESLint 没有理由为一条已经停止支持的分支付出兼容成本。如果你线上还在跑 v19,升级 ESLint 之前先把 Node 挪到 20 或者 18.18 以上,这一步绕不过去。
版本确认完再装。
# 安装最新版本的ESLint
npm install eslint@9 --save-dev
# 或指定版本
npm install eslint@9.0.0 --save-dev
# 二、Flat Config 是怎么回事
ESLint 9 最大的变化是 Flat Config 成为默认配置格式,原来基于 .eslintrc.* 的方式正式被弃用。
新的配置文件名有三种。
eslint.config.jseslint.config.mjseslint.config.cjs
ESLint 会从当前工作目录往上找这三个文件,找不到就报错,而不是回退去读 .eslintrc.json。开头那个「找不到配置文件」的报错就是这么来的。
那为什么要换。老的 eslintrc 系统有几个绕不开的设计问题。extends 的继承链是字符串解析出来的,你很难知道最终生效的规则是哪一层给的;插件名要遵守 eslint-plugin- 前缀约定,ESLint 自己去 node_modules 里找,路径解析在 monorepo 里经常出错;env 这种预设集合是硬编码在 ESLint 内部的,加一个新环境要改核心代码。
Flat Config 的思路是把这些全部交还给 JS 模块系统。配置文件就是一个导出数组的普通模块,插件是你自己 import 进来的对象,继承就是数组展开,谁覆盖谁一目了然。
# 新配置文件的结构
// eslint.config.js
export default [
{
name: 'my-configuration',
files: ['**/*.js'],
ignores: ['**/dist/**'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
browser: true,
node: true,
es2022: true
}
},
linterOptions: {
noInlineConfig: false,
reportUnusedDisableDirectives: 'warn'
},
plugins: {
react: reactPlugin
},
rules: {
'no-unused-vars': 'error',
'react/react-in-jsx-scope': 'off'
}
}
];
数组里每一项叫一个 config object,ESLint 会按顺序把匹配当前文件的所有配置项合并,后面的覆盖前面的。这个规则很简单,但正是因为简单,你才能靠肉眼推断出最终结果。
各字段的作用整理成表。
| 属性 | 说明 |
|---|---|
name |
配置名称,用于错误消息中识别配置 |
files |
glob模式,指定配置适用的文件 |
ignores |
glob模式,指定忽略的文件 |
languageOptions |
JavaScript语言配置 |
linterOptions |
Linter过程相关设置 |
plugins |
插件配置 |
rules |
规则配置 |
settings |
共享设置 |
有个坑要注意。ignores 写在带 files 的配置项里,只对这一项生效;单独写一个只有 ignores 的配置项,才是全局忽略。另外 .eslintignore 文件在 Flat Config 下彻底不被读取了,从 8 升上来最容易漏掉这一步,表现是构建产物目录突然开始被 lint,报出成千上万条错误。
用官方推荐规则集的写法也变了。
// eslint.config.js
import js from '@eslint/js'
export default [
js.configs.recommended,
{
rules: {
'no-unused-vars': 'warn'
}
}
]
以前写 extends: ['eslint:recommended'],现在要装 @eslint/js 然后 import 进来。这个包是从 ESLint 核心里拆出去的,装 eslint 时不会自动带上,得单独装。
# 老项目怎么过渡
如果项目庞大到没法一次性重写配置,@eslint/eslintrc 提供了 FlatCompat,可以把老格式的配置塞进新系统。
// eslint.config.js
import { FlatCompat } from '@eslint/eslintrc'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname
})
export default [
...compat.extends('standard', 'react'),
...compat.env({
es2022: true,
node: true
}),
...compat.plugins('react'),
{
rules: {
'semi': 'error'
}
}
]
compat.extends() 对应老的 extends,compat.env() 对应 env,compat.plugins() 对应 plugins 数组。它做的事情是把老写法翻译成等价的 config object 数组再展开进去。
我的建议是把 FlatCompat 当过渡手段而不是终点。它能让你今天就完成升级,但配置的可读性会变差,而且社区的配置包陆续都提供原生 Flat Config 导出之后,留着 compat 层反而是负担。等你依赖的几个 config 包都出了新版本,就该把它拆掉。
如果你想看一份已经完全是 Flat Config 写法、跑在 Next.js 项目上的完整配置,可以看我另一篇 基于 ESLint 9 配置前端开发规范,那篇把规则取舍也一条条说了。
# 三、ESLint 8 与 9 的对比清单
这一节是升级时最实用的部分,遇到报错先来这里对一眼。
# 配置方式
| 特性 | ESLint 8 | ESLint 9 |
|---|---|---|
| 配置文件 | .eslintrc.json |
eslint.config.js |
| 配置格式 | 对象形式 | 数组形式 |
| 插件引用 | plugins: ['react'] |
plugins: { react: reactPlugin } |
| 环境变量 | env属性 |
languageOptions.globals |
| 全局变量 | globals属性 |
languageOptions.globals |
| 推荐规则 | extends: ['eslint:recommended'] |
js.configs.recommended |
env 这一行的变化影响面最广。以前写 env: { browser: true },ESLint 内部会把浏览器的一整套全局变量塞进来。现在没有这个预设了,要么自己列,要么装 globals 这个包,写成 globals: { ...globals.browser }。忘了这一步的典型症状是满屏的 'window' is not defined。
# API 变化
| 特性 | ESLint 8 | ESLint 9 |
|---|---|---|
| 主类 | ESLint |
ESLint (原FlatESLint) |
| 加载配置 | 自动 | loadESLint() |
| CLI | eslint CLI |
eslint CLI (默认flat config) |
| 环境变量 | ESLINT_USE_FLAT_CONFIG=false |
默认开启 |
ESLINT_USE_FLAT_CONFIG=false 这个环境变量是 9.x 留的后门,设了它 ESLint 会退回去读 eslintrc。实在来不及改配置又必须先升 ESLint 的话,可以拿它救一下急,但要清楚这是临时方案,后续大版本会把这条路封掉。
# 移除的格式化程序
| 移除的格式化器 | 替代npm包 |
|---|---|
checkstyle |
eslint-formatter-checkstyle |
compact |
eslint-formatter-compact |
jslint-xml |
eslint-formatter-jslint-xml |
junit |
eslint-formatter-junit |
tap |
eslint-formatter-tap |
unix |
eslint-formatter-unix |
visualstudio |
eslint-formatter-visualstudio |
保留在核心里的只有 stylish、html、json、json-with-metadata 四个。
这条对 CI 影响最大。很多流水线用 --format junit 把结果喂给测试报告系统,升级之后直接报找不到 formatter,构建全红。改法很简单,把对应的 eslint-formatter-* 包装上就行,用法不变。踩到这个坑的人特别多,因为本地跑 lint 用的是默认 stylish,根本发现不了。
# 移除的规则
| 移除的规则 | 替代方案 |
|---|---|
valid-jsdoc |
使用 eslint-plugin-jsdoc |
require-jsdoc |
使用 eslint-plugin-jsdoc |
这两条本来就长期处于弃用状态,JSDoc 相关的检查交给专门的插件做更合适。