lenec ru

← все посты

ESLint flat config: миграция с .eslintrc

11K

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-паттернами. Теперь это базовый механизм.

Что делает миграция

Простой процесс:

  1. Создать eslint.config.js в корне, скопировать туда все правила из старого .eslintrc.
  2. Перенести содержимое .eslintignore в первый блок с ignores.
  3. Удалить .eslintrc и .eslintignore.
  4. Прогнать 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. Конфиг сокращается, и читать его становится проще.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.