scripts/release-tools.mjs
Rama main · réplica de lectura del repositorio original.
export const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-(?:alpha|beta|rc)\.\d+)?$/;
export const APPROVED_LICENSE_IDS = new Set([
"0BSD",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BlueOak-1.0.0",
"CC0-1.0",
"ISC",
"MIT",
"Python-2.0",
"Unicode-3.0",
"Unlicense",
"Zlib"
]);
// Dual-licensed dependencies require one explicit, version-pinned review.
// DOMPurify publishes an Apache-2.0 alternative. This exception does not
// approve MPL-2.0 as a general license for other production dependencies.
const REVIEWED_PACKAGE_LICENSES = new Map([
["dompurify@3.4.17", "(MPL-2.0 OR Apache-2.0)"],
// khroma 2.1.0 npm omits SPDX, but upstream /license contains the MIT grant.
["khroma@2.1.0", "Unknown"]
]);
export function normalizeVersion(value) {
const version = String(value ?? "").trim().replace(/^v/, "");
if (!VERSION_PATTERN.test(version)) {
throw new Error(
`Unsupported version: ${version || "<empty>"}; use X.Y.Z or X.Y.Z-alpha.N / beta.N / rc.N`
);
}
return version;
}
function extractSection(markdown, label) {
const lines = markdown.split(/\r?\n/);
const prefix = `## [${label}]`;
const start = lines.findIndex(
(line) => line === prefix || line.startsWith(`${prefix} `)
);
if (start < 0) return undefined;
let end = lines.length;
for (let index = start + 1; index < lines.length; index += 1) {
if (/^##\s+/.test(lines[index])) {
end = index;
break;
}
}
return lines.slice(start + 1, end).join("\n").trim();
}
export function extractReleaseSection(markdown, version) {
return extractSection(markdown, normalizeVersion(version));
}
function isStandaloneHtmlComment(line) {
if (!line.startsWith("<!--") || !line.endsWith("-->")) return false;
const body = line.slice(4, -3);
return !body.includes("<!--") && !body.includes("-->");
}
export function isUnreleasedEmpty(markdown) {
const body = extractSection(markdown, "Unreleased");
if (body === undefined) return false;
return body
.split(/\r?\n/)
.map((line) => line.trim())
.every((line) => line.length === 0 || isStandaloneHtmlComment(line));
}
function isApprovedLicenseExpression(expression) {
const value = String(expression ?? "").trim();
if (
!value ||
/unknown|unlicensed|see license in|proprietary|custom/i.test(value) ||
!/^[A-Za-z0-9.+()\-\s]+$/.test(value)
) {
return false;
}
const tokens = value.match(/[A-Za-z0-9][A-Za-z0-9.+-]*/g) ?? [];
let licenseCount = 0;
for (const token of tokens) {
if (/^(?:AND|OR)$/i.test(token)) continue;
if (/^WITH$/i.test(token)) return false;
licenseCount += 1;
if (!APPROVED_LICENSE_IDS.has(token)) return false;
}
return licenseCount > 0;
}
export function inspectLicenseReport(report) {
const packages = [];
if (Array.isArray(report)) {
for (const item of report) {
if (item && typeof item === "object") {
packages.push({
name: String(item.name ?? "<unknown>"),
versions: Array.isArray(item.versions)
? item.versions.map(String)
: [String(item.version ?? "<unknown>")],
license: String(item.license ?? "Unknown")
});
}
}
} else if (report && typeof report === "object") {
for (const [groupLicense, entries] of Object.entries(report)) {
if (!Array.isArray(entries)) continue;
for (const item of entries) {
if (!item || typeof item !== "object") continue;
packages.push({
name: String(item.name ?? "<unknown>"),
versions: Array.isArray(item.versions)
? item.versions.map(String)
: [String(item.version ?? "<unknown>")],
license: String(item.license ?? groupLicense ?? "Unknown")
});
}
}
}
if (packages.length === 0) {
throw new Error("pnpm returned no production dependency license records");
}
const failures = [];
for (const pkg of packages) {
const specificallyReviewed = pkg.versions.length === 1 &&
REVIEWED_PACKAGE_LICENSES.get(`${pkg.name}@${pkg.versions[0]}`) === pkg.license;
if (!isApprovedLicenseExpression(pkg.license) && !specificallyReviewed) {
failures.push(
`${pkg.name}@${pkg.versions.join(",")} = ${pkg.license}`
);
}
}
if (failures.length > 0) {
throw new Error(
"Production dependency licenses require explicit review:\n - " +
failures.join("\n - ")
);
}
return {
packageCount: packages.length,
licenses: [...new Set(packages.map((pkg) => pkg.license))].sort()
};
}