xoong
Menu · Command line
DocsToolsCommand line

Command line

xoong is the command that goes with the framework. It reads your OpenAPI document and writes the table of operations the runtime uses, checks your HTML screens against it, serves the app while you work, and writes static files that run under a strict Content-Security-Policy. It runs in Node, in development and in your build; none of it is sent to the browser.

Install and run

The command is part of @xoong/cli. @xoong/xoong, the one install for the runtime and the admin set, depends on it, so either gives you xoong. Both need Node 22.12 or newer. The command reads no environment variables.

# the runtime, the admin set and the command in one install
npm install @xoong/xoong@next

# or only the command, as a development tool
npm install --save-dev @xoong/cli@next

npx xoong --version
npx xoong --help

npx xoong runs the copy in node_modules. xoong --help (or -h, or xoong help, anywhere on the line) prints every command with every option and does nothing else; xoong --version (-v) prints the version. An option that does not exist is refused with the list of the ones that do, never ignored. The MCP server and the language server are separate programs: xoong-mcp and xoong-lsp.

The commands at a glance

CommandWhat it doesReadsWrites
xoong syncTurns an OpenAPI document into the files the runtime and the editor read.An OpenAPI 3 or Swagger 2 document (a file or a URL), a hints file.xoong/operations.json, api.d.ts, xoong.html-data.json
xoong doctorAudits the document and says what limits xoong, with a fix for each finding.The document, a hints fileNothing
xoong newCreates a project with a first list screen.The documentA project folder
xoong checkChecks screens against your API: operations that do not exist, attributes in the wrong place, missing names.HTML, JavaScript and TypeScript files, operations.jsonNothing
xoong renderPrerenders a page, with its data, into plain HTML.A page, the API it reads from, operations.jsonThe HTML: a file, or standard output
xoong buildWrites static files that run under a strict Content-Security-Policy.Every page under the root and what they referencedist/ and dist/build-report.json
xoong i18n extractLists the translatable texts (xo-t) with their source text.HTML and JavaScript filesA catalog (JSON): a file, or standard output
xoong i18n checkCompares a translation with the source catalog.Two catalog filesNothing
xoong devServes the app while you work: static files, an API proxy, TypeScript.The project folderNothing

A project uses them in this order: sync when the API changes, check while you write screens, dev to look at them, build to ship them.

npx xoong sync openapi.json --hints xoong.hints.json   # after the API changed
npx xoong check index.html src                          # as you write screens
npx xoong dev                                           # http://127.0.0.1:5173
npx xoong build --out dist                              # what you deploy

xoong sync

Turns your OpenAPI document into what the rest of xoong reads. Run it once, and again whenever the API changes. It reads OpenAPI 3.x as JSON or YAML, from a file or an http(s) URL (Swagger 2.0 is converted first), and writes three files into .xoong/:

xoong sync <openapi.json|yaml|url> [--out .xoong] [--hints hints.json]
OptionDefaultWhat it does
--out.xoongThe folder for the generated files (created when it is missing).
--hintsnoneA hints file (xoong.hints.json). It is never looked for on its own: pass it on every run.

It prints one line with the counts. Errors print in full, with the code, the place, the message and the fix, and make the exit code 1. What the document asks for and xoong does not do (XO113) is one warning line per kind. Any other warnings are only counted: xoong doctor lists them.

$ npx xoong sync openapi.json --hints xoong.hints.json
synced 5 operations, 1 entities

$ npx xoong sync openapi.json
synced 1 operations, 0 entities
error XO150 #/components/schemas/Order
  $ref points to something that does not exist
  fix: Define the target or remove the reference
warning XO113: the required header X-Tenant-Id (listOrders) is not sent by xoong. Send it on every call: configure({ headers: () => ({ 'X-Tenant-Id': value }) })

The second run exits with 1 because of the error; the warning alone would not.

xoong doctor

A health check of the OpenAPI document, for when you do not own it. It prints what limits xoong (a missing operationId, undocumented errors, schemas that are entities in all but name), each with a fix, and writes nothing. The codes are the XO1xx ones in Error codes.

xoong doctor <openapi.json|yaml|url> [--hints hints.json] [--all]
OptionDefaultWhat it does
--hintsnoneThe same hints file you give to sync, so the audit sees what sync sees.
--alloffPrint every warning. By default each warning code shows its first five and counts the rest.

Errors always print in full. Each warning code shows its first five findings and counts the rest, so a large API stays readable; --all prints everything. The exit code is 1 when there is an error, 0 otherwise.

$ npx xoong doctor openapi.json --hints xoong.hints.json
error XO150 #/components/schemas/Order
  $ref points to something that does not exist
  fix: Define the target or remove the reference
warn XO113 #/paths (parameters in: header)
  the required header X-Tenant-Id (listOrders) is not sent by xoong
  fix: Send it on every call: configure({ headers: () => ({ 'X-Tenant-Id': value }) })

1 operations, 0 entities, 1 errors, 1 warnings

xoong new

Creates a project folder from an OpenAPI document: a page with a table of the first list the API offers (the first GET operation without path parameters whose response is, or contains, a list of objects), a test for it, the configuration and the generated files. It refuses to run in a folder that already has a package.json.

xoong new <folder> --spec <openapi.json|yaml|url> [--title "My app"] [--link <xoong checkout>]
OptionDefaultWhat it does
--specrequiredThe OpenAPI document the project is made from (a file or a URL). It is copied into the project.
--titlethe title of the OpenAPI documentThe heading of the first screen.
--linknoneA checkout of the xoong repository: the project then uses its packages by path instead of from npm.

It writes package.json (with the scripts dev, sync, check and test), xoong.config.json, xoong.hints.json, index.html, src/main.ts, src/app.css, vitest.config.mjs, test/index.spec.mjs, README.md, a copy of the document as openapi.json, and .xoong/ as sync would.

$ npx xoong new people-app --spec openapi.json
created /home/you/people-app
  first screen: GET /api/people
  next: cd people-app && npm install && npm run dev
  (the @xoong/* packages come from npm; to use a checkout of the xoong repository instead, run again with --link <path to the xoong repository>)

xoong check

Finds the mistakes that would otherwise show up as a button that does nothing, before you open a browser. It reads HTML files and, in .js, .mjs, .ts and .mts files, the HTML inside html`...` templates and the operation named in a client.load, query, mutate or stream call. It compares what it finds with operations.json. Give it files and folders; folders are walked without node_modules, dist, build, coverage and folders that start with a dot.

xoong check <file|folder>... [--operations .xoong/operations.json] [--format text|json] [--exclude glob]...
OptionDefaultWhat it does
--operations.xoong/operations.jsonThe operations table that sync wrote.
--formattexttext prints one finding with its fix per line; json prints the list of findings, for tools. One of text or json.
--excludenoneSkip the files and folders that match (paths relative to the current folder). It adds to check.exclude of xoong.config.json. May be given more than once.

Every finding has a code, a place (file, line, column), a message and a fix. An error makes the exit code 1; a warning is printed and does not. Without .xoong/operations.json (or the file --operations names) it stops and tells you to run xoong sync. --exclude and "check": { "exclude": [] } of xoong.config.json take globs, see Configuration.

<xo-view get="GET /api/people">
  <button xo-click="deletePersn" xo-confirm-kind="danger">Delete</button>
  <xo-table columns="name,email" sortable></xo-table>
</xo-view>
$ npx xoong check screens
screens/people.html:2:11  error XO201  unknown operation "deletePersn" (closest: "deletePerson")
    fix: Use "deletePerson"
screens/people.html:2:34  error XO270  xo-confirm-kind does nothing without xo-confirm on the same element
    fix: Add xo-confirm="Question?" beside it, or remove it

1 files, 2 errors, 0 warnings
$ npx xoong check screens --format json
[
  {
    "level": "error",
    "code": "XO201",
    "file": "screens/people.html",
    "line": 2,
    "column": 11,
    "message": "unknown operation \"deletePersn\" (closest: \"deletePerson\")",
    "fix": "Use \"deletePerson\""
  },
  {
    "level": "error",
    "code": "XO270",
    "file": "screens/people.html",
    "line": 2,
    "column": 34,
    "message": "xo-confirm-kind does nothing without xo-confirm on the same element",
    "fix": "Add xo-confirm=\"Question?\" beside it, or remove it"
  }
]

The rules come in families. Each code is explained on the Error codes page, and the MCP tool explain_error gives the same text to an assistant.

CodesWhat they find
XO201, XO202, XO203, XO204Operations in screens. A name in xo-click, xo-submit, get or op that is not in the synced API, or an operation used the wrong way round: a click that calls a read, a view that reads a write, and in JavaScript load on a write or mutate on a read.
XO210Path parameters. A view whose operation has a {param} in its path and no p-param to fill it.
XO220, XO221, XO222Forms and expressions. xo-submit away from a form, a form outside an xo-view (it never binds), an xo-each, xo-if or xo-body with no path.
XO230Unknown names. An xo-* element or attribute that xoong does not define. Nearly always a typo; the message names the closest one.
XO240, XO241, XO242, XO243Accessibility and images. A button without a name, a click that a keyboard cannot reach, an xo-image without alt, an img whose src is bound inside a view.
XO250, XO251, XO252, XO253, XO254, XO255Tables, pagers, generated forms and selection. A table or pager asking for sorting, paging or filters that the operation does not offer, or sitting outside a view; a generated form with nothing to generate; xo-select and xo-selected used wrongly.
XO260, XO261, XO262, XO263Routes and layouts. A route path that is missing, malformed or repeated, a layout that is not declared or has no outlet, a route parameter that the route does not have.
XO270, XO271, XO272, XO273, XO274, XO275, XO276Attributes in the wrong place. An attribute that does nothing where it stands, a value outside its set or malformed, a name that the operation does not have, an attribute outside the element that gives it meaning.

xoong dev

Serves the app while you work. It needs no bundler and no configuration beyond the proxy:

xoong dev [--root .] [--base /admin] [--port 5173] [--proxy /api=http://localhost:8080]... [--spa]
OptionDefaultWhat it does
--root.The folder to serve. xoong.config.json is read from it.
--basenoneThe URL path the app is served under, the same value as build --base. The import map and root-absolute asset URLs are prefixed, and a request that still carries the prefix is served from the root. X-Forwarded-Prefix overrides it when the header is a safe path.
--port5173The port, a number from 0 to 65535 (0 lets the system pick one). The server listens on 127.0.0.1 only.
--proxynoneForward the requests under a prefix to a backend, written /prefix=http://host:port. It adds to proxy of xoong.config.json; for the same prefix the flag wins. May be given more than once.
--spaoffAnswer every path without a file extension with index.html, for an app with an <xo-router>.
$ npx xoong dev
xoong dev  http://127.0.0.1:5173   (root: /home/you/people-app)
  proxy /api -> http://localhost:8080

It runs until you stop it. It exits with 1 when it cannot start (a port in use, a --port that is not a number from 0 to 65535, an unreadable xoong.config.json). The inline import map is blocked by a strict policy, so never deploy what the dev server sends: use xoong build.

xoong build

Writes static files that run under a strict Content-Security-Policy: no import map, no inline script, no inline style. It starts from every .html page under the root and follows what each one references: scripts, stylesheets, images, the modules the scripts import, CSS url() and @import. Installed packages are found in node_modules and copied to the same URLs the dev server uses. Then:

xoong build [--root .] [--out dist] [--base /admin] [--include path]... [--exclude glob]... [--inline-scripts remove|error] [--no-preload] [--minify] [--bundle] [--budget KB]
OptionDefaultWhat it does
--root.The folder with the pages. Every .html file under it is a page.
--outdistThe output folder. It is never read as input, even when it sits inside the root.
--basenoneThe URL path the app is served under. Root-absolute URLs get this prefix. By default the app is served at /.
--includenoneA file or folder (relative to the root) to publish although no page names it: data fetched from JavaScript, images named in code. May be given more than once.
--excludenoneKeep matching .html files out of the start list (partials, drafts). A file that a page references is still published. May be given more than once.
--inline-scriptsremoveInline JavaScript is removed, because script-src 'self' blocks it. error does the same and makes the build exit 1. JSON and JSON-LD scripts stay. One of remove or error.
--no-preloadoffDo not add <link rel="modulepreload"> to the pages.
--minifyoffMinify the scripts and stylesheets (and JSON) with the esbuild your project has installed; the output stays strict-CSP and the imports are rewritten as usual. Without esbuild installed the build warns and writes the files as they are.
--bundleoffBundle scripts into CSP-clean ESM with the esbuild your project has installed. Static imports go into the entry; a dynamic import() stays its own chunk. Without esbuild the build warns and copies every module. CSS stays separate.
--budgetnonePrint the first load of every page (HTML, stylesheets, scripts and their static imports, gzip, no images or data) and exit 1 when one is above this many KB. Also "build": { "budget": 120 } in xoong.config.json. build-report.json always lists the sizes.

HTML that is not a page (a partial, a fixture, a draft) is kept out with --exclude; a file that a page references is still published. What the build cannot see, such as a fetch() of a data file or an image named in JavaScript, is added with --include.

import { configure } from '@xoong/data';
import '@xoong/data/view';
import operations from '/.xoong/operations.json' with { type: 'json' };
import { configure } from '/@xoong/data/src/index.js';
import '/@xoong/data/src/view.js';
import operations from '/.xoong/operations.json' with { type: 'json' };
import { configure } from '/admin/@xoong/data/src/index.js';
import '/admin/@xoong/data/src/view.js';
import operations from '/admin/.xoong/operations.json' with { type: 'json' };

The strict-CSP model

The build prints the policy its output is tested with, and writes it into dist/build-report.json next to the size of every file (raw and gzipped) and the warnings. It does not send the policy: your host sets the Content-Security-Policy header. The CSP model explains why the runtime needs nothing the policy forbids.

$ npx xoong build --out dist
built 23 files into /home/you/people-app/dist (135.5 KB, 47.6 KB gzip)

runs under:  Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; font-src 'self'; base-uri 'none'; form-action 'self'

The build warns about what that policy would block in your own pages: an inline <script>, a style="" attribute, an inline <style>. It also warns about a bare import it cannot resolve and a referenced file that does not exist. A warning does not fail the build (the exit code stays 0); it is printed and written to the report, so a CI job can fail on it.

warning: /index.html:2: a style="" attribute is blocked by style-src 'self'; use a class
warning: /index.html:3: an inline <script> is blocked by script-src 'self'; move it to a file
{
  "csp": "default-src 'none'; script-src 'self'; ...",
  "files": [
    { "path": "/index.html", "bytes": 1927, "gzip": 551 },
    { "path": "/src/main.js", "bytes": 493, "gzip": 311 }
  ],
  "warnings": []
}

Serve dist as static files. With an <xo-router>, make the host answer every path under the base with the app's index.html. Recipes has the deployment notes.

xoong render

Prerenders a page into plain HTML that already shows its data, for crawlers, for people without JavaScript and for the first paint. It runs the real <xo-view> runtime in Node and fetches the data from --api; in the browser the view draws again from the saved template, so nothing is hydrated. It needs @xoong/ssr, which is not part of @xoong/xoong: npm install @xoong/ssr. Without it the command stops and says so.

xoong render <page.html> --api <url> [--url /users/7] [--operations .xoong/operations.json] [--out file]
OptionDefaultWhat it does
--apirequiredThe base URL of the API the page reads its data from.
--urlnoneRender the route of this URL, for a page with an <xo-router>.
--operations.xoong/operations.jsonThe operations table that sync wrote.
--outnoneWrite the HTML to this file (its folders are created). Without it the HTML goes to standard output.

For a page with an <xo-router>, --url renders the route of that URL, with its <title> and description. Public pages only: the data is as fresh as the last render, and a page that needs the visitor's session is drawn in the browser. build does not call it: run render for each public page, and publish the files.

$ npx xoong render index.html --api http://localhost:8080 --out out/index.html
rendered index.html -> out/index.html

See Prerendering with @xoong/ssr for what the output contains and its limits.

xoong i18n

Two sub-commands keep translations honest. The text written in the HTML is the default of a key (Language), so a catalog for the source language can be made from the pages themselves.

i18n extract

Lists the keys a project uses with their source text: xo-t in HTML (and in HTML written inside JavaScript template strings) and text('key', 'default') calls. The catalog is sorted by key. A key used with two different texts, or a text that is not a valid message, is reported on standard error and makes the exit code 1; the catalog is still written. Folders are walked without node_modules, dist and folders that start with a dot.

xoong i18n extract <html|js files or folders>... [--out catalogs/en.json]
OptionDefaultWhat it does
--outnoneWrite the catalog to this file (its folders are created). Without it the JSON goes to standard output.

i18n check

Compares a translation with the source catalog and prints one line per problem: a key that is missing, a key the source does not use, a message that cannot be read, placeholders that differ from the source's. It ends with the number of problems, or ok, and exits with 1 when there are problems.

xoong i18n check <catalog.json> --source <en.json>
OptionDefaultWhat it does
--sourcerequiredThe source-language catalog, the file that i18n extract wrote. The translation is checked against it.
$ npx xoong i18n extract src --out catalogs/en.json
5 messages -> catalogs/en.json

$ npx xoong i18n check catalogs/vi.json --source catalogs/en.json
catalogs/vi.json: "home.count" uses {count} but the source uses {n}
catalogs/vi.json: missing "home.title"
catalogs/vi.json: missing "search.hint"
catalogs/vi.json: "extra.key" is not used by the source
4 problems

Configuration

xoong.config.json holds what you would otherwise type on every run. All keys are optional, and a flag wins over the file. dev and build read it from their root folder (--root, the current folder by default); check reads it from the current folder. The other commands do not read it. A file that cannot be read stops the command with a message.

KeyUsed byDefaultMeaning
rootdev.The folder to serve. The --root flag wins.
portdev5173The port. The --port flag wins.
proxydevnoneAn object of prefixes and backends, { "/api": "http://localhost:8080" }. It adds to the --proxy flags; for the same prefix the flag wins.
spadevfalseThe same as --spa.
basedev, buildnoneThe URL path the app is served under, the same value as --base. Dev prefixes the import map and root-absolute asset URLs; a request that still carries the prefix is served from the root. X-Forwarded-Prefix overrides it when the header is a safe path.
allowedHostsdev[]Host names the dev server accepts besides localhost, 127.0.0.1 and ::1. Any other Host header gets a 403, which stops a web page from reaching your machine through DNS rebinding. Add a name when you reach the server through a tunnel. "*" turns the check off. X-Forwarded-Host is not trusted in place of Host.
packagesdev, build[]Installed packages, besides xoong's own, whose browser code the app imports. They are served at /@name, listed in the import map and copied by the build.
build.excludebuild[]Globs of HTML files that are not pages. They add to the --exclude flags of build.
build.preloadbuildtruefalse turns off the modulepreload links, like --no-preload.
build.minifybuildfalsetrue minifies scripts, stylesheets and JSON with the esbuild the project has installed, like --minify; without esbuild the build warns and does not minify.
build.bundlebuildfalsetrue bundles scripts into CSP-clean ESM with that esbuild, like --bundle. A dynamic import() stays its own chunk. Without esbuild the build warns and copies every module.
build.inlineScriptsbuildremoveerror makes the build exit 1 when it removes an inline script, like --inline-scripts error. JSON and JSON-LD scripts stay either way.
check.excludecheck[]Globs of files and folders that check skips. They add to its --exclude flags.
{
  "port": 5173,
  "proxy": { "/api": "http://localhost:8080" },
  "packages": ["rainlet"],
  "build": { "exclude": ["screens/**", "*.draft.html"], "preload": true },
  "check": { "exclude": ["vendor/"] }
}

Globs

--exclude and the exclude keys take paths relative to the root (for check, the current folder) with / separators. * is any characters except /, ** any characters including /, ? one character, and a trailing / means the folder and everything in it. A pattern without a / matches that name at any depth (*.draft.html, partials). A folder that matches is skipped with all it holds.

Hints

xoong.hints.json lets the front end say what the OpenAPI document does not, without editing the backend's file. Give it to sync and doctor with --hints; it is not looked for on its own, so the same flag belongs in your sync script. A key that is not one of these is reported (XO171), because it would do nothing.

KeyExampleWhat it does
idNames["id", "gid"]Property names that identify a record, besides id. A schema that has one is an entity: its records are stored once and shared between screens.
schemas{ "SessionResponse": { "entity": "Session", "id": "session_id" } }Which schemas are one entity (entity) and which property is its id (id), for documents you cannot edit.
unwrap"data"Responses shaped { code, message, data } are read as their data.
idempotencyKeys{ "sendMessage": "client_message_id" }A fresh UUID is put into that field of the request body on every call of the operation.
fields{ "Person.email": { "label": "E-mail", "order": 2 } }How a generated form draws a field, keyed by Schema.property: label, hint, placeholder, order, group, hidden, readOnly and widget. See Forms.

In CI

Three steps cover a project: sync so the table is current (or commit .xoong/ and skip it), check to fail on a screen that does not match the API, and build to prove the site builds. Each exits with 1 on a problem, which fails the job. --format json gives other tools the findings.

name: xoong
on: [push, pull_request]
jobs:
  screens:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx xoong sync openapi.json --hints xoong.hints.json
      - run: npx xoong check index.html src
      - run: npx xoong build --out dist
      # a warning of the build does not fail it: this step does
      - run: jq -e '.warnings | length == 0' dist/build-report.json

Exit codes

CodeMeaning
0done
1findings or a failure (also a refused option)
2unknown command or a missing argument

See also: MCP server · Editor support · Error codes · Install · Data and tables · Recipes