// Regenerates public/licenses.txt: the license text of every bundled dependency. // The minifier strips legal comments, and MIT/Apache-2.0 both require the notice // to reach whoever receives the code — so it ships beside the build instead. import { readdir, readFile, writeFile } from 'node:fs/promises'; const LICENSE_FILE = /^(LICENSE|LICENCE|COPYING)(\.\w+)?$/i; // Read from disk, never through require: an export map that hides package.json // would silently drop the package (TipTap, odf-kit and docx all do). const manifest = async (name) => { try { return JSON.parse(await readFile(`node_modules/${name}/package.json`, 'utf8')); } catch { return null; } }; const { dependencies } = JSON.parse(await readFile('package.json', 'utf8')); // Svelte is a devDependency but compiles its runtime into the bundle; the rest of // the dev tooling (Vite, TypeScript, vitest) never reaches a user. const roots = [...Object.keys(dependencies), 'svelte']; // A dependency's own dependencies are bundled too, so walk the whole tree. async function resolveTree(names, seen = new Set()) { for (const name of names) { if (seen.has(name)) continue; const pkg = await manifest(name); if (!pkg) continue; seen.add(name); await resolveTree(Object.keys(pkg.dependencies ?? {}), seen); } return seen; } // Some packages carry their notice only in the README's License section; accept // it as a fallback, but only when it contains an actual copyright line. async function readmeLicense(name) { const md = await readFile(`node_modules/${name}/README.md`, 'utf8').catch(() => ''); const start = md.search(/^#{1,6} *licen[cs]e\b/im); if (start < 0) return null; const body = md.slice(md.indexOf('\n', start) + 1); const next = body.search(/^#{1,6} /m); const text = (next < 0 ? body : body.slice(0, next)) .trim() .replace(/</g, '<') .replace(/>/g, '>'); return /copyright/i.test(text) ? text : null; } async function entry(name) { const pkg = await manifest(name); const file = (await readdir(`node_modules/${name}`)).find((f) => LICENSE_FILE.test(f)); const text = file ? (await readFile(`node_modules/${name}/${file}`, 'utf8')).trim() : await readmeLicense(name); if (!text) return null; const who = typeof pkg.author === 'string' ? pkg.author : pkg.author?.name; return { name, license: pkg.license, who, text }; } const found = []; const missing = []; for (const name of [...(await resolveTree(roots))].sort()) { const found_ = await entry(name); (found_ ? found : missing).push(found_ ?? name); } const header = `Third-party licenses bundled into EdenText ========================================== EdenText itself is licensed under the AGPL-3.0-only; see the LICENSE file in the source repository (linked from the app's About dialog). The components below are compiled into the application bundle and keep their own licenses, reproduced in full. Fonts and language data carry their licenses beside them, under /font-licenses/, /dictionaries/ and /thesaurus/. Generated by scripts/collect-licenses.mjs — do not edit by hand. `; const body = found .map(({ name, license, who, text }) => { const head = `${name}${license ? ` — ${license}` : ''}${who ? ` — ${who}` : ''}`; return `${'-'.repeat(78)}\n${head}\n${'-'.repeat(78)}\n\n${text}\n`; }) .join('\n'); // Packages that ship no license file still declare one; name them rather than // let them vanish from the list. const declared = await Promise.all( missing.map(async (name) => ` ${name} — ${(await manifest(name))?.license ?? 'unknown'}`), ); const tail = declared.length ? `\n${'-'.repeat(78)}\nDeclared in package metadata, no license text shipped upstream\n${'-'.repeat(78)}\n\n${declared.join('\n')}\n` : ''; await writeFile('public/licenses.txt', `${header}\n${body}${tail}`); console.log(`public/licenses.txt: ${found.length} licenses, ${missing.length} declared-only`);