- commit
- aa5f209734f9f64cec7d1fc116167a53bc64cc7b
- parent
- a00a0107bda3c1a018a3a2c33f95b13b3810072e
- author
- Dami <dami.etoile@gmail.com>
Binary file.
@@ -0,0 +1,103 @@
1+//go:build !d2_host_eval2+3+package jsrunner4+5+import (6+ "context"7+ "errors"8+ "fmt"9+ "os"10+11+ "github.com/dop251/goja"12+)13+14+type gojaRunner struct {15+ vm *goja.Runtime16+}17+18+type gojaValue struct {19+ val goja.Value20+ vm *goja.Runtime21+}22+23+func NewJSRunner() JSRunner {24+ return &gojaRunner{vm: goja.New()}25+}26+27+func (g *gojaRunner) MustGet(key string) (JSValue, error) {28+ return nil, nil29+}30+31+func (g *gojaRunner) Engine() Engine {32+ return Goja33+}34+35+func (g *gojaRunner) RunString(code string) (JSValue, error) {36+ val, err := g.vm.RunString(code)37+ if err != nil {38+ return nil, err39+ }40+ return &gojaValue{val: val, vm: g.vm}, nil41+}42+43+func (v *gojaValue) String() string {44+ return v.val.String()45+}46+47+func (v *gojaValue) Export() interface{} {48+ return v.val.Export()49+}50+51+func (g *gojaRunner) NewObject() JSObject {52+ return &gojaValue{val: g.vm.NewObject(), vm: g.vm}53+}54+55+func (g *gojaRunner) Set(name string, value interface{}) error {56+ if name == "console" {57+ console, err := g.createConsole()58+ if err != nil {59+ return err60+ }61+ return g.vm.Set(name, console)62+ }63+ return g.vm.Set(name, value)64+}65+66+func (g *gojaRunner) WaitPromise(ctx context.Context, val JSValue) (interface{}, error) {67+ gVal := val.(*gojaValue)68+ promise := gVal.val.Export().(*goja.Promise)69+ for promise.State() == goja.PromiseStatePending {70+ if err := ctx.Err(); err != nil {71+ return nil, err72+ }73+ }74+ if promise.State() == goja.PromiseStateRejected {75+ return nil, errors.New("promise rejected")76+ }77+ return promise.Result().Export(), nil78+}79+80+func (g *gojaRunner) createConsole() (*goja.Object, error) {81+ console := g.vm.NewObject()82+ if err := console.Set("log", g.vm.ToValue(func(call goja.FunctionCall) goja.Value {83+ args := make([]interface{}, len(call.Arguments))84+ for i, arg := range call.Arguments {85+ args[i] = arg.Export()86+ }87+ fmt.Println(args...)88+ return nil89+ })); err != nil {90+ return nil, err91+ }92+ if err := console.Set("error", g.vm.ToValue(func(call goja.FunctionCall) goja.Value {93+ args := make([]interface{}, len(call.Arguments))94+ for i, arg := range call.Arguments {95+ args[i] = arg.Export()96+ }97+ fmt.Fprintln(os.Stderr, args...)98+ return nil99+ })); err != nil {100+ return nil, err101+ }102+ return console, nil103+}@@ -0,0 +1,7 @@
1+//go:build js && wasm && d2_host_eval2+3+package jsrunner4+5+// D2 normally selects a syscall/js runner for Wasm and evaluates Dagre in the6+// host global scope. Workers prohibit that dynamic evaluation. The build7+// overlay excludes the host runner so Dagre executes in the Goja runtime.@@ -2,12 +2,13 @@
22 33 - Engine: D2 parser/compiler/SVG renderer `v0.7.1`44 - Artifact: `d2-custom.wasm`5-- SHA-256: `5ab7315998cf4316d17dbb095c3b4eea4fad46e48554b5d76146437f1dba3344`6-- Build inputs: [`source/worker.go`](source/worker.go), [`source/main.go`](source/main.go), [`source/go.mod`](source/go.mod), and [`source/go.sum`](source/go.sum)5+- SHA-256: `c8ad712d37c19fd6ae721f2968e3ca2da9526d59cb7ae550854b7b5e9235cf70`6+- Build inputs: [`source/worker.go`](source/worker.go), [`source/main.go`](source/main.go), [`source/go.mod`](source/go.mod), [`source/go.sum`](source/go.sum), and the Worker-safe [`overlays/jsrunner`](overlays/jsrunner) source overlay.77 - Build command: `GOOS=js GOARCH=wasm go build -trimpath -buildvcs=false -ldflags='-s -w' -o ../d2-custom.wasm .`88 - Build toolchain: Go `go1.26.6`; use `npm run build:d2-wasm` from `diagramzip-render`.99 - D2 dependency: peeled module `oss.terrastruct.com/d2@v0.7.1` (module is deprecated upstream in favor of `github.com/d2lang/d2`; the lockfile remains authoritative for this artifact).10-- Layout: deterministic pure-Go grid with straight-line edge routing; Dagre and ELK are intentionally not invoked.10+- Layout: D2's bundled Dagre renderer with its default top-to-bottom flow. ELK is not included.11+- JavaScript runtime: Dagre executes inside the pure-Go Goja runtime. The build stages the pinned D2 module in a temporary directory and replaces only its runtime-selection files; the upstream Wasm host-eval runner is excluded because Workers prohibit dynamic code generation.1112 - Host shim: Go standard `wasm_exec.js` from the build Go toolchain.1213 1314 The D2 source and renderer are MPL-2.0. The generated artifact remains subject to the upstream D2 license and notices.@@ -8,55 +8,52 @@ import (
88 "os"99 1010 "oss.terrastruct.com/d2/d2graph"11+ "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"1112 "oss.terrastruct.com/d2/d2lib"1213 "oss.terrastruct.com/d2/d2renderers/d2svg"13- "oss.terrastruct.com/d2/lib/geo"1414 "oss.terrastruct.com/d2/lib/textmeasure"1515 )1616 17-func gridLayout(ctx context.Context, g *d2graph.Graph) error {18- if err := ctx.Err(); err != nil { return err }19- const cols = 420- const gap = 80.021- for i, o := range g.Objects {22- if o == g.Root || o.Box == nil { continue }23- o.Box.TopLeft = geo.NewPoint(float64(i%cols)*220, float64(i/cols)*160)17+func dagreLayoutResolver(engine string) (d2graph.LayoutGraph, error) {18+ if engine != "dagre" {19+ return nil, fmt.Errorf("unexpected layout %q", engine)2420 }25- for _, e := range g.Edges {26- if e.Src != nil && e.Dst != nil && e.Src.Box != nil && e.Dst.Box != nil {27- e.Route = []*geo.Point{e.Src.Box.Center(), e.Dst.Box.Center()}28- }29- }30- return nil21+ return func(ctx context.Context, graph *d2graph.Graph) error {22+ return d2dagrelayout.Layout(ctx, graph, nil)23+ }, nil3124 }3225 3326 func main() {3427 input := "x -> y\nsteps: {\n 1: {\n x -> z\n }\n}"35- layout := "grid"28+ layout := "dagre"3629 pad := int64(50)3730 ruler, err := textmeasure.NewRuler()38- if err != nil { panic(err) }31+ if err != nil {32+ panic(err)33+ }3934 d, graph, err := d2lib.Compile(context.Background(), input, &d2lib.CompileOptions{40- Layout: &layout,41- Ruler: ruler,42- LayoutResolver: func(engine string) (d2graph.LayoutGraph, error) {43- if engine != "grid" { return nil, fmt.Errorf("unexpected layout %q", engine) }44- return gridLayout, nil45- },46- RouterResolver: func(engine string) (d2graph.RouteEdges, error) {47- return func(ctx context.Context, g *d2graph.Graph, edges []*d2graph.Edge) error {48- for _, e := range edges {49- if e.Src == nil || e.Dst == nil || e.Src.Box == nil || e.Dst.Box == nil { continue }50- e.Route = []*geo.Point{e.Src.Box.Center(), e.Dst.Box.Center()}51- }52- return nil53- }, nil54- },35+ Layout: &layout,36+ Ruler: ruler,37+ LayoutResolver: dagreLayoutResolver,5538 }, &d2svg.RenderOpts{Pad: &pad})56- if err != nil { panic(err) }39+ if err != nil {40+ panic(err)41+ }5742 fmt.Printf("shapes=%d connections=%d\n", len(d.Shapes), len(d.Connections))58- for _, o := range graph.Objects { fmt.Printf("obj=%s box=%v\n", o.ID, o.Box) }59- for _, c := range d.Connections { fmt.Printf("route=%d src=%s dst=%s\n", len(c.Route), c.Src, c.Dst) }60- if err := os.WriteFile("/tmp/d2-custom.svg", func() []byte { b, e := d2svg.Render(d, &d2svg.RenderOpts{Pad: &pad}); if e != nil { panic(e) }; return b }(), 0644); err != nil { panic(err) }43+ for _, o := range graph.Objects {44+ fmt.Printf("obj=%s box=%v\n", o.ID, o.Box)45+ }46+ for _, c := range d.Connections {47+ fmt.Printf("route=%d src=%s dst=%s\n", len(c.Route), c.Src, c.Dst)48+ }49+ if err := os.WriteFile("/tmp/d2-custom.svg", func() []byte {50+ b, e := d2svg.Render(d, &d2svg.RenderOpts{Pad: &pad})51+ if e != nil {52+ panic(e)53+ }54+ return b55+ }(), 0644); err != nil {56+ panic(err)57+ }6158 fmt.Printf("layers=%d scenarios=%d steps=%d\n", len(d.Layers), len(d.Scenarios), len(d.Steps))6259 }@@ -10,67 +10,30 @@ import (
1010 "runtime/debug"1111 "syscall/js"1212 13- "oss.terrastruct.com/util-go/go2"14-1513 "oss.terrastruct.com/d2/d2graph"14+ "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"1615 "oss.terrastruct.com/d2/d2lib"1716 "oss.terrastruct.com/d2/d2renderers/d2animate"1817 "oss.terrastruct.com/d2/d2renderers/d2svg"19- "oss.terrastruct.com/d2/lib/geo"20- "oss.terrastruct.com/d2/lib/label"2118 d2log "oss.terrastruct.com/d2/lib/log"2219 "oss.terrastruct.com/d2/lib/textmeasure"2320 )2421 25-func gridLayout(ctx context.Context, g *d2graph.Graph) error {26- if err := ctx.Err(); err != nil {27- return err28- }29- for i, o := range g.Objects {30- if o == g.Root || o.Box == nil {31- continue32- }33- if o.HasLabel() && o.LabelPosition == nil {34- o.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())35- }36- if o.HasIcon() && o.IconPosition == nil {37- o.IconPosition = go2.Pointer(label.InsideMiddleCenter.String())38- }39- o.Box.TopLeft = geo.NewPoint(float64(i%4)*220, float64(i/4)*160)40- }41- for _, e := range g.Edges {42- if e.Src != nil && e.Dst != nil && e.Src.Box != nil && e.Dst.Box != nil {43- e.Route = []*geo.Point{e.Src.Box.Center(), e.Dst.Box.Center()}44- e.TraceToShape(e.Route, 0, len(e.Route)-1)45- if e.Label.Value != "" {46- e.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())47- }48- }22+func dagreLayoutResolver(engine string) (d2graph.LayoutGraph, error) {23+ if engine != "dagre" {24+ return nil, fmt.Errorf("unexpected layout %q", engine)4925 }50- return nil26+ return func(ctx context.Context, graph *d2graph.Graph) error {27+ return d2dagrelayout.Layout(ctx, graph, nil)28+ }, nil5129 }5230 5331 type response struct {5432 SVG string `json:"svg"`5533 Boards []string `json:"boards"`5634 }57-type renderOptions struct { AnimateInterval int64 `json:"animateInterval"` }58-59-func straightRouter(ctx context.Context, g *d2graph.Graph, edges []*d2graph.Edge) error {60- if err := ctx.Err(); err != nil {61- return err62- }63- for _, e := range edges {64- if e.Src == nil || e.Dst == nil || e.Src.Box == nil || e.Dst.Box == nil {65- continue66- }67- e.Route = []*geo.Point{e.Src.Box.Center(), e.Dst.Box.Center()}68- e.TraceToShape(e.Route, 0, len(e.Route)-1)69- if e.Label.Value != "" {70- e.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())71- }72- }73- return nil35+type renderOptions struct {36+ AnimateInterval int64 `json:"animateInterval"`7437 }7538 7639 func render(this js.Value, args []js.Value) (result any) {@@ -85,19 +48,21 @@ func render(this js.Value, args []js.Value) (result any) {
8548 }8649 var inputOptions renderOptions8750 if len(args) == 2 && args[1].String() != "" {88- if err := json.Unmarshal([]byte(args[1].String()), &inputOptions); err != nil { return js.ValueOf(fmt.Sprintf(`{"error":%q}`, err.Error())) }51+ if err := json.Unmarshal([]byte(args[1].String()), &inputOptions); err != nil {52+ return js.ValueOf(fmt.Sprintf(`{"error":%q}`, err.Error()))53+ }8954 }9055 ruler, err := textmeasure.NewRuler()9156 if err != nil {9257 return js.ValueOf(fmt.Sprintf(`{"error":%q}`, err.Error()))9358 }94- layout := "grid"59+ layout := "dagre"9560 pad := int64(100)9661 ctx := d2log.WithDefault(context.Background())9762 d, _, err := d2lib.Compile(ctx, args[0].String(), &d2lib.CompileOptions{98- Layout: &layout, Ruler: ruler,99- LayoutResolver: func(string) (d2graph.LayoutGraph, error) { return gridLayout, nil },100- RouterResolver: func(string) (d2graph.RouteEdges, error) { return straightRouter, nil },63+ Layout: &layout,64+ Ruler: ruler,65+ LayoutResolver: dagreLayoutResolver,10166 }, &d2svg.RenderOpts{Pad: &pad})10267 if err != nil {10368 return js.ValueOf(fmt.Sprintf(`{"error":%q}`, err.Error()))@@ -110,7 +75,9 @@ func render(this js.Value, args []js.Value) (result any) {
11075 base := boards[0]11176 if inputOptions.AnimateInterval > 0 {11277 base, err = d2animate.Wrap(d, boards, *opts, int(inputOptions.AnimateInterval))113- if err != nil { return js.ValueOf(fmt.Sprintf(`{"error":%q}`, err.Error())) }78+ if err != nil {79+ return js.ValueOf(fmt.Sprintf(`{"error":%q}`, err.Error()))80+ }11481 // The wrapper is itself the XML document; nested board declarations would11582 // otherwise be rejected by the shared SVG sanitizer as extra roots.11683 base = bytes.ReplaceAll(base, []byte(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>`), nil)@@ -1,9 +1,10 @@
11 import { execFileSync } from 'node:child_process'2-import { copyFileSync, mkdirSync, renameSync } from 'node:fs'2+import { chmodSync, copyFileSync, cpSync, mkdirSync, renameSync, rmSync } from 'node:fs'33 import { resolve } from 'node:path'44 55 const root = resolve(import.meta.dirname, '..')66 const source = resolve(root, 'd2-wasm/source')7+const jsrunnerOverlay = resolve(root, 'd2-wasm/overlays/jsrunner')78 const output = resolve(root, 'd2-wasm/d2-custom.wasm')89 const temporary = resolve(root, 'd2-wasm/.d2-custom.wasm.tmp')910 @@ -12,12 +13,35 @@ if (!goVersion.includes('go1.26.6')) {
1213 throw new Error(`D2 Wasm build requires Go 1.26.6; found ${goVersion}`)1314 }1415 15-execFileSync('go', ['build', '-trimpath', '-buildvcs=false', '-ldflags=-s -w', '-o', temporary, '.'], {16+const d2Module = execFileSync('go', ['list', '-m', '-f', '{{.Dir}}', 'oss.terrastruct.com/d2'], {1617 cwd: source,17- stdio: 'inherit',18- env: { ...process.env, GOOS: 'js', GOARCH: 'wasm' },19-})20-renameSync(temporary, output)18+ encoding: 'utf8',19+}).trim()20+const staging = resolve(root, 'd2-wasm/.d2-build')21+const stagedModule = resolve(staging, 'd2')22+const stagedModfile = resolve(staging, 'worker.mod')23+rmSync(staging, { force: true, recursive: true })24+mkdirSync(staging, { recursive: true })25+cpSync(d2Module, stagedModule, { recursive: true })26+chmodSync(resolve(stagedModule, 'lib/jsrunner/js.go'), 0o644)27+chmodSync(resolve(stagedModule, 'lib/jsrunner/goja.go'), 0o644)28+copyFileSync(resolve(jsrunnerOverlay, 'js.go'), resolve(stagedModule, 'lib/jsrunner/js.go'))29+copyFileSync(resolve(jsrunnerOverlay, 'goja.go'), resolve(stagedModule, 'lib/jsrunner/goja.go'))30+copyFileSync(resolve(source, 'go.mod'), stagedModfile)31+copyFileSync(resolve(source, 'go.sum'), resolve(staging, 'worker.sum'))32+execFileSync('go', ['mod', 'edit', `-modfile=${stagedModfile}`, `-replace=oss.terrastruct.com/d2=${stagedModule}`], { cwd: source })33+34+try {35+ execFileSync('go', ['build', `-modfile=${stagedModfile}`, '-trimpath', '-buildvcs=false', '-ldflags=-s -w', '-o', temporary, '.'], {36+ cwd: source,37+ stdio: 'inherit',38+ env: { ...process.env, GOOS: 'js', GOARCH: 'wasm' },39+ })40+ renameSync(temporary, output)41+} finally {42+ rmSync(staging, { force: true, recursive: true })43+ rmSync(temporary, { force: true })44+}2145 2246 const goroot = execFileSync('go', ['env', 'GOROOT'], { encoding: 'utf8' }).trim()2347 copyFileSync(resolve(goroot, 'lib/wasm/wasm_exec.js'), resolve(root, 'd2-wasm/wasm_exec.js'))@@ -182,7 +182,7 @@ async function smoke([engine, filename]) {
182182 }183183 if (d2Engines.has(engine)) {184184 if (response.headers.get('X-Diagram-Renderer') !== 'edge-wasm') throw new Error(`${engine}: expected edge-wasm renderer, received ${response.headers.get('X-Diagram-Renderer')}`)185- if (!response.headers.get('X-Renderer-Build')?.startsWith('d2-0.7.1-custom-grid-')) throw new Error(`${engine}: unexpected D2 Worker build ${response.headers.get('X-Renderer-Build')}`)185+ if (!response.headers.get('X-Renderer-Build')?.startsWith('d2-0.7.1-dagre-wasm-')) throw new Error(`${engine}: unexpected D2 Worker build ${response.headers.get('X-Renderer-Build')}`)186186 }187187 if (symbolatorEngines.has(engine)) {188188 if (response.headers.get('X-Diagram-Renderer') !== 'edge-python') throw new Error(`${engine}: expected edge-python renderer, received ${response.headers.get('X-Diagram-Renderer')}`)@@ -4,7 +4,7 @@ import { RenderError } from '../../errors'
44 import type { RendererAdapter } from '../../types'55 import { edgeResult } from './types'66 7-const VERSION = 'd2@0.7.1/custom-grid-1'7+const VERSION = 'd2@0.7.1/dagre-wasm-1'88 const MAX_SOURCE_BYTES = 512 * 102499 const SUPPORTED_OPTIONS = new Set(['layout', 'animate-interval'])1010 @@ -81,8 +81,8 @@ export const d2Adapter: RendererAdapter = {
8181 throw new RenderError(400, 'unsupported_options', `Unsupported D2 option: ${name}.`)8282 }8383 }84- if (request.options.layout !== undefined && request.options.layout.toLowerCase() !== 'grid') {85- throw new RenderError(400, 'unsupported_options', 'D2 only supports the deterministic grid layout in the edge unit; dagre and elk are unavailable.')84+ if (request.options.layout !== undefined && request.options.layout.toLowerCase() !== 'dagre') {85+ throw new RenderError(400, 'unsupported_options', 'D2 supports Dagre layout in the edge unit; ELK is unavailable.')8686 }8787 const animateInterval = request.options['animate-interval']8888 if (animateInterval !== undefined && (!/^\d+$/.test(animateInterval) || Number(animateInterval) < 1 || Number(animateInterval) > 60_000)) {@@ -100,7 +100,7 @@ const versions: Record<EngineId, string> = {
100100 erd: 'erd@0.2.1.0+graphviz@15.1.1/edge-wasm-1',101101 wireviz: 'wireviz@0.3.2/python-translator-1',102102 structurizr: 'structurizr@6.2.2+plantuml@1.2026.6/translation-1',103- d2: 'd2@0.7.1/custom-grid-1',103+ d2: 'd2@0.7.1/dagre-wasm-1',104104 ditaa: 'ditaa-ascii+svgbob@0.7.6/edge-wasm-1',105105 symbolator: 'symbolator@1.2.2/python-translation-1',106106 tikz: '@planktimerr/tikzjax@1.0.63/client-unit-1',@@ -189,8 +189,7 @@ const losses: Partial<Record<EngineId, readonly string[]>> = {
189189 ],190190 d2: [191191 'Only SVG is supported.',192- 'The edge build uses official D2 v0.7.1 parser/compiler/SVG renderer with a deterministic grid layout and straight-line routing; Dagre and ELK layouts are unavailable.',193- 'D2 syntax, labels, shapes, containers, steps/scenarios, and animated edges are retained, but layout quality differs substantially from Dagre/ELK.',192+ 'The edge build uses D2 v0.7.1 with its official bundled Dagre layout. ELK is unavailable.',194193 'The supported animation-interval option is bounded to 1–60000 milliseconds; animated edges and multi-board SVG output remain supported.',195194 ],196195 ditaa: [@@ -7,8 +7,7 @@ export default createRendererUnit({
77 adapter: d2Adapter,88 knownLosses: [99 'Only SVG is supported.',10- 'The edge build uses official D2 v0.7.1 parser/compiler/SVG renderer with a deterministic grid layout and straight-line routing; Dagre and ELK layouts are unavailable.',11- 'D2 syntax, labels, shapes, containers, steps/scenarios, and animated edges are retained, but layout quality differs substantially from Dagre/ELK.',10+ 'The edge build uses D2 v0.7.1 with its official bundled Dagre layout. ELK is unavailable.',1211 'The supported animation-interval option is bounded to 1–60000 milliseconds; animated edges and multi-board SVG output remain supported.',1312 ],1413 })@@ -62,7 +62,7 @@ describe('engine catalog', () => {
6262 it('marks D2 as a dedicated edge-Wasm unit', () => {6363 const d2 = ENGINE_CATALOG.find(engine => engine.id === 'd2')!6464 expect(d2.activeRuntime).toBe('edge-wasm')65- expect(d2.version).toContain('d2@0.7.1/custom-grid')65+ expect(d2.version).toContain('d2@0.7.1/dagre-wasm')6666 })6767 6868 it('marks Symbolator as a dedicated Python translation unit', () => {@@ -1,4 +1,5 @@
11 import { describe, expect, it } from 'vitest'2+import { DOMParser } from 'linkedom'23 import { d2Adapter } from '../src/adapters/edge/d2'34 import type { RenderRequest } from '../src/types'45 @@ -10,7 +11,20 @@ const request = (source: string, options: Record<string, string> = {}): RenderRe
1011 })1112 1213 describe('D2 dedicated Worker adapter', () => {13- it('rejects Dagre/ELK and out-of-range animation options explicitly', async () => {14+ it('uses the Dagre top-to-bottom default layout', async () => {15+ const result = await d2Adapter.render(request('a -> b -> c'), new AbortController().signal)16+ const document = new DOMParser().parseFromString(result.body, 'image/svg+xml')17+ const nodes = [...document.querySelectorAll('g.shape > rect')].slice(0, 3)18+ const x = nodes.map(node => Number(node.getAttribute('x')))19+ const y = nodes.map(node => Number(node.getAttribute('y')))20+ const span = (values: number[]) => Math.max(...values) - Math.min(...values)21+22+ expect(nodes).toHaveLength(3)23+ expect(span(y)).toBeGreaterThan(span(x))24+ })25+26+ it('accepts Dagre and rejects ELK and out-of-range animation options explicitly', async () => {27+ await expect(d2Adapter.render(request('a -> b', { layout: 'dagre' }), new AbortController().signal)).resolves.toMatchObject({ contentType: 'image/svg+xml' })1428 await expect(d2Adapter.render(request('a -> b', { layout: 'elk' }), new AbortController().signal)).rejects.toMatchObject({ code: 'unsupported_options', status: 400 })1529 await expect(d2Adapter.render(request('a -> b', { 'animate-interval': '0' }), new AbortController().signal)).rejects.toMatchObject({ code: 'invalid_options', status: 400 })1630 await expect(d2Adapter.render(request('a -> b', { 'animate-interval': '60001' }), new AbortController().signal)).rejects.toMatchObject({ code: 'invalid_options', status: 400 })@@ -1 +1 @@
1-{"$schema":"../node_modules/wrangler/config-schema.json","name":"diagramzip-render-d2","main":"../src/units/d2.ts","compatibility_date":"2026-08-20","compatibility_flags":["nodejs_compat"],"observability":{"enabled":true},"vars":{"RENDERER_BUILD":"d2-0.7.1-custom-grid-4"},"rules":[{"type":"CompiledWasm","globs":["**/*.wasm"],"fallthrough":false}],"routes":[{"pattern":"d2.render.diagram.zip","custom_domain":true}]}1+{"$schema":"../node_modules/wrangler/config-schema.json","name":"diagramzip-render-d2","main":"../src/units/d2.ts","compatibility_date":"2026-08-20","compatibility_flags":["nodejs_compat"],"observability":{"enabled":true},"vars":{"RENDERER_BUILD":"d2-0.7.1-dagre-wasm-1"},"rules":[{"type":"CompiledWasm","globs":["**/*.wasm"],"fallthrough":false}],"routes":[{"pattern":"d2.render.diagram.zip","custom_domain":true}]}