brust 2.0.0-alpha.5
Guide / Dev mode

Dev mode

brust dev rebuilds when you save, swaps the new build in without dropping the old one, and shows build errors above the last good page. A browser overlay explains what each page was made of. None of it exists under brust start.

Run it

~/my-app
$ cd my-app
$ brust dev
$ brust dev routes.tsx --port 3000 --workers 2

The entry is routes.tsx unless you name another file. --port sets the port you open in the browser (0 picks a free one); --workers sets the Bun workers behind it.

Dev mode uses one worker unless you ask for more, and reads the host and port from the same configuration as brust start, with --port on top.

Note

brust dev --target cloudflare is a different loop (it runs wrangler). It is described on the CLI page.

What happens when you save

The process you start is a small proxy. It owns your port and forwards every request to a child: a normal brust start running on a loopback port of its own.

  1. A file changes. After a 150 ms pause the watcher starts one build. A change that arrives while a build runs is kept and builds next: nothing is dropped, and two builds never overlap.
  2. The build runs as its own process into a fresh folder under .brust/dev/, so nothing from the previous build is cached in memory.
  3. A new child starts on that build and the proxy waits for it to print [brust] ready (60 seconds at most).
  4. Only then does the proxy start sending requests to the new child, tell the open pages to reload, and stop the old child. Until that moment the old one kept serving.

So a build that fails, or a new child that does not come up, never takes the site down: the old child stays active. The server is never reloaded in place; every build gets a fresh one.

Every attempt takes a number, so a failed build uses one up: in the output below a syntax error was build 3 and its fix is gen 4.

brust dev, plain output
[brust] dev server on http://127.0.0.1:4873 — [r]eload  [o]pen  [w]arnings  [c]lear  [q]uit
[brust] building http://127.0.0.1:4873 gen 0
[brust] change detected: .
[brust] ready http://127.0.0.1:4873 gen 1 (55ms)
GET / 200 2ms
[brust] change detected: pages/HomePage.tsx
[brust] building http://127.0.0.1:4873 gen 1 (55ms)
[brust] ready http://127.0.0.1:4873 gen 2 (46ms)
[brust] css public/app.css (no reload)
[brust] change detected: pages/HomePage.tsx
[brust] building http://127.0.0.1:4873 gen 2 (46ms)
[brust] build BuildMessage: Expected identifier but found end of file
[brust] error http://127.0.0.1:4873 gen 2 (46ms) — build BuildMessage: Expected identifier but found end of file
[brust] change detected: pages/HomePage.tsx
[brust] building http://127.0.0.1:4873 gen 2 (46ms)
[brust] ready http://127.0.0.1:4873 gen 4 (51ms)

Before the first build is ready, the page you open is a short building… notice (status 503) that reloads on its own.

Try it

The same loop with the lines it prints. Nothing is built here: each button applies the rules above to the numbers on the left.

Live demoDevLoopDemo · native
Servinggen 1
Builds tried1
Stateready
Browserfirst load
[brust] dev server on http://127.0.0.1:1337 — [r]eload  [o]pen  [w]arnings  [c]lear  [q]uit
[brust] ready http://127.0.0.1:1337 gen 1

What reloads and what does not

The proxy keeps a WebSocket open to every page and tells it what to do. There are three outcomes.

You changeWhat the proxy doesThe page
public/*.cssCopies the file into the running build and skips the build.Swaps the stylesheet link. No reload, no state lost.
A stylesheet you import (a global file or a CSS Module)Rebuilds and boots a new child as usual.Swaps each built stylesheet link when the old and new builds have the same stylesheets one to one. If one was added or removed, the page reloads.
Anything else: a page, a component, a loader, brust.tomlRebuilds, boots a new child, swaps it in.Reloads.

A page that was open while the dev server was down reconnects on its own (retrying every 150 ms, backing off to 2 seconds) and reloads if it is behind the build that is now serving.

The watcher ignores node_modules, .git, .wrangler, .brust, dist, editor swap and temporary files, and database files (.sqlite, .db).

Build errors

When a build fails, the pages already open show the error as an overlay: the rule, the file and position when there is one, and the build's own text. The page under it is the last good build, still working.

The overlay is a shadow-root element, so the page's CSS cannot touch it and it cannot touch the page. It goes away by itself on the next successful build. A tab you open while the build is broken shows it at once.

If your own code returns a server error for a page, the proxy shows it as a plain page with the status and the response body, and the reload channel stays connected.

The terminal

In a terminal, brust dev prints request lines that scroll normally and keeps a short status block at the bottom (never more than six lines, and it never takes over the screen). When the output is not a terminal, or CI or NO_COLOR is set, you get plain lines only, as in the log above.

status block, a terminal
● ready  http://127.0.0.1:1337  gen 3 · built in 46ms
  last: GET / → 200  ·  2 static 1 native 0 react
  [r]eload  [o]pen  [w]arnings  [c]lear  [q]uit

The second line is the last page you loaded and how many of its components were static, native and React (taken from that page's trace). While the build is broken it shows the error instead, and a count of the warnings the build printed.

status block, a failing build
✖ error  http://127.0.0.1:1337  gen 2 · built in 46ms
  error: build Expected identifier but found end of file
  warnings: 1 — hook-unsupported components/X.tsx:12:26 hook useReducer is not supported
  [r]eload  [o]pen  [w]arnings  [c]lear  [q]uit
KeyDoes
rReloads every open page.
oOpens the dev URL in your browser.
wPrints the warnings of the last build (or says there are none).
cClears the screen and redraws the status block.
q or Ctrl-CStops the build, the child and the proxy, and removes .brust/dev.

The devtools overlay

Every HTML page served through the dev proxy carries <brust-devtools>, a small indicator in the bottom-right corner that reads like ready · 2s 1n 0r: the build state and how many components of this page are static, native and React. Click it, or press Alt+Shift+B, to open the panel; Esc closes it.

While focus is inside a text field, the shortcut does not open the panel (the field takes the key). Click the indicator instead.

PanelShows
PageThe route and its pattern, the params, status, cache result, loader and render time, the number of worker calls, and a render error if there was one.
ComponentsThe layouts and page that rendered, each with its tier and source file. A React component also shows why the compiler chose React, and its warnings. Hover or focus a row to outline that component on the page.
JobsThe jobs this render called, whether each was a cache hit, and its key and tags. A cache hit page says no jobs ran.
StoreFor each store on the page, the server-rendered seed and the live values of its signals.
IslandsEach React island and whether it has hydrated.
BuildThe build state and generation, the current error, and every warning of the build with its file, rule and remediation.
RequestsThe last 50 page and action requests the dev proxy saw, newest first, with status, time and cache result.

It works from the keyboard: the arrow keys, Home and End move between tabs, and focus returns to where it was when you close it. It lives in a shadow root, so it does not change your page's styles, and your styles do not change it.

The dev trace

The panels read a trace of each page: which route matched, which components rendered, which jobs ran. The Rust core records it only when the request carries a secret that brust dev made up at start-up and gave to its child, and answers it in an x-brust-dev header (base64 of JSON, 32 KiB at most; a bigger one is cut down and flagged truncated). A trace is never cached.

The proxy adds the secret to every request it forwards (replacing any value a browser sent), reads the trace header and removes it. The browser never sees the header: the proxy puts the trace into the page and keeps the last 50.

#__brust_trace
{
  "route": { "id": "r1", "pattern": "/", "chain": ["layout_9e14e5d1", "homePage_76d6b8d1"], "params": {} },
  "status": 200,
  "cache": "-",
  "loader_ms": null,
  "jobs": [],
  "jobs_more": 0,
  "truncated": false,
  "render_ms": 0.4255,
  "render_error": null,
  "bun_calls": 0,
  "rendered": ["layout_9e14e5d1", "homePage_76d6b8d1", "counter_4a7ef039"]
}

Three dev-only URLs on the dev port expose the same data: the #__brust_trace script in each traced page, /_brust/dev/traces (the last 50 requests), and /_brust/dev/meta.

/_brust/dev/meta is dev.json, written only by brust build --dev (which brust dev runs for you). It maps each component id in the trace to its source file, its tier, why the compiler chose React when it did, and the build's diagnostics for it.

/_brust/dev/meta (dev.json)
{
  "version": 1,
  "components": {
    "layout_9e14e5d1":   { "file": "components/Layout.tsx",  "tier": "static", "diagnostics": [] },
    "homePage_76d6b8d1": { "file": "pages/HomePage.tsx",     "tier": "static", "diagnostics": [] },
    "counter_4a7ef039":  { "file": "components/Counter.tsx", "tier": "native", "diagnostics": [] }
  }
}

What is dev only

brust start and brust build without --dev never include any of this: no overlay, no client script, no trace header, no dev.json. A production server has no dev token, so it records no trace.

The dev server answers only to its own names: loopback, the host you configured, and any names in BRUST_DEV_ALLOWED_HOSTS (comma separated, for a tunnel or a local alias). A request with another Host header gets a 421, so a web page on another site cannot reach your app or your source paths through the dev port by pointing its own name at 127.0.0.1. The reload WebSocket refuses a page from another origin.

~
$ curl -s -o /dev/null -w "%{http_code}\n" -H 'Host: evil.example' localhost:4874/
421
Note

Dev mode is for your machine. Do not expose the dev port to a network you do not trust: the build error overlay and the trace carry file paths and source positions.