ESLint flat config: миграция с .eslintrc
ESLint flat config (он же eslint.config.js) — формат, который заменил .eslintrc в качестве дефолтного. С версии 9 старый формат отключён по умолчанию. Я мигрировал три проекта, и хотя задача звучит страшно, на практике она занимает день-два аккуратной работы. Расскажу, что делать.
Зачем меняли формат
Старый .eslintrc поддерживал каскадную систему: ESLint поднимался по дереву директорий и собирал конфиги отовсюду. Это было гибко, но медленно и непредсказуемо: понять, какие правила в итоге применяются к файлу, было сложно.
Flat config — один файл в корне проекта, в котором явно описаны все правила. Никакого волшебства, никаких неявных слияний. Проще понимать, проще дебажить.
Минимальный flat config
Создай eslint.config.js в корне проекта:
import js from '@eslint/js';
import globals from 'globals';
export default [
js.configs.recommended,
{
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
globals: { ...globals.browser, ...globals.node },
},
rules: {
'no-unused-vars': 'warn',
'no-console': 'warn',
},
},
];Это полный конфиг для базового JS-проекта. ESLint 9 ожидает массив объектов, каждый из которых описывает «слой» правил.
Структура объекта конфига
Каждый элемент массива — объект с полями:
files— паттерны файлов, к которым применяется этот блок.ignores— паттерны, которые исключаются.languageOptions— глобалы, parser, ecmaVersion.plugins— карта плагинов, доступных по имени.rules— правила.linterOptions— настройки линтера (warning thresholds и т.п.).
Применение каскадное: каждый следующий блок может переопределить или добавить к предыдущим.
TypeScript
Главный пакет — typescript-eslint. В flat-конфиге используется через хелпер:
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
},
},
);Хелпер tseslint.config делает обычную типизацию массива, чтобы редактор подсказывал доступные правила. Без него работать тоже можно, но автокомплит ухудшается.
React
Плагин react-hooks для flat-конфига:
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
export default [
// ... другие блоки
{
files: ['**/*.{jsx,tsx}'],
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': 'warn',
},
},
];Не все плагины успели полноценно поддержать flat-конфиг. Если плагин не имеет flat/-конфигурации, придётся импортировать его вручную и подключить так, как показано выше.
Игноры
В .eslintrc игноры лежали в .eslintignore. В flat-конфиге — в первом объекте без files:
export default [
{ ignores: ['dist/', 'node_modules/', '*.config.js'] },
// ... другие блоки
];Файл .eslintignore в новой системе не работает. Перенос делается за пять минут.
Условные блоки
Один из удобных моментов flat-конфига: разные блоки для разных типов файлов в одном месте.
export default [
// общие правила
{
rules: { 'no-console': 'warn' },
},
// тесты
{
files: ['**/*.test.ts', '**/*.spec.ts'],
languageOptions: { globals: { ...globals.vitest } },
rules: { '@typescript-eslint/no-explicit-any': 'off' },
},
// конфиги
{
files: ['*.config.js', '*.config.ts'],
rules: { 'no-undef': 'off' },
},
];Раньше для этого нужны были overrides с files-паттернами. Теперь это базовый механизм.
Что делает миграция
Простой процесс:
- Создать
eslint.config.jsв корне, скопировать туда все правила из старого.eslintrc. - Перенести содержимое
.eslintignoreв первый блок сignores. - Удалить
.eslintrcи.eslintignore. - Прогнать
eslint .и убедиться, что нет регрессий.
На больших конфигах с десятками правил и плагинов руками копировать долго. Я использовал утилиту @eslint/migrate-config:
npx @eslint/migrate-config .eslintrc.jsonОна генерирует базовый eslint.config.js. Потом я прохожусь и подчищаю — иногда автоматическая миграция оставляет странные импорты.
Грабли, которые я ловил
Плагины без поддержки flat
Часть плагинов в 2025 ещё не обновили API под flat. Они работают через утилиту FlatCompat:
import { FlatCompat } from '@eslint/eslintrc';
import path from 'node:path';
const compat = new FlatCompat({
baseDirectory: path.dirname(new URL(import.meta.url).pathname),
});
export default [
...compat.config({
extends: ['plugin:legacy-plugin/recommended'],
}),
// ... остальные блоки
];В 2026 году большинство популярных плагинов уже обновлено, но если у тебя в extends есть что-то экзотическое — этот путь.
Расширение через extends
В flat нет extends. Если ты привык к шаблонам (airbnb, standard, prettier), их теперь нужно импортировать как массивы и спрэдить:
import airbnb from 'eslint-config-airbnb';
export default [
...airbnb,
{
rules: { /* твои переопределения */ },
},
];Не все шаблоны поддерживают новый формат. Многие пока через FlatCompat.
VS Code и редакторы
Обнови расширение ESLint до 3.0+. Старая версия не понимала flat-конфиг и тихо игнорировала его. Если тебе кажется, что «ESLint не работает» — первым делом обнови расширение.
Глобалы для разных сред
Раньше env: { browser: true, node: true } закрывал тему. В flat нужно явно подключать пакет globals:
import globals from 'globals';
// в блоке
languageOptions: {
globals: { ...globals.browser, ...globals.node },
}Стоит ли мигрировать сейчас
Да. ESLint 9+ только flat. Если ты ещё на 8.x с .eslintrc — обновление через год-два станет неизбежным, и чем дольше копится legacy, тем дороже миграция. Лучше сделать сейчас, пока команда помнит контекст.
Мой опыт: репо из 12 пакетов с общим конфигом мигрируется за день. Большой проект с разными правилами на разных директориях — два-три дня. Это не катастрофа, и после миграции конфиг становится ощутимо чище.
Что копать дальше
После миграции пройдись по правилам ещё раз. Часть из них была актуальна для ES5/CommonJS и больше не нужна. Я после миграции выкинул около 15 правил, которые автоматически закрывались TypeScript. Конфиг сокращается, и читать его становится проще.