// Builds the installable draw.io plugin. Unlike the other reference plugins // this one carries a bundled application: the official draw.io editor webapp // (pinned release, fetched once into vendor/ — same pattern as the font // build, deploy/fonts/build-fonts.mjs) ships as static assets and runs // entirely inside the sandbox; the frame CSP pins every request to the // plugin's own asset path, so nothing ever talks to diagrams.net. import { execFileSync } from 'node:child_process'; 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 DRAWIO_VERSION = '30.3.6'; const DRAWIO_TARBALL = `https://github.com/jgraph/drawio/archive/refs/tags/v${DRAWIO_VERSION}.tar.gz`; const root = dirname(fileURLToPath(import.meta.url)); const manifest = JSON.parse(readFileSync(join(root, 'manifest.json'), 'utf8')); const vendor = join(root, 'vendor'); const webapp = join(vendor, `drawio-${DRAWIO_VERSION}`, 'src', 'main', 'webapp'); // --- 1. Fetch + unpack the pinned draw.io release (cached in vendor/) ----- // In CI the vendor fetch is skipped (network + 60 MB — the fonts-build // lesson): the controller bundle still builds, only the installable ZIP // needs a dev machine (or a pre-populated vendor/ cache). if (!existsSync(webapp)) { if (process.env.CI) { console.log('CI: skipping draw.io vendor fetch — bundling plugin.js only, no ZIP'); await bundleController(); process.exit(0); } mkdirSync(vendor, { recursive: true }); const tarball = join(vendor, `drawio-${DRAWIO_VERSION}.tar.gz`); if (!existsSync(tarball)) { console.log(`fetching draw.io v${DRAWIO_VERSION} …`); execFileSync('curl', ['-sfL', '-o', tarball, DRAWIO_TARBALL], { stdio: 'inherit' }); } execFileSync('tar', ['-xzf', tarball, '-C', vendor, `drawio-${DRAWIO_VERSION}/src/main/webapp`], { stdio: 'inherit', }); } // --- 2. Select the runtime subset ----------------------------------------- // Included: everything the production bootstrap chain and its lazy loaders // reach. Excluded: dev sources (js/diagramly, js/grapheditor — app.min.js // contains them), the embed.diagrams.net integrations bundle, standalone // viewers, MathJax (math typesetting off), file templates, server-side // leftovers, and the PWA service worker. const EXCLUDE_TOP = new Set([ 'WEB-INF', 'META-INF', 'templates', 'math4', 'plugins', 'service-worker.js', 'service-worker.js.map', 'workbox-05b6c01b.js', 'workbox-05b6c01b.js.map', 'manifest.json', // PWA manifest, collides with the plugin manifest anyway ]); const EXCLUDE_JS = new Set([ 'integrate.min.js', 'viewer.min.js', 'viewer-static.min.js', 'diagramly', 'grapheditor', 'embed.dev.js', ]); // Editor UI languages shipped: keep the product languages + the fallback. const RESOURCE_KEEP = new Set(['dia.txt', 'dia_de.txt']); /** Recursively collect files below `dir` (absolute), applying the filters. */ function collect(dir, baseRel) { const files = {}; for (const name of readdirSync(dir)) { const abs = join(dir, name); const rel = baseRel === '' ? name : `${baseRel}/${name}`; const top = rel.split('/')[0]; if (baseRel === '' && EXCLUDE_TOP.has(name)) continue; if (top === 'js' && rel.split('/').length === 2 && EXCLUDE_JS.has(name)) continue; if (top === 'resources' && !RESOURCE_KEEP.has(name) && !statSync(abs).isDirectory()) continue; if (name.endsWith('.map')) continue; if (statSync(abs).isDirectory()) { Object.assign(files, collect(abs, rel)); } else { files[rel] = readFileSync(abs); } } return files; } const drawioFiles = collect(webapp, ''); // --- 3. Bundle the plugin controller --------------------------------------- async function bundleController() { mkdirSync(join(root, 'dist'), { recursive: true }); await build({ entryPoints: [join(root, 'src/plugin.ts')], bundle: true, format: 'esm', outfile: join(root, 'dist/plugin.js'), minify: true, }); } await bundleController(); // --- 4. 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)); } for (const [rel, bytes] of Object.entries(drawioFiles)) { files[`assets/drawio/${rel}`] = bytes; } 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)`, );