场景:react-select 有个字体 bug
假设 react-select@5.8.0 的某个 CSS 变量写错了,上游修复还在 review——你需要现在上线。
Step 1:创建临时编辑目录
pnpm patch react-select@5.8.0 # pnpm 会把包解压到临时目录,并打印出路径 # You can now edit the following folder: /private/var/.../node_modules/react-select
这是"可编辑的工作副本",改里面的文件不影响 store,也不影响其它项目。
Step 2:编辑
code /private/var/.../node_modules/react-select # 改 src/something.css 里错的变量名
Step 3:提交补丁
pnpm patch-commit /private/var/.../node_modules/react-select # pnpm 做 diff,生成 patches/react-select@5.8.0.patch # 并修改 package.json:
{
"pnpm": {
"patchedDependencies": {
"react-select@5.8.0": "patches/react-select@5.8.0.patch"
}
}
}
Step 4:提交到 git
git add patches/react-select@5.8.0.patch package.json pnpm-lock.yaml git commit -m "patch: fix react-select font var bug (#1234)"
队友 pnpm install 时自动应用补丁——完全无感接手。
patch 文件长什么样
diff --git a/dist/index-CMo6LJVK.cjs b/dist/index-CMo6LJVK.cjs index a1b2c3d..e4f5g6h 100644 --- a/dist/index-CMo6LJVK.cjs +++ b/dist/index-CMo6LJVK.cjs @@ -142,7 +142,7 @@ const Control = styled.div` - font-family: var(--font-familly); + font-family: var(--font-family); `;
标准 unified diff,看得懂 git 的人都能 review。
patches 目录进 git
把
把
patches/ 目录提交到仓库——这是"这个项目用了哪些补丁"的唯一来源。CI、队友、新人 clone 后一次 install 就能自动应用。
为什么不直接改 node_modules?
别这么干:
- pnpm 用硬链接,改 node_modules 其实在改 store——其它项目一起中招
- 下次
pnpm install --force或 store prune 后丢失 - 没有 diff 记录,新人不知道哪里被改过
pnpm patch 解决了全部——改动集中、可审计、再跑 install 自动回放。
Step 5:升级时怎么迁
# 上游 5.9.0 可能已经原生修了 pnpm up react-select # pnpm 会警告:patched 的是 5.8.0,现在是 5.9.0,补丁可能不再适用
# 两种策略: # A) 上游修了 → 删补丁 rm patches/react-select@5.8.0.patch # 删 package.json 里的 patchedDependencies # B) 上游还没修 → 重新打补丁 pnpm patch react-select@5.9.0 # 把老补丁的修改在新版本上重做,再 patch-commit
patch 和 lockfile 的关系
# pnpm-lock.yaml
packages:
react-select@5.8.0:
resolution: { integrity: sha512-xxx... }
react-select@5.8.0(patch_hash=abc123...):
resolution:
integrity: sha512-yyy...
patchSourceHash: sha512-zzz...
patched 版本在 lockfile 里有独立条目,带 patch_hash——补丁变了,hash 就变,install 会重新应用并报错提示。
allowNonAppliedPatches(慎用)
{
"pnpm": {
"allowNonAppliedPatches": true
}
}
默认 false——patch 无法应用时 install 会报错。改 true 后允许部分/全部补丁应用失败,日志警告——只在迁移期间临时打开。
给子依赖打补丁
# 比如 react-select 内部用的 react-virtualized 有 bug pnpm patch react-virtualized@9.22.5 # 即使你没在 package.json 里直接声明它,也能打补丁
补丁作用于 store 里那个具体版本——所有从 store 链接到这个版本的路径都会看到。
Monorepo 里的 patch
# pnpm-workspace.yaml packages: - 'packages/*' - 'apps/*' overrides: # 强制所有子包用某个版本 lodash: ^4.17.21 patchedDependencies: # v9 起 patch 也可放 workspace.yaml react-select@5.8.0: patches/react-select@5.8.0.patch
Monorepo 里把 patchedDependencies 提到 workspace.yaml,所有子包共享同一个补丁版本。
overrides:强制某个版本
# pnpm-workspace.yaml 或 package.json 的 pnpm.overrides overrides: lodash: ^4.17.21 # 所有 lodash 必须是 4.17.21+ '@babel/core>chalk': ^5.0.0 # @babel/core 内部的 chalk 强制 5 react: 18.3.1 # 不管谁声明什么版本,用 18.3.1
overrides vs patch 的边界
overrides 是「换一个版本」;patch 是「基于这个版本改代码」。能用 overrides 换掉的问题优先 overrides(可以
overrides 是「换一个版本」;patch 是「基于这个版本改代码」。能用 overrides 换掉的问题优先 overrides(可以
npm-force-resolutions 那样)——patch 留给必须改代码的场景。
vs patch-package
| patch-package | pnpm patch | |
|---|---|---|
| 依赖 | 第三方 npm 包 | pnpm 内置 |
| 钩子 | postinstall 手动跑 | install 自动 |
| lockfile 集成 | ❌ 无,补丁变了仍装成功 | ✅ 补丁 hash 进 lockfile |
| 子依赖 | ✅ 支持 | ✅ 支持 |
| monorepo | 要每个包各自配 | workspace 集中 |
从 patch-package 迁移:pnpm patch 重新做一次即可,把 patches/*.patch 的内容 apply 到解压目录就能复用。
常见使用场景
修 bug 等待上游合并
典型场景——提 PR + 本地 patch 同步,合并后删补丁。
关掉某个 noisy 的 console.log
第三方包在生产疯狂 log,作者不配合——patch 删掉那行。
兼容性 shim
老库用了 Node 18 才有的 API,你的环境 Node 16——patch 换成 polyfill。
安全热修
CVE 爆了、作者没发新版——patch 先堵住,等正式版。
撤销补丁
# 方法 1:删文件 + 改 package.json rm patches/react-select@5.8.0.patch # 手动从 package.json 的 patchedDependencies 里删掉那行 pnpm install # 方法 2:pnpm 命令 pnpm patch-remove react-select@5.8.0
本章小结
pnpm patch pkg@ver解压临时目录,pnpm patch-commit生成 .patch 文件- 补丁提交到 git,下次 install 自动应用——团队无感接手
- 补丁 hash 写进 lockfile,版本升级时警告「可能不再适用」
- monorepo 里放 pnpm-workspace.yaml 的
patchedDependencies全仓共享 overrides用于换版本,patch用于改代码——边界清晰