Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

init d2.js #2278

Merged
merged 1 commit into from
Jan 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.POSIX:

.PHONY: all
all: fmt gen lint build test
all: fmt gen js lint build test

.PHONY: fmt
fmt:
Expand All @@ -21,3 +21,6 @@ test: fmt
.PHONY: race
race: fmt
prefix "$@" ./ci/test.sh --race ./...
.PHONY: js
js:
cd d2js/js && prefix "$@" ./make.sh
30 changes: 0 additions & 30 deletions d2js/README.md

This file was deleted.

2 changes: 1 addition & 1 deletion d2js/d2wasm/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
27 changes: 27 additions & 0 deletions d2js/js/.gitignore
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions d2js/js/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions d2js/js/Makefile
Original file line number Diff line number Diff line change
@@ -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 changes: 112 additions & 0 deletions d2js/js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# D2.js

[![npm version](https://badge.fury.io/js/%40terrastruct%2Fd2.svg)](https://www.npmjs.com/package/@terrastruct/d2)
[![License: MPL-2.0](https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg)](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 changes: 35 additions & 0 deletions d2js/js/build.js
Original file line number Diff line number Diff line change
@@ -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");
Binary file added d2js/js/bun.lockb
Binary file not shown.
18 changes: 18 additions & 0 deletions d2js/js/ci/build.sh
Original file line number Diff line number Diff line change
@@ -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 changes: 57 additions & 0 deletions d2js/js/dev-server.js
Original file line number Diff line number Diff line change
@@ -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 changes: 49 additions & 0 deletions d2js/js/examples/basic.html
Original file line number Diff line number Diff line change
@@ -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>
Loading
Loading