#!/usr/bin/env node /** * Export an Excalidraw JSON file to PNG using Playwright + the official Excalidraw library. * * Usage: node export_png.mjs [output.png] * * All rendering happens locally. Diagram data never leaves the machine. * The Excalidraw JS library is fetched from esm.sh CDN (code only, not user data). */ import { createRequire } from "module"; import { readFileSync, writeFileSync, copyFileSync } from "fs"; import { createServer } from "http"; import { join, extname, dirname } from "path"; import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const RUNTIME_DIR = join(__dirname, ".export-runtime"); const HTML_PATH = join(__dirname, "export.html"); // Resolve playwright from the runtime directory, not the script's location const require = createRequire(join(RUNTIME_DIR, "node_modules", "playwright", "index.mjs")); const { chromium } = await import(join(RUNTIME_DIR, "node_modules", "playwright", "index.mjs")); const inputPath = process.argv[2]; if (!inputPath) { console.error("Usage: node export_png.mjs [output.png]"); process.exit(1); } const outputPath = process.argv[3] || inputPath.replace(/\.excalidraw$/, ".png"); // Set up a temp serving directory const SERVE_DIR = join(__dirname, ".export-tmp"); const { mkdirSync, rmSync } = await import("fs"); mkdirSync(SERVE_DIR, { recursive: true }); copyFileSync(HTML_PATH, join(SERVE_DIR, "export.html")); copyFileSync(inputPath, join(SERVE_DIR, "diagram.excalidraw")); const MIME = { ".html": "text/html", ".json": "application/json", ".excalidraw": "application/json", }; const server = createServer((req, res) => { const file = join(SERVE_DIR, req.url === "/" ? "export.html" : req.url); try { const data = readFileSync(file); res.writeHead(200, { "Content-Type": MIME[extname(file)] || "application/octet-stream" }); res.end(data); } catch { res.writeHead(404); res.end("Not found"); } }); server.listen(0, "127.0.0.1", async () => { const port = server.address().port; let browser; try { browser = await chromium.launch({ headless: true }); const page = await browser.newPage(); page.on("pageerror", err => console.error("Page error:", err.message)); await page.goto(`http://127.0.0.1:${port}`); await page.waitForFunction( () => document.title.startsWith("READY") || document.title.startsWith("ERROR"), { timeout: 30000 } ); const title = await page.title(); if (title.startsWith("ERROR")) { console.error("Export failed:", title); process.exit(1); } const dataUrl = await page.evaluate(() => window.__PNG_DATA__); const base64 = dataUrl.replace(/^data:image\/png;base64,/, ""); writeFileSync(outputPath, Buffer.from(base64, "base64")); console.log(outputPath); } finally { if (browser) await browser.close(); server.close(); rmSync(SERVE_DIR, { recursive: true, force: true }); } });