Spaces:
Running
Running
File size: 4,739 Bytes
7acc713 1a6d0d2 7acc713 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | const fs = require("node:fs");
const path = require("node:path");
const ALLOWED = new Set([
"MIT",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Unlicense",
"CC0-1.0",
"0BSD",
"LGPL-2.1",
"LGPL-3.0",
// MPL-2.0 is file-level copyleft; combining it with Apache-2.0 in one
// program is explicitly permitted by Mozilla's own FAQ (Q13,
// https://www.mozilla.org/en-US/MPL/2.0/FAQ/). Ships via vite's
// lightningcss binaries, unmodified.
"MPL-2.0",
]);
// Each entry below was verified by hand against the LICENSE file shipped in
// that package's own node_modules directory. A new opaque-license package
// must be added here ONLY after a human has read its license file.
const OVERRIDE = {
flatbuffers: "Apache-2.0",
"keyword-extractor": "MIT",
};
const OPAQUE = "SEE LICENSE IN LICENSE.txt";
// OR = any side may be allowed; AND = every side must be allowed.
function isAllowed(license) {
if (typeof license !== "string") return false;
if (ALLOWED.has(license)) return true;
if (license.includes(" OR ")) {
return license
.split(" OR ")
.some((side) => ALLOWED.has(side.replace(/^\(|\)$/g, "").trim()));
}
if (license.includes(" AND ")) {
return license
.split(" AND ")
.every((side) => ALLOWED.has(side.replace(/^\(|\)$/g, "").trim()));
}
return false;
}
// A missing license field and the opaque "SEE LICENSE IN LICENSE.txt" field
// are only allowed via the OVERRIDE map, which a human must populate after
// reading the package's own license. Any other package with such a field
// fails the gate (safety net).
function isAllowedLicense(name, license) {
if (license === undefined || license === OPAQUE) {
return isAllowed(OVERRIDE[name]);
}
return isAllowed(license);
}
function productionPackages(lock) {
const packages = [];
for (const [key, entry] of Object.entries(lock.packages ?? {})) {
if (key === "") continue;
if (entry.dev) continue;
if (entry.link) continue;
const name = key.split("node_modules/").pop();
packages.push({
name,
version: entry.version ?? "unknown",
license: entry.license,
});
}
return packages;
}
function formatLicense(license) {
return typeof license === "string" ? license : "no license field";
}
function writeReport(reportPath, packages) {
const rows = [...packages].sort((a, b) => a.name.localeCompare(b.name));
const lines = [
"# Dependency Licenses",
"",
`Project license: **Apache-2.0**`,
"",
"| Package | License |",
"| --- | --- |",
...rows.map((p) => `| ${p.name} | ${formatLicense(p.license)} |`),
"",
];
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, lines.join("\n"), "utf8");
}
function run(lockPath, reportPath) {
const lock = JSON.parse(fs.readFileSync(lockPath, "utf8"));
const packages = productionPackages(lock);
const violations = packages.filter(
(p) => !isAllowedLicense(p.name, p.license),
);
if (violations.length > 0) {
for (const v of violations) {
console.log(
`NOT ALLOWED: ${v.name}@${v.version} (${formatLicense(v.license)})`,
);
}
console.log(
`\n${violations.length} of ${packages.length} production packages have a license that is not compatible with Apache-2.0`,
);
if (reportPath) writeReport(reportPath, packages);
return 1;
}
console.log(
`All ${packages.length} production packages have a license compatible with Apache-2.0`,
);
if (reportPath) writeReport(reportPath, packages);
return 0;
}
if (require.main === module) {
const args = process.argv.slice(2);
const usage =
"Usage: node scripts/license-check.cjs [lockfilePath] [--report <path>]";
let lockfilePath;
let reportPath;
for (let i = 0; i < args.length; i++) {
if (args[i] === "--report") {
reportPath = args[++i];
if (!reportPath) {
console.error(`β --report requires a path\n ${usage}`);
process.exit(1);
}
} else if (args[i].startsWith("-")) {
console.error(`β Unknown option: ${args[i]}\n ${usage}`);
process.exit(1);
} else if (lockfilePath === undefined) {
lockfilePath = args[i];
} else {
console.error(`β Too many arguments\n ${usage}`);
process.exit(1);
}
}
const rootDir = path.resolve(__dirname, "..");
lockfilePath = lockfilePath
? path.resolve(lockfilePath)
: path.join(rootDir, "package-lock.json");
if (!fs.existsSync(lockfilePath)) {
console.error(`β Lockfile not found: ${lockfilePath}`);
process.exit(1);
}
process.exit(run(lockfilePath, reportPath));
}
module.exports = { run, isAllowedLicense, ALLOWED, OVERRIDE };
|