返回博客

Vitest 测 .tsx 报 React is not defined,构建却能过——esbuild classic runtime 惹的祸

加了一个新的字段渲染器,文件是标准的 .tsx,TypeScript 类型检查全绿,next build 一次过,提 PR 前跑单测——三个渲染相关的用例全报 ReferenceError: React is not defined,另外六个纯函数用例却完全正常。更奇怪的是,用同样方式写的其他 handler 文件一点问题没有。

现象

ReferenceError: React is not defined
 ❯ renderStringEditor src/engine/field-types/handlers/string.tsx:49:3
  • npm run build 通过
  • tsc --noEmit 通过
  • npm run test 挂在含 JSX 返回值的函数上,纯逻辑函数不受影响
  • 错误堆栈指向 .tsx 文件里第一处 JSX 语法(<Input ...>

根因

这是 两个编译器对同一份 tsconfig 产生了不同行为

项目的 tsconfig.json 里配置了:

{
  "compilerOptions": {
    "jsx": "preserve"
  }
}

jsx: "preserve" 是 Next.js 官方推荐值——它告诉 TypeScript 编译器「别动 JSX,原样保留,让下游工具处理」。

  • Next.js 生产构建用 SWC 处理 .tsx,SWC 默认走 automatic runtime(React 17+ 引入的 _jsx/_jsxs transform),不需要每个文件手动 import React,所以代码写不写都能过
  • Vitestesbuild 转译 .tsx,esbuild 遇到 jsx: "preserve" 时不知道下游是 automatic 还是 classic,默认回退到 classic runtime,即把所有 JSX 编译成 React.createElement(...),但 React 根本没有在当前模块作用域里——爆了

两个工具链,两种 JSX 运行时,同一份 tsconfig,行为正好相反。

坑:构建通过 ≠ 测试通过,JSX 运行时两套

Next.js SWC 走 automatic runtime,不需要显式 import React。 Vitest esbuild 遇到 jsx: "preserve" 时走 classic runtime,必须 React 在作用域里。 二者同时用时,单测里含 JSX 的函数会在运行时悄悄爆炸,而构建环境完全察觉不到。

正解

方案 A(推荐):在该文件顶部加一行 import

'use client'
 
import * as React from 'react'  // ← 加这一行
import { Lock } from 'lucide-react'
// ...其他 import
  • 侵入最小,只改有问题的文件
  • 与项目里其他 UI 组件文件(如 shadcn 的 input.tsx)保持一致
  • import * as React 在 automatic runtime 构建时会被 tree-shake 掉,不影响 bundle 体积

方案 B:修改 vitest.config.ts 统一走 automatic runtime

// vitest.config.ts
export default defineConfig({
  esbuild: {
    jsx: 'automatic',
    jsxImportSource: 'react',
  },
  // ...
})

好处是所有 .tsx 文件都不用改,一劳永逸。坏处是和 tsconfig.json 里的 jsx: "preserve" 显式矛盾,新接手项目的人看到两个文件对 JSX 的描述不一致会困惑。

结论:优先方案 A。只有当项目里有大量新增 .tsx 都需要加 React 引入时才考虑方案 B。

正解:import * as React 最小修复

在需要被 Vitest 测试的 .tsx 文件顶部加 import * as React from 'react',esbuild classic runtime 就能找到 React.createElement,测试通过;SWC/Webpack 构建时该 import 无副作用。

快速验证

写完新的 .tsx handler 后,先跑一次单文件测试确认:

npm run test -- path/to/new-handler.test.ts

看到 ReferenceError: React is not defined → 加一行 import 即可,不需要其他任何改动。

一句话外卖

Next.js + Vitest 混用时,tsconfig jsx: "preserve" 让 SWC 走 automatic、让 esbuild 走 classic,同一份 .tsx 文件「构建过、测试挂」是两套运行时不对齐的必然结果——加一行 import * as React 是最小无副作用修复。