ESLint Flat Config 迁移指南:从 .eslintrc 到 eslint.config.js 的完整实践

2676 字
13 分钟
ESLint Flat Config 迁移指南:从 .eslintrc 到 eslint.config.js 的完整实践

ESLint Flat Config 迁移指南:从 .eslintrc 到 eslint.config.js 的完整实践#

ESLint v9 正式将 Flat Config 作为默认配置系统,旧的 .eslintrc.* 格式进入废弃期。这不只是换个文件名——Flat Config 重新设计了配置的组织方式,解决了旧系统中层叠合并、插件解析等长期痛点。本文将带你理解新系统的设计原理,并完成一次完整的迁移实践。

一、为什么需要 Flat Config#

1.1 旧配置系统的痛点#

痛点一:配置层叠地狱

旧系统支持多种配置文件格式和层叠:

project/
├── .eslintrc.json # 项目根配置
├── src/
│ ├── .eslintrc.yml # src 目录配置(覆盖根配置)
│ └── components/
│ └── .eslintrc.js # 再覆盖一层
├── package.json # eslintConfig 字段也是配置
└── node_modules/
└── some-config/
└── .eslintrc.json # 共享配置内部可能还有

ESLint 需要沿目录树向上搜索配置文件,然后合并。这导致:

  • 配置来源难以追踪
  • 合并规则不直观
  • root: true 用来截断搜索,经常被遗忘

痛点二:extends 的隐式解析

{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:vue/vue3-recommended"
]
}

"plugin:xxx/yyy" 是什么意思?ESLint 需要:

  1. 找到 eslint-plugin-xxx
  2. 读取它导出的 configs.yyy
  3. 合并到当前配置

这个隐式的解析过程让配置变得不透明。

痛点三:插件的字符串引用

{
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-unused-vars": "error"
}
}

"@typescript-eslint" 只是一个字符串,ESLint 需要通过 require() 去查找 @typescript-eslint/eslint-plugin。这在 monorepo 或 pnpm 严格模式下经常出问题。

1.2 Flat Config 的解决方案#

// eslint.config.js — 一切都是显式的 JavaScript
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
export default [
// 数组中的每个元素是一个配置对象
// 按顺序合并,后面的覆盖前面的
...tseslint.configs.recommended,
...pluginVue.configs['flat/recommended'],
{
rules: {
'no-console': 'warn',
},
},
];

核心改变:

  1. 单一配置文件:只有一个 eslint.config.js(或 .mjs/.cjs
  2. 显式导入:插件通过 import 引入,不再是字符串
  3. 扁平数组:配置是一个数组,按顺序合并
  4. 不再有 extends:直接展开共享配置

二、Flat Config 核心概念#

2.1 配置数组#

eslint.config.js
export default [
// 配置对象 1:全局设置
{
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
},
},
// 配置对象 2:只对 TypeScript 文件生效
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'no-undef': 'off', // TypeScript 处理这个
},
},
// 配置对象 3:忽略文件
{
ignores: ['dist/', 'node_modules/', '*.min.js'],
},
];

2.2 files 和 ignores#

filesignores 使用 glob 模式,类似 .gitignore

export default [
// 全局忽略(只有 ignores,没有其他属性)
{
ignores: [
'dist/**',
'node_modules/**',
'coverage/**',
'*.config.js', // 忽略所有配置文件
'!eslint.config.js', // 但不忽略 ESLint 配置本身
],
},
// 匹配特定文件
{
files: ['src/**/*.ts'],
rules: { /* TypeScript 规则 */ },
},
// 匹配测试文件
{
files: ['**/*.test.ts', '**/*.spec.ts', 'tests/**'],
rules: {
'no-console': 'off',
},
},
];

重要区别

  • 只有 ignores 的配置对象 = 全局忽略(替代 .eslintignore
  • files + ignores 的配置对象 = 在匹配的文件中排除某些

2.3 languageOptions#

替代旧的 envparserOptionsparserglobals

import globals from 'globals';
export default [
{
languageOptions: {
// 替代 parserOptions.ecmaVersion
ecmaVersion: 2024,
// 替代 parserOptions.sourceType
sourceType: 'module',
// 替代 env(使用 globals 包)
globals: {
...globals.browser,
...globals.es2024,
...globals.node,
// 自定义全局变量
myGlobal: 'readonly',
__DEV__: 'readonly',
},
// 替代 parser(直接传入解析器对象)
// parser: tsParser,
// 替代 parserOptions
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
},
];

2.4 plugins#

插件不再是字符串,而是直接导入的对象:

import tsPlugin from '@typescript-eslint/eslint-plugin';
import importPlugin from 'eslint-plugin-import-x';
export default [
{
plugins: {
// key 是命名空间前缀,value 是插件对象
'@typescript-eslint': tsPlugin,
'import-x': importPlugin,
},
rules: {
'@typescript-eslint/no-unused-vars': 'error',
'import-x/order': 'error',
},
},
];

三、迁移实战#

3.1 迁移前的旧配置#

假设你的项目有这样的旧配置:

.eslintrc.json
{
"root": true,
"env": {
"browser": true,
"es2024": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:vue/vue3-recommended",
"prettier"
],
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"no-console": "warn",
"no-debugger": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_"
}],
"vue/multi-word-component-names": "off",
"vue/no-v-html": "off"
},
"overrides": [
{
"files": ["*.test.ts", "*.spec.ts"],
"env": {
"jest": true
},
"rules": {
"no-console": "off"
}
}
]
}

3.2 迁移后的新配置#

eslint.config.js
import globals from 'globals';
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import eslintConfigPrettier from 'eslint-config-prettier';
export default tseslint.config(
// 全局忽略
{
ignores: [
'dist/**',
'node_modules/**',
'coverage/**',
'*.d.ts',
],
},
// 基础配置
{
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
globals: {
...globals.browser,
...globals.es2024,
...globals.node,
},
},
},
// ESLint 推荐规则
// eslint v9+ 导出为 flat config
{
rules: {
// eslint:recommended 规则已内置在 tseslint.configs.recommended 中
},
},
// TypeScript 推荐规则
...tseslint.configs.recommended,
// Vue 推荐规则
...pluginVue.configs['flat/recommended'],
// Vue 文件的解析器配置
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: tseslint.parser,
},
},
},
// 自定义规则
{
rules: {
'no-console': 'warn',
'no-debugger': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'off',
},
},
// 测试文件
{
files: ['**/*.test.ts', '**/*.spec.ts', 'tests/**'],
languageOptions: {
globals: {
...globals.jest,
},
},
rules: {
'no-console': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
// Prettier 兼容(必须放最后)
eslintConfigPrettier,
);

3.3 安装依赖#

Terminal window
# 移除旧依赖
pnpm remove eslint-plugin-vue @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier
# 安装新依赖
pnpm add -D eslint@^9 typescript-eslint eslint-plugin-vue eslint-config-prettier globals

3.4 清理旧文件#

Terminal window
# 删除旧配置文件
rm .eslintrc.json .eslintrc.js .eslintrc.yml .eslintrc
rm .eslintignore # 不再需要,用 ignores 替代

3.5 更新 package.json#

{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}
}

注意:ESLint v9 默认使用 Flat Config,不需要任何额外标志。

四、typescript-eslint 的 Flat Config 用法#

4.1 基础用法#

import tseslint from 'typescript-eslint';
export default tseslint.config(
// tseslint.config() 是一个辅助函数,提供类型提示
...tseslint.configs.recommended,
);

4.2 类型感知的规则#

如果你想启用需要类型信息的规则(更强大但更慢):

import tseslint from 'typescript-eslint';
export default tseslint.config(
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
// 启用类型感知
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
// 非 TS 文件禁用类型感知规则
{
files: ['**/*.js', '**/*.mjs'],
...tseslint.configs.disableTypeChecked,
},
);

4.3 严格模式#

// 从宽松到严格
...tseslint.configs.recommended, // 基础推荐
...tseslint.configs.strict, // 更严格
...tseslint.configs.strictTypeChecked, // 最严格 + 类型感知
// 风格相关
...tseslint.configs.stylistic, // 风格规则
...tseslint.configs.stylisticTypeChecked, // 风格 + 类型感知

五、常见插件迁移#

5.1 eslint-plugin-import → eslint-plugin-import-x#

原版 eslint-plugin-import 对 Flat Config 支持不佳,推荐使用 fork 版本 eslint-plugin-import-x

Terminal window
pnpm add -D eslint-plugin-import-x
import importX from 'eslint-plugin-import-x';
export default [
importX.flatConfigs.recommended,
importX.flatConfigs.typescript,
{
rules: {
'import-x/order': ['error', {
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
],
'newlines-between': 'always',
alphabetize: {
order: 'asc',
caseInsensitive: true,
},
}],
'import-x/no-duplicates': 'error',
'import-x/no-unresolved': 'off', // TypeScript 处理
},
},
];

5.2 eslint-plugin-vue#

Vue 插件已经原生支持 Flat Config:

import pluginVue from 'eslint-plugin-vue';
export default [
// 选择一个预设
...pluginVue.configs['flat/recommended'],
// 或
// ...pluginVue.configs['flat/essential'],
// ...pluginVue.configs['flat/strongly-recommended'],
{
files: ['**/*.vue'],
rules: {
'vue/block-order': ['error', {
order: ['script', 'template', 'style'],
}],
'vue/define-macros-order': ['error', {
order: ['defineProps', 'defineEmits'],
}],
'vue/no-unused-refs': 'error',
'vue/padding-line-between-blocks': 'error',
},
},
];

5.3 Prettier 集成#

import eslintConfigPrettier from 'eslint-config-prettier';
export default [
// ... 其他配置
// Prettier 必须放最后,关闭所有与格式化冲突的规则
eslintConfigPrettier,
];

六、自定义规则#

Flat Config 让自定义规则变得更简单:

6.1 内联自定义规则#

eslint.config.js
const noFooBarRule = {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow variable names "foo" and "bar"',
},
fixable: 'code',
schema: [],
messages: {
noFooBar: 'Variable name "{{name}}" is not allowed. Use meaningful names.',
},
},
create(context) {
return {
VariableDeclarator(node) {
if (
node.id.type === 'Identifier' &&
['foo', 'bar', 'baz'].includes(node.id.name)
) {
context.report({
node: node.id,
messageId: 'noFooBar',
data: { name: node.id.name },
});
}
},
};
},
};
export default [
{
plugins: {
custom: {
rules: {
'no-foo-bar': noFooBarRule,
},
},
},
rules: {
'custom/no-foo-bar': 'error',
},
},
];

6.2 完整的自定义插件#

plugins/eslint-plugin-my-team/index.js
import noMagicNumbers from './rules/no-magic-numbers.js';
import requireJsdoc from './rules/require-jsdoc.js';
import noTodoWithoutIssue from './rules/no-todo-without-issue.js';
const plugin = {
meta: {
name: 'eslint-plugin-my-team',
version: '1.0.0',
},
rules: {
'no-magic-numbers': noMagicNumbers,
'require-jsdoc': requireJsdoc,
'no-todo-without-issue': noTodoWithoutIssue,
},
configs: {},
};
// 自引用配置(Flat Config 风格)
plugin.configs.recommended = [
{
plugins: {
'my-team': plugin,
},
rules: {
'my-team/no-magic-numbers': 'warn',
'my-team/no-todo-without-issue': 'error',
},
},
];
export default plugin;
rules/no-todo-without-issue.js
export default {
meta: {
type: 'suggestion',
docs: {
description: 'Require TODO comments to include an issue link',
},
schema: [{
type: 'object',
properties: {
pattern: { type: 'string' },
},
additionalProperties: false,
}],
messages: {
missingIssue: 'TODO comment must include an issue link (e.g., TODO(#123))',
},
},
create(context) {
const pattern = context.options[0]?.pattern || '#\\d+';
const issueRegex = new RegExp(pattern);
return {
Program() {
const comments = context.sourceCode.getAllComments();
for (const comment of comments) {
const value = comment.value.trim();
if (/\bTODO\b/i.test(value) && !issueRegex.test(value)) {
context.report({
node: comment,
messageId: 'missingIssue',
});
}
}
},
};
},
};

使用自定义插件:

eslint.config.js
import myTeamPlugin from './plugins/eslint-plugin-my-team/index.js';
export default [
...myTeamPlugin.configs.recommended,
{
rules: {
'my-team/require-jsdoc': ['error', {
require: { FunctionDeclaration: true },
}],
},
},
];

七、高级配置技巧#

7.1 配置工厂函数#

封装可复用的配置:

configs/base.js
import globals from 'globals';
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import eslintConfigPrettier from 'eslint-config-prettier';
export function createConfig(options = {}) {
const {
vue = false,
typescript = true,
prettier = true,
testFramework = 'vitest',
} = options;
const configs = [
// 全局忽略
{
ignores: ['dist/**', 'node_modules/**', 'coverage/**'],
},
// 基础
{
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
globals: {
...globals.browser,
...globals.node,
},
},
},
];
// TypeScript
if (typescript) {
configs.push(...tseslint.configs.recommended);
}
// Vue
if (vue) {
configs.push(
...pluginVue.configs['flat/recommended'],
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: tseslint.parser,
},
},
},
);
}
// 测试文件
const testGlobals = testFramework === 'vitest'
? { describe: 'readonly', it: 'readonly', expect: 'readonly', vi: 'readonly', beforeEach: 'readonly', afterEach: 'readonly' }
: globals.jest;
configs.push({
files: ['**/*.test.*', '**/*.spec.*', 'tests/**'],
languageOptions: { globals: testGlobals },
rules: { 'no-console': 'off' },
});
// Prettier(必须最后)
if (prettier) {
configs.push(eslintConfigPrettier);
}
return configs;
}

使用:

eslint.config.js
import { createConfig } from './configs/base.js';
export default [
...createConfig({ vue: true, testFramework: 'vitest' }),
// 项目特定规则
{
rules: {
'no-console': 'warn',
},
},
];

7.2 条件配置#

import tseslint from 'typescript-eslint';
const isCI = process.env.CI === 'true';
export default tseslint.config(
...tseslint.configs.recommended,
// CI 环境更严格
...(isCI ? [{
rules: {
'no-console': 'error',
'no-debugger': 'error',
'@typescript-eslint/no-explicit-any': 'error',
},
}] : [{
rules: {
'no-console': 'warn',
'no-debugger': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
},
}]),
);

7.3 Monorepo 中的配置共享#

packages/eslint-config/index.js
import globals from 'globals';
import tseslint from 'typescript-eslint';
export const base = tseslint.config(
...tseslint.configs.recommended,
{
languageOptions: {
ecmaVersion: 2024,
globals: { ...globals.browser, ...globals.node },
},
rules: {
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
}],
},
},
);
// 各子包使用
// apps/web/eslint.config.js
import { base } from '@monorepo/eslint-config';
export default [
...base,
{
rules: {
// 项目特定规则
},
},
];

八、调试与排查#

8.1 配置检查器#

ESLint v9 提供了内置的配置检查器:

Terminal window
# 启动配置检查器 Web UI
npx eslint --inspect-config
# 或者
npx @eslint/config-inspector

这会打开一个浏览器界面,让你可视化地查看:

  • 每个配置对象的内容
  • 规则的最终值
  • 每个文件匹配了哪些配置

8.2 调试特定文件#

Terminal window
# 查看某个文件的最终配置
npx eslint --print-config src/App.vue
# 查看某个文件匹配了哪些规则
npx eslint --debug src/App.vue 2>&1 | grep "config"

8.3 常见迁移错误#

错误一:忘记 flat/ 前缀

// ❌ 旧格式
...pluginVue.configs['vue3-recommended'],
// ✅ Flat Config 格式
...pluginVue.configs['flat/recommended'],

错误二:插件格式错误

// ❌ 旧格式(字符串)
{ plugins: ['@typescript-eslint'] }
// ✅ Flat Config(对象)
import tsPlugin from '@typescript-eslint/eslint-plugin';
{ plugins: { '@typescript-eslint': tsPlugin } }
// ✅✅ 更推荐:使用 typescript-eslint 的便捷导出
import tseslint from 'typescript-eslint';
...tseslint.configs.recommended

错误三:globals 未安装

Terminal window
# globals 包提供浏览器、Node.js 等环境的全局变量定义
pnpm add -D globals

九、与编辑器集成#

9.1 VS Code#

确保安装了 ESLint 扩展(dbaeumer.vscode-eslint),v3+ 版本原生支持 Flat Config。

.vscode/settings.json
{
"eslint.useFlatConfig": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"javascript",
"typescript",
"vue"
]
}

9.2 与 lint-staged 集成#

lint-staged.config.js
export default {
'*.{js,ts,vue}': ['eslint --fix'],
'*.{json,md,yml}': ['prettier --write'],
};
package.json
{
"scripts": {
"prepare": "husky"
}
}
.husky/pre-commit
npx lint-staged

十、总结#

Flat Config 迁移清单:

  1. ✅ 升级 ESLint 到 v9+
  2. ✅ 安装 globals
  3. ✅ 创建 eslint.config.js(ESM 格式)
  4. ✅ 将 extends 改为直接 import 并展开
  5. ✅ 将 env 改为 languageOptions.globals
  6. ✅ 将 parser 改为 languageOptions.parser(直接传对象)
  7. ✅ 将 plugins 从字符串改为导入的对象
  8. ✅ 将 overrides 改为带 files 的配置对象
  9. ✅ 将 .eslintignore 内容移到全局 ignores
  10. ✅ 删除旧的 .eslintrc.*.eslintignore

Flat Config 的本质是让 ESLint 配置回归 JavaScript 的本质——一切都是显式的、可追踪的、可调试的。不再有魔法字符串、隐式解析和复杂的层叠规则。如果你还在用旧配置,现在是迁移的最佳时机。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

ESLint Flat Config 迁移指南:从 .eslintrc 到 eslint.config.js 的完整实践
https://boke.hackerdream.xyz/posts/eslint-flat-config/
作者
晴天
发布于
2026-01-11
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录