ESLint
eslint-plugin-textjs:コンソールで安全に使えるJSX、セル幅の字形、フレーム間隔に合わせたタイマー、App Routerのチェック。
eslint-plugin-textjs は、コンソールコンポーネント内のDOMタグ、テキスト内のボックス、Menloにないグリフ、余分な console.log 呼び出し、暗黙に無視されるApp Routerのエクスポートなど、Text.jsの誤りをエディターで検出します。
セットアップ
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);このプラグインは、フラット設定のESLint 9と10をサポートします。ルールは構文ベースで、型情報や parserOptions.project は不要です。TypeScriptとJSXを解析できるパーサーなら使えます。
設定
| 設定 | 有効になるルール |
|---|---|
textjs.configs.recommended | 正しさに関するすべてのルールをエラー、コンソール、タイマー、フレーム間隔のルールを警告として有効化 |
textjs.configs.strict | すべてをエラーとして有効化し、さらに prefer-use-keymap を警告として追加 |
代わりに、ルールを個別に選択できます。
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" }], }, },];コンソールファイル
ほとんどのルールは、コンソールに描画するファイルにのみ適用されます。対象は、@textjs/app または @textjs/react からコンポーネントやフック(Box、Text、useInput、…)をインポートするファイルです。react-dom または @textjs/react/dom をインポートするファイルはページのコードとして扱われ、検査されないため、1つのプロジェクトでDOMページとコンソールアプリを混在させられます。tokenize や parseMarkdown のようなユーティリティーをインポートしても、コンソールファイルにはなりません。
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" }, }, },];App Routerのルールは、src/app/ を含む任意の app/ ディレクトリー内で、ファイル名(page.tsx、layout.tsx、route.ts、…)に基づいて適用されます。別のディレクトリー名を使っている場合は、valid-route-module、valid-metadata、route-handler-exports に { appDir: "routes" } を渡してください。
ルール
| ルール | 推奨設定 | 修正 |
|---|---|---|
no-dom-elements | エラー | 提案 |
no-box-in-text | エラー | |
no-unsupported-glyphs | エラー | 自動修正、提案 |
no-console-in-components | 警告 | 提案 |
prefer-use-interval | 警告 | 提案 |
no-sub-frame-timers | 警告 | |
prefer-use-keymap | strictのみ | |
valid-route-module | エラー | |
valid-metadata | エラー | |
route-handler-exports | エラー | 提案 |
no-dom-elements
<div>、<span>、カスタム要素などのDOMタグはコンソールには存在しません。リコンサイラーは独自のホストノードのみを認識し、それ以外には TX003 をスローします。
<div className="flex-col"> // ✗<Box className="flex-col"> // ✓<strong>Deployed</strong> // ✗<Text bold>Deployed</Text> // ✓修正提案は、タグを最も近いコンポーネント(div → Box、span/p → Text、strong → Text bold、a → Link、input → Input、hr → Divider、…)に置き換え、インポートを追加します。
no-box-in-text
<Text> はスタイル付きスパンの連なりで、ボックスを含められません。<Box> や、ルートがボックスのコンポーネント(<ScrollBox>、<Input>、<Code>、<ProgressBar>、<Divider>、…)を入れ子にすると、描画時に TX004 がスローされます。このルールは、&&、三項演算子、フラグメント、.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は、すべての文字を固定セルのグリッドに配置します。macOSのDevToolsコンソールフォントであるMenloにないグリフは、セルより幅が広いフォールバックフォントで描画されるため、それ以降のすべてのセルがずれます。
- 点字(U+2800〜U+28FF、従来のターミナルスピナーのフレーム)。ブロック文字や四分割文字(
▘▝▗▖)、<Spinner variant="dots">、またはチャートのmarker="quadrant"を使ってください。ターミナルでtext runのみを使って実行するアプリでは、{ allowBraille: true }を設定してください。 - その他の記号および矢印(U+2B00〜U+2BFF、小さい四角のU+2B1Dなど)。修正提案にはMenloの対応する文字(
·、●、←)が表示されます。 - 1セル記号の絵文字表示(
▶または↩の後にU+FE0Fが続くもの)。グリッドは1セルを確保しますが、異体字セレクターによって2セルの絵文字になります。自動修正でこのセレクターを削除します。
幅の広い絵文字(🚀、✅)は問題ありません。Text.jsは2セルとして計測します。
no-console-in-components
各フレームは1回の console.log です。独自のログはフレームの間に入り、次のフレームによってスクロールされ、最下部に追従する表示では見えなくなります。console.clear() はアプリ自体を消去します。
"textjs/no-console-in-components": ["warn", { allow: ["error"], logger: "log" }]useToast() やログ用の <ScrollBox> でUIにメッセージを表示するか、[▼] Dev Overlay(Ctrl+`)で状態を確認します。logger を指定すると、修正候補は console.warn(…) を log.warn(…) に書き換えます。テストファイルと @textjs/app/testing によるレンダリングは対象外です。
prefer-use-interval
コンポーネント内で setInterval を直接使う場合、古い状態の参照を避けるためにエフェクト、クリーンアップ、refが必要です。useInterval はこの3つを処理し、null で停止します。
useEffect(() => { if (paused) return; const id = setInterval(tick, 1000); return () => clearInterval(id);}, [paused]);useInterval(tick, paused ? null : 1000); // the suggestionアニメーションでは、useAnimationFrame がレンダラーのフレーム間隔に従い、出力の一時停止中は停止します。ユーティリティーフックを参照してください。
no-sub-frame-timers
Text.jsは66msごとに最大1フレーム(約15fps)を出力します。16ms間隔では、出力される1フレームにつき4回描画されます。余分なフレームはまとめられ、CPUを消費するだけです。setInterval/useInterval の遅延が minInterval(デフォルトは 66)未満の場合と、useAnimationFrame の fps が15を超える場合に報告します。
prefer-use-keymap
e.key を手動で比較しても動作しますが、ショートカットは表示されません。useKeymap は、ラベル付きの割り当てを <KeyHelp>、<KeyHints>(hint: true の割り当て)、コマンドパレットに表示し、g g のようなシーケンスを処理し、一致したキーの伝播を停止します。2つ以上のキーリテラル(minKeys)を比較する useInput ハンドラーを報告し、文字入力のために e.input を読み取るハンドラーはスキップします。
useKeymap({ "up, k": { label: "Increment", run: inc, hint: true }, "down, j": { label: "Decrement", run: dec, hint: true },});キーボードショートカットを参照してください。
valid-route-module
page、layout、loading、error、not-foundファイルは、コンポーネントをデフォルトエクスポートする必要があります(TX015)。export const dynamicは"auto"、"force-static"、"force-dynamic"のいずれかである必要があります(TX031)。Text.jsはNext.jsの"error"をサポートしません。ページ、レイアウト、ルートハンドラーから読み取られます。ページとレイアウトでは、通常の文字列リテラルで記述してください。text buildはモジュールを実行せずにソースから読み取ります。
valid-metadata
Metadataはレイアウトから順に、最後にページ(または not-found)からマージされます。このルールは、ルーターが暗黙に無視するものを報告します。
loadingまたはerrorファイル内のmetadataまたはgenerateMetadata- 1つのファイル内の両方のエクスポート
async generateMetadata(同期的に呼び出されるため、Promiseではタイトルが設定されません)- 文字列でも
{ default, template?, absolute? }でもないtitle、またはスペルを誤ったキー %sのないtitle.template、または効果のないページ内のtitle.template- 文字列ではない
description
route-handler-exports
route.ts ファイルは、エクスポートしたHTTPメソッド(GET、HEAD、POST、PUT、PATCH、DELETE、OPTIONS)にのみ応答します。小文字を含む get や Post は呼び出されません(リクエストには 405 が返ります)。修正提案で名前を変更します。dynamic と revalidate のルートセグメント設定を除き、デフォルトエクスポート、関数ではないハンドラー、その他のエクスポートを報告します。共通コードは lib/ に移動するか、追加の名前を { allow: [...] } で指定してください。
export function get() { … } // ✗ never calledexport function GET() { … } // ✓