依赖管理困境:pnpm 的硬链接与幽灵依赖
揭开 node_modules 的底层机制:pnpm 如何用硬链接与符号链接同时实现省磁盘与防幽灵依赖。
「为什么我的 node_modules 里没有直接依赖的包,代码却还能 import 成功?」这类「幽灵依赖」问题是 npm 扁平化结构的顽疾。pnpm 给出了优雅的答案,但它的机制常被误解。
npm 的扁平化困境
npm 为了减少重复,把依赖扁平提升到 node_modules 顶层。这带来一个副作用:即使你没有声明 package A,只要某个依赖声明了它,你也能 import A——这就是幽灵依赖。
更麻烦的是,扁平化是非确定性的:不同安装顺序可能产生不同的提升结果,导致「我本地能跑,CI 上却报错」。
pnpm 的结构:硬链接 + 符号链接
pnpm 不做扁平提升,而是用两层结构:
- 全局 store:所有包的真实文件统一存在
~/.pnpm-store,各项目通过硬链接引用,磁盘只存一份; - 符号链接:
node_modules里每个包是一个指向 store 的符号链接,且包自己的依赖也通过嵌套的符号链接组织。
node_modules/
├── .pnpm/ # 真实存储(硬链接到全局 store)
│ └── react@18/
└── react -> .pnpm/react@18/node_modules/react
严格模式带来的好处
由于每个包的依赖被隔离在自己的目录里,import 只能解析到 package.json 里声明过的依赖。幽灵依赖从根上被杜绝,这也倒逼你把所有直接依赖写清楚。
常见坑与应对
- 某些工具依赖幽灵依赖:老项目迁移时,用
pnpm的shamefully-hoist临时兼容,但别长期依赖; - 硬链接在跨分区/Docker 卷上失效:store 与项目不在同一文件系统时,pnpm 会退回复制,注意挂载点;
- 全局 store 损坏:可用
pnpm store prune清理孤儿包。
结语
pnpm 的价值不只是快和省磁盘,更在于它用结构设计强制了依赖的正确性。理解它的机制,你会在遇到「依赖玄学」时多一份从容。