Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .agents/skills/webjs/references/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,21 @@ Three seams pick a runtime-specific implementation, all inside the framework, no
| Listener | `node:http` shell | native `Bun.serve` (faster on the listening path only, not end-to-end, because SSR render dominates a real page) |
| TS strip | built-in `module.stripTypeScriptTypes` | `amaro` (byte-identical, position-preserving) |
| SQLite | built-in `node:sqlite` + `drizzle-orm/node-sqlite` | built-in `bun:sqlite` + `drizzle-orm/bun-sqlite` |
| Hot reload | restart on change (the `webjs dev` supervisor, #1521) | `bun --hot`, plus a restart for a `*.server.*` edit and, with source locations on, any app module edit (#1550) |
| Hot reload | restart on change (the `webjs dev` supervisor, #1521) | in place in one long-lived process (`bun --hot` resets the module registry, #1575); only an `instrumentation.*` / `env.*` edit restarts it |
| WebSocket | the `ws` library | native `Bun.serve` + a bridge adapter |
| 103 Early Hints | yes | no (`Bun.serve` has no informational-response API) |
| Dev edit to a page / layout | full reload (the dev restart replaces the process) | refreshes IN PLACE, no reload (#1398), unless source locations are on |
| Dev edit to a page / layout | full reload (the dev restart replaces the process) | refreshes IN PLACE, no reload (#1398) |
| Reverse-proxy headers | `X-Forwarded-Proto` / `X-Forwarded-Host` honored | same |

**`webjs dev` lets an idle host sleep (#1507).** The live-reload stream sends nothing between edits (no keepalive) and is held open only while a tab showing the app is visible, so a sandbox or preview host that suspends on network quiet can suspend with a backgrounded dev tab open. Showing the tab reconnects, and an edit made meanwhile reloads the page on return. A host that counts an OPEN request as activity (a sandbox that suspends on idle) also needs `"webjs": { "dev": { "reloadIdle": 20 } }` (or `WEBJS_DEV_RELOAD_IDLE=20`): after that many seconds with no edit and no interaction the stream closes, and the next interaction, a tab showing, or an embed-bridge host command (`{ source: 'webjs-embed-host', type: 'resume' }`) reopens it. Off by default. Every reconnect also compares the server's state with the state the page on screen was rendered at, so an edit whose reload signal was lost while the stream was being replaced (a host that closes a held stream when it wakes) still reloads the page.

**The in-place dev refresh (#1398) needs the server process to SURVIVE the edit,** which is the whole of the Node-versus-Bun difference in that row. A page or layout never hydrates, so a freshly rendered page is the complete truth for it and the client router can swap it in without a reload, keeping scroll and (for a page edit) the hydrated state of components outside the changed region. The server classifies the changed file and puts the verdict on the live-reload event, so this needs a process that is still alive to do the classifying.

Bun's `bun --hot` invalidates modules in place without restarting, so it gets the refresh. It cannot reload a module a `Bun.plugin` serves, though (#1550): every `'use server'` `*.server.*` module (the action-seeding plugin) and, when dev source locations are on (`WEBJS_SOURCE_LOCATIONS=1`), every app module (the source-locations plugin). The supervisor restarts the server for an edit to one of those, so on Bun with source locations on every app edit is a full reload, as on Node. On Node the `webjs dev` supervisor (it replaced `node --watch` in #1521) RESTARTS the server process on a change under `app`, `components`, `modules`, `lib`, or `actions`, or to a root `middleware.{ts,js,mts,mjs}`, and a fresh process holds no record of what changed, so those edits are always a full reload. Two Node cases still refresh in place: an edit OUTSIDE that watched set (`db/schema.server.ts`, a `webjs.dev.watch` content dir), and running `npm run dev -- --no-hot`, which keeps the server in one process on either runtime. An in-place refresh loads the rebuilt stylesheets (a `webjs.dev.regenerate` compile runs on that request) BEFORE it swaps the new markup in, and drops the old sheets only after, so an element that gained a utility class never paints without its rule (#1535; a full reload never had the gap, since a head stylesheet is render-blocking). A component edit is a full reload everywhere by design, because `customElements.define` is once-per-tag and swapping fresh markup onto the old class would be worse than the reload.
Bun keeps ONE dev server process for the whole session (#1575), so it gets the refresh. `bun --hot` re-runs the CLI on a change; the first run's server owns the process (its listener, live-reload stream, watchers and analysis caches) and a re-run only tells it the module registry was reset, so nothing is started twice and memory stays flat over hundreds of edits (it used to grow about 25 MB an edit until `bun --hot` stopped reloading). The app's modules load through a `Bun.plugin` that reads them fresh, and its `#` imports resolve through the app's own `imports` map, because Bun keeps the old source of a file that was replaced (an atomic save) and a stale directory listing for a new file next to a `*.server.*` module. So `bun --hot` no longer sees app edits itself, and the dev server asks it for a registry reset after an edit to a module some other module imports, a new module, or a `*.server.*` module, all without a process restart. A page, layout or route handler nothing imports needs no reset: the dev re-import is keyed by the file's content, so an unchanged file reuses its loaded module (no new module instance per request) and an edited one is a new import. `instrumentation.*` and `env.*` run once per process, so an edit to one restarts the server on both runtimes. On Node the `webjs dev` supervisor (it replaced `node --watch` in #1521) RESTARTS the server process on a change under `app`, `components`, `modules`, `lib`, or `actions`, or to a root `middleware.{ts,js,mts,mjs}`, and a fresh process holds no record of what changed, so those edits are always a full reload. Two Node cases still refresh in place: an edit OUTSIDE that watched set (`db/schema.server.ts`, a `webjs.dev.watch` content dir), and running `npm run dev -- --no-hot`, which keeps the server in one process on either runtime. An in-place refresh loads the rebuilt stylesheets (a `webjs.dev.regenerate` compile runs on that request) BEFORE it swaps the new markup in, and drops the old sheets only after, so an element that gained a utility class never paints without its rule (#1535; a full reload never had the gap, since a head stylesheet is render-blocking). A component edit is a full reload everywhere by design, because `customElements.define` is once-per-tag and swapping fresh markup onto the old class would be worse than the reload.

**`webjs dev` and `webjs start` serve the directory they are started in, and refuse anywhere else (#1526).** Run them in the app directory, the one holding `app/`. In a workspace (`apps/web` under a root `package.json` with `workspaces`) that is the member, even when the CLI is hoisted to the root `node_modules`: the hoisted bin and the Bun `--hot` child both keep the directory they were started in. Started where there is no `app/` (the workspace root, or a subdirectory such as `app/` itself), both exit 1 before any `before` step runs, naming the app to start (`cd apps/web && webjs dev`), instead of booting a server that answers 404 for every route.

**`webjs dev` does not stay down (#1521).** The supervisor and the server's own watcher handle every watcher error: a file in a watched dir the dev server cannot read or watch (the 0600 temp file `sed -i` creates when another user runs it, a file removed mid-scan) logs one `file watcher skipped <path> (EACCES)` warning and both keep running, where `node --watch` used to crash and leave the preview dead. Edits that REPLACE a file (`sed -i`, an editor saving through a temp file, an atomic write) are heard every time, however often the same file is replaced (#1529: on Linux under Node the watchers watch each directory, since Node 24's own recursive watcher stopped hearing a file after its first replacement). A server process that crashes is started again on the next file change, or by itself after a backoff of 0.5s growing to 10s for repeated crashes. On Bun the supervisor restarts the server only for an edit `bun --hot` cannot reload in place (a `*.server.*` module, or with source locations on any app module, #1550), and otherwise does only the crash recovery. Stopping `webjs dev` (Ctrl-C, SIGTERM) stops the server child too, and a child whose supervisor was killed outright exits on its own, so nothing is left holding the port.
**`webjs dev` does not stay down (#1521).** The supervisor and the server's own watcher handle every watcher error: a file in a watched dir the dev server cannot read or watch (the 0600 temp file `sed -i` creates when another user runs it, a file removed mid-scan) logs one `file watcher skipped <path> (EACCES)` warning and both keep running, where `node --watch` used to crash and leave the preview dead. Edits that REPLACE a file (`sed -i`, an editor saving through a temp file, an atomic write) are heard every time, however often the same file is replaced (#1529: on Linux under Node the watchers watch each directory, since Node 24's own recursive watcher stopped hearing a file after its first replacement). A server process that crashes is started again on the next file change, or by itself after a backoff of 0.5s growing to 10s for repeated crashes. On Bun the supervisor restarts the server only for an `instrumentation.*` / `env.*` edit (#1575), and otherwise does only the crash recovery. An agent writing files the way an AI editor does (bursts, partial writes, syntax errors then fixes, renames, deletes, atomic writes) is exercised by `scripts/dev-reload-stress.mjs` (`node scripts/dev-reload-stress.mjs <appDir> <url>` against any running app). Stopping `webjs dev` (Ctrl-C, SIGTERM) stops the server child too, and a child whose supervisor was killed outright exits on its own, so nothing is left holding the port.

The 103 Early Hints gap costs only a small first-load latency edge where an edge proxy forwards the 103, never correctness. The `modulepreload` hints still ship in the document head on both runtimes.

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ Rules: **always scaffold via `webjs create`** (never hand-roll). **Default to a
## CLI reference

```sh
webjs dev [--port N] [--no-hot] # dev server with live reload (a restart-on-change supervisor on Node, bun --hot on Bun plus a restart for a module a Bun plugin serves, #1550; a watcher error never stops it and a crashed server is restarted, #1521). --no-hot runs in-process. Runs webjs.dev.before + webjs.dev.parallel (#550). A page or layout edit REFRESHES IN PLACE where the server process survives it (#1398); a component edit always reloads
webjs dev [--port N] [--no-hot] # dev server with live reload (a restart-on-change supervisor on Node; on Bun one long-lived server that `bun --hot` re-runs only to reset the module registry, restarted only for an instrumentation.*/env.* edit, #1575; a watcher error never stops it and a crashed server is restarted, #1521). --no-hot runs in-process. Runs webjs.dev.before + webjs.dev.parallel (#550). A page or layout edit REFRESHES IN PLACE where the server process survives it (#1398); a component edit always reloads
webjs start [--port N] # prod server; source IS the runtime, plain HTTP/1.1 (reverse-proxy for TLS + HTTP/2). Runs webjs.start.before first (#550). dev AND start exit 1 when the cwd has no app/ (a workspace root, an app subdir), naming the app to start, instead of serving 404s (#1526)
webjs test [--server] [--browser] [--watch]
webjs check [--rules] [--json] # correctness validator (report-only, no autofix); --json for an agent loop
Expand Down
12 changes: 7 additions & 5 deletions packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ lib/
`supervise` with `restartOnChange: true` on Node
(restart the child on a change under `WATCH_DIRS` / the
root `WATCH_FILES`), `supervise` with `bun --hot` and a
`restartFor` filter on Bun (`bun --hot` reloads in
place but never a module a `Bun.plugin` serves, so a
`*.server.*` edit, and with source locations on any
app module edit, restarts the child), or `inline`
`restartFor` filter on Bun (a `*.server.*` edit, with
source locations on any app module edit, and the root
boot hooks `instrumentation.*` / `env.*`), narrowed to
`inPlaceRestartFor` (the boot hooks only) once the
child announces over IPC that it reloads in place
(#1575, `inPlaceRestarts` in `dev-reload.js`), or `inline`
(run in-process) for `--no-hot`.
Node no longer runs under `node --watch`, which crashed
on an unhandled watcher error and never came back.
Expand Down Expand Up @@ -222,7 +224,7 @@ README.md npm-facing package readme.

| Command | Implementation |
|---|---|
| `webjs dev` | Runs `startServer({ dev: true })` in a child under WebJs's own reload supervisor (#514, #1521, `lib/dev-supervisor.js` plans, `lib/dev-reload.js` runs): on Node it restarts the child on a change (fresh ESM cache, plus the dev re-import's `?t=` query); on Bun the child runs under `bun --hot` (in-place module invalidation, since Bun keys its cache by path and ignores `?t=`). On both, a watcher error (EACCES / ENOENT on one path) is a warning, never a crash, and a crashed child is restarted on the next change or after a backoff. `--no-hot` opts out and runs the server in-process on either runtime. In the parent (pre-spawn) it runs the configured dev orchestration (#550, `lib/run-tasks.js`): the `webjs.dev.before` steps (one-shot) to completion, then the `webjs.dev.parallel` watchers (e.g. the Tailwind CLI) alongside the server, torn down on exit. So a bare `webjs dev` runs the same before-steps and watchers as `npm run dev`. The scaffold ships `webjs db migrate` as both a `dev.before` and a `start.before` step (#725), so a `db:generate`'d migration is applied on the next boot in dev and prod alike with no manual `db:migrate`; `.env` is loaded before the dev before-steps (same as start), so a Postgres dev migrate sees `DATABASE_URL`. Local binaries (`drizzle-kit`, `tailwindcss`) resolve because the spawn PATH is prepended with the ancestor `node_modules/.bin` dirs, npm-style. **Before dispatching either dev or start**, a directory-relative resolve probe (`checkFrameworkResolves` from `lib/doctor.js`) checks that `@webjsdev/core` resolves from `process.cwd()`; if not (the fresh-git-worktree-without-node_modules trap, #954), it prints the cause + remedy and exits 1 instead of letting a raw `ERR_MODULE_NOT_FOUND` bubble from deep in SSR. A no-op single resolve on the happy path. |
| `webjs dev` | Runs `startServer({ dev: true })` in a child under WebJs's own reload supervisor (#514, #1521, `lib/dev-supervisor.js` plans, `lib/dev-reload.js` runs): on Node it restarts the child on a change (fresh ESM cache, plus the dev re-import's `?t=` query); on Bun the child runs under `bun --hot`, whose re-run of this CLI is handed to the first run's server (`lib/dev-hot-rerun.js`, #1575) so one server lives for the whole session, and only an `instrumentation.*` / `env.*` edit restarts it. On both, a watcher error (EACCES / ENOENT on one path) is a warning, never a crash, and a crashed child is restarted on the next change or after a backoff. `--no-hot` opts out and runs the server in-process on either runtime. In the parent (pre-spawn) it runs the configured dev orchestration (#550, `lib/run-tasks.js`): the `webjs.dev.before` steps (one-shot) to completion, then the `webjs.dev.parallel` watchers (e.g. the Tailwind CLI) alongside the server, torn down on exit. So a bare `webjs dev` runs the same before-steps and watchers as `npm run dev`. The scaffold ships `webjs db migrate` as both a `dev.before` and a `start.before` step (#725), so a `db:generate`'d migration is applied on the next boot in dev and prod alike with no manual `db:migrate`; `.env` is loaded before the dev before-steps (same as start), so a Postgres dev migrate sees `DATABASE_URL`. Local binaries (`drizzle-kit`, `tailwindcss`) resolve because the spawn PATH is prepended with the ancestor `node_modules/.bin` dirs, npm-style. **Before dispatching either dev or start**, a directory-relative resolve probe (`checkFrameworkResolves` from `lib/doctor.js`) checks that `@webjsdev/core` resolves from `process.cwd()`; if not (the fresh-git-worktree-without-node_modules trap, #954), it prints the cause + remedy and exits 1 instead of letting a raw `ERR_MODULE_NOT_FOUND` bubble from deep in SSR. A no-op single resolve on the happy path. |
| `webjs start` | `startServer({ dev: false })`, plain HTTP/1.1 (front a reverse proxy for TLS + HTTP/2). Shares the dev framework-resolve preflight above (#954). Both `dev` and `start` first refuse with exit 1 when the cwd has no `app/` (#1526, `refuseOutsideApp` in the bin, `findServeTarget` / `notAnAppServeMessage` in `lib/check-target.js`, the same predicate as `webjs check`'s #1301 guard): at a workspace root the message names the member apps, inside an app's subdirectory the app root. It runs before the resolve probe and every before-step, and the dev child skips it (its parent checked the same cwd). Without it a launch from a workspace root booted, said it was ready, and answered 404 for every route. Tests: `test/cli/check-target.test.mjs`, `test/e2e/dev-workspace-member.test.mjs` (hoisted bin under Node and Bun). |
| `webjs test [--server\|--browser]` | Runtime-native test runner (#570): server tests run under `node --test` on Node and `bun test` on Bun (`bun --test` is invalid), dispatched on `process.versions.bun`; browser tests run the app's resolved `@web/test-runner` (`wtr`) bin via `process.execPath` (no `npx`). |
| `webjs ci [-f\|--fail-fast] [--only <title>]... [--json] [--signoff]` | Local CI (#1471), the Rails `bin/ci` posture: runs the step list `package.json` declares under `webjs.ci.steps` (`readCiConfig` in `lib/ci-config.js`, `runCi` in `lib/ci-runner.js`). Each step is timed with a `✅` / `❌` result line, the run ends with a failure list and one total line, and the exit is 1 on any failure (130 on interrupt). A group with `parallel: N` runs N steps at once with each step's output captured and replayed whole; a group nested in it takes one slot and runs sequentially. Every child gets `CI=true`, `node_modules/.bin` on PATH, and the step's `env`; `.env` is loaded first like dev / start. The predicate is the CONFIG, not an `app/` directory, so a workspace root (this monorepo) is a legitimate target; no block is exit 1 naming the workspace members that declare one (`--json`: `{ error: { code: 'NO_CI_CONFIG' } }`), a malformed block is `INVALID_CI_CONFIG` with every problem's JSON path, an unknown `--only` title is `UNKNOWN_ONLY`. `--json` puts one document on stdout (`{ ok, seconds, interrupted, steps[] }`, failed steps carrying their captured output) and the human report on stderr. Under `GITHUB_ACTIONS` each step is a `::group::` and a failure an `::error::` annotation, and a `$GITHUB_STEP_SUMMARY` table is appended, so one cloud job running the whole list still names the layer that broke. `--signoff` runs `gh signoff` after a green run (a red run prints the do-not-merge heading). Tests: `test/cli/ci.test.mjs`, `test/bun/ci-runner.mjs` (both runtimes), `test/ci-config/`, `test/ci-runner/` |
Expand Down
Loading
Loading