
pnpm 快速锁文件更新移除最后一个 catalog 依赖后不再残留过期条目【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm导读本文围绕 pnpm 最新一次 patch 级修复展开当通过“快速锁文件更新”fast lockfile update机制移除项目中最后一个引用某个 catalog 条目的依赖时pnpm 不再把该条目的快照残留在pnpm-lock.yaml的catalogs:段中。文章会先回顾 catalog 协议与锁文件格式再深入 Rust 实现中的依赖图维护与 catalog 剪枝逻辑最后给出测试与验证方法帮助读者理解快速更新路径的取舍边界。变更背景一次 patch 级修复在仓库的 .changeset/fast-update-single-staging.md 中记录了本次变更pnpm/installing.deps-installer: patch pacquet: patch pnpm: patch变更说明原文为Removing the last dependency that references a catalog entry via the fast lockfile update no longer leaves the stale catalog entry inpnpm-lock.yaml.它同时影响 TypeScript 生态的pnpm/installing.deps-installer、Rust 实现的pnpmpnpm/crates 目录下的 workspace crate以及pacquet说明该逻辑位于三者共享的依赖安装/锁文件维护核心中。从修复内容看这是一次行为修正而非新功能旧行为会在特定场景下产生“过期 catalog 条目”新行为将其彻底清理。前置知识catalog 协议与锁文件中的catalogs:段workspace 中如何声明 catalogcatalog目录是 pnpm 在 workspace 中集中管理依赖版本声明的机制。在pnpm-workspace.yaml中通过catalog:段声明版本范围例如仓库测试夹具 has-outdated-deps-using-catalog-protocol/pnpm-workspace.yamlpackages: - . catalog: is-negative: ^1.0.0 sharedWorkspaceLockfile: false各子项目在package.json中通过catalog:协议引用见 has-outdated-deps-using-catalog-protocol/package.json{ dependencies: { is-negative: catalog: } }其中catalog:不带名字指默认 catalog也可写成catalog:default命名 catalog 则写作catalog:name。锁文件如何记录 catalog 快照解析完成后锁文件的catalogs:段保存每个 catalog 条目的已解析快照见 has-outdated-deps-using-catalog-protocol/pnpm-lock.yamllockfileVersion: 9.0 catalogs: default: is-negative: specifier: ^1.0.0 version: 1.0.0 importers: .: dependencies: is-negative: specifier: catalog: version: 1.0.0 packages: is-negative1.0.0: resolution: {integrity: sha512-1aKMsFUc7vYQGzt//8zhkjRWPoYkajY/I5MJEvrc0pDoHXrW7n5ri8DYxhy3rRDk0QFl7GjHHsZU1sppQrWtw} engines: {node: 0.10.0} snapshots: is-negative1.0.0: {}可见 catalog 快照条目的结构为specifierworkspace 中声明的范围version实际锁定的版本它与importers中的catalog:引用一一对应。当某个catalog:引用被移除且没有其他依赖再引用它时对应快照条目便成了“过期条目”stale catalog entry。问题场景快速更新路径上被遗忘的清理“快速锁文件更新”fast lockfile update是 pnpm 在锁文件仍与 manifest 基本一致、仅发生局部增删改时的优化路径它不再执行完整的依赖解析而是直接基于现有pnpm-lock.yaml的packages:/snapshots:数据做图上的增删与重定向从而显著提速。快速更新的入口位于 install/run/wanted.rs当may_fast_update_lockfile判断可行时调用try_fast_update_lockfile传入锁文件、项目 manifest 与新鲜度输入LockfileFreshnessInputs其结果存放在lockfiles.wanted.fast_updated中供后续写入决策使用。修复前的缺陷在于快速更新会正确地剪掉packages:/snapshots:中不可达的包却可能遗漏catalogs:段的清理——当移除的是“最后一个”引用某 catalog 条目的依赖时该条目的快照会残留在锁文件中造成锁文件与真实依赖图不一致。修复实现依赖图维护的四步收尾修复的核心在 fast_update_lockfile.rs 的finish_graph_edits函数。快速更新期间各处理器移除依赖、分组移动、override 重写、catalog 重定向等只修改图并记录GraphEdits被切断的边集合DroppedEdges与optional标志是否过期真正的收尾统一在finish_graph_edits中一次性完成pub(crate) fn finish_graph_edits(candidate: mut Lockfile, edits: GraphEdits) - bool { if !edits.dropped.is_empty() { prune_unreachable_packages(candidate); if !peer_suffixes_are_independent_of(candidate, edits.dropped) { return false; } prune_unreferenced_catalog_entries(candidate); } if !edits.dropped.is_empty() || edits.optional_flags_are_stale { recompute_optional_flags(candidate); } true }收尾分四步prune_unreachable_packages以所有 importer 的直接依赖为根做 BFS 可达性分析删除不可达的snapshots:与packages:条目fast_update_lockfile.rspeer_suffixes_are_independent_of检查存活的 snapshot 键的 peer 后缀是否引用了被切断的包若引用则意味着需要重写键而非简单剪枝函数返回false调用方回退到完整解析路径fast_update_lockfile.rsprune_unreferenced_catalog_entries本次修复的核心专门清理过期 catalog 快照详见下一节recompute_optional_flags从 importer 出发按dependencies保持上下文与optionalDependencies强制进入 optional 上下文遍历重算每个 snapshot 的optional标志fast_update_lockfile.rs。其中第 3 步正是 changeset 描述的行为即使依赖图剪枝已经完成只要catalogs:段仍有未被任何 importer 引用的条目就会被此步骤清除。深入 catalog 条目剪枝引用判定与三种更新结果谁在引用这个 catalog 条目判定逻辑是catalog_entry_is_referencedfast_update_catalogs.rs遍历所有 importer 的dependencies/devDependencies/optionalDependencies用parse_catalog_protocol解析依赖的 specifier判断其是否指向指定 catalog 的指定别名。注释特别指出通过解析而非字符串比较可以正确覆盖catalog:与catalog:default两种“默认 catalog”拼写。三种更新结果在 fast_update_catalogs.rs 的retarget_catalog_entry中对锁文件里每个已记录条目给出三种结果Keptworkspace 仍声明该条目且 specifier 未变Retargetedspecifier 变化但锁定的版本仍满足新范围仅更新specifier字段、保留versionDroppedworkspace 不再声明该条目且catalog_entry_is_referenced判定无任何 importer 引用——此时删除该快照。若条目仍被引用但 workspace 已不再声明或锁定版本无法满足新范围函数返回None表示快速路径无法安全处理交由完整解析。这与剪枝的整体安全策略一致宁可回退完整解析也不产生错误的锁文件。实际的删除动作prune_unreferenced_catalog_entriesfast_update_lockfile.rs收集所有“无引用”的(catalog_name, alias)后逐个移除若某 catalog 的条目被清空则删除该 catalog若catalogs:整体为空则将该字段置为None。测试 removes_an_unreferenced_stale_snapshot 直接验证了这一点锁文件中有catalog:default → foo的快照而 importer 为空时快速更新后catalogs被清空。测试如何锁定修复行为crates/package-manager/src/fast_update_catalogs/tests.rs 中的一组测试完整定义了 catalog 快速更新的行为边界retains_a_version_that_satisfies_the_updated_range范围更新但锁定版本仍满足时保留版本、仅更新specifierrejects_an_updated_range_that_excludes_the_locked_version新范围不包含锁定版本时返回Unsupported回退完整解析rejects_a_malformed_locked_version锁文件版本非法时同样拒绝快速路径catalog_backed_overrides_do_not_disable_reuse_when_catalogs_are_unchangedoverride 依赖 catalog 但 catalog 未变化时仍可复用configured_catalogs_require_existing_lockfile_snapshots/referenced_catalog_entries_require_lockfile_snapshots缺少锁文件快照时无法走快速路径removes_an_unreferenced_stale_snapshot对应本次修复——无引用的快照条目被移除rejects_removing_a_snapshot_referenced_by_the_default_catalogcatalog:与catalog:default两种拼写下的引用均阻止删除防止误删仍在使用中的条目。这些测试同时印证了该功能遵循的“可证明安全才快速更新”原则能安全剪枝的剪枝不能证明安全的整体回退。对使用者的影响与验证方式本次修复对日常用户是透明的行为修正无需新配置项使用 catalog 协议pnpm-workspace.yaml的catalog:段 package.json中的catalog:引用的 workspace在移除某依赖后执行安装/更新若它是最后一个引用某 catalog 条目的依赖pnpm-lock.yaml的catalogs:段会同步清除对应快照与完整解析的结果保持一致若改动同时涉及 peer 后缀、override 重写或范围不再满足锁定版本等无法安全证明的场景pnpm 会自动回退到完整解析路径保证锁文件正确性可通过移除最后一个 catalog 引用依赖后运行pnpm install并检查pnpm-lock.yaml的catalogs:段来复现验证修复后不再残留specifier/version快照。小结本次 patch 修复补齐了快速锁文件更新路径上的一处收尾遗漏依赖图剪枝之后catalogs:段的过期快照同样需要清理。从 fast_update_lockfile.rs 与 fast_update_catalogs.rs 的实现可以看出快速更新路径并非盲目的增量修改而是通过“引用判定 可达性剪枝 安全回退”三件套在性能与正确性之间取得平衡——这也是 pnpm 在增量安装场景下能够同时保证速度与锁文件一致性的关键设计。【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考