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

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çãoO que ela habilita
textjs.configs.recommendedTodas as regras de correção como erro; regras de console, temporizadores e cadência de quadros como avisos
textjs.configs.strictTudo como erro, mais prefer-use-keymap como aviso

Como alternativa, escolha as regras 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" }],    },  },];

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.

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

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

RegraRecomendadaCorreção
no-dom-elementserrosugestão
no-box-in-texterro
no-unsupported-glyphserrocorreção automática, sugestão
no-console-in-componentsavisosugestão
prefer-use-intervalavisosugestão
no-sub-frame-timersaviso
prefer-use-keymapapenas strict
valid-route-moduleerro
valid-metadataerro
route-handler-exportserrosugestã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 components

no-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"> ou marker="quadrant" nos gráficos. Defina { allowBraille: true } para apps que só executam com text run em 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 suggestion

Para 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, error e not-found devem exportar um componente como padrão (TX015).
  • export const dynamic deve 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 build o 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:

  • metadata ou generateMetadata em arquivos loading ou error
  • ambos os exports em um arquivo
  • async generateMetadata: a chamada é síncrona, então uma Promise nunca define o título
  • title que não seja uma string ou { default, template?, absolute? }, ou uma chave escrita incorretamente
  • title.template sem %s, ou em uma página, onde não tem efeito
  • description que 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: [...] }.

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