Совместимость с существующими плагинами

Экосистема ESLint исторически строилась вокруг плагинов и shareable-конфигураций, распространяемых через npm. Совместимость между версиями линтера и сторонними расширениями зависит от нескольких слоёв: версии ядра ESLint, формата конфигурации, API плагинов и механизма резолва зависимостей. Любое изменение в одном из этих слоёв способно вызвать каскад несовместимостей, особенно при переходе между крупными версиями ESLint.


Архитектура плагинов и точки совместимости

Плагин ESLint представляет собой npm-пакет, экспортирующий набор правил, а также иногда дополнительные конфигурации и процессоры. Базовая структура плагина выглядит следующим образом:

  • rules — набор правил линтинга
  • configs — предустановленные конфигурации
  • processors — обработка нестандартных форматов файлов
  • environments (в старых плагинах) — описание глобальных переменных

Совместимость определяется тем, как плагин взаимодействует с:

  • публичным API ESLint (Rule API)
  • системой конфигурации
  • системой резолва модулей Node.js
  • внутренними структурами AST (обычно через Espree или кастомный парсер)

Версионная совместимость ESLint и плагинов

Ключевой фактор стабильности — соответствие peerDependencies в плагине. Большинство современных плагинов явно указывают диапазон поддерживаемых версий ESLint:

{
  "peerDependencies": {
    "eslint": ">=7.0.0 <9.0.0"
  }
}

Такой диапазон означает, что плагин не гарантирует корректную работу вне указанных версий ядра.

Основные категории несовместимости:

Разрыв API между мажорными версиями

При переходе между ESLint 7 → 8 и особенно 8 → 9 происходили изменения:

  • устаревание старых форматов конфигурации
  • изменения в резолве правил
  • изменение структуры контекста правил (context API)
  • усиление строгой типизации опций правил

Плагины, использующие внутренние или неофициальные свойства context, часто ломаются первыми.


Совместимость с ESLint 8

ESLint 8 стал промежуточной стабильной веткой, где большинство экосистемы обновилось:

  • большинство популярных плагинов (eslint-plugin-import, eslint-plugin-react, eslint-plugin-jsx-a11y) адаптировались
  • сохранялась поддержка .eslintrc.*
  • активно использовались legacy-конфигурации

Однако даже в этой версии возникали проблемы с:

  • плагинами, завязанными на deprecated APIs
  • кастомными процессорами
  • пакетами, использующими старый resolver Node.js

Переход к ESLint 9 и flat config

ESLint 9 усилил переход к flat configuration (eslint.config.js). Это изменило модель совместимости радикально.

Ключевые изменения:

  • отказ от .eslintrc.* как основного механизма (legacy режим теперь опционален)
  • изменение структуры конфигурации: массив объектов вместо иерархии extends
  • явная регистрация плагинов в конфиге
  • строгий контроль области видимости правил

Пример flat-конфигурации:

import js from "@eslint/js";
import react from "eslint-plugin-react";

export default [
  js.configs.recommended,
  {
    plugins: {
      react
    },
    rules: {
      "react/jsx-uses-react": "error"
    }
  }
];

Проблемы совместимости в этом переходе:

  • плагины, не экспортирующие ESM-совместимый интерфейс
  • конфиги, завязанные на extends
  • плагины, ожидающие автоматическую регистрацию через строковые имена

Формат экспорта плагинов и ESM/CJS несовместимости

Современная экосистема Node.js усилила разрыв между CommonJS и ESM. ESLint 9 ориентирован на ESM-first подход, что приводит к проблемам:

CommonJS плагины

module.exports = {
  rules: {
    "no-foo": require("./rules/no-foo")
  }
};

Такие плагины могут работать через interop, но:

  • нарушается tree-shaking
  • возможны проблемы с динамическим импортом
  • иногда требуется явное указание createRequire

ESM плагины

export const rules = {
  "no-foo": rule
};

ESM-формат требует корректной настройки type: module в package.json, иначе загрузка плагина ломается.


Совместимость shareable configs

Shareable-конфигурации (eslint-config-*) особенно чувствительны к изменениям ESLint.

Основные источники проблем:

Использование extends

Legacy-конфигурации завязаны на:

{
  "extends": ["plugin:react/recommended"]
}

В flat config это заменяется явным импортом, и старые конфиги становятся частично несовместимыми.


Peer-зависимости конфигураций

Shareable config часто включает плагины как peer dependencies:

{
  "peerDependencies": {
    "eslint": ">=8",
    "eslint-plugin-react": ">=7"
  }
}

Несовпадение версий приводит к:

  • отсутствию правил
  • падению резолва конфигурации
  • конфликтам типов опций

AST и совместимость правил

Правила ESLint зависят от структуры AST, формируемой парсером (например, Espree, Babel ESLint, TypeScript ESLint).

Совместимость нарушается при:

  • использовании разных парсеров для одного правила
  • ожидании нестандартных узлов AST
  • несовпадении версии ECMAScript

Пример проблемного сценария:

  • правило ожидает OptionalChainingExpression
  • старый парсер возвращает иной тип узла
  • правило падает или игнорирует выражение

eslint-plugin-* и namespace конфликтов

Плагины регистрируются через namespace:

{
  "plugins": ["react"]
}

И используются как:

react/jsx-uses-react

Проблемы возникают при:

  • совпадении имён правил между плагинами
  • неправильной регистрации namespace в flat config
  • ручной маппинг plugins вместо строкового имени

Flat config требует явного связывания:

plugins: {
  react: reactPlugin
}

Проблемы резолвинга зависимостей

ESLint использует Node resolution algorithm, но плагины могут быть:

  • локально установлены
  • hoisted (npm/yarn/pnpm workspaces)
  • установлены транзитивно

Типовые проблемы:

  • дублирование eslint в node_modules
  • конфликт версий плагинов
  • невозможность найти plugin при нестандартной структуре монорепозитория

Особенно чувствительны:

  • pnpm (из-за строгих symlink)
  • monorepo с workspace-hoisting
  • проекты с глобальной установкой eslint

Совместимость TypeScript-плагинов

@typescript-eslint является наиболее критичным примером зависимости от версии ESLint.

Причины сложности:

  • собственный parser (@typescript-eslint/parser)
  • собственный scope manager
  • дополнительные AST расширения

Совместимость определяется тройкой:

  • версия ESLint
  • версия TypeScript
  • версия @typescript-eslint/*

Несовпадение любой из этих частей приводит к:

  • некорректному анализу типов
  • отсутствию правил
  • ошибкам при парсинге JSX/TSX

Стратегии обеспечения совместимости

Явная фиксация версий

Использование фиксированных диапазонов:

{
  "eslint": "8.57.0",
  "eslint-plugin-react": "7.34.0"
}

уменьшает вероятность неожиданных разрывов API.


Использование совместимых конфигураций

Выбор конфигов, поддерживающих несколько поколений ESLint:

  • dual support (legacy + flat config)
  • условная загрузка конфигураций
  • отдельные entry points для ESLint 8 и 9

Изоляция плагинов

В монорепозиториях применяется стратегия:

  • локальные node_modules для каждого пакета
  • отключение hoisting ESLint
  • единая версия ESLint через workspace root

Адаптация кастомных правил

При разработке собственных плагинов учитываются:

  • только публичное API Rule Creator
  • отсутствие доступа к внутренним структурам ESLint
  • минимизация зависимости от конкретного AST

Эволюция экосистемы и долгосрочная совместимость

Экосистема ESLint постепенно движется к:

  • ESM-first архитектуре
  • flat config как единому стандарту
  • явной регистрации зависимостей
  • строгому контролю версий через peerDependencies

Это снижает хаос совместимости, но увеличивает стоимость миграций. Плагины, не обновлённые под новые стандарты, постепенно выпадают из экосистемы или требуют wrapper-адаптеров.

Совместимость становится не статическим свойством, а управляемым состоянием конфигурации проекта, зависящим от версии линтера, набора плагинов и выбранного формата конфигурации.