Architecture
This page explains how ireko is built, and why. The review of the project has the full analysis and the plan.
The modules
Section titled “The modules”Each module is an entry point of the package, so an application imports only what it uses.
| Entry point | Contents | Imports |
|---|---|---|
ireko | The model, the engine, the strategies, the routers and the builder | nothing |
ireko/text | parse and print | the core |
ireko/render | The theme, the viewport, the animation and mount | the core |
ireko/svg, ireko/dom, ireko/canvas | The renderers | ireko/render |
ireko/react | useLayout, LayoutSvg, LayoutView | the renderers, React |
ireko/xyflow | toXYFlow, layoutFlow, PolylineEdge, GroupNode | the core, ireko/render, React Flow |
ireko/examples | The examples of the gallery | the core |
The rules
Section titled “The rules”- The core has no DOM. A separate TypeScript project compiles
src/corewithout the types of the DOM. The core operates in a browser, a worker, Node.js and a build step. - Dependencies go down. A renderer imports the core. The core does not import a renderer. Oxlint finds import cycles.
- The data is plain. The input and the output are JSON. The output has absolute positions, so a renderer does no layout work.
- The engine does not change.
createEnginegives a frozen engine, andextendgives a new one. Two parts of an application can have different strategies without a conflict. - Each layout examines itself. The audit operates after each layout, and its findings are a part of the output.
- Same input, same output. The engine uses no random numbers and no clock. A golden file for each example holds its layout, and a test compares them.
Three extension points
Section titled “Three extension points”| Extension | Contract | Register it with |
|---|---|---|
| A strategy | arrange(children) → offsets, with an option schema | createEngine({ strategies }) |
| A router | route(source, target) → points | createEngine({ routers }) |
| A renderer | Read Layout, or give a Surface to mount | nothing |
Each contract is small, so an extension is a small module. A strategy does not know about recursion, padding, fit or absolute positions. A router does not know about the tree. A renderer does not know about the strategies. The guides show a strategy, a router and a renderer.
Why these choices
Section titled “Why these choices”The first version broke most of these rules. Its strategies read global singletons, and its routers depended on the render layer. The engine made its registry of strategies again at each run, so a custom strategy had no effect. The audit compared each child with its parent only, so it missed all overlaps between siblings.
The rewrite started from the data model, not from the old classes. The two passes fixed the overlaps. The small contracts made the strategies short: grid, radial and tree together are fewer than 400 lines, with their comments.
| Test | What it examines |
|---|---|
| Unit tests | Each function of the core, the parser and each renderer |
| Property tests | Random trees, strategies, modes, sizes, fit policies and options. The audit and a second, independent check must find nothing. |
| Golden files | The layout and the SVG of each example |
| Parity | SVG, DOM and Canvas draw the same boxes, to 0.001 pixels |
| Round trip | parse(print(input)) gives the input again |
| Site | Each page loads with no console error, the playground works, and axe finds no serious problem in both themes |
The coverage gate of the library is 98 % of the lines and 96 % of the branches. At this time, the tests cover more than 99 % of the lines.