pnpm Monorepo 项目过 SCA 安全扫描的终极方案:零偏差生成 package-lock.json

1 阅读3分钟

背景

在企业级开发中,安全合规要求对项目进行 SCA(软件成分分析)扫描。主流扫描工具通常强制要求提供 package.jsonpackage-lock.json

然而,对于使用 pnpm Monorepo 架构的项目,这带来了一系列痛点:

  1. 文件缺失:项目只有 pnpm-lock.yaml,没有 package-lock.json
  2. 协议不兼容:npm 无法识别 pnpm 的 workspace: 协议。
  3. 环境冲突:直接运行 npm install 会因 pnpm 的软链结构导致 Cannot read properties of null 报错。
  4. 版本漂移:npm 重新计算依赖树会导致版本与 pnpm 锁定版本不一致,造成漏扫或误报。
  5. 幽灵依赖丢失:使用 --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(例如 webpackvite 等核心构建工具)可能不会被写入 package-lock.json,导致 SCA 扫描无法识别具体版本(或报出虚假的低版本)。

解决方案: 检查 pnpm-lock.yaml 中的真实版本,将这些包手动写入根目录 package.jsondevDependenciesoverrides 中。

// package.json
{
  "devDependencies": {
    // 显式锁死版本,强制写入 lock 文件
    "webpack": "5.99.5" 
  }
}

修改后,重新执行步骤 3。

验证方案

为了确保生成的 package-lock.jsonpnpm-lock.yaml 版本一致,建议编写脚本进行自动化比对。

校验逻辑

  1. 解析 pnpm-lock.yaml 获取所有包名及版本。
  2. 解析 package-lock.json 获取所有包名及版本。
  3. 取交集比对,重点关注版本号是否完全一致。

验证脚本 (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 文件,这会导致公共依赖漏扫。

最佳实践是:

  1. 全量扫描:以根目录为准。
  2. 物理隔离:利用 hoisted 模式消除 pnpm 软链结构。
  3. 人工兜底:通过 devDependencies 显式声明丢失的 PeerDependencies。

此方案生成的介质既满足 SCA 工具的文件格式要求,又保证了依赖版本的真实性。