Files
edentext/scripts/collect-licenses.mjs
T
Steffen Becker c336127006 fix: ship every bundled MIT notice and link licenses.txt in About
collect-licenses falls back to the README's License section (only when it
carries a copyright line) for packages without a LICENSE file; hash.js and
isarray now ship their full notice. The About dialog links /licenses.txt,
which was previously reachable only by knowing the URL.
2026-08-20 15:15:52 +02:00

98 lines
3.9 KiB
JavaScript

// 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(/&lt;/g, '<')
.replace(/&gt;/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`);