症状
这种失败只光顾做干净安装的环境:Error: Cannot find module ‘lodash’,而开发者的笔记本上一直正常,因为他们的 node_modules 是增量演化的,提升上来的副本还在。对比两侧的 require.resolve 结果可以把差异落到实处:笔记本上路径指向根 node_modules(提升来的、借用的),服务器上直接抛 MODULE_NOT_FOUND。npm ls lodash 在笔记本上显示它嵌套在别的包下面,就是应用从未拥有过它的凭证。
诊断思路
第一步在生产与本地分别执行 require.resolve 对比解析路径,路径钻进其他包的 node_modules 即证实依赖提升;第二步用 npx depcheck 机械性枚举代码里导入了但 package.json 没声明的包,每一条都是等待锁文件变化的定时炸弹;第三步用 npm ls 查看真实树形,UNMET 或仅嵌套出现的输出说明该依赖是被借用的。干净安装、锁文件重新生成或切换到 pnpm 严格布局,都会改变提升结果,从而触发故障。
根因
代码导入了一个从未在 dependencies 里声明的包,解析之所以成功,只是因为 npm 把某个传递依赖的副本提升到了 node_modules 根部。干净安装或更换更严格的包管理器后布局改变,这条侥幸链断裂。
修复与验证
先全量枚举欠账,再一次声明齐:用 depcheck 找出所有缺失项,npm install 显式版本写入 dependencies。在 CI 中加入与生产完全一致的安装方式(npm ci –omit=dev)并实际启动应用做健康检查——这是唯一能复现提升运气的测试。想让保证结构化的团队可以直接切 pnpm,它默认拒绝未声明导入,把这类事故降级为构建错误。验证时删除 node_modules 从干净缓存安装并启动,应用应零报错;再重新生成一次锁文件重复冷启动,证明解析结果不再取决于树形布局。只修当前报错的那一个模块会给其余幽灵依赖留下引信,务必同一次变更里全部声明。
代码示例
node:internal/modules/cjs/loader:1080
throw err;
^
Error: Cannot find module 'lodash'
# laptop: node_modules/lodash (hoisted, borrowed)
# server: throws MODULE_NOT_FOUND
npx depcheck
npm install lodash@^4.17.21
- run: npm ci --omit=dev
- run: node server.js & sleep 5 && curl -f localhost:3000/health