#!/usr/bin/env node import { execSync } from 'node:child_process'; import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import { argv } from 'node:process'; const inputIdx = argv.indexOf('--input'); const INPUT_FILE = inputIdx > -1 ? argv[inputIdx + 1] : null; const POSTPROCESS_ONLY = !!INPUT_FILE; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * 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, 4)); 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; }