ESLint

eslint-plugin-textjs: JSX seguro para la consola, glifos del ancho de una celda, temporizadores sincronizados con los fotogramas y comprobaciones de App Router.

eslint-plugin-textjs detecta errores de Text.js en el editor: etiquetas DOM en componentes de consola, cajas dentro de texto, glifos que Menlo no tiene, llamadas sueltas a console.log y exportaciones de App Router que se ignoran en silencio.

Configuración inicial

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);

El plugin admite ESLint 9 y 10 con configuración plana. Las reglas son sintácticas y no necesitan información de tipos ni parserOptions.project. Funciona cualquier analizador que entienda TypeScript y JSX.

Configuraciones

ConfiguraciónQué activa
textjs.configs.recommendedTodas las reglas de corrección como errores; las reglas de consola, temporizadores y cadencia de fotogramas como advertencias
textjs.configs.strictTodo como error, además de prefer-use-keymap como advertencia

También puedes elegir las reglas individualmente:

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" }],    },  },];

Archivos de consola

La mayoría de las reglas solo se aplican a archivos que se renderizan en la consola: archivos que importan un componente o hook (Box, Text, useInput, …) de @textjs/app o @textjs/react. Los archivos que importan react-dom o @textjs/react/dom son código de página y no se modifican, por lo que un sitio web puede combinar páginas DOM y apps de consola en un mismo proyecto. Importar una utilidad como tokenize o parseMarkdown no convierte un archivo en un archivo de consola.

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"      },    },  },];

Las reglas de App Router se basan en el nombre del archivo (page.tsx, layout.tsx, route.ts, …) dentro de cualquier directorio app/, incluido src/app/. Si el tuyo tiene otro nombre, pasa { appDir: "routes" } a valid-route-module, valid-metadata y route-handler-exports.

Reglas

ReglaRecomendadaCorrección
no-dom-elementserrorsugerencia
no-box-in-texterror
no-unsupported-glyphserrorcorrección automática, sugerencia
no-console-in-componentsadvertenciasugerencia
prefer-use-intervaladvertenciasugerencia
no-sub-frame-timersadvertencia
prefer-use-keymapsolo en modo estricto
valid-route-moduleerror
valid-metadataerror
route-handler-exportserrorsugerencia

no-dom-elements

<div>, <span>, los elementos personalizados y otras etiquetas DOM no existen en la consola. El reconciliador solo reconoce sus propios nodos anfitriones y lanza TX003 ante cualquier otro.

<div className="flex-col">       // ✗<Box className="flex-col">       // ✓<strong>Deployed</strong>        // ✗<Text bold>Deployed</Text>       // ✓

La sugerencia sustituye la etiqueta por el componente más cercano (div → Box, span/p → Text, strong → Text bold, a → Link, input → Input, hr → Divider, …) y añade la importación.

no-box-in-text

<Text> es una secuencia de spans con estilo; no puede contener una caja. Anidar <Box> o cualquier componente cuya raíz sea una caja (<ScrollBox>, <Input>, <Code>, <ProgressBar>, <Divider>, …) lanza TX004 durante el renderizado. Esta regla lo detecta en el editor, incluso dentro de &&, ternarios, fragmentos y .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 distribuye cada carácter en una cuadrícula de celdas fijas. Los glifos que Menlo, la fuente de la consola de DevTools en macOS, no tiene se renderizan con una fuente alternativa más ancha que una celda, por lo que todas las celdas posteriores se desplazan.

  • Braille (U+2800–U+28FF, los fotogramas clásicos del indicador de carga de terminal): usa caracteres de bloque o cuadrante (▘▝▗▖), <Spinner variant="dots"> o marker="quadrant" en los gráficos. Configura { allowBraille: true } para apps que solo se ejecuten con text run en una terminal.
  • Símbolos y flechas diversos (U+2B00–U+2BFF, como el cuadrado pequeño U+2B1D): las sugerencias ofrecen el equivalente en Menlo (·, ●, ←).
  • Presentación como emoji en símbolos de una celda (▶ o ↩ seguidos de U+FE0F): el selector de variación los convierte en emojis de dos celdas mientras la cuadrícula reserva una. La corrección automática lo elimina.

Los emojis anchos (🚀, ✅) funcionan bien: Text.js los mide como dos celdas.

no-console-in-components

Cada fotograma es un console.log. Tus propios registros quedan entre fotogramas, el siguiente los desplaza fuera de la vista y quedan ocultos bajo la vista anclada al final. console.clear() borra la propia app.

"textjs/no-console-in-components": ["warn", { allow: ["error"], logger: "log" }]

Muestra mensajes en la interfaz con useToast() o un <ScrollBox> de registros, o inspecciona el estado en el panel de desarrollo [▼] (Ctrl+`). Con logger, la sugerencia reescribe console.warn(…) como log.warn(…). Se omiten los archivos de pruebas y los renderizados de @textjs/app/testing.

prefer-use-interval

Un setInterval directo en un componente necesita un efecto, una limpieza y una referencia para evitar un estado obsoleto. useInterval se encarga de las tres cosas, y null lo detiene.

useEffect(() => {  if (paused) return;  const id = setInterval(tick, 1000);  return () => clearInterval(id);}, [paused]);useInterval(tick, paused ? null : 1000); // the suggestion

Para animaciones, useAnimationFrame sigue la cadencia de fotogramas del renderizador y se detiene mientras la salida está en pausa. Consulta Hooks de utilidad.

no-sub-frame-timers

Text.js emite como máximo un fotograma cada 66 ms (unos 15 fps). Un intervalo de 16 ms renderiza cuatro veces por cada fotograma emitido; los fotogramas adicionales se agrupan y solo consumen CPU. Detecta retardos de setInterval/useInterval inferiores a minInterval (por defecto 66) y useAnimationFrame con fps superior a 15.

prefer-use-keymap

Comparar e.key a mano funciona, pero los atajos son invisibles. useKeymap coloca asignaciones etiquetadas en <KeyHelp>, <KeyHints> (asignaciones con hint: true) y la paleta de comandos, gestiona secuencias como g g y detiene la propagación de las teclas coincidentes. Detecta manejadores de useInput que comparan dos o más literales de tecla (minKeys) y omite los manejadores que leen e.input para la escritura.

useKeymap({  "up, k": { label: "Increment", run: inc, hint: true },  "down, j": { label: "Decrement", run: dec, hint: true },});

Consulta Atajos de teclado.

valid-route-module

  • Los archivos page, layout, loading, error y not-found deben exportar un componente por defecto (TX015).
  • export const dynamic debe ser "auto", "force-static" o "force-dynamic" (TX031). Text.js no admite "error" de Next.js. Se lee de páginas, layouts y manejadores de ruta. En páginas y layouts, escríbelo como un literal de cadena simple: text build lo lee del código fuente sin ejecutar el módulo.

valid-metadata

Metadata se combina a partir de los layouts y después de la página (o not-found). La regla detecta lo que el enrutador ignoraría en silencio:

  • metadata o generateMetadata en archivos loading o error
  • ambas exportaciones en un archivo
  • async generateMetadata: se llama de forma síncrona, por lo que una Promise nunca establece el título
  • un title que no sea una cadena ni { default, template?, absolute? }, o una clave mal escrita
  • title.template sin %s, o en una página, donde no tiene efecto
  • una description que no sea una cadena

route-handler-exports

Los archivos route.ts solo responden a los métodos HTTP que exportan: GET, HEAD, POST, PUT, PATCH, DELETE y OPTIONS. Nunca se llama a get en minúsculas ni a Post (las solicitudes reciben 405); la sugerencia cambia el nombre. Se notifican las exportaciones predeterminadas, los manejadores que no son funciones y otras exportaciones, salvo dynamic y revalidate de la configuración del segmento de ruta. Mueve el código compartido a lib/, o enumera los nombres adicionales con { allow: [...] }.

app/api/health/route.tsts
export function get() { … }   // ✗ never calledexport function GET() { … }   // ✓
Editar esta página en GitHub