mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 18:44:05 +02:00
# Description of Changes Changes the strategy for autoformatting to reject PRs if they are not formatted correctly instead of allowing them to merge and then spawning a new PR to fix the formatting. The old strategy just caused more work for us because we'd have to manually approve the followup PR and get it merged, which required 2 reviewers so in practice it rarely got done and just meant everyone's PRs ended up containing reformatting for unrelated files, which makes code review unnecessarily difficult. If the PR's code is not formatted correctly after this PR, a comment will be added automatically to tell the author how to run the formatter script to fix their code so it can go in. This also enables autoformatting for the frontend code, using Prettier. I've enabled it for pretty much everything in the frontend folder, other than 3rd party files and files it doesn't make sense for. I also excluded Markdown because it sounds likely to be more annoying to have to autoformat the Markdown in the frontend folder but nowhere else. Open to changing this though if people disagree. > [!note] > > Advice to reviewers: The first commit contains all of the actual logic I've introduced (CI changes, Prettier config, etc.) > The second commit is just the reformatting of the entire frontend folder. > The first commit needs proper review, the second one just give it a spot-check that it's doing what you'd expect.
450 lines
16 KiB
JavaScript
450 lines
16 KiB
JavaScript
#!/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 [email protected] --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;
|
|
}
|