pnpm 冻结锁文件安装清理不可达快照:修复 `--frozen-lockfile` 下幽灵依赖与生命周期脚本重复执行
2026/9/19 20:08:13 网站建设 项目流程

pnpm 冻结锁文件安装清理不可达快照:修复--frozen-lockfile下幽灵依赖与生命周期脚本重复执行

【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm

导读

本指南聚焦 pnpm 在 Rust 重写实现(pnpm/cratespnpm11等目录)中的一个具体修复:当项目使用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 runpnpm 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的行为如下:

  1. 解析锁文件,发现"不可达"快照;
  2. 由于冻结模式不修改锁文件,旧实现把该快照视为"仍需安装"的依赖,把它(重新)物化到node_modules
  3. 物化过程中执行该包的生命周期脚本;
  4. 由于每次运行都重复上述流程,每次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.rsRunDepsStatus枚举的三种结果值得展开:

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 定义了OptimisticRepeatInstallCheckcheck_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的真实状态一致,且不能因为不可达快照而误判。

复现与验证指南

验证"不可达快照不再重复安装"

  1. 准备一个项目,安装一个包(例如is-number),生成pnpm-lock.yaml
  2. package.json移除该依赖,但保留锁文件中对应快照不动(例如用pnpm install生成锁文件后手动编辑package.json并在冻结模式下安装);
  3. 运行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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询