// Generate frontend/dist/THIRD_PARTY_LICENSES.txt: the third-party attribution file for the // npm packages bundled into the built frontend. // // Self-contained (no extra dependency): it asks npm for the *production* dependency tree, then // reads each package's own license file from node_modules and concatenates them. Run via // `npm run licenses` (also invoked by the CMake `frontend` build target). import { execFileSync } from "node:child_process"; import { readFileSync, readdirSync, existsSync, mkdirSync, writeFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const frontendDir = join(dirname(fileURLToPath(import.meta.url)), ".."); const nodeModules = join(frontendDir, "node_modules"); const outFile = join(frontendDir, "dist", "THIRD_PARTY_LICENSES.txt"); const LICENSE_FILE = /^(licen[sc]e|copying|notice)([.-].*)?$/i; // Set of "name@version" strings that actually ship in production (excludes devDependencies). function productionPackages() { const json = execFileSync("npm", ["ls", "--omit=dev", "--all", "--json"], { cwd: frontendDir, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, }); const set = new Set(); const walk = (deps) => { for (const [name, info] of Object.entries(deps || {})) { if (info.version) set.add(`${name}@${info.version}`); walk(info.dependencies); } }; walk(JSON.parse(json).dependencies); return set; } // Map "name@version" -> package directory, by scanning every node_modules in the install tree. function installedPackages() { const map = new Map(); const scan = (nm) => { if (!existsSync(nm)) return; for (const entry of readdirSync(nm)) { if (entry.startsWith(".")) continue; if (entry.startsWith("@")) { for (const sub of readdirSync(join(nm, entry))) record(join(nm, entry, sub)); } else { record(join(nm, entry)); } } }; const record = (dir) => { const pkgJson = join(dir, "package.json"); if (!existsSync(pkgJson)) return; try { const pkg = JSON.parse(readFileSync(pkgJson, "utf8")); if (pkg.name && pkg.version) map.set(`${pkg.name}@${pkg.version}`, { dir, pkg }); } catch { /* ignore unparseable package.json */ } scan(join(dir, "node_modules")); }; scan(nodeModules); return map; } function findLicenseText(dir) { const file = readdirSync(dir).find((f) => LICENSE_FILE.test(f)); if (!file) return null; try { return readFileSync(join(dir, file), "utf8").trimEnd(); } catch { return null; } } function licenseId(pkg) { if (typeof pkg.license === "string") return pkg.license; if (pkg.license?.type) return pkg.license.type; if (Array.isArray(pkg.licenses)) return pkg.licenses.map((l) => l.type || l).join(" OR "); return "UNKNOWN"; } function repoUrl(pkg) { const r = pkg.repository; if (typeof r === "string") return r; if (r?.url) return r.url.replace(/^git\+/, "").replace(/\.git$/, ""); return pkg.homepage || ""; } const prod = productionPackages(); const installed = installedPackages(); const blocks = []; const missing = []; for (const id of [...prod].sort((a, b) => a.localeCompare(b))) { const found = installed.get(id); if (!found) continue; // present in tree report but not on disk (e.g. bundled) — skip const { dir, pkg } = found; const text = findLicenseText(dir); if (!text) missing.push(`${id} (${licenseId(pkg)})`); const url = repoUrl(pkg); blocks.push( `${"=".repeat(80)}\n` + `${id} — ${licenseId(pkg)}\n` + (url ? `${url}\n` : "") + `${"=".repeat(80)}\n\n` + (text || `License: ${licenseId(pkg)} (no license file shipped in this package)`), ); } const header = "Third-party software bundled in the Jungfraujoch frontend\n" + "=========================================================\n\n" + "The Jungfraujoch frontend is licensed under GPL-3.0. It bundles the following npm packages\n" + "(production dependencies). Each package's license and notice text follows.\n\n" + `Generated by scripts/generate-third-party-licenses.mjs — ${prod.size} packages.\n` + (missing.length ? `\nPackages without a bundled license file (license field shown above):\n ${missing.join("\n ")}\n` : ""); mkdirSync(dirname(outFile), { recursive: true }); writeFileSync(outFile, header + "\n" + blocks.join("\n\n") + "\n", "utf8"); console.log(`Wrote ${outFile} (${blocks.length} packages, ${missing.length} without license file).`);