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:
1 parent
946b7e44be
commit
f1b63c722c
5 files changed
+3755
-6
No files matched your search
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user