2025-07-18 13:50:40 +01:00
#!/usr/bin/env node
2025-09-26 19:11:03 +01:00
const { execSync } = require ( 'node:child_process' );
const { existsSync , mkdirSync , writeFileSync , readFileSync } = require ( 'node:fs' );
const path = require ( 'node:path' );
2025-09-24 14:07:27 +02:00
2025-09-26 19:11:03 +01:00
const { argv } = require ( 'node:process' );
2025-09-24 14:07:27 +02:00
const inputIdx = argv . indexOf ( '--input' );
const INPUT_FILE = inputIdx > - 1 ? argv [ inputIdx + 1 ] : null ;
const POSTPROCESS_ONLY = !! INPUT_FILE ;
2025-09-26 19:11:03 +01:00
// __dirname is available in CommonJS by default
2025-07-18 13:50:40 +01:00
/**
* 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 );
2025-09-24 14:07:27 +02:00
if ( ! existsSync ( outputDir )) {
mkdirSync ( outputDir , { recursive : true });
2025-07-18 13:50:40 +01:00
}
console . log ( '🔍 Generating frontend license report...' );
try {
2025-09-24 14:07:27 +02:00
// 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 );
2025-07-18 13:50:40 +01:00
}
let licenseData ;
2025-09-24 14:07:27 +02:00
// 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' );
2025-07-18 13:50:40 +01:00
process . exit ( 1 );
2025-09-24 14:07:27 +02:00
}
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 );
}
2025-07-18 13:50:40 +01:00
}
2025-08-28 15:42:33 +01:00
2025-09-24 14:07:27 +02:00
if ( ! Array . isArray ( licenseData )) {
2025-07-18 13:50:40 +01:00
console . error ( '❌ Invalid license data structure' );
process . exit ( 1 );
}
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// Convert license-checker format to array
2025-09-24 14:07:27 +02:00
const licenseArray = licenseData . map ( dep => {
let licenseType = dep . licenseType ;
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// Handle missing or null licenses
if ( ! licenseType || licenseType === null || licenseType === undefined ) {
licenseType = 'Unknown' ;
}
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// Handle empty string licenses
if ( licenseType === '' ) {
licenseType = 'Unknown' ;
}
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// Handle array licenses (rare but possible)
if ( Array . isArray ( licenseType )) {
licenseType = licenseType . join ( ' AND ' );
}
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// Handle object licenses (fallback)
if ( typeof licenseType === 'object' && licenseType !== null ) {
licenseType = 'Unknown' ;
}
2025-08-28 15:42:33 +01:00
2025-09-24 14:07:27 +02:00
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" ;
}
2025-07-18 13:50:40 +01:00
return {
2025-09-24 14:07:27 +02:00
name : dep . name ,
version : dep . installedVersion || dep . definedVersion || dep . remoteVersion || 'unknown' ,
2025-07-18 13:50:40 +01:00
licenseType : licenseType ,
2025-09-24 14:07:27 +02:00
repository : dep . link ,
url : dep . link ,
link : dep . link
2025-07-18 13:50:40 +01:00
};
});
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// 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 );
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
return {
moduleName : dep . name ,
moduleUrl : dep . repository || dep . url || `https://www.npmjs.com/package/ ${ dep . name } ` ,
moduleVersion : dep . version ,
moduleLicense : licenseType ,
moduleLicenseUrl : licenseUrl
};
})
};
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// 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 ;
}, {});
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
console . log ( '📊 License types found:' );
Object . entries ( licenseSummary ). forEach (([ license , count ]) => {
console . log ( ` ${ license } : ${ count } packages` );
});
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// Log any complex or unusual license formats for debugging
2025-08-28 15:42:33 +01:00
const complexLicenses = licenseArray . filter ( dep =>
2025-07-18 13:50:40 +01:00
dep . licenseType && (
2025-08-28 15:42:33 +01:00
dep . licenseType . includes ( 'AND' ) ||
dep . licenseType . includes ( 'OR' ) ||
2025-07-18 13:50:40 +01:00
dep . licenseType === 'Unknown' ||
dep . licenseType . includes ( 'SEE LICENSE' )
)
);
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
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 } ` );
});
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// Write license warnings to a separate file for CI/CD
const warningsFile = path . join ( __dirname , '..' , 'src' , 'assets' , 'license-warnings.json' );
2025-09-24 14:07:27 +02:00
writeFileSync ( warningsFile , JSON . stringify ({
2025-07-18 13:50:40 +01:00
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
2025-09-24 14:07:27 +02:00
writeFileSync ( OUTPUT_FILE , JSON . stringify ( transformedData , null , 4 ));
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
console . log ( `✅ License report generated successfully!` );
console . log ( `📄 Found ${ transformedData . dependencies . length } dependencies` );
console . log ( `💾 Saved to: ${ OUTPUT_FILE } ` );
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
} 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 '' ;
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
const licenseUrls = {
'MIT' : 'https://opensource.org/licenses/MIT' ,
2025-08-28 15:42:33 +01:00
'MIT*' : 'https://opensource.org/licenses/MIT' ,
2025-07-18 13:50:40 +01:00
'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' : ''
};
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// Try exact match first
if ( licenseUrls [ licenseType ]) {
return licenseUrls [ licenseType ];
}
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// Try case-insensitive match
const lowerType = licenseType . toLowerCase ();
for ( const [ key , url ] of Object . entries ( licenseUrls )) {
if ( key . toLowerCase () === lowerType ) {
return url ;
}
}
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// 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
2025-09-04 16:04:49 +01:00
const match = licenseType . match ( /\(?\s*([A-Za-z0-9\-.]+)/ );
2025-07-18 13:50:40 +01:00
if ( match && licenseUrls [ match [ 1 ]]) {
return licenseUrls [ match [ 1 ]];
}
}
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// 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 = [];
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// 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' ,
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// 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'
};
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// Known good licenses (no warnings needed)
const goodLicenses = new Set ([
2025-08-28 15:42:33 +01:00
'MIT' , 'MIT*' , 'Apache-2.0' , 'Apache License 2.0' , 'BSD-2-Clause' , 'BSD-3-Clause' , 'BSD' ,
2025-07-18 13:50:40 +01:00
'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' ,
2025-09-24 14:07:27 +02:00
'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'
2025-07-18 13:50:40 +01:00
]);
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// Helper function to normalize license names for comparison
function normalizeLicense ( license ) {
return license
. replace ( /-or-later$/ , '' ) // Remove -or-later suffix
2025-08-28 15:42:33 +01:00
. replace ( /\+$/ , '' ) // Remove + suffix
2025-07-18 13:50:40 +01:00
. trim ();
}
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// Check each license type
Object . entries ( licenseSummary ). forEach (([ license , count ]) => {
// Skip known good licenses
if ( goodLicenses . has ( license )) {
return ;
}
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// 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 ;
});
2025-08-28 15:42:33 +01:00
const isOnlyOurPackages = affectedPackages . every ( dep =>
dep . name === 'frontend' ||
2025-07-18 13:50:40 +01:00
dep . name . toLowerCase (). includes ( 'stirling-pdf' ) ||
dep . name . toLowerCase (). includes ( 'stirling_pdf' ) ||
dep . name . toLowerCase (). includes ( 'stirlingpdf' )
);
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
if ( isOnlyOurPackages && ( license === 'UNLICENSED' || license . startsWith ( 'SEE LICENSE IN' ))) {
return ; // Skip warnings for our own Stirling-PDF packages
}
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// 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 ());
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// 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 );
});
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
if ( hasGoodLicense ) {
return ; // Skip warning - can use the good license option
}
}
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// For AND licenses or OR licenses with no good options, check for problematic components
2025-08-28 15:42:33 +01:00
const hasProblematicComponent = Object . keys ( problematicLicenses ). some ( problematic =>
2025-07-18 13:50:40 +01:00
license . includes ( problematic )
);
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
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 } `
}));
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
const licenseType = license . includes ( 'AND' ) ? 'AND' : 'OR' ;
2025-08-28 15:42:33 +01:00
const reason = licenseType === 'AND'
2025-07-18 13:50:40 +01:00
? 'Compound license with AND requirement - all components must be compatible'
: 'Compound license with potentially problematic components and no good fallback options' ;
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
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 ;
}
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
// 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 } `
}));
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
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' ;
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
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 } `
}));
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
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
});
}
});
2025-08-28 15:42:33 +01:00
2025-07-18 13:50:40 +01:00
return warnings ;
2025-08-28 15:42:33 +01:00
}