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-eslint
eslint.config.jsjs
import { 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 как предупреждение

Или выберите правила по отдельности:

eslint.config.jsjs
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 не делает файл консольным.

eslint.config.jsjs
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 components

no-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: [...] }.

app/api/health/route.tsts
export function get() { … }   // ✗ never calledexport function GET() { … }   // ✓
Изменить эту страницу на GitHub