Files
deepseek-harness/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.zh.md
T

44 lines
5.8 KiB
Markdown
Raw Normal View History

# RFC:以 pnpm 替代 Yarn 4 作为包管理器
Status: implemented
[English](2026-06-16-pnpm-over-yarn.md) | 中文
## 问题
本仓库最初使用 **Yarn 4** 搭配 `node-modules` linker 发布——这是一个刻意保守的选择:行为类似 npm 的扁平布局,同时提供 Yarn 的 workspace 和 `yarn constraints`。它能用。但 Yarn 4 的 Plug'n'Play 血统使得 `node-modules` linker 成为非主流模式,而更广泛的 JS 生态——工具默认值、CI action、Corepack 示例、贡献者熟悉度——正日益以 pnpm 为中心。对于一个主要由 agent 构建、偶尔有人类贡献者阅读的仓库来说,「大多数工具和人所预期的包管理器」具有实际价值:更少的意外、更成熟的故障路径、更多可直接复用的答案。
切换成本目前处于最低点。本仓库尚无任何包发布(所有 package 均为 `private: true`);开发/测试/演示全部通过 tsx **未构建**运行,因此包管理器只需做到 (a) 解析并链接 `node_modules`,(b) 运行 workspace 脚本,(c) 强制执行 workspace 约束。唯一的 Yarn 专属资产是 `yarn.config.cjs`(`@yarnpkg/types` 约束引擎),体量小且可机械地重新表达。这与 [tsdown 决策](2026-06-11-tsdown-over-dumble.md)的逻辑一致:趁爆炸半径还小,把承重工具换成生态更健康的选项。
## 决策
采用 **pnpm 11.7.0**,通过 `packageManager` 字段固定、经 Corepack 安装(与 Yarn 使用的机制相同):
- **Workspace** 从 `package.json` 的 `workspaces` 数组加 `.yarnrc.yml` 迁移到 `pnpm-workspace.yaml`(`vendor/*`、`packages/*`——相同的 glob;`examples/*` 保持非 workspace,与先前设置及 tsdown 的显式 glob 一致)。
- **严格符号链接 linker**(pnpm 默认)取代 Yarn 的 hoisted `node-modules` linker。我们刻意**不**添加 `node-linker=hoisted` / `shamefully-hoist` 逃生口:pnpm 的非扁平 `node_modules` 会让幽灵依赖(引用未声明的传递依赖)大声失败,这对一个以机械门禁为整体质量策略的仓库而言是一个*优点*(见[机械质量门禁](2026-06-11-quality-gates.md))。门禁套件——类型检查、lint、测试、构建、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` 范围上强制执行完全相同的不变式:所有 package `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**:零变动,但押注于使用者更少的 linker 模式和绑定单一包管理器的约束引擎。
- **npm workspaces**:无处不在,但没有约束机制,monorepo 人体工学也更弱。
- **pnpm 搭配 hoisted linker**:迁移更平滑,但放弃了幽灵依赖安全性——而这正是迁移的首要正确性理由。
## 后果
约束检查失去了 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 |
|---|---|---|
| Cold (empty cache/store, no `node_modules`) | ~14 s | ~16 s |
| Warm relink (cache/store warm, `node_modules` removed) | ~12–14 s | ~15–22 s |
| Frozen, `node_modules` present (no-op revalidate) | ~2–8 s | ~0.5–7 s |
在快速本地磁盘上,pnpm 的内容寻址 store 通常在冷/热安装上胜出,尤其在多次 checkout 的**磁盘占用**上优势明显(一个全局 store 通过硬链接进入每个 `node_modules`,而 Yarn 每个 worktree 复制约 279 MB——部分开发者日常保持约 10 个或更多 worktree)。这一去重优势在上述迁移时数据中**未**体现,因为测试 store 和 `node_modules` 位于不同文件系统,硬链接失效;在单文件系统的开发机或 CI 缓存上该优势成立。诚实的总结:在我们的 NFS 开发文件系统上,安装速度在噪声范围内不分伯仲;迁移的理由是生态对齐、幽灵依赖安全性和跨 checkout 磁盘去重——而非原始安装时间的胜出。
所有质量门禁(约束、类型检查、lint、doc-sync、100% test:coverage、构建、knip、publint、echo-agent 演示冒烟测试)在 pnpm 上原样通过,这是 linker 切换未引入幽灵依赖破坏的正确性证明。