Skip to content

Commit

Permalink
Merge pull request #2292 from alixander/d2js-dev-server
Browse files Browse the repository at this point in the history
d2js: dev server changes
  • Loading branch information
alixander authored Jan 15, 2025
2 parents 2be0348 + 10a3c01 commit fcbe464
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 35 deletions.
5 changes: 5 additions & 0 deletions d2js/js/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ fmt: node_modules
build: fmt
prefix "$@" ./ci/build.sh

.PHONY: dev
dev: build
prefix "$@" git checkout -- src/platform.js src/worker.js
prefix "$@" bun run dev

.PHONY: test
test: build
prefix "$@" bun test:all
Expand Down
6 changes: 3 additions & 3 deletions d2js/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,12 @@ If you change the main D2 source code, you should regenerate the WASM file:
./make.sh build
```

### Running the Development Server
### Running the dev server

Make sure you've built already, then run:
You can browse the examples by running the dev server:

```bash
bun run dev
./make.sh dev
```

Visit `http://localhost:3000` to see the example page.
Expand Down
79 changes: 47 additions & 32 deletions d2js/js/dev-server.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const fs = require("fs/promises");
const path = require("path");

const MIME_TYPES = {
".html": "text/html",
".js": "text/javascript",
Expand All @@ -11,45 +14,57 @@ 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";
}
let filePath = url.pathname.slice(1); // Remove leading "/"

// 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/");
}
if (filePath === "") {
filePath = "examples/";
}

try {
const filePath = path.slice(1);
const file = Bun.file(filePath);
const exists = await file.exists();
const fullPath = path.join(process.cwd(), filePath);
const stats = await fs.stat(fullPath);

if (!exists) {
return new Response(`File not found: ${path}`, { status: 404 });
}
if (stats.isDirectory()) {
const entries = await fs.readdir(fullPath);
const links = await Promise.all(
entries.map(async (entry) => {
const entryPath = path.join(fullPath, entry);
const isDir = (await fs.stat(entryPath)).isDirectory();
const slash = isDir ? "/" : "";
return `<li><a href="${filePath}${entry}${slash}">${entry}${slash}</a></li>`;
})
);

const html = `
<html>
<body>
<h1>Examples</h1>
<ul>
${links.join("")}
</ul>
</body>
</html>
`;
return new Response(html, {
headers: { "Content-Type": "text/html" },
});
} else {
const ext = path.extname(filePath);
const mimeType = MIME_TYPES[ext] || "application/octet-stream";

// 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",
},
});
const file = Bun.file(filePath);
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.error(`Error serving ${filePath}:`, err);
return new Response(`File not found: ${filePath}`, { status: 404 });
}
},
});
Expand Down
103 changes: 103 additions & 0 deletions d2js/js/examples/customizable.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
display: flex;
gap: 20px;
padding: 20px;
height: 100vh;
margin: 0;
font-family: system-ui, -apple-system, sans-serif;
}
.controls {
display: flex;
flex-direction: column;
gap: 12px;
width: 400px;
}
textarea {
width: 100%;
height: 300px;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: monospace;
}
.layout-toggle {
display: flex;
gap: 16px;
align-items: center;
}
.radio-group {
display: flex;
gap: 12px;
}
.radio-label {
display: flex;
gap: 4px;
align-items: center;
cursor: pointer;
}
button {
padding: 8px 16px;
background: #0066cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0052a3;
}
#output {
flex: 1;
overflow: auto;
border: 1px solid #eee;
border-radius: 4px;
padding: 16px;
}
#output svg {
max-width: 100%;
max-height: 90vh;
}
</style>
</head>
<body>
<div class="controls">
<textarea id="input">x -> y</textarea>
<div class="layout-toggle">
<span>Layout:</span>
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="layout" value="dagre" checked />
Dagre
</label>
<label class="radio-label">
<input type="radio" name="layout" value="elk" />
ELK
</label>
</div>
</div>
<button onclick="compile()">Compile</button>
</div>
<div id="output"></div>
<script type="module">
import { D2 } from "../dist/browser/index.js";
const d2 = new D2();
window.compile = async () => {
const input = document.getElementById("input").value;
const layout = document.querySelector('input[name="layout"]:checked').value;
try {
const result = await d2.compile(input, { layout });
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>

0 comments on commit fcbe464

Please sign in to comment.