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

このプラグインは、フラット設定のESLint 9と10をサポートします。ルールは構文ベースで、型情報や parserOptions.project は不要です。TypeScriptとJSXを解析できるパーサーなら使えます。

設定

設定有効になるルール
textjs.configs.recommended正しさに関するすべてのルールをエラー、コンソール、タイマー、フレーム間隔のルールを警告として有効化
textjs.configs.strictすべてをエラーとして有効化し、さらに prefer-use-keymap を警告として追加

代わりに、ルールを個別に選択できます。

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

コンソールファイル

ほとんどのルールは、コンソールに描画するファイルにのみ適用されます。対象は、@textjs/app または @textjs/react からコンポーネントやフック(Box、Text、useInput、…)をインポートするファイルです。react-dom または @textjs/react/dom をインポートするファイルはページのコードとして扱われ、検査されないため、1つのプロジェクトでDOMページとコンソールアプリを混在させられます。tokenize や parseMarkdown のようなユーティリティーをインポートしても、コンソールファイルにはなりません。

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

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-keymapstrictのみ
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 components

no-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: [...] } で指定してください。

app/api/health/route.tsts
export function get() { … }   // ✗ never calledexport function GET() { … }   // ✓
GitHubでこのページを編集