@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -11,6 +11,9 @@ deserialization path, same segment system. The worker handles every request --
11
11
  there are NO static .html or .rsc files served from assets. The worker reads
12
12
  pre-computed Flight payloads instead of executing handler code.
13
13
 
14
+ Canonical semantics reference:
15
+ [docs/execution-model.md](../../docs/internal/execution-model.md)
16
+
14
17
  ## API: Prerender
15
18
 
16
19
  ### Static Route (no params)
@@ -69,13 +72,14 @@ export const ProductPage = Prerender(
69
72
 
70
73
  Controls whether the handler stays in the RSC server bundle after build:
71
74
 
72
- | | `passthrough: false` (default) | `passthrough: true` |
73
- | -------------- | --------------------------------------- | --------------------------------------- |
74
- | Known params | Served from pre-rendered Flight payload | Served from pre-rendered Flight payload |
75
- | Unknown params | Handler evicted, no live fallback | Handler runs live at request time |
76
- | Bundle size | Handler code + imports removed | Handler code kept in RSC bundle |
77
- | `revalidate()` | Not allowed (handler gone) | Allowed (handler can re-render) |
78
- | `loading()` | Ignored (segments fully resolved) | Works for live fallback renders |
75
+ | | `passthrough: false` (default) | `passthrough: true` |
76
+ | ------------------- | --------------------------------------- | --------------------------------------- |
77
+ | Known params | Served from pre-rendered Flight payload | Served from pre-rendered Flight payload |
78
+ | Unknown params | Handler evicted, no live fallback | Handler runs live at request time |
79
+ | `ctx.passthrough()` | Throws (not allowed) | Skips artifact, defers to live fallback |
80
+ | Bundle size | Handler code + imports removed | Handler code kept in RSC bundle |
81
+ | `revalidate()` | Not allowed (handler gone) | Allowed (handler can re-render) |
82
+ | `loading()` | Ignored (segments fully resolved) | Works for live fallback renders |
79
83
 
80
84
  ### When to use passthrough
81
85
 
@@ -98,6 +102,7 @@ Handlers receive `BuildContext` at build time, a subset of the runtime `HandlerC
98
102
  ```typescript
99
103
  interface BuildContext<TParams> {
100
104
  params: TParams; // From getParams
105
+ build: true; // Always true at build time
101
106
  use: <T>(handle: Handle<T>) => (data: T) => void; // Push handle data
102
107
  url: URL; // Synthetic URL from pattern + params
103
108
  pathname: string; // Pathname from synthetic URL
@@ -105,6 +110,12 @@ interface BuildContext<TParams> {
105
110
  set<T>(contextVar: ContextVar<T>, value: T): void; // Set typed context variable
106
111
  get(key: string): any; // Read context variable (string key)
107
112
  get<T>(contextVar: ContextVar<T>): T | undefined; // Read typed context variable
113
+ reverse(
114
+ name: string,
115
+ params?: Record<string, string>,
116
+ search?: Record<string, unknown>,
117
+ ): string; // URL generation
118
+ passthrough(): PrerenderPassthroughResult; // Skip local artifact (passthrough routes only)
108
119
  // NOT available: req, headers, cookies, env (throws descriptive errors)
109
120
  }
110
121
  ```
@@ -130,6 +141,16 @@ All items inside the path's use() callback (child layouts, parallels) also recei
130
141
  `BuildContext` during pre-rendering. Loaders are the exception -- they run at
131
142
  request time with full server context.
132
143
 
144
+ This is one reason prerender is a good fit for handler-first composition:
145
+ the handler and its child layouts/parallels participate in the same full
146
+ render pass, so data set with `ctx.set()` is available downstream via
147
+ `ctx.get()`.
148
+
149
+ At runtime, partial action revalidation follows a narrower rule: only
150
+ revalidated segments are recomputed. If a child segment depends on data
151
+ established by an outer handler/layout, that outer segment must also be
152
+ revalidated, or the child must load/guard the data independently.
153
+
133
154
  ## Supported Export Patterns
134
155
 
135
156
  All of the following are equivalent and fully supported by the Vite transform:
@@ -218,11 +239,25 @@ path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
218
239
  | `loading()` | Ignored without passthrough. Works for live fallback with passthrough. |
219
240
  | `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()` conditions are skipped at build time (all intercepts are pre-rendered unconditionally). |
220
241
 
242
+ When passthrough revalidation is enabled, remember that revalidation is
243
+ still partial: opting a child segment into revalidation does not
244
+ implicitly re-run outer prerender-derived handlers/layouts.
245
+
221
246
  ## Dev Mode
222
247
 
223
- In dev mode, `Prerender` is a normal handler. Routes render live
224
- on every request. No stubbing, no build-time pre-rendering. The handler runs
225
- with full runtime context (not BuildContext).
248
+ In dev mode there is no production-style prerender build pass and no handler
249
+ stubbing.
250
+
251
+ **Node.js dev server** — `Prerender` acts as a normal handler. Routes render
252
+ live on every request with full runtime context (`ctx.build === false`).
253
+
254
+ **Non-Node runtimes (Cloudflare workerd, Deno workers)** — Handlers that
255
+ depend on Node APIs (e.g. `node:fs`) cannot run in-process. The Vite plugin
256
+ can intercept these requests and resolve them via the `/__rsc_prerender`
257
+ endpoint, which runs `matchForPrerender` in a Node.js temp server. In this
258
+ path the handler receives `BuildContext` (`ctx.build === true`) and segments
259
+ are resolved identically to production prerendering, then served on-demand.
260
+ This only applies when `__PRERENDER_DEV_URL` is set by the plugin.
226
261
 
227
262
  ## Storage Layout
228
263
 
@@ -288,8 +323,10 @@ export const TocSidebar = Static(() => {
288
323
 
289
324
  ### Error behavior at build time
290
325
 
291
- | Throw type | Effect |
326
+ | Handler outcome | Effect |
292
327
  | --------------------------- | ----------------------------------------------------- |
328
+ | JSX / `null` | Normal prerender entry, log OK |
329
+ | `return ctx.passthrough()` | Skip entry, log PASS, continue (passthrough routes) |
293
330
  | `throw new Skip("reason")` | Skip entry, log SKIP, continue with remaining entries |
294
331
  | `throw new Error("reason")` | Log FAIL, stop ALL pre-rendering, fail the build |
295
332
 
@@ -303,21 +340,108 @@ The build produces per-URL timing logs:
303
340
  ```
304
341
  [rsc-router] Pre-rendering 12 URL(s) (concurrency: 4)...
305
342
  [rsc-router] OK /articles/hello (42ms)
343
+ [rsc-router] PASS /articles/remote-only (5ms) - live fallback
306
344
  [rsc-router] SKIP /articles/draft-post (3ms) - Article is a draft
307
- [rsc-router] FAIL /articles/broken (15ms) - DB connection refused
308
- [rsc-router] Pre-render complete: 10 ok, 1 skipped, 1 failed (1204ms total)
345
+ [rsc-router] Pre-render complete: 11 done, 1 skipped (1204ms total)
309
346
 
310
347
  [rsc-router] Rendering 3 static handler(s)...
311
348
  [rsc-router] OK DocsLayout (28ms)
312
349
  [rsc-router] SKIP TocSidebar (1ms) - Not ready
313
- [rsc-router] Static render complete: 2 ok, 1 skipped (120ms total)
350
+ [rsc-router] Static render complete: 2 done, 1 skipped (120ms total)
314
351
  ```
315
352
 
353
+ A `FAIL` line is logged per-URL when a handler throws a non-Skip error. The
354
+ error is re-thrown immediately, so no summary line is printed — the build
355
+ stops at the first failure.
356
+
316
357
  ### Dev mode behavior
317
358
 
318
- In dev mode, `Skip` is a regular Error. Throwing it in a handler will surface
319
- as a runtime error (no build-time skip logic exists in dev). This matches the
320
- general dev-mode principle where Prerender handlers run live per request.
359
+ **Node.js dev server** `Skip` behaves like a regular runtime error because
360
+ the handler runs live with `ctx.build === false`.
361
+
362
+ **Non-Node runtimes using `/__rsc_prerender`** — `Skip` participates in the
363
+ on-demand prerender path, so build-style skip logic does run for that request.
364
+ The dev prerender endpoint treats it like a prerender miss and the request
365
+ falls back according to normal dev/runtime behavior.
366
+
367
+ ## Per-Param Passthrough with ctx.passthrough()
368
+
369
+ On `{ passthrough: true }` routes, a handler can return `ctx.passthrough()`
370
+ to skip writing a local prerender artifact for a specific param set. At
371
+ runtime, the missing entry falls through to the live handler.
372
+
373
+ ```typescript
374
+ export const BlogPost = Prerender(
375
+ async () => [{ slug: "a" }, { slug: "b" }, { slug: "c" }],
376
+ async (ctx) => {
377
+ const post = await getPost(ctx.params.slug);
378
+ if (!post) return ctx.passthrough();
379
+ return <article>{post.content}</article>;
380
+ },
381
+ { passthrough: true },
382
+ );
383
+ ```
384
+
385
+ ### Semantics
386
+
387
+ - JSX or `null` from the handler produces a normal prerender entry.
388
+ - `ctx.passthrough()` returns a sentinel that signals "no local artifact".
389
+ The build skips the manifest entry for that param set.
390
+ - `ctx.passthrough()` on a non-passthrough route throws an invariant error.
391
+ - `ctx.passthrough()` at runtime (`ctx.build === false`) also throws.
392
+ It is a build-time-only control flow.
393
+ - `getParams()` still enumerates the param set; the handler decides per-param
394
+ whether to produce an artifact or defer to runtime.
395
+
396
+ ### Difference from Skip
397
+
398
+ | Mechanism | Effect on build | Runtime behavior |
399
+ | ------------------- | ---------------------- | ---------------------------------------------------- |
400
+ | `throw new Skip()` | Skips entry, logs SKIP | No artifact, no live fallback unless passthrough |
401
+ | `ctx.passthrough()` | Skips entry, logs PASS | Always defers to live handler (requires passthrough) |
402
+
403
+ Use `ctx.passthrough()` when you want the handler to run live at request time
404
+ for specific params. Use `Skip` when you want to exclude params entirely.
405
+
406
+ ### Use case: Remote storage
407
+
408
+ `ctx.passthrough()` enables a pattern where build-time data is stored in a
409
+ remote KV store instead of the local prerender manifest. The handler
410
+ pre-computes data during `getParams`, pushes it to KV, then calls
411
+ `ctx.passthrough()` so the local build skips the artifact. At runtime,
412
+ the live handler reads from KV:
413
+
414
+ ```typescript
415
+ export const Product = Prerender(
416
+ async () => {
417
+ const products = await db.getFeaturedProducts();
418
+ // Pre-compute and store in remote KV during build
419
+ for (const p of products) {
420
+ await kv.put(`product:${p.id}`, await renderProduct(p));
421
+ }
422
+ return products.map(p => ({ id: p.id }));
423
+ },
424
+ async (ctx) => {
425
+ // At build time: skip local artifact, data is in KV
426
+ if (ctx.build) return ctx.passthrough();
427
+ // At runtime: read from KV
428
+ const cached = await kv.get(`product:${ctx.params.id}`);
429
+ if (cached) return cached;
430
+ return <Product data={await db.getProduct(ctx.params.id)} />;
431
+ },
432
+ { passthrough: true },
433
+ );
434
+ ```
435
+
436
+ ### Build logs
437
+
438
+ Passthrough entries are logged distinctly:
439
+
440
+ ```
441
+ [rsc-router] OK /blog/a (42ms)
442
+ [rsc-router] PASS /blog/b (3ms) - live fallback
443
+ [rsc-router] OK /blog/c (38ms)
444
+ ```
321
445
 
322
446
  ## Edge Cases and Constraints
323
447
 
@@ -397,10 +521,10 @@ export const guidesPatterns = urls(({ path }) => [
397
521
  ]);
398
522
 
399
523
  // urls.tsx
400
- import { urls, include } from "@rangojs/router";
524
+ import { urls } from "@rangojs/router";
401
525
  import { guidesPatterns } from "./pages/guides.js";
402
526
 
403
- export const urlpatterns = urls(({ path }) => [
527
+ export const urlpatterns = urls(({ path, include }) => [
404
528
  path("/", HomePage, { name: "home" }),
405
529
  include("/guides", guidesPatterns, { name: "guides" }),
406
530
  ]);
@@ -471,3 +595,49 @@ At runtime, the cache-lookup middleware uses these flags:
471
595
  - `pr + hit` -- serve pre-rendered Flight payload
472
596
  - `pr + pt + miss` -- fall through to live handler (handler kept in bundle)
473
597
  - `pr + miss` (no pt) -- fall through (handler stubbed, no live render)
598
+
599
+ ## Contributor Checklist
600
+
601
+ Before changing prerender behavior, read these docs and run these tests.
602
+
603
+ ### Docs to re-read
604
+
605
+ - [Prerender API design](../../docs/prerender-api-design.md) -- canonical
606
+ architecture: build-time flow, runtime flow, storage, passthrough, intercept
607
+ - [Execution model](../../docs/internal/execution-model.md) -- handler-first
608
+ ordering, middleware scope, context visibility rules
609
+ - [Semantic change checklist](../../docs/internal/semantic-change-checklist.md)
610
+ -- gate for any change to execution semantics
611
+
612
+ ### Tests to run
613
+
614
+ ```bash
615
+ # Core prerender e2e (passthrough, eviction, loaders, sub-use, intercept)
616
+ pnpm --filter @rangojs/router exec playwright test prerender
617
+
618
+ # Prerender-specific unit test
619
+ pnpm --filter @rangojs/router run test:unit -- prerender-passthrough
620
+
621
+ # Semantic matrix (prerender rows cover intercept + ctx propagation)
622
+ pnpm --filter @rangojs/router exec playwright test semantic-matrix
623
+
624
+ # Handler-first (ctx.set/get visibility with prerender handlers)
625
+ pnpm --filter @rangojs/router exec playwright test handler-first
626
+ ```
627
+
628
+ ### Dev-only vs build-parity
629
+
630
+ - Prerender e2e tests run against a real production build by default (the
631
+ fixture builds the test app). Dev-mode prerender behavior is tested via
632
+ `/__rsc_prerender` endpoint tests and node.js dev-server fallback.
633
+ - Log-based assertions (build output lines, debug cache logs) are inherently
634
+ dev/build-only and do not need a production counterpart.
635
+ - Behavioral assertions (rendered content, loader freshness, passthrough
636
+ fallback, intercept variant selection) must work in the production build.
637
+
638
+ ## Maintenance References
639
+
640
+ - [Stability next steps plan](../../docs/internal/stability-next-steps-plan.md)
641
+ -- completed parity and cleanup pass (reference for decisions made)
642
+ - [Test quality baseline](../../docs/internal/test-quality-baseline.md) --
643
+ measured test inventory, sleep debt, production coverage gaps
@@ -32,7 +32,6 @@ Django-inspired RSC router with composable URL patterns, type-safe href, and ser
32
32
  | `/response-routes` | JSON/text/HTML/XML/stream endpoints with `path.json()`, `path.text()` |
33
33
  | `/mime-routes` | Content negotiation — same URL, different response types via Accept header |
34
34
  | `/fonts` | Load web fonts with preload hints |
35
- | `/testing` | Unit test route trees with `buildRouteTree()` |
36
35
 
37
36
  ## Quick Start
38
37
 
@@ -51,7 +50,7 @@ export const urlpatterns = urls(({ path, layout }) => [
51
50
  import { createRouter } from "@rangojs/router";
52
51
  import { urlpatterns } from "./urls";
53
52
 
54
- export default createRouter({ document: Document }).urls(urlpatterns);
53
+ export default createRouter({ document: Document }).routes(urlpatterns);
55
54
  ```
56
55
 
57
56
  Use `/typesafety` for type-safe href and environment setup.
@@ -94,7 +94,7 @@ interface ResponseHandlerContext<TParams, TEnv> {
94
94
  reverse: (name: string, params?: Record<string, string>) => string;
95
95
  get: GetVariableFn; // Read middleware variables
96
96
  header: (name: string, value: string) => void;
97
- setCookie: (name: string, value: string, options?: CookieOptions) => void;
97
+ // Use cookies().set(name, value, opts) for cookie mutations (standalone API)
98
98
  }
99
99
  ```
100
100
 
@@ -108,14 +108,14 @@ path.md(
108
108
  "/docs/:slug.md",
109
109
  (ctx) => {
110
110
  ctx.header("Cache-Control", "public, max-age=3600");
111
- ctx.setCookie("last-doc", ctx.params.slug, { path: "/" });
111
+ cookies().set("last-doc", ctx.params.slug, { path: "/" });
112
112
  return `# ${ctx.params.slug}\n\nContent here.`;
113
113
  },
114
114
  { name: "docs" },
115
115
  );
116
116
  ```
117
117
 
118
- Headers and cookies set via `ctx.header()` / `ctx.setCookie()` are merged into the
118
+ Headers set via `ctx.header()` and cookies set via `cookies().set()` are merged into the
119
119
  auto-wrapped Response. If the handler returns a `Response` directly, these are ignored
120
120
  (use the Response headers instead).
121
121
 
@@ -103,8 +103,8 @@ export const SearchPage: Handler<"search"> = (ctx) => {
103
103
  ```
104
104
 
105
105
  Supported types: `"string"`, `"number"`, `"boolean"`, with `?` suffix for optional.
106
- Required params default to zero values when missing (`""`, `0`, `false`).
107
- Optional params are omitted from the result when not in the query string.
106
+ Missing params are `undefined` regardless of required/optional. The required/optional
107
+ distinction is a consumer-facing contract (for `href()` and `reverse()` autocomplete).
108
108
 
109
109
  Use `RouteSearchParams<"name">` and `RouteParams<"name">` to extract types for props:
110
110
 
@@ -181,6 +181,47 @@ String keys still work (`ctx.set("key", value)` / `ctx.get("key")`), but
181
181
  Only route handlers and middleware can call `ctx.set()`. Layouts, parallels,
182
182
  and intercepts can only read via `ctx.get()`.
183
183
 
184
+ ### Revalidation Contracts for Handler Data
185
+
186
+ Handler-first guarantees apply within a single full render pass. For partial
187
+ action revalidation, define named revalidation contracts and reuse them on both
188
+ the producer route and the consumer child segments.
189
+
190
+ ```typescript
191
+ // revalidation-contracts.ts
192
+ export const revalidateCheckoutData = ({ actionId }) =>
193
+ actionId?.includes("src/actions/checkout.ts#") ?? false;
194
+
195
+ path("/checkout", CheckoutPage, { name: "checkout" }, () => [
196
+ revalidate(revalidateCheckoutData), // producer (route handler) reruns
197
+ layout(CheckoutLayout, () => [
198
+ revalidate(revalidateCheckoutData), // consumer reruns
199
+ parallel({ "@summary": CheckoutSummary }, () => [
200
+ revalidate(revalidateCheckoutData),
201
+ ]),
202
+ ]),
203
+ ]);
204
+ ```
205
+
206
+ If children depend on multiple upstream domains, compose multiple contracts on
207
+ the same segment (`revalidateAuthData`, `revalidateCheckoutData`, and so on).
208
+
209
+ For cleaner route trees, expose contract helpers and spread them:
210
+
211
+ ```typescript
212
+ import { revalidate } from "@rangojs/router";
213
+
214
+ export const revalidateCheckout = () => [revalidate(revalidateCheckoutData)];
215
+
216
+ path("/checkout", CheckoutPage, { name: "checkout" }, () => [
217
+ revalidateCheckout(),
218
+ layout(CheckoutLayout, () => [revalidateCheckout()]),
219
+ ]);
220
+ ```
221
+
222
+ For scope/revalidation guarantees and non-guarantees, see:
223
+ [docs/execution-model.md](../../docs/internal/execution-model.md)
224
+
184
225
  ## Redirects
185
226
 
186
227
  ### Basic redirect
@@ -242,7 +283,7 @@ Attach location state to any server response (not just redirects):
242
283
 
243
284
  ```typescript
244
285
  path("/dashboard", (ctx) => {
245
- ctx.setLocationState([ServerInfo({ data: "welcome" })]);
286
+ ctx.setLocationState(ServerInfo({ data: "welcome" }));
246
287
  return <Dashboard />;
247
288
  }, { name: "dashboard" })
248
289
  ```
@@ -78,7 +78,7 @@ interface RSCRouterOptions<TEnv> {
78
78
  // Document component wrapping entire app
79
79
  document?: ComponentType<{ children: ReactNode }>;
80
80
 
81
- // Enable performance metrics
81
+ // Enable per-request performance timeline (console waterfall + Server-Timing header)
82
82
  debugPerformance?: boolean;
83
83
 
84
84
  // Default error boundary
@@ -99,6 +99,12 @@ interface RSCRouterOptions<TEnv> {
99
99
  // Theme configuration
100
100
  theme?: ThemeConfig | true;
101
101
 
102
+ // SSR options (streaming policy)
103
+ ssr?: SSROptions<TEnv>;
104
+
105
+ // Telemetry sink for structured lifecycle events
106
+ telemetry?: TelemetrySink;
107
+
102
108
  // Connection warmup (default: true)
103
109
  warmup?: boolean;
104
110
 
@@ -291,10 +297,10 @@ export const shopPatterns = urls(({ path, layout }) => [
291
297
  ]);
292
298
 
293
299
  // src/urls.tsx
294
- import { urls, include } from "@rangojs/router";
300
+ import { urls } from "@rangojs/router";
295
301
  import { shopPatterns } from "./urls/shop";
296
302
 
297
- export const urlpatterns = urls(({ path }) => [
303
+ export const urlpatterns = urls(({ path, include }) => [
298
304
  path("/", HomePage, { name: "home" }),
299
305
  include("/shop", shopPatterns, { name: "shop" }),
300
306
  ]);
@@ -355,3 +361,74 @@ const router = createRouter({
355
361
 
356
362
  The warmup request is relative to the current page path, so it works correctly
357
363
  with subpath deployments (reverse proxy, base path).
364
+
365
+ ## Telemetry
366
+
367
+ The router emits structured lifecycle events through a pluggable telemetry sink.
368
+ Zero overhead when not configured.
369
+
370
+ ```typescript
371
+ // Console sink for development
372
+ import { createRouter, createConsoleSink } from "@rangojs/router";
373
+
374
+ const router = createRouter({
375
+ document: Document,
376
+ urls: urlpatterns,
377
+ telemetry: createConsoleSink(),
378
+ });
379
+ ```
380
+
381
+ ```typescript
382
+ // OpenTelemetry for production
383
+ import { createRouter, createOTelSink } from "@rangojs/router";
384
+ import { trace } from "@opentelemetry/api";
385
+
386
+ const router = createRouter({
387
+ document: Document,
388
+ urls: urlpatterns,
389
+ telemetry: createOTelSink(trace.getTracer("my-app")),
390
+ });
391
+ ```
392
+
393
+ ```typescript
394
+ // Custom sink
395
+ const router = createRouter({
396
+ telemetry: {
397
+ emit(event) {
398
+ // Send to any observability backend
399
+ myTracer.record(event);
400
+ },
401
+ },
402
+ });
403
+ ```
404
+
405
+ Events emitted: `request.start/end/error`, `loader.start/end/error`,
406
+ `handler.error`, `cache.decision`, `revalidation.decision`.
407
+
408
+ ## SSR Streaming Policy
409
+
410
+ Control whether HTML SSR responses stream progressively or wait for all content:
411
+
412
+ ```typescript
413
+ import { createRouter, type SSRStreamMode } from "@rangojs/router";
414
+
415
+ const router = createRouter({
416
+ ssr: {
417
+ resolveStreaming: ({ request }) => {
418
+ const ua = request.headers.get("user-agent") ?? "";
419
+ // Bots that can't process streamed HTML get a fully resolved page
420
+ if (/Googlebot|bingbot/i.test(ua)) return "allReady";
421
+ return "stream";
422
+ },
423
+ },
424
+ });
425
+ ```
426
+
427
+ `SSRStreamMode` is `"stream" | "allReady"`:
428
+
429
+ - `"stream"` (default) — flush HTML as React renders. Suspense fallbacks appear first, then resolved content streams in. Best for real users (fastest TTFB).
430
+ - `"allReady"` — await `stream.allReady` before flushing. The full page arrives in one shot. Use for bots that cannot execute JavaScript or process chunked HTML.
431
+
432
+ The resolver receives `{ request, env, url }` and may be sync or async. It only runs on HTML SSR paths — RSC partials, `__rsc` requests, and response routes are unaffected.
433
+
434
+ When `resolveStreaming` is not configured, the default is `"stream"`.
@@ -36,20 +36,21 @@ const router = createRouter<Env>({
36
36
  ## Server (in loaders/middleware)
37
37
 
38
38
  ```typescript
39
- import { createLoader, createMiddleware } from "@rangojs/router";
39
+ import { createLoader } from "@rangojs/router";
40
+ import type { Middleware } from "@rangojs/router";
40
41
 
41
42
  // In a loader
42
- export const SettingsLoader = createLoader("settings", async (ctx) => {
43
+ export const SettingsLoader = createLoader(async (ctx) => {
43
44
  const currentTheme = ctx.theme; // read from cookie
44
45
  return { theme: currentTheme };
45
46
  });
46
47
 
47
48
  // In middleware
48
- export const themeMiddleware = createMiddleware(async (ctx, next) => {
49
+ export const themeMiddleware: Middleware = async (ctx, next) => {
49
50
  // Set theme based on user preference
50
51
  ctx.setTheme("dark");
51
52
  await next();
52
- });
53
+ };
53
54
  ```
54
55
 
55
56
  ## Client