ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

pnpm overrides 接管自动安装的 peer 依赖:从 changeset 到 hoistPeers 源码解析

pnpm overrides 接管自动安装的 peer 依赖:从 changeset 到 hoistPeers 源码解析 pnpm overrides 接管自动安装的 peer 依赖从 changeset 到 hoistPeers 源码解析【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm本篇围绕 changeset overrides-reach-auto-installed-peers.md 展开overrides现在也能约束 pnpm 在autoInstallPeers开启时自动安装的 peer 依赖。读完后你将理解这次行为变更要解决重复副本问题的机理能在自己的项目中复现并验证该场景并掌握从 hoistPeers 源码 到测试用例的完整实现证据链。背景pnpm 如何自动安装 peer 依赖pnpm 的autoInstallPeers默认开启会处理一个常见痛点某依赖声明了peerDependencies但没有任何项目显式声明它。此时 pnpm 不会把 peer 留空而是把它作为缺失的必需 peer补进依赖图并安装。这条自动补装路径的核心在依赖解析器中。resolveDependencies.ts 中有一段while (true)循环每一轮收集各 importer 的missingPeers交给hoistPeers计算应补装哪些依赖再递归解析直到没有新的缺失 peer。关键调用点见该文件第 416-423 行与第 466 行const _hoistPeers hoistPeers.bind(null, { autoInstallPeers: ctx.autoInstallPeers, allPreferredVersions: ctx.allPreferredVersions, workspaceRootDeps, overrideBareSpecifier: ctx.overrideBareSpecifier null ? undefined : (name, range) ctx.overrideBareSpecifier!(name, range, options.prefix), })注意这里的options.prefiximporter 所在目录被一并传入 override 函数——这为后面本地file:/link:覆盖目标按声明方目录解析埋下伏笔。旧行为的缺口overrides 只改写有人声明的依赖pnpm 的overrides传统上通过 read-package hook 生效解析每个包的 manifest 时把命中的依赖改写或移除。但自动安装的 peer没有声明它的 manifest——它是由解析器凭空补装的边read-package hook 根本没机会触及它。旧版本下这类 peer 只能按其声明方给出的 peer range 去注册表解析。后果就是 changeset 里描述的场景你本想用overrides把某个包锁死到唯一版本结果自动安装的 peer 绕过了锁按 peer range 又解析出一份旧版本项目里出现同一包的两个副本。changeset 给出的例子是# pnpm-workspace.yaml或 .npmrc 的 overrides 配置 overrides: react: npm:react19.2.0项目只依赖了lucide-react它声明react为 peer。旧行为下 pnpm 会为这个没人显式声明的 peer 安装react18.3.1变更之后安装的正是被 override 钉住的react19.2.0。该变更对应上游 issue #13320。changeset 的 frontmatter 声明了本次受影响的包及 bump 级别pnpm/hooks.read-package-hook与pnpm/installing.deps-resolver为 minorpnpm/installing.deps-installer、pacquet与pnpm为 patch。新实现DependencyOverrider 直通 hoistPeers1. 新的 override 入口createDependencyOverrider改动在 read-package-hook 包中新增了独立于 manifest 改写的入口。createVersionsOverrider.ts 定义了函数类型DependencyOverrider及其工厂函数createDependencyOverrider第 40-55 行并经 index.ts 导出/** * Resolves the specifier an override imposes on a single dependency edge, or * undefined when no override claims it. - means the edge is removed. * * Edges that have no declaring manifest — a peer pnpm auto-installs — reach * the overrides through this function instead of through the read-package * hook, so parent-scoped overrides (parentchild) never apply to them. */ export type DependencyOverrider (name: string, bareSpecifier: string, dir?: string) string | undefined源码注释明确了两点语义返回-表示该依赖边被移除返回undefined表示没有任何 override 声称它。没有声明方 manifest 的边即 pnpm 自动安装的 peer走这条新通道因此父级作用域 overrideparentchild形式天然不适用——parentchild的匹配依赖声明方的name/version见同文件pickOverridesOfParent第 131-139 行而自动安装的 peer 没有声明方。工厂函数还有一个性能短路若配置里只有parentchild形式的 override没有任何能声称未声明依赖的项直接返回undefined解析器就完全跳过逐 peer 的 override 查询。对于没有 overrides 的普通项目这是零开销。2. hoistPeers 中 override 的裁决规则hoistPeers.ts 的hoistPeers新增overrideBareSpecifier选项第 24 行裁决逻辑在第 36-44 行const rootBareSpecifier findWorkspaceRootDep(opts.workspaceRootDeps, peerName)?.normalizedBareSpecifier // An override redirects a hoist; it must never create one, or disabling // autoInstallPeers would still install a peer nobody depends on. const overridden opts.autoInstallPeers || rootBareSpecifier ? opts.overrideBareSpecifier?.(peerName, range) : undefined if (overridden ! null) { if (overridden ! -) { dependencies[peerName] overridden } continue }从源码结构看这里确立了四条规则override 只能改道不能创造。只有autoInstallPeers开启、或 workspace 根目录自己声明了该包此时即便关闭 autoInstallPeers 也会发生去重抬升时才咨询 override。若两者都不成立即使配置了 override 也不会为无人依赖的 peer 安装任何副本。override 结果优先于 preferred-versions 去重。命中后直接continue跳过后面基于allPreferredVersions的版本收敛逻辑——这正是修复第二份副本的关键去重只能收敛到图里已解析的版本如react18.3.1而 override 能把边指向钉住的react19.2.0。-即删除。override 值为-时该 peer 不被安装。未命中 override 时原有的workspace 根依赖抬升 → preferred versions 去重 → 按 peer range 解析链路保持不变。3. 接线位置安装主流程与 peer 问题检查deps-installer 在两处把 override 通道接进解析器安装主流程 install/index.ts 第 1998 行overrideBareSpecifier: createDependencyOverrider(opts.parsedOverrides, opts.lockfileDir)。peer 依赖问题检查pnpm peers等命令的分析路径getPeerDependencyIssues.ts 第 84 行做了同样接线保证诊断视图与实际安装行为一致。另外createVersionsOverrider.ts 的resolveOverriddenBareSpecifier/resolveLocalOverride第 312-322 行负责本地覆盖目标的解析file:/link:形式的 override 若用相对路径声明会相对声明该依赖的 importer 目录即传入的dir/options.prefix换算与该项目显式声明该依赖时的行为完全一致。行为边界五个测试用例划定的契约hoistPeers.test.ts 第 333-394 行集中固化了本次变更的行为契约五个用例分别对应上述四条规则用例场景期望结果hoistPeers installs an auto-installed peer at the overridden specifierL333autoInstallPeers: truepreferred versions 含react18.3.1override 指向npm:react19.2.0安装npm:react19.2.0而非去重收敛到的 18.3.1... does not let an override install a peer that nothing provides when peers are not auto-installedL348autoInstallPeers: false根目录也无 react 依赖返回{}override 不创造安装... leaves a deduplicating hoist to the graph when peers are not auto-installedL357autoInstallPeers: false但图里已有react18.3.1的 preferred version抬升去重版本18.3.1override 不介入此去重... redirects the workspace roots hoist through an override when peers are not auto-installedL372autoInstallPeers: false但 workspace 根声明了react18.3.1根目录的抬升被 override 改道为npm:react19.2.0hoistPeers leaves a peer removed by an override uninstalledL383override 值为-该 peer 不安装返回{}在 deps-installer 的安装集成测试 autoInstallPeers.ts第 737 行附近与 validatePeerDependencies.ts 中也有覆盖 overrides 与 autoInstallPeers/peer 校验联动 的安装级用例可在仓库中进一步查证端到端行为。实践要点与适用边界适用前提本变更属于当前仓库pnpm11/下的 TS 实现对应 changeset 中列出的pnpm/installing.*与pnpm/hooks.read-package-hook包。升级包含此 changeset 的 pnpm 版本后overrides对自动安装 peer 的约束自动生效无需额外配置。语义提醒parentchild形式的 override 不作用于自动安装的 peer没有声明方 manifest 可匹配需要钉这类 peer 时使用全局形式的 override如overrides: { react: npm:react19.2.0 }。override 返回-会把自动安装的 peer 一并移除关闭autoInstallPeers且根目录未声明该包时override 不会补装任何东西。验证方式可用pnpm why react或pnpm ls观察自动安装 peer 的解析结果行为边界可直接对照 hoistPeers.test.ts 的五个单测实现细节见 hoistPeers.ts 与 createVersionsOverrider.ts。【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进