Manual Build

Build

elements man build Read as markdown

A build in Elements means doing everything required to get an application ready for delivery: installing packages, compiling, migrating, testing, and finally releasing the new application to the .elements/release directory. You don't run individual commands to get the app ready. The project server does this automatically as you save files.

This page is the loop itself: the server, what elements start and elements build show you, the -json contract, and how the compiler emits, caches, and transforms. The parts of the loop that are features in their own right have their own pages: packages for the installer, tests for the test runner, migrations for the database, typescript for the checker, and deploy for shipping the result.

The Build Loop

The build loop is the same idea as the JavaScript event loop, applied to the build system. It's a concurrent-safe sequential task loop. File change events, manual build requests, install commands, and test runs are all sequenced through it, so concurrent clients never conflict and no two pieces of work step on each other. A single-file save often builds and hot-reloads in a few milliseconds.

Every file in the project directory is a source. The watcher covers the whole tree apart from node_modules and names that start with a dot, and a change to any file it covers is a build. That includes files nothing imports: a log an app writes into its own directory, or a data file a script drops next to the code. A build that changes no code reloads nothing, so such a write does not disturb the browser, but it still costs a build on every line. Write logs and scratch output under .elements/logs, or anywhere outside the project, which the watcher skips.

Verifying UI

A green build means the code compiles, not that the page looks right. When you change a template or a stylesheet, look at the rendered route. With the app running (elements start, serving http://localhost:4000), screenshot it with the Chromium build already on the machine:

# Windows: chrome.exe or msedge.exe. Linux: google-chrome.
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --headless --disable-gpu --hide-scrollbars \
  --window-size=1440,900 --virtual-time-budget=3000 \
  --screenshot=/tmp/page.png "http://localhost:4000/your-route"

Then open the image. You do not need Playwright, Puppeteer, chromedriver, or any npm install, and you should not add one.

--window-size sizes the window rather than the viewport, and the platform floors it, so it is a desktop tool only: at 390,844 macOS lays the page out at 500 and crops the image to 390. Phone widths, clicking, and reading layout back out of the page are all in elements man browser.

The Project Server

The project server is started automatically when you work in a project. It stays running between commands so multiple clients can connect concurrently and see the same build state. Your terminal, the editor LSP, and any agents running in parallel all talk to the same server. State is cached and incremental, so a second elements build -json against an unchanged project answers in microseconds without redoing work.

In development the server idle-shuts-down after a period of inactivity. You can stop it manually with elements kill from inside a project directory, but you shouldn't need to.

elements start

Builds and runs your program. Pass a file to run a different program.

The program is one long-lived process, hot reloaded on every save and never restarted. elements start exits when the program exits, with its exit code. An app that cannot bind its port prints what to do and exits with 78, so nobody is left watching a live runner over a program that is gone. For a script you edit and rerun, elements start index.ts -repl stays after the program exits and runs it on the next build.

The output is dual-mode:

  • When there are build errors, it opens a terminal view that lets you navigate the errors.
  • When build errors are at zero, you see the green OK state and the app logs flow as normal.

An agent should start the app with elements start & once there is something to look at, unless it is already serving, which means the user started it. The & matters: the app then dies with the agent's session, where nohup strands a server the user cannot stop.

elements build

elements build shows you the build state. It does not start any user programs. Unlike elements start, there's no application running underneath and no app logs in the output, just the build view itself. Green OK means everything is good. Red Error means something needs fixing. The view watches for changes automatically.

elements build                          # build view, watch mode (in a tty)
elements build -json                    # structured json (one-shot, ideal for agents)
elements build app/pages/*.ts           # shell glob expands to matching files

These commands return the latest build state. They don't trigger a build by themselves; the server is already building in response to file changes.

-json

Every command that reports diagnostics takes -json: one shot, structured, and exit 1 when there are errors. The envelope is the same for every command.

{
  "path": "/abs/path/to/project",
  "ok": false,
  "errorCount": 1,
  "warningCount": 0,
  "message": "The build has 1 error.",
  "buildGen": 42,
  "buildRanAt": "2026-08-21T09:15:06.612-07:00",
  "elapsedSeconds": 0.259,
  "diagnostics": [
    {
      "code": 200012,
      "level": "error",
      "message": "Test equality failed. Got: 1, Want: 2.",
      "path": "app/pages/home/test.ts",
      "loc": { "start": 220, "finish": 231, "line": 13, "column": 5 }
    }
  ]
}

Read ok for the outcome; message says the same thing as a sentence. Diagnostic paths are relative to path (files outside the project stay absolute), and loc.line/loc.column are 1-based. Source text is not included: read the file when you want the code around a diagnostic.

The answer always reflects what is on disk. Write a file and ask in the same command: the query waits for your edit to build rather than returning the state from before it. buildGen moves only when a build runs, so comparing it across two calls says whether anything rebuilt; buildRanAt is when that build finished. elapsedSeconds times the request, not the build.

elements test -json adds a tests object: total, passed, failed, and a files array with each file's pass/fail tree. A failing assertion is also a diagnostic, with a breadcrumbs trail naming the test.

The .elements Directory

.elements/ is the project's working directory. Don't modify it by hand. You'll occasionally want to look at logs under .elements/logs/ when debugging the project server.

  • .elements/build/ is the scratch directory for the in-progress build.
  • .elements/release/ is the atomic release directory. In development it's a symlink to the current build. In production it's updated in one shot, when a new build is fully ready, so a live service never sees individual file drift mid-deploy.

Modules and Emit

Elements emits a release graph structured for fast hot reloads and aggressive browser caching. Four properties matter:

  1. All reachable program files ship. Elements walks the import graph from the program entry points and writes every file the program actually reaches, including reachable node modules. Nothing the program can't reach ships in the release.

  2. Build-time resolution equals runtime resolution. Elements rewrites every package import path to point directly at the resolved file, rather than relying on Node.js resolution semantics at runtime. If a path resolved to a particular file at build time, that's the exact file you'll resolve to at runtime. By construction.

  3. CJS at runtime on both targets. Every module is transformed to CJS, on browser and server. Two reasons. First, hot reloading Node.js requires CJS. Second, the browser supports ESM, but not every npm package is written in ESM, and converting ESM to CJS is the safe direction. Converting CJS to ESM safely isn't always possible. CJS at runtime keeps both targets compatible with every package, and keeps hot reload working everywhere.

  4. Browser modules are linked via a small loader. Each HTML page automatically embeds a tiny module loader. The loader lets modules require other modules across different HTTP assets. They don't have to be bundled into one file. Each browser file name carries a content hash, and Elements tells the browser to cache those URLs forever. Cache invalidation is just a URL change.

Why It Matters

  • Hot reloading gives near-instant feedback from code change to running app.
  • Tests run near-instantly.
  • The browser's require system works across disparate hashed-URL assets, so a small code change invalidates a small number of URLs and the rest stay cached at the edge.

Build Caching and Versioning

Elements has a sophisticated graph versioning system. Every source has a version derived from its content plus the versions of everything it depends on. Type checking, compilation, and emit each consult that version before doing any work. If the version hasn't changed since the last run, the cached result is reused. If it has, the affected node and its dependents recompute and nothing else does.

Work happens only when it has to, at every layer: watching, scanning, parsing, binding, type checking, emit, hot reload, and the wire protocols between clients and the project server. A change deep in the graph rebuilds exactly the affected subgraph, and nothing else.

Build Transforms

A small set of compiler transforms run during the build to make code easier to write:

  • RPC. Functions marked @rpc are callable from the browser. The compiler securely rewrites browser call sites into network requests and strips the function body out of the browser bundle.
  • Sync-style async. Participating function calls (sql, tx, Channel, LiveTable, @rpc) are automatically converted to await calls, and the surrounding function declaration becomes async. The conversion propagates up the call stack.
  • Server-only stripping. Server-only code is removed from the browser bundle automatically. You do not mark anything: the compiler knows which declarations are server-only and follows the call graph. Calling one (sql, tx, session.login) from browser-reachable code is a compile error pointing at the call site, so a query or a secret cannot reach the browser by accident.
  • Module-level tree shaking. Unused exports are dropped from the output, file by file. Combined with content-hashed URLs, this keeps the wire payload minimal and cache invalidation precise.

Config and Env

Elements evaluates environment variables at build time. Every env reference resolves against the active .env file as part of the build, so missing variables, typos, and wrong-typed values fail the build before they reach a release. Because the values are resolved at build time, constant folding works perfectly off them. For example:

if (process.env.ENV === "production") {
  // production-only code
} else {
  // dev-only code
}

The compiler folds the comparison against the actual value of ENV and drops the unreachable branch from the output entirely.

JSOC config files follow the same model. Every env(...) call inside config.jsoc resolves at build time, and the compiler emits the result as a plain JSON file. At runtime your app reads that JSON directly. There's no JSOC parsing and no env lookup at runtime.

Full config syntax: elements man config.

Peer-to-Peer Deploys

The build system runs peer-to-peer on deploy. A project server on the deploy machine talks directly to your local project server over the SSH tunnel. The two diff their source graphs, transfer only what changed, and the remote side does a minimal incremental build against its own cache. Deploys frequently complete in under a second.

Performance

A single-file save often builds and hot-reloads in a few milliseconds. Several factors compound:

  • Graph versioning means most work is cached. A second build that touches no sources returns instantly.
  • The TypeScript checker runs in the same process as the build, against the same graph.
  • Hot reload patches the running process or browser at the smallest level required by the change.
  • Peer-to-peer deploy reuses the same versioning and caching, so production releases are as incremental as development builds.

Related

  • packages: the installer. Part of the build, fast, node module compatible, and local packages that are watched like source.
  • tests: the test runner. Concurrent workers, rerun only on change, each test in a transaction, and the gate on every release.
  • typescript: the checker, compiled into the binary, and the Elements languages built on it.
  • migrations: SQL files applied as you save.
  • deploy: the same build system, peer to peer, to a machine you own.