bromaid

Diagrams-as-code for TypeScript. Parse a tiny DSL, get an SVG string. Pure functions, zero DOM, runs anywhere.

parse → layout → renderSVG. Each step is a pure function. No browser globals, no worker URLs, no DOM. Works in Node, Bun, Deno, Cloudflare Workers, the edge, a CLI — and inside a React Server Component.


Install

npm install bromaid
# or
pnpm add bromaid
# or
yarn add bromaid

The bromaid package is the batteries-included umbrella — it bundles @bromaid/core, @bromaid/layout, @bromaid/renderer, and @bromaid/theme. It has one runtime dep: elkjs (no-worker bundle).

If you want finer control, install the individual packages — see Packages below.

Requires: Node 18+ (or any modern runtime with ES2022 + fetch).


Quick start

import { renderFromSource } from 'bromaid';

const svg = await renderFromSource(`
  actor user "User"
  service api "API"
  edge user -> api
`);

console.log(svg); // <svg xmlns="..."> … </svg>

That's it. svg is a string you can write to a file, return from an HTTP handler, or drop into HTML.


Usage

Node script

import { writeFile } from 'node:fs/promises';
import { renderFromSource } from 'bromaid';

const svg = await renderFromSource(source, { mode: 'dark' });
await writeFile('diagram.svg', svg);

Next.js (App Router, RSC)

// app/page.tsx
import { readFile } from 'node:fs/promises';
import { renderFromSource } from 'bromaid';

export default async function Page() {
  const source = await readFile('arch.bro', 'utf8');
  const svg = await renderFromSource(source, { mode: 'dark' });
  return <div dangerouslySetInnerHTML={{ __html: svg }} />;
}

Renders at build time. The browser receives inline SVG and nothing else.

Step-by-step (when you want the intermediate stages)

import { parse, layout, renderSVG, defaultTheme } from 'bromaid';

const program = parse(source);          // source → AST
const graph = await layout(program);    // AST → laid-out graph (uses elkjs)
const svg = renderSVG(graph, {          // graph → SVG string
  theme: defaultTheme,
  mode: 'dark',
});

Useful if you want to inspect the AST, cache the layout, or swap renderers.

Cloudflare Worker / edge

Works as-is. No special config. The layout uses elkjs configured without web workers, so it runs in any V8 isolate.

export default {
  async fetch(request: Request) {
    const source = await request.text();
    const svg = await renderFromSource(source);
    return new Response(svg, { headers: { 'content-type': 'image/svg+xml' } });
  },
};

DSL

The DSL is intentionally small. Full reference: SPEC.md (also at bromaid.com/spec).

actor user "User"

region aws:eu-west-1 "AWS EU-WEST-1" {
  vpc "Production VPC" {
    subnet.public "Public" {
      lb "Load Balancer"
      gateway "API Gateway"
    }
    subnet.private "Services" {
      auth "Auth Service"
      orders "Orders Service"
    }
  }
}

external stripe "Stripe"

edge user -> lb
edge lb -> gateway
edge gateway -> auth
edge gateway -> orders
edge orders -> stripe [label: payments]

More samples in examples/ (micro.bro, simple.bro, full.bro).

For LLMs / AI agents: the spec is also served as plain text at bromaid.com/llms.txt.


API

import {
  parse,
  layout,
  renderSVG,
  renderFromSource,
  defaultTheme,
  defineTheme,
  mergeTheme,
} from 'bromaid';
Export Signature What it does
parse (source: string) => Program Tokenize + parse DSL. Throws BromaidError on syntax errors with line/col info.
layout (program: Program, opts?: LayoutOptions) => Promise<LayoutGraph> Run elkjs layout. Async because elkjs is async.
renderSVG (graph: LayoutGraph, opts?: RenderOptions) => string Serialize graph to SVG. Sync. DOM-free.
renderFromSource (source: string, opts?: RenderOptions) => Promise<string> Convenience: parse → layout → renderSVG.
defaultTheme Theme The shipped theme.
defineTheme (input: DefineThemeInput) => Theme Build a theme from a partial spec.
mergeTheme (base: Theme, overrides: ThemeOverrides) => Theme Override slots of an existing theme.

RenderOptions

type RenderOptions = {
  theme?: Theme;        // default: defaultTheme
  mode?: 'light' | 'dark';  // default: 'dark'
};

Errors

parse throws BromaidError with a stable code (BromaidErrorCode), the offending source span, and a human message. Catch it and render a friendly diagnostic.

import { parse, type BromaidError } from 'bromaid';

try {
  parse(source);
} catch (err) {
  const e = err as BromaidError;
  console.error(`[${e.code}] ${e.message} at ${e.span?.start.line}:${e.span?.start.col}`);
}

Theming

import { defineTheme, mergeTheme, defaultTheme, renderFromSource } from 'bromaid';

const myTheme = mergeTheme(defaultTheme, {
  slugs: {
    'aws:*': { accent: '#ff9900' },
    'stripe': { accent: '#635bff' },
  },
});

const svg = await renderFromSource(source, { theme: myTheme, mode: 'dark' });

Themes resolve in this order: explicit slug match → type default → theme fallback. See packages/theme for the full slot list.


Packages

If you don't want the umbrella, install pieces directly:

Package What it does
bromaid Umbrella. Re-exports everything below + renderFromSource.
@bromaid/core Tokenizer, parser, AST, structured errors. Pure TS, no deps.
@bromaid/layout Wraps elkjs (no-worker bundle). Program → LayoutGraph.
@bromaid/renderer LayoutGraph → SVG string. DOM-free.
@bromaid/theme Slug + type theme registry. Pluggable via defineTheme / mergeTheme.

Examples


Roadmap

Phase 1 (shipping): the packages above.

Phase 2 / 3 (planned):


Development

pnpm i
pnpm typecheck
pnpm test
pnpm build
pnpm smoke         # render examples/full.bro to architecture.svg
pnpm smoke:next    # build the Next.js App Router smoke test

Source of truth for the DSL is packages/core/src/schema.ts — enums, regex, descriptions, and Zod schemas live there. SPEC.md is regenerated from it via pnpm gen:spec.


License

MIT

Analytics cookiesWe'd like to use PostHog analytics to understand how the playground is used so we can improve it. See our privacy policy for details.