Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m51s
CI / Build container images (pull_request) Successful in 1m13s
CI / Auth e2e pack (pull_request) Successful in 9m28s
CI / Import/export fidelity gate (pull_request) Successful in 54s
CD / Build and push images (push) Successful in 15s
CD / Deploy to Test (push) Successful in 16s
CD / Smoke tests against Test (push) Successful in 1m18s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m57s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 9m7s
CI / Import/export fidelity gate (push) Successful in 57s
Restore drill / Restore the latest backup into a scratch stack (push) Failing after 17s
The drawio, excalidraw, and mermaid plugin packages redistribute third-party material (the draw.io webapp, the Excalidraw editor and its fonts, mermaid and its dependency tree) without the license texts their licenses require. Every affected ZIP now carries a licenses/ directory: - licenses/THIRD-PARTY-NOTICES.txt is generated from the esbuild metafile (packages/plugins/third-party-licenses.mjs), so the notice list is derived from what actually lands in plugin.js and cannot drift the way a hand-maintained list would. - drawio additionally extracts the upstream LICENSE from the pinned release tarball (Apache-2.0 requires the text with redistribution); the extraction guard also heals vendor/ caches from before this change. The CI fast path (no vendor fetch, no ZIP) is unchanged. - excalidraw additionally commits curated texts (MIT for Excalidraw, per-font OFL-1.1/MIT with each font's own copyright statement, plus a FONT-NOTICES.md attribution table), because neither the npm package nor upstream ships any license files for them. The api-side package validator accepts additional ZIP entries, so installed plugins are unaffected beyond the new files. Closes #345 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aoPvnakfBP28nAfijgUY9
83 lines
3.3 KiB
JavaScript
83 lines
3.3 KiB
JavaScript
// Third-party license notices for plugin ZIPs (issue #345). A plugin that
|
|
// redistributes third-party material must ship the license texts alongside it
|
|
// (Apache-2.0 §4(a), MIT's notice clause, OFL §2). The bundled-package list is
|
|
// derived from the esbuild metafile — the set of files that actually ended up
|
|
// in plugin.js — so the notices can never drift from the bundle the way a
|
|
// hand-maintained list would. Non-bundled material (vendored webapps, copied
|
|
// font assets) cannot appear in a metafile; callers pass those as `extras`.
|
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
|
|
const LICENSE_FILE_PATTERN = /^(licen[cs]e|copying|notice)(\.|$)/i;
|
|
|
|
/** Walk up from `file` to the nearest package.json that names a package. */
|
|
function packageRootOf(file) {
|
|
let dir = dirname(resolve(file));
|
|
while (dir !== dirname(dir)) {
|
|
const pj = join(dir, 'package.json');
|
|
if (existsSync(pj)) {
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(pj, 'utf8'));
|
|
if (parsed.name) return { dir, pkg: parsed };
|
|
} catch {
|
|
// unreadable package.json (e.g. a fixture) — keep walking up
|
|
}
|
|
}
|
|
dir = dirname(dir);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function shippedLicenseText(dir) {
|
|
const names = readdirSync(dir).filter((name) => LICENSE_FILE_PATTERN.test(name));
|
|
return names
|
|
.sort()
|
|
.map((name) => readFileSync(join(dir, name), 'utf8').trim())
|
|
.join('\n\n');
|
|
}
|
|
|
|
/**
|
|
* All third-party npm packages whose files the metafile lists as bundle
|
|
* inputs, deduplicated by name@version. First-party `@dorfteich/*` packages
|
|
* are covered by the repository LICENSE and skipped.
|
|
*/
|
|
export function bundledPackages(metafile) {
|
|
const seen = new Map();
|
|
for (const input of Object.keys(metafile.inputs)) {
|
|
if (!input.split(sep).includes('node_modules') && !input.includes('/node_modules/')) continue;
|
|
const found = packageRootOf(input);
|
|
if (!found || found.pkg.name.startsWith('@dorfteich/')) continue;
|
|
const key = `${found.pkg.name}@${found.pkg.version}`;
|
|
if (!seen.has(key)) {
|
|
seen.set(key, {
|
|
name: found.pkg.name,
|
|
version: found.pkg.version,
|
|
license: typeof found.pkg.license === 'string' ? found.pkg.license : 'see license text',
|
|
text: shippedLicenseText(found.dir),
|
|
});
|
|
}
|
|
}
|
|
return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
/**
|
|
* Renders `licenses/THIRD-PARTY-NOTICES.txt` for a plugin ZIP: one section per
|
|
* bundled package (license expression + the license file it ships), then one
|
|
* per caller-supplied extra ({ title, license, note?, text? }).
|
|
*/
|
|
export function thirdPartyNotices(metafile, extras = []) {
|
|
const rule = '='.repeat(72);
|
|
const sections = [
|
|
'THIRD-PARTY NOTICES\n\nThis plugin package redistributes the third-party components listed\nbelow, each under its own license.\n',
|
|
];
|
|
for (const pkg of bundledPackages(metafile)) {
|
|
const body = pkg.text || `License: ${pkg.license} (no license file shipped in the npm package)`;
|
|
sections.push(`${rule}\n${pkg.name} ${pkg.version} — ${pkg.license}\n${rule}\n\n${body}\n`);
|
|
}
|
|
for (const extra of extras) {
|
|
const parts = [extra.note, extra.text].filter(Boolean).join('\n\n');
|
|
sections.push(`${rule}\n${extra.title} — ${extra.license}\n${rule}\n\n${parts}\n`);
|
|
}
|
|
return sections.join('\n');
|
|
}
|