359MB 的巨型压缩包与午夜报错 Cannot find module
把十个站点合进 pnpm Monorepo 之后,开发是爽了,但往海外云服务器部署的时候却给我们结结实实上了一课。
Next.js 虽然提供了 output: 'standalone' 模式,但在 pnpm workspace 的软链接(symlink)结构下,它打出来的原始产物有两大毛病:
第一是死沉。因为顺着工作区把一堆不相关的开发依赖、SourceMap(.map)、TypeScript 类型文件(.d.ts)甚至好几个不同操作系统的 Prisma 二进制引擎全拷进去了,单站压缩包动辄几百兆(当时实测原始产物高达 359MB),跨国传一次包慢得让人抓狂。
第二是软链接炸雷。有好几次手工或者脚本用 tar 打包上传时漏加了 -h(跟随软链接)参数,结果传到服务器上解压出来全是指向本地开发机路径的死链接,Docker 容器一重启就狂报 Error: Cannot find module 当场瘫痪。当时为了救火,甚至干出过连进生产容器里手动 pnpm install 补传碎文件的蠢事,越补越乱。
7月24日:写下熔断铁律,诞生 flatten-standalone.js 与 deploy-app.js
痛定思痛,我在 2026 年 7 月 24 日 先写了一份详尽的《standalone 部署卡点分析报告》,然后动手把整条打包发布链路彻底重写成了两个核心脚本:
scripts/flatten-standalone.js(构建后摊平与清洗):挂在每个应用的postbuild钩子上。它会自动把静态资源、公用文件、生成的 Prisma 客户端与入口server.jsshim 组装到位,把多层嵌套的 pnpm 软链接摊平处理;scripts/deploy-app.js(唯一标准部署入口):强制使用tar -chzf跟随软链接打包,默认开启瘦身过滤(自动剔除*.map、*.d.ts、Next.js 文档以及非 Linux 平台的 Prisma 引擎),算出release.sha256指纹。如果远端服务器上的指纹跟本次一致,直接跳过上传;解压切换时自动保留远端的.env、log和ssl目录,重启容器后自动抓取日志做健康检查,一旦发现Cannot find module立即熔断报警。
后来我们更是立下硬规矩:严禁任何人在服务器上手动装依赖补碎文件,且生产部署统一收归 GitHub Actions(deploy.yml)在云端 Runner 里标准执行。
10月24日(PR #288):按依赖闭包再砍一刀
到了 2026 年 10 月 24 日(PR #288),我们发现当仓库里新增了带重型依赖的包(比如带图形库或大 SDK 的模块)时,哪怕像 sso 这样轻量的站点没引用它,也会被 standalone 连带打包进去。于是我又给 flatten-standalone.js 加上了基于 package.json 依赖闭包的精确裁剪逻辑——只保留当前站点真正用得到的工作区包。这一刀下去,直接把 SSO 站点的解压后体积从 685MB 压到了 289MB,上传和重启速度直接起飞。