本文主要套路在npm迁移到pnpm时,patch包不兼容的一些问题…


前言

在前端项目开发中,我们经常需要修改 node_modules 中第三方包的代码来修复 bug 或添加功能。npm 生态中的 patch-package 是一个广泛使用的解决方案,但当项目迁移到 pnpm 时,会遇到补丁包不兼容的问题。本文将详细介绍如何在 pnpm 环境下处理这种兼容性问题。

相关文章:

背景:为什么需要补丁包?

在我们的项目中,需要修改 @vant/cligen-package-entry.js 文件,添加自定义函数 initMobileUI 的导出逻辑。这个修改在每次 npm install 后都会丢失,因此需要使用补丁包来持久化这些修改。

一、pnpm patch 原生流程

1.1 创建补丁

pnpm 提供了原生的补丁功能,使用起来非常简单:

# 创建补丁的临时工作目录
$ pnpm patch <package-name@版本号> # 如:pnpm patch @vant/cli@5.1.0

执行后,pnpm 会输出临时目录路径:

You can now edit the following folder: /private/var/folders/.../vant-cli@5.0.1
Once you're done with your changes, run "pnpm patch-commit <path>"

1.2 修改代码

进入临时目录修改需要的文件:

cd /private/var/folders/.../vant-cli@5.0.1
# 编辑需要修改的文件
vim lib/compiler/gen-package-entry.js

在我们的案例中,需要添加以下修改:

// 在 genPackageEntry 函数中添加
const components = names.map(pascalize);

// 添加对 utils 的导入,特别是 initMobileUI
const utilsImport = `import { initMobileUI } from '${getPathByName('utils', pathResolver)}';`;
const content = `${genImports(names, pathResolver, namedExport)}
${utilsImport}

// ... 其他代码

// 在导出部分添加
${genExports(names, pathResolver, namedExport)}
export { initMobileUI };

注意:这里 进入临时目录,支持同时修改多个文件,提交所有修改后生成单一补丁包 ​。如​​:

  • 修改 src/compile.js:修复编译逻辑
  • 修改 lib/config.js:新增配置项

1.3 提交补丁

修改完成后,回到项目根目录提交补丁:

pnpm patch-commit /private/var/folders/.../vant-cli@5.0.1

会在 patches/ 目录下生成 @vant__cli@5.1.0.patch 文件(注意是双下划线)。

自动生成的补丁文件:patches/vant-cli@5.0.1.patch,包含所有(单个或多个)文件变更

1.4 补丁文件格式

pnpm 生成的补丁文件格式如下:

diff --git a/lib/compiler/gen-package-entry.js b/lib/compiler/gen-package-entry.js
index d07e0a7..b5b9200 100644
--- a/lib/compiler/gen-package-entry.js
+++ b/lib/compiler/gen-package-entry.js
@@ -47,7 +47,11 @@ export function genPackageEntry({ outputPath, pathResolver, }) {
     const skipInstall = (((_b = vantConfig.build) === null || _b === void 0 ? void 0 : _b.skipInstall) || []).map(pascalize);
     const version = process.env.PACKAGE_VERSION || getPackageJson().version;
     const components = names.map(pascalize);
+    
+    // 添加对 utils 的导入,特别是 initMobileUI
+    const utilsImport = `import { initMobileUI } from '${getPathByName('utils', pathResolver)}';`;
     const content = `${genImports(names, pathResolver, namedExport)}
+${utilsImport}
 
 const version = '${version}';

二、npm patch-package 兼容性问题

2.1 问题描述

当项目从 npm 迁移到 pnpm 时,原有的 patch-package 补丁文件(格式为 @vant+cli+5.1.0.patch,使用加号)无法被 pnpm 识别和应用。

2.2 两种补丁格式的区别

特性npm (patch-package)pnpm (原生)
文件命名@vant+cli+5.1.0.patch@vant__cli@5.1.0.patch
分隔符加号 +双下划线 __
应用时机手动或 postinstall自动应用
路径前缀node_modules/相对路径

2.3 直接迁移的问题

如果直接使用 pnpm,原有的历史补丁文件将无法应用,导致:

  • 历史功能修改丢失
  • 需要重新创建所有补丁
  • 可能影响项目的正常运行

三、兼容性解决方案

3.1 方案设计思路

为了同时支持历史补丁和新的 pnpm 补丁,我们采用了混合方案:

  1. pnpm 原生补丁:处理新的修改(如 initMobileUI 导出)
  2. 自定义脚本:应用历史的 npm 补丁文件
  3. postinstall 钩子:确保补丁在依赖安装后自动应用

3.2 实现步骤

步骤1:创建补丁应用脚本

创建 apply-patches.js 文件:

const { execSync } = require('child_process');
const fs = require('fs');

console.log('应用自定义补丁...');

try {
  // 应用历史的 npm 补丁文件
  console.log('应用 vant-cli 历史补丁...');
  try {
    execSync('patch -p1 -i patches/@vant+cli+5.1.0.patch --forward --reject-file=-', { 
      stdio: 'inherit' 
    });
  } catch (error) {
    console.log('补丁可能已经应用过,继续执行...');
  }
  
  console.log('✅ 补丁应用完成');
} catch (error) {
  console.error('❌ 补丁应用失败:', error.message);
  process.exit(1);
}

步骤2:配置 package.json

package.json 中添加相关脚本:

{
  "scripts": {
    "patch": "node apply-patches.js",
    "postinstall": "node apply-patches.js"
  }
}

步骤3:补丁文件管理

项目中会同时存在两种补丁文件:

patches/
├── @vant+cli+5.1.0.patch      # npm 格式,包含历史修改
└── @vant__cli@5.1.0.patch     # pnpm 格式,包含新修改

3.3 实际案例:initMobileUI 导出问题

问题背景

需要在 @vant/cli 的构建产物中导出自定义的 initMobileUI 函数,但该函数在每次构建后都会丢失。

解决步骤

  1. 创建 pnpm 补丁

    pnpm patch @vant/cli@5.1.0
    # 修改 gen-package-entry.js 添加 initMobileUI 导出逻辑
    pnpm patch-commit '/path/to/temp/dir'
    
  2. 保留历史补丁

    • 保持原有的 @vant+cli+5.1.0.patch 文件
    • 通过 apply-patches.js 脚本应用
  3. 验证结果

    # 检查构建产物
    grep -n "initMobileUI" es/index.js lib/index.js
    # 输出:
    # es/index.js:110:import { initMobileUI } from "./utils";
    # es/index.js:348:  initMobileUI,
    # lib/index.js:22:  initMobileUI: () => import_utils.initMobileUI,
    

常见问题处理

在迁移过程中可能遇到的依赖问题:

# TypeScript 类型声明问题
pnpm add -D @types/node postcss

3.4 完整工作流程

# 1. 安装依赖
pnpm install
# → pnpm 自动应用 @vant__cli@5.1.0.patch
# → postinstall 钩子执行 apply-patches.js
# → 脚本应用 @vant+cli+5.1.0.patch

# 2. 构建项目
npm run build
# → 所有补丁都已生效,构建成功

四、最佳实践和总结

4.1 核心方案优势

通过本文介绍的混合方案,我们成功解决了从 npm 到 pnpm 迁移过程中的补丁包兼容性问题。这个方案的核心优势在于:

  1. 向后兼容:保持历史补丁的有效性,避免重复工作
  2. 向前发展:充分利用 pnpm 的原生补丁功能
  3. 自动化管理:通过 postinstall 钩子和脚本实现自动应用
  4. 可维护性:清晰的文件结构和完善的错误处理机制

4.2 实施要点

  1. 补丁文件管理

    • 新功能使用 pnpm 原生补丁(@package__name@version.patch
    • 历史修改保持 npm 格式补丁(@package+name+version.patch
    • 通过自定义脚本统一管理和应用
  2. 关键注意事项

    • 避免冲突:两种补丁文件不要修改同一文件的同一部分
    • 路径差异:npm 补丁使用 node_modules/ 前缀,pnpm 补丁使用相对路径
    • 版本控制:将所有补丁文件和应用脚本纳入版本控制
  3. 错误处理策略

    • 使用 --forward 参数避免重复应用补丁
    • 补丁应用失败时不中断整个构建过程
    • 提供清晰的错误信息和解决建议

4.3 常见问题排查

# 问题1:补丁文件格式错误
# 解决:检查补丁文件的路径前缀和命名格式

# 问题2:补丁已经应用过
# 解决:使用 --forward 参数忽略已应用的补丁

# 问题3:权限问题
# 解决:确保对 node_modules 目录有写权限

4.4 推广应用

这种混合方案不仅适用于 @vant/cli 的修改,也可以推广到其他需要补丁包的场景中。在实际项目中,建议根据具体需求调整脚本逻辑,但核心思路保持不变。

希望这个方案能帮助到其他遇到类似问题的开发者,让项目迁移过程更加顺畅。


附录:完整代码示例

apply-patches.js 完整代码

const { execSync } = require('child_process');
const fs = require('fs');

console.log('应用自定义补丁...');

try {
  // 1. 应用原有的补丁文件
  console.log('应用 vant-cli 历史补丁...');
  try {
    execSync('patch -p1 -i patches/@vant+cli+5.1.0.patch --forward --reject-file=-', { 
      stdio: 'inherit' 
    });
  } catch (error) {
    console.log('补丁可能已经应用过,继续执行...');
  }
  
  console.log('✅ 补丁应用完成');
} catch (error) {
  console.error('❌ 补丁应用失败:', error.message);
  process.exit(1);
}

package.json 配置示例

{
  "name": "@zto/mzui",
  "version": "v3.12.0",
  "scripts": {
    "patch": "node apply-patches.js",
    "postinstall": "node apply-patches.js",
    "build": "vant-cli build && npm run build:rem",
    "build:rem": "node ./build-rem.js"
  },
  "devDependencies": {
    "@types/node": "^24.0.14",
    "postcss": "^8.5.6",
    "patch-package": "^6.4.7"
  }
}

目录结构示例

project/
├── patches/
│   ├── @vant+cli+5.1.0.patch      # npm 格式补丁
│   └── @vant__cli@5.1.0.patch     # pnpm 格式补丁
├── apply-patches.js                # 补丁应用脚本
├── package.json                    # 项目配置
└── src/
    └── utils/
        └── global-config.ts        # initMobileUI 函数定义

最后, 希望大家早日实现:成为编程高手的伟大梦想!
欢迎交流~

微信公众号

本文版权归原作者曜灵所有!未经允许,严禁转载!对非法转载者, 原作者保留采用法律手段追究的权利!
若需转载,请联系微信公众号:连先生有猫病,可获取作者联系方式!