Files
edentext/scripts/collect-licenses.mjs
T
Steffen Becker f1b63c722c feat: the build ships its dependency licenses and its commit
The minifier strips every legal comment, so the bundle reached users without
the notice MIT and Apache-2.0 require to travel with a copy. collect-licenses
quotes all 97 bundled packages into public/licenses.txt, which deploys like the
font and dictionary licenses do. The About dialog's version now carries the
short commit, so a user of the hosted app can identify the source it was built
from - what AGPL 13 asks for and a fixed 0.1.0 could not answer.
2026-08-18 16:44:42 +02:00

81 lines
3.2 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;
}
async function entry(name) {
const pkg = await manifest(name);
const file = (await readdir(`node_modules/${name}`)).find((f) => LICENSE_FILE.test(f));
if (!file) return null;
const text = await readFile(`node_modules/${name}/${file}`, 'utf8');
const who = typeof pkg.author === 'string' ? pkg.author : pkg.author?.name;
return { name, license: pkg.license, who, text: text.trim() };
}
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 file 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`);