Files
edentext/scripts/collect-licenses.mjs
Steffen Becker 629408e8fc docs: the vendored count.js carries its ISC notice in licenses.txt
The collector walks node_modules, so a file copied into public/ had no entry.
The copy has no copyright line of its own; the text is upstream's LICENSE
section for count.js, with the project's copyright holder.
2026-09-06 21:20:44 +02:00

118 lines
4.8 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;
// Copied into public/ rather than installed, so no package.json carries the notice.
const VENDORED = [{
name: 'public/count.js (GoatCounter)',
license: 'ISC',
who: 'Martin Tournoij <martin@arp242.net>',
text: `Copyright © Martin Tournoij <martin@arp242.net>
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.`,
}];
// 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 = [...VENDORED];
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`);