Skip to content

Commit

Permalink
Merge pull request #2278 from alixander/d2js-package
Browse files Browse the repository at this point in the history
init d2.js
  • Loading branch information
alixander authored Jan 12, 2025
2 parents 13af63a + 8d71e8f commit 5630b0f
Show file tree
Hide file tree
Showing 21 changed files with 1,176 additions and 32 deletions.
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

0 comments on commit 5630b0f

Please sign in to comment.