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
2 changes: 1 addition & 1 deletion .agents/skills/webjs/references/muscle-memory-gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ Export `GET` / `POST` / etc. as named async functions `(request, { params }) =>

### `middleware.ts` is per-segment and chainable, not one matcher config

The file stays `middleware.ts`, NOT Next 16's renamed `proxy.ts`. WebJs middleware is in-process, chainable, and per-segment (the Remix / Koa model). There is no `export const config = { matcher }` and no single-file restriction. The default export is `async (req, next) => Response`: return a Response to short-circuit, or call `next()` and post-process. Colocate `app/admin/middleware.ts` next to the admin routes and it runs for that subtree only. An optional root `middleware.ts` runs on every request, outermost to innermost.
The file stays `middleware.ts`, NOT Next 16's renamed `proxy.ts`. WebJs middleware is in-process, chainable, and per-segment (the Remix / Koa model). There is no `export const config = { matcher }` and no single-file restriction. The default export is `async (req, next) => Response`: return a Response to short-circuit, or call `next()` and post-process. Colocate `app/admin/middleware.ts` next to the admin routes and it runs for that subtree only. An optional root `middleware.ts` runs on every app request, outermost to innermost. Some requests are answered before it and never reach it, and the RULE is what to remember, not the list: anything the listener shell or the framework's pre-analysis stage answers bypasses root middleware, and everything routed with the app reaches it. That covers WebSocket upgrades bound for a `route.ts` exporting `WS`, the dev SSE stream at `/__webjs/events`, and the framework's own `/__webjs/*` runtime assets and probes; in DEV only it also covers `/public/*` plus the `/sw.js` / `/offline.html` root remaps and `/favicon.ico`, so a stylesheet is never queued behind the dev startup analysis. In production those static files go through root middleware normally. **`webjs.redirects` and `webjs.trailingSlash` are the case intuition gets wrong**: you configure them, but the framework resolves them ahead of middleware, so a 308 from a redirect rule is answered without root middleware running (redirect in the middleware instead when it has to observe those requests). Server actions go the other way: they are routed with the app, so middleware DOES run for an action call, which is what lets you gate actions with auth or rate limiting.

### No `<Link>`, no `next/navigation`, no `next/*` libraries

Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,17 @@ jobs:
# replays the retained dev error frame with its own code (node writes into
# a node:http res, Bun enqueues into a ReadableStream controller), so the
# frame's new url field has to be proven on both (#1047).
- name: webjs dev extra-watch + reload-retry + overlay-scope on Bun
# dev-public-before-warm rides here too: the #1397 hoist serves /public/*
# ahead of ensureReady() in dev, and the two listener shells reach
# handle() differently (the Bun shell may rebuild the Request and
# classifies the body through readBufferedOrStream), so the ordering has
# to be proven on both.
- name: webjs dev extra-watch + reload-retry + overlay-scope + public-before-warm on Bun
run: |
bun test/bun/dev-extra-watch.mjs
bun test/bun/dev-reload-retry.mjs
bun test/bun/dev-overlay-scope.mjs
bun test/bun/dev-public-before-warm.mjs
# The app-source deploy signal (#899) is derived from an fs source walk +
# a node:crypto digest, so it must be byte-identical on the Bun.serve path.
- name: App-source deploy signal on Bun
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ app/ ROUTING ONLY (thin adapters importing from modules/;
<path>/route.js HTTP handler at /<path>
<segment>/middleware.js per-segment middleware
<segment>/loading.js auto Suspense boundary
middleware.js root middleware (every request; .ts/.mts/.mjs too)
middleware.js root middleware (every app request; .ts/.mts/.mjs too)
readiness.js optional /__webjs/ready check (return false/throw = 503)
env.js optional boot-time env validation (schema or validator fn; fails fast)
instrumentation.js optional boot-time hook (register(); wire APM via setOnError, #848)
Expand Down
2 changes: 1 addition & 1 deletion examples/blog/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Global middleware. Runs on every request before webjs routes it.
* Global middleware. Runs on every app request before WebJs routes it.
* Return a Response to short-circuit; call next() to continue.
*
* To add framework sessions:
Expand Down
2 changes: 2 additions & 0 deletions framework-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,6 @@ In development, three error sources push a structured error frame to the open ta

**A render frame is scoped to the URL that produced it (#1047).** A `render` frame carries that url, and the browser half is the single gate deciding whether a frame belongs on the page currently being viewed, so an overlay comes down when the client router navigates away and never goes up for someone else's page. Three consequences worth knowing. A speculative link PREFETCH of a throwing page reports no frame at all, so hovering a link cannot break the page you are looking at (the reported symptom: the frame fans out to every open tab over the shared SSE channel, with no navigation anywhere). A render error in one tab raises nothing in a tab viewing a different page. And a successful render of a url supersedes a retained error for that SAME url, so the replay cannot hand a recovered error to a freshly-connected tab; a good render of an unrelated page deliberately leaves it standing. `ts-strip` and `rebuild` frames carry NO url and are never scoped, because they describe a still-broken build rather than one page, so navigation leaves them alone and only the next successful rebuild clears them. The gate is order-independent: the SSE frame is pushed during the render, before the navigation response is even sent, so a frame for the page being navigated TO is held and rendered once the URL advances. A held frame renders only for the navigation it belongs to, so one that arrived while the tab sat idle is dropped rather than painted on a later visit, when the page may well render fine. An idle-time frame comes from a render this tab did not navigate for (another tab's page, a background fetch of some other url), never from a link prefetch, which reports nothing at all. Mechanism: `renderDevOverlay` + `syncDevOverlayToLocation` + `installDevOverlayNavSync` in `packages/server/src/dev-overlay.js`, wired by the dev reload client to the client router's `webjs:navigate` and `popstate` (a navigation finished) plus `webjs:before-cache` (a navigation STARTED, since the router snapshots the page it is leaving first). That last one also detaches the overlay across the snapshot read and re-attaches it a microtask later, so the cached HTML never carries a copy the module does not own; what it must not do is strip the overlay for good, which would tear a `rebuild` overlay off the page on any link click. `packages/core` is untouched by any of it.

**A reload is coalesced (#1397), so after a burst of edits the page reloads once the edits settle** rather than once per saved file. Each save produces two reload signals (the in-process rebuild frame, then a changed boot id when the browser reconnects to the process `node --watch` restarted), and acting on every one reloads into a server about to be killed again, which is what leaves the page unstyled. The relay holds the reload until the signals stop for 2 seconds, or at most 5 seconds into a sustained burst. So if a reload looks "missing" right after you saved, wait two seconds before looking for a bug: that is the first thing to check. An error overlay is never held, since it is not a reload.

The overlay client uses `textContent` throughout (never `innerHTML`), so the error content cannot inject markup. It is **strictly dev-only**: `reportDevError` early-returns when `!dev`, `/__webjs/reload.js` 404s in prod, and the prod 500 stays terse (only `error.message`, never the stack or a file path), so no source leaks. An embedding host can observe the same frames via the `onDevError` option on `createRequestHandler` / `startServer`. Mechanism: `buildDevErrorFrame` in `packages/server/src/dev-error.js`, `reportDevError` + the SSE push in `packages/server/src/dev.js`, the SSR-catch hook in `packages/server/src/ssr.js`.
Loading