How it works inside
This page is for contributors and the curious. You do not need any of it to use docspp.
The pipeline
Section titled “The pipeline”The compiler runs once, on the server. Everything the browser needs arrives as JSON, and playback is a clock over a pure function.
Nodes, edges, views and scenarios. No coordinates.
Every error carries file:line:col and, where it can, a suggestion.
In astro build an error here stops the build, so a broken diagram never reaches the site.
The island loads when the diagram scrolls into view.
Cards are rendered offscreen at their fixed width so their real heights are known.
- Compile, on the server.
@packagelab/docspp-core/nodereadsdiagram.yaml, expands shorthand, validates it with a Zod schema and resolves icons and Markdown. The result is aCompiledDiagram: plain JSON. - Hand over. The Astro integration exposes every compiled diagram as
virtual:docspp/diagramsand fails the build on errors.<Diagram>passes one diagram to a React island as props. - 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. - 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 advancest.
Packages
Section titled “Packages”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, loaderpackages/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 commandpackages/create-docspp npm create docspp; carries a copy of the templateRules that keep it working
Section titled “Rules that keep it working”- The main entry of core stays browser-safe. Nothing reachable from
src/index.tsmay importnode:*,yaml,zod, Iconify or unified. They live behindsrc/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.
buildTimelineandframeAthold 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,
rowsandrow/gridgroups 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: transformon 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.
Decisions
Section titled “Decisions”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.
Known limits
Section titled “Known limits”- 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
roworgridgroup 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.