ESLint

eslint-plugin-textjs: konsolensicheres JSX, zellbreite Glyphen, Timer im Frame-Takt und App Router-Prüfungen.

eslint-plugin-textjs erkennt Text.js-Fehler im Editor: DOM-Tags in Konsolenkomponenten, Boxen innerhalb von Text, Glyphen, die Menlo fehlen, verirrte console.log-Aufrufe und App Router-Exporte, die stillschweigend ignoriert werden.

Einrichtung

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

Das Plugin unterstützt ESLint 9 und 10 mit Flat Config. Die Regeln sind syntaktisch und benötigen weder Typinformationen noch parserOptions.project. Jeder Parser, der TypeScript und JSX versteht, funktioniert.

Konfigurationen

KonfigurationAktivierte Regeln
textjs.configs.recommendedJede Korrektheitsregel als Fehler; Konsolen-, Timer- und Frame-Taktungsregeln als Warnungen
textjs.configs.strictAlles als Fehler, dazu prefer-use-keymap als Warnung

Wähle stattdessen Regeln einzeln aus:

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

Konsolendateien

Die meisten Regeln gelten nur für Dateien, die in die Konsole rendern: Dateien, die eine Komponente oder einen Hook (Box, Text, useInput, …) aus @textjs/app oder @textjs/react importieren. Dateien, die react-dom oder @textjs/react/dom importieren, sind Seitencode und bleiben unberührt, sodass eine Website DOM-Seiten und Konsolen-Apps in einem Projekt kombinieren kann. Der Import einer Hilfsfunktion wie tokenize oder parseMarkdown macht eine Datei nicht zur Konsolendatei.

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

Die App Router-Regeln richten sich nach dem Dateinamen (page.tsx, layout.tsx, route.ts, …) innerhalb jedes app/-Verzeichnisses, einschließlich src/app/. Wenn deines anders heißt, übergib { appDir: "routes" } an valid-route-module, valid-metadata und route-handler-exports.

Regeln

RegelEmpfohlenKorrektur
no-dom-elementsFehlerVorschlag
no-box-in-textFehler
no-unsupported-glyphsFehlerautomatische Korrektur, Vorschlag
no-console-in-componentsWarnungVorschlag
prefer-use-intervalWarnungVorschlag
no-sub-frame-timersWarnung
prefer-use-keymapnur strict
valid-route-moduleFehler
valid-metadataFehler
route-handler-exportsFehlerVorschlag

no-dom-elements

<div>, <span>, benutzerdefinierte Elemente und andere DOM-Tags gibt es in der Konsole nicht. Der Reconciler kennt nur seine eigenen Host-Knoten und löst bei allem anderen TX003 aus.

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

Der Vorschlag ersetzt das Tag durch die passendste Komponente (div → Box, span/p → Text, strong → Text bold, a → Link, input → Input, hr → Divider, …) und fügt den Import hinzu.

no-box-in-text

<Text> ist eine Folge formatierter Spans; sie kann keine Box enthalten. Das Verschachteln von <Box> oder einer Komponente mit einer Box als Wurzel (<ScrollBox>, <Input>, <Code>, <ProgressBar>, <Divider>, …) löst beim Rendern TX004 aus. Diese Regel erkennt das stattdessen im Editor, auch innerhalb von &&, ternären Ausdrücken, Fragmenten und .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 ordnet jedes Zeichen in einem festen Zellraster an. Glyphen, die Menlo, der DevTools-Konsolenschrift auf macOS, fehlen, werden in einer Ersatzschrift dargestellt, die breiter als eine Zelle ist, sodass sich jede nachfolgende Zelle verschiebt.

  • Braille (U+2800–U+28FF, die klassischen Frames von Terminal-Spinnern): Verwende Block- oder Quadrantenzeichen (▘▝▗▖), <Spinner variant="dots"> oder marker="quadrant" bei Diagrammen. Setze { allowBraille: true } für Apps, die nur mit text run in einem Terminal laufen.
  • Verschiedene Symbole und Pfeile (U+2B00–U+2BFF, etwa das kleine Quadrat U+2B1D): Vorschläge bieten das Menlo-Äquivalent an (·, ●, ←).
  • Emoji-Darstellung bei einzelligen Symbolen (▶ oder ↩ gefolgt von U+FE0F): Der Variationsselektor macht daraus zweizellige Emoji, während das Raster eine Zelle reserviert. Die automatische Korrektur entfernt ihn.

Breite Emoji (🚀, ✅) sind unproblematisch: Text.js misst sie als zwei Zellen.

no-console-in-components

Jeder Frame ist ein console.log. Deine eigenen Logs landen zwischen Frames, werden vom nächsten weggescrollt und sind in der am unteren Rand fixierten Ansicht verborgen. console.clear() löscht die App selbst.

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

Zeige Nachrichten in der UI mit useToast() oder einer Log-<ScrollBox> an, oder untersuche den Zustand im [▼] Dev Overlay (Ctrl+`). Mit logger schreibt der Vorschlag console.warn(…) in log.warn(…) um. Testdateien und Render-Aufrufe von @textjs/app/testing werden übersprungen.

prefer-use-interval

Ein direktes setInterval in einer Komponente benötigt einen Effekt, eine Aufräumfunktion und eine Ref, um veralteten Zustand zu vermeiden. useInterval übernimmt alle drei Aufgaben, und null stoppt es.

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

Für Animationen folgt useAnimationFrame der Frame-Taktung des Renderers und stoppt, solange die Ausgabe pausiert ist. Siehe Hilfs-Hooks.

no-sub-frame-timers

Text.js gibt höchstens alle 66 ms einen Frame aus (etwa 15 fps). Ein Intervall von 16 ms rendert viermal pro ausgegebenem Frame; die zusätzlichen Frames werden zusammengefasst und verbrauchen nur CPU-Zeit. Meldet setInterval/useInterval-Verzögerungen unter minInterval (Standard 66) und useAnimationFrame mit fps über 15.

prefer-use-keymap

e.key von Hand zu vergleichen funktioniert, aber die Tastenkürzel bleiben unsichtbar. useKeymap zeigt beschriftete Tastenbelegungen in <KeyHelp>, <KeyHints> (Belegungen mit hint: true) und der Befehlspalette an, verarbeitet Sequenzen wie g g und stoppt die Weitergabe bei passenden Tasten. Meldet useInput-Handler, die zwei oder mehr Tastenliterale (minKeys) vergleichen, und überspringt Handler, die e.input für Texteingaben lesen.

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

Siehe Tastenkürzel.

valid-route-module

  • page-, layout-, loading-, error- und not-found-Dateien müssen eine Komponente als Standardexport bereitstellen (TX015).
  • export const dynamic muss "auto", "force-static" oder "force-dynamic" sein (TX031). Text.js unterstützt "error" aus Next.js nicht. Der Wert wird aus Seiten, Layouts und Route-Handlern gelesen. Schreibe ihn in Seiten und Layouts als reines String-Literal: text build liest ihn aus dem Quellcode, ohne das Modul auszuführen.

valid-metadata

Metadata wird aus den Layouts und anschließend aus der Seite (oder not-found) zusammengeführt. Die Regel meldet, was der Router stillschweigend ignorieren würde:

  • metadata oder generateMetadata in loading- oder error-Dateien
  • beide Exporte in einer Datei
  • async generateMetadata: Der Aufruf erfolgt synchron, daher setzt ein Promise nie den Titel
  • title, das weder ein String noch { default, template?, absolute? } ist, oder ein falsch geschriebener Schlüssel
  • title.template ohne %s oder in einer Seite, wo es keine Wirkung hat
  • eine description, die kein String ist

route-handler-exports

route.ts-Dateien beantworten nur die HTTP-Methoden, die sie exportieren: GET, HEAD, POST, PUT, PATCH, DELETE und OPTIONS. Kleingeschriebenes get oder Post wird nie aufgerufen (Anfragen erhalten 405); der Vorschlag benennt es um. Gemeldet werden Standardexporte, Handler, die keine Funktionen sind, und andere Exporte, mit Ausnahme der Routensegment-Konfiguration dynamic und revalidate. Verschiebe gemeinsam genutzten Code nach lib/ oder liste zusätzliche Namen mit { allow: [...] } auf.

app/api/health/route.tsts
export function get() { … }   // ✗ never calledexport function GET() { … }   // ✓
Diese Seite auf GitHub bearbeiten