Skip to content

How it works inside

This page is for contributors and the curious. You do not need any of it to use docspp.

Scenario

The compiler runs once, on the server. Everything the browser needs arrives as JSON, and playback is a clock over a pure function.

HTTP request / responseData access
Loading diagram…
IWrite
1
diagram.yamlyou edit the file

Nodes, edges, views and scenarios. No coordinates.

2
diagram.yaml→Compilerparse and validate

Every error carries file:line:col and, where it can, a suggestion.

3
nodes/<id>.md→Compilernode docs
IICheck
4
Compiler→docspp checkdiagnostics
5
Compiler→Astro integrationcompiled diagrams

In astro build an error here stops the build, so a broken diagram never reaches the site.

6
Astro integration→CompiledDiagramserialise
IIIDraw
7
CompiledDiagram→<Diagram> islandprops

The island loads when the diagram scrolls into view.

8
<Diagram> island→Layoutmeasure cards

Cards are rendered offscreen at their fixed width so their real heights are known.

9
Layout→Canvas and playerpositions and routes
10
<Diagram> island→Timelinebuild the timeline
11
Timeline→Canvas and playerframe at t
12
Canvas and player→Readerpackets fly
  1. Compile, on the server. @packagelab/docspp-core/node reads diagram.yaml, expands shorthand, validates it with a Zod schema and resolves icons and Markdown. The result is a CompiledDiagram: plain JSON.
  2. Hand over. The Astro integration exposes every compiled diagram as virtual:docspp/diagrams and fails the build on errors. <Diagram> passes one diagram to a React island as props.
  3. Measure and lay out, in the browser. The island renders each card offscreen at its fixed width to learn its height, then calls layoutView. That runs ELK once per group, bottom up, then arranges the zones and routes the edges ELK cannot see.
  4. Play. buildTimeline(scenario, edgeLengths) turns a scenario into a timeline. frameAt(timeline, t) turns a time into a frame: where each packet is, which nodes are lit. The player is a clock that advances t.
packages/core schema, compiler, layout, router, timeline. No UI.
src/index.ts browser-safe entry: types, layout, route, timeline, geometry
src/node.ts node-only entry: YAML, Zod, compiler, icons, loader
packages/react <DiagramView> and its parts, plus styles.css (all tokens are --docspp-*)
packages/astro Astro integration (a Vite plugin) and <Diagram>
packages/cli the docspp command
packages/create-docspp npm create docspp; carries a copy of the template
  • The main entry of core stays browser-safe. Nothing reachable from src/index.ts may import node:*, yaml, zod, Iconify or unified. They live behind src/node.ts, so the client bundle only gets layout, route, timeline and types.
  • Compiled output is JSON. It crosses the server and client boundary as island props: no functions, classes, Maps or Dates.
  • The timeline is pure. buildTimeline and frameAt hold all playback logic. The React player only runs the clock, which makes scrubbing, stepping and testing trivial.
  • The story model is pure too. buildStory(diagram, scenario) turns a scenario into lanes, boxes and links. The view only measures the DOM and draws connectors over it.
  • Layout is group by group, bottom up. Flow groups use ELK, rows and row/grid groups are packed by docspp, zones are arranged by one more ELK run, and every edge ELK cannot see end to end goes through the router.
  • Validate in the compiler, not the renderer. A diagram that compiles must render.
  • Raw HTML in Markdown is dropped on purpose. Compiled docs are injected into the page.
  • Never leave will-change: transform on the zoomed layer. It makes the browser scale a bitmap, which blurs everything when zoomed in. The canvas sets it only while panning or zooming, and an end-to-end test guards the resting state.

Its own renderer on d3-zoom, not React Flow. Once ELK computes every position and every edge route, React Flow’s node, handle and edge model is bypassed, and what remains is pan and zoom, which d3-zoom does in a few lines. Owning the layers also makes the packet overlay, the dimming during playback and the follow camera straightforward.

YAML, not a custom language. A new language costs a parser and a language server. YAML with a JSON Schema gives editor autocomplete for free, and is easy for people and agents to write. Shorthand covers the common cases.

Model, views, scenarios. A node is written once. Views choose subsets, and scenarios play over a view. This is what keeps a large system maintainable.

Layout by group, not one ELK run. A single run over the whole hierarchy forces one direction on every zone. A platform-style diagram came out about three times too wide (3500 px against 1800 px) until each zone was laid out separately and the zones arranged afterwards. The price is routing the edges between zones in docspp’s own router. rows: exists because explicit rows are how people draw stacks, and ELK cannot do that on its own.

Hints, never coordinates. Declaration order, rows, layout: row and grid, direction and w. If a diagram needs more than that, the model is probably too big for one picture.

The timeline is data. Because a scenario compiles to a timeline and frameAt(t) is a pure function, scrubbing, stepping, deep links and tests all fall out of that one idea.

Compile and validate at build time. The build fails on a broken diagram, and the client receives plain JSON.

  • Auto layout is good, not perfect. Dense diagrams want a few hints.
  • Very large diagrams (30 nodes or more) render small until the reader zooms, goes full screen, or lets the camera follow a scenario.
  • An edge that ends on a member of a row or grid group stops at the group’s border.
  • ELK runs on the main thread. The diagram chunk is large, nearly all of it ELK, and loads only when a diagram scrolls into view.
  • Card heights are measured in the browser, so the first paint of a diagram shows a placeholder.