ESLint
eslint-plugin-textjs: безопасный для консоли JSX, глифы шириной в ячейку, таймеры в ритме кадров и проверки App Router.
eslint-plugin-textjs находит ошибки Text.js в редакторе: DOM-теги в консольных компонентах, блоки внутри текста, глифы, которых нет в Menlo, случайные вызовы console.log и экспорты App Router, которые молча игнорируются.
Настройка
bun add -d eslint eslint-plugin-textjs typescript-eslintimport { defineConfig } from "eslint/config";import tseslint from "typescript-eslint";import textjs from "eslint-plugin-textjs";export default defineConfig(tseslint.configs.recommended, textjs.configs.recommended);Плагин поддерживает ESLint 9 и 10 с плоской конфигурацией. Правила проверяют синтаксис и не требуют информации о типах или parserOptions.project. Подойдёт любой парсер, который понимает TypeScript и JSX.
Конфигурации
| Конфигурация | Что она включает |
|---|---|
textjs.configs.recommended | Все правила корректности как ошибки; правила консоли, таймеров и частоты кадров как предупреждения |
textjs.configs.strict | Всё как ошибки, плюс prefer-use-keymap как предупреждение |
Или выберите правила по отдельности:
import textjs from "eslint-plugin-textjs";export default [ { plugins: { textjs }, rules: { "textjs/no-box-in-text": "error", "textjs/no-console-in-components": ["warn", { allow: ["error"], logger: "log" }], }, },];Консольные файлы
Большинство правил применяется только к файлам, которые рендерят в консоль: файлам, импортирующим компонент или хук (Box, Text, useInput, …) из @textjs/app или @textjs/react. Файлы, импортирующие react-dom или @textjs/react/dom, считаются кодом страницы и не проверяются, поэтому сайт может совмещать DOM-страницы и консольные приложения в одном проекте. Импорт утилиты вроде tokenize или parseMarkdown не делает файл консольным.
import textjs from "eslint-plugin-textjs";export default [ textjs.configs.recommended, { settings: { textjs: { packages: ["@acme/console-ui"], // also re-exports Text.js components consoleFiles: "imports", // or "all" }, }, },];Правила App Router ориентируются на имя файла (page.tsx, layout.tsx, route.ts, …) внутри любого каталога app/, включая src/app/. Если ваш каталог называется иначе, передайте { appDir: "routes" } в valid-route-module, valid-metadata и route-handler-exports.
Правила
| Правило | Рекомендуемая конфигурация | Исправление |
|---|---|---|
no-dom-elements | ошибка | предложение |
no-box-in-text | ошибка | |
no-unsupported-glyphs | ошибка | автоисправление, предложение |
no-console-in-components | предупреждение | предложение |
prefer-use-interval | предупреждение | предложение |
no-sub-frame-timers | предупреждение | |
prefer-use-keymap | только в strict | |
valid-route-module | ошибка | |
valid-metadata | ошибка | |
route-handler-exports | ошибка | предложение |
no-dom-elements
<div>, <span>, пользовательских элементов и других DOM-тегов в консоли нет. Реконсайлер знает только собственные узлы среды и выдаёт TX003 для всего остального.
<div className="flex-col"> // ✗<Box className="flex-col"> // ✓<strong>Deployed</strong> // ✗<Text bold>Deployed</Text> // ✓Предложение заменяет тег ближайшим компонентом (div → Box, span/p → Text, strong → Text bold, a → Link, input → Input, hr → Divider, …) и добавляет импорт.
no-box-in-text
<Text> — последовательность стилизованных span; он не может содержать блок. Вложенный <Box> или любой компонент с блоком в корне (<ScrollBox>, <Input>, <Code>, <ProgressBar>, <Divider>, …) вызывает TX004 при рендеринге. Это правило находит такую ошибку ещё в редакторе, в том числе внутри &&, тернарных выражений, фрагментов и .map().
<Text>Status <Box /></Text> // ✗<Box className="flex-row gap-1"><Text>Status</Text><Box /></Box> // ✓<Text>Press <Kbd>⌘K</Kbd> <Spinner /></Text> // ✓ inline componentsno-unsupported-glyphs
Text.js размещает каждый символ в фиксированной сетке ячеек. Глифы, которых нет в Menlo, шрифте консоли DevTools на macOS, отображаются резервным шрифтом шире ячейки, поэтому все ячейки после них смещаются.
- Брайль (U+2800–U+28FF, классические кадры терминального спиннера): используйте блочные или квадрантные символы (
▘▝▗▖),<Spinner variant="dots">илиmarker="quadrant"на графиках. Задайте{ allowBraille: true }для приложений, которые запускаются только черезtext runв терминале. - Разные символы и стрелки (U+2B00–U+2BFF, например маленький квадрат U+2B1D): предложения предлагают эквивалент из Menlo (
·,●,←). - Отображение одноячеечных символов как эмодзи (
▶или↩, за которыми следует U+FE0F): селектор варианта превращает их в эмодзи шириной в две ячейки, хотя сетка выделяет одну. Автоисправление удаляет его.
Широкие эмодзи (🚀, ✅) допустимы: Text.js измеряет их как две ячейки.
no-console-in-components
Каждый кадр — один console.log. Ваши собственные логи попадают между кадрами, прокручиваются следующим кадром и скрываются из-за привязки прокрутки к низу. console.clear() стирает само приложение.
"textjs/no-console-in-components": ["warn", { allow: ["error"], logger: "log" }]Показывайте сообщения в интерфейсе через useToast() или <ScrollBox> с журналом либо проверяйте состояние в панели разработчика [▼] (Ctrl+`). При заданном logger предложение заменяет console.warn(…) на log.warn(…). Тестовые файлы и рендеринг через @textjs/app/testing пропускаются.
prefer-use-interval
Обычный setInterval в компоненте требует эффекта, очистки и ref, чтобы избежать устаревшего состояния. useInterval берёт на себя все три задачи, а null останавливает его.
useEffect(() => { if (paused) return; const id = setInterval(tick, 1000); return () => clearInterval(id);}, [paused]);useInterval(tick, paused ? null : 1000); // the suggestionДля анимации useAnimationFrame следует частоте кадров рендерера и останавливается, пока вывод приостановлен. См. Вспомогательные хуки.
no-sub-frame-timers
Text.js выводит не более одного кадра каждые 66 мс (около 15 кадров/с). Интервал в 16 мс запускает рендеринг четыре раза на один выведенный кадр; лишние кадры объединяются и лишь расходуют ресурсы процессора. Правило сообщает о задержках setInterval/useInterval ниже minInterval (по умолчанию 66) и о useAnimationFrame с fps выше 15.
prefer-use-keymap
Сравнение e.key вручную работает, но сочетания клавиш остаются невидимыми. useKeymap помещает привязки с подписями в <KeyHelp>, <KeyHints> (привязки с hint: true) и палитру команд, обрабатывает последовательности вроде g g и останавливает распространение событий для совпавших клавиш. Правило сообщает об обработчиках useInput, которые сравнивают два или более литерала клавиш (minKeys), и пропускает обработчики, читающие e.input для ввода текста.
useKeymap({ "up, k": { label: "Increment", run: inc, hint: true }, "down, j": { label: "Decrement", run: dec, hint: true },});См. Сочетания клавиш.
valid-route-module
- Файлы
page,layout,loading,errorиnot-foundдолжны экспортировать компонент по умолчанию (TX015). - Значение
export const dynamicдолжно быть"auto","force-static"или"force-dynamic"(TX031). Text.js не поддерживает"error"из Next.js. Оно считывается из страниц, макетов и обработчиков маршрутов. В страницах и макетах задавайте его обычным строковым литералом:text buildчитает его из исходного кода, не запуская модуль.
valid-metadata
Metadata объединяется из макетов, затем из страницы (или not-found). Правило сообщает о том, что маршрутизатор молча проигнорировал бы:
metadataилиgenerateMetadataв файлахloadingилиerror- оба экспорта в одном файле
async generateMetadata: функция вызывается синхронно, поэтому Promise никогда не задаёт заголовокtitle, который не является строкой или{ default, template?, absolute? }, либо ключ с опечаткойtitle.templateбез%sили на странице, где он не действуетdescription, который не является строкой
route-handler-exports
Файлы route.ts отвечают только на те HTTP-методы, которые экспортируют: GET, HEAD, POST, PUT, PATCH, DELETE и OPTIONS. Варианты get или Post с буквами в нижнем регистре никогда не вызываются (запросы получают 405); предложение переименовывает их. Правило сообщает об экспортах по умолчанию, обработчиках, которые не являются функциями, и других экспортах, кроме dynamic и revalidate из конфигурации сегмента маршрута. Перенесите общий код в lib/ или перечислите дополнительные имена через { allow: [...] }.
export function get() { … } // ✗ never calledexport function GET() { … } // ✓