5.9 KiB
Agent Note: 使用 pnpm 替代 Yarn 4 作为包管理器
Status: implemented
English | 中文
问题
本仓库最初使用 Yarn 4 搭配 node-modules 链接器启动。这是一个刻意保守的选择:行为类似 npm 的扁平布局,同时享有 Yarn 的 workspaces 和 yarn constraints。它能正常工作。但 Yarn 4 源自 Plug'n'Play 的血统,使得 node-modules 链接器成为非主流模式;而更广泛的 JS 生态——工具默认值、CI action、Corepack 示例、贡献者的熟悉度——正日益以 pnpm 为中心。对于一个主要由 agent(智能体)构建、偶尔有人类贡献者阅读的仓库而言,「大多数工具和人所期望的包管理器」具有实际价值:更少的意外、更成熟的故障路径、更多可直接复用的解答。
切换成本目前处于最低点。本仓库尚无任何包(package)发布(每个包都是 private: true);开发/测试/演示全部通过 tsx 未构建运行,因此包管理器只需做到:(a)解析并链接 node_modules,(b)运行 workspace 脚本,(c)强制执行 workspace 约束。唯一的 Yarn 特有资产是 yarn.config.cjs(@yarnpkg/types 约束引擎),体量小且可机械地重新表达。这与 tsdown 决策的逻辑一致:在爆炸半径尚小时,将承重工具换为生态更健康的选项。
决策
采用 pnpm 11.7.0,通过 packageManager 字段固定版本,经 Corepack 安装(与 Yarn 使用的机制相同):
- Workspaces 从
package.json的workspaces数组 +.yarnrc.yml迁移到pnpm-workspace.yaml(vendor/*、packages/*——同样的 glob;examples/*保持非 workspace,与先前设置及 tsdown 的显式 glob 一致)。 - 严格符号链接链接器(pnpm 默认)取代 Yarn 的提升式
node-modules链接器。我们刻意不添加node-linker=hoisted/shamefully-hoist逃生口:pnpm 的非扁平node_modules会让幻影依赖(引用未声明的传递依赖)大声失败,这对于一个以机械门禁为核心质量保障的仓库(见机械质量门禁)是一项优势。门禁套件(类型检查、lint、test、build、knip)是证明不存在此类幻影导入的安全网。 - 构建脚本白名单。 pnpm 10+ 不运行依赖的生命周期脚本,除非将其加入白名单。
pnpm-workspace.yaml携带一份显式的allowBuilds映射(esbuild、lefthook、@google/genai、protobufjs)——与本仓库对模型/工具输出已有的供应链加固姿态一致,现在也应用于安装时的代码执行。peerDependencyRules.allowedVersions.typescript: '>=5 <7'消除仓库内 TypeScript 的良性 peer 范围警告。 - 约束变为包管理器无关。
yarn.config.cjs(导入@yarnpkg/types,使用Yarn.workspaces()/workspace.set())被scripts/check-workspace-constraints.ts取代——一个纯 tsx 脚本,通过pnpm run constraints运行。它在相同的vendor+packages范围上强制执行完全相同的不变式:每个包private: true;@deepseek-ai/dsh-*包将cordis同时声明为对等依赖(peer dependency)和 dev 依赖且范围一致、使用根package.json的版本、设置type: module;vendor 包仅检查 privacy。 - 所有 CI、lefthook 钩子、
package.json脚本和文档中的yarn …动词变为pnpm …/pnpm run …。yarn.lock→pnpm-lock.yaml(lockfile v9)。.gitignore将.yarn/换为.pnpm-store/。vendor README(如vendor/cordis/README.md)按 Vendoring Policy 保持其上游yarn示例不变。
曾考虑的替代方案
- 保留 Yarn 4——零变动,但押注于使用率较低的链接器模式和一个绑定单一包管理器的约束引擎。
- npm workspaces——无处不在,但没有约束方案,monorepo 开发体验也较差。
- pnpm 搭配提升式链接器——迁移更平滑,但放弃了幻影依赖安全性,而这正是迁移的核心正确性理由。
后果
约束检查失去了 Yarn 的自动修复能力(workspace.set() 能原地改写 manifest);tsx 脚本仅做检查,不通过时以非零退出码和消息退出。这是可接受的:CI 从未运行过 --fix,且需要手动编辑的情况很少。贡献者现在为 pnpm 而非 Yarn 运行 corepack enable;pnpm exec lefthook install 取代 yarn lefthook install(postinstall 钩子仍会运行 lefthook install)。
性能(迁移时在开发 NFS 文件系统上测量;单次运行样本,方差大——仅供方向性参考,非基准测试套件):
| 场景 | Yarn 4 | pnpm 11 |
|---|---|---|
冷启动(空缓存/store,无 node_modules) |
~14 s | ~16 s |
热重链接(缓存/store 已热,node_modules 已删除) |
~12–14 s | ~15–22 s |
冻结,node_modules 存在(无操作重验证) |
~2–8 s | ~0.5–7 s |
在快速本地磁盘上,pnpm 的内容寻址 store 通常在冷/热安装中胜出,尤其在多个检出之间的磁盘占用方面优势明显(一个全局 store 通过硬链接接入每个 node_modules,而 Yarn 每个 worktree 复制约 279 MB——部分开发者经常为本仓库保持约 10 个或更多 worktree)。该去重优势在上述迁移时数据中未能体现,因为测试 store 和 node_modules 位于不同文件系统,硬链接失效;在单文件系统的开发机或 CI 缓存上则适用。诚实的总结:在我们的 NFS 开发文件系统上,安装速度在噪声范围内不分伯仲;迁移的理由是生态对齐、幻影依赖安全性和跨检出磁盘去重,而非原始安装时间的胜出。
所有质量门禁(constraints、类型检查、lint、doc-sync、达到 100% 的 test:coverage、构建、knip、publint 以及已构建应用的冒烟测试)均在 pnpm 下通过,证明更换 linker 没有引入幽灵依赖故障。