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.
81 lines
3.2 KiB
JavaScript
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`);
|