desktop/scripts/lca/generate-third-party.ts (204 lines of code) (raw):
// eslint-disable no-console
import { program } from "commander";
import * as fs from "fs";
import * as path from "path";
import { Constants } from "../../src/client/client-constants";
interface ThirdPartyNoticeOptions {
check?: boolean;
}
interface Dependency {
name: string;
version: string;
url: string;
repoUrl?: string | null;
licenseType: string;
}
interface License {
content: string;
}
const defaultThirdPartyNoticeOptions: ThirdPartyNoticeOptions = {
check: false,
};
const thirdPartyNoticeFile = path.join(Constants.root, "ThirdPartyNotices.txt");
const output: string[] = [];
const gitUrlRegex = /(?:git|ssh|https?|git@[-\w.]+):(\/\/[-\w@.]+\/)?(.*?)(\.git)?(\/?|#[-\d\w._]+?)$/;
const repoNameRegex = /https?:\/\/github\.com\/(.*)/;
const innerSeparator = "-".repeat(60);
const outerSeparator = "=".repeat(60);
const defaultLicenseRoot = path.join(__dirname, "default-licenses");
const defaultLicenses = {
"mit": "mit.txt",
"bsd-2-clause": "bsd-2-clause.txt",
"(ofl-1.1 and mit)": "ofl-1.1-and-mit.txt",
"apache-2.0": "apache-2.0.txt",
"electron": "electron.txt",
};
const additionalDependencies: Dependency[] = [
{
name: "node.js",
version: "18.16.1",
url: "https://nodejs.org/en/",
repoUrl: "https://github.com/nodejs/node",
licenseType: "MIT",
},
{
name: "Electron",
version: "26.1.0",
url: "https://electronjs.org/",
repoUrl: "https://github.com/electron/electron",
licenseType: "electron",
},
{
name: "Chromium",
version: "116.0",
url: "https://www.chromium.org/",
repoUrl: "https://github.com/chromium/chromium",
licenseType: "BSD-3-Clause"
}
];
function listDependencies(): string[] {
const packageJsonPath = path.join(Constants.root, "package.json");
const batchExplorerPackage = JSON.parse(fs.readFileSync(packageJsonPath).toString());
const dependencies: string[] = Object.keys(batchExplorerPackage.dependencies);
return dependencies.sort((a, b) => {
if (a < b) { return -1; }
if (a > b) { return 1; }
return 0;
});
}
function loadDependency(name: string): Dependency {
const contents = fs.readFileSync(`node_modules/${name}/package.json`).toString();
const dependency = JSON.parse(contents);
const repoUrl = getRepoUrl(dependency);
const url = dependency.homepage || repoUrl;
return {
name: dependency.name,
version: dependency.version,
url,
repoUrl,
licenseType: dependency.license,
};
}
function getRepoUrl(dependency) {
const repo = dependency.repository;
if (!repo) { return null; }
if (typeof repo === "string") {
if (repo.startsWith(`https://github.com/`)) {
return repo;
}
return `https://github.com/${repo.replace(/^github:/, "")}`;
}
const match = gitUrlRegex.exec(repo.url);
if (!match) { return null; }
return `https://github.com/${match[2]}`;
}
function getRepoName(repoUrl: string | null): string | null {
if (!repoUrl) { return null; }
const match = repoNameRegex.exec(repoUrl);
if (!match) {
console.error("Couldn't get repo name for ", repoUrl);
return null;
}
const value = match[1];
return value.split("/").slice(0, 2).join("/");
}
async function loadLicense(dependency: Dependency, anonymous = false):
Promise<License> {
const { repoUrl = null } = dependency;
const repoName = getRepoName(repoUrl);
const licenseUrl = `https://api.github.com/repos/${repoName}/license`;
const headers: HeadersInit = anonymous ? {} :
{ Authorization: `token ${process.env.GH_TOKEN}` };
return fetch(licenseUrl, { headers }).then(async (res) => {
/** Will look up default license if cannot find a license */
if (!anonymous && res.status === 403) {
console.warn(`Access denied, retrying request anonymously. Url: ${licenseUrl}`);
return loadLicense(dependency, anonymous = true);
} else if (res.status >= 300 && res.status != 404) {
throw new Error(`Response status ${res.status} ${res.statusText} with url: ${licenseUrl}`);
} else if (res.status === 404) {
console.warn(`No license found for ${repoName}. Using default license ${dependency.licenseType} instead.`);
}
return await res.json();
}).catch((error) => {
console.error(`Error loading license for ${repoName}`, error);
process.exit(1);
});
}
function decode64(content: string) {
return Buffer.from(content, "base64").toString();
}
function getHeader() {
return fs.readFileSync(path.join(__dirname, "header.txt")).toString();
}
function getLicenseContent(dependency, license): string | null {
if (!license.content) {
const licenseType = dependency?.licenseType.toLowerCase();
if (Object.keys(defaultLicenses).includes(licenseType)) {
const licenseFilePath =
path.join(defaultLicenseRoot, defaultLicenses[licenseType]);
const licenseContent = fs.readFileSync(licenseFilePath).toString();
return licenseContent;
} else {
console.warn(`Repo ${dependency.name} doesn't have a license file`
+ ` for ${licenseType} and no default provided`);
return null;
}
} else {
return decode64(license.content);
}
}
function checkNoticeUpToDate(notices: string) {
const existingNotices = fs.readFileSync(thirdPartyNoticeFile).toString();
if (existingNotices === notices) {
console.log("ThirdPartyNotice.txt is up to date.");
process.exit(0);
} else {
console.error("ThirdPartyNotice.txt is not up to date."
+ " Please run 'npm run ts scripts/lca/generate-third-party'");
process.exit(1);
}
}
function run(options: ThirdPartyNoticeOptions = {}) {
options = { ...defaultThirdPartyNoticeOptions, ...options };
output.push(getHeader());
output.push("");
const dependencyNames = listDependencies();
const dependencies = dependencyNames
.map(dep => loadDependency(dep))
.concat(additionalDependencies);
console.log("Loading dependencies...");
const toc = dependencies.map((dependency, index) => {
return `${index + 1}. ${dependency.name} (${dependency.url}) - ${dependency.licenseType}`;
});
output.push(toc.join("\n"));
output.push("");
const licensePromises = dependencies.map((dependency) => {
return loadLicense(dependency);
});
console.log("Loading licenses...");
Promise.all(licensePromises).then((licenses) => {
for (const [i, license] of licenses.entries()) {
const dependency = dependencies[i];
output.push(outerSeparator);
output.push(` Start license for ${dependency.name}`);
output.push(innerSeparator);
const licenseContent = getLicenseContent(dependency, license);
output.push(licenseContent || "[NO LICENSE]");
output.push(innerSeparator);
output.push(` End license for ${dependency.name}`);
output.push(outerSeparator);
output.push("");
}
const notices = output.join("\n");
if (options.check) {
checkNoticeUpToDate(notices);
} else {
fs.writeFileSync(thirdPartyNoticeFile, notices);
console.log(`Generated third party notice file at ${thirdPartyNoticeFile}`);
}
});
}
const options = program
.option("-c, --check",
"Check the current third party notice file is valid.")
.parse(process.argv);
run({ check: options.getOptionValue("check") });