#!/usr/bin/env node const { execSync } = require("node:child_process"); const { existsSync, mkdirSync, writeFileSync, readFileSync } = require("node:fs"); const path = require("node:path"); const { argv } = require("node:process"); const inputIdx = argv.indexOf("--input"); const INPUT_FILE = inputIdx > -1 ? argv[inputIdx + 1] : null; const POSTPROCESS_ONLY = !!INPUT_FILE; // __dirname is available in CommonJS by default /** * Generate 3rd party licenses for frontend dependencies * This script creates a JSON file similar to the Java backend's 3rdPartyLicenses.json */ const OUTPUT_FILE = path.join(__dirname, "..", "src", "assets", "3rdPartyLicenses.json"); const PACKAGE_JSON = path.join(__dirname, "..", "package.json"); // Ensure the output directory exists const outputDir = path.dirname(OUTPUT_FILE); if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }); } console.log("šŸ” Generating frontend license report..."); try { // Safety guard: don't run this script on fork PRs (workflow setzt PR_IS_FORK) if (process.env.PR_IS_FORK === "true" && !POSTPROCESS_ONLY) { console.error("Fork PR detected: only --input (postprocess-only) mode is allowed."); process.exit(2); } let licenseData; // Generate license report using pinned license-checker; disable lifecycle scripts if (POSTPROCESS_ONLY) { if (!INPUT_FILE || !existsSync(INPUT_FILE)) { console.error("āŒ --input file missing or not found"); process.exit(1); } licenseData = JSON.parse(readFileSync(INPUT_FILE, "utf8")); } else { const licenseReport = execSync( // 'npx --yes license-checker@25.0.1 --production --json', "npx --yes license-report --only=prod --output=json", { encoding: "utf8", cwd: path.dirname(PACKAGE_JSON), env: { ...process.env, NPM_CONFIG_IGNORE_SCRIPTS: "true" }, }, ); try { licenseData = JSON.parse(licenseReport); } catch (parseError) { console.error("āŒ Failed to parse license data:", parseError.message); console.error("Raw output:", licenseReport.substring(0, 500) + "..."); process.exit(1); } } if (!Array.isArray(licenseData)) { console.error("āŒ Invalid license data structure"); process.exit(1); } // Convert license-checker format to array const licenseArray = licenseData.map((dep) => { let licenseType = dep.licenseType; // Handle missing or null licenses if (!licenseType || licenseType === null || licenseType === undefined) { licenseType = "Unknown"; } // Handle empty string licenses if (licenseType === "") { licenseType = "Unknown"; } // Handle array licenses (rare but possible) if (Array.isArray(licenseType)) { licenseType = licenseType.join(" AND "); } // Handle object licenses (fallback) if (typeof licenseType === "object" && licenseType !== null) { licenseType = "Unknown"; } if ("posthog-js" === dep.name && licenseType.startsWith("SEE LICENSE IN LICENSE")) { licenseType = "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE"; } return { name: dep.name, version: dep.installedVersion || dep.definedVersion || dep.remoteVersion || "unknown", licenseType: licenseType, repository: dep.link, url: dep.link, link: dep.link, }; }); // Transform to match Java backend format const transformedData = { dependencies: licenseArray.map((dep) => { const licenseType = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType || "Unknown"; const licenseUrl = dep.link || getLicenseUrl(licenseType); return { moduleName: dep.name, moduleUrl: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`, moduleVersion: dep.version, moduleLicense: licenseType, moduleLicenseUrl: licenseUrl, }; }), }; // Log summary of license types found const licenseSummary = licenseArray.reduce((acc, dep) => { const license = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType || "Unknown"; acc[license] = (acc[license] || 0) + 1; return acc; }, {}); console.log("šŸ“Š License types found:"); Object.entries(licenseSummary).forEach(([license, count]) => { console.log(` ${license}: ${count} packages`); }); // Log any complex or unusual license formats for debugging const complexLicenses = licenseArray.filter( (dep) => dep.licenseType && (dep.licenseType.includes("AND") || dep.licenseType.includes("OR") || dep.licenseType === "Unknown" || dep.licenseType.includes("SEE LICENSE")), ); if (complexLicenses.length > 0) { console.log("\nšŸ” Complex/Edge case licenses detected:"); complexLicenses.forEach((dep) => { console.log(` ${dep.name}@${dep.version}: "${dep.licenseType}"`); }); } // Check for potentially problematic licenses const problematicLicenses = checkLicenseCompatibility(licenseSummary, licenseArray); if (problematicLicenses.length > 0) { console.log("\nāš ļø License compatibility warnings:"); problematicLicenses.forEach((warning) => { console.log(` ${warning.message}`); }); // Write license warnings to a separate file for CI/CD const warningsFile = path.join(__dirname, "..", "src", "assets", "license-warnings.json"); writeFileSync( warningsFile, JSON.stringify( { warnings: problematicLicenses, generated: new Date().toISOString(), }, null, 2, ), ); console.log(`āš ļø License warnings saved to: ${warningsFile}`); } else { console.log("\nāœ… All licenses appear to be corporate-friendly"); } // Write to file writeFileSync(OUTPUT_FILE, JSON.stringify(transformedData, null, 2) + "\n"); console.log(`āœ… License report generated successfully!`); console.log(`šŸ“„ Found ${transformedData.dependencies.length} dependencies`); console.log(`šŸ’¾ Saved to: ${OUTPUT_FILE}`); } catch (error) { console.error("āŒ Error generating license report:", error.message); process.exit(1); } /** * Get standard license URLs for common licenses */ function getLicenseUrl(licenseType) { if (!licenseType || licenseType === "Unknown") return ""; const licenseUrls = { MIT: "https://opensource.org/licenses/MIT", "MIT*": "https://opensource.org/licenses/MIT", "Apache-2.0": "https://www.apache.org/licenses/LICENSE-2.0", "Apache License 2.0": "https://www.apache.org/licenses/LICENSE-2.0", "BSD-3-Clause": "https://opensource.org/licenses/BSD-3-Clause", "BSD-2-Clause": "https://opensource.org/licenses/BSD-2-Clause", BSD: "https://opensource.org/licenses/BSD-3-Clause", "GPL-3.0": "https://www.gnu.org/licenses/gpl-3.0.html", "GPL-2.0": "https://www.gnu.org/licenses/gpl-2.0.html", "LGPL-2.1": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html", "LGPL-3.0": "https://www.gnu.org/licenses/lgpl-3.0.html", ISC: "https://opensource.org/licenses/ISC", "CC0-1.0": "https://creativecommons.org/publicdomain/zero/1.0/", Unlicense: "https://unlicense.org/", "MPL-2.0": "https://www.mozilla.org/en-US/MPL/2.0/", WTFPL: "http://www.wtfpl.net/", Zlib: "https://opensource.org/licenses/Zlib", "Artistic-2.0": "https://opensource.org/licenses/Artistic-2.0", "EPL-1.0": "https://www.eclipse.org/legal/epl-v10.html", "EPL-2.0": "https://www.eclipse.org/legal/epl-2.0/", "CDDL-1.0": "https://opensource.org/licenses/CDDL-1.0", Ruby: "https://www.ruby-lang.org/en/about/license.txt", "Python-2.0": "https://www.python.org/download/releases/2.0/license/", "Public Domain": "https://creativecommons.org/publicdomain/zero/1.0/", UNLICENSED: "", }; // Try exact match first if (licenseUrls[licenseType]) { return licenseUrls[licenseType]; } // Try case-insensitive match const lowerType = licenseType.toLowerCase(); for (const [key, url] of Object.entries(licenseUrls)) { if (key.toLowerCase() === lowerType) { return url; } } // Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" if (licenseType.includes("AND") || licenseType.includes("OR")) { // Extract the first license from compound expressions for URL const match = licenseType.match(/\(?\s*([A-Za-z0-9\-.]+)/); if (match && licenseUrls[match[1]]) { return licenseUrls[match[1]]; } } // For non-standard licenses, return empty string (will use package link if available) return ""; } /** * Check for potentially problematic licenses that may not be MIT/corporate compatible */ function checkLicenseCompatibility(licenseSummary, licenseArray) { const warnings = []; // Define problematic license patterns const problematicLicenses = { // Copyleft licenses "GPL-2.0": "Strong copyleft license - requires derivative works to be GPL", "GPL-3.0": "Strong copyleft license - requires derivative works to be GPL", "LGPL-2.1": "Weak copyleft license - may require source disclosure for modifications", "LGPL-3.0": "Weak copyleft license - may require source disclosure for modifications", "AGPL-3.0": "Network copyleft license - requires source disclosure for network use", "AGPL-1.0": "Network copyleft license - requires source disclosure for network use", // Other potentially problematic licenses WTFPL: "Potentially problematic license - legal uncertainty", "CC-BY-SA-4.0": "ShareAlike license - requires derivative works to use same license", "CC-BY-SA-3.0": "ShareAlike license - requires derivative works to use same license", "CC-BY-NC-4.0": "Non-commercial license - prohibits commercial use", "CC-BY-NC-3.0": "Non-commercial license - prohibits commercial use", "OSL-3.0": "Copyleft license - requires derivative works to be OSL", "EPL-1.0": "Weak copyleft license - may require source disclosure", "EPL-2.0": "Weak copyleft license - may require source disclosure", "CDDL-1.0": "Weak copyleft license - may require source disclosure", "CDDL-1.1": "Weak copyleft license - may require source disclosure", "CPL-1.0": "Weak copyleft license - may require source disclosure", "MPL-1.1": "Weak copyleft license - may require source disclosure", "EUPL-1.1": "Copyleft license - requires derivative works to be EUPL", "EUPL-1.2": "Copyleft license - requires derivative works to be EUPL", UNLICENSED: "No license specified - usage rights unclear", Unknown: "License not detected - manual review required", }; // Known good licenses (no warnings needed) const goodLicenses = new Set([ "MIT", "MIT*", "Apache-2.0", "Apache License 2.0", "BSD-2-Clause", "BSD-3-Clause", "BSD", "ISC", "CC0-1.0", "Public Domain", "Unlicense", "0BSD", "BlueOak-1.0.0", "Zlib", "Artistic-2.0", "Python-2.0", "Ruby", "MPL-2.0", "CC-BY-4.0", "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE", "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE", ]); // Helper function to normalize license names for comparison function normalizeLicense(license) { return license .replace(/-or-later$/, "") // Remove -or-later suffix .replace(/\+$/, "") // Remove + suffix .trim(); } // Check each license type Object.entries(licenseSummary).forEach(([license, count]) => { // Skip known good licenses if (goodLicenses.has(license)) { return; } // Check if this license only affects our own packages const affectedPackages = licenseArray.filter((dep) => { const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType; return depLicense === license; }); const isOnlyOurPackages = affectedPackages.every( (dep) => dep.name === "frontend" || dep.name.toLowerCase().includes("stirling-pdf") || dep.name.toLowerCase().includes("stirling_pdf") || dep.name.toLowerCase().includes("stirlingpdf"), ); if (isOnlyOurPackages && (license === "UNLICENSED" || license.startsWith("SEE LICENSE IN"))) { return; // Skip warnings for our own Stirling-PDF packages } // Check for compound licenses like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" if (license.includes("AND") || license.includes("OR")) { // For OR licenses, check if there's at least one acceptable license option if (license.includes("OR")) { // Extract license components from OR expression const orComponents = license .replace(/[()]/g, "") // Remove parentheses .split(" OR ") .map((component) => component.trim()); // Check if any component is in the goodLicenses set (with normalization) const hasGoodLicense = orComponents.some((component) => { const normalized = normalizeLicense(component); return goodLicenses.has(component) || goodLicenses.has(normalized); }); if (hasGoodLicense) { return; // Skip warning - can use the good license option } } // For AND licenses or OR licenses with no good options, check for problematic components const hasProblematicComponent = Object.keys(problematicLicenses).some((problematic) => license.includes(problematic)); if (hasProblematicComponent) { const affectedPackages = licenseArray .filter((dep) => { const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType; return depLicense === license; }) .map((dep) => ({ name: dep.name, version: dep.version, url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`, })); const licenseType = license.includes("AND") ? "AND" : "OR"; const reason = licenseType === "AND" ? "Compound license with AND requirement - all components must be compatible" : "Compound license with potentially problematic components and no good fallback options"; warnings.push({ message: `šŸ“‹ This PR contains ${count} package${count > 1 ? "s" : ""} with compound license "${license}" - manual review recommended`, licenseType: license, licenseUrl: "", reason: reason, packageCount: count, affectedDependencies: affectedPackages, }); } return; } // Check for exact matches with problematic licenses if (problematicLicenses[license]) { const affectedPackages = licenseArray .filter((dep) => { const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType; return depLicense === license; }) .map((dep) => ({ name: dep.name, version: dep.version, url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`, })); const packageList = affectedPackages .map((pkg) => pkg.name) .slice(0, 5) .join(", ") + (affectedPackages.length > 5 ? `, and ${affectedPackages.length - 5} more` : ""); const licenseUrl = getLicenseUrl(license) || "https://opensource.org/licenses"; warnings.push({ message: `āš ļø This PR contains ${count} package${count > 1 ? "s" : ""} with license type [${license}](${licenseUrl}) - ${problematicLicenses[license]}. Affected packages: ${packageList}`, licenseType: license, licenseUrl: licenseUrl, reason: problematicLicenses[license], packageCount: count, affectedDependencies: affectedPackages, }); } else { // Unknown license type - flag for manual review const affectedPackages = licenseArray .filter((dep) => { const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType; return depLicense === license; }) .map((dep) => ({ name: dep.name, version: dep.version, url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`, })); warnings.push({ message: `ā“ This PR contains ${count} package${count > 1 ? "s" : ""} with unknown license type "${license}" - manual review required`, licenseType: license, licenseUrl: "", reason: "Unknown license type", packageCount: count, affectedDependencies: affectedPackages, }); } }); return warnings; }