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.
This commit is contained in:
Steffen Becker committed 2026-08-18 16:44:42 +02:00
1 parent 946b7e44be
commit f1b63c722c
5 files changed
+3755 -6

No files matched your search

+1
View File
@@ -20,6 +20,7 @@ npm run test:smoke # boots the dist/ build in headless Chromium (tests/smoke/r
npm run test:coverage # vitest + v8 coverage over src/ → coverage/index.html
npm run test:parity # render parity vs LibreOffice (tests/render-parity/README.md)
node scripts/make-thesaurus.mjs # re-vendor public/thesaurus/ from LibreOffice's MyThes data
node scripts/collect-licenses.mjs # regenerate public/licenses.txt after a dependency change
```
Tests live in `tests/` (outside `src/`, so `svelte-check` ignores them), jsdom via Vitest.
+13 -5
View File
@@ -63,14 +63,22 @@ Both thesaurus files are generated from LibreOffice's own MyThes data by
| `odf-kit` | Apache-2.0 |
| Vite, TypeScript, `@sveltejs/vite-plugin-svelte`, `@tsconfig/svelte` (dev) | MIT / Apache-2.0 |
The authoritative license for each dependency is the one shipped in its package
inside `node_modules/<pkg>/`. This table is a convenience summary and may need
updating when dependencies change.
The table above is a convenience summary of the direct dependencies. The
authoritative text — every bundled package including transitive ones, quoted in
full — is [`public/licenses.txt`](./public/licenses.txt), which ships with the
build and is what MIT and Apache-2.0 require to reach a recipient: the minifier
strips the license comments the source files carry.
## Regenerating this list
When dependencies change, you can regenerate an up-to-date overview with a tool
such as [`license-checker`](https://www.npmjs.com/package/license-checker):
Regenerate `public/licenses.txt` after any dependency change:
```bash
node scripts/collect-licenses.mjs
```
For an overview of the license *kinds* in the tree, a tool such as
[`license-checker`](https://www.npmjs.com/package/license-checker) works:
```bash
npx license-checker --summary
+3645
View File
File diff suppressed because it is too large. Load diff
+80
View File
@@ -0,0 +1,80 @@
// 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`);
+16 -1
View File
@@ -1,14 +1,29 @@
/// <reference types="vitest/config" />
import { execSync } from 'node:child_process';
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import pkg from './package.json' with { type: 'json' };
// The commit the build came from, so a user of the hosted app can identify the
// corresponding source (AGPL §13). Absent when building outside a git checkout.
const commit = (() => {
try {
return execSync('git rev-parse --short HEAD', { stdio: ['ignore', 'pipe', 'ignore'] })
.toString()
.trim();
} catch {
return '';
}
})();
export default defineConfig({
// Relative asset URLs, so the same build works at the domain root and under a
// subpath (GitHub Pages project site). sw.js and the manifest are relative too.
base: './',
plugins: [svelte()],
define: { __APP_VERSION__: JSON.stringify(pkg.version) },
define: {
__APP_VERSION__: JSON.stringify(commit ? `${pkg.version} (${commit})` : pkg.version),
},
// hunspell-asm's ESM chain calls CJS/UMD deps as functions that a bundler
// yields as non-callable namespaces (glue → "runtimeModule is not a function";
// nanoid → CANNOT_CALL_NAMESPACE). The CJS builds' require-interop fixes both.