Skip to content

Latest commit

 

History

History
134 lines (91 loc) · 12.4 KB

File metadata and controls

134 lines (91 loc) · 12.4 KB

Dev experience

What borgo dev does while you work: what hot-applies and what reloads, how styling is compiled, what happens when a build breaks, and the diagnostics for when the machine — not the code — is the problem. The CLI reference is at the end.

Fast refresh

borgo dev runs both servers and keeps the browser hot over a WebSocket channel (/__borgo/dev). What that means concretely, in decreasing order of magic:

  • Component, page and hook edits apply in place through react-refresh with the full Babel transform (dev builds only). The current route's new chunk is imported, loader props are refetched, and component state survives a body edit. Change a component's hooks — add, remove, reorder, or alter a custom hook's signature — and just that component remounts, Next-style; the rest of the page keeps its state. Editing a custom hook's body hot-applies with dependent state intact.
  • Style edits swap the stylesheet in place — no reload, no state loss.
  • Everything else is a full reload: layouts, error pages, index.html, and any Go change. Layouts because they are imported statically into the shared entry chunk; _404/_500 because the update planner reloads on them by name, there being no safe in-place swap for a page the router may not currently be showing. On a Go edit the API binary is rebuilt while the old one keeps serving, swapped in, and the browser reloads exactly once — after the new API actually answers, so you never land on a dead backend.
  • A broken build does not take the port down. The front server keeps serving the error overlay and the dev channel, and the page reloads itself when the next good save lands.

The mechanics, honestly: an edit restarts the front server so the server module graph is clean. The browser never reloads for that — it reconnects and hot-applies the change from the boot greeting it receives.

Two details you would otherwise discover the hard way. Identical content does not trigger a rebuild: the watcher hashes what it reads, so an editor that touches a .go, .tsx/.ts or .html file without changing it costs nothing. Stylesheets are exempt — an unchanged save of style.scss still recompiles and reswaps. And the Go rebuild is deduplicated on its output, because Windows can deliver a change event while the file is still half-written — a torn read that fails to compile no longer queues a second pointless rebuild.

Nothing outlives the session

Every process borgo starts watches the one that started it and exits with it. Kill the task runner (bun run dev, bunx), kill borgo dev itself with a signal that runs no handlers — the front server and the Go API still go away, releasing their ports and the API binary. Measured on Windows with taskkill /F, no /T: both ports free and .borgo/api.exe released within three seconds.

How: the Go API opens a handle on its parent on Windows and waits on it in 250 ms ticks; on unix it polls getppid every 250 ms, and a parent that exited but was never reaped is read from /proc as the corpse it is. The front server probes its parent once before binding the port — a parent already gone gets no server at all — and polls it every 2 s after that; borgo dev watches its own parent on the same 2 s poll. Uncertainty answers alive: a parent that cannot be opened (another user, an EDR) is a live one out of reach, never a dead one, because the wrong answer there shuts down a healthy session.

One launch shape is not covered, and it is the terminal itself — force-killed as a process, not closed. bun run dev and bunx borgo dev both put a wrapper between your shell and borgo dev, and that wrapper is the parent borgo watches. Windows does not take children down with a parent, so a taskkill /F on the shell leaves the wrapper alive, the wrapper leaves the session alive, and nothing notices: measured, every process and both ports stayed up. Closing the terminal window is different — Windows ends every process attached to that console — but that was not measured here. If you need the session gone, kill the wrapper or borgo dev, not the shell.

This is not a nicety. Before it existed, a force-killed session left an API process holding .borgo/api.exe, and the next borgo dev could not swap the binary in; you had to find the orphan in the task manager. If you see that symptom now, borgo doctor names the process for you.

One gap is known and left open on purpose. On Windows, borgo dev watches its own launcher — the wrapper or shell above it, which borgo did not start — by pid alone, and Windows reuses pids. If that shell dies and the number is handed to another process within one poll, the watch keeps waiting on a stranger. Measured, a freed pid comes back after 740 spawns at the soonest while one poll window sees about 180, so it takes a machine spawning several hundred processes a second to reach. Closing it means reading the launcher's creation time through a native call, and a wrong answer there kills a healthy dev session — the opposite direction, and the more expensive one. Until the margin is shown to shrink in practice, the gap stays. Every process borgo itself starts is unaffected: those sit in a job object that takes them down with their parent.

Styling

The default pipeline compiles the single root style.scss into public/assets/style.css — expanded in dev, compressed in production. Plain CSS is valid SCSS, so you do not need to know Sass to use it.

Tailwind, opt-in

Tailwind v4 rides behind a CLI flag, never autodetection: your CSS pipeline should not change because a package appeared in node_modules.

New projects get it wired by the scaffolder:

bunx create-borgo@latest my-app --tailwind

For an existing app, three steps. Install it:

bun add tailwindcss @tailwindcss/postcss postcss

Create style.css in the app root:

@import "tailwindcss";

And pass the flag in package.json:

{
  "scripts": {
    "dev": "borgo dev --tailwind",
    "build": "borgo build --tailwind",
    "start": "borgo start --tailwind",
    "export": "borgo export --tailwind"
  }
}

export is on that list for a reason, and leaving it off is not cosmetic: borgo export compiles CSS too, so without the flag it looks for the style.scss the Tailwind setup deleted, finds none, and removes public/assets/style.css — the app's only stylesheet, in a gitignored directory, while the pages it just exported still link it. create-borgo --tailwind appends the flag to all four scripts.

With the flag, Tailwind owns the stylesheet: it scans your pages and islands for class names and rewrites public/assets/style.css, minified in production builds. Editing a page hot-applies new utilities through the normal refresh cycle, and editing style.css swaps the stylesheet in place. Without the flag, the SCSS pipeline stays in charge and style.css is ignored.

borgo drives Tailwind through its PostCSS plugin rather than its CLI, and the reason is worth knowing if you are wiring this by hand: the CLI is a process spawned per rebuild, while the plugin stays in the one borgo already runs — a warm rebuild goes from roughly 300 ms to under 20. It also drops the CLI's native file-watcher dependency, whose install script Bun blocks by default. An app still on @tailwindcss/cli keeps working: borgo falls back to it when the plugin is not installed.

One honest edge of staying in-process: within a single long-lived borgo dev session, a utility class you delete keeps its rule in the stylesheet until you restart. The rule is inert — nothing references the class — and borgo build is always a fresh process, so production output is exact.

The error overlay

In dev, a server-side render error becomes a readable overlay page instead of the production 500. The client runtime also surfaces uncaught errors and unhandled rejections in the browser, in a dismissable overlay showing the stack.

If the build is what broke, the fallback server keeps /__borgo/dev alive so the page can heal itself: fix the file, and the browser reloads on its own.

borgo doctor

bunx borgo doctor diagnoses the environment — the class of problem that is never in your code:

Fifteen checks, in three groups. A check that has nothing to say about your app — no package.json, no api/, no playwright dependency, a filesystem that will not report free space — is skipped rather than reported as passing.

Check Group What it catches
bun toolchain not on PATH, older than the required minimum (borgo's own, or a higher floor your package.json declares in engines.bun), or an npm-installed shim shadowing the real one
bun on PATH toolchain a .cmd/.bat/.ps1 shim resolving ahead of a real bun.exe that is also installed — informational
engines.bun toolchain an engines.bun range borgo cannot read as a minimum, so the bun check fell back to borgo's own floor — informational, and only shown when that happens
go toolchain missing, or older than your go.mod requires
node toolchain present or not, and its version — borgo needs none, so this is purely informational
docker toolchain not installed, or installed with an unreachable daemon — informational
port 3000 (front) machine PORT already in use, naming the process and its pid
port 3501 (api) machine API_PORT already in use, same
disk space machine less free space than a build wants
api binary project .borgo/api.exe not opening for writing — a running API, an antivirus, a sync client, a lost permission. A note, not a failure: dev kills its own API before the rename and retries
api types project .borgo/api-types.d.ts missing, or stale against your api/*.go
node_modules project missing or not installed
app deps project borgo-framework, react and react-dom present and consistent, and go.mod carrying the borgogen tool directive
write access project .borgo, public/assets or dist not writable — a read-only checkout, a synced folder, an antivirus
playwright project playwright is a dependency but no browsers are installed

Every failing check prints its one-line fix. The exit code is 1 when a real check fails, so you can gate a script on it; the informational ones print a note in blue and are deliberately kept out of it, because not having Docker on a laptop is not a broken environment. On Windows it reads netstat by row shape rather than by the English word LISTENING, so it still names the process holding a port on a localized system.

CLI reference

Run these through Bun (bun run dev) or directly (bunx borgo dev).

Command What it does
borgo dev both servers, file watching, fast refresh, error overlay
borgo build generates types, builds client assets into public/assets/, compiles the Go binary into dist/
borgo start runs from the build output, supervising both processes
borgo export prerenders exportable pages into dist/site/ — see static export
borgo deploy init <target> writes a deploy config: caddy, nginx, systemd or compose — see deploy
borgo pwa init writes public/manifest.webmanifest and a working public/sw.js — see PWA
borgo doctor environment diagnosis, exit 1 on any failing check that is not informational

Flags:

Flag Applies to Meaning
--tailwind any command (parsed globally); it matters to dev, build, start and export, the four that compile CSS compile CSS with Tailwind instead of SCSS
--front-only start run the front server alone, for split deployments where the API lives elsewhere (point API_URL at it)
--force deploy init, pwa init overwrite an existing generated file
-h, --help, -v, --version print the banner and exit 0

borgo build fails loudly rather than shipping something stale: if type generation fails, the build stops instead of leaving you with yesterday's types. And borgo start notices when public/assets was last written by borgo dev — a development bundle, unminified and uncompressed — and rebuilds it for production instead of serving it silently.

When the problem is not the environment but the framework, FAQ and troubleshooting collects the symptoms with their fixes.