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-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);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
| Konfiguration | Aktivierte Regeln |
|---|---|
textjs.configs.recommended | Jede Korrektheitsregel als Fehler; Konsolen-, Timer- und Frame-Taktungsregeln als Warnungen |
textjs.configs.strict | Alles als Fehler, dazu prefer-use-keymap als Warnung |
Wähle stattdessen Regeln einzeln aus:
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.
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
| Regel | Empfohlen | Korrektur |
|---|---|---|
no-dom-elements | Fehler | Vorschlag |
no-box-in-text | Fehler | |
no-unsupported-glyphs | Fehler | automatische Korrektur, Vorschlag |
no-console-in-components | Warnung | Vorschlag |
prefer-use-interval | Warnung | Vorschlag |
no-sub-frame-timers | Warnung | |
prefer-use-keymap | nur strict | |
valid-route-module | Fehler | |
valid-metadata | Fehler | |
route-handler-exports | Fehler | Vorschlag |
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 componentsno-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">odermarker="quadrant"bei Diagrammen. Setze{ allowBraille: true }für Apps, die nur mittext runin 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 suggestionFü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- undnot-found-Dateien müssen eine Komponente als Standardexport bereitstellen (TX015).export const dynamicmuss"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 buildliest 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:
metadataodergenerateMetadatainloading- odererror-Dateien- beide Exporte in einer Datei
async generateMetadata: Der Aufruf erfolgt synchron, daher setzt ein Promise nie den Titeltitle, das weder ein String noch{ default, template?, absolute? }ist, oder ein falsch geschriebener Schlüsseltitle.templateohne%soder 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.
export function get() { … } // ✗ never calledexport function GET() { … } // ✓