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.
118 lines
4.8 KiB
JavaScript
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(/</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 = [...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`);
|