问题背景
在使用 Lerna + Yarn 管理的 monorepo 项目中,执行 yarn pub-alpha 发布 beta 测试包时,遇到以下问题:
- Yarn 警告 :提示项目中存在
package-lock.json,建议不要混用包管理器 - 版本不匹配:循环重试 5 次后依然无法安装正确的 beta 版本,最终发布失败
bash
尝试安装 @tyc/tyc-sdk@2.0.101-beta.202511280653 (第 5/5 次)
✗ 版本不匹配,等待 5 秒后重试...
发布过程中出错: Error: 无法安装正确版本的 @tyc/tyc-sdk@2.0.101-beta.202511280653
问题分析
问题 1:包管理器混用
现象:
package-lock.json found. Your project contains lock files generated by
tools other than Yarn. It is advised not to mix package managers in order
to avoid resolution inconsistencies caused by unsynchronized lock files.
原因:
项目中同时存在 package-lock.json(npm 生成)和 yarn.lock(yarn 生成):
- 发布脚本使用的是
yarn命令 - 但项目中同时维护两种锁文件,导致依赖解析不一致
影响:
- 不同开发者可能安装不同版本的依赖
- CI/CD 环境可能安装错误的依赖版本
- 发布流程中的依赖版本难以保证一致性
问题 2:Beta 版本安装失败
现象:
package.json 中依赖版本写的是 ^2.0.101-beta.202511280653,但实际安装的是正式版 2.0.101。
原因分析:
通过检查安装的实际版本:
bash
$ cat packages/adapter/node_modules/@tyc/tyc-sdk/package.json | grep version
"version": "2.0.101", # 安装了正式版
$ yarn list --pattern @tyc/tyc-sdk
└─ @tyc/tyc-sdk@2.0.101 # 不是期望的 beta 版本
问题出在两个地方:
- 版本范围符号
^的语义问题
javascript // scripts/publish-alpha.js:137 adapterPackageJson.dependencies["@tyc/tyc-sdk"] = `^${sdkPublishedVersion}`; // 生成: "^2.0.101-beta.202511280653"
使用 ^ 前缀时,Yarn 在解析 semver 范围时:
-
会寻找满足
^2.0.101的最新版本 -
可能会忽略
-beta.xxx的 prerelease 标签 -
优先选择仓库中的正式版
2.0.101
- 安装方式不够明确
javascript // scripts/publish-alpha.js:72 (旧代码) execSync("yarn install --force", { cwd: installPath, stdio: "inherit" });
使用 yarn install 会根据 package.json 中的版本范围进行解析,而不是强制安装指定的精确版本。
解决方案
解决方案 1:统一包管理器
删除所有 package-lock.json 文件,只保留 yarn.lock:
bash
# 删除根目录的锁文件
rm package-lock.json
# 删除所有子包的锁文件
find packages -name "package-lock.json" -type f -delete
# 或者手动删除
rm packages/adapter/package-lock.json
rm packages/tyc-sdk/package-lock.json
防止再次生成:
方案 A - 添加到 .gitignore:
gitignore
package-lock.json
**/package-lock.json
方案 B - 配置 .npmrc:
ini
package-lock=false
解决方案 2:修复版本安装逻辑
修改 scripts/publish-alpha.js:
修改点 1:使用精确版本(移除 ^)
diff
// 发布 adapter beta版本
const adapterPackageJsonPath = path.join(adapterPath, "package.json");
const adapterPackageJson = require(adapterPackageJsonPath);
- adapterPackageJson.dependencies["@tyc/tyc-sdk"] = `^${sdkPublishedVersion}`;
+ adapterPackageJson.dependencies["@tyc/tyc-sdk"] = sdkPublishedVersion;
fs.writeFileSync(
adapterPackageJsonPath,
JSON.stringify(adapterPackageJson, null, 2) + "\n"
);
修改点 2:使用 yarn add 精确安装
diff
for (let i = 0; i < maxRetries; i++) {
console.log(`\n尝试安装 ${packageName}@${expectedVersion} (第 ${i + 1}/${maxRetries} 次)`);
// 清除缓存并安装
try {
execSync(`yarn cache clean ${packageName}`, { cwd: installPath, stdio: "inherit" });
} catch (error) {
// 忽略清除缓存的错误
}
- execSync("yarn install --force", { cwd: installPath, stdio: "inherit" });
+ // 使用 yarn add 直接安装指定版本,而不是依赖 yarn install 的解析
+ execSync(`yarn add ${packageName}@${expectedVersion}`, { cwd: installPath, stdio: "inherit" });
// 验证安装的版本
if (verifyInstalledVersion(packageName, expectedVersion, nodeModulesPath)) {
console.log(`✓ 成功安装 ${packageName}@${expectedVersion}`);
关键区别:
| 方式 | 命令 | 行为 |
|---|---|---|
| 旧方式 | yarn install --force |
根据 package.json 中的版本范围解析,可能选择错误版本 |
| 新方式 | yarn add @pkg@1.0.0-beta.x |
强制安装指定的精确版本,忽略 semver 范围 |
技术原理
Semver 中的 Prerelease 版本
根据 Semver 规范:
^2.0.101匹配>=2.0.101 <3.0.0^2.0.101-beta.202511280653的语义较为复杂:-beta.xxx是 prerelease 标识符- 在版本比较时,
2.0.101 > 2.0.101-beta.xxx(正式版优先级更高) ^范围运算符可能会跳过 prerelease 版本,选择最新的正式版
Yarn 的版本解析策略
Yarn 在执行 yarn install 时:
- 读取
package.json中的依赖声明 - 根据 semver 范围查询 registry 中的可用版本
- 选择满足范围的"最佳"版本(通常是最新的稳定版)
- prerelease 版本通常不会被自动选中(除非显式指定或使用 tag)
而 yarn add @pkg@exact-version:
- 直接下载指定的精确版本
- 不进行 semver 范围解析
- 更新
package.json为精确版本
最佳实践
1. 包管理器选择
在团队中统一使用一种包管理器:
推荐方式:
- 使用 Yarn(v1.x 或 v3+)或 pnpm
- 在
package.json中添加packageManager字段(Node 16.9+):
json { "packageManager": "yarn@1.22.21" } - 使用 Corepack 强制包管理器版本
Git 配置:
gitignore
# 只保留一种锁文件
package-lock.json
pnpm-lock.yaml
2. Monorepo Beta 版本发布
依赖版本管理:
javascript
// 使用精确版本,而非范围版本
dependencies["@scope/pkg"] = exactVersion; // ✅ 推荐
dependencies["@scope/pkg"] = `^${version}`; // ❌ 避免(对 prerelease 版本)
安装策略:
javascript
// 对于 beta/alpha 版本,使用显式安装
execSync(`yarn add ${pkg}@${version}`); // ✅ 推荐
execSync(`yarn install --force`); // ❌ 可能解析错误
发布流程:
- 发布 SDK beta 版本到 registry
- 清除 yarn cache 确保获取最新版本
- 使用
yarn add pkg@version精确安装 - 验证安装的版本是否正确
- 构建并发布依赖该 SDK 的包
3. 版本验证
在发布脚本中添加版本验证:
javascript
const verifyInstalledVersion = (packageName, expectedVersion, nodeModulesPath) => {
try {
const installedPackagePath = path.join(nodeModulesPath, packageName, "package.json");
if (!fs.existsSync(installedPackagePath)) {
return false;
}
// 清除 require 缓存
delete require.cache[require.resolve(installedPackagePath)];
const installedPackage = require(installedPackagePath);
return installedPackage.version === expectedVersion;
} catch (error) {
return false;
}
};
4. CI/CD 环境配置
确保 CI/CD 环境中:
- 使用统一的包管理器版本
- 配置正确的 registry URL
- 清除缓存后再安装依赖
- 验证关键依赖的版本
总结
这个问题涉及两个核心点:
- 包管理器混用导致锁文件不一致,进而引发依赖解析问题
- Semver prerelease 版本的特殊性 :使用范围符号(
^/~)+yarn install可能无法正确解析 beta 版本
解决思路:
- 统一包管理工具,只保留一种锁文件
- 对于 prerelease 版本,使用精确版本 +
yarn add显式安装 - 添加版本验证机制,确保安装的版本符合预期
通过这些改进,可以确保 monorepo 项目中 beta 版本的发布流程稳定可靠。