Chapter 08

给第三方包临时打补丁

第三方库里有个 bug 或者要改一行逻辑,提 PR 等合并可能要一个月——怎么办?不是 fork,不是 patch-package 那种半自动——pnpm 内置 pnpm patch,结构化、可审计、可回滚。

场景: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?

别这么干:

  1. pnpm 用硬链接,改 node_modules 其实在改 store——其它项目一起中招
  2. 下次 pnpm install --force 或 store prune 后丢失
  3. 没有 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(可以 npm-force-resolutions 那样)——patch 留给必须改代码的场景。

vs patch-package

patch-packagepnpm 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

本章小结