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
142 lines
5.4 KiB
JavaScript
142 lines
5.4 KiB
JavaScript
// Builds the installable Excalidraw plugin. Unlike draw.io (which vendors a
|
|
// standalone webapp) Excalidraw is an npm React component: esbuild bundles the
|
|
// controller + React + Excalidraw into plugin.js, and its font/worker assets
|
|
// are copied from node_modules into `excalidraw-assets/` so the runtime loads
|
|
// them from the plugin's own asset path (EXCALIDRAW_ASSET_PATH) — nothing ever
|
|
// talks to excalidraw.com, and the sandbox CSP pins every request to self.
|
|
import { createRequire } from 'node:module';
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { dirname, join, relative } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { build } from 'esbuild';
|
|
import { zipSync } from 'fflate';
|
|
|
|
import { thirdPartyNotices } from '../third-party-licenses.mjs';
|
|
|
|
const root = dirname(fileURLToPath(import.meta.url));
|
|
const manifest = JSON.parse(readFileSync(join(root, 'manifest.json'), 'utf8'));
|
|
const require = createRequire(import.meta.url);
|
|
|
|
// --- 1. Locate Excalidraw's prebuilt font/worker assets --------------------
|
|
// The package's `exports` map hides ./package.json, so resolve the main entry
|
|
// and walk up to the package root instead.
|
|
function packageRootOf(specifier) {
|
|
let dir = dirname(require.resolve(specifier));
|
|
while (dir !== dirname(dir)) {
|
|
const pj = join(dir, 'package.json');
|
|
if (existsSync(pj)) {
|
|
try {
|
|
if (JSON.parse(readFileSync(pj, 'utf8')).name === specifier) return dir;
|
|
} catch {
|
|
// keep walking up
|
|
}
|
|
}
|
|
dir = dirname(dir);
|
|
}
|
|
throw new Error(`package root not found for ${specifier}`);
|
|
}
|
|
const excalidrawPkg = packageRootOf('@excalidraw/excalidraw');
|
|
const prodDir = join(excalidrawPkg, 'dist', 'prod');
|
|
if (!existsSync(prodDir)) {
|
|
throw new Error(`Excalidraw prod build not found at ${prodDir}. Check the dist layout.`);
|
|
}
|
|
// Runtime assets loaded relative to EXCALIDRAW_ASSET_PATH (set to the plugin's
|
|
// asset base in plugin.tsx): fonts render the sketch, locales translate the
|
|
// editor UI, data holds font metadata. Copied to the ZIP root so the sandbox
|
|
// CSP (self only) can serve them.
|
|
const ASSET_SUBDIRS = ['fonts', 'locales', 'data'];
|
|
|
|
/** Recursively collect files below `dir` (absolute), keyed by path relative to
|
|
* it, each prefixed with `prefix`. */
|
|
function collect(dir, prefix) {
|
|
const files = {};
|
|
for (const name of readdirSync(dir)) {
|
|
const abs = join(dir, name);
|
|
const rel = `${prefix}${name}`;
|
|
if (statSync(abs).isDirectory()) Object.assign(files, collect(abs, `${rel}/`));
|
|
else files[rel] = readFileSync(abs);
|
|
}
|
|
return files;
|
|
}
|
|
const assetFiles = {};
|
|
for (const sub of ASSET_SUBDIRS) {
|
|
const dir = join(prodDir, sub);
|
|
if (existsSync(dir)) Object.assign(assetFiles, collect(dir, `${sub}/`));
|
|
}
|
|
|
|
// --- 2. Bundle the plugin controller (React + Excalidraw) ------------------
|
|
mkdirSync(join(root, 'dist'), { recursive: true });
|
|
const buildResult = await build({
|
|
entryPoints: [join(root, 'src/plugin.tsx')],
|
|
bundle: true,
|
|
format: 'esm',
|
|
platform: 'browser',
|
|
// Excalidraw's `exports` gate index.js / index.css behind `production` /
|
|
// `development` conditions (no `default`); select the production build.
|
|
conditions: ['production'],
|
|
outfile: join(root, 'dist/plugin.js'),
|
|
minify: true,
|
|
jsx: 'automatic',
|
|
loader: { '.css': 'text', '.woff2': 'dataurl', '.ttf': 'dataurl', '.svg': 'text' },
|
|
define: {
|
|
'process.env.NODE_ENV': '"production"',
|
|
'process.env.IS_PREACT': '"false"',
|
|
},
|
|
metafile: true,
|
|
});
|
|
|
|
// --- 3. Pack the ZIP --------------------------------------------------------
|
|
const files = {
|
|
'manifest.json': readFileSync(join(root, 'manifest.json')),
|
|
'plugin.js': readFileSync(join(root, 'dist/plugin.js')),
|
|
};
|
|
for (const name of readdirSync(join(root, 'i18n'))) {
|
|
files[`i18n/${name}`] = readFileSync(join(root, 'i18n', name));
|
|
}
|
|
// Asset paths already carry their subdir prefix (fonts/…, locales/…, data/…)
|
|
// and sit at the ZIP root so they resolve under EXCALIDRAW_ASSET_PATH.
|
|
Object.assign(files, assetFiles);
|
|
|
|
// License texts for the redistributed material (issue #345): bundled npm
|
|
// packages come from the metafile; the shipped fonts have no license files
|
|
// upstream at all, so the texts are curated in licenses/ (see FONT-NOTICES.md)
|
|
// — @excalidraw/excalidraw ships no LICENSE file either, hence the committed
|
|
// MIT text instead of the metafile fallback line.
|
|
for (const name of readdirSync(join(root, 'licenses'))) {
|
|
files[`licenses/${name}`] = readFileSync(join(root, 'licenses', name));
|
|
}
|
|
files['licenses/THIRD-PARTY-NOTICES.txt'] = Buffer.from(
|
|
thirdPartyNotices(buildResult.metafile, [
|
|
{
|
|
title: 'Excalidraw (bundled into plugin.js)',
|
|
license: 'MIT',
|
|
note: 'Full license text in licenses/excalidraw-MIT.txt.',
|
|
},
|
|
{
|
|
title: 'Fonts (shipped under fonts/)',
|
|
license: 'OFL-1.1 and MIT, per family',
|
|
note: 'Attribution table in licenses/FONT-NOTICES.md; per-font license texts alongside it.',
|
|
},
|
|
]),
|
|
);
|
|
|
|
const target = join(root, 'dist', `${manifest.id}-${manifest.version}.zip`);
|
|
rmSync(target, { force: true });
|
|
writeFileSync(target, zipSync(files, { level: 6 }));
|
|
const unpacked = Object.values(files).reduce((sum, bytes) => sum + bytes.length, 0);
|
|
console.log(
|
|
`wrote ${relative(process.cwd(), target)} ` +
|
|
`(zip ${(statSync(target).size / 1024 / 1024).toFixed(1)} MiB, ` +
|
|
`unpacked ${(unpacked / 1024 / 1024).toFixed(1)} MiB, ` +
|
|
`${Object.keys(files).length} files)`,
|
|
);
|