pnpm --frozen-lockfile 失败 fallback install 的时间漂移雪崩
某客户机 Win Server 上 BPM 流程表单点保存炸,根因不在表单库本身,在构建链路允许 frozen-lockfile 失败时静默回退到无锁 pnpm install。通用,不限于具体库。
故障表现
客户机装出来三个跨包套件跨 minor 错配,BPM 表单点保存控制台两条 Uncaught (in promise) 异常,无 POST 请求。
另一台 Linux 构建机上同 fork 同 master 同 pnpm 8.15.9 装出来是上一周的版本组合,实测保存正常。
同样 pnpm 版本装出不同精确版本——单看 pnpm 没问题,问题在输入。
根因链(4 环)
1. package.json 顶层 ^x.y.z 范围依赖跨包套件
"@scope/designer": "^3.2.6",
"@scope/element-ui": "^3.2.11"^ 是 npm 生态默认,本身不是 bug——约定是”package.json 写范围 + lock 锁精确”。
2. 上游 fork 带来的 lock 是 pnpm 9.x 写的(lockfileVersion 9.0)
上游开发者机器用什么 pnpm,push 出来的 lock 就什么版本。本身不是 bug——上游有上游的环境约定。
3. 构建机 pnpm 钉死在 8.15.9(lockfileVersion 6.0)
win-fetch-runtime.ps1 硬编码下载 pnpm-win-x64.exe v8.15.9。pnpm 8.x 看不懂 lockfileVersion 9.0 → --frozen-lockfile 必然失败。
这一环开始失稳——但还能救:让构建在这里停下来报错就行。
4. win-build.ps1 静默 fallback 到 pnpm install(关键雷管)
Invoke-Native { & $PNPM_EXE install --frozen-lockfile }
if ($LASTEXITCODE -ne 0) {
Info 'frozen-lockfile 失败,回退到普通 install' # ← 这就是雷管
Invoke-Native { & $PNPM_EXE install }
if ($LASTEXITCODE -ne 0) { Die "pnpm install 失败" }
}失败回退到无 lock 约束的 install,pnpm 联网问 npm registry”当下满足 ^3.2.6 的最新是啥”——时间敏感:
| 时间点 | npm registry 当下回答 |
|---|---|
| 两周前(首装) | designer 3.4.0、element-ui 3.2.41、wangeditor 3.2.14 |
| 本周(再装) | designer 3.5.0、element-ui 3.3.1、wangeditor 3.3.0 |
上游作者在两个日期之间发了一波升级。同一台机器同一个 pnpm 同一个 package.json,装出不同精确版本——时间漂移就这么进来的。
首装那次意外没炸:designer 3.4.0 在自己 package.json 内部把 element-ui 钉死成 3.2.41(精确,不是范围),顶层 ^3.2.11 也解析到 3.2.41——意外达成一致,没出双版本。
再装那次炸:designer 3.5.0 内部钉 element-ui 3.3.1,顶层 ^3.2.11 独立解析可能解到 3.2.x 最新(不是 3.3.1)——顶层有一份 3.2.x、designer 拿一份 3.3.1,同名两版本,运行时撞结构差异抛错。
关键证据(诊断现场用)
node_modules/.modules.yaml顶部记录packageManager: pnpm@8.15.9+ 文件写入时间 → 反推哪次 install 干的、用什么 pnpm- 工作区
pnpm-lock.yaml头一行lockfileVersion: '6.0'vs git HEAD 的'9.0'→ 工作区 lock 是 install 重写的(否则不会变) - 产物里
grep -arhoE "@scope/[a-z-]+ v[0-9]+\.[0-9]+\.[0-9]+"可以从 bundle banner 抓到精确版本
修法(4 层防线,缺一不可)
| 层 | 文件 / 字段 | 作用 |
|---|---|---|
| 1. pnpm 版本绑定 | package.json packageManager: "pnpm@8.15.9+sha512..." | corepack 强制拉对应 pnpm,lock 永远跟 pnpm 兼容 |
| 2. lock 锁精确依赖 | pnpm-lock.yaml 与 packageManager 同 major | 保证 install 锁定精确版本 |
| 3. frozen 失败 fail-fast | 构建脚本(win-build.ps1) | lock 不可用时直接 exit,不允许静默 fallback |
| 4. 跨包套件顶层精确化 | package.json 去掉跨包套件的 ^ | 多一层防御,即使 1-3 都失守,套件 minor 不会独立漂 |
实操片段
{
"packageManager": "pnpm@8.15.9+sha512.499434c9d8fdd1a2794ebf4552b3b25c0a633abcee5bb15e7b5de90f32f47b513aca98cd5cfd001c31f0db454bc3804edccd578501e4ca293a6816166bbd9f81"
}sha512 后缀让 corepack 验证完整性。corepack enable 环境(Node 16.13+ 默认带)读这字段会自动下载并切换到指定 pnpm。
构建脚本 fail-fast 模板(PowerShell):
Invoke-Native { & $PNPM_EXE install --frozen-lockfile }
if ($LASTEXITCODE -ne 0) {
$pnpmVer = Invoke-Native { (& $PNPM_EXE -v 2>&1 | Out-String).Trim() }
$lockVer = (Get-Content "$FrontendRepo\pnpm-lock.yaml" -TotalCount 1).Trim()
Die @"
frozen-lockfile 失败. pnpm=$pnpmVer lock=$lockVer
拒绝 fallback 到无锁 install. 排查:
1. lockfileVersion 与 pnpm 大版本不匹配 -> 改 packageManager 字段拉对应 pnpm
2. lock 没重新生成 -> 联网开发机跑 pnpm install 重生成
3. 私有 registry / 网络不通
"@
}shell 版同理:set -e + 不写 fallback 分支即可。
反模式 1:全 pin
诱惑:既然 ^ 会漂,全 pin 不就稳了?
不要:
- npm 生态约定是”package.json 写范围 + lock 锁精确”,改约定等于跟生态对着干
- 每次升级都得改 package.json,即使是 patch 修复
- 真正的问题不在
^本身,在”lock 不生效时^才被求值”那条路径——堵了路径,^永远不被求值,跟 pin 等效
只对”跨包套件顶层”做精确化(兄弟包内部钉死版本的那种),作为多一层防御。不必全 pin。
反模式 2:只 pin 不堵 fallback
诱惑:lock 已经锁了 designer:3.4.0,装出来应该是 3.4.0 啊?
只有 --frozen-lockfile 成功时才严格按 lock 装。一旦走 pnpm install(无 frozen),pnpm 优先满足 package.json 顶层 spec,lock 只是 hint,会被改写。
所以 lock 锁的版本只有在”frozen 永远生效”时才管用。第 3 层 fail-fast 就是保这个。没第 3 层,第 2 层等于摆设。
反模式 3:遇到 frozen 失败的项目就 fallback
很多 yarn / pnpm 教程 / CI 模板写”frozen 失败 → install 兜底”,看起来稳健,实际是把”构建可重现性”换成”构建一定能跑完”。
构建一定能跑完不是目标,构建装出来跟你 6 个月前装出来字节级一致才是目标。fallback 静默换掉版本,产物再也不可复现——这就是这次 BPM 保存事故的根因。