git worktree + 软链 node_modules 不等于隔离:pnpm monorepo 下的幻象验证
并行开发常见场景:主工作树被其他 session 的 WIP(未提交的半成品)污染,类型报错一堆,但你想证明自己新加的代码类型检查和构建都是绿的。直觉反应是 git worktree add /tmp/verify HEAD——拿出一棵干净的 HEAD 树——然后把主仓库的 node_modules 软链进去,省掉漫长的 pnpm install。
看起来天衣无缝。然而 tsc 一跑,报出一堆 '"@scope/ui"' has no exported member 'FooShowcase'。你明明刚刚把 FooShowcase 加进源码了,怎么会"不存在"?
现象
- worktree 里执行全量 typecheck,报出大批「no exported member」,涉及你新加的组件以及其他 session 同样刚加的若干组件,都是最近新增的 export,老的都没问题。
- turbo 在这个"干净" worktree 里跑,直接命中缓存(
Cached: N cached),压根没重新构建。 - 把软链去掉,老老实实
pnpm install,报错全消失。
根因
两个渗漏叠加,缺一不可。
渗漏一:workspace 包解析绑回了主仓库。 ln -s <main>/node_modules <worktree>/node_modules 之后,软链目标里的 node_modules/@scope/ui 本身又是一条指向 ../../packages/ui 的内部软链。这条相对路径的解析基准是软链物理存放位置,也就是主仓库——于是 @scope/ui 实际指向主仓库的 packages/ui,带着其他 session 的 WIP 代码。更糟的是,消费方通过 package.json 的 "types"/"exports" 找到的是主仓库里最后一次构建出来的 dist/index.d.ts,而那份 .d.ts 早于你新加的组件——于是 FooShowcase 在类型层面真的"不存在"。这是分辨率假象,不是真实类型错误。
渗漏二:turbo 缓存共享。 node_modules/.cache/turbo 跟着软链住在主仓库,worktree 直接复用主仓库的缓存记录,把主仓库(可能污染)的上一次运行当成缓存命中跳过,隔离目的彻底落空。
pnpm workspace 的内部包是符号链接套符号链接,解析基准是物理路径,不是你当前工作树的路径。软链 node_modules 进 worktree,等于把主仓库的 source、dist、turbo cache 一并引入,worktree 只是个空壳,隔离没有发生。
正解
不要为了省时间而软链 node_modules 进 worktree。真正能证明"我的代码是干净的"有更简单的办法:
在主工作树里归因验证,而不是逃离主工作树。
# 1. 只对你的包做类型检查——它会编译你的全部源文件(含新加的)
pnpm --filter @scope/ui exec tsc --noEmit
# 2. 如果要跑全量,过滤出属于自己的错误
pnpm typecheck 2>&1 | grep "error TS" | grep -v "其他session的WIP路径"
# 3. 确认报错路径都在 git status 里显示为 ?? 或 M(不是你的文件)
git status --short如果第 1 步绿,第 2 步所有报错都指向别人的 untracked 文件,你就有充分证据说明自己的变更类型安全。不需要 worktree。
如果确实必须完全隔离构建,唯一正确做法是在 worktree 里做真实的 pnpm install,而不是软链:
git worktree add /tmp/verify HEAD
cd /tmp/verify
pnpm install # 慢,但真的隔离
pnpm typecheck验证"我的代码"是否干净,pnpm --filter <yourpkg> exec tsc --noEmit 已经足够;全量报错用路径归因到 untracked WIP 即可。非要 worktree,就做真实 install,软链是捷径陷阱。
一句话外卖
pnpm workspace 的包是链套链,软链 node_modules 进 worktree 不会给你隔离,只会给你一个带着旧 dist 和主仓库缓存的幻象——真要隔离,要么在主树归因,要么老实
pnpm install。