init d2.js
This commit is contained in:
parent
13af63a032
commit
8d71e8fff8
21 changed files with 1176 additions and 32 deletions
5
Makefile
5
Makefile
|
|
@ -1,7 +1,7 @@
|
|||
.POSIX:
|
||||
|
||||
.PHONY: all
|
||||
all: fmt gen lint build test
|
||||
all: fmt gen js lint build test
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
|
|
@ -21,3 +21,6 @@ test: fmt
|
|||
.PHONY: race
|
||||
race: fmt
|
||||
prefix "$@" ./ci/test.sh --race ./...
|
||||
.PHONY: js
|
||||
js:
|
||||
cd d2js/js && prefix "$@" ./make.sh
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
# D2 as a Javascript library
|
||||
|
||||
D2 is runnable as a Javascript library, on both the client and server side. This means you
|
||||
can run D2 entirely on the browser.
|
||||
|
||||
This is achieved by a JS wrapper around a WASM file.
|
||||
|
||||
## Install
|
||||
|
||||
### NPM
|
||||
|
||||
```sh
|
||||
npm install @terrastruct/d2
|
||||
```
|
||||
|
||||
### Yarn
|
||||
|
||||
```sh
|
||||
yarn add @terrastruct/d2
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
GOOS=js GOARCH=wasm go build -ldflags='-s -w' -trimpath -o main.wasm ./d2js
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
todo
|
||||
|
|
@ -151,7 +151,7 @@ func Compile(args []js.Value) (interface{}, error) {
|
|||
|
||||
renderOpts := &d2svg.RenderOpts{}
|
||||
var fontFamily *d2fonts.FontFamily
|
||||
if input.Opts != nil && input.Opts.Sketch != nil {
|
||||
if input.Opts != nil && input.Opts.Sketch != nil && *input.Opts.Sketch {
|
||||
fontFamily = go2.Pointer(d2fonts.HandDrawn)
|
||||
renderOpts.Sketch = input.Opts.Sketch
|
||||
}
|
||||
|
|
|
|||
27
d2js/js/.gitignore
vendored
Normal file
27
d2js/js/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
node_modules
|
||||
.npm
|
||||
bun.lockb
|
||||
|
||||
wasm/d2.wasm
|
||||
dist/
|
||||
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
coverage/
|
||||
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
8
d2js/js/CHANGELOG.md
Normal file
8
d2js/js/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to only the d2.js package will be documented in this file. **Does not
|
||||
include changes to the main d2 project.**
|
||||
|
||||
## [0.1.0] - 2025-01-12
|
||||
|
||||
First public release
|
||||
20
d2js/js/Makefile
Normal file
20
d2js/js/Makefile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
.POSIX:
|
||||
.PHONY: all
|
||||
all: fmt build test
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: node_modules
|
||||
prefix "$@" ../../ci/sub/bin/fmt.sh
|
||||
prefix "$@" rm -f yarn.lock
|
||||
|
||||
.PHONY: build
|
||||
build: node_modules
|
||||
prefix "$@" ./ci/build.sh
|
||||
|
||||
.PHONY: test
|
||||
test: build
|
||||
prefix "$@" bun test:all
|
||||
|
||||
.PHONY: node_modules
|
||||
node_modules:
|
||||
prefix "$@" bun install $${CI:+--frozen-lockfile}
|
||||
112
d2js/js/README.md
Normal file
112
d2js/js/README.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# D2.js
|
||||
|
||||
[](https://www.npmjs.com/package/@terrastruct/d2)
|
||||
[](https://mozilla.org/MPL/2.0/)
|
||||
|
||||
D2.js is a JavaScript wrapper around D2, the modern diagram scripting language. It enables running D2 directly in browsers and Node environments through WebAssembly.
|
||||
|
||||
## Features
|
||||
|
||||
- 🌐 **Universal** - Works in both browser and Node environments
|
||||
- 🚀 **Modern** - Built with ESM modules, with CJS fallback
|
||||
- 🔄 **Isomorphic** - Same API everywhere
|
||||
- ⚡ **Fast** - Powered by WebAssembly for near-native performance
|
||||
- 📦 **Lightweight** - Minimal wrapper around the core D2 engine
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install @terrastruct/d2
|
||||
|
||||
# yarn
|
||||
yarn add @terrastruct/d2
|
||||
|
||||
# pnpm
|
||||
pnpm add @terrastruct/d2
|
||||
|
||||
# bun
|
||||
bun add @terrastruct/d2
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Browser
|
||||
|
||||
```javascript
|
||||
import { D2 } from '@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
|
||||
|
||||
### `new D2()`
|
||||
Creates a new D2 instance.
|
||||
|
||||
### `compile(input: string, options?: CompileOptions): Promise<CompileResult>`
|
||||
Compiles D2 markup into an intermediate representation.
|
||||
|
||||
Options:
|
||||
- `layout`: Layout engine to use ('dagre' | 'elk') [default: 'dagre']
|
||||
- `sketch`: Enable sketch mode [default: false]
|
||||
|
||||
### `render(diagram: Diagram, options?: RenderOptions): Promise<string>`
|
||||
Renders a compiled diagram to SVG.
|
||||
|
||||
## Development
|
||||
|
||||
D2.js uses Bun, so install this first.
|
||||
|
||||
### Building from source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/terrastruct/d2.git
|
||||
cd d2/d2js/js
|
||||
./make.sh
|
||||
```
|
||||
|
||||
If you change the main D2 source code, you should regenerate the WASM file:
|
||||
```bash
|
||||
./make.sh build
|
||||
```
|
||||
|
||||
### Running the Development Server
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Visit `http://localhost:3000` to see the example page.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome!
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Mozilla Public License Version 2.0.
|
||||
35
d2js/js/build.js
Normal file
35
d2js/js/build.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { build } from "bun";
|
||||
import { copyFile, mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
await mkdir("./dist/esm", { recursive: true });
|
||||
await mkdir("./dist/cjs", { recursive: true });
|
||||
|
||||
const commonConfig = {
|
||||
target: "node",
|
||||
splitting: false,
|
||||
sourcemap: "external",
|
||||
minify: true,
|
||||
naming: {
|
||||
entry: "[dir]/[name].js",
|
||||
chunk: "[name]-[hash].js",
|
||||
asset: "[name]-[hash][ext]",
|
||||
},
|
||||
};
|
||||
|
||||
async function buildAndCopy(format) {
|
||||
const outdir = `./dist/${format}`;
|
||||
|
||||
await build({
|
||||
...commonConfig,
|
||||
entrypoints: ["./src/index.js", "./src/worker.js", "./src/platform.js"],
|
||||
outdir,
|
||||
format,
|
||||
});
|
||||
|
||||
await copyFile("./wasm/d2.wasm", join(outdir, "d2.wasm"));
|
||||
await copyFile("./wasm/wasm_exec.js", join(outdir, "wasm_exec.js"));
|
||||
}
|
||||
|
||||
await buildAndCopy("esm");
|
||||
await buildAndCopy("cjs");
|
||||
BIN
d2js/js/bun.lockb
Executable file
BIN
d2js/js/bun.lockb
Executable file
Binary file not shown.
18
d2js/js/ci/build.sh
Executable file
18
d2js/js/ci/build.sh
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
. "$(dirname "$0")/../../../ci/sub/lib.sh"
|
||||
cd -- "$(dirname "$0")/.."
|
||||
|
||||
cd ../..
|
||||
sh_c "GOOS=js GOARCH=wasm go build -ldflags='-s -w' -trimpath -o main.wasm ./d2js"
|
||||
sh_c "mv main.wasm ./d2js/js/wasm/d2.wasm"
|
||||
|
||||
if [ ! -f ./d2js/js/wasm/d2.wasm ]; then
|
||||
echoerr "Error: d2.wasm is missing"
|
||||
exit 1
|
||||
else
|
||||
stat --printf="Size: %s bytes\n" ./d2js/js/wasm/d2.wasm || ls -lh ./d2js/js/wasm/d2.wasm
|
||||
fi
|
||||
|
||||
cd d2js/js
|
||||
sh_c bun run build
|
||||
57
d2js/js/dev-server.js
Normal file
57
d2js/js/dev-server.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
const MIME_TYPES = {
|
||||
".html": "text/html",
|
||||
".js": "text/javascript",
|
||||
".mjs": "text/javascript",
|
||||
".css": "text/css",
|
||||
".wasm": "application/wasm",
|
||||
".svg": "image/svg+xml",
|
||||
};
|
||||
|
||||
const server = Bun.serve({
|
||||
port: 3000,
|
||||
async fetch(request) {
|
||||
const url = new URL(request.url);
|
||||
let path = url.pathname;
|
||||
|
||||
// Serve index page by default
|
||||
if (path === "/") {
|
||||
path = "/examples/basic.html";
|
||||
}
|
||||
|
||||
// Handle attempts to access files in src
|
||||
if (path.startsWith("/src/")) {
|
||||
const wasmFile = path.includes("wasm_exec.js") || path.includes("d2.wasm");
|
||||
if (wasmFile) {
|
||||
path = path.replace("/src/", "/wasm/");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const filePath = path.slice(1);
|
||||
const file = Bun.file(filePath);
|
||||
const exists = await file.exists();
|
||||
|
||||
if (!exists) {
|
||||
return new Response(`File not found: ${path}`, { status: 404 });
|
||||
}
|
||||
|
||||
// Get file extension and corresponding MIME type
|
||||
const ext = "." + filePath.split(".").pop();
|
||||
const mimeType = MIME_TYPES[ext] || "application/octet-stream";
|
||||
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
"Content-Type": mimeType,
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Error serving ${path}:`, err);
|
||||
return new Response(`Server error: ${err.message}`, { status: 500 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Server running at http://localhost:3000`);
|
||||
49
d2js/js/examples/basic.html
Normal file
49
d2js/js/examples/basic.html
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
textarea {
|
||||
width: 400px;
|
||||
height: 300px;
|
||||
}
|
||||
#output {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
#output svg {
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<textarea id="input">x -> y</textarea>
|
||||
<button onclick="compile()">Compile</button>
|
||||
</div>
|
||||
<div id="output"></div>
|
||||
<script type="module">
|
||||
import { D2 } from "../src/index.js";
|
||||
const d2 = new D2();
|
||||
window.compile = async () => {
|
||||
const input = document.getElementById("input").value;
|
||||
try {
|
||||
const result = await d2.compile(input);
|
||||
const svg = await d2.render(result.diagram);
|
||||
document.getElementById("output").innerHTML = svg;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById("output").textContent = err.message;
|
||||
}
|
||||
};
|
||||
compile();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
23
d2js/js/make.sh
Executable file
23
d2js/js/make.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
if [ ! -e "$(dirname "$0")/../../ci/sub/.git" ]; then
|
||||
set -x
|
||||
git submodule update --init
|
||||
set +x
|
||||
fi
|
||||
. "$(dirname "$0")/../../ci/sub/lib.sh"
|
||||
PATH="$(cd -- "$(dirname "$0")" && pwd)/../../ci/sub/bin:$PATH"
|
||||
cd -- "$(dirname "$0")"
|
||||
|
||||
if ! command -v bun >/dev/null 2>&1; then
|
||||
if [ -n "${CI-}" ]; then
|
||||
echo "Bun is not installed. Installing Bun..."
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
export PATH="$HOME/.bun/bin:$PATH"
|
||||
else
|
||||
echoerr "You need bun to build d2.js: curl -fsSL https://bun.sh/install | bash"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_make "$@"
|
||||
53
d2js/js/package.json
Normal file
53
d2js/js/package.json
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"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.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/terrastruct/d2.git",
|
||||
"directory": "d2js/js"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/terrastruct/d2/issues"
|
||||
},
|
||||
"homepage": "https://github.com/terrastruct/d2/tree/master/d2js/js#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/esm/index.js",
|
||||
"require": "./dist/cjs/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "bun build.js",
|
||||
"test": "bun test test/unit",
|
||||
"test:integration": "bun run build && bun test test/integration",
|
||||
"test:all": "bun run test && bun run test:integration",
|
||||
"dev": "bun --watch dev-server.js",
|
||||
"prepublishOnly": "./make.sh"
|
||||
},
|
||||
"keywords": [
|
||||
"d2",
|
||||
"d2lang",
|
||||
"diagram",
|
||||
"wasm",
|
||||
"text-to-diagram",
|
||||
"go"
|
||||
],
|
||||
"engines": {
|
||||
"bun": ">=1.0.0"
|
||||
},
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"bun": "latest"
|
||||
}
|
||||
}
|
||||
107
d2js/js/src/index.js
Normal file
107
d2js/js/src/index.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { createWorker, loadFile } from "./platform.js";
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
layout: "dagre",
|
||||
sketch: false,
|
||||
};
|
||||
|
||||
export class D2 {
|
||||
constructor() {
|
||||
this.ready = this.init();
|
||||
}
|
||||
|
||||
setupMessageHandler() {
|
||||
const isNode = typeof window === "undefined";
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isNode) {
|
||||
this.worker.on("message", (data) => {
|
||||
if (data.type === "ready") resolve();
|
||||
if (data.type === "error") reject(new Error(data.error));
|
||||
if (data.type === "result" && this.currentResolve) {
|
||||
this.currentResolve(data.data);
|
||||
}
|
||||
if (data.type === "error" && this.currentReject) {
|
||||
this.currentReject(new Error(data.error));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.worker.onmessage = (e) => {
|
||||
if (e.data.type === "ready") resolve();
|
||||
if (e.data.type === "error") reject(new Error(e.data.error));
|
||||
if (e.data.type === "result" && this.currentResolve) {
|
||||
this.currentResolve(e.data.data);
|
||||
}
|
||||
if (e.data.type === "error" && this.currentReject) {
|
||||
this.currentReject(new Error(e.data.error));
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.worker = await createWorker();
|
||||
|
||||
const wasmExecContent = await loadFile("./wasm_exec.js");
|
||||
const wasmBinary = await loadFile("./d2.wasm");
|
||||
|
||||
const isNode = typeof window === "undefined";
|
||||
const messageHandler = this.setupMessageHandler();
|
||||
|
||||
if (isNode) {
|
||||
this.worker.on("error", (error) => {
|
||||
console.error("Worker encountered an error:", error.message || error);
|
||||
});
|
||||
} else {
|
||||
this.worker.onerror = (error) => {
|
||||
console.error("Worker encountered an error:", error.message || error);
|
||||
};
|
||||
}
|
||||
|
||||
this.worker.postMessage({
|
||||
type: "init",
|
||||
data: {
|
||||
wasm: wasmBinary,
|
||||
wasmExecContent: isNode ? wasmExecContent.toString() : null,
|
||||
wasmExecUrl: isNode
|
||||
? null
|
||||
: URL.createObjectURL(
|
||||
new Blob([wasmExecContent], { type: "application/javascript" })
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
return messageHandler;
|
||||
}
|
||||
|
||||
async sendMessage(type, data) {
|
||||
await this.ready;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.currentResolve = resolve;
|
||||
this.currentReject = reject;
|
||||
this.worker.postMessage({ type, data });
|
||||
});
|
||||
}
|
||||
|
||||
async compile(input, options = {}) {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
const request =
|
||||
typeof input === "string"
|
||||
? { fs: { index: input }, options: opts }
|
||||
: { ...input, options: { ...opts, ...input.options } };
|
||||
return this.sendMessage("compile", request);
|
||||
}
|
||||
|
||||
async render(diagram, options = {}) {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
return this.sendMessage("render", { diagram, options: opts });
|
||||
}
|
||||
|
||||
async encode(script) {
|
||||
return this.sendMessage("encode", script);
|
||||
}
|
||||
|
||||
async decode(encoded) {
|
||||
return this.sendMessage("decode", encoded);
|
||||
}
|
||||
}
|
||||
37
d2js/js/src/platform.js
Normal file
37
d2js/js/src/platform.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
export async function loadFile(path) {
|
||||
if (typeof window === "undefined") {
|
||||
const fs = await import("node:fs/promises");
|
||||
const { fileURLToPath } = await import("node:url");
|
||||
const { join, dirname } = await import("node:path");
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
try {
|
||||
return await fs.readFile(join(__dirname, path));
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
return await fs.readFile(join(__dirname, "../wasm", path.replace("./", "")));
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const response = await fetch(new URL(path, import.meta.url));
|
||||
return await response.arrayBuffer();
|
||||
} catch {
|
||||
const response = await fetch(
|
||||
new URL(`../wasm/${path.replace("./", "")}`, import.meta.url)
|
||||
);
|
||||
return await response.arrayBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
export async function createWorker() {
|
||||
if (typeof window === "undefined") {
|
||||
const { Worker } = await import("node:worker_threads");
|
||||
const { fileURLToPath } = await import("node:url");
|
||||
const { join, dirname } = await import("node:path");
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
return new Worker(join(__dirname, "worker.js"));
|
||||
}
|
||||
return new window.Worker(new URL("./worker.js", import.meta.url));
|
||||
}
|
||||
94
d2js/js/src/worker.js
Normal file
94
d2js/js/src/worker.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
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);
|
||||
} else {
|
||||
importScripts(data.wasmExecUrl);
|
||||
}
|
||||
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);
|
||||
});
|
||||
11
d2js/js/test/integration/cjs.test.js
Normal file
11
d2js/js/test/integration/cjs.test.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { expect, test, describe } from "bun:test";
|
||||
|
||||
describe("D2 CJS Integration", () => {
|
||||
test("can require and use CJS build", async () => {
|
||||
const { D2 } = require("../../dist/cjs/index.js");
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y");
|
||||
expect(result.diagram).toBeDefined();
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
});
|
||||
11
d2js/js/test/integration/esm.test.js
Normal file
11
d2js/js/test/integration/esm.test.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { expect, test, describe } from "bun:test";
|
||||
import { D2 } from "../../dist/esm/index.js";
|
||||
|
||||
describe("D2 ESM Integration", () => {
|
||||
test("can import and use ESM build", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y");
|
||||
expect(result.diagram).toBeDefined();
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
});
|
||||
32
d2js/js/test/unit/basic.test.js
Normal file
32
d2js/js/test/unit/basic.test.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { expect, test, describe } from "bun:test";
|
||||
import { D2 } from "../../src/index.js";
|
||||
|
||||
describe("D2 Unit Tests", () => {
|
||||
test("basic compilation works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y");
|
||||
expect(result.diagram).toBeDefined();
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("render works", async () => {
|
||||
const d2 = new D2();
|
||||
const result = await d2.compile("x -> y");
|
||||
const svg = await d2.render(result.diagram);
|
||||
expect(svg).toContain("<svg");
|
||||
expect(svg).toContain("</svg>");
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
|
||||
test("handles syntax errors correctly", async () => {
|
||||
const d2 = new D2();
|
||||
try {
|
||||
await d2.compile("invalid -> -> syntax");
|
||||
throw new Error("Should have thrown syntax error");
|
||||
} catch (err) {
|
||||
expect(err).toBeDefined();
|
||||
expect(err.message).not.toContain("Should have thrown syntax error");
|
||||
}
|
||||
await d2.worker.terminate();
|
||||
}, 20000);
|
||||
});
|
||||
477
d2js/js/wasm/wasm_exec.js
Normal file
477
d2js/js/wasm/wasm_exec.js
Normal file
|
|
@ -0,0 +1,477 @@
|
|||
"use strict";
|
||||
(() => {
|
||||
const o = () => {
|
||||
const h = new Error("not implemented");
|
||||
return (h.code = "ENOSYS"), h;
|
||||
};
|
||||
if (!globalThis.fs) {
|
||||
let h = "";
|
||||
globalThis.fs = {
|
||||
constants: {
|
||||
O_WRONLY: -1,
|
||||
O_RDWR: -1,
|
||||
O_CREAT: -1,
|
||||
O_TRUNC: -1,
|
||||
O_APPEND: -1,
|
||||
O_EXCL: -1,
|
||||
},
|
||||
writeSync(n, s) {
|
||||
h += y.decode(s);
|
||||
const i = h.lastIndexOf(`
|
||||
`);
|
||||
return (
|
||||
i != -1 && (console.log(h.substring(0, i)), (h = h.substring(i + 1))), s.length
|
||||
);
|
||||
},
|
||||
write(n, s, i, r, f, u) {
|
||||
if (i !== 0 || r !== s.length || f !== null) {
|
||||
u(o());
|
||||
return;
|
||||
}
|
||||
const d = this.writeSync(n, s);
|
||||
u(null, d);
|
||||
},
|
||||
chmod(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
chown(n, s, i, r) {
|
||||
r(o());
|
||||
},
|
||||
close(n, s) {
|
||||
s(o());
|
||||
},
|
||||
fchmod(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
fchown(n, s, i, r) {
|
||||
r(o());
|
||||
},
|
||||
fstat(n, s) {
|
||||
s(o());
|
||||
},
|
||||
fsync(n, s) {
|
||||
s(null);
|
||||
},
|
||||
ftruncate(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
lchown(n, s, i, r) {
|
||||
r(o());
|
||||
},
|
||||
link(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
lstat(n, s) {
|
||||
s(o());
|
||||
},
|
||||
mkdir(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
open(n, s, i, r) {
|
||||
r(o());
|
||||
},
|
||||
read(n, s, i, r, f, u) {
|
||||
u(o());
|
||||
},
|
||||
readdir(n, s) {
|
||||
s(o());
|
||||
},
|
||||
readlink(n, s) {
|
||||
s(o());
|
||||
},
|
||||
rename(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
rmdir(n, s) {
|
||||
s(o());
|
||||
},
|
||||
stat(n, s) {
|
||||
s(o());
|
||||
},
|
||||
symlink(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
truncate(n, s, i) {
|
||||
i(o());
|
||||
},
|
||||
unlink(n, s) {
|
||||
s(o());
|
||||
},
|
||||
utimes(n, s, i, r) {
|
||||
r(o());
|
||||
},
|
||||
};
|
||||
}
|
||||
if (
|
||||
(globalThis.process ||
|
||||
(globalThis.process = {
|
||||
getuid() {
|
||||
return -1;
|
||||
},
|
||||
getgid() {
|
||||
return -1;
|
||||
},
|
||||
geteuid() {
|
||||
return -1;
|
||||
},
|
||||
getegid() {
|
||||
return -1;
|
||||
},
|
||||
getgroups() {
|
||||
throw o();
|
||||
},
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
umask() {
|
||||
throw o();
|
||||
},
|
||||
cwd() {
|
||||
throw o();
|
||||
},
|
||||
chdir() {
|
||||
throw o();
|
||||
},
|
||||
}),
|
||||
!globalThis.crypto)
|
||||
)
|
||||
throw new Error(
|
||||
"globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"
|
||||
);
|
||||
if (!globalThis.performance)
|
||||
throw new Error(
|
||||
"globalThis.performance is not available, polyfill required (performance.now only)"
|
||||
);
|
||||
if (!globalThis.TextEncoder)
|
||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||
if (!globalThis.TextDecoder)
|
||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||
const g = new TextEncoder("utf-8"),
|
||||
y = new TextDecoder("utf-8");
|
||||
globalThis.Go = class {
|
||||
constructor() {
|
||||
(this.argv = ["js"]),
|
||||
(this.env = {}),
|
||||
(this.exit = (t) => {
|
||||
t !== 0 && console.warn("exit code:", t);
|
||||
}),
|
||||
(this._exitPromise = new Promise((t) => {
|
||||
this._resolveExitPromise = t;
|
||||
})),
|
||||
(this._pendingEvent = null),
|
||||
(this._scheduledTimeouts = new Map()),
|
||||
(this._nextCallbackTimeoutID = 1);
|
||||
const h = (t, e) => {
|
||||
this.mem.setUint32(t + 0, e, !0),
|
||||
this.mem.setUint32(t + 4, Math.floor(e / 4294967296), !0);
|
||||
},
|
||||
n = (t, e) => {
|
||||
this.mem.setUint32(t + 0, e, !0);
|
||||
},
|
||||
s = (t) => {
|
||||
const e = this.mem.getUint32(t + 0, !0),
|
||||
l = this.mem.getInt32(t + 4, !0);
|
||||
return e + l * 4294967296;
|
||||
},
|
||||
i = (t) => {
|
||||
const e = this.mem.getFloat64(t, !0);
|
||||
if (e === 0) return;
|
||||
if (!isNaN(e)) return e;
|
||||
const l = this.mem.getUint32(t, !0);
|
||||
return this._values[l];
|
||||
},
|
||||
r = (t, e) => {
|
||||
if (typeof e == "number" && e !== 0) {
|
||||
if (isNaN(e)) {
|
||||
this.mem.setUint32(t + 4, 2146959360, !0), this.mem.setUint32(t, 0, !0);
|
||||
return;
|
||||
}
|
||||
this.mem.setFloat64(t, e, !0);
|
||||
return;
|
||||
}
|
||||
if (e === void 0) {
|
||||
this.mem.setFloat64(t, 0, !0);
|
||||
return;
|
||||
}
|
||||
let a = this._ids.get(e);
|
||||
a === void 0 &&
|
||||
((a = this._idPool.pop()),
|
||||
a === void 0 && (a = this._values.length),
|
||||
(this._values[a] = e),
|
||||
(this._goRefCounts[a] = 0),
|
||||
this._ids.set(e, a)),
|
||||
this._goRefCounts[a]++;
|
||||
let c = 0;
|
||||
switch (typeof e) {
|
||||
case "object":
|
||||
e !== null && (c = 1);
|
||||
break;
|
||||
case "string":
|
||||
c = 2;
|
||||
break;
|
||||
case "symbol":
|
||||
c = 3;
|
||||
break;
|
||||
case "function":
|
||||
c = 4;
|
||||
break;
|
||||
}
|
||||
this.mem.setUint32(t + 4, 2146959360 | c, !0), this.mem.setUint32(t, a, !0);
|
||||
},
|
||||
f = (t) => {
|
||||
const e = s(t + 0),
|
||||
l = s(t + 8);
|
||||
return new Uint8Array(this._inst.exports.mem.buffer, e, l);
|
||||
},
|
||||
u = (t) => {
|
||||
const e = s(t + 0),
|
||||
l = s(t + 8),
|
||||
a = new Array(l);
|
||||
for (let c = 0; c < l; c++) a[c] = i(e + c * 8);
|
||||
return a;
|
||||
},
|
||||
d = (t) => {
|
||||
const e = s(t + 0),
|
||||
l = s(t + 8);
|
||||
return y.decode(new DataView(this._inst.exports.mem.buffer, e, l));
|
||||
},
|
||||
m = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
_gotest: { add: (t, e) => t + e },
|
||||
gojs: {
|
||||
"runtime.wasmExit": (t) => {
|
||||
t >>>= 0;
|
||||
const e = this.mem.getInt32(t + 8, !0);
|
||||
(this.exited = !0),
|
||||
delete this._inst,
|
||||
delete this._values,
|
||||
delete this._goRefCounts,
|
||||
delete this._ids,
|
||||
delete this._idPool,
|
||||
this.exit(e);
|
||||
},
|
||||
"runtime.wasmWrite": (t) => {
|
||||
t >>>= 0;
|
||||
const e = s(t + 8),
|
||||
l = s(t + 16),
|
||||
a = this.mem.getInt32(t + 24, !0);
|
||||
fs.writeSync(e, new Uint8Array(this._inst.exports.mem.buffer, l, a));
|
||||
},
|
||||
"runtime.resetMemoryDataView": (t) => {
|
||||
(t >>>= 0), (this.mem = new DataView(this._inst.exports.mem.buffer));
|
||||
},
|
||||
"runtime.nanotime1": (t) => {
|
||||
(t >>>= 0), h(t + 8, (m + performance.now()) * 1e6);
|
||||
},
|
||||
"runtime.walltime": (t) => {
|
||||
t >>>= 0;
|
||||
const e = new Date().getTime();
|
||||
h(t + 8, e / 1e3), this.mem.setInt32(t + 16, (e % 1e3) * 1e6, !0);
|
||||
},
|
||||
"runtime.scheduleTimeoutEvent": (t) => {
|
||||
t >>>= 0;
|
||||
const e = this._nextCallbackTimeoutID;
|
||||
this._nextCallbackTimeoutID++,
|
||||
this._scheduledTimeouts.set(
|
||||
e,
|
||||
setTimeout(() => {
|
||||
for (this._resume(); this._scheduledTimeouts.has(e); )
|
||||
console.warn("scheduleTimeoutEvent: missed timeout event"),
|
||||
this._resume();
|
||||
}, s(t + 8))
|
||||
),
|
||||
this.mem.setInt32(t + 16, e, !0);
|
||||
},
|
||||
"runtime.clearTimeoutEvent": (t) => {
|
||||
t >>>= 0;
|
||||
const e = this.mem.getInt32(t + 8, !0);
|
||||
clearTimeout(this._scheduledTimeouts.get(e)),
|
||||
this._scheduledTimeouts.delete(e);
|
||||
},
|
||||
"runtime.getRandomData": (t) => {
|
||||
(t >>>= 0), crypto.getRandomValues(f(t + 8));
|
||||
},
|
||||
"syscall/js.finalizeRef": (t) => {
|
||||
t >>>= 0;
|
||||
const e = this.mem.getUint32(t + 8, !0);
|
||||
if ((this._goRefCounts[e]--, this._goRefCounts[e] === 0)) {
|
||||
const l = this._values[e];
|
||||
(this._values[e] = null), this._ids.delete(l), this._idPool.push(e);
|
||||
}
|
||||
},
|
||||
"syscall/js.stringVal": (t) => {
|
||||
(t >>>= 0), r(t + 24, d(t + 8));
|
||||
},
|
||||
"syscall/js.valueGet": (t) => {
|
||||
t >>>= 0;
|
||||
const e = Reflect.get(i(t + 8), d(t + 16));
|
||||
(t = this._inst.exports.getsp() >>> 0), r(t + 32, e);
|
||||
},
|
||||
"syscall/js.valueSet": (t) => {
|
||||
(t >>>= 0), Reflect.set(i(t + 8), d(t + 16), i(t + 32));
|
||||
},
|
||||
"syscall/js.valueDelete": (t) => {
|
||||
(t >>>= 0), Reflect.deleteProperty(i(t + 8), d(t + 16));
|
||||
},
|
||||
"syscall/js.valueIndex": (t) => {
|
||||
(t >>>= 0), r(t + 24, Reflect.get(i(t + 8), s(t + 16)));
|
||||
},
|
||||
"syscall/js.valueSetIndex": (t) => {
|
||||
(t >>>= 0), Reflect.set(i(t + 8), s(t + 16), i(t + 24));
|
||||
},
|
||||
"syscall/js.valueCall": (t) => {
|
||||
t >>>= 0;
|
||||
try {
|
||||
const e = i(t + 8),
|
||||
l = Reflect.get(e, d(t + 16)),
|
||||
a = u(t + 32),
|
||||
c = Reflect.apply(l, e, a);
|
||||
(t = this._inst.exports.getsp() >>> 0),
|
||||
r(t + 56, c),
|
||||
this.mem.setUint8(t + 64, 1);
|
||||
} catch (e) {
|
||||
(t = this._inst.exports.getsp() >>> 0),
|
||||
r(t + 56, e),
|
||||
this.mem.setUint8(t + 64, 0);
|
||||
}
|
||||
},
|
||||
"syscall/js.valueInvoke": (t) => {
|
||||
t >>>= 0;
|
||||
try {
|
||||
const e = i(t + 8),
|
||||
l = u(t + 16),
|
||||
a = Reflect.apply(e, void 0, l);
|
||||
(t = this._inst.exports.getsp() >>> 0),
|
||||
r(t + 40, a),
|
||||
this.mem.setUint8(t + 48, 1);
|
||||
} catch (e) {
|
||||
(t = this._inst.exports.getsp() >>> 0),
|
||||
r(t + 40, e),
|
||||
this.mem.setUint8(t + 48, 0);
|
||||
}
|
||||
},
|
||||
"syscall/js.valueNew": (t) => {
|
||||
t >>>= 0;
|
||||
try {
|
||||
const e = i(t + 8),
|
||||
l = u(t + 16),
|
||||
a = Reflect.construct(e, l);
|
||||
(t = this._inst.exports.getsp() >>> 0),
|
||||
r(t + 40, a),
|
||||
this.mem.setUint8(t + 48, 1);
|
||||
} catch (e) {
|
||||
(t = this._inst.exports.getsp() >>> 0),
|
||||
r(t + 40, e),
|
||||
this.mem.setUint8(t + 48, 0);
|
||||
}
|
||||
},
|
||||
"syscall/js.valueLength": (t) => {
|
||||
(t >>>= 0), h(t + 16, parseInt(i(t + 8).length));
|
||||
},
|
||||
"syscall/js.valuePrepareString": (t) => {
|
||||
t >>>= 0;
|
||||
const e = g.encode(String(i(t + 8)));
|
||||
r(t + 16, e), h(t + 24, e.length);
|
||||
},
|
||||
"syscall/js.valueLoadString": (t) => {
|
||||
t >>>= 0;
|
||||
const e = i(t + 8);
|
||||
f(t + 16).set(e);
|
||||
},
|
||||
"syscall/js.valueInstanceOf": (t) => {
|
||||
(t >>>= 0), this.mem.setUint8(t + 24, i(t + 8) instanceof i(t + 16) ? 1 : 0);
|
||||
},
|
||||
"syscall/js.copyBytesToGo": (t) => {
|
||||
t >>>= 0;
|
||||
const e = f(t + 8),
|
||||
l = i(t + 32);
|
||||
if (!(l instanceof Uint8Array || l instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(t + 48, 0);
|
||||
return;
|
||||
}
|
||||
const a = l.subarray(0, e.length);
|
||||
e.set(a), h(t + 40, a.length), this.mem.setUint8(t + 48, 1);
|
||||
},
|
||||
"syscall/js.copyBytesToJS": (t) => {
|
||||
t >>>= 0;
|
||||
const e = i(t + 8),
|
||||
l = f(t + 16);
|
||||
if (!(e instanceof Uint8Array || e instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(t + 48, 0);
|
||||
return;
|
||||
}
|
||||
const a = l.subarray(0, e.length);
|
||||
e.set(a), h(t + 40, a.length), this.mem.setUint8(t + 48, 1);
|
||||
},
|
||||
debug: (t) => {
|
||||
console.log(t);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
async run(h) {
|
||||
if (!(h instanceof WebAssembly.Instance))
|
||||
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||
(this._inst = h),
|
||||
(this.mem = new DataView(this._inst.exports.mem.buffer)),
|
||||
(this._values = [NaN, 0, null, !0, !1, globalThis, this]),
|
||||
(this._goRefCounts = new Array(this._values.length).fill(1 / 0)),
|
||||
(this._ids = new Map([
|
||||
[0, 1],
|
||||
[null, 2],
|
||||
[!0, 3],
|
||||
[!1, 4],
|
||||
[globalThis, 5],
|
||||
[this, 6],
|
||||
])),
|
||||
(this._idPool = []),
|
||||
(this.exited = !1);
|
||||
let n = 4096;
|
||||
const s = (m) => {
|
||||
const t = n,
|
||||
e = g.encode(m + "\0");
|
||||
return (
|
||||
new Uint8Array(this.mem.buffer, n, e.length).set(e),
|
||||
(n += e.length),
|
||||
n % 8 !== 0 && (n += 8 - (n % 8)),
|
||||
t
|
||||
);
|
||||
},
|
||||
i = this.argv.length,
|
||||
r = [];
|
||||
this.argv.forEach((m) => {
|
||||
r.push(s(m));
|
||||
}),
|
||||
r.push(0),
|
||||
Object.keys(this.env)
|
||||
.sort()
|
||||
.forEach((m) => {
|
||||
r.push(s(`${m}=${this.env[m]}`));
|
||||
}),
|
||||
r.push(0);
|
||||
const u = n;
|
||||
if (
|
||||
(r.forEach((m) => {
|
||||
this.mem.setUint32(n, m, !0), this.mem.setUint32(n + 4, 0, !0), (n += 8);
|
||||
}),
|
||||
n >= 12288)
|
||||
)
|
||||
throw new Error(
|
||||
"total length of command line and environment variables exceeds limit"
|
||||
);
|
||||
this._inst.exports.run(i, u),
|
||||
this.exited && this._resolveExitPromise(),
|
||||
await this._exitPromise;
|
||||
}
|
||||
_resume() {
|
||||
if (this.exited) throw new Error("Go program has already exited");
|
||||
this._inst.exports.resume(), this.exited && this._resolveExitPromise();
|
||||
}
|
||||
_makeFuncWrapper(h) {
|
||||
const n = this;
|
||||
return function () {
|
||||
const s = { id: h, this: this, args: arguments };
|
||||
return (n._pendingEvent = s), n._resume(), s.result;
|
||||
};
|
||||
}
|
||||
};
|
||||
})();
|
||||
Loading…
Reference in a new issue