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 .PHONY: cleanup
cleanup: test 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 ## Usage
### Browser D2.js uses webworkers to call a WASM file.
```javascript ```javascript
// Same for Node or browser
import { D2 } from '@terrastruct/d2'; import { D2 } from '@terrastruct/d2';
// Or using a CDN
// import { D2 } from 'https://esm.sh/@terrastruct/d2';
const d2 = new D2(); const d2 = new D2();
const result = await d2.compile('x -> y'); const result = await d2.compile('x -> y');
const svg = await d2.render(result.diagram); 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 ## API Reference
@ -87,7 +69,7 @@ D2.js uses Bun, so install this first.
```bash ```bash
git clone https://github.com/terrastruct/d2.git git clone https://github.com/terrastruct/d2.git
cd d2/d2js/js cd d2/d2js/js
./make.sh ./make.sh all
``` ```
If you change the main D2 source code, you should regenerate the WASM file: 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 ### Running the Development Server
Make sure you've built already, then run:
```bash ```bash
bun run dev bun run dev
``` ```

View file

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

View file

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

View file

@ -2,7 +2,7 @@
"name": "@terrastruct/d2", "name": "@terrastruct/d2",
"author": "Terrastruct, Inc.", "author": "Terrastruct, Inc.",
"description": "D2.js is a wrapper around the WASM build of D2, the modern text-to-diagram language.", "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": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/terrastruct/d2.git", "url": "git+https://github.com/terrastruct/d2.git",
@ -21,10 +21,14 @@
"exports": { "exports": {
".": { ".": {
"browser": "./dist/browser/index.js", "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", "require": "./dist/node-cjs/index.js",
"default": "./dist/node-esm/index.js" "default": "./dist/node-esm/index.js"
} },
"./worker": "./dist/browser/worker.js"
}, },
"files": [ "files": [
"dist" "dist"

View file

@ -11,18 +11,19 @@ export async function loadFile(path) {
} }
export async function createWorker() { export async function createWorker() {
// Combine wasmExecJs with worker script let response = await fetch(new URL("./worker.js", import.meta.url));
const workerResponse = await fetch(new URL("./worker.js", import.meta.url)); if (!response.ok)
if (!workerResponse.ok) {
throw new Error( throw new Error(
`Failed to load worker.js: ${workerResponse.status} ${workerResponse.statusText}` `Failed to load worker.js: ${response.status} ${response.statusText}`
); );
} let workerScript = await response.text();
const workerJs = await workerResponse.text();
const blob = new Blob(["(() => {", wasmExecJs, "})();", workerJs], { let blob = new Blob([wasmExecJs, workerScript], {
type: "application/javascript", 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"; // Replaced at build time
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);
});

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);
}
}