Больше свободы агентам — жёстче проверки: линтинг React 19 на ESLint 10
- понедельник, 24 августа 2026 г. в 00:00:11

Чем дальше, тем больше доверяю ИИ-агентам, наверное потому что я пока жестко не влетел. Более того, процесс идёт постепенно - тут разрешил машину в облаке запустить когда задача XXX, не спрашивая у меня разрешения на каждый чих, здесь ещё что-то.
Это очень сильно ускоряет работу, хотя и влететь можно сильно и больно. Пока меня это обошло стороной, поэтому в разрезе агентов и их креатива больше всего прилетает о того что они пишут больше кода чем надо, делают мелкие опечатки и ошибки и для борьбы, или хотя бы её видимости во всех проектах натыканы guardrails - механические детерминированные правила, которые запускаются при каждом commit, да и каждый раз когда агенты рапортуют: “Задача выполнена.”
К таким проверкам относятся:
tests
pre-commit hooks
linters
formatters
Такие проверки превращают требования в однозначный результат: прошло или не прошло. Небольшие коммиты сужают область поиска, когда что-то ломается.
Есть ещё менее детерминированные, но не менее важные: SKILLs, особенно un-slop - но это отдельная история, на сейчас skills эволюционируют быстрее кода, и тут рано пока делать какие-то выводы.
Линтинг — один из таких защитных механизмов.
И эта история началась, когда привычный React-линтинг сломался при переходе на ESLint 10.
Я поддерживаю несколько React-приложений. Когда я попытался перевести их на ESLint 10, миграция остановилась на eslint-plugin-react.
В ESLint 10 удалили устаревшие API контекста правил. Текущая версия eslint-plugin-react@7.37.5 заявляет поддержку только до ESLint 9 включительно, а под ESLint 10 падает с ошибкой:
TypeError: contextOrFilename.getFilename is not a function
Issue на эту ему открыли 7 февраля 2026 года. На 23 августа она оставалась открытой уже больше шести месяцев. PR с исправлением, открытый 30 июля, тоже оставался открытым.
Хотелось сохранить детерминированную проверку React и перейти на ESLint 10, поэтому выпустил @ternaus/eslint-plugin-react — самостоятельно поддерживаемое продолжение плагина для React 19 и ESLint 10.
У пакета намеренно узкая матрица поддержки (плясал от своих задач):
Инструмент | Поддерживаемая версия |
|---|---|
React | 19+ |
ESLint | 10 |
Biome | 2.5.8+ |
Node.js | 22.13, 24 или 26 |
Конфигурация ESLint | Плоская конфигурация |
React 18, ESLint 9 и .eslintrc* не поддерживаются.
В моих проектах форматированием и основной частью линтинга занимается Biome. Он проверяет JavaScript, TypeScript, JSX, DOM и большую часть правил React.
ESLint остаётся для проверок, которых нет в Biome: правил фреймворков и нескольких проверок поведения React 19.
Эта конфигурация выросла из реальных проектов на Next.js и React: Albumentations.ai, sportscategory.info и my-roots.me.
Next.js 16
React 19
TypeScript
ESLint 10 с плоской конфигурацией
Biome с набором all
Yarn 4
Node.js 22, 24 и 26
Biome активно развивается и старается быть и linter и formatter, поэтому когда он и Eslint тестируют одно и то же поведение, дублирующая проверка Eslint не нужна. Для других Eslint плагинов я их выключаю в конфиге, а тут я их просто вырезал - меньше кода => меньше багов и прочего головняка.
Взял за основу исходный репозиторий и сохранил его историю Git, лицензию MIT и сведения об авторах. Дальше проект развивается независимо.
На момент ветвления плагин экспортировал 104 модуля с правилами. Набор all включал 102 активных правила, ещё два были помечены устаревшими. В recommended находилось 22 правила, но react/no-unsafe было явно выключено, поэтому фактически работало 21.
Механическая проверка полезна, только когда понятно, какую проблему она ловит и какой инструмент за неё отвечает. Переносить весь этот объём не имело смысла. Я поставил более узкую задачу: оставить проверки корректности React 19, которые дополняют Biome.
Я разделил правила по назначению:
Группа | Решение |
|---|---|
Biome уже сообщает о той же проблеме | Удалить из плагина |
Форматирование, именование, структура файлов или политика команды | Оставить Biome или настройкам проекта |
Эвристики, которым нужны данные обо всём проекте или информация о типах | Удалить, чтобы не выдавать ненадёжные предупреждения |
React 18, классическая конфигурация ESLint, обходные пути для парсеров или устаревшие API React | Исключить из контракта React 19 |
Корректность React 19 или полезное React-специфичное предупреждение о производительности | Оставить или реализовать |
В первую группу попали ровно 20 правил, среди них jsx-key, no-danger, no-unknown-property и self-closing-comp. Полное сопоставление с Biome записано в описании поддержки правил исходного плагина.
Правила jsx-sort-props, function-component-definition и prefer-stateless-function задают стиль или политику команды, а не проверяют корректность React 19. no-unused-prop-types и no-unused-state пытаются ответить на вопросы обо всём проекте с помощью локального анализа AST, поэтому дают ненадёжный результат. Остальные исключённые правила обслуживали устаревшие конфигурации и версии за пределами матрицы поддержки.
После отбора осталось четыре исходных идентификатора правил: jsx-no-constructed-context-values, no-deprecated, no-direct-mutation-state и no-invalid-html-attribute.
Среди предложений, относящихся к переходу на React 19, я рассмотрел три правила, которые так и не были приняты в исходный плагин:
Сначала я реализовал все три. Затем удалил no-render-return-undefined: React 19 разрешает возвращать undefined, поэтому запрет выражал бы соглашение команды, замаскированное под требование фреймворка. Два других предложения стали правилами no-function-default-props и prefer-use-state-lazy-initialization. Первое находит механизм, который React 19 игнорирует. Второе предупреждает о лишних вычислениях при каждом рендере.
Ещё пять правил я добавил или сузил до конкретного поведения React 19: controlled-form-requires-handler, jsx-no-key-after-spread, no-implicit-ref-callback-return, no-misspelled-lifecycle-methods и no-prop-types.
В версии 8.0.0 осталось 11 правил. Все они входят в recommended: девять проверок корректности имеют уровень error, две проверки производительности — warn. Отдельного набора all нет: он либо полностью повторял бы recommended, либо отличался бы только уровнем предупреждений.
К версии 8.0.0-rc.3 проходили все модульные тесты и проверки пакета. Затем я подключил плагин к Rooted и Albumentations.ai — и нашёл проблемы, которых не было в тестах.
Первый класс ошибок был связан с метаданными HTML-атрибутов. Правило no-invalid-html-attribute отклоняло корректные атрибуты: alt, accept, name, loading, form, а также value у <select>, <option> и <textarea>. Исправления пришлось выпускать в три этапа: задача #21 и PR #22, затем задача #25 и PR #26, и наконец задача #29 и PR #31. В финальной реализации HTML-атрибуты из стандарта WHATWG и свойства React DOM стали двумя отдельными слоями. Один источник метаданных не описывает оба контракта полностью.
Вторую ошибку обнаружил Next.js. eslint-config-next@16 собирает плоскую конфигурацию, но читает правила из поля старой формы — react.configs.recommended.rules. Мой пакет экспортировал только react.configs.flat.recommended, поэтому конфигурация падала ещё до проверки файлов. В задаче #24 и PR #27 я добавил поле configs.recommended.rules, которого ждал Next.js, не возвращая поддержку .eslintrc. Также пришлось описать resolutions для Yarn: Next.js импортирует плагин как eslint-plugin-react, без префикса @ternaus/.
Эти интеграции привели к выпускам RC4, RC5 и RC6. Они изменили и стратегию тестирования. Финальная версия проверяет собранный npm-архив с помощью publint, загружает его из тестовых проектов на ESM, CommonJS и TypeScript и проверяет форму конфигурации, которую использует Next.js. CI запускается на Node.js 22.13, 24 и 26.
В результате пакет использует нативный ESM, плоскую конфигурацию ESLint и привычное пространство имён правил react/*.
В примерах используется Yarn 4.
Установите Biome, ESLint 10 и плагин:
yarn add --dev @biomejs/biome@'>=2.5.8' eslint@^10 @ternaus/eslint-plugin-react@^8.0.0
Включите полный стабильный набор правил Biome и домен React:
{ "linter": { "domains": { "react": "all" }, "rules": { "preset": "all" } } }
Затем добавьте оставшиеся React-проверки в eslint.config.js:
import react from '@ternaus/eslint-plugin-react'; export default [ { files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'], ...react.configs.flat.recommended, }, ];
Если шаблон файлов включает TypeScript, парсер с его поддержкой нужно подключить выше в плоской конфигурации.
Запускайте оба инструмента:
yarn biome check . yarn eslint .
Идентификаторы правил остаются в пространстве имён react:
{ rules: { 'react/no-deprecated': 'error', 'react/no-implicit-ref-callback-return': 'error', }, }
eslint-config-next импортирует пакет как eslint-plugin-react, без префикса @ternaus/. В Yarn эту зависимость нужно направить на поддерживаемый пакет:
{ "devDependencies": { "@ternaus/eslint-plugin-react": "8.0.0" }, "resolutions": { "eslint-plugin-react": "npm:@ternaus/eslint-plugin-react@8.0.0" } }
Версии в обоих полях должны совпадать. Тогда eslint-config-next не установит рядом исходный плагин, который поддерживает только ESLint 9.
Next.js регистрирует пакет под именем react, поэтому существующие идентификаторы react/* продолжат работать.
Этот пакет не содержит все правила исходного eslint-plugin-react.
Если конфигурация зависит от react/jsx-sort-props, react/display-name, react/prop-types или других отсутствующих правил, перед миграцией проверьте полный каталог.
Для React Native отдельного контракта совместимости тоже нет. DOM-правила анализируют только однозначно распознанные HTML-элементы, записанные в нижнем регистре.
В моих проектах свобода агентов растёт вместе с числом контрактов, которые можно запустить как обычную команду. Агент сам выполняет те же хуки pre-commit, тесты, правила линтера и проверки пакета, которые позже запустит CI.
Решения об архитектуре, компромиссах и исключениях остаются за человеком. Одиннадцать правил @ternaus/eslint-plugin-react отсекают предсказуемые ошибки React 19 до ручной проверки. Правила стиля и устаревшие слои совместимости остались за границами пакета.
Если ваша конфигурация совпадает с матрицей поддержки, начните с recommended, запустите Biome и ESLint на реальном проекте и проверьте каталог удалённых правил до миграции. Именно интеграция в настоящие приложения нашла проблемы, которых не было в модульных тестах.