ESLint

eslint-plugin-textjs:控制台安全 JSX、单元格宽度字形、按帧节奏运行的定时器和 App Router 检查。

eslint-plugin-textjs 在编辑器中检测 Text.js 错误:控制台组件中的 DOM 标签、文本中的盒子、Menlo 缺少的字形、多余的 console.log 调用,以及被静默忽略的 App Router 导出。

设置

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 导入组件或 Hook(Box、Text、useInput 等)的文件。导入 react-dom 或 @textjs/react/dom 的文件属于页面代码,不受这些规则检查,因此网站可以在同一项目中混用 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 规则根据任意 app/ 目录中的文件名(page.tsx、layout.tsx、route.ts 等)判断,也包括 src/app/。如果你的目录使用其他名称,请向 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仅严格配置
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> 是一串带样式的 span,不能包含盒子。嵌套 <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 中的对应字符(·、●、←)。
  • 单单元格符号的表情符号显示形式(▶ 或 ↩ 后跟 U+FE0F):变体选择符会将其变为占两个单元格的表情符号,而网格只预留了一个单元格。自动修复会移除该选择符。

宽表情符号(🚀、✅)没有问题:Text.js 将其计为两个单元格。

no-console-in-components

每一帧都是一次 console.log。你自己的日志会出现在帧之间,被下一帧滚出视野,并在固定到底部的视图中隐藏。console.clear() 会清除应用本身。

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

使用 useToast() 或日志 <ScrollBox> 在 UI 中显示消息,或在 [▼] 开发浮层(Ctrl+`)中检查状态。配置 logger 后,修复建议会将 console.warn(…) 改写为 log.warn(…)。测试文件和 @textjs/app/testing 渲染会跳过检查。

prefer-use-interval

在组件中直接使用 setInterval,需要副作用、清理函数和 ref 来避免读取过期状态。useInterval 处理这三项工作,传入 null 即可停止。

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

对于动画,useAnimationFrame 会遵循渲染器的帧节奏,并在输出暂停时停止。参见工具 Hook。

no-sub-frame-timers

Text.js 每 66 ms 最多输出一帧(约 15 fps)。16 ms 的定时器会在每个输出帧内渲染四次;多余的帧会被合并,只会消耗 CPU。此规则会报告延迟低于 minInterval(默认 66)的 setInterval/useInterval,以及 fps 高于 15 的 useAnimationFrame。

prefer-use-keymap

手动比较 e.key 可以工作,但快捷键不会显示出来。useKeymap 将带标签的绑定放入 <KeyHelp>、<KeyHints>(带有 hint: true 的绑定)和命令面板,处理 g g 这样的按键序列,并阻止匹配按键的事件传播。此规则会报告比较两个或更多按键字面量(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
  • 同一文件中同时存在这两种导出
  • 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 上编辑此页