diff --git a/d2js/js/build.js b/d2js/js/build.js index e0d272986..9f0c11227 100644 --- a/d2js/js/build.js +++ b/d2js/js/build.js @@ -27,7 +27,7 @@ const commonConfig = { minify: true, }; -async function buildPlatformFile(platform) { +async function buildDynamicFiles(platform) { const platformContent = platform === "node" ? `export * from "./platform.node.js";` @@ -35,6 +35,14 @@ async function buildPlatformFile(platform) { const platformPath = join(SRC_DIR, "platform.js"); await writeFile(platformPath, platformContent); + + const workerContent = + platform === "node" + ? `export * from "./worker.node.js";` + : `export * from "./worker.browser.js";`; + + const workerPath = join(SRC_DIR, "worker.js"); + await writeFile(workerPath, workerContent); } async function buildAndCopy(buildType) { @@ -69,7 +77,7 @@ async function buildAndCopy(buildType) { }; const config = configs[buildType]; - await buildPlatformFile(config.platform); + await buildDynamicFiles(config.platform); const result = await build({ ...commonConfig, diff --git a/d2js/js/package.json b/d2js/js/package.json index 8abca7165..1212c5ff6 100644 --- a/d2js/js/package.json +++ b/d2js/js/package.json @@ -2,7 +2,7 @@ "name": "@terrastruct/d2", "author": "Terrastruct, Inc.", "description": "D2.js is a wrapper around the WASM build of D2, the modern text-to-diagram language.", - "version": "0.1.12", + "version": "0.1.17", "repository": { "type": "git", "url": "git+https://github.com/terrastruct/d2.git", diff --git a/d2js/js/src/index.js b/d2js/js/src/index.js index 880e48295..a0be58a00 100644 --- a/d2js/js/src/index.js +++ b/d2js/js/src/index.js @@ -54,7 +54,10 @@ export class D2 { }); } else { this.worker.onerror = (error) => { - console.error("Worker encountered an error:", error.message || error); + console.error("Worker detailed error:", error); + console.error("Error message:", error.message); + console.error("Error filename:", error.filename); + console.error("Error lineno:", error.lineno); }; } diff --git a/d2js/js/src/platform.browser.js b/d2js/js/src/platform.browser.js index 9c8f9d469..1909b2d0a 100644 --- a/d2js/js/src/platform.browser.js +++ b/d2js/js/src/platform.browser.js @@ -18,10 +18,34 @@ export async function createWorker() { ); let workerScript = await response.text(); - let blob = new Blob(["(() => {", wasmExecJs, "})();", workerScript], { - type: "text/javascript;charset=utf-8", - }); - return new Worker(URL.createObjectURL(blob), { + // Create global Go without IIFE in module context + let blob = new Blob( + [ + // First establish Go in global scope + wasmExecJs, + // Then the module code + workerScript, + ], + { + type: "text/javascript;charset=utf-8", + } + ); + + console.log("about to create worker"); + const worker = new Worker(URL.createObjectURL(blob), { type: "module", }); + console.log("worker", worker); + + // Add error handler to see initialization errors + worker.onerror = (error) => { + console.error("Worker initialization error:", { + message: error.message, + filename: error.filename, + lineno: error.lineno, + error: error.error, + }); + }; + + return worker; } diff --git a/d2js/js/src/worker.browser.js b/d2js/js/src/worker.browser.js new file mode 100644 index 000000000..2eaed3ff0 --- /dev/null +++ b/d2js/js/src/worker.browser.js @@ -0,0 +1,10 @@ +import { setupMessageHandler } from "./worker.shared.js"; + +async function initWasmBrowser(wasmBinary) { + const go = new Go(); + const result = await WebAssembly.instantiate(wasmBinary, go.importObject); + go.run(result.instance); + return self.d2; +} + +setupMessageHandler(self, initWasmBrowser); diff --git a/d2js/js/src/worker.js b/d2js/js/src/worker.js index 0878809d1..769950402 100644 --- a/d2js/js/src/worker.js +++ b/d2js/js/src/worker.js @@ -1,92 +1 @@ -const isNode = typeof process !== "undefined" && process.release?.name === "node"; -let currentPort; -let wasm; -let d2; - -async function initWasm(wasmBinary) { - const go = new Go(); - const result = await WebAssembly.instantiate(wasmBinary, go.importObject); - go.run(result.instance); - return isNode ? global.d2 : self.d2; -} - -function setupMessageHandler(port) { - currentPort = port; - if (isNode) { - port.on("message", handleMessage); - } else { - port.onmessage = (e) => handleMessage(e.data); - } -} - -async function handleMessage(e) { - const { type, data } = e; - - switch (type) { - case "init": - try { - if (isNode) { - eval(data.wasmExecContent); - } - d2 = await initWasm(data.wasm); - currentPort.postMessage({ type: "ready" }); - } catch (err) { - currentPort.postMessage({ - type: "error", - error: err.message, - }); - } - break; - - case "compile": - try { - const result = await d2.compile(JSON.stringify(data)); - const response = JSON.parse(result); - if (response.error) { - throw new Error(response.error.message); - } - currentPort.postMessage({ - type: "result", - data: response.data, - }); - } catch (err) { - currentPort.postMessage({ - type: "error", - error: err.message, - }); - } - break; - - case "render": - try { - const result = await d2.render(JSON.stringify(data)); - const response = JSON.parse(result); - if (response.error) { - throw new Error(response.error.message); - } - currentPort.postMessage({ - type: "result", - data: atob(response.data), - }); - } catch (err) { - currentPort.postMessage({ - type: "error", - error: err.message, - }); - } - break; - } -} - -async function init() { - if (isNode) { - const { parentPort } = await import("node:worker_threads"); - setupMessageHandler(parentPort); - } else { - setupMessageHandler(self); - } -} - -init().catch((err) => { - console.error("Initialization error:", err); -}); +export * from "./worker.node.js"; \ No newline at end of file diff --git a/d2js/js/src/worker.node.js b/d2js/js/src/worker.node.js new file mode 100644 index 000000000..b9d9bc6f6 --- /dev/null +++ b/d2js/js/src/worker.node.js @@ -0,0 +1,11 @@ +import { parentPort } from "node:worker_threads"; +import { setupMessageHandler } from "./worker.shared.js"; + +async function initWasmNode(wasmBinary) { + const go = new Go(); + const result = await WebAssembly.instantiate(wasmBinary, go.importObject); + go.run(result.instance); + return global.d2; +} + +setupMessageHandler(parentPort, initWasmNode); diff --git a/d2js/js/src/worker.shared.js b/d2js/js/src/worker.shared.js new file mode 100644 index 000000000..e98bf9e1e --- /dev/null +++ b/d2js/js/src/worker.shared.js @@ -0,0 +1,49 @@ +let currentPort; +let d2; + +export function setupMessageHandler(port, initWasm) { + currentPort = port; + + const handleMessage = async (e) => { + const { type, data } = e; + + switch (type) { + case "init": + try { + d2 = await initWasm(data.wasm); + currentPort.postMessage({ type: "ready" }); + } catch (err) { + currentPort.postMessage({ type: "error", error: err.message }); + } + break; + + case "compile": + try { + const result = await d2.compile(JSON.stringify(data)); + const response = JSON.parse(result); + if (response.error) throw new Error(response.error.message); + currentPort.postMessage({ type: "result", data: response.data }); + } catch (err) { + currentPort.postMessage({ type: "error", error: err.message }); + } + break; + + case "render": + try { + const result = await d2.render(JSON.stringify(data)); + const response = JSON.parse(result); + if (response.error) throw new Error(response.error.message); + currentPort.postMessage({ type: "result", data: atob(response.data) }); + } catch (err) { + currentPort.postMessage({ type: "error", error: err.message }); + } + break; + } + }; + + if (typeof process !== "undefined" && process.release?.name === "node") { + port.on("message", handleMessage); + } else { + port.onmessage = (e) => handleMessage(e.data); + } +} diff --git a/d2js/js/test-bundle.js b/d2js/js/test-bundle.js new file mode 100644 index 000000000..e6fc6d08d --- /dev/null +++ b/d2js/js/test-bundle.js @@ -0,0 +1,133 @@ +// test-bundle.js +import { build } from "bun"; +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +// Ensure output directory exists +await mkdir("./test-dist", { recursive: true }); + +// First, write a temporary platform.js that uses browser code +const platformContent = `export * from "./platform.browser.js";`; +await writeFile("./src/platform.js", platformContent); + +console.log("Building main bundle..."); +const result = await build({ + entrypoints: ["./src/index.js"], + outdir: "./test-dist", + format: "esm", + target: "browser", + platform: "browser", + minify: true, +}); + +if (!result.success) { + console.error("Main bundle build failed:", result.logs); + process.exit(1); +} + +console.log("Building worker bundle..."); +const workerResult = await build({ + entrypoints: ["./src/worker.js"], + outdir: "./test-dist", + format: "esm", + target: "browser", + platform: "browser", + minify: true, +}); + +if (!workerResult.success) { + console.error("Worker bundle build failed:", workerResult.logs); + process.exit(1); +} + +console.log("Builds complete"); + +// Create a simple server to serve the bundles +const server = Bun.serve({ + port: 3001, + async fetch(req) { + const url = new URL(req.url); + + try { + // Serve main bundle + if (url.pathname === "/d2.mjs") { + const file = await Bun.file("./test-dist/index.js").text(); + return new Response(file, { + headers: { + "Content-Type": "application/javascript", + "Access-Control-Allow-Origin": "*", + }, + }); + } + + // Serve worker bundle + if (url.pathname === "/worker.js") { + const file = await Bun.file("./test-dist/worker.js").text(); + return new Response(file, { + headers: { + "Content-Type": "application/javascript", + "Access-Control-Allow-Origin": "*", + }, + }); + } + + // Serve test page + if (url.pathname === "/") { + return new Response( + ` + + +
+