3. 从 package.json 提取依赖许可证信息
为什么不能只读 package.json 的 dependencies
Node.js 项目的 package.json 只声明直接依赖及其版本范围,而每个直接依赖又会引入自己的依赖,形成依赖树。许可证风险往往藏在间接依赖里,因此需要同时解析 dependencies、devDependencies、peerDependencies、optionalDependencies,再结合已安装的 node_modules/<pkg>/package.json 递归展开,才能得到完整的依赖许可证清单。
提取脚本实现
以下脚本读取项目根 package.json,遍历所有依赖类型,再递归读取 node_modules 中每个包的 package.json,提取 license 或 licenses 字段,输出结构化清单。
// extract-licenses.js
const fs = require('fs');
const path = require('path');
const ROOT = process.cwd();
const DEP_TYPES = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
function readJson(p) {
try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
catch { return null; }
}
function normalizeLicense(pkg) {
if (!pkg) return { license: 'UNKNOWN', source: 'missing-package' };
if (typeof pkg.license === 'string') return { license: pkg.license, source: 'license' };
if (pkg.license && typeof pkg.license === 'object') return { license: pkg.license.type || 'UNKNOWN', source: 'license.type' };
if (Array.isArray(pkg.licenses)) {
const types = pkg.licenses.map(l => (typeof l === 'string' ? l : l.type)).filter(Boolean);
return { license: types.join(' OR ') || 'UNKNOWN', source: 'licenses[]' };
}
return { license: 'UNKNOWN', source: 'no-license-field' };
}
function collectDeps(pkgJson) {
const set = new Set();
for (const t of DEP_TYPES) {
const d = pkgJson[t];
if (d && typeof d === 'object') Object.keys(d).forEach(n => set.add(n));
}
return [...set];
}
function resolveInstalled(name) {
const p = path.join(ROOT, 'node_modules', name, 'package.json');
return readJson(p);
}
function walk(name, visited, results, depth) {
if (visited.has(name)) return;
visited.add(name);
const pkg = resolveInstalled(name);
const { license, source } = normalizeLicense(pkg);
results.push({ name, version: pkg ? pkg.version : 'N/A', license, source, depth });
if (pkg) {
const children = collectDeps(pkg);
for (const c of children) walk(c, visited, results, depth + 1);
}
}
const rootPkg = readJson(path.join(ROOT, 'package.json'));
if (!rootPkg) { console.error('未找到 package.json'); process.exit(1); }
const directDeps = collectDeps(rootPkg);
const visited = new Set();
const results = [];
for (const d of directDeps) walk(d, visited, results, 1);
// 输出 CSV
const header = 'name,version,license,source,depth';
const rows = results.map(r => `${r.name},${r.version},${r.license},${r.source},${r.depth}`);
console.log([header, ...rows].join('\n'));
console.error(`\n共提取 ${results.length} 个依赖包`);
运行与结果判读
在项目根目录执行 node extract-licenses.js > licenses.csv,即可得到依赖许可证清单。判读时重点关注:
- license 为 UNKNOWN:该包未声明许可证,需人工核查其仓库或联系维护者。
- source 为 licenses[]:使用了旧版数组格式,需确认是否可标准化为 SPDX 表达式。
- depth 大于 1:间接依赖,容易被忽略,但同样构成合规风险。
- license 含 OR / AND:复合表达式,需按 SPDX 规则进一步解析选择条件。
若项目使用 pnpm 或 yarn PnP,
node_modules 结构不同,需改用 pnpm list --json 或 yarn info 获取依赖树后再提取许可证。本脚本仅读取已安装的
node_modules,若依赖未安装或版本范围未锁定,提取结果可能与实际部署不一致。建议在 npm ci 后执行。