#!/usr/bin/env -S node -r esbin import fs from "node:fs" import { resolve } from "node:path" import { load } from "js-yaml" process.chdir(resolve(__dirname, "..")) const candidates = [ "master/LICENSE", "master/LICENCE", "master/LICENSE.md", "master/license.md", "main/LICENSE.txt", ] async function getLicense(repo: string) { for (const candidate of candidates) { const path = `https://raw.githubusercontent.com/${repo}/${candidate}` const res = await fetch(path) if (!res.ok) continue const text = await res.text() return text } throw new Error(`Could not find license for ${repo}`) } async function getSDPXLicense(license: string) { const path = `https://raw.githubusercontent.com/spdx/license-list-data/main/text/${license}.txt` const res = await fetch(path) if (!res.ok) throw new Error(`Could not find license ${license}`) const text = await res.text() return text } function getVendorLicenses() { return fs .readdirSync("src/vendor") .map(name => ({ name, path: `src/vendor/${name}/LICENSE` })) .filter(({ path }) => fs.existsSync(path)) .map(({ name, path }) => [name, fs.readFileSync(path, "utf8")]) } async function getPackageLicense(pkg: string) { const files = ["LICENSE", "LICENCE", "license.md"] for (const file of files) { const path = `node_modules/${pkg}/${file}` if (fs.existsSync(path)) { return fs.readFileSync(path, "utf8") } } const pkgJson = JSON.parse(fs.readFileSync(`node_modules/${pkg}/package.json`, "utf8")) const licenseName = pkgJson.license as string if (!licenseName) throw new Error(`Could not find license for ${pkg}`) return getSDPXLicense(licenseName) } function getPackageLicenses() { const { dependencies = {}, devDependencies = {}, peerDependencies = {}, } = JSON.parse(fs.readFileSync("package.json", "utf8")) const packages = Object.keys({ ...dependencies, ...devDependencies, ...peerDependencies, }) return Promise.all(packages.map(async pkg => [pkg, await getPackageLicense(pkg)])) } class Licenses { libs = new Set() constructor(readonly json: Record) {} set(name: string, license: string) { this.libs.add(name) this.json[name] = license } async setIfAbsent(name: string, license: () => Promise) { if (!this.json[name]) { const text = await license() this.set(name, text) } } assign(others: Record) { for (const [name, license] of Object.entries(others)) { this.set(name, license) } } purge() { for (const name of Object.keys(this.json)) { if (!this.libs.has(name)) { delete this.json[name] } } } toJSON() { return this.json } } async function main() { const outPath = "src/generated/licenses.json" if (!fs.existsSync(outPath)) { fs.writeFileSync(outPath, "{}") } const res = new Licenses( JSON.parse(fs.readFileSync(outPath, "utf8")) as Record, ) const config = load(fs.readFileSync("src/licenses.yml", "utf8")) as { github: string[] licenses: { [name: string]: string } } // res.set("archive", await getSPLicense("AGPL-3.0-or-later")); for (const repo of config.github) { await res.setIfAbsent(repo, () => getLicense(repo)) } for (const [name, license] of Object.entries(config.licenses)) { await res.setIfAbsent(name, () => getSDPXLicense(license)) } res.assign(Object.fromEntries(getVendorLicenses())) res.assign(Object.fromEntries(await getPackageLicenses())) res.purge() fs.writeFileSync( outPath, JSON.stringify( Object.fromEntries( Object.entries(res.toJSON()).sort(([a], [b]) => a.localeCompare(b)), ), null, 2, ) + "\n", ) } void main()