Stirling-PDF/frontend/scripts/generate-licenses.js
Anthony Stirling 0742364a03
V2 frontend license checker (#3944)
# Added scripts for checking the licenses of dependencies similar to the backend app
2025-07-18 13:50:40 +01:00

415 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
/**
* 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 (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
console.log('🔍 Generating frontend license report...');
try {
// Install license-checker if not present
try {
require.resolve('license-checker');
} catch (e) {
console.log('📦 Installing license-checker...');
execSync('npm install --save-dev license-checker', { stdio: 'inherit' });
}
// Generate license report using license-checker (more reliable)
const licenseReport = execSync('npx license-checker --production --json', {
encoding: 'utf8',
cwd: path.dirname(PACKAGE_JSON)
});
let licenseData;
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 (!licenseData || typeof licenseData !== 'object') {
console.error('❌ Invalid license data structure');
process.exit(1);
}
// Convert license-checker format to array
const licenseArray = Object.entries(licenseData).map(([key, value]) => {
let name, version;
// Handle scoped packages like @mantine/core@1.0.0
if (key.startsWith('@')) {
const parts = key.split('@');
name = `@${parts[1]}`;
version = parts[2];
} else {
// Handle regular packages like react@18.0.0
const lastAtIndex = key.lastIndexOf('@');
name = key.substring(0, lastAtIndex);
version = key.substring(lastAtIndex + 1);
}
// Normalize license types for edge cases
let licenseType = value.licenses;
// 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';
}
return {
name: name,
version: version || value.version || 'unknown',
licenseType: licenseType,
repository: value.repository,
url: value.url,
link: value.licenseUrl
};
});
// 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');
fs.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
fs.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',
'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', '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'
]);
// 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;
}