返回博客

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,而 desktopbeforeBuildCommand 又会再跑一次 pnpm --filter www build

两个 next build 进程并发写同一个 .next/out/ 目录,相互覆盖、截断,导致竞争失败。

此外,桌面壳通常需要前端以 output: 'export' 模式产出静态文件(out/),但日常开发时 next.config 未必开启这个模式,beforeBuildCommand 跑出来的根本就不是 desktop 期望的产物——这意味着即便没有竞争,桌面构建在日常全图里也是注定失败的。

坑:重跑变绿不代表修好了

第二次全图构建"绿了",大概率是 turbo 命中了 www 的构建缓存,直接跳过了重新构建,两次并发的情况没有复现。这是缓存回放,不是竞争消失。不要用"retry 通过"来判断问题是否解决——必须连跑两次无缓存验证。

正解

日常 web/库开发(不打包桌面 App):把构建门控限定到前端,用 --filterdesktop 排除在外:

# 稳定的三道门禁——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 排除 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 把顺序交回给调度器。