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:coverage # vitest + v8 coverage over src/ → coverage/index.html
|
||||||
npm run test:parity # render parity vs LibreOffice (tests/render-parity/README.md)
|
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/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.
|
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 |
|
| `odf-kit` | Apache-2.0 |
|
||||||
| Vite, TypeScript, `@sveltejs/vite-plugin-svelte`, `@tsconfig/svelte` (dev) | MIT / 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
|
The table above is a convenience summary of the direct dependencies. The
|
||||||
inside `node_modules/<pkg>/`. This table is a convenience summary and may need
|
authoritative text — every bundled package including transitive ones, quoted in
|
||||||
updating when dependencies change.
|
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
|
## Regenerating this list
|
||||||
|
|
||||||
When dependencies change, you can regenerate an up-to-date overview with a tool
|
Regenerate `public/licenses.txt` after any dependency change:
|
||||||
such as [`license-checker`](https://www.npmjs.com/package/license-checker):
|
|
||||||
|
```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
|
```bash
|
||||||
npx license-checker --summary
|
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" />
|
/// <reference types="vitest/config" />
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
import pkg from './package.json' with { type: 'json' };
|
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({
|
export default defineConfig({
|
||||||
// Relative asset URLs, so the same build works at the domain root and under a
|
// 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.
|
// subpath (GitHub Pages project site). sw.js and the manifest are relative too.
|
||||||
base: './',
|
base: './',
|
||||||
plugins: [svelte()],
|
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
|
// 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";
|
// yields as non-callable namespaces (glue → "runtimeModule is not a function";
|
||||||
// nanoid → CANNOT_CALL_NAMESPACE). The CJS builds' require-interop fixes both.
|
// nanoid → CANNOT_CALL_NAMESPACE). The CJS builds' require-interop fixes both.
|
||||||
|
|||||||
Reference in new issue
Block a user