# Build 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: ```bash # 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 runs it as a background task its harness tracks, never with a trailing `&`. A `&` job belongs to a throwaway shell, so nothing signals it when the session ends: the server outlives the agent and keeps the port. A tracked task gets SIGTERM. Before starting one, read `.elements/run`. The app writes a file there once it is listening, named for its pid and carrying the url and port. A file whose pid is alive means the app is already serving, so use that url and start nothing. An empty directory means start it. A port already taken belongs to something else, so set `PORT` in `config/env/development.env` to a free one and start again. Everything the program writes to stdout and stderr is mirrored to `.elements/logs/program.log` as well as the terminal. That is where a route that threw at request time shows up, and `elements build -json` will be green while it happens: a build covers compile, migrate and test, never what a request does. The file is readable by anything, so a subagent can follow the app without owning the process that runs it. ## `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. ```json { "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. `started`, `finished`, and `elapsedSeconds` time the request, not the build. `elements test -json` is the same envelope plus a `tests` object: ```json "tests": { "total": 3, "passed": 2, "failed": 1, "notRun": 0, "files": [ { "path": "app/pages/home/test.ts", "status": "fail", "tests": [ { "description": "home", "status": "fail", "children": [ { "description": "renders", "status": "pass" }, { "description": "counts", "status": "fail" } ] } ] } ] } ``` `status` is `pass`, `fail`, or `not-run`, and the counts are leaf tests, so `total` is `passed + failed + notRun`. `notRun` counts tests that did not run, usually because code they depend on has an error; they are not passes. The tree says which tests ran, not why one failed: a failing assertion is a diagnostic in `diagnostics`, 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/logs/program.log` is everything the running program wrote. - `.elements/run/` holds one file per running server, named for its pid, carrying the url and port it is serving on. The app writes its own on the way up and removes it on the way down; a file whose pid is gone is a crash, and readers skip it. - `.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: ```ts 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.