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-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);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ón | Qué activa |
|---|---|
textjs.configs.recommended | Todas las reglas de corrección como errores; las reglas de consola, temporizadores y cadencia de fotogramas como advertencias |
textjs.configs.strict | Todo como error, además de prefer-use-keymap como advertencia |
También puedes elegir las reglas individualmente:
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.
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
| Regla | Recomendada | Corrección |
|---|---|---|
no-dom-elements | error | sugerencia |
no-box-in-text | error | |
no-unsupported-glyphs | error | corrección automática, sugerencia |
no-console-in-components | advertencia | sugerencia |
prefer-use-interval | advertencia | sugerencia |
no-sub-frame-timers | advertencia | |
prefer-use-keymap | solo en modo estricto | |
valid-route-module | error | |
valid-metadata | error | |
route-handler-exports | error | sugerencia |
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 componentsno-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">omarker="quadrant"en los gráficos. Configura{ allowBraille: true }para apps que solo se ejecuten context runen 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 suggestionPara 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,errorynot-founddeben exportar un componente por defecto (TX015). export const dynamicdebe 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 buildlo 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:
metadataogenerateMetadataen archivosloadingoerror- ambas exportaciones en un archivo
async generateMetadata: se llama de forma síncrona, por lo que una Promise nunca establece el título- un
titleque no sea una cadena ni{ default, template?, absolute? }, o una clave mal escrita title.templatesin%s, o en una página, donde no tiene efecto- una
descriptionque 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: [...] }.
export function get() { … } // ✗ never calledexport function GET() { … } // ✓