@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.71

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 (307) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +4951 -930
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +92 -31
  18. package/skills/loader/SKILL.md +404 -44
  19. package/skills/middleware/SKILL.md +173 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +204 -1
  22. package/skills/prerender/SKILL.md +685 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +257 -14
  26. package/skills/router-setup/SKILL.md +210 -32
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +328 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/app-version.ts +14 -0
  36. package/src/browser/event-controller.ts +92 -64
  37. package/src/browser/history-state.ts +80 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +24 -4
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +20 -12
  42. package/src/browser/navigation-bridge.ts +296 -558
  43. package/src/browser/navigation-client.ts +179 -69
  44. package/src/browser/navigation-store.ts +73 -55
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +328 -313
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +150 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +160 -0
  53. package/src/browser/prefetch/resource-ready.ts +77 -0
  54. package/src/browser/rango-state.ts +112 -0
  55. package/src/browser/react/Link.tsx +230 -74
  56. package/src/browser/react/NavigationProvider.tsx +87 -11
  57. package/src/browser/react/context.ts +11 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +12 -12
  60. package/src/browser/react/location-state-shared.ts +95 -53
  61. package/src/browser/react/location-state.ts +60 -15
  62. package/src/browser/react/mount-context.ts +6 -1
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +29 -51
  66. package/src/browser/react/use-client-cache.ts +5 -3
  67. package/src/browser/react/use-handle.ts +30 -126
  68. package/src/browser/react/use-href.tsx +2 -2
  69. package/src/browser/react/use-link-status.ts +6 -5
  70. package/src/browser/react/use-navigation.ts +22 -63
  71. package/src/browser/react/use-params.ts +65 -0
  72. package/src/browser/react/use-pathname.ts +47 -0
  73. package/src/browser/react/use-router.ts +76 -0
  74. package/src/browser/react/use-search-params.ts +56 -0
  75. package/src/browser/react/use-segments.ts +80 -97
  76. package/src/browser/response-adapter.ts +73 -0
  77. package/src/browser/rsc-router.tsx +214 -58
  78. package/src/browser/scroll-restoration.ts +127 -52
  79. package/src/browser/segment-reconciler.ts +221 -0
  80. package/src/browser/segment-structure-assert.ts +16 -0
  81. package/src/browser/server-action-bridge.ts +510 -603
  82. package/src/browser/shallow.ts +6 -1
  83. package/src/browser/types.ts +141 -48
  84. package/src/browser/validate-redirect-origin.ts +29 -0
  85. package/src/build/generate-manifest.ts +235 -24
  86. package/src/build/generate-route-types.ts +39 -0
  87. package/src/build/index.ts +13 -0
  88. package/src/build/route-trie.ts +265 -0
  89. package/src/build/route-types/ast-helpers.ts +25 -0
  90. package/src/build/route-types/ast-route-extraction.ts +98 -0
  91. package/src/build/route-types/codegen.ts +102 -0
  92. package/src/build/route-types/include-resolution.ts +418 -0
  93. package/src/build/route-types/param-extraction.ts +48 -0
  94. package/src/build/route-types/per-module-writer.ts +128 -0
  95. package/src/build/route-types/router-processing.ts +618 -0
  96. package/src/build/route-types/scan-filter.ts +85 -0
  97. package/src/build/runtime-discovery.ts +231 -0
  98. package/src/cache/background-task.ts +34 -0
  99. package/src/cache/cache-key-utils.ts +44 -0
  100. package/src/cache/cache-policy.ts +125 -0
  101. package/src/cache/cache-runtime.ts +342 -0
  102. package/src/cache/cache-scope.ts +167 -309
  103. package/src/cache/cf/cf-cache-store.ts +571 -17
  104. package/src/cache/cf/index.ts +13 -3
  105. package/src/cache/document-cache.ts +116 -77
  106. package/src/cache/handle-capture.ts +81 -0
  107. package/src/cache/handle-snapshot.ts +41 -0
  108. package/src/cache/index.ts +1 -15
  109. package/src/cache/memory-segment-store.ts +191 -13
  110. package/src/cache/profile-registry.ts +73 -0
  111. package/src/cache/read-through-swr.ts +134 -0
  112. package/src/cache/segment-codec.ts +256 -0
  113. package/src/cache/taint.ts +153 -0
  114. package/src/cache/types.ts +72 -122
  115. package/src/client.rsc.tsx +3 -1
  116. package/src/client.tsx +105 -179
  117. package/src/component-utils.ts +4 -4
  118. package/src/components/DefaultDocument.tsx +5 -1
  119. package/src/context-var.ts +156 -0
  120. package/src/debug.ts +19 -9
  121. package/src/errors.ts +108 -2
  122. package/src/handle.ts +55 -29
  123. package/src/handles/MetaTags.tsx +73 -20
  124. package/src/handles/breadcrumbs.ts +66 -0
  125. package/src/handles/index.ts +1 -0
  126. package/src/handles/meta.ts +30 -13
  127. package/src/host/cookie-handler.ts +21 -15
  128. package/src/host/errors.ts +8 -8
  129. package/src/host/index.ts +4 -7
  130. package/src/host/pattern-matcher.ts +27 -27
  131. package/src/host/router.ts +61 -39
  132. package/src/host/testing.ts +8 -8
  133. package/src/host/types.ts +15 -7
  134. package/src/host/utils.ts +1 -1
  135. package/src/href-client.ts +119 -29
  136. package/src/index.rsc.ts +155 -19
  137. package/src/index.ts +223 -30
  138. package/src/internal-debug.ts +11 -0
  139. package/src/loader.rsc.ts +26 -157
  140. package/src/loader.ts +27 -10
  141. package/src/network-error-thrower.tsx +3 -1
  142. package/src/outlet-provider.tsx +45 -0
  143. package/src/prerender/param-hash.ts +37 -0
  144. package/src/prerender/store.ts +186 -0
  145. package/src/prerender.ts +524 -0
  146. package/src/reverse.ts +351 -0
  147. package/src/root-error-boundary.tsx +41 -29
  148. package/src/route-content-wrapper.tsx +7 -4
  149. package/src/route-definition/dsl-helpers.ts +982 -0
  150. package/src/route-definition/helper-factories.ts +200 -0
  151. package/src/route-definition/helpers-types.ts +434 -0
  152. package/src/route-definition/index.ts +55 -0
  153. package/src/route-definition/redirect.ts +101 -0
  154. package/src/route-definition/resolve-handler-use.ts +149 -0
  155. package/src/route-definition.ts +1 -1428
  156. package/src/route-map-builder.ts +217 -123
  157. package/src/route-name.ts +53 -0
  158. package/src/route-types.ts +70 -8
  159. package/src/router/content-negotiation.ts +215 -0
  160. package/src/router/debug-manifest.ts +72 -0
  161. package/src/router/error-handling.ts +9 -9
  162. package/src/router/find-match.ts +160 -0
  163. package/src/router/handler-context.ts +435 -86
  164. package/src/router/intercept-resolution.ts +402 -0
  165. package/src/router/lazy-includes.ts +237 -0
  166. package/src/router/loader-resolution.ts +356 -128
  167. package/src/router/logging.ts +251 -0
  168. package/src/router/manifest.ts +154 -35
  169. package/src/router/match-api.ts +555 -0
  170. package/src/router/match-context.ts +5 -3
  171. package/src/router/match-handlers.ts +440 -0
  172. package/src/router/match-middleware/background-revalidation.ts +108 -93
  173. package/src/router/match-middleware/cache-lookup.ts +459 -10
  174. package/src/router/match-middleware/cache-store.ts +98 -26
  175. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  176. package/src/router/match-middleware/segment-resolution.ts +80 -6
  177. package/src/router/match-pipelines.ts +10 -45
  178. package/src/router/match-result.ts +135 -35
  179. package/src/router/metrics.ts +240 -15
  180. package/src/router/middleware-cookies.ts +55 -0
  181. package/src/router/middleware-types.ts +220 -0
  182. package/src/router/middleware.ts +324 -369
  183. package/src/router/navigation-snapshot.ts +182 -0
  184. package/src/router/pattern-matching.ts +211 -43
  185. package/src/router/prerender-match.ts +502 -0
  186. package/src/router/preview-match.ts +98 -0
  187. package/src/router/request-classification.ts +310 -0
  188. package/src/router/revalidation.ts +137 -38
  189. package/src/router/route-snapshot.ts +245 -0
  190. package/src/router/router-context.ts +41 -21
  191. package/src/router/router-interfaces.ts +484 -0
  192. package/src/router/router-options.ts +618 -0
  193. package/src/router/router-registry.ts +24 -0
  194. package/src/router/segment-resolution/fresh.ts +748 -0
  195. package/src/router/segment-resolution/helpers.ts +268 -0
  196. package/src/router/segment-resolution/loader-cache.ts +199 -0
  197. package/src/router/segment-resolution/revalidation.ts +1379 -0
  198. package/src/router/segment-resolution/static-store.ts +67 -0
  199. package/src/router/segment-resolution.ts +21 -0
  200. package/src/router/segment-wrappers.ts +291 -0
  201. package/src/router/telemetry-otel.ts +299 -0
  202. package/src/router/telemetry.ts +300 -0
  203. package/src/router/timeout.ts +148 -0
  204. package/src/router/trie-matching.ts +239 -0
  205. package/src/router/types.ts +78 -3
  206. package/src/router.ts +740 -4252
  207. package/src/rsc/handler-context.ts +45 -0
  208. package/src/rsc/handler.ts +907 -797
  209. package/src/rsc/helpers.ts +140 -6
  210. package/src/rsc/index.ts +0 -20
  211. package/src/rsc/loader-fetch.ts +229 -0
  212. package/src/rsc/manifest-init.ts +90 -0
  213. package/src/rsc/nonce.ts +14 -0
  214. package/src/rsc/origin-guard.ts +141 -0
  215. package/src/rsc/progressive-enhancement.ts +391 -0
  216. package/src/rsc/response-error.ts +37 -0
  217. package/src/rsc/response-route-handler.ts +347 -0
  218. package/src/rsc/rsc-rendering.ts +246 -0
  219. package/src/rsc/runtime-warnings.ts +42 -0
  220. package/src/rsc/server-action.ts +356 -0
  221. package/src/rsc/ssr-setup.ts +128 -0
  222. package/src/rsc/types.ts +46 -11
  223. package/src/search-params.ts +230 -0
  224. package/src/segment-system.tsx +165 -17
  225. package/src/server/context.ts +315 -58
  226. package/src/server/cookie-store.ts +190 -0
  227. package/src/server/fetchable-loader-store.ts +37 -0
  228. package/src/server/handle-store.ts +113 -15
  229. package/src/server/loader-registry.ts +24 -64
  230. package/src/server/request-context.ts +607 -81
  231. package/src/server.ts +35 -130
  232. package/src/ssr/index.tsx +103 -30
  233. package/src/static-handler.ts +126 -0
  234. package/src/theme/ThemeProvider.tsx +21 -15
  235. package/src/theme/ThemeScript.tsx +5 -5
  236. package/src/theme/constants.ts +5 -2
  237. package/src/theme/index.ts +4 -14
  238. package/src/theme/theme-context.ts +4 -30
  239. package/src/theme/theme-script.ts +21 -18
  240. package/src/types/boundaries.ts +158 -0
  241. package/src/types/cache-types.ts +198 -0
  242. package/src/types/error-types.ts +192 -0
  243. package/src/types/global-namespace.ts +100 -0
  244. package/src/types/handler-context.ts +791 -0
  245. package/src/types/index.ts +88 -0
  246. package/src/types/loader-types.ts +210 -0
  247. package/src/types/route-config.ts +170 -0
  248. package/src/types/route-entry.ts +109 -0
  249. package/src/types/segments.ts +151 -0
  250. package/src/types.ts +1 -1623
  251. package/src/urls/include-helper.ts +197 -0
  252. package/src/urls/index.ts +53 -0
  253. package/src/urls/path-helper-types.ts +346 -0
  254. package/src/urls/path-helper.ts +364 -0
  255. package/src/urls/pattern-types.ts +107 -0
  256. package/src/urls/response-types.ts +116 -0
  257. package/src/urls/type-extraction.ts +372 -0
  258. package/src/urls/urls-function.ts +98 -0
  259. package/src/urls.ts +1 -802
  260. package/src/use-loader.tsx +161 -81
  261. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  262. package/src/vite/discovery/discover-routers.ts +348 -0
  263. package/src/vite/discovery/prerender-collection.ts +439 -0
  264. package/src/vite/discovery/route-types-writer.ts +258 -0
  265. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  266. package/src/vite/discovery/state.ts +117 -0
  267. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  268. package/src/vite/index.ts +15 -1129
  269. package/src/vite/plugin-types.ts +103 -0
  270. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  271. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  272. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  273. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  274. package/src/vite/plugins/expose-id-utils.ts +299 -0
  275. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  276. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  277. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  278. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  279. package/src/vite/plugins/expose-ids/types.ts +45 -0
  280. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  281. package/src/vite/plugins/performance-tracks.ts +88 -0
  282. package/src/vite/plugins/refresh-cmd.ts +127 -0
  283. package/src/vite/plugins/use-cache-transform.ts +323 -0
  284. package/src/vite/plugins/version-injector.ts +83 -0
  285. package/src/vite/plugins/version-plugin.ts +266 -0
  286. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  287. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  288. package/src/vite/rango.ts +462 -0
  289. package/src/vite/router-discovery.ts +918 -0
  290. package/src/vite/utils/ast-handler-extract.ts +517 -0
  291. package/src/vite/utils/banner.ts +36 -0
  292. package/src/vite/utils/bundle-analysis.ts +137 -0
  293. package/src/vite/utils/manifest-utils.ts +70 -0
  294. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  295. package/src/vite/utils/prerender-utils.ts +207 -0
  296. package/src/vite/utils/shared-utils.ts +170 -0
  297. package/CLAUDE.md +0 -43
  298. package/src/browser/lru-cache.ts +0 -69
  299. package/src/browser/request-controller.ts +0 -164
  300. package/src/cache/memory-store.ts +0 -253
  301. package/src/href-context.ts +0 -33
  302. package/src/href.ts +0 -255
  303. package/src/server/route-manifest-cache.ts +0 -173
  304. package/src/vite/expose-handle-id.ts +0 -209
  305. package/src/vite/expose-loader-id.ts +0 -426
  306. package/src/vite/expose-location-state-id.ts +0 -177
  307. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,685 @@
1
+ ---
2
+ name: prerender
3
+ description: Pre-render route segments at build time with Prerender and Passthrough live fallback
4
+ argument-hint: [passthrough]
5
+ ---
6
+
7
+ # Pre-rendering with Prerender
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.
13
+
14
+ Canonical semantics reference:
15
+ [docs/execution-model.md](../../docs/internal/execution-model.md)
16
+
17
+ ## API: Prerender
18
+
19
+ ### Static Route (no params)
20
+
21
+ ```typescript
22
+ import { Prerender } from "@rangojs/router";
23
+
24
+ export const AboutPage = Prerender(async (ctx) => {
25
+ const content = await fs.readFile("content/about.md", "utf-8");
26
+ return <Page content={markdownToJsx(content)} />;
27
+ });
28
+
29
+ // urls.tsx
30
+ path("/about", AboutPage, { name: "about" })
31
+ ```
32
+
33
+ ### Dynamic Route (with params)
34
+
35
+ Params come first, handler second:
36
+
37
+ ```typescript
38
+ export const BlogPost = Prerender(
39
+ // 1. Params: which slugs to pre-render
40
+ async () => {
41
+ const files = await glob("content/blog/*.md");
42
+ return files.map(f => ({ slug: basename(f, ".md") }));
43
+ },
44
+ // 2. Handler: runs at build time with BuildContext
45
+ async (ctx) => {
46
+ const md = await fs.readFile(`content/${ctx.params.slug}.md`, "utf-8");
47
+ return <Article content={markdownToJsx(md)} />;
48
+ }
49
+ );
50
+
51
+ // urls.tsx
52
+ path("/blog/:slug", BlogPost, { name: "blog.post" })
53
+ ```
54
+
55
+ ### With Passthrough (live fallback for unknown params)
56
+
57
+ Wrap a `Prerender` definition with `Passthrough()` to add a separate live handler
58
+ for unknown params at runtime. The build handler runs at build time, the live
59
+ handler runs at request time.
60
+
61
+ ```typescript
62
+ import { Prerender, Passthrough } from "@rangojs/router";
63
+
64
+ export const ProductPageDef = Prerender(
65
+ async () => {
66
+ const top = await db.query("SELECT id FROM products WHERE featured");
67
+ return top.map(p => ({ id: p.id }));
68
+ },
69
+ async (ctx) => {
70
+ const product = await db.query("SELECT * FROM products WHERE id = ?", ctx.params.id);
71
+ return <Product data={product} />;
72
+ },
73
+ { concurrency: 4 }
74
+ );
75
+
76
+ // In route definition:
77
+ path("/products/:id", Passthrough(ProductPageDef, async (ctx) => {
78
+ const product = await ctx.env.DB.query("SELECT * FROM products WHERE id = ?", ctx.params.id);
79
+ return <Product data={product} />;
80
+ }), { name: "product" })
81
+ ```
82
+
83
+ ## Passthrough Wrapper
84
+
85
+ `Passthrough(prerenderDef, liveHandler)` wraps a `Prerender` definition with a
86
+ separate handler for runtime fallback. The build and live handlers are separate
87
+ functions — no `ctx.build` branching needed.
88
+
89
+ | | Plain `Prerender` (no wrapper) | `Passthrough(def, liveHandler)` |
90
+ | ------------------- | --------------------------------------- | ---------------------------------------- |
91
+ | Known params | Served from pre-rendered Flight payload | Served from pre-rendered Flight payload |
92
+ | Unknown params | Handler evicted, no live fallback | Live handler runs at request time |
93
+ | `ctx.passthrough()` | Throws (not on Passthrough route) | Skips artifact, defers to live handler |
94
+ | Bundle size | Build handler code + imports removed | Build handler evicted, live handler kept |
95
+ | `revalidate()` | Not allowed (handler gone) | Allowed (live handler can re-render) |
96
+ | `loading()` | Ignored (segments fully resolved) | Works for live fallback renders |
97
+
98
+ ### When to use Passthrough
99
+
100
+ Use `Passthrough()` when:
101
+
102
+ - The route has a large or open-ended param space (e.g., user profiles, product pages)
103
+ - You want to pre-render popular/known params for speed but still serve unknown params live
104
+ - You need `revalidate()` on the route
105
+ - The live handler needs runtime bindings (e.g., `ctx.env.DB`)
106
+
107
+ Use plain `Prerender` (no wrapper) when:
108
+
109
+ - All possible params are known at build time (e.g., markdown files, config-driven pages)
110
+ - You want maximum bundle size reduction (handler code + node:fs imports removed)
111
+ - The route uses build-only APIs (node:fs, local files) not available at runtime
112
+
113
+ ## BuildContext
114
+
115
+ Handlers receive `BuildContext` at build time, a subset of the runtime `HandlerContext`:
116
+
117
+ ```typescript
118
+ interface BuildContext<TParams> {
119
+ params: TParams; // From getParams
120
+ build: true; // Always true at build time
121
+ dev: boolean; // true in Vite dev mode, false during production build
122
+ use: <T>(handle: Handle<T>) => (data: T) => void; // Push handle data
123
+ url: URL; // Synthetic URL from pattern + params
124
+ pathname: string; // Pathname from synthetic URL
125
+ set(key: string, value: any): void; // Set context variable (string key)
126
+ set<T>(contextVar: ContextVar<T>, value: T): void; // Set typed context variable
127
+ get(key: string): any; // Read context variable (string key)
128
+ get<T>(contextVar: ContextVar<T>): T | undefined; // Read typed context variable
129
+ reverse(
130
+ name: string,
131
+ params?: Record<string, string>,
132
+ search?: Record<string, unknown>,
133
+ ): string; // URL generation
134
+ passthrough(): PrerenderPassthroughResult; // Skip local artifact (Passthrough routes only)
135
+ env: DefaultEnv; // Available when buildEnv is configured in rango() (throws otherwise)
136
+ // NOT available: request, headers, cookies (always throw)
137
+ }
138
+ ```
139
+
140
+ Use `createVar<T>()` to share typed data from a Prerender handler to child layouts:
141
+
142
+ ```typescript
143
+ import { Prerender, createVar } from "@rangojs/router";
144
+
145
+ interface PaginationData { current: number; total: number; }
146
+ export const Pagination = createVar<PaginationData>();
147
+
148
+ export const ArticleList = Prerender<{ page: string }>(
149
+ async () => [{ page: "1" }, { page: "2" }],
150
+ async (ctx) => {
151
+ ctx.set(Pagination, { current: Number(ctx.params.page), total: 2 });
152
+ return <Articles />;
153
+ },
154
+ );
155
+ ```
156
+
157
+ All items inside the path's use() callback (child layouts, parallels) also receive
158
+ `BuildContext` during pre-rendering. Loaders are the exception -- they run at
159
+ request time with full server context.
160
+
161
+ This is one reason prerender is a good fit for handler-first composition:
162
+ the handler and its child layouts/parallels participate in the same full
163
+ render pass, so data set with `ctx.set()` is available downstream via
164
+ `ctx.get()`.
165
+
166
+ At runtime, partial action revalidation follows a narrower rule: only
167
+ revalidated segments are recomputed. If a child segment depends on data
168
+ established by an outer handler/layout, that outer segment must also be
169
+ revalidated, or the child must load/guard the data independently.
170
+
171
+ ## Supported Export Patterns
172
+
173
+ All of the following are equivalent and fully supported by the Vite transform:
174
+
175
+ ```typescript
176
+ // Direct export (most common)
177
+ export const BlogPost = Prerender(getParams, handler);
178
+
179
+ // Separate declaration + named export
180
+ const BlogPost = Prerender(getParams, handler);
181
+ export { BlogPost };
182
+
183
+ // Aliased export
184
+ const InternalPage = Prerender(getParams, handler);
185
+ export { InternalPage as BlogPost };
186
+
187
+ // Aliased import
188
+ import { Prerender as cph } from "@rangojs/router";
189
+ export const BlogPost = cph(getParams, handler);
190
+ ```
191
+
192
+ All patterns support whole-file stubbing, expression stubbing, and build-time
193
+ module tracking. The same applies to `Static`.
194
+
195
+ ## Handler Eviction
196
+
197
+ In production builds, `Prerender` exports are replaced with stubs:
198
+
199
+ ```typescript
200
+ // Original
201
+ export const BlogPost = Prerender(getParams, handler);
202
+
203
+ // Stubbed (all Prerender handlers are evicted)
204
+ export const BlogPost = {
205
+ __brand: "prerenderHandler",
206
+ $$id: "abc123#BlogPost",
207
+ };
208
+ ```
209
+
210
+ All Prerender handlers are evicted in production. The live handler for
211
+ `Passthrough()` routes lives in the urls module and is not evicted.
212
+
213
+ In client and SSR environments, ALL prerender handlers are always stubbed.
214
+
215
+ ## Sub-use Semantics
216
+
217
+ Everything inside the path's use() callback is part of the B segment and gets
218
+ pre-rendered:
219
+
220
+ ```typescript
221
+ path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
222
+ layout(<PostLayout />, () => [ // inside B -> pre-rendered
223
+ loader(PostMetaLoader), // live at runtime, bundled normally
224
+ ]),
225
+ parallel({ "@sidebar": BlogSidebar }), // inside B -> pre-rendered
226
+ ])
227
+ ```
228
+
229
+ If a parallel or child layout uses node APIs, wrap it in `Prerender`
230
+ (static, no getParams) so the Vite plugin can stub it:
231
+
232
+ ```typescript
233
+ // sidebar.tsx -- uses node:fs, must be a Prerender
234
+ export const BlogSidebar = Prerender(async (ctx) => {
235
+ const files = await fs.readdir("content/blog/");
236
+ return <Sidebar posts={files.map(f => basename(f, ".md"))} />;
237
+ });
238
+
239
+ // urls.tsx
240
+ path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
241
+ parallel({ "@sidebar": BlogSidebar }), // stubbable, node:fs excluded
242
+ ])
243
+ ```
244
+
245
+ ## Interaction with DSL Items
246
+
247
+ | DSL item | Behavior with Prerender |
248
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
249
+ | `loader()` | Live at runtime, bundled normally. Use `cache()` for caching. |
250
+ | `revalidate()` | Not allowed without Passthrough. Allowed with Passthrough. |
251
+ | `cache()` | Orthogonal -- use on parent layouts and loaders. |
252
+ | `layout()` | Child layouts inside path are pre-rendered. Parent layouts are live. |
253
+ | `parallel()` | Parallel slots inside path are pre-rendered. |
254
+ | `middleware()` | Skipped during pre-render (no request). Runs at request time for loaders. |
255
+ | `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough. |
256
+ | `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). |
257
+
258
+ When Passthrough revalidation is enabled, remember that revalidation is
259
+ still partial: opting a child segment into revalidation does not
260
+ implicitly re-run outer prerender-derived handlers/layouts.
261
+
262
+ ## Dev Mode
263
+
264
+ In dev mode there is no production-style prerender build pass and no handler
265
+ stubbing.
266
+
267
+ **Node.js dev server** — `Prerender` acts as a normal handler. Routes render
268
+ live on every request with full runtime context (`ctx.build === false`).
269
+
270
+ **Non-Node runtimes (Cloudflare workerd, Deno workers)** — Handlers that
271
+ depend on Node APIs (e.g. `node:fs`) cannot run in-process. The Vite plugin
272
+ can intercept these requests and resolve them via the `/__rsc_prerender`
273
+ endpoint, which runs `matchForPrerender` in a Node.js temp server. In this
274
+ path the handler receives `BuildContext` (`ctx.build === true`) and segments
275
+ are resolved identically to production prerendering, then served on-demand.
276
+ This only applies when `__PRERENDER_DEV_URL` is set by the plugin.
277
+
278
+ ## Storage Layout
279
+
280
+ Pre-rendered Flight payloads are stored in the build output:
281
+
282
+ ```
283
+ dist/static/__<hash>/
284
+ prerender/
285
+ blog.post/
286
+ d4e5f6a7.flight # hash of { slug: "hello-world" }
287
+ b8c9d0e1.flight # hash of { slug: "getting-started" }
288
+ about/
289
+ _.flight # static route, no params
290
+ ```
291
+
292
+ ## Concurrency
293
+
294
+ Prerender handlers can specify how many param sets render in parallel:
295
+
296
+ ```typescript
297
+ export const BlogPost = Prerender(
298
+ async () => posts.map(p => ({ slug: p.slug })),
299
+ async (ctx) => <PostPage slug={ctx.params.slug} />,
300
+ { concurrency: 4 },
301
+ );
302
+ ```
303
+
304
+ Default is `1` (sequential). Only `Prerender` supports concurrency; `Static` handlers
305
+ always render sequentially.
306
+
307
+ ## Skipping Entries with Skip
308
+
309
+ Throw `Skip` inside a Prerender or Static handler to skip an individual entry
310
+ without failing the build:
311
+
312
+ ```typescript
313
+ import { Prerender, Skip } from "@rangojs/router";
314
+
315
+ export const BlogPost = Prerender(
316
+ async () => [{ slug: "published" }, { slug: "draft" }],
317
+ async (ctx) => {
318
+ if (ctx.params.slug === "draft") {
319
+ throw new Skip("Draft articles are not pre-rendered");
320
+ }
321
+ return <PostPage slug={ctx.params.slug} />;
322
+ },
323
+ );
324
+
325
+ // Wrap with Passthrough to serve skipped params live at runtime
326
+ export const BlogPost = Passthrough(BlogPostDef, async (ctx) => {
327
+ if (ctx.params.slug === "draft") {
328
+ throw new Skip("Draft articles are not pre-rendered");
329
+ }
330
+ return <PostPage slug={ctx.params.slug} />;
331
+ });
332
+ ```
333
+
334
+ Skipped entries are excluded from the build output. With `Passthrough()`,
335
+ the live handler serves skipped params at request time.
336
+
337
+ `Skip` also works in `Static` handlers:
338
+
339
+ ```typescript
340
+ import { Static, Skip } from "@rangojs/router";
341
+
342
+ export const TocSidebar = Static(() => {
343
+ throw new Skip("Not ready for pre-rendering");
344
+ });
345
+ ```
346
+
347
+ ### Error behavior at build time
348
+
349
+ | Handler outcome | Effect |
350
+ | --------------------------- | ----------------------------------------------------- |
351
+ | JSX / `null` | Normal prerender entry, log OK |
352
+ | `return ctx.passthrough()` | Skip entry, log PASS, continue (Passthrough routes) |
353
+ | `throw new Skip("reason")` | Skip entry, log SKIP, continue with remaining entries |
354
+ | `throw new Error("reason")` | Log FAIL, stop ALL pre-rendering, fail the build |
355
+
356
+ Both error types propagate to the router's `onError` callback with phase
357
+ `"prerender"` or `"static"`.
358
+
359
+ ### Build logs
360
+
361
+ The build produces per-URL timing logs:
362
+
363
+ ```
364
+ [rsc-router] Pre-rendering 12 URL(s) (concurrency: 4)...
365
+ [rsc-router] OK /articles/hello (42ms)
366
+ [rsc-router] PASS /articles/remote-only (5ms) - live fallback
367
+ [rsc-router] SKIP /articles/draft-post (3ms) - Article is a draft
368
+ [rsc-router] Pre-render complete: 11 done, 1 skipped (1204ms total)
369
+
370
+ [rsc-router] Rendering 3 static handler(s)...
371
+ [rsc-router] OK DocsLayout (28ms)
372
+ [rsc-router] SKIP TocSidebar (1ms) - Not ready
373
+ [rsc-router] Static render complete: 2 done, 1 skipped (120ms total)
374
+ ```
375
+
376
+ A `FAIL` line is logged per-URL when a handler throws a non-Skip error. The
377
+ error is re-thrown immediately, so no summary line is printed — the build
378
+ stops at the first failure.
379
+
380
+ ### Dev mode behavior
381
+
382
+ **Node.js dev server** — `Skip` behaves like a regular runtime error because
383
+ the handler runs live with `ctx.build === false`.
384
+
385
+ **Non-Node runtimes using `/__rsc_prerender`** — `Skip` participates in the
386
+ on-demand prerender path, so build-style skip logic does run for that request.
387
+ The dev prerender endpoint treats it like a prerender miss and the request
388
+ falls back according to normal dev/runtime behavior.
389
+
390
+ ## Per-Param Passthrough with ctx.passthrough()
391
+
392
+ On routes wrapped with `Passthrough()`, the build handler can return
393
+ `ctx.passthrough()` to skip writing a local prerender artifact for a specific
394
+ param set. At runtime, the missing entry falls through to the live handler.
395
+
396
+ ```typescript
397
+ export const BlogPostDef = Prerender(
398
+ async () => [{ slug: "a" }, { slug: "b" }, { slug: "c" }],
399
+ async (ctx) => {
400
+ const post = await getPost(ctx.params.slug);
401
+ if (!post) return ctx.passthrough();
402
+ return <article>{post.content}</article>;
403
+ },
404
+ );
405
+
406
+ export const BlogPost = Passthrough(BlogPostDef, async (ctx) => {
407
+ const post = await getPost(ctx.params.slug);
408
+ return <article>{post.content}</article>;
409
+ });
410
+ ```
411
+
412
+ ### Semantics
413
+
414
+ - JSX or `null` from the build handler produces a normal prerender entry.
415
+ - `ctx.passthrough()` returns a sentinel that signals "no local artifact".
416
+ The build skips the manifest entry for that param set.
417
+ - `ctx.passthrough()` on a route not wrapped with `Passthrough()` throws.
418
+ - `ctx.passthrough()` at runtime (`ctx.build === false`) also throws.
419
+ It is a build-time-only control flow.
420
+ - `getParams()` still enumerates the param set; the build handler decides
421
+ per-param whether to produce an artifact or defer to the live handler.
422
+
423
+ ### Difference from Skip
424
+
425
+ | Mechanism | Effect on build | Runtime behavior |
426
+ | ------------------- | ---------------------- | ------------------------------------------------------ |
427
+ | `throw new Skip()` | Skips entry, logs SKIP | No artifact, no live fallback unless Passthrough route |
428
+ | `ctx.passthrough()` | Skips entry, logs PASS | Always defers to live handler (requires Passthrough) |
429
+
430
+ Use `ctx.passthrough()` when you want the live handler to run at request time
431
+ for specific params. Use `Skip` when you want to exclude params entirely.
432
+
433
+ ### Use case: Remote storage
434
+
435
+ `ctx.passthrough()` enables a pattern where build-time data is stored in a
436
+ remote KV store instead of the local prerender manifest. The build handler
437
+ pre-computes data during `getParams`, pushes it to KV, then calls
438
+ `ctx.passthrough()` so the local build skips the artifact. At runtime,
439
+ the Passthrough live handler reads from KV:
440
+
441
+ ```typescript
442
+ export const ProductDef = Prerender(
443
+ async () => {
444
+ const products = await db.getFeaturedProducts();
445
+ for (const p of products) {
446
+ await kv.put(`product:${p.id}`, await renderProduct(p));
447
+ }
448
+ return products.map(p => ({ id: p.id }));
449
+ },
450
+ async (ctx) => {
451
+ // At build time: skip local artifact, data is in KV
452
+ return ctx.passthrough();
453
+ },
454
+ );
455
+
456
+ export const Product = Passthrough(ProductDef, async (ctx) => {
457
+ // At runtime: read from KV, fall back to DB
458
+ const cached = await kv.get(`product:${ctx.params.id}`);
459
+ if (cached) return cached;
460
+ return <Product data={await ctx.env.DB.getProduct(ctx.params.id)} />;
461
+ });
462
+ ```
463
+
464
+ ### Build logs
465
+
466
+ Passthrough entries are logged distinctly:
467
+
468
+ ```
469
+ [rsc-router] OK /blog/a (42ms)
470
+ [rsc-router] PASS /blog/b (3ms) - live fallback
471
+ [rsc-router] OK /blog/c (38ms)
472
+ ```
473
+
474
+ ## Edge Cases and Constraints
475
+
476
+ ### Loaders are always live
477
+
478
+ Loaders on pre-rendered routes run at request time. They are bundled normally
479
+ and need `cache()` for caching. Do not use build-only APIs in loaders.
480
+
481
+ ### Handle data is frozen
482
+
483
+ Handle values pushed via `ctx.use()` during pre-rendering are baked into the
484
+ Flight payload. They do not update at request time.
485
+
486
+ ### Server actions work normally
487
+
488
+ Actions do not re-render the B segment. The pre-rendered handler output stays
489
+ frozen. Loaders are live and can be revalidated by actions. With `Passthrough()`
490
+ and `revalidate()`, the live handler can re-render.
491
+
492
+ ### Empty getParams
493
+
494
+ If `getParams` returns an empty array, no Flight payloads are written. No error.
495
+
496
+ ### Route name is required
497
+
498
+ Routes using `Prerender` must have a `name` in path options.
499
+ The name is used as the storage key for Flight payloads.
500
+
501
+ ### No revalidate without Passthrough
502
+
503
+ Using `revalidate()` without `Passthrough()` produces a build-time warning.
504
+ The handler is evicted -- there is nothing to re-render.
505
+
506
+ ### loading() is ignored without Passthrough
507
+
508
+ Pre-rendered segments are fully resolved at build time and never suspend.
509
+ With `Passthrough()`, `loading()` works for live fallback renders.
510
+
511
+ ## Complete Example
512
+
513
+ ```typescript
514
+ // pages/guides-handler.tsx
515
+ import { Prerender, Passthrough } from "@rangojs/router";
516
+ import { Link } from "@rangojs/router/client";
517
+ import { href } from "../router.js";
518
+
519
+ const knownGuides: Record<string, string> = {
520
+ routing: "Routing Guide",
521
+ caching: "Caching Guide",
522
+ };
523
+
524
+ export const GuidesDetailDef = Prerender<{ slug: string }>(
525
+ async () => Object.keys(knownGuides).map((slug) => ({ slug })),
526
+ async (ctx) => {
527
+ const title = knownGuides[ctx.params.slug] ?? `Guide: ${ctx.params.slug}`;
528
+ return (
529
+ <div>
530
+ <h1>{title}</h1>
531
+ <p>Slug: {ctx.params.slug}</p>
532
+ <nav>
533
+ <Link to={href("guides.detail", { slug: "routing" })}>Routing</Link>
534
+ {" | "}
535
+ <Link to={href("guides.detail", { slug: "dynamic-test" })}>Dynamic</Link>
536
+ </nav>
537
+ </div>
538
+ );
539
+ },
540
+ );
541
+
542
+ export const GuidesDetail = Passthrough(GuidesDetailDef, async (ctx) => {
543
+ const title = knownGuides[ctx.params.slug] ?? `Guide: ${ctx.params.slug}`;
544
+ return (
545
+ <div>
546
+ <h1>{title}</h1>
547
+ <p>Slug: {ctx.params.slug}</p>
548
+ <nav>
549
+ <Link to={href("guides.detail", { slug: "routing" })}>Routing</Link>
550
+ {" | "}
551
+ <Link to={href("guides.detail", { slug: "dynamic-test" })}>Dynamic</Link>
552
+ </nav>
553
+ </div>
554
+ );
555
+ });
556
+
557
+ // pages/guides.tsx
558
+ import { urls } from "@rangojs/router";
559
+ import { GuidesDetail } from "./guides-handler.js";
560
+
561
+ export const guidesPatterns = urls(({ path }) => [
562
+ path("/:slug", GuidesDetail, { name: "detail" }),
563
+ ]);
564
+
565
+ // urls.tsx
566
+ import { urls } from "@rangojs/router";
567
+ import { guidesPatterns } from "./pages/guides.js";
568
+
569
+ export const urlpatterns = urls(({ path, include }) => [
570
+ path("/", HomePage, { name: "home" }),
571
+ include("/guides", guidesPatterns, { name: "guides" }),
572
+ ]);
573
+ ```
574
+
575
+ ## Interaction with intercept()
576
+
577
+ When a pre-rendered route is also the target of an `intercept()`, the build system
578
+ resolves the intercept handler at build time and stores a combined entry (main
579
+ segments + intercept segments) under an `/i`-suffixed key alongside the main entry:
580
+
581
+ ```
582
+ prerender store keys:
583
+ "blog.post/a1b2c3" -> main segments (full page)
584
+ "blog.post/a1b2c3/i" -> main segments + intercept segments (modal variant)
585
+ ```
586
+
587
+ At runtime, the cache-lookup middleware checks `ctx.isIntercept`:
588
+
589
+ - **Intercept navigation**: looks up `paramHash/i` first. If found, yields
590
+ the combined entry. `handleCacheHitIntercept()` extracts intercept segments
591
+ (filtered by `namespace?.startsWith("intercept:")`) and sets up slots.
592
+ - **Direct navigation**: looks up `paramHash` (no suffix). Standard prerender path.
593
+ - **Intercept miss (no `/i` entry)**: falls through to the normal pipeline so
594
+ intercept-resolution middleware runs live. This handles `when()` conditions
595
+ that prevented pre-rendering.
596
+
597
+ The `when()` callback receives an `InterceptSelectorContext` with `from.pathname`
598
+ which is unknown at build time. All intercepts are pre-rendered unconditionally;
599
+ `when()` is evaluated at runtime by the intercept-resolution middleware.
600
+
601
+ ### Example: Pre-rendered route with intercept
602
+
603
+ ```typescript
604
+ // Route handler is pre-rendered at build time
605
+ export const ProductDetail = Prerender(
606
+ async () => [{ slug: "shoes" }, { slug: "jacket" }],
607
+ async (ctx) => <ProductPage slug={ctx.params.slug} />,
608
+ );
609
+
610
+ // urls.tsx
611
+ layout(ShopLayout, () => [
612
+ path("/:slug", ProductDetail, { name: "detail" }, () => [
613
+ loader(ProductLoader),
614
+ ]),
615
+
616
+ // Intercept detail from shop index into a modal.
617
+ // At build time, this is resolved and stored under the /i key.
618
+ intercept("@modal", ".detail", <ProductModal />, () => [
619
+ when(({ from }) => from.pathname === "/shop"),
620
+ loader(ProductLoader),
621
+ ]),
622
+ ])
623
+ ```
624
+
625
+ Both `ProductPage` (main) and `ProductModal` (intercept) are frozen at build time.
626
+ Loaders run fresh at request time for both variants.
627
+
628
+ ## Trie Flags
629
+
630
+ Pre-rendered routes set flags on the route trie leaf at build time:
631
+
632
+ - `pr: true` -- route has pre-rendered B segment data
633
+ - `pt: true` -- route wrapped with `Passthrough()` (live handler available)
634
+
635
+ At runtime, the cache-lookup middleware uses these flags:
636
+
637
+ - `pr + hit` -- serve pre-rendered Flight payload
638
+ - `pr + pt + miss` -- fall through to Passthrough live handler
639
+ - `pr + miss` (no pt) -- fall through (handler stubbed, no live render)
640
+
641
+ ## Contributor Checklist
642
+
643
+ Before changing prerender behavior, read these docs and run these tests.
644
+
645
+ ### Docs to re-read
646
+
647
+ - [Prerender API design](../../docs/prerender-api-design.md) -- canonical
648
+ architecture: build-time flow, runtime flow, storage, Passthrough, intercept
649
+ - [Execution model](../../docs/internal/execution-model.md) -- handler-first
650
+ ordering, middleware scope, context visibility rules
651
+ - [Semantic change checklist](../../docs/internal/semantic-change-checklist.md)
652
+ -- gate for any change to execution semantics
653
+
654
+ ### Tests to run
655
+
656
+ ```bash
657
+ # Core prerender e2e (Passthrough, eviction, loaders, sub-use, intercept)
658
+ pnpm --filter @rangojs/router exec playwright test prerender
659
+
660
+ # Prerender-specific unit test
661
+ pnpm --filter @rangojs/router run test:unit -- prerender-passthrough
662
+
663
+ # Semantic matrix (prerender rows cover intercept + ctx propagation)
664
+ pnpm --filter @rangojs/router exec playwright test semantic-matrix
665
+
666
+ # Handler-first (ctx.set/get visibility with prerender handlers)
667
+ pnpm --filter @rangojs/router exec playwright test handler-first
668
+ ```
669
+
670
+ ### Dev-only vs build-parity
671
+
672
+ - Prerender e2e tests run against a real production build by default (the
673
+ fixture builds the test app). Dev-mode prerender behavior is tested via
674
+ `/__rsc_prerender` endpoint tests and node.js dev-server fallback.
675
+ - Log-based assertions (build output lines, debug cache logs) are inherently
676
+ dev/build-only and do not need a production counterpart.
677
+ - Behavioral assertions (rendered content, loader freshness, Passthrough
678
+ fallback, intercept variant selection) must work in the production build.
679
+
680
+ ## Maintenance References
681
+
682
+ - [Stability next steps plan](../../docs/internal/stability-next-steps-plan.md)
683
+ -- completed parity and cleanup pass (reference for decisions made)
684
+ - [Test quality baseline](../../docs/internal/test-quality-baseline.md) --
685
+ measured test inventory, sleep debt, production coverage gaps