Build
elements man build Read as markdownA 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:
-
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.
-
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.
-
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.
-
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
@rpcare 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 toawaitcalls, and the surrounding function declaration becomesasync. 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.