单仓里同时放前端和服务端时,构建命令没有唯一正确答案,先看依赖方向:前端与服务端互不引用,就是两个独立入口,写两条命令或用串行脚本串起来;共享包被两边引用,共享包必须先构建一次,再构建前端和服务端。判断依据是仓库内的 import 引用关系,以及构建产物里能否找到共享模块,而不是哪条命令看起来更短。
适用场景:monorepo 内前端、服务端、共享包共存,构建脚本只写了一条或干脆没写。操作动作:先列目录和依赖方向,再确认共享代码是打包进产物还是独立发布,然后写分步命令、逐条验证产物,最后收敛成一条串行且出错即中断的组合命令。验证方式:每步产物路径存在、共享模块能在产物里被找到、全量清理后重建产物时间戳与文件数量都变化。风险边界:共享包若被外部以包名引用,跳过单独构建会在运行时报模块找不到;串行组合命令比并行慢,但顺序正确优先。
在仓库里标出各个包,以及它们之间的依赖方向
先看目录,不要先看脚本。典型的 Void 单仓结构大致是这样,实际名称以仓库为准:
repo/
├── package.json # 根,只放脚本和 workspace 配置
├── packages/
│ ├── shared/ # 共享类型、工具函数、常量
│ ├── web/ # 前端
│ └── server/ # 服务端
└── pnpm-workspace.yaml # 或 package.json 里的 workspaces 字段
依赖方向靠两处确认:各子包的 dependencies 里有没有写对方包名,源码里有没有跨包 import。通常会出现的情况是 web 和 server 都依赖 shared,而 web 与 server 之间互不引用。被其他包引用的包就是共享包,它在依赖图上位于上游,构建顺序必须排在引用它的包之前。
把结论写成一句顺序关系再动手:shared → web、shared → server,web 与 server 之间无先后。很多构建失败不是命令写错,而是没意识到 shared 必须先产出可被引用的东西。
确认共享代码是打包进各自产物,还是作为独立包被引用
这一步决定共享包要不要单独构建一次。看引用写法:
// 方式一:按包名引用,shared 需要先构建出 dist 或类型声明
import { formatDate } from '@repo/shared';
// 方式二:相对路径直接引用源码,由前端/服务端打包器一起编译
import { formatDate } from '../../shared/src/date';
按包名引用时,shared 的 package.json 里 main/module/types 通常指向 dist,那么不先构建 shared,前端和服务端的构建会因找不到入口或类型而失败。相对路径引用源码时,共享代码会被各自的打包器分别编进产物,shared 不必单独构建,但要注意两边打包配置对它是否一致。
产物检查很直接:构建完前端后,在前端产物目录里搜索共享包里一个特征明显的函数名或字符串常量。能搜到,说明共享代码已被打包进去;搜不到却运行时正常,说明它是以外部包形式被引用,那就必须确保该包在运行环境里可解析。两种结论对应两种构建命令写法,不能混着猜。
写出分步执行的命令序列并逐条验证产物
先保证每一步单独能跑通,再谈合并。命令控制在三条以内,顺序与依赖方向一致:
pnpm `--filter` shared build—— 预期产物packages/shared/dist/,进去看是否有编译后的 js 与类型声明文件。pnpm `--filter` web build—— 预期产物通常是packages/web/dist/,检查入口 html 与静态资源是否齐全。pnpm `--filter` server build—— 预期产物通常是packages/server/dist/或等价的服务端输出目录。
失败时的判断依据要具体:如果第 2 步报模块找不到且指向共享包名,先回退检查第 1 步产物是否真的生成、包名与路径是否对得上;如果第 1 步就报错,问题在共享包自身配置,不要继续往下跑。每一步的验证方式是产物路径存在、关键文件存在,不是终端最后一行显示成功就结束。
把分步命令收敛成一条组合命令
分步验证通过后,把它写成脚本,减少手工顺序错误。根 package.json 里可以这样写:
{
"scripts": {
"build": "pnpm `--filter` shared build && pnpm `--filter` web build && pnpm `--filter` server build"
}
}
关键在 &&:前一步非零退出就中断,不会拿旧的或不完整的共享包产物去构建前端。不要写成 & 或并行跑三个 `--filter`,有依赖关系的步骤并行执行时,前端可能在共享包还没产出时就开始编译,结果时对时错,排查成本很高。只有确认 web 与 server 互不依赖且都只依赖已完成的 shared 时,才考虑把后两步并行;保守做法是先全程串行,稳定后再评估。
做一次全量重建,确认没有残留旧产物
旧产物会掩盖构建错误:新命令其实没产出正确文件,但目录里躺着上次的文件,命令看起来是成功的。先清理,再重建:
rm -rf packages/shared/dist packages/web/dist packages/server/dist
pnpm build
重建后对比两件事:目录里是否重新出现了预期的产物文件,以及文件修改时间是否更新到本次构建时刻。文件数量与结构也应与清理前大体一致,若某个目录明显缺失文件,说明对应包的构建没有真正执行或输出路径配置不一致。若仓库有统一的清理脚本或构建工具自带的 clean 参数,用仓库既有的方式更稳妥,避免误删源码目录。
做完这一轮,构建命令该写哪条就有了可验证的答案:依赖方向决定顺序,共享包引用方式决定要不要多一道构建,组合命令用串行加 && 保证顺序,全量重建用来确认结论不是旧文件撑出来的假象。