pnpm 冻结锁文件安装清理不可达快照:修复--frozen-lockfile下幽灵依赖与生命周期脚本重复执行
【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm
导读
本指南聚焦 pnpm 在 Rust 重写实现(pnpm/crates与pnpm11等目录)中的一个具体修复:当项目使用pnpm install --frozen-lockfile(或--frozen-lockfile同族标志)安装依赖时,若pnpm-lock.yaml中已没有任何项目再依赖某个包,pnpm 现在会将该包从node_modules中删除。此前这个"不可达锁文件快照"会在每次运行时被重新安装,导致其生命周期脚本(install/postinstall)反复执行;同时verifyDepsBeforeRun也会在每次pnpm run/pnpm exec之前额外触发安装。读完本文,你将理解该修复的来龙去脉、它在当前仓库中的实现位置、相关的配置参数(verifyDepsBeforeRun)以及可复现的验证方法。
变更背景:问题 #14891 的修复
该变更为 pnpm 仓库中的一次补丁(changeset),对应上游 issue pnpm/pnpm#14891,其核心结论是:
pnpm install --frozen-lockfile现在会在没有任何项目再依赖某个包时,将该包从node_modules中移除。此前这种包会在每次运行时被重新安装,从而每次都重新执行其生命周期脚本。verifyDepsBeforeRun此前也会在每次pnpm run和pnpm exec之前执行安装。
changeset 文件 .changeset/fix-unreachable-lockfile-snapshots.md 以"pacquet": patch形式声明这是一个针对 pacquet(pnpm 的 Rust 实现代号)的 patch 级修复,即行为修正而非新功能。
在继续阅读源码细节之前,先明确三个关键术语:
- 不可达锁文件快照(unreachable lockfile snapshot):
pnpm-lock.yaml中记录的某个包快照,在当前所有项目(workspace 中的 importer)的依赖声明中已不再被引用。它仍然残留在锁文件里(可能是历史安装留下的),旧实现会在安装时把它重新物化到node_modules。 - 冻结锁文件安装:
--frozen-lockfile(以及--frozen-lockfile同族的--frozen-lockfile/--lockfile-only等)要求完全按照锁文件安装,不进行任何版本解析变更;CI 环境中常用--frozen-lockfile保证可复现构建。 - 生命周期脚本:包在安装时执行的
preinstall/install/postinstall脚本,反复执行它们既浪费时间,也可能带来副作用(例如下载二进制、修改全局状态)。
修复前的症状:每次安装都重新执行生命周期脚本
在修复之前,当pnpm-lock.yaml中存在某个快照、但当前没有任何项目依赖它时,pnpm install --frozen-lockfile的行为如下:
- 解析锁文件,发现"不可达"快照;
- 由于冻结模式不修改锁文件,旧实现把该快照视为"仍需安装"的依赖,把它(重新)物化到
node_modules; - 物化过程中执行该包的生命周期脚本;
- 由于每次运行都重复上述流程,每次
pnpm install --frozen-lockfile都会重复执行该包的生命周期脚本。
这带来两类实际问题:
- 重复副作用:例如二进制下载、缓存预热、环境探测类
postinstall脚本被反复执行; - 构建不确定性:CI 中出现"每次安装都做同样的事"的冗余工作,且与"冻结锁文件"所承诺的确定性语义相悖。
修复后,pnpm 会在本次安装中识别出该包已不再被任何项目依赖,直接将其从node_modules中移除,不再安装、不再执行脚本。
修复后的行为:移除不可达包,保留锁文件不变
修复后的--frozen-lockfile语义可以归纳为:
- 移除动作:不再被任何项目依赖的包,会被从
node_modules中移除(而不是重新安装); - 锁文件不变:冻结模式下依然不修改
pnpm-lock.yaml;"不可达快照"的清理是node_modules层面的动作; - 生命周期脚本不再重复:因为包不再被安装,其
install/postinstall等脚本自然不再执行; verifyDepsBeforeRun不再重复触发安装:pnpm run/pnpm exec前的依赖一致性检查(见下文)不会再因为这类"幽灵快照"而判定不同步、进而触发额外安装。
与pnpm prune的关系
需要注意的是,该修复解决的是"冻结安装过程中的清理"问题,与显式的pnpm prune(按锁文件修剪node_modules)不同:本修复发生在常规install --frozen-lockfile流程内,属于安装器自身的一致性维护,而不是一个独立的清理命令。从仓库中可以看到,安装阶段有一系列相关行为(如 merged-branch-lockfiles-drop-removed-deps.md 中"合并分支后的锁文件会丢弃已移除依赖"的 changeset),说明 pnpm 团队在持续打磨"锁文件与node_modules之间的一致性"这一主题。
仓库中的实现证据:从 changeset 到源码
Rust 实现(pacquet)中的对应代码
当前仓库的 Rust 实现中,与verifyDepsBeforeRun直接对应的源码位于:
- pnpm/crates/cli/src/cli_args/verify_deps.rs:实现了
verify_deps_before_run门控。注释明确写到:"The verify-deps-before-run gate: beforepnpm run/pnpm execexecute anything, verify thatnode_modulesis in sync with the lockfile and apply the configured action — spawn an install, prompt for one, error out, or warn. pnpm's counterpart isrunDepsStatusCheckinexec/commands."(即 pnpm 的 TS 实现对应exec/commands中的runDepsStatusCheck); - pnpm/crates/package-manager/src/optimistic_repeat_install/deps_status.rs:实现了
check_deps_status_before_run这一"预运行依赖状态检查",它复用了安装快速路径(optimistic repeat install)的新鲜度检查,但做了差异化处理; - pnpm/crates/cli/tests/suite/verify_deps_before_run.rs:端到端测试,覆盖"默认
install动作会在脚本执行前先安装"、"生产模式安装(install --prod)被正确复现"等场景。
deps_status.rs中RunDepsStatus枚举的三种结果值得展开:
pub enum RunDepsStatus { UpToDate, // 一切同步,脚本直接执行 SkippedPnp, // node-linker=pnp 下无法检查,警告后直接运行 Outdated { issue: String, install_args: Vec<String> }, // 检测到漂移 }当Outdated时,install_args_from_state会根据上次安装记录的依赖组(--prod/--dev/--no-optional)重建pnpm install参数,保证补装的安装与项目上次的安装方式一致——这正是"修复后不会因为不可达快照而重复安装"的关键:门控判定同步后,直接放行脚本执行。
冻结锁文件在 CLI 层的接线
--frozen-lockfile标志的解析与分发位于:
- pnpm/crates/cli/src/cli_args/install.rs:安装命令参数入口;
- pnpm/crates/cli/src/cli_args/install/arguments.rs:具体参数解析;
- pnpm/crates/cli/tests/suite/ci_frozen_lockfile.rs:面向 CI 冻结锁文件场景的端到端测试。
这些文件共同保证了--frozen-lockfile在解析、分发、以及 CI 场景下的行为一致性。
安装快速路径:OptimisticRepeatInstallCheck
verifyDepsBeforeRun之所以能"快速判定同步",是因为它复用了安装器的乐观重复安装检查(optimistic repeat install):
- pnpm/crates/package-manager/src/optimistic_repeat_install.rs 定义了
OptimisticRepeatInstallCheck与check_optimistic_repeat_install,当一切未变化时安装直接输出 "Already up to date"; deps_status.rs中的check_deps_status_before_run是它的"预运行孪生":它无论optimisticRepeatInstall配置如何都会运行、从不把本地 file 依赖视为过期、忽略dev/optional/production漂移(脚本始终以默认组执行),但会比对配置依赖,并以面向用户的措辞报告漂移。
从源码结构可以推断:修复后,"不可达锁文件快照"不会再让上述检查判定为 Outdated,因而不会再在每次pnpm run/pnpm exec前触发额外安装,这与 changeset 的表述完全一致。
配置项:verifyDepsBeforeRun
该变更同时把verifyDepsBeforeRun的行为纳入修复范围。该配置控制pnpm run/pnpm exec之前是否检查node_modules与锁文件的同步状态,以及不同步时如何处置。可用取值与行为如下(依据 pnpm/crates/cli/src/cli_args/verify_deps.rs 的match分支):
| 取值 | 行为 | 备注 |
|---|---|---|
install(默认) | 自动派生一次pnpm install,然后再执行脚本 | 派生安装带--verify-deps-before-run-install --use-stderr,并沿用--reporter设置;失败时透传子进程退出码 |
prompt | 交互式询问用户是否执行安装 | 非交互环境(stdin 非 TTY)下报ERR_PNPM_VERIFY_DEPS_BEFORE_RUN/CannotPrompt错误;用户中断(Esc / Ctrl-C)时以退出码 1 退出 |
error | 直接报错,提示运行pnpm install | 错误码ERR_PNPM_VERIFY_DEPS_BEFORE_RUN,帮助信息为 Run "pnpm install" |
warn | 打印告警后继续执行脚本 | 告警文本形如 "Your node_modules are out of sync with your lockfile. ..." |
true/false | 布尔值:true只做检查不采取动作;false直接跳过检查 | 与node-linker=pnp组合时,检查被跳过并输出 "verify-deps-before-run does not work with node-linker=pnp" 告警 |
如何配置
在pnpm-workspace.yaml中设置:
# pnpm-workspace.yaml verifyDepsBeforeRun: install # 或 prompt / error / warn / true / false或在命令行直接传递:
pnpm config set verify-deps-before-run install pnpm run --verify-deps-before-run=prompt build端到端测试如何验证该行为
仓库中提供了直接可读的测试佐证:
- pnpm/crates/cli/tests/suite/verify_deps_before_run.rs:文件头注释说明其镜像了
pnpm11/pnpm/test/verifyDepsBeforeRun/下的 TypeScript 场景。其中:default_install_action_installs_before_running_the_script:验证默认install动作下,新项目的首次run会先派生安装、再执行脚本(断言 marker 文件存在且node_modules已生成);install_action_reruns_a_production_only_install:验证派生安装会复现上次记录的依赖组(如install --prod),从而保证生产模式安装下pnpm run依然可用;
- 对应的 TypeScript 侧场景位于
pnpm11/pnpm/test/verifyDepsBeforeRun/目录。
这些测试从"行为契约"层面锁定了本 changeset 的修复语义:预运行检查必须与锁文件/node_modules的真实状态一致,且不能因为不可达快照而误判。
复现与验证指南
验证"不可达快照不再重复安装"
- 准备一个项目,安装一个包(例如
is-number),生成pnpm-lock.yaml; - 从
package.json移除该依赖,但保留锁文件中对应快照不动(例如用pnpm install生成锁文件后手动编辑package.json并在冻结模式下安装); - 运行
pnpm install --frozen-lockfile,观察行为:- 修复前:该包的快照仍会被重新物化,
install/postinstall脚本每次都会执行; - 修复后:该包被从
node_modules移除,锁文件不变,且不再执行其生命周期脚本。
- 修复前:该包的快照仍会被重新物化,
验证verifyDepsBeforeRun
# 在锁文件与 node_modules 不同步时运行脚本 pnpm run --verify-deps-before-run=warn build # 预期:打印告警后仍执行脚本 pnpm run --verify-deps-before-run=error build # 预期:报 ERR_PNPM_VERIFY_DEPS_BEFORE_RUN pnpm run --verify-deps-before-run=install build # 预期:先派生安装,再执行脚本注意:非交互终端下使用prompt会失败(无法弹确认框),应改用warn/error/install。
相关 changeset 与后续演进
本修复并非孤立变更,仓库.changeset/目录中的一系列相关条目共同勾勒出"锁文件 /node_modules一致性"的演进脉络:
- drop-removed-dependencies-without-resolving.md:不解析直接丢弃已移除依赖;
- merged-branch-lockfiles-drop-removed-deps.md:合并分支后的锁文件会丢弃已移除依赖;
- unused-patch-after-removal.md:依赖被移除后清理不再使用的 patch;
- frozen-lockfile-package-manager-repair.md 与 frozen-lockfile-package-manager-deps.md:冻结锁文件场景下包管理器自身依赖的修复。
小结
本次 patch 修复回答了一个长期困扰 CI 用户的问题:--frozen-lockfile本应保证"只按锁文件安装",却因为不可达快照的存在,每次运行都重复安装一个已无引用的包并反复执行其生命周期脚本。修复后 pnpm 会在安装阶段直接清理这类包,同时让verifyDepsBeforeRun不再因此误判同步状态、在每次pnpm run/pnpm exec前重复安装。其实现证据分布在 pnpm/crates/cli/src/cli_args/verify_deps.rs、pnpm/crates/package-manager/src/optimistic_repeat_install/deps_status.rs 以及对应的端到端测试 pnpm/crates/cli/tests/suite/verify_deps_before_run.rs 中,读者可沿着这些路径进一步阅读源码细节。
【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考