近日,不少前端开发者在将项目从 npm 迁移至 pnpm 时,频繁遭遇一个棘手的编译错误:“Cannot find module 'react/jsx-runtime' or its corresponding type declarations”。该问题在 React 18+ 与 TypeScript 项目中出现尤为集中,导致 CI 流水线阻塞、本地开发环境崩溃,引发社区广泛讨论。本文将从根因、复现场景、修复策略三个维度进行深度拆解。

一、问题现象:熟悉的“失踪”模块

当开发者执行 pnpm install 后,运行 tscvite build 时,终端输出如下错误:

node_modules/@types/react/index.d.ts:8:35 - error TS2307: Cannot find module 'react/jsx-runtime' or its corresponding type declarations.

该错误指向 @types/react 的类型声明文件,它尝试引入 react/jsx-runtime 子路径,但 pnpm 的严格模块解析机制下,该模块并未被正确解析。与此同时,React 运行时本身并未缺失(react 包已安装),只有类型声明路径出现问题。

二、原因剖析:pnpm 的“幽灵依赖”治理策略

问题核心在于 pnpm 与 npm 不同的依赖解析哲学。npm 默认将所有依赖扁平化到 node_modules 根目录,导致“幽灵依赖”泛滥——即子依赖可被不直接声明它的包访问。而 pnpm 使用符号链接+硬链接机制,严格遵循 package.json 中的依赖声明,只将直接依赖暴露给项目,子依赖则被隔离在 .pnpm 目录中。

对于 React 18+ 项目,react/jsx-runtime 是 React 运行时自带的内部模块(用于新的 JSX 转换),它仅由 react 包提供。但 @types/react 的类型声明中引用了 react/jsx-runtime,当 @types/react 被提升到 node_modules 根目录时,TypeScript 会尝试根据 @types/reactpackage.json 中的 types 字段(或 typings)去查找 react/jsx-runtime 的类型。然而 pnpm 的符号链接机制使得 react 包并未被提升到根目录下的 node_modules/react 位置,而是作为 @types/react 的 peerDependency 被嵌套在 .pnpm 内部,导致 TypeScript 无法根据相对路径定位。

简言之,类型声明文件“以为”react/jsx-runtime 在常规 node_modules/react 下,但 pnpm 并未将它放在那里

三、五种修复方案:从快速解法到工程化选择

1. 推荐方案:显式安装 @types/react 并匹配版本

确保 @types/reactreact 的版本完全对齐。在 package.json 中:

"devDependencies": {
  "@types/react": "^18.2.0",
  "typescript": "^5.0.0"
},
"dependencies": {
  "react": "^18.2.0"
}

然后执行 pnpm install,pnpm 会将 react 作为直接依赖提升到根 node_modules,从而满足类型查找。

2. 激进方案:启用 shamefully-hoist

在项目根目录 .npmrc 中添加:

shamefully-hoist=true

此配置强制 pnpm 将依赖扁平化,模拟 npm 行为。注意:这会丧失 pnpm 的磁盘空间与安全优势,仅建议作为临时过渡。

3. 精准方案:使用 .pnpmfile.cjs 显式提升

创建 pnpmfile.cjs

module.exports = {
  hooks: {
    readPackage(pkg) {
      if (pkg.name === '@types/react') {
        pkg.dependencies = pkg.dependencies || {};
        pkg.dependencies.react = '^18.2.0';
      }
      return pkg;
    }
  }
}

然后执行 pnpm install,会强制 @types/reactreact 作为直接依赖而非 peerDependency,从而避免路径问题。

4. 类型层面规避:在 tsconfig.json 中排除类型检查

若项目不依赖 @types/react 的类型检查,可临时:

{
  "compilerOptions": {
    "types": [],
    "skipLibCheck": true
  }
}

不推荐长期使用,会丧失类型安全。

5. 使用 resolutions 字段锁定子依赖

package.json 中加入:

"pnpm": {
  "overrides": {
    "@types/react": {
      "react": "^18.2.0"
    }
  }
}

四、社区声音与前瞻

该错误并非 pnpm 的 bug,而是其严格依赖管理理念与部分类型库设计假设之间的矛盾。React 核心团队已在 RFC 中讨论将 jsx-runtime 的类型声明内联到 @types/react,避免依赖外部路径。同时,TypeScript 5.1 引入的 moduleSuffixes 配置也可能缓解此类问题。

截至发稿,pnpm 官方建议开发者将 @types/reactreact 同时列为直接依赖,并保持版本同步。对于已有大型项目,迁移前建议使用 pnpm import 命令从 npm lockfile 生成 pnpm lockfile,并逐个排查类型依赖。

五、结语

“Cannot find module 'react/jsx-runtime'” 是现代化包管理器演进中的典型阵痛。它提醒开发者:选择工具的同时,也在选择一套依赖治理哲学。无论是回退到 shamefully-hoist,还是主动适配 pnpm 的严格模式,核心在于理解“幽灵依赖的危害”与“类型声明解析规则”之间的平衡。随着 React 18.3+ 及 TypeScript 团队改进类型发布策略,未来此类跨工具不兼容问题将逐渐减少。在这之前,掌握上述修复方法,将是每位 React 开发者必备的生存技能。