Skip to content

Architecture

This page explains how ireko is built, and why. The review of the project has the full analysis and the plan.

This figure is an ireko layout too. Each arrow is an import. No arrow goes up.The modules of irekoAdaptersRenderersSharedThe core: ireko (no DOM)ireko/reactireko/xyflowireko/svgireko/domireko/canvasireko/renderireko/textireko/examplesmodelenginestrategiesroutersbuilder
This figure is an ireko layout too. Each arrow is an import. No arrow goes up.Open in the playground →

Each module is an entry point of the package, so an application imports only what it uses.

Entry pointContentsImports
irekoThe model, the engine, the strategies, the routers and the buildernothing
ireko/textparse and printthe core
ireko/renderThe theme, the viewport, the animation and mountthe core
ireko/svg, ireko/dom, ireko/canvasThe renderersireko/render
ireko/reactuseLayout, LayoutSvg, LayoutViewthe renderers, React
ireko/xyflowtoXYFlow, layoutFlow, PolylineEdge, GroupNodethe core, ireko/render, React Flow
ireko/examplesThe examples of the gallerythe core
  1. The core has no DOM. A separate TypeScript project compiles src/core without the types of the DOM. The core operates in a browser, a worker, Node.js and a build step.
  2. Dependencies go down. A renderer imports the core. The core does not import a renderer. Oxlint finds import cycles.
  3. The data is plain. The input and the output are JSON. The output has absolute positions, so a renderer does no layout work.
  4. The engine does not change. createEngine gives a frozen engine, and extend gives a new one. Two parts of an application can have different strategies without a conflict.
  5. Each layout examines itself. The audit operates after each layout, and its findings are a part of the output.
  6. 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.
ExtensionContractRegister it with
A strategyarrange(children) → offsets, with an option schemacreateEngine({ strategies })
A routerroute(source, target) → pointscreateEngine({ routers })
A rendererRead Layout, or give a Surface to mountnothing

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.

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.

TestWhat it examines
Unit testsEach function of the core, the parser and each renderer
Property testsRandom trees, strategies, modes, sizes, fit policies and options. The audit and a second, independent check must find nothing.
Golden filesThe layout and the SVG of each example
ParitySVG, DOM and Canvas draw the same boxes, to 0.001 pixels
Round tripparse(print(input)) gives the input again
SiteEach 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.