ESLint
eslint-plugin-textjs: JSX bezpieczny dla konsoli, glify o szerokości komórki, timery w rytmie klatek i kontrole App Router.
eslint-plugin-textjs wykrywa błędy Text.js w edytorze: tagi DOM w komponentach konsoli, kontenery wewnątrz tekstu, glify niedostępne w Menlo, przypadkowe wywołania console.log i eksporty App Router, które są ignorowane bez ostrzeżenia.
Konfiguracja
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);Wtyczka obsługuje ESLint 9 i 10 z płaską konfiguracją. Reguły analizują składnię i nie potrzebują informacji o typach ani parserOptions.project. Działa każdy parser obsługujący TypeScript i JSX.
Zestawy konfiguracji
| Konfiguracja | Co włącza |
|---|---|
textjs.configs.recommended | Wszystkie reguły poprawności jako błędy; reguły konsoli, timerów i tempa klatek jako ostrzeżenia |
textjs.configs.strict | Wszystko jako błędy, dodatkowo prefer-use-keymap jako ostrzeżenie |
Zamiast tego wybierz reguły pojedynczo:
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" }], }, },];Pliki konsoli
Większość reguł dotyczy tylko plików renderujących w konsoli: plików importujących komponent lub hook (Box, Text, useInput, …) z @textjs/app lub @textjs/react. Pliki importujące react-dom lub @textjs/react/dom są kodem strony i pozostają bez zmian, więc witryna może łączyć strony DOM i aplikacje konsolowe w jednym projekcie. Import funkcji pomocniczej takiej jak tokenize lub parseMarkdown nie czyni pliku plikiem konsoli.
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" }, }, },];Reguły App Router kierują się nazwą pliku (page.tsx, layout.tsx, route.ts, …) w dowolnym katalogu app/, w tym src/app/. Jeśli twój katalog ma inną nazwę, przekaż { appDir: "routes" } do valid-route-module, valid-metadata i route-handler-exports.
Reguły
| Reguła | Zalecany poziom | Poprawka |
|---|---|---|
no-dom-elements | błąd | sugestia |
no-box-in-text | błąd | |
no-unsupported-glyphs | błąd | automatyczna poprawka, sugestia |
no-console-in-components | ostrzeżenie | sugestia |
prefer-use-interval | ostrzeżenie | sugestia |
no-sub-frame-timers | ostrzeżenie | |
prefer-use-keymap | tylko strict | |
valid-route-module | błąd | |
valid-metadata | błąd | |
route-handler-exports | błąd | sugestia |
no-dom-elements
<div>, <span>, elementy niestandardowe i inne tagi DOM nie istnieją w konsoli. Reconciler rozpoznaje tylko własne węzły hosta i zgłasza TX003 dla wszystkich pozostałych.
<div className="flex-col"> // ✗<Box className="flex-col"> // ✓<strong>Deployed</strong> // ✗<Text bold>Deployed</Text> // ✓Sugestia zastępuje tag najbliższym komponentem (div → Box, span/p → Text, strong → Text bold, a → Link, input → Input, hr → Divider, …) i dodaje import.
no-box-in-text
<Text> to ciąg stylowanych spanów; nie może zawierać kontenera. Zagnieżdżenie <Box> lub dowolnego komponentu z kontenerem w korzeniu (<ScrollBox>, <Input>, <Code>, <ProgressBar>, <Divider>, …) powoduje błąd TX004 podczas renderowania. Ta reguła wykrywa go już w edytorze, również wewnątrz &&, operatorów trójargumentowych, fragmentów i .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 rozmieszcza każdy znak w stałej siatce komórek. Glify niedostępne w Menlo, czcionce konsoli DevTools w systemie macOS, są renderowane czcionką zastępczą szerszą od komórki, więc każda kolejna komórka się przesuwa.
- Brajl (U+2800–U+28FF, klasyczne klatki spinnera terminalowego): użyj znaków blokowych lub ćwiartkowych (
▘▝▗▖),<Spinner variant="dots">lubmarker="quadrant"na wykresach. Ustaw{ allowBraille: true }dla aplikacji uruchamianych wyłącznie przeztext runw terminalu. - Różne symbole i strzałki (U+2B00–U+2BFF, na przykład mały kwadrat U+2B1D): sugestie proponują odpowiednik z Menlo (
·,●,←). - Prezentacja emoji dla symboli jednokomórkowych (
▶lub↩z następującym U+FE0F): selektor wariantu zamienia je w emoji zajmujące dwie komórki, podczas gdy siatka rezerwuje jedną. Automatyczna poprawka go usuwa.
Szerokie emoji (🚀, ✅) działają poprawnie: Text.js mierzy je jako dwie komórki.
no-console-in-components
Każda klatka to jedno wywołanie console.log. Twoje logi trafiają między klatki, są przewijane przez następną i pozostają ukryte w widoku przyklejonym do dołu. console.clear() usuwa samą aplikację.
"textjs/no-console-in-components": ["warn", { allow: ["error"], logger: "log" }]Wyświetlaj komunikaty w interfejsie za pomocą useToast() lub komponentu <ScrollBox> z logami albo sprawdzaj stan w nakładce deweloperskiej [▼] (Ctrl+`). Przy ustawionym logger sugestia zamienia console.warn(…) na log.warn(…). Pliki testowe i renderowania @textjs/app/testing są pomijane.
prefer-use-interval
Bezpośrednie użycie setInterval w komponencie wymaga efektu, funkcji sprzątającej i referencji, aby uniknąć nieaktualnego stanu. useInterval zapewnia wszystkie trzy, a null go zatrzymuje.
useEffect(() => { if (paused) return; const id = setInterval(tick, 1000); return () => clearInterval(id);}, [paused]);useInterval(tick, paused ? null : 1000); // the suggestionW animacjach useAnimationFrame dostosowuje się do tempa klatek renderera i zatrzymuje się, gdy wyjście jest wstrzymane. Zobacz Hooki pomocnicze.
no-sub-frame-timers
Text.js emituje najwyżej jedną klatkę co 66 ms (około 15 klatek/s). Interwał 16 ms renderuje cztery razy na każdą emitowaną klatkę; dodatkowe klatki są scalane i tylko zużywają czas procesora. Reguła zgłasza opóźnienia setInterval/useInterval poniżej minInterval (domyślnie 66) oraz useAnimationFrame z fps powyżej 15.
prefer-use-keymap
Ręczne porównywanie e.key działa, ale skróty klawiszowe są niewidoczne. useKeymap umieszcza powiązania z etykietami w <KeyHelp>, <KeyHints> (powiązania z hint: true) i palecie poleceń, obsługuje sekwencje takie jak g g i zatrzymuje propagację dla dopasowanych klawiszy. Reguła zgłasza handlery useInput, które porównują co najmniej dwa literały klawiszy (minKeys), i pomija handlery odczytujące e.input do wpisywania tekstu.
useKeymap({ "up, k": { label: "Increment", run: inc, hint: true }, "down, j": { label: "Decrement", run: dec, hint: true },});Zobacz Skróty klawiszowe.
valid-route-module
- Pliki
page,layout,loading,errorinot-foundmuszą eksportować komponent jako eksport domyślny (TX015). export const dynamicmusi mieć wartość"auto","force-static"lub"force-dynamic"(TX031). Text.js nie obsługuje wartości"error"z Next.js. Wartość jest odczytywana ze stron, layoutów i handlerów tras. W stronach i layoutach zapisz ją jako zwykły literał tekstowy:text buildodczytuje ją z kodu źródłowego bez uruchamiania modułu.
valid-metadata
Metadata są scalane z layoutów, a następnie ze strony (lub not-found). Reguła zgłasza to, co router zignorowałby bez ostrzeżenia:
metadatalubgenerateMetadataw plikachloadingluberror- oba eksporty w jednym pliku
async generateMetadata: funkcja jest wywoływana synchronicznie, więc Promise nigdy nie ustawia tytułutitle, który nie jest ciągiem znaków ani{ default, template?, absolute? }, lub błędnie zapisany klucztitle.templatebez%slub na stronie, gdzie nie ma żadnego efektudescription, który nie jest ciągiem znaków
route-handler-exports
Pliki route.ts odpowiadają tylko na eksportowane metody HTTP: GET, HEAD, POST, PUT, PATCH, DELETE i OPTIONS. Zapisane małymi literami get lub Post nigdy nie są wywoływane (żądania otrzymują 405); sugestia zmienia ich nazwę. Reguła zgłasza eksporty domyślne, handlery niebędące funkcjami i inne eksporty, z wyjątkiem dynamic i revalidate z konfiguracji segmentu trasy. Przenieś wspólny kod do lib/ lub wymień dodatkowe nazwy za pomocą { allow: [...] }.
export function get() { … } // ✗ never calledexport function GET() { … } // ✓