ESLint 10 把 .eslintrc 删了:flat config 到底怎么配,我帮你踩完了

从 .eslintrc 到 eslint.config.mjs:一次升级让我重新理解了 ESLint

你升级了 ESLint,兴冲冲地重跑一遍 lint,结果屏幕上出现的不是熟悉的 warning 列表,而是一句陌生的报错:

text 复制代码
ESLint couldn't find an eslint.config.(js|mjs|cjs) file.

你第一反应是:我的 .eslintrc.json 写得好好的,怎么突然找不到了?

不是你写错了,是 ESLint 10 把 .eslintrc 整个删了。

从 ESLint 9 开始,旧版 .eslintrc 被标记为 deprecated;到了 ESLint 10(2025 年 10 月发布),它被彻底移除 ------不是"不推荐用",是"读都不读"。你项目里那些 .eslintrc.json.eslintrc.jsextendsenv,在新版本面前等于空气。

这篇文章就是来解决这个问题的:为什么 ESLint 要删掉用了十年的配置格式,以及新的 flat config 到底怎么写。


一句话搞懂 flat config 是什么

flat config 就是:把原来"层层级联、extends 到处继承"的配置,拍平成一个 JS 数组。数组里每个对象就是一份完整、独立的配置块。

.eslintrc 长这样(一个文件,一堆顶层字段):

json 复制代码
{
  "extends": ["eslint:recommended"],
  "env": { "node": true },
  "parserOptions": { "ecmaVersion": 2022, "sourceType": "module" },
  "plugins": ["react"],
  "rules": { "quotes": ["error", "double"] },
  "overrides": [{ "files": ["*.test.js"], "env": { "jest": true } }]
}

新 flat config 长这样(一个 .mjs 文件,导出一个数组):

javascript 复制代码
import js from "@eslint/js";
import globals from "globals";

export default [
  {
    files: ["**/*.{js,mjs,cjs}"],
    plugins: { js },
    extends: ["js/recommended"],
    languageOptions: { globals: globals.node },
    rules: {
      "no-var": 2,
      "no-console": 1,
      "quotes": ["error", "double"],
      "semi": ["error", "always"],
      "indent": ["error", 2],
    },
  },
];

一眼能看出的区别:从"一棵树"变成了"一排卡片"。 没有 extends 的继承链了,没有 env 了,没有 overrides 了。


为什么 ESLint 要自断双臂?------这才是关键

很多人看到 flat config 的第一反应是"这不就是把 JSON 换成 JS 了吗,换汤不换药"。不是。 换成 JS 只是表象,背后是 ESLint 对"配置是怎么被解析出来的"这件事的彻底重做。

.eslintrc 的解析方式叫 级联(cascade) 。当 ESLint 要检查 src/a.js 时,它会:

text 复制代码
src/a.js
    │
    ├─ 读 src/.eslintrc.json
    ├─ 再往上读 .eslintrc.json
    ├─ extends: "eslint:recommended" → 去 node_modules 解析这个共享配置
    ├─ extends: ["plugin:react/recommended"] → 再解析 react 插件的配置
    └─ 把上面所有结果合并成一个"最终 config",才轮到检查文件

看懂问题了吗?为每个文件都要重新走一遍这条"向上找 + 解析 extends"的链路。 你的项目越大、共享配置越深,这个解析过程就越慢、越不可预测。

更要命的是隐式合并:最终生效的 config 到底是哪些文件拼出来的、拼的顺序是什么、谁覆盖了谁,你基本只能靠"试"。配置 bug 成了 ESLint 最让人头疼的问题------不是规则写错了,而是"这条规则其实没生效,因为被另一个 extends 悄悄覆盖了"。

flat config 的答案是釜底抽薪:配置就是一段 JS,你导出一个数组,ESLint 按数组顺序读,谁在前谁在后一目了然。 没有级联、没有隐式合并、没有 extends 解析------extends: "eslint:recommended" 变成了显式的 import + plugins + extends

一句话总结这个"为什么"

.eslintrc 是"运行时递归拼配置",flat config 是"你写什么就是什么,顺序即优先级"。前者省了几行配置,代价是解析慢、结果不可预测;后者多写几个 import,换来确定性和可调试性。

这就是 ESLint 团队敢删 .eslintrc 的底气------不是新瓶装旧酒,是把"配置解析"这个烂摊子整个重写了。


逐行拆解:这 10 行代码到底在做什么

下面这份就是完整可跑的 eslint.config.mjs(这个 demo 里就在用):

javascript 复制代码
// 这个文件是 ESLint 的 flat config:导出一个配置对象数组
import js from "@eslint/js";          // ① 内置推荐规则的官方包
import globals from "globals";        // ② 全局变量包(替代旧 env)

export default [
  {
    files: ["**/*.{js,mjs,cjs}"],     // ③ 这份配置作用在哪些文件
    plugins: { js },                  // ④ 把 @eslint/js 挂成一个叫 js 的插件
    extends: ["js/recommended"],      // ⑤ 引用这个插件里的 recommended 配置
    languageOptions: {
      globals: globals.node,          // ⑥ 声明 Node 全局变量
    },
    rules: {
      "no-var": 2,                    // ⑦ 规则列表(2=error 1=warn 0=off)
      "no-console": 1,
      "quotes": ["error", "double"],
      "semi": ["error", "always"],
      "indent": ["error", 2],
    },
  },
];

逐个解释几个最容易被卡住的点:

import js from "@eslint/js" --- 旧版 extends: "eslint:recommended" 是 ESLint 内置的,不用装。新版把它拆成了独立的 @eslint/js 包,得 npm i -D @eslint/js。这个包导出的 js 对象里带着 recommended 这份配置。

import globals from "globals" --- 旧版写 "env": { "node": true } 就声明了 processconsole__dirname 这些 Node 全局变量。新版改成装 globals 包,然后 globals.node。这是迁移时最容易被忽略的一步 ------漏了它,process 会被当成未定义变量报 no-undef

④ + ⑤ 为什么 extends 还要留着? 注意这里 extends: ["js/recommended"] 不是旧版那种"字符串去 node_modules 找"。它是"在我第 ④ 步挂载的 js 插件里,找名叫 recommended 的那份配置"。本质已经从"继承"变成"引用本地变量"了。

⑦ 数字 vs 字符串 --- "no-var": 2 里的 2 是历史遗留的简写:0 = off1 = warn2 = error。flat config 里两种都能用,写成 "error" 更可读,但 demo 里用数字是为了演示这个映射关系。


新旧映射表:照着改就能迁移

把旧 .eslintrc 的每个字段搬到 flat config,对照这张表:

.eslintrc(旧) flat config(新) 说明
extends: "eslint:recommended" import js from "@eslint/js" + plugins: { js } + extends: ["js/recommended"] 内置推荐规则拆成独立包
env: { node: true } import globals from "globals" + languageOptions: { globals: globals.node } 全局变量改成显式包
parserOptions: { ecmaVersion, sourceType } languageOptions: { ecmaVersion, sourceType } 字段挪进 languageOptions
plugins: ["react"] plugins: { react } 从字符串数组变成 import 对象
rules: { ... } rules: { ... } 完全不变
overrides: [{ files, rules }] 数组里每个对象自带 files 字段 overrides 概念消失,每块配置独立声明作用文件

注意最后一行的深意:旧版用 overrides 去"覆盖"默认配置,新版没有"默认 + 覆盖"这回事了------每份配置自己声明 files 作用域,数组顺序就是覆盖顺序。 后写的配置块,规则优先级更高。


实验:这个 demo 到底抓到了什么

光讲概念没意思,跑一遍给你看真实输出。这个 demo 里有一个故意写得不干净的 index.mjs

javascript 复制代码
let name = "wuxianhong";
let a = 1;
function hello() {
  console.log(name + "hello");
}
hello();

执行 npx eslint .,真实输出:

text 复制代码
C:\...\eslint-demo\index.mjs
  2:5  error    'a' is assigned a value but never used  no-unused-vars
  4:3  warning  Unexpected console statement            no-console

✖ 2 problems (1 error, 1 warning)

这里藏着三个值得记住的点:

① 真正帮你抓 bug 的,是 js/recommended 自带的 no-unused-vars,不是你手写的那几条规则。 let a = 1; 声明了却从没用到,被标成 error。这正是 extends: ["js/recommended"] 的价值------它打包了一堆"潜在 bug 检测"规则,你一条没写也在生效。

no-console 报的是 warning 而不是 error ,因为 demo 里写的是 "no-console": 11 = warn)。所以 lint 虽然报问题,但退出码是 1 是因为有 error,warning 本身不会让 CI 挂。

③ 那几条 quotes / semi / indent 规则"没反应" ------ 不是因为失效,是因为这份代码刚好已经用了双引号、加分号、2 空格缩进。规则只在你违反它时显形,这恰恰说明"配了规则 ≠ 规则在干活",得拿一条真的会违反的代码去验证它。


再补一刀:--fix 不是万能的

很多人以为 eslint . --fix 能一键修复所有问题。跑一下:

text 复制代码
$ npx eslint . --fix

C:\...\eslint-demo\index.mjs
  2:5  error    'a' is assigned a value but never used  no-unused-vars
  4:3  warning  Unexpected console statement            no-console

✖ 2 problems (1 error, 1 warning)

结果一模一样,什么都没修。 因为 no-unused-varsno-console不可自动修复的规则------ESLint 没法替你判断"这个没用的变量是删掉还是你本来想用",也没法替你决定"这个 console.log 是调试残留还是真的日志"。

只有格式类 规则(quotessemiindentcomma-dangle)能被 --fix 自动修。逻辑类、语义类规则只能报,不能修。

这个认知很重要 :如果你希望 lint 在 CI 里自动修格式、只把"必须人工看"的问题留给开发者,就要区分这两类规则------格式类交给 --fix,语义类让它报出来。


两个我在真实项目里踩过的坑

坑一:type: commonjs.mjs 打架

看这个 demo 的 package.json

json 复制代码
{
  "type": "commonjs",
  "main": "index.js",
  "scripts": { "lint": "eslint ." }
}

"type": "commonjs" 意味着 .js 文件按 CommonJS 解析,但配置文件是 .mjs、入口是 index.mjs。这里 .mjs 后缀强制 ESM,所以能跑------但 main 字段写的是 index.js实际文件却是 index.mjs ,你 node . 会直接 Cannot find module

迁移到 flat config 时,最省心的做法是:配置文件统一用 eslint.config.mjs.mjs 永远按 ESM 解析,不受 package.jsontype 影响),别让 type 字段和文件后缀互相打架。

坑二:漏装 globals,报一堆 no-undef

迁移时最容易漏的一步。旧版 env: { node: true } 是"免费"的,新版要 npm i -D globalslanguageOptions: { globals: globals.node }。漏了它,项目里所有 processconsole__dirname 全报 'process' is not defined,你会以为是规则配错,其实是全局变量没声明。


总结:记住这三件事

第一句(迁移口诀)extends@eslint/jsenvglobalsoverrides 换成数组里的 filesrules 原样不动。

第二句(理解本质)

flat config 不是"把 JSON 换成 JS",而是把"运行时递归拼配置"改成"你写的数组顺序即优先级"。

第三句(一个行为改变) :下次再看到 .eslintrc 教程,先看一眼 ESLint 版本------9 是过渡,10 已经删了,照着旧教程配只会得到一句 "couldn't find an eslint.config file"。

一个开放问题 :你的项目还在用 .eslintrc 吗?迁移到 flat config 时,是 envglobals 替换卡住了你,还是某个自定义插件(比如 eslint-plugin-react 的新版写法)卡住了你?评论区聊聊,我帮你对。


相关推荐
_约书亚_3 小时前
Chapter 4 并发同步操作 · 归纳总结
代码规范
Asize6 小时前
ESLint 到底在帮我们守住什么?从规则配置到工程实践
代码规范·eslint
嘟嘟07176 小时前
ESLint 入门:从 npm run lint 到 --fix 与规则级别一次讲清
javascript·代码规范·eslint
不好听6137 小时前
ESLint 从零到落地:把"代码规范"变成机器的强制检查
代码规范
烬羽7 小时前
彻底搞懂 'use client':Next.js 组件根本不是"只在浏览器渲染"
全栈·next.js·前端工程化
烬羽7 小时前
彻底搞懂 App Router 约定式路由:文件放对位置,路由就自动生成了
react.js·next.js·前端工程化
用户9385156350719 小时前
ESLint 代码规范完全指南——从 AST 原理到 flat config 逐行解析
javascript·后端·代码规范
梦梦代码精2 天前
连锁品牌数字化:从门店扩张到用户资产运营的技术底座
大数据·人工智能·低代码·docker·开源·代码规范
嘟嘟07172 天前
用单例模式管理弹窗:从一段原生 JS 理解 Singlet
前端·javascript·代码规范