ESLint
eslint-plugin-textjs: JSX seguro para o console, glifos com largura de célula, temporizadores no ritmo dos quadros e verificações do App Router.
eslint-plugin-textjs detecta erros de Text.js no editor: tags DOM em componentes do console, caixas dentro de texto, glifos que a Menlo não tem, chamadas avulsas de console.log e exports do App Router que são ignorados silenciosamente.
Configuração
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);O plugin oferece suporte ao ESLint 9 e 10 com configuração plana. As regras são sintáticas e não precisam de informações de tipo nem de parserOptions.project. Qualquer parser que entenda TypeScript e JSX funciona.
Configurações
| Configuração | O que ela habilita |
|---|---|
textjs.configs.recommended | Todas as regras de correção como erro; regras de console, temporizadores e cadência de quadros como avisos |
textjs.configs.strict | Tudo como erro, mais prefer-use-keymap como aviso |
Como alternativa, escolha as regras 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" }], }, },];Arquivos de console
A maioria das regras só se aplica a arquivos que renderizam no console: arquivos que importam um componente ou hook (Box, Text, useInput, …) de @textjs/app ou @textjs/react. Arquivos que importam react-dom ou @textjs/react/dom são código de página e não são verificados, então um site pode combinar páginas DOM e apps de console em um projeto. Importar um utilitário como tokenize ou parseMarkdown não torna um arquivo um arquivo de console.
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" }, }, },];As regras do App Router usam o nome do arquivo (page.tsx, layout.tsx, route.ts, …) dentro de qualquer diretório app/, incluindo src/app/. Se o seu tiver outro nome, passe { appDir: "routes" } para valid-route-module, valid-metadata e route-handler-exports.
Regras
| Regra | Recomendada | Correção |
|---|---|---|
no-dom-elements | erro | sugestão |
no-box-in-text | erro | |
no-unsupported-glyphs | erro | correção automática, sugestão |
no-console-in-components | aviso | sugestão |
prefer-use-interval | aviso | sugestão |
no-sub-frame-timers | aviso | |
prefer-use-keymap | apenas strict | |
valid-route-module | erro | |
valid-metadata | erro | |
route-handler-exports | erro | sugestão |
no-dom-elements
<div>, <span>, elementos personalizados e outras tags DOM não existem no console. O reconciliador só reconhece seus próprios nós hospedeiros e lança TX003 para qualquer outra coisa.
<div className="flex-col"> // ✗<Box className="flex-col"> // ✓<strong>Deployed</strong> // ✗<Text bold>Deployed</Text> // ✓A sugestão troca a tag pelo componente mais próximo (div → Box, span/p → Text, strong → Text bold, a → Link, input → Input, hr → Divider, …) e adiciona o import.
no-box-in-text
<Text> é uma sequência de spans estilizados; ele não pode conter uma caixa. Aninhar <Box> ou qualquer componente cuja raiz seja uma caixa (<ScrollBox>, <Input>, <Code>, <ProgressBar>, <Divider>, …) lança TX004 durante a renderização. Esta regra detecta isso no editor, inclusive dentro de &&, ternários, fragmentos e .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 organiza cada caractere em uma grade de células fixas. Glifos que a Menlo, a fonte do console do DevTools no macOS, não tem são renderizados em uma fonte alternativa mais larga que uma célula, deslocando todas as células seguintes.
- Braille (U+2800–U+28FF, os quadros clássicos de spinner de terminal): use caracteres de bloco ou quadrante (
▘▝▗▖),<Spinner variant="dots">oumarker="quadrant"nos gráficos. Defina{ allowBraille: true }para apps que só executam comtext runem um terminal. - Símbolos e setas diversos (U+2B00–U+2BFF, como o quadrado pequeno U+2B1D): as sugestões oferecem o equivalente da Menlo (
·,●,←). - Apresentação de emoji em símbolos de uma célula (
▶ou↩seguido de U+FE0F): o seletor de variação os transforma em emojis de duas células enquanto a grade reserva uma. A correção automática o remove.
Emojis largos (🚀, ✅) funcionam: Text.js os mede como duas células.
no-console-in-components
Cada quadro é um console.log. Seus próprios logs aparecem entre os quadros, saem da área visível com a rolagem do próximo e ficam ocultos pela visualização fixada no final. console.clear() apaga o próprio app.
"textjs/no-console-in-components": ["warn", { allow: ["error"], logger: "log" }]Mostre mensagens na interface com useToast() ou um <ScrollBox> de logs, ou inspecione o estado no Dev Overlay [▼] (Ctrl+`). Com logger, a sugestão reescreve console.warn(…) como log.warn(…). Arquivos de teste e renderizações de @textjs/app/testing são ignorados.
prefer-use-interval
Um setInterval direto em um componente precisa de um efeito, uma limpeza e uma ref para evitar estado desatualizado. useInterval cuida dos três, e null o interrompe.
useEffect(() => { if (paused) return; const id = setInterval(tick, 1000); return () => clearInterval(id);}, [paused]);useInterval(tick, paused ? null : 1000); // the suggestionPara animações, useAnimationFrame segue a cadência de quadros do renderizador e para enquanto a saída está pausada. Consulte Hooks utilitários.
no-sub-frame-timers
Text.js emite no máximo um quadro a cada 66 ms (cerca de 15 fps). Um intervalo de 16 ms renderiza quatro vezes por quadro emitido; os quadros extras são agrupados e só consomem CPU. Sinaliza atrasos de setInterval/useInterval abaixo de minInterval (padrão 66) e useAnimationFrame com fps acima de 15.
prefer-use-keymap
Comparar e.key manualmente funciona, mas os atalhos ficam invisíveis. useKeymap coloca associações com rótulos em <KeyHelp>, <KeyHints> (associações com hint: true) e na paleta de comandos, trata sequências como g g e interrompe a propagação das teclas correspondentes. Sinaliza manipuladores de useInput que comparam dois ou mais literais de tecla (minKeys) e ignora manipuladores que leem e.input para digitação.
useKeymap({ "up, k": { label: "Increment", run: inc, hint: true }, "down, j": { label: "Decrement", run: dec, hint: true },});Consulte Atalhos de teclado.
valid-route-module
- Arquivos
page,layout,loading,errorenot-founddevem exportar um componente como padrão (TX015). export const dynamicdeve ser"auto","force-static"ou"force-dynamic"(TX031). Text.js não oferece suporte ao"error"do Next.js. O valor é lido de páginas, layouts e manipuladores de rota. Em páginas e layouts, escreva-o como um literal de string simples:text buildo lê do código-fonte sem executar o módulo.
valid-metadata
Metadata é mesclado a partir dos layouts e depois da página (ou de not-found). A regra sinaliza o que o roteador ignoraria silenciosamente:
metadataougenerateMetadataem arquivosloadingouerror- ambos os exports em um arquivo
async generateMetadata: a chamada é síncrona, então uma Promise nunca define o títulotitleque não seja uma string ou{ default, template?, absolute? }, ou uma chave escrita incorretamentetitle.templatesem%s, ou em uma página, onde não tem efeitodescriptionque não seja uma string
route-handler-exports
Arquivos route.ts só respondem aos métodos HTTP que exportam: GET, HEAD, POST, PUT, PATCH, DELETE e OPTIONS. get em minúsculas ou Post nunca são chamados (as requisições recebem 405); a sugestão renomeia o método. Exportações padrão, manipuladores que não são funções e outras exportações são sinalizados, exceto as configurações de segmento de rota dynamic e revalidate. Mova o código compartilhado para lib/, ou liste nomes extras com { allow: [...] }.
export function get() { … } // ✗ never calledexport function GET() { … } // ✓