Merge pull request #2283 from alixander/d2js-split

d2js: remove entrypoints and code split esm module
This commit is contained in:
Alexander Wang 2025-01-14 09:45:50 -07:00 committed by GitHub
commit a416253a05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 117 additions and 148 deletions

View file

@ -21,4 +21,4 @@ node_modules:
.PHONY: cleanup
cleanup: test
prefix "$@" git checkout -- src/platform.js
prefix "$@" git checkout -- src/platform.js src/worker.js

View file

@ -31,36 +31,18 @@ bun add @terrastruct/d2
## Usage
### Browser
D2.js uses webworkers to call a WASM file.
```javascript
// Same for Node or browser
import { D2 } from '@terrastruct/d2';
// Or using a CDN
// import { D2 } from 'https://esm.sh/@terrastruct/d2';
const d2 = new D2();
const result = await d2.compile('x -> y');
const svg = await d2.render(result.diagram);
const result = await d2.compile('x -> y', {
layout: 'dagre',
sketch: true
});
```
### Node
```javascript
import { D2 } from '@terrastruct/d2';
const d2 = new D2();
async function createDiagram() {
const result = await d2.compile('x -> y');
const svg = await d2.render(result.diagram);
console.log(svg);
}
createDiagram();
```
## API Reference
@ -87,7 +69,7 @@ D2.js uses Bun, so install this first.
```bash
git clone https://github.com/terrastruct/d2.git
cd d2/d2js/js
./make.sh
./make.sh all
```
If you change the main D2 source code, you should regenerate the WASM file:
@ -97,6 +79,8 @@ If you change the main D2 source code, you should regenerate the WASM file:
### Running the Development Server
Make sure you've built already, then run:
```bash
bun run dev
```

View file

@ -23,12 +23,11 @@ await writeFile(
);
const commonConfig = {
splitting: false,
sourcemap: "external",
minify: true,
};
async function buildPlatformFile(platform) {
async function buildDynamicFiles(platform) {
const platformContent =
platform === "node"
? `export * from "./platform.node.js";`
@ -36,51 +35,50 @@ async function buildPlatformFile(platform) {
const platformPath = join(SRC_DIR, "platform.js");
await writeFile(platformPath, platformContent);
const workerSource =
platform === "node"
? join(SRC_DIR, "worker.node.js")
: join(SRC_DIR, "worker.browser.js");
const workerTarget = join(SRC_DIR, "worker.js");
const workerContent = await readFile(workerSource, "utf8");
await writeFile(workerTarget, workerContent);
}
async function buildAndCopy(buildType) {
const configs = {
browser: {
outdir: resolve(ROOT_DIR, "dist/browser"),
splitting: false,
format: "esm",
target: "browser",
platform: "browser",
loader: {
".js": "jsx",
},
entrypoints: [
resolve(SRC_DIR, "index.js"),
resolve(SRC_DIR, "worker.js"),
resolve(SRC_DIR, "platform.js"),
resolve(SRC_DIR, "wasm-loader.browser.js"),
],
entrypoints: [resolve(SRC_DIR, "index.js"), resolve(SRC_DIR, "worker.js")],
},
"node-esm": {
outdir: resolve(ROOT_DIR, "dist/node-esm"),
splitting: true,
format: "esm",
target: "node",
platform: "node",
entrypoints: [
resolve(SRC_DIR, "index.js"),
resolve(SRC_DIR, "worker.js"),
resolve(SRC_DIR, "platform.js"),
],
entrypoints: [resolve(SRC_DIR, "index.js"), resolve(SRC_DIR, "worker.js")],
},
"node-cjs": {
outdir: resolve(ROOT_DIR, "dist/node-cjs"),
splitting: false,
format: "cjs",
target: "node",
platform: "node",
entrypoints: [
resolve(SRC_DIR, "index.js"),
resolve(SRC_DIR, "worker.js"),
resolve(SRC_DIR, "platform.js"),
],
entrypoints: [resolve(SRC_DIR, "index.js"), resolve(SRC_DIR, "worker.js")],
},
};
const config = configs[buildType];
await buildPlatformFile(config.platform);
await buildDynamicFiles(config.platform);
const result = await build({
...commonConfig,

View file

@ -30,7 +30,7 @@
</div>
<div id="output"></div>
<script type="module">
import { D2 } from "../src/index.js";
import { D2 } from "../dist/browser/index.js";
const d2 = new D2();
window.compile = async () => {
const input = document.getElementById("input").value;

View file

@ -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.11",
"version": "0.1.19",
"repository": {
"type": "git",
"url": "git+https://github.com/terrastruct/d2.git",
@ -21,10 +21,14 @@
"exports": {
".": {
"browser": "./dist/browser/index.js",
"import": "./dist/node-esm/index.js",
"import": {
"browser": "./dist/browser/index.js",
"default": "./dist/node-esm/index.js"
},
"require": "./dist/node-cjs/index.js",
"default": "./dist/node-esm/index.js"
}
},
"./worker": "./dist/browser/worker.js"
},
"files": [
"dist"

View file

@ -11,18 +11,19 @@ export async function loadFile(path) {
}
export async function createWorker() {
// Combine wasmExecJs with worker script
const workerResponse = await fetch(new URL("./worker.js", import.meta.url));
if (!workerResponse.ok) {
let response = await fetch(new URL("./worker.js", import.meta.url));
if (!response.ok)
throw new Error(
`Failed to load worker.js: ${workerResponse.status} ${workerResponse.statusText}`
`Failed to load worker.js: ${response.status} ${response.statusText}`
);
}
const workerJs = await workerResponse.text();
let workerScript = await response.text();
const blob = new Blob(["(() => {", wasmExecJs, "})();", workerJs], {
type: "application/javascript",
let blob = new Blob([wasmExecJs, workerScript], {
type: "text/javascript;charset=utf-8",
});
return new Worker(URL.createObjectURL(blob));
const worker = new Worker(URL.createObjectURL(blob), {
type: "module",
});
return worker;
}

View file

@ -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(false, self, initWasmBrowser);

View file

@ -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);
});
// Replaced at build time

View file

@ -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(true, parentPort, initWasmNode);

View file

@ -0,0 +1,52 @@
let currentPort;
let d2;
export function setupMessageHandler(isNode, port, initWasm) {
currentPort = port;
const handleMessage = async (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;
}
};
if (isNode) {
port.on("message", handleMessage);
} else {
port.onmessage = (e) => handleMessage(e.data);
}
}