Turborepo Monorepo 里桌面 Shell 的 beforeBuildCommand 把前端构建触发了两次
某个 pnpm + Turborepo monorepo,结构大致是 apps/www(Next.js 前端)+ apps/desktop(Tauri 桌面壳)。跑全图构建 pnpm build 时,偶发红——日志里 .next/ 写到一半就乱了,或者 next build 进程直接挂住。但如果只跑 pnpm build --filter=www,每次都稳定绿。于是开始怀疑是自己最新的改动引了这个问题……直到发现,在最早的 baseline commit 上全图构建也是红的。
现象
pnpm build(全图):偶发失败,报.next/或静态导出目录out/写入异常,有时进程直接挂死。pnpm build --filter=www(只构建前端):每次稳定通过。- 重跑一次就绿了:这让人误以为是偶发网络/环境问题,实际上是 turbo 命中了前一次的构建缓存,把真正的竞争掩盖了。
根因
apps/desktop 的 Tauri 配置里有一个 beforeBuildCommand:
// apps/desktop/src-tauri/tauri.conf.json
"build": {
"frontendDist": "../../www/out",
"beforeBuildCommand": "pnpm --filter www build"
}turbo run build 执行时,任务图会按依赖顺序先构建 www——这是一次 next build。紧接着构建 desktop,而 desktop 的 beforeBuildCommand 又会再跑一次 pnpm --filter www build。
两个 next build 进程并发写同一个 .next/ 和 out/ 目录,相互覆盖、截断,导致竞争失败。
此外,桌面壳通常需要前端以 output: 'export' 模式产出静态文件(out/),但日常开发时 next.config 未必开启这个模式,beforeBuildCommand 跑出来的根本就不是 desktop 期望的产物——这意味着即便没有竞争,桌面构建在日常全图里也是注定失败的。
第二次全图构建"绿了",大概率是 turbo 命中了 www 的构建缓存,直接跳过了重新构建,两次并发的情况没有复现。这是缓存回放,不是竞争消失。不要用"retry 通过"来判断问题是否解决——必须连跑两次无缓存验证。
正解
日常 web/库开发(不打包桌面 App):把构建门控限定到前端,用 --filter 把 desktop 排除在外:
# 稳定的三道门禁——typecheck/test 可以跑全图(desktop 没定义这两个 script,turbo 会跳过)
pnpm typecheck && pnpm test && pnpm build --filter=www--filter=www 会自动带入 www 的 workspace 依赖,不会触发 desktop,也就不会触发 beforeBuildCommand,竞争彻底消失。
根本修法(留到打包/发版阶段再做):让 desktop 通过 turbo 任务图 声明对 www#build 的依赖,而不是在 beforeBuildCommand 里重新触发:
// turbo.json(伪示意)
{
"tasks": {
"desktop#build": {
"dependsOn": ["www#build"]
}
}
}然后把 beforeBuildCommand 改成空操作或完全移除。turbo 会保证 www 先构建完成,desktop 再接着跑,不再有并发写入。
日常构建加 --filter=www 立即消除竞争;长期在 turbo.json 里用 dependsOn: ["www#build"] 替代 beforeBuildCommand,让调度器管并发而不是让两个独立进程各自"帮忙"构建同一个目录。
排查
验证时必须跑两次无缓存:
# 清缓存后连跑两次,每次都绿才算真修好
pnpm turbo run build --filter=www --force
pnpm turbo run build --filter=www --force观察构建日志中 next build 出现的次数——正常应该只有一次,且路由/页面列表完整无截断。
一句话外卖
Tauri/Electron 的
beforeBuildCommand会在 turbo 任务图之外独立触发前端构建,全图跑时必然双重写同一输出目录;--filter排除桌面包是立竿见影的止血,根治要靠dependsOn把顺序交回给调度器。