xoong
Menu · Layers
DocsArchitectureLayers

Architecture overview

xoong is a stack of small packages. Each owns one job and imports only packages below it. The ones that run in the browser import nothing from outside xoong.

The layers

The runtime is seven packages, plus @xoong/xoong, one package that re-exports the common ones. They are plain ES modules with no build step: the browser loads the files you installed.

The runtime packages as layers At the bottom is @xoong/core. On the left, directly above it, is @xoong/data. On the right, directly above core, are @xoong/controls and @xoong/richtext, which import only core. Above data sit @xoong/forms and @xoong/i18n, which import core and data. At the top is @xoong/ui, which imports forms, data and core. Every arrow points downward, from a package to a package it imports: nothing imports upward. @xoong/uitable, form, dialog, toast, router @xoong/formsmodel, binder @xoong/i18nxo-t, t() @xoong/dataclient, cache, store, xo-view @xoong/controlstabs, menu, combobox @xoong/richtextxo-richtext @xoong/coresignals, html templates, XoElement, XoControl An arrow means "imports". Nothing imports upward.

The runtime packages. An arrow means "imports"; a package may also import packages further down (ui imports data and core directly). Nothing imports upward.

PackageOwns
@xoong/coresignal, computed, effect, batch, untracked; the html template tag; XoElement and XoControl; define; the text() hook that packages use to ask for words; the list of xo-* names (syntax.js) that the tools check against.
@xoong/dataThe client (configure, getClient, createClient), the query cache, the entity store, derived, the live-update stream, XoongError, and <xo-view> (the @xoong/data/view entry).
@xoong/formsThe request-schema model, the form model (createForm) and the DOM binder (attach).
@xoong/ui<xo-table>, <xo-form>, <xo-dialog>, <xo-toast>, <xo-router> with the router object, the confirm dialog, and xoong.css.
@xoong/i18nxo-t, t(), configureI18n, the message format, locale-aware formats.
@xoong/controls<xo-tabs>, <xo-menu>, <xo-combobox>, <xo-tags>, <xo-carousel>, the xo-tooltip attribute.
@xoong/richtext<xo-richtext> and the HTML sanitiser it uses.
@xoong/xoongOne name to install: re-exports core, data, data/view and ui, and carries the xoong command.

The dependency rule

A runtime package may import only the packages in its row below, and nothing else: no npm package, no Node built-in. This is checked on every test run by test/architecture.test.mjs, which reads each package.json and each source file.

PackageMay import
corenothing
data, controls, richtextcore
forms, i18ncore, data
uicore, data, forms

The same test also fails when data imports something from core that is not one of its public entries (@xoong/core, /signals, /closest, /syntax), when a runtime package imports tooling (cli, server, mcp, test), and when a pure-logic module touches document or window while it loads. That last rule keeps signals.js and everything in data except view.js importable from plain Node, which is how the tools and the prerenderer reuse them.

How does a lower package use a higher one, then? It does not: the higher package registers itself into a seam the lower one provides. Importing @xoong/forms calls setFormEngine(); importing @xoong/i18n calls setTranslator(), setTextProvider() and setLocaleSource(); the router calls setParamSource() and setUrlSync(); @xoong/ui calls setConfirm(). Without the higher package the lower one still works, with a plain default (English text, the browser's confirm(), the History API directly).

A higher package registers into a seam of a lower one Two participants: @xoong/forms, the higher package, and @xoong/data, the lower one. Above the arrows is the code that makes the first step happen, in a window named main.js. The code window shows main.js: import '@xoong/data/view'; import '@xoong/forms';. Steps: 1. Your app imports @xoong/forms. On load, forms calls setFormEngine({ attach }) from @xoong/data, and data keeps the function. 2. When an xo-view draws a form with xo-submit for an operation that has a request schema, data calls the registered attach with the form, its schema and its record. 3. attach returns an engine, the form model of that form. Data keeps it and detaches it when the view goes away. When the animation plays, a dot travels along each arrow in the order of the steps, while the line of code that causes the step is lit: the registration when forms is imported, then, after a pause at the note, the call back into forms and its answer. @xoong/forms @xoong/data 1 when imported: setFormEngine({ attach }) Later: xo-view draws a form with xo-submit 2 attach(form, { schema, ... }) 3 returns an engine

The higher package registers itself when it is imported; the lower package calls it back later. Nothing imports upward. Dashed: the answer. In motion, a dot travels along each arrow in the order of the steps, while the line of code that causes the step is lit: the registration when forms is imported, then, after a pause at the note, the call back into forms and its answer. Use the slider, or press a number, to look at one step.

// packages/forms/src/index.js: forms import data, so data cannot import forms.
// forms registers itself, and every <form xo-submit> then uses the form model.
import { setFormEngine } from '@xoong/data/view';
import { attach as attachForm } from './form-dom.js';
setFormEngine({ attach: attachForm });

The tooling

Tooling runs in Node (or a test runner) and is allowed to have dependencies: @xoong/cli uses parse5 and yaml, @xoong/server uses amaro to strip TypeScript types, @xoong/ssr uses happy-dom. None of it ships to the browser.

The tooling packages The xoong command (@xoong/cli) is used by the language server (xoong-lsp) and the MCP server (xoong-mcp), which both import it. The command imports @xoong/server for the dev server and the build. It loads @xoong/ssr only when you run xoong render, shown as a dashed arrow. @xoong/ssr imports the runtime packages. @xoong/test, the test helpers, also imports the runtime packages. xoong-mcp@xoong/mcp xoong-lsp@xoong/lsp xoong command@xoong/cli test helpers@xoong/test @xoong/serverhandler, Node adapters @xoong/ssrprerender Runtime packagescore, data, forms, ui, ... xoong render only

The tooling packages. Dashed: loaded only when needed. They run in Node or in a test runner, never in the browser.

PackageOwns
@xoong/cliThe xoong command: sync, doctor, new, check, render, build, dev, i18n extract, i18n check. Also the OpenAPI analysis and the xoong check rule engine.
@xoong/serverOne web-standard handler, (Request) => Promise<Response>: static files, API proxy, import map, TypeScript stripping. @xoong/server/node adds the Node file reader and HTTP server. It imports no other xoong package.
@xoong/ssrrenderPage(): prerender a page with its data, using the real <xo-view> on a happy-dom document.
@xoong/testmockApi() and mount() for screen tests.
@xoong/lsp, @xoong/mcpThe same checks as xoong check, for editors and for AI assistants.

The stream gateway described in older design notes is not built yet. The server package today does static files, proxy, import map and type stripping.

Three contracts

Light DOM. Elements draw into the ordinary DOM, so your CSS, your tests, axe and the browser's own tools see exactly what the person sees. There is no Shadow DOM (decision record 0002).

Plain CSS. xoong ships design tokens as custom properties (--xo-color-*, --xo-radius, ...) and a few class names. You write CSS. See Styling.

OpenAPI. Operation names, URLs, query parameters, entity identity, request schemas and error shapes come from your OpenAPI document. If the document cannot say something, xoong doctor tells you what to add, or a hints file says it for you without editing the backend's document. See Data flow.

What runs when

WhenWhereWhat
Development, once per API changeNodexoong sync reads the OpenAPI document (a file or a URL; YAML, JSON, Swagger 2.0) and writes .xoong/operations.json, api.d.ts and xoong.html-data.json.
DevelopmentNodexoong dev serves the files, proxies the API and strips types; xoong check and the language server lint screens.
ReleaseNodexoong build writes static, strict-CSP output; xoong render prerenders public pages.
AlwaysBrowserAll runtime packages. operations.json is plain JSON that your main.js imports. Nothing in the browser reads OpenAPI.

A screen at runtime: the HTML arrives; the browser upgrades each xo-* element whose package you imported; <xo-view> waits until the page is parsed, then asks the client for its operation; the answer lands in the store; effects write exactly the DOM nodes that read it. There is no hydration step and no tree re-render. The next pages follow each part: data, forms, rendering, routing, build, tooling.

Where to look in the code