Neues Referenz-Block-Plugin „Excalidraw" (handgezeichnete Whiteboard-
Skizzen), analog zum draw.io-Plugin. Anders als draw.io (vendored Webapp)
ist Excalidraw eine React-npm-Lib: esbuild bündelt Controller + React +
Excalidraw in plugin.js, die Font-/Locale-/Data-Assets werden aus
node_modules in den ZIP-Root kopiert und zur Laufzeit über
EXCALIDRAW_ASSET_PATH (Plugin-Asset-Basis) geladen — nichts spricht mit
excalidraw.com, die Sandbox-CSP pinnt jede Anfrage auf self.
- manifest.json: kind=code, Block-Extension-Point diagram,
permissions blockData+ui, fallback "[Excalidraw]".
- src/plugin.tsx: Render-Modus zeigt gespeichertes SVG; Edit-Modus zeigt
Snapshot + Bearbeiten-Knopf (leerer Block öffnet direkt); Vollbild via
host.ui.enterFullscreen mountet <Excalidraw> (React), „Speichern &
Beenden" exportiert per exportToSvg, persistiert {scene, svg} über
host.blockData.setData → Fallback-Renderer bedient Lese-/Public-Ansicht
+ Exporte ohne Backend-Änderung.
- build.mjs: esbuild (jsx automatic, css→text, production-conditions) +
fflate-ZIP. Build erzeugt excalidraw-1.0.0.zip: 15,5 MiB zip /
22,3 MiB unpacked (Limits 64/256 MiB — passt).
- i18n de+en, globals.d.ts (CSS-Modul-Deklaration).
pnpm-Override @floating-ui/react-dom@2.1.2: Excalidraw 0.18.1 zieht sonst
@floating-ui/dom@^1.8.0, das (noch) nicht im Registry ist und `pnpm
install` repo-weit bricht (dokumentiert in pnpm-workspace.yaml).
VERIFIZIERT: typecheck/lint, Manifest-Validierung (SDK), Build+ZIP-Größe.
NICHT lokal verifiziert (braucht Preview/Test-Stage): Laufzeit —
Excalidraw-Rendering + Speichern unter Sandbox-CSP, Font-Laden vom
Asset-Pfad. Prod-Installation macht Stefan als Site-Admin.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
116 lines
4.4 KiB
JavaScript
116 lines
4.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';
|
|
|
|
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 });
|
|
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"',
|
|
},
|
|
});
|
|
|
|
// --- 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);
|
|
|
|
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)`,
|
|
);
|