@rangojs/router 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +7 -4
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +1 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/types/cache-types.d.ts +14 -10
  67. package/dist/types/types/handler-context.d.ts +14 -0
  68. package/dist/types/types/loader-types.d.ts +8 -6
  69. package/dist/types/urls/response-types.d.ts +3 -12
  70. package/dist/types/use-loader.d.ts +12 -3
  71. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  72. package/dist/types/vite/rango.d.ts +1 -1
  73. package/dist/vite/index.js +1381 -1346
  74. package/package.json +2 -2
  75. package/skills/api-client/SKILL.md +22 -10
  76. package/skills/breadcrumbs/SKILL.md +65 -22
  77. package/skills/bundle-analysis/SKILL.md +22 -22
  78. package/skills/cache-guide/SKILL.md +207 -97
  79. package/skills/caching/SKILL.md +295 -93
  80. package/skills/catalog.json +16 -16
  81. package/skills/client-urls/SKILL.md +53 -31
  82. package/skills/cloudflare/SKILL.md +72 -20
  83. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  84. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  85. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  86. package/skills/comparison/SKILL.md +5 -0
  87. package/skills/comparison/references/framework-comparison.md +39 -33
  88. package/skills/composability/SKILL.md +38 -29
  89. package/skills/css/SKILL.md +10 -13
  90. package/skills/debug-manifest/SKILL.md +74 -31
  91. package/skills/defer-hydration/SKILL.md +2 -1
  92. package/skills/deployment-caching/SKILL.md +6 -1
  93. package/skills/document-cache/SKILL.md +92 -28
  94. package/skills/fonts/SKILL.md +16 -8
  95. package/skills/handler-use/SKILL.md +53 -17
  96. package/skills/hooks/SKILL.md +28 -20
  97. package/skills/hooks/data.md +23 -3
  98. package/skills/hooks/handle-and-actions.md +31 -16
  99. package/skills/hooks/navigation.md +39 -10
  100. package/skills/hooks/outlets.md +13 -6
  101. package/skills/hooks/state.md +37 -15
  102. package/skills/hooks/urls.md +7 -5
  103. package/skills/host-router/SKILL.md +64 -12
  104. package/skills/i18n/SKILL.md +60 -34
  105. package/skills/intercept/SKILL.md +161 -26
  106. package/skills/layout/SKILL.md +106 -50
  107. package/skills/links/SKILL.md +51 -19
  108. package/skills/loader/SKILL.md +242 -106
  109. package/skills/middleware/SKILL.md +119 -23
  110. package/skills/migrate-nextjs/SKILL.md +100 -45
  111. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  112. package/skills/migrate-react-router/SKILL.md +49 -38
  113. package/skills/migrate-react-router/component-migration.md +25 -6
  114. package/skills/migrate-react-router/data-and-actions.md +18 -5
  115. package/skills/migrate-react-router/route-mapping.md +12 -6
  116. package/skills/mime-routes/SKILL.md +12 -7
  117. package/skills/observability/SKILL.md +28 -14
  118. package/skills/parallel/SKILL.md +75 -39
  119. package/skills/ppr/SKILL.md +345 -150
  120. package/skills/prerender/SKILL.md +189 -100
  121. package/skills/rango/SKILL.md +65 -34
  122. package/skills/react-compiler/SKILL.md +6 -5
  123. package/skills/response-routes/SKILL.md +133 -25
  124. package/skills/route/SKILL.md +165 -60
  125. package/skills/router-setup/SKILL.md +418 -240
  126. package/skills/scripts/SKILL.md +31 -20
  127. package/skills/server-actions/SKILL.md +220 -59
  128. package/skills/shell-manifest/SKILL.md +12 -7
  129. package/skills/streams-and-websockets/SKILL.md +32 -14
  130. package/skills/tailwind/SKILL.md +37 -33
  131. package/skills/testing/SKILL.md +68 -32
  132. package/skills/testing/cache-prerender.md +55 -29
  133. package/skills/testing/client-components.md +59 -6
  134. package/skills/testing/e2e-parity.md +12 -2
  135. package/skills/testing/flight.md +19 -11
  136. package/skills/testing/handles.md +8 -5
  137. package/skills/testing/loader.md +8 -5
  138. package/skills/testing/middleware.md +8 -3
  139. package/skills/testing/predicates.md +120 -0
  140. package/skills/testing/render-handler.md +70 -22
  141. package/skills/testing/response-routes.md +11 -4
  142. package/skills/testing/reverse-and-types.md +20 -14
  143. package/skills/testing/server-actions.md +12 -3
  144. package/skills/testing/server-tree.md +9 -0
  145. package/skills/testing/setup.md +9 -5
  146. package/skills/theme/SKILL.md +106 -29
  147. package/skills/typesafety/SKILL.md +20 -9
  148. package/skills/typesafety/env-and-bindings.md +126 -47
  149. package/skills/typesafety/generated-files-and-cli.md +26 -14
  150. package/skills/typesafety/params-and-search.md +44 -19
  151. package/skills/typesafety/route-types.md +50 -14
  152. package/skills/use-cache/SKILL.md +200 -82
  153. package/skills/vercel/SKILL.md +21 -11
  154. package/skills/view-transitions/SKILL.md +41 -20
  155. package/src/__augment-tests__/augmented.check.ts +26 -1
  156. package/src/browser/index.ts +2 -2
  157. package/src/browser/navigation-bridge.ts +3 -4
  158. package/src/browser/navigation-transaction.ts +22 -2
  159. package/src/browser/partial-update.ts +35 -25
  160. package/src/browser/react/ScrollRestoration.tsx +6 -6
  161. package/src/browser/rsc-router.tsx +1 -1
  162. package/src/browser/scroll-restoration.ts +53 -6
  163. package/src/cache/cache-exec-scope.ts +37 -4
  164. package/src/cache/cache-key-utils.ts +1 -6
  165. package/src/cache/cache-runtime.ts +230 -61
  166. package/src/cache/cache-scope.ts +76 -12
  167. package/src/cache/document-cache.ts +15 -0
  168. package/src/cache/handle-capture.ts +90 -10
  169. package/src/cache/handle-snapshot.ts +132 -28
  170. package/src/cache/segment-codec.ts +26 -7
  171. package/src/cache/tag-invalidation.ts +4 -3
  172. package/src/cache/taint.ts +1 -1
  173. package/src/cache/types.ts +16 -3
  174. package/src/client.rsc.tsx +10 -9
  175. package/src/client.tsx +5 -4
  176. package/src/components/DefaultDocument.tsx +7 -2
  177. package/src/context-var.ts +6 -5
  178. package/src/escape-script.ts +1 -1
  179. package/src/handles/MetaTags.tsx +13 -11
  180. package/src/handles/Scripts.tsx +11 -10
  181. package/src/handles/meta.ts +2 -2
  182. package/src/handles/script.ts +17 -16
  183. package/src/href-client.ts +1 -1
  184. package/src/html.ts +12 -0
  185. package/src/index.rsc.ts +5 -0
  186. package/src/index.ts +1 -0
  187. package/src/loader-store.ts +7 -4
  188. package/src/prerender/build-shell-capture.ts +8 -11
  189. package/src/reverse.ts +44 -4
  190. package/src/route-definition/dsl-helpers.ts +117 -30
  191. package/src/route-definition/helpers-types.ts +33 -9
  192. package/src/route-definition/resolve-handler-use.ts +121 -6
  193. package/src/route-types.ts +0 -3
  194. package/src/router/debug-manifest.ts +10 -2
  195. package/src/router/error-handling.ts +51 -15
  196. package/src/router/handler-context.ts +34 -25
  197. package/src/router/intercept-resolution.ts +125 -39
  198. package/src/router/loader-resolution.ts +56 -11
  199. package/src/router/match-api.ts +12 -17
  200. package/src/router/match-middleware/background-revalidation.ts +161 -96
  201. package/src/router/match-middleware/cache-lookup.ts +93 -10
  202. package/src/router/match-middleware/cache-store.ts +17 -90
  203. package/src/router/middleware-types.ts +3 -5
  204. package/src/router/middleware.ts +52 -9
  205. package/src/router/prerender-match.ts +53 -19
  206. package/src/router/route-snapshot.ts +11 -1
  207. package/src/router/router-context.ts +2 -0
  208. package/src/router/router-interfaces.ts +25 -2
  209. package/src/router/router-options.ts +8 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -12
  211. package/src/router/segment-resolution/loader-cache.ts +205 -45
  212. package/src/router/segment-resolution/loader-mask.ts +12 -22
  213. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  214. package/src/router/segment-resolution/mask-nested.ts +7 -7
  215. package/src/router/segment-resolution/revalidation.ts +34 -6
  216. package/src/router/segment-wrappers.ts +4 -0
  217. package/src/rsc/render-pipeline.ts +3 -0
  218. package/src/rsc/response-route-handler.ts +11 -3
  219. package/src/rsc/rsc-rendering.ts +8 -45
  220. package/src/rsc/shell-capture-constants.ts +13 -0
  221. package/src/rsc/shell-capture.ts +111 -90
  222. package/src/rsc/ssr-setup.ts +14 -8
  223. package/src/rsc/types.ts +11 -0
  224. package/src/server/context.ts +88 -11
  225. package/src/server/cookie-store.ts +5 -4
  226. package/src/server/handle-store.ts +232 -7
  227. package/src/server/request-context.ts +35 -30
  228. package/src/ssr/index.tsx +40 -7
  229. package/src/static-handler.ts +4 -1
  230. package/src/testing/flight-runtime.d.ts +21 -0
  231. package/src/testing/generated-routes.ts +2 -2
  232. package/src/testing/internal/context.ts +1 -1
  233. package/src/testing/internal/flight-client-globals.ts +7 -5
  234. package/src/testing/render-route.tsx +85 -24
  235. package/src/testing/run-loader.ts +5 -1
  236. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  237. package/src/testing/vitest.ts +62 -1
  238. package/src/theme/ThemeScript.tsx +3 -3
  239. package/src/types/cache-types.ts +14 -10
  240. package/src/types/handler-context.ts +14 -0
  241. package/src/types/loader-types.ts +8 -6
  242. package/src/urls/response-types.ts +3 -23
  243. package/src/use-loader.tsx +12 -3
  244. package/src/vite/plugins/use-cache-transform.ts +118 -74
  245. package/src/vite/rango.ts +7 -3
@@ -1,15 +1,22 @@
1
1
  ---
2
2
  name: prerender
3
- description: Pre-render route segments at build time with Prerender and Passthrough live fallback. Use when a page's content is mostly static and shouldn't render on every request, speeding up cold responses, or deciding which routes to prerender vs render live.
3
+ description: Pre-render route segments at build time with Prerender (with getParams), Passthrough live fallback, Static segments, and Skip. Use when a page's content is mostly static and shouldn't render on every request, speeding up cold responses, reading build-only data (files, build env), or deciding which routes to prerender vs render live.
4
4
  argument-hint: [passthrough]
5
5
  ---
6
6
 
7
7
  # Pre-rendering with Prerender
8
8
 
9
- Pre-rendering is **caching at build time**. Same serialization format, same
10
- deserialization path, same segment system. The worker handles every request --
11
- there are NO static .html or .rsc files served from assets. The worker reads
12
- pre-computed Flight payloads instead of executing handler code.
9
+ Pre-rendering is **caching at build time**. `Prerender()` renders a route's
10
+ segments once per known param set during `vite build`, and `Static()` renders a
11
+ single segment once. Same serialization format, same deserialization path, same
12
+ segment system as the runtime `cache()`. The worker handles every request --
13
+ there are NO static .html or .rsc files served from assets. At runtime the
14
+ worker reads the pre-computed Flight payloads instead of executing handler code,
15
+ exactly like a cache hit; middleware and loaders still run on every request.
16
+
17
+ Use it when the page content is known at build time (markdown files, a CMS
18
+ snapshot, a fixed list of products) or needs build-only APIs such as `node:fs`.
19
+ The handler code and its imports are removed from the production bundle.
13
20
 
14
21
  ## Not this skill if…
15
22
 
@@ -17,10 +24,11 @@ pre-computed Flight payloads instead of executing handler code.
17
24
  staying live per request — see `/ppr`.
18
25
  - You want runtime segment caching with TTL/SWR — that is the `cache()` DSL:
19
26
  see `/caching`. Prerender is the same cache filled at build time.
27
+ - You are unsure which cache layer you need — start at `/cache-guide`.
20
28
 
21
29
  ## API: Prerender
22
30
 
23
- ### Static Route (no params)
31
+ ### Route without params
24
32
 
25
33
  ```typescript
26
34
  import { Prerender } from "@rangojs/router";
@@ -96,7 +104,7 @@ functions — no `ctx.build` branching needed.
96
104
  | Unknown params | Handler evicted, no live fallback | Live handler runs at request time |
97
105
  | `ctx.passthrough()` | Throws (not on Passthrough route) | Skips artifact, defers to live handler |
98
106
  | Bundle size | Build handler code + imports removed | Build handler evicted, live handler kept |
99
- | `revalidate()` | Not allowed (handler gone) | Allowed (live handler can re-render) |
107
+ | `revalidate()` | No effect on prerendered segments | Allowed (live handler can re-render) |
100
108
  | `loading()` | Ignored (segments fully resolved) | Works for live fallback renders |
101
109
 
102
110
  ### When to use Passthrough
@@ -138,12 +146,19 @@ interface BuildContext<TParams> {
138
146
  search?: Record<string, unknown>,
139
147
  ): string; // URL generation
140
148
  passthrough(): PrerenderPassthroughResult; // Skip local artifact (Passthrough routes only)
141
- dynamic(): void; // No-op in Prerender/Static handlers; use middleware for PPR shell opt-out
142
149
  env: DefaultEnv; // Available when buildEnv is configured in rango() (throws otherwise)
143
- // NOT available: request, headers, cookies (always throw)
150
+ // ctx.request, ctx.res, ctx.headers throw: there is no request at build time.
151
+ // ctx.waitUntil() is a no-op. ctx.dynamic() exists at runtime but is a no-op
152
+ // here -- opt a URL out of a build shell from middleware instead.
144
153
  }
145
154
  ```
146
155
 
156
+ `getParams` receives a smaller `GetParamsContext` (`build`, `dev`, `env`, `set`,
157
+ `reverse`). Values it stores with `ctx.set()` are copied into every param set's
158
+ handler context and read there with `ctx.get()`. `env` is the `buildEnv` from
159
+ `rango()` — build-time bindings shared by the whole build, not a live request
160
+ env.
161
+
147
162
  Use `createVar<T>()` to share typed data from a Prerender handler to child layouts:
148
163
 
149
164
  ```typescript
@@ -199,6 +214,13 @@ export const BlogPost = cph(getParams, handler);
199
214
  All patterns support whole-file stubbing, expression stubbing, and build-time
200
215
  module tracking. The same applies to `Static`.
201
216
 
217
+ Anything else gets no build-injected id, and the call throws
218
+ `Prerender: missing $$id` when the module loads. Unsupported shapes include
219
+ `export let`/`export var`, and calling `Prerender(...)` inline — for example
220
+ directly inside `path()`. Declare it as an exported `const` and pass the
221
+ binding. (`Passthrough(def, liveHandler)` needs no id and can be written inline
222
+ in `path()`.)
223
+
202
224
  ## Handler Eviction
203
225
 
204
226
  In production builds, `Prerender` exports are replaced with stubs:
@@ -221,8 +243,9 @@ In client and SSR environments, ALL prerender handlers are always stubbed.
221
243
 
222
244
  ## Sub-use Semantics
223
245
 
224
- Everything inside the path's use() callback is part of the B segment and gets
225
- pre-rendered:
246
+ Everything inside the path's use() callback is part of the pre-rendered route
247
+ subtree (the "B segment" in the design docs) and gets pre-rendered. Loaders are
248
+ the exception — they stay live:
226
249
 
227
250
  ```typescript
228
251
  path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
@@ -233,34 +256,69 @@ path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
233
256
  ])
234
257
  ```
235
258
 
236
- If a parallel or child layout uses node APIs, wrap it in `Prerender`
237
- (static, no getParams) so the Vite plugin can stub it:
259
+ Only the route's own `Prerender` handler is evicted from the bundle. If a
260
+ parallel or child layout uses build-only APIs such as `node:fs`, wrap it in
261
+ `Static()` (see "Static segments" below) so the Vite plugin can stub it too.
262
+ `layout()` and `parallel()` accept `Static()` definitions; they do not accept
263
+ `Prerender()` definitions.
238
264
 
239
265
  ```typescript
240
- // sidebar.tsx -- uses node:fs, must be a Prerender
241
- export const BlogSidebar = Prerender(async (ctx) => {
266
+ // sidebar.tsx -- uses node:fs, so it must be stubbed out of the bundle
267
+ import { Static } from "@rangojs/router";
268
+
269
+ export const BlogSidebar = Static(async () => {
242
270
  const files = await fs.readdir("content/blog/");
243
- return <Sidebar posts={files.map(f => basename(f, ".md"))} />;
271
+ return <Sidebar posts={files.map((f) => basename(f, ".md"))} />;
244
272
  });
245
273
 
246
274
  // urls.tsx
247
275
  path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
248
- parallel({ "@sidebar": BlogSidebar }), // stubbable, node:fs excluded
276
+ parallel({ "@sidebar": BlogSidebar }), // stubbable, node:fs excluded
249
277
  ])
250
278
  ```
251
279
 
280
+ ## Static segments
281
+
282
+ `Static(handler, options?)` renders ONE segment once at build time. It has no
283
+ params and produces no URLs; it works on `layout()`, `parallel()`, and `path()`.
284
+ Use it for build-time chrome such as a docs navigation built from files, or for
285
+ a static route with no params.
286
+
287
+ ```typescript
288
+ import { Static } from "@rangojs/router";
289
+
290
+ export const DocsNav = Static(async (ctx) => {
291
+ const docs = await readDocsIndex(); // build-only I/O
292
+ return <Nav docs={docs} />;
293
+ });
294
+
295
+ // urls.tsx
296
+ layout(DocsNav, () => [
297
+ path("/docs/:slug", DocPage, { name: "doc" }),
298
+ ]);
299
+ ```
300
+
301
+ - The handler receives `StaticBuildContext`: `build`, `dev`, `env` (with
302
+ `buildEnv`), `get`/`set`, `use(handle)`, and `reverse`. `params`, `url`,
303
+ `pathname`, and `request` throw.
304
+ - `Static(handler, { passthrough: true })` skips the build render and keeps the
305
+ handler in the bundle, rendering live at request time.
306
+ - `Static()` always renders sequentially (no `concurrency` option) and supports
307
+ `throw new Skip()` (see "Skipping Entries with Skip").
308
+ - The same export rules as `Prerender` apply (`export const X = Static(...)`).
309
+
252
310
  ## Interaction with DSL Items
253
311
 
254
- | DSL item | Behavior with Prerender |
255
- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
256
- | `loader()` | Live at runtime, bundled normally. Use `cache()` for caching. |
257
- | `revalidate()` | Not allowed without Passthrough. Allowed with Passthrough. |
258
- | `cache()` | Orthogonal -- use on parent layouts and loaders. |
259
- | `layout()` | Child layouts inside path are pre-rendered. Parent layouts are live. |
260
- | `parallel()` | Parallel slots inside path are pre-rendered. |
261
- | `middleware()` | Skipped while collecting build-time Flight payloads (no request). For `Prerender` + `ppr`, producer B replays global and route middleware during build-shell capture with `ctx.build === true`; `ctx.dynamic()` skips that shell. Runs at request time for loaders. |
262
- | `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough. |
263
- | `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside main segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when` config conditions are skipped at build time (all intercepts are pre-rendered unconditionally). |
312
+ | DSL item | Behavior with Prerender |
313
+ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
314
+ | `loader()` | Live at runtime, bundled normally. Use `cache()` for caching. |
315
+ | `revalidate()` | Without Passthrough: no effect on the prerendered segments (re-served from the build artifact); loader revalidation still works. With Passthrough: re-renders through the live handler. |
316
+ | `cache()` | Orthogonal -- prerendered segments bypass the runtime cache. Use `cache()` on parent layouts and loaders. |
317
+ | `layout()` | Child layouts inside path are pre-rendered. Parent layouts are live. |
318
+ | `parallel()` | Parallel slots inside path are pre-rendered. |
319
+ | `middleware()` | Skipped while collecting build-time Flight payloads (no request). For `Prerender` + `ppr`, the build-shell step replays global and route middleware with `ctx.build === true`; `ctx.dynamic()` skips that shell. At request time middleware runs normally on every request. |
320
+ | `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough. |
321
+ | `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside main segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when` config conditions are skipped at build time (all intercepts are pre-rendered unconditionally). |
264
322
 
265
323
  When Passthrough revalidation is enabled, remember that revalidation is
266
324
  still partial: opting a child segment into revalidation does not
@@ -269,9 +327,9 @@ implicitly re-run outer prerender-derived handlers/layouts.
269
327
  ## Prerender + PPR Build Shells
270
328
 
271
329
  A `Prerender` page may also declare `ppr` on the path option. The build still
272
- stores the Flight payload first. After that, producer B tries to bake the HTML
273
- shell for each generated URL so the first document request can be an
274
- `x-rango-shell: HIT`.
330
+ stores the Flight payload first. After that, a build-shell step ("producer B"
331
+ in the design docs) tries to bake the HTML shell for each generated URL so the
332
+ first document request can be an `x-rango-shell: HIT`.
275
333
 
276
334
  That shell capture is request-shaped enough to run middleware safely:
277
335
 
@@ -284,6 +342,11 @@ Use `ctx.build` inside middleware to avoid runtime-only side effects during
284
342
  build shell capture, or call `ctx.dynamic()` to leave that route to runtime
285
343
  PPR. Runtime requests still run the normal middleware chain.
286
344
 
345
+ Build shells are keyed by pathname and served only for requests without a query
346
+ string (after `cache.searchParams` filtering). A URL with search params has its
347
+ own shell identity and is captured at runtime. The runtime shell store is read
348
+ first, so a runtime capture supersedes the baked entry once it exists.
349
+
287
350
  ### Freshness of a build shell
288
351
 
289
352
  A build-baked shell is not on a wall clock the way a pure runtime `ppr` shell is.
@@ -305,24 +368,25 @@ redeploy) instead of a shorter `ttl`.
305
368
 
306
369
  `ctx.dynamic()` opts a request off the shell axis ONLY. A `Prerender` route has
307
370
  no live handler to fall back to (it was evicted), so a `dynamic()` request still
308
- serves the build-baked B-segments — fresh loaders in their holes, not a fresh
371
+ serves the build-baked segments — fresh loaders in their holes, not a fresh
309
372
  handler render. There is no "fully dynamic" render for a prerendered route.
310
373
 
311
374
  ## Dev Mode
312
375
 
313
- In dev mode there is no production-style prerender build pass and no handler
314
- stubbing.
315
-
316
- **Node.js dev server** — `Prerender` acts as a normal handler. Routes render
317
- live on every request with full runtime context (`ctx.build === false`).
318
-
319
- **Non-Node runtimes (Cloudflare workerd, Deno workers)** — Handlers that
320
- depend on Node APIs (e.g. `node:fs`) cannot run in-process. The Vite plugin
321
- can intercept these requests and resolve them via the `/__rsc_prerender`
322
- endpoint, which runs `matchForPrerender` in a Node.js temp server. In this
323
- path the handler receives `BuildContext` (`ctx.build === true`) and segments
324
- are resolved identically to production prerendering, then served on-demand.
325
- This only applies when `__PRERENDER_DEV_URL` is set by the plugin.
376
+ In dev there is no build pass and no handler stubbing. Instead, a request to a
377
+ prerendered route is rendered **on demand** through the dev server's
378
+ `/__rsc_prerender` endpoint, which runs the same build-time resolution in
379
+ Node.js (so `node:fs` and `buildEnv` work even when the app itself runs in
380
+ workerd). This applies to both the Node and the Cloudflare presets.
381
+
382
+ - The handler receives `BuildContext` (`ctx.build === true`, `ctx.dev === true`),
383
+ and segments resolve exactly as in the production prerender.
384
+ - Results are memoized until you edit a file in the router's module graph, so
385
+ `getParams()` and handler side effects run once per edit, not once per request.
386
+ The endpoint reports `x-rango-prerender-cache: HIT | MISS`.
387
+ - If the on-demand render has no entry for the URL (unknown param, `Skip`,
388
+ `ctx.passthrough()`, or a render error, which is logged), the request falls
389
+ through to a live render — the Passthrough live handler when there is one.
326
390
 
327
391
  ## Storage Layout
328
392
 
@@ -378,13 +442,28 @@ export const BlogPost = Prerender(
378
442
  },
379
443
  );
380
444
 
381
- // Wrap with Passthrough to serve skipped params live at runtime
382
- export const BlogPost = Passthrough(BlogPostDef, async (ctx) => {
383
- if (ctx.params.slug === "draft") {
384
- throw new Skip("Draft articles are not pre-rendered");
385
- }
386
- return <PostPage slug={ctx.params.slug} />;
387
- });
445
+ ```
446
+
447
+ To serve skipped params live at runtime, give the definition its own name and
448
+ wrap it with `Passthrough()`; the live handler renders them:
449
+
450
+ ```typescript
451
+ import { Prerender, Passthrough, Skip } from "@rangojs/router";
452
+
453
+ export const BlogPostDef = Prerender(
454
+ async () => [{ slug: "published" }, { slug: "draft" }],
455
+ async (ctx) => {
456
+ if (ctx.params.slug === "draft") {
457
+ throw new Skip("Draft articles are not pre-rendered");
458
+ }
459
+ return <PostPage slug={ctx.params.slug} />;
460
+ },
461
+ );
462
+
463
+ // Live handler: runs at request time for "draft" and any unknown slug
464
+ export const BlogPost = Passthrough(BlogPostDef, async (ctx) => (
465
+ <PostPage slug={ctx.params.slug} />
466
+ ));
388
467
  ```
389
468
 
390
469
  Skipped entries are excluded from the build output. With `Passthrough()`,
@@ -416,6 +495,12 @@ rango({ prerender: { onError: "warn" } }); // default is "fail"
416
495
  | `return ctx.passthrough()` | Skip entry, log PASS (Passthrough routes) | Skip entry, log PASS |
417
496
  | `throw new Skip("reason")` | Skip entry, log SKIP, continue | Skip entry, log SKIP, continue |
418
497
  | `throw new Error("reason")` | Log FAIL, stop ALL pre-rendering, fail build | Log WARN, skip the URL, continue |
498
+ | A child component throws | Log FAIL, stop ALL pre-rendering, fail build | Log WARN, skip the URL, continue |
499
+
500
+ The last row covers a handler that returns fine but whose tree holds an async
501
+ server component that throws while the page is rendered at build, for example
502
+ because its fetch fails. That error goes through the same policy instead of being
503
+ baked as an error row, and a `Skip` thrown there skips the URL.
419
504
 
420
505
  With `"warn"` the errored entry is logged and left un-baked (never served as a baked
421
506
  200 error page). `"warn"` is a build-unblock, not a runtime contract: the route falls
@@ -427,7 +512,8 @@ fallback) or `throw new Skip()` (an intentional skip — works in the render fn,
427
512
  only `getParams()`); otherwise prefer the default `"fail"`.
428
513
 
429
514
  Both `Skip` and hard errors propagate to the router's `onError` callback with phase
430
- `"prerender"` or `"static"`.
515
+ `"prerender"` or `"static"`; skipped entries (a `Skip`, or an error under
516
+ `"warn"`) carry `metadata.skipped: true`.
431
517
 
432
518
  ### Build logs
433
519
 
@@ -454,13 +540,9 @@ the build continues.
454
540
 
455
541
  ### Dev mode behavior
456
542
 
457
- **Node.js dev server** — `Skip` behaves like a regular runtime error because
458
- the handler runs live with `ctx.build === false`.
459
-
460
- **Non-Node runtimes using `/__rsc_prerender`** — `Skip` participates in the
461
- on-demand prerender path, so build-style skip logic does run for that request.
462
- The dev prerender endpoint treats it like a prerender miss and the request
463
- falls back according to normal dev/runtime behavior.
543
+ In dev, `Skip` runs inside the on-demand `/__rsc_prerender` render, so
544
+ build-style skip logic runs for that request. The endpoint treats it as a
545
+ prerender miss and the request falls through to a live render (see "Dev Mode").
464
546
 
465
547
  ## Per-Param Passthrough with ctx.passthrough()
466
548
 
@@ -564,23 +646,48 @@ say) is request-time data — delivery follows the loader race model, see
564
646
 
565
647
  ### Server actions work normally
566
648
 
567
- Actions do not re-render the B segment. The pre-rendered handler output stays
568
- frozen. Loaders are live and can be revalidated by actions. With `Passthrough()`
569
- and `revalidate()`, the live handler can re-render.
649
+ Actions run normally; what changes is the re-render after them. Loaders are live
650
+ and are revalidated by actions as usual.
651
+
652
+ - **Plain `Prerender`**: the handler was evicted, so the action re-render serves
653
+ the stored prerender entry again. The prerendered output stays frozen; the
654
+ action's return value reaches the client as usual.
655
+ - **`Passthrough()`**: the action re-render runs the live handler, replacing the
656
+ prerendered output. To keep the frozen tree mounted after actions (for example
657
+ so a client component's form state survives), suppress action
658
+ revalidation on the route while keeping navigation defaults:
659
+ `revalidate((ctx) => (ctx.isAction() ? false : undefined))`.
660
+
661
+ A handler may also embed an inline `"use server"` action that closes over
662
+ build-time values; the captured values are frozen at build and the action body
663
+ runs live when invoked.
570
664
 
571
665
  ### Empty getParams
572
666
 
573
667
  If `getParams` returns an empty array, no Flight payloads are written. No error.
668
+ Every request to the route then behaves like an unknown param.
574
669
 
575
670
  ### Route name is required
576
671
 
577
672
  Routes using `Prerender` must have a `name` in path options.
578
673
  The name is used as the storage key for Flight payloads.
579
674
 
580
- ### No revalidate without Passthrough
675
+ ### revalidate() needs Passthrough
676
+
677
+ Without `Passthrough()` the handler is evicted, so there is nothing to
678
+ re-render: a revalidation re-serves the prerendered segments from the build
679
+ artifact. `revalidate()` on the route's loaders still works. No warning is
680
+ emitted — wrap the route in `Passthrough()` when its segments must re-render.
681
+
682
+ ### Tag invalidation does not reach prerendered segments
581
683
 
582
- Using `revalidate()` without `Passthrough()` produces a build-time warning.
583
- The handler is evicted -- there is nothing to re-render.
684
+ `updateTag()`/`revalidateTag()` evict runtime cache entries. They do not refresh
685
+ a prerendered route's build-time Flight payload, which is served before the tag
686
+ system is consulted, and a `cacheTag()` inside a `"use cache"` function that runs
687
+ during the build has no runtime effect. A redeploy replaces the payload. To make
688
+ a route tag-invalidatable, serve it from the runtime cache instead (`cache()`,
689
+ or a `Passthrough()` live handler). A build-baked **ppr shell** is different: a
690
+ tag it carries does drop it (see "Freshness of a build shell").
584
691
 
585
692
  ### loading() is ignored without Passthrough
586
693
 
@@ -589,11 +696,13 @@ With `Passthrough()`, `loading()` works for live fallback renders.
589
696
 
590
697
  ## Complete Example
591
698
 
699
+ Known guides are pre-rendered; any other slug is rendered live by the
700
+ Passthrough handler.
701
+
592
702
  ```typescript
593
703
  // pages/guides-handler.tsx
594
704
  import { Prerender, Passthrough } from "@rangojs/router";
595
705
  import { Link } from "@rangojs/router/client";
596
- import { href } from "../router.js";
597
706
 
598
707
  const knownGuides: Record<string, string> = {
599
708
  routing: "Routing Guide",
@@ -609,9 +718,9 @@ export const GuidesDetailDef = Prerender<{ slug: string }>(
609
718
  <h1>{title}</h1>
610
719
  <p>Slug: {ctx.params.slug}</p>
611
720
  <nav>
612
- <Link to={href("guides.detail", { slug: "routing" })}>Routing</Link>
721
+ <Link to={ctx.reverse("guides.detail", { slug: "routing" })}>Routing</Link>
613
722
  {" | "}
614
- <Link to={href("guides.detail", { slug: "dynamic-test" })}>Dynamic</Link>
723
+ <Link to={ctx.reverse("guides.detail", { slug: "dynamic-test" })}>Dynamic</Link>
615
724
  </nav>
616
725
  </div>
617
726
  );
@@ -625,9 +734,9 @@ export const GuidesDetail = Passthrough(GuidesDetailDef, async (ctx) => {
625
734
  <h1>{title}</h1>
626
735
  <p>Slug: {ctx.params.slug}</p>
627
736
  <nav>
628
- <Link to={href("guides.detail", { slug: "routing" })}>Routing</Link>
737
+ <Link to={ctx.reverse("guides.detail", { slug: "routing" })}>Routing</Link>
629
738
  {" | "}
630
- <Link to={href("guides.detail", { slug: "dynamic-test" })}>Dynamic</Link>
739
+ <Link to={ctx.reverse("guides.detail", { slug: "dynamic-test" })}>Dynamic</Link>
631
740
  </nav>
632
741
  </div>
633
742
  );
@@ -711,41 +820,21 @@ Loaders run fresh at request time for both variants.
711
820
 
712
821
  Pre-rendered routes set flags on the route trie leaf at build time:
713
822
 
714
- - `pr: true` -- route has pre-rendered B segment data
823
+ - `pr: true` -- route has pre-rendered segment data
715
824
  - `pt: true` -- route wrapped with `Passthrough()` (live handler available)
716
825
 
717
826
  At runtime, the cache-lookup middleware uses these flags:
718
827
 
719
828
  - `pr + hit` -- serve pre-rendered Flight payload
720
829
  - `pr + pt + miss` -- fall through to Passthrough live handler
721
- - `pr + miss` (no pt) -- fall through (handler stubbed, no live render)
722
-
723
- ## Contributor Checklist
724
-
725
- Before changing prerender behavior, run these tests.
726
-
727
- ### Tests to run
728
-
729
- ```bash
730
- # Core prerender e2e (Passthrough, eviction, loaders, sub-use, intercept)
731
- pnpm --filter @rangojs/router exec playwright test prerender
732
-
733
- # Prerender-specific unit test
734
- pnpm --filter @rangojs/router run test:unit -- prerender-passthrough
735
-
736
- # Semantic matrix (prerender rows cover intercept + ctx propagation)
737
- pnpm --filter @rangojs/router exec playwright test semantic-matrix
738
-
739
- # Handler-first (ctx.set/get visibility with prerender handlers)
740
- pnpm --filter @rangojs/router exec playwright test handler-first
741
- ```
830
+ - `pr + miss` (no pt) -- fall through to the stubbed handler, which throws a
831
+ not-found error ("No prerender data found for this route"); there is no live render
742
832
 
743
- ### Dev-only vs build-parity
833
+ ## Related
744
834
 
745
- - Prerender e2e tests run against a real production build by default (the
746
- fixture builds the test app). Dev-mode prerender behavior is tested via
747
- `/__rsc_prerender` endpoint tests and node.js dev-server fallback.
748
- - Log-based assertions (build output lines, debug cache logs) are inherently
749
- dev/build-only and do not need a production counterpart.
750
- - Behavioral assertions (rendered content, loader freshness, Passthrough
751
- fallback, intercept variant selection) must work in the production build.
835
+ - `/cache-guide` — how prerendering compares with the runtime cache layers
836
+ - `/caching` — runtime `cache()` segments and loader caching
837
+ - `/ppr` — the `ppr` path option; combine with `Prerender` for build-time shells
838
+ - `/shell-manifest` — prerendered shell feeding ids to a live loader
839
+ - `/deployment-caching` — why prerender output is not a CDN static file
840
+ - `/loader` — loaders stay live on prerendered routes