背景
在企业级开发中,安全合规要求对项目进行 SCA(软件成分分析)扫描。主流扫描工具通常强制要求提供 package.json 和 package-lock.json。
然而,对于使用 pnpm Monorepo 架构的项目,这带来了一系列痛点:
- 文件缺失:项目只有
pnpm-lock.yaml,没有package-lock.json。 - 协议不兼容:npm 无法识别 pnpm 的
workspace:协议。 - 环境冲突:直接运行
npm install会因 pnpm 的软链结构导致Cannot read properties of null报错。 - 版本漂移:npm 重新计算依赖树会导致版本与 pnpm 锁定版本不一致,造成漏扫或误报。
- 幽灵依赖丢失:使用
--legacy-peer-deps可能导致关键的 PeerDependencies(如 Webpack)在 Lock 文件中消失。
本文提供一套经过验证的标准化流程,在不破坏原有 pnpm 结构的前提下,生成一份版本精准的 package-lock.json。
核心思路
不能直接在子项目生成,必须在 根目录 进行全量扫描。利用 pnpm 的 hoisted 模式将依赖铺平(伪装成 npm 的结构),强制 npm 读取本地已安装的包版本,从而实现“零版本漂移”的锁定。
操作步骤
1. 清理环境
必须彻底清除原有的 node_modules,防止 pnpm 的软链接干扰 npm 的解析器。
# 根目录下执行
rm -rf node_modules
rm -rf packages/*/node_modules
2. 依赖铺平(关键步骤)
使用 pnpm 的 node-linker=hoisted 参数安装依赖。这会告诉 pnpm 放弃软链接机制,采用类似 npm/yarn 的扁平化结构将依赖安装到根目录。
目的:让 node_modules 变成 npm “看得懂”的样子,且版本严格遵循 pnpm-lock.yaml。
pnpm install --config.node-linker=hoisted --no-frozen-lockfile
3. 生成 Lock 文件
在扁平化的环境下,使用 npm 生成锁文件。
--package-lock-only: 只生成文件,不下载包。--legacy-peer-deps: 忽略 PeerDependencies 冲突(npm v7+ 默认严格检查导致报错,必须忽略)。--ignore-scripts: 忽略脚本运行,加速过程。
npm install --package-lock-only --legacy-peer-deps --ignore-scripts
4. 补全丢失的 PeerDependencies(填坑)
由于使用了 --legacy-peer-deps,部分未在 package.json 中显式声明的 PeerDependencies(例如 webpack、vite 等核心构建工具)可能不会被写入 package-lock.json,导致 SCA 扫描无法识别具体版本(或报出虚假的低版本)。
解决方案: 检查 pnpm-lock.yaml 中的真实版本,将这些包手动写入根目录 package.json 的 devDependencies 或 overrides 中。
// package.json
{
"devDependencies": {
// 显式锁死版本,强制写入 lock 文件
"webpack": "5.99.5"
}
}
修改后,重新执行步骤 3。
验证方案
为了确保生成的 package-lock.json 与 pnpm-lock.yaml 版本一致,建议编写脚本进行自动化比对。
校验逻辑:
- 解析
pnpm-lock.yaml获取所有包名及版本。 - 解析
package-lock.json获取所有包名及版本。 - 取交集比对,重点关注版本号是否完全一致。
验证脚本 (verify-lock.cjs) 简版:
const fs = require('fs');
const yaml = require('js-yaml'); // 需安装 js-yaml
const pnpmLock = yaml.load(fs.readFileSync('./pnpm-lock.yaml', 'utf8'));
const npmLock = JSON.parse(fs.readFileSync('./package-lock.json', 'utf8'));
// ... 解析逻辑省略,重点在于对比 map 中的 version 字段 ...
// 核心比对
if (npmVersion !== pnpmVersion) {
console.error(`❌ 版本不一致: ${pkgName} (npm: ${npmVersion} vs pnpm: ${pnpmVersion})`);
} else {
console.log('✅ 版本一致');
}
总结
对于 Monorepo 项目的安全扫描,不要试图在子项目中单独生成 Lock 文件,这会导致公共依赖漏扫。
最佳实践是:
- 全量扫描:以根目录为准。
- 物理隔离:利用
hoisted模式消除 pnpm 软链结构。 - 人工兜底:通过
devDependencies显式声明丢失的 PeerDependencies。
此方案生成的介质既满足 SCA 工具的文件格式要求,又保证了依赖版本的真实性。