近日,不少前端开发者在将项目从 npm 迁移至 pnpm 时,频繁遭遇一个棘手的编译错误:“Cannot find module 'react/jsx-runtime' or its corresponding type declarations”。该问题在 React 18+ 与 TypeScript 项目中出现尤为集中,导致 CI 流水线阻塞、本地开发环境崩溃,引发社区广泛讨论。本文将从根因、复现场景、修复策略三个维度进行深度拆解。
一、问题现象:熟悉的“失踪”模块
当开发者执行 pnpm install 后,运行 tsc 或 vite 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/react 的 package.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/react 与 react 的版本完全对齐。在 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/react 将 react 作为直接依赖而非 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/react 与 react 同时列为直接依赖,并保持版本同步。对于已有大型项目,迁移前建议使用 pnpm import 命令从 npm lockfile 生成 pnpm lockfile,并逐个排查类型依赖。
五、结语
“Cannot find module 'react/jsx-runtime'” 是现代化包管理器演进中的典型阵痛。它提醒开发者:选择工具的同时,也在选择一套依赖治理哲学。无论是回退到 shamefully-hoist,还是主动适配 pnpm 的严格模式,核心在于理解“幽灵依赖的危害”与“类型声明解析规则”之间的平衡。随着 React 18.3+ 及 TypeScript 团队改进类型发布策略,未来此类跨工具不兼容问题将逐渐减少。在这之前,掌握上述修复方法,将是每位 React 开发者必备的生存技能。