@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.8a4d0430

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 (300) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4474 -863
  5. package/package.json +60 -51
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +50 -21
  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 +89 -30
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +318 -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/event-controller.ts +87 -64
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/intercept-utils.ts +52 -0
  38. package/src/browser/link-interceptor.ts +24 -4
  39. package/src/browser/logging.ts +55 -0
  40. package/src/browser/merge-segment-loaders.ts +20 -12
  41. package/src/browser/navigation-bridge.ts +285 -553
  42. package/src/browser/navigation-client.ts +124 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +295 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +258 -308
  47. package/src/browser/prefetch/cache.ts +146 -0
  48. package/src/browser/prefetch/fetch.ts +135 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +42 -0
  51. package/src/browser/prefetch/queue.ts +88 -0
  52. package/src/browser/rango-state.ts +112 -0
  53. package/src/browser/react/Link.tsx +185 -73
  54. package/src/browser/react/NavigationProvider.tsx +51 -11
  55. package/src/browser/react/context.ts +6 -0
  56. package/src/browser/react/filter-segment-order.ts +11 -0
  57. package/src/browser/react/index.ts +12 -12
  58. package/src/browser/react/location-state-shared.ts +95 -53
  59. package/src/browser/react/location-state.ts +60 -15
  60. package/src/browser/react/mount-context.ts +6 -1
  61. package/src/browser/react/nonce-context.ts +23 -0
  62. package/src/browser/react/shallow-equal.ts +27 -0
  63. package/src/browser/react/use-action.ts +29 -51
  64. package/src/browser/react/use-client-cache.ts +5 -3
  65. package/src/browser/react/use-handle.ts +32 -79
  66. package/src/browser/react/use-href.tsx +2 -2
  67. package/src/browser/react/use-link-status.ts +6 -5
  68. package/src/browser/react/use-navigation.ts +22 -63
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +107 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +16 -0
  79. package/src/browser/server-action-bridge.ts +504 -599
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +109 -47
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +235 -24
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +13 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +3 -1
  114. package/src/client.tsx +106 -126
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +15 -29
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/breadcrumbs.ts +66 -0
  123. package/src/handles/index.ts +1 -0
  124. package/src/handles/meta.ts +30 -13
  125. package/src/host/cookie-handler.ts +21 -15
  126. package/src/host/errors.ts +8 -8
  127. package/src/host/index.ts +4 -7
  128. package/src/host/pattern-matcher.ts +27 -27
  129. package/src/host/router.ts +61 -39
  130. package/src/host/testing.ts +8 -8
  131. package/src/host/types.ts +15 -7
  132. package/src/host/utils.ts +1 -1
  133. package/src/href-client.ts +119 -29
  134. package/src/index.rsc.ts +153 -19
  135. package/src/index.ts +211 -30
  136. package/src/internal-debug.ts +11 -0
  137. package/src/loader.rsc.ts +26 -157
  138. package/src/loader.ts +27 -10
  139. package/src/network-error-thrower.tsx +3 -1
  140. package/src/outlet-provider.tsx +45 -0
  141. package/src/prerender/param-hash.ts +37 -0
  142. package/src/prerender/store.ts +185 -0
  143. package/src/prerender.ts +463 -0
  144. package/src/reverse.ts +330 -0
  145. package/src/root-error-boundary.tsx +41 -29
  146. package/src/route-content-wrapper.tsx +7 -4
  147. package/src/route-definition/dsl-helpers.ts +934 -0
  148. package/src/route-definition/helper-factories.ts +200 -0
  149. package/src/route-definition/helpers-types.ts +430 -0
  150. package/src/route-definition/index.ts +52 -0
  151. package/src/route-definition/redirect.ts +93 -0
  152. package/src/route-definition.ts +1 -1428
  153. package/src/route-map-builder.ts +211 -123
  154. package/src/route-name.ts +53 -0
  155. package/src/route-types.ts +59 -8
  156. package/src/router/content-negotiation.ts +116 -0
  157. package/src/router/debug-manifest.ts +72 -0
  158. package/src/router/error-handling.ts +9 -9
  159. package/src/router/find-match.ts +158 -0
  160. package/src/router/handler-context.ts +374 -81
  161. package/src/router/intercept-resolution.ts +395 -0
  162. package/src/router/lazy-includes.ts +234 -0
  163. package/src/router/loader-resolution.ts +215 -122
  164. package/src/router/logging.ts +248 -0
  165. package/src/router/manifest.ts +148 -35
  166. package/src/router/match-api.ts +620 -0
  167. package/src/router/match-context.ts +5 -3
  168. package/src/router/match-handlers.ts +440 -0
  169. package/src/router/match-middleware/background-revalidation.ts +80 -93
  170. package/src/router/match-middleware/cache-lookup.ts +382 -9
  171. package/src/router/match-middleware/cache-store.ts +51 -22
  172. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  173. package/src/router/match-middleware/segment-resolution.ts +24 -6
  174. package/src/router/match-pipelines.ts +10 -45
  175. package/src/router/match-result.ts +34 -28
  176. package/src/router/metrics.ts +235 -15
  177. package/src/router/middleware-cookies.ts +55 -0
  178. package/src/router/middleware-types.ts +222 -0
  179. package/src/router/middleware.ts +324 -367
  180. package/src/router/pattern-matching.ts +211 -43
  181. package/src/router/prerender-match.ts +402 -0
  182. package/src/router/preview-match.ts +170 -0
  183. package/src/router/revalidation.ts +137 -38
  184. package/src/router/router-context.ts +36 -21
  185. package/src/router/router-interfaces.ts +452 -0
  186. package/src/router/router-options.ts +592 -0
  187. package/src/router/router-registry.ts +24 -0
  188. package/src/router/segment-resolution/fresh.ts +570 -0
  189. package/src/router/segment-resolution/helpers.ts +263 -0
  190. package/src/router/segment-resolution/loader-cache.ts +198 -0
  191. package/src/router/segment-resolution/revalidation.ts +1241 -0
  192. package/src/router/segment-resolution/static-store.ts +67 -0
  193. package/src/router/segment-resolution.ts +21 -0
  194. package/src/router/segment-wrappers.ts +289 -0
  195. package/src/router/telemetry-otel.ts +299 -0
  196. package/src/router/telemetry.ts +300 -0
  197. package/src/router/timeout.ts +148 -0
  198. package/src/router/trie-matching.ts +239 -0
  199. package/src/router/types.ts +77 -3
  200. package/src/router.ts +692 -4257
  201. package/src/rsc/handler-context.ts +45 -0
  202. package/src/rsc/handler.ts +764 -754
  203. package/src/rsc/helpers.ts +140 -6
  204. package/src/rsc/index.ts +0 -20
  205. package/src/rsc/loader-fetch.ts +209 -0
  206. package/src/rsc/manifest-init.ts +86 -0
  207. package/src/rsc/nonce.ts +14 -0
  208. package/src/rsc/origin-guard.ts +141 -0
  209. package/src/rsc/progressive-enhancement.ts +379 -0
  210. package/src/rsc/response-error.ts +37 -0
  211. package/src/rsc/response-route-handler.ts +347 -0
  212. package/src/rsc/rsc-rendering.ts +235 -0
  213. package/src/rsc/runtime-warnings.ts +42 -0
  214. package/src/rsc/server-action.ts +348 -0
  215. package/src/rsc/ssr-setup.ts +128 -0
  216. package/src/rsc/types.ts +38 -11
  217. package/src/search-params.ts +230 -0
  218. package/src/segment-system.tsx +25 -13
  219. package/src/server/context.ts +182 -51
  220. package/src/server/cookie-store.ts +190 -0
  221. package/src/server/fetchable-loader-store.ts +37 -0
  222. package/src/server/handle-store.ts +94 -15
  223. package/src/server/loader-registry.ts +15 -56
  224. package/src/server/request-context.ts +430 -70
  225. package/src/server.ts +35 -130
  226. package/src/ssr/index.tsx +100 -31
  227. package/src/static-handler.ts +114 -0
  228. package/src/theme/ThemeProvider.tsx +21 -15
  229. package/src/theme/ThemeScript.tsx +5 -5
  230. package/src/theme/constants.ts +5 -2
  231. package/src/theme/index.ts +4 -14
  232. package/src/theme/theme-context.ts +4 -30
  233. package/src/theme/theme-script.ts +21 -18
  234. package/src/types/boundaries.ts +158 -0
  235. package/src/types/cache-types.ts +198 -0
  236. package/src/types/error-types.ts +192 -0
  237. package/src/types/global-namespace.ts +100 -0
  238. package/src/types/handler-context.ts +687 -0
  239. package/src/types/index.ts +88 -0
  240. package/src/types/loader-types.ts +183 -0
  241. package/src/types/route-config.ts +170 -0
  242. package/src/types/route-entry.ts +102 -0
  243. package/src/types/segments.ts +148 -0
  244. package/src/types.ts +1 -1623
  245. package/src/urls/include-helper.ts +197 -0
  246. package/src/urls/index.ts +53 -0
  247. package/src/urls/path-helper-types.ts +339 -0
  248. package/src/urls/path-helper.ts +329 -0
  249. package/src/urls/pattern-types.ts +95 -0
  250. package/src/urls/response-types.ts +106 -0
  251. package/src/urls/type-extraction.ts +372 -0
  252. package/src/urls/urls-function.ts +98 -0
  253. package/src/urls.ts +1 -802
  254. package/src/use-loader.tsx +85 -77
  255. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  256. package/src/vite/discovery/discover-routers.ts +344 -0
  257. package/src/vite/discovery/prerender-collection.ts +385 -0
  258. package/src/vite/discovery/route-types-writer.ts +258 -0
  259. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  260. package/src/vite/discovery/state.ts +110 -0
  261. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  262. package/src/vite/index.ts +11 -1129
  263. package/src/vite/plugin-types.ts +131 -0
  264. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  265. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  266. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  267. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  268. package/src/vite/plugins/expose-id-utils.ts +287 -0
  269. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  270. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  271. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  272. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  273. package/src/vite/plugins/expose-ids/types.ts +45 -0
  274. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  275. package/src/vite/plugins/refresh-cmd.ts +65 -0
  276. package/src/vite/plugins/use-cache-transform.ts +323 -0
  277. package/src/vite/plugins/version-injector.ts +83 -0
  278. package/src/vite/plugins/version-plugin.ts +254 -0
  279. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  280. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  281. package/src/vite/rango.ts +510 -0
  282. package/src/vite/router-discovery.ts +785 -0
  283. package/src/vite/utils/ast-handler-extract.ts +517 -0
  284. package/src/vite/utils/banner.ts +36 -0
  285. package/src/vite/utils/bundle-analysis.ts +137 -0
  286. package/src/vite/utils/manifest-utils.ts +70 -0
  287. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  288. package/src/vite/utils/prerender-utils.ts +189 -0
  289. package/src/vite/utils/shared-utils.ts +169 -0
  290. package/CLAUDE.md +0 -43
  291. package/src/browser/lru-cache.ts +0 -69
  292. package/src/browser/request-controller.ts +0 -164
  293. package/src/cache/memory-store.ts +0 -253
  294. package/src/href-context.ts +0 -33
  295. package/src/href.ts +0 -255
  296. package/src/server/route-manifest-cache.ts +0 -173
  297. package/src/vite/expose-handle-id.ts +0 -209
  298. package/src/vite/expose-loader-id.ts +0 -426
  299. package/src/vite/expose-location-state-id.ts +0 -177
  300. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -74,8 +74,45 @@ path("/product/:slug", async (ctx) => {
74
74
 
75
75
  ```typescript
76
76
  path("/product/:slug", ProductPage, {
77
- name: "product", // Route name for href() and navigation
78
- })
77
+ name: "product", // Route name for href() and navigation
78
+ });
79
+ ```
80
+
81
+ ### Typed Search Params
82
+
83
+ Add a `search` schema to get typed `ctx.search`:
84
+
85
+ ```typescript
86
+ path("/search", SearchPage, {
87
+ name: "search",
88
+ search: { q: "string", page: "number?", sort: "string?" },
89
+ });
90
+ ```
91
+
92
+ Use `Handler<"name">` for typed search params (resolves from the generated route map automatically):
93
+
94
+ ```typescript
95
+ import type { Handler } from "@rangojs/router";
96
+
97
+ export const SearchPage: Handler<"search"> = (ctx) => {
98
+ // ctx.search is typed: { q: string; page?: number; sort?: string }
99
+ const { q, page, sort } = ctx.search;
100
+ // ctx.searchParams is always URLSearchParams
101
+ return <SearchResults q={q} page={page} sort={sort} />;
102
+ };
103
+ ```
104
+
105
+ Supported types: `"string"`, `"number"`, `"boolean"`, with `?` suffix for optional.
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
+
109
+ Use `RouteSearchParams<"name">` and `RouteParams<"name">` to extract types for props:
110
+
111
+ ```typescript
112
+ import type { RouteSearchParams, RouteParams } from "@rangojs/router";
113
+
114
+ type SP = RouteSearchParams<"search">; // { q: string; page?: number; sort?: string }
115
+ type P = RouteParams<"blogPost">; // { slug: string }
79
116
  ```
80
117
 
81
118
  ## Route Children
@@ -90,17 +127,193 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
90
127
  ])
91
128
  ```
92
129
 
130
+ ## Handler Data Ownership
131
+
132
+ When a route has children (orphan layouts, parallels), the handler executes
133
+ first. Use `ctx.set(key, value)` to share data with children, who read it
134
+ via `ctx.get(key)`. Caching wraps all segments together, so either all run
135
+ or none do.
136
+
137
+ ### Typed context variables with createVar
138
+
139
+ Use `createVar<T>()` to create a typed token for `ctx.set()`/`ctx.get()`.
140
+ The token is imported by both the handler (producer) and layout (consumer),
141
+ making the data contract explicit and compile-time verified:
142
+
143
+ ```typescript
144
+ import { createVar } from "@rangojs/router";
145
+ import { Outlet, ParallelOutlet } from "@rangojs/router/client";
146
+
147
+ // Typed token -- shared between handler and layout
148
+ interface DashboardData {
149
+ title: string;
150
+ stats: { views: number };
151
+ }
152
+ const Dashboard = createVar<DashboardData>();
153
+
154
+ path("/dashboard/:id", async (ctx) => {
155
+ const data = await fetchDashboard(ctx.params.id);
156
+ ctx.set(Dashboard, data); // type-checked
157
+ return <DashboardPage data={data} />;
158
+ }, { name: "dashboard" }, () => [
159
+ layout((ctx) => {
160
+ const data = ctx.get(Dashboard); // typed as DashboardData | undefined
161
+ return (
162
+ <div>
163
+ <h1>{data?.title}</h1>
164
+ <Outlet />
165
+ <ParallelOutlet name="@sidebar" />
166
+ </div>
167
+ );
168
+ }),
169
+ parallel({
170
+ "@sidebar": (ctx) => {
171
+ const data = ctx.get(Dashboard);
172
+ return <Sidebar stats={data?.stats} />;
173
+ },
174
+ }),
175
+ ])
176
+ ```
177
+
178
+ String keys still work (`ctx.set("key", value)` / `ctx.get("key")`), but
179
+ `createVar<T>()` is preferred for type safety.
180
+
181
+ Only route handlers and middleware can call `ctx.set()`. Layouts, parallels,
182
+ and intercepts can only read via `ctx.get()`.
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
+
225
+ ## Redirects
226
+
227
+ ### Basic redirect
228
+
229
+ ```typescript
230
+ import { redirect } from "@rangojs/router";
231
+
232
+ path("/old-page", () => redirect("/new-page"), { name: "oldPage" });
233
+ ```
234
+
235
+ ### Redirect with custom status
236
+
237
+ ```typescript
238
+ path("/moved", () => redirect("/new-location", 301), { name: "moved" });
239
+ ```
240
+
241
+ ### Redirect with location state
242
+
243
+ Carry typed state through redirects (e.g. flash messages):
244
+
245
+ ```typescript
246
+ import { redirect, createLocationState } from "@rangojs/router";
247
+
248
+ export const FlashMessage = createLocationState<{ text: string }>({
249
+ flash: true,
250
+ });
251
+
252
+ path(
253
+ "/save",
254
+ (ctx) => {
255
+ // ... save logic
256
+ return redirect("/dashboard", {
257
+ state: [FlashMessage({ text: "Item saved!" })],
258
+ });
259
+ },
260
+ { name: "save" },
261
+ );
262
+
263
+ // With custom status + state
264
+ path(
265
+ "/action",
266
+ (ctx) => {
267
+ return redirect("/target", {
268
+ status: 303,
269
+ state: [FlashMessage({ text: "Action complete" })],
270
+ });
271
+ },
272
+ { name: "action" },
273
+ );
274
+ ```
275
+
276
+ Read the state on the target page with `useLocationState(FlashMessage)`. The
277
+ `{ flash: true }` option makes it auto-clear. Without `{ flash: true }`,
278
+ state persists on back/forward. See `/hooks` for details.
279
+
280
+ ### ctx.setLocationState()
281
+
282
+ Attach location state to any server response (not just redirects):
283
+
284
+ ```typescript
285
+ path("/dashboard", (ctx) => {
286
+ ctx.setLocationState(ServerInfo({ data: "welcome" }));
287
+ return <Dashboard />;
288
+ }, { name: "dashboard" })
289
+ ```
290
+
291
+ State flows to the browser via the RSC payload and is merged into
292
+ `history.pushState()`. Only works for SPA (partial) navigations.
293
+
93
294
  ## Handler Context
94
295
 
95
296
  Every handler receives a context object:
96
297
 
97
298
  ```typescript
98
- interface HandlerContext<TParams = Record<string, string>> {
99
- params: TParams; // URL parameters
100
- request: Request; // Original request
101
- url: URL; // Parsed URL
102
- env: TEnv; // Environment (bindings + variables)
103
- use<T>(handle: Handle<T>): T; // Access handles
299
+ interface HandlerContext<TParams = {}, TEnv = DefaultEnv, TSearch = {}> {
300
+ params: TParams; // URL parameters
301
+ request: Request; // Original request
302
+ searchParams: URLSearchParams; // Query params (always URLSearchParams)
303
+ search: {} | ResolveSearchSchema<TSearch>; // Typed search params (from search schema)
304
+ url: URL; // Parsed URL
305
+ env: TEnv; // Environment (bindings + variables)
306
+ set(key: string, value: any): void; // Set context variable (untyped string key)
307
+ set<T>(contextVar: ContextVar<T>, value: T): void; // Set typed context variable
308
+ get(key: string): any; // Read context variable (untyped string key)
309
+ get<T>(contextVar: ContextVar<T>): T | undefined; // Read typed context variable
310
+ use<T>(handle: Handle<T>): T; // Access handles
311
+ reverse(
312
+ name: string,
313
+ params?: Record<string, string>,
314
+ search?: Record<string, unknown>,
315
+ ): string; // URL generation
316
+ setLocationState(entries: LocationStateEntry[]): void; // Attach state to response
104
317
  }
105
318
  ```
106
319
 
@@ -111,11 +324,11 @@ path("/product/:slug", (ctx) => {
111
324
  // Access URL params
112
325
  const { slug } = ctx.params;
113
326
 
114
- // Access query params
115
- const tab = ctx.url.searchParams.get("tab");
327
+ // Access query params (untyped - use search schema for typed access)
328
+ const tab = ctx.searchParams.get("tab");
116
329
 
117
- // Access environment
118
- const db = ctx.env.Bindings.DB;
330
+ // Access platform bindings
331
+ const db = ctx.env.DB;
119
332
 
120
333
  // Access handles
121
334
  const breadcrumbs = ctx.use(Breadcrumbs);
@@ -142,8 +355,7 @@ urls(({ path, layout }) => [
142
355
  ## Complete Example
143
356
 
144
357
  ```typescript
145
- import { urls } from "@rangojs/router";
146
- import { Breadcrumbs } from "./handles/breadcrumbs";
358
+ import { urls, Breadcrumbs } from "@rangojs/router";
147
359
 
148
360
  export const urlpatterns = urls(({ path, layout, loader, loading }) => [
149
361
  // Simple route
@@ -50,20 +50,22 @@ export const urlpatterns = urls(({ path, layout, loader, loading }) => [
50
50
  The `urls()` function provides a callback with all available DSL functions:
51
51
 
52
52
  ```typescript
53
- urls(({
54
- path, // Define a route
55
- layout, // Wrap routes in a layout
56
- parallel, // Define parallel routes (slots)
57
- loader, // Add data loader
58
- loading, // Add loading skeleton
59
- cache, // Configure caching
60
- middleware, // Add middleware
61
- revalidate, // Control revalidation
62
- intercept, // Intercept routes for modals
63
- when, // Conditional rendering
64
- }) => [
65
- // Route definitions here
66
- ]);
53
+ urls(
54
+ ({
55
+ path, // Define a route
56
+ layout, // Wrap routes in a layout
57
+ parallel, // Define parallel routes (slots)
58
+ loader, // Add data loader
59
+ loading, // Add loading skeleton
60
+ cache, // Configure caching
61
+ middleware, // Add middleware
62
+ revalidate, // Control revalidation
63
+ intercept, // Intercept routes for modals
64
+ when, // Conditional rendering
65
+ }) => [
66
+ // Route definitions here
67
+ ],
68
+ );
67
69
  ```
68
70
 
69
71
  ## Router Options
@@ -76,7 +78,7 @@ interface RSCRouterOptions<TEnv> {
76
78
  // Document component wrapping entire app
77
79
  document?: ComponentType<{ children: ReactNode }>;
78
80
 
79
- // Enable performance metrics
81
+ // Enable per-request performance timeline (console waterfall + Server-Timing header)
80
82
  debugPerformance?: boolean;
81
83
 
82
84
  // Default error boundary
@@ -97,11 +99,25 @@ interface RSCRouterOptions<TEnv> {
97
99
  // Theme configuration
98
100
  theme?: ThemeConfig | true;
99
101
 
102
+ // SSR options (streaming policy)
103
+ ssr?: SSROptions<TEnv>;
104
+
105
+ // Telemetry sink for structured lifecycle events
106
+ telemetry?: TelemetrySink;
107
+
100
108
  // Connection warmup (default: true)
101
109
  warmup?: boolean;
102
110
 
111
+ // Prefetch cache TTL in seconds (default: 300)
112
+ // Controls in-memory cache duration and Cache-Control max-age for prefetch responses.
113
+ // Set to false to disable prefetch caching.
114
+ prefetchCacheTTL?: number | false;
115
+
103
116
  // CSP nonce provider (for router.fetch)
104
- nonce?: (request: Request, env: TEnv) => string | true | Promise<string | true>;
117
+ nonce?: (
118
+ request: Request,
119
+ env: TEnv,
120
+ ) => string | true | Promise<string | true>;
105
121
 
106
122
  // RSC version string (for router.fetch)
107
123
  version?: string;
@@ -160,7 +176,7 @@ import { createRouter } from "@rangojs/router";
160
176
  import { Document } from "./document";
161
177
  import { urlpatterns } from "./urls";
162
178
 
163
- export const router = createRouter<AppEnv>({
179
+ export const router = createRouter<AppBindings>({
164
180
  document: Document,
165
181
  urls: urlpatterns,
166
182
  });
@@ -170,7 +186,7 @@ import { router } from "./router";
170
186
 
171
187
  export default {
172
188
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
173
- return router.fetch(request, { Bindings: env, Variables: {}, ctx });
189
+ return router.fetch(request, { env, ctx });
174
190
  },
175
191
  };
176
192
  ```
@@ -184,12 +200,12 @@ For per-request cache configuration (e.g., Cloudflare Workers with ExecutionCont
184
200
  import { createRouter } from "@rangojs/router";
185
201
  import { CFCacheStore } from "@rangojs/router/cache";
186
202
 
187
- export const router = createRouter<AppEnv>({
203
+ export const router = createRouter<AppBindings>({
188
204
  document: Document,
189
205
  urls: urlpatterns,
190
- // Cache config receives env with ctx for ExecutionContext access
191
- cache: (env) => ({
192
- store: new CFCacheStore({ ctx: env.ctx, defaults: { ttl: 60 } }),
206
+ // Cache config receives (env, ctx) separately
207
+ cache: (_env, ctx) => ({
208
+ store: new CFCacheStore({ ctx: ctx!, defaults: { ttl: 60 } }),
193
209
  }),
194
210
  });
195
211
 
@@ -198,7 +214,7 @@ import { router } from "./router";
198
214
 
199
215
  export default {
200
216
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
201
- return router.fetch(request, { Bindings: env, Variables: {}, ctx });
217
+ return router.fetch(request, { env, ctx });
202
218
  },
203
219
  };
204
220
  ```
@@ -286,10 +302,10 @@ export const shopPatterns = urls(({ path, layout }) => [
286
302
  ]);
287
303
 
288
304
  // src/urls.tsx
289
- import { urls, include } from "@rangojs/router";
305
+ import { urls } from "@rangojs/router";
290
306
  import { shopPatterns } from "./urls/shop";
291
307
 
292
- export const urlpatterns = urls(({ path }) => [
308
+ export const urlpatterns = urls(({ path, include }) => [
293
309
  path("/", HomePage, { name: "home" }),
294
310
  include("/shop", shopPatterns, { name: "shop" }),
295
311
  ]);
@@ -298,23 +314,29 @@ export const urlpatterns = urls(({ path }) => [
298
314
  ## Environment Types
299
315
 
300
316
  ```typescript
301
- import type { RouterEnv } from "@rangojs/router";
302
-
317
+ // Bindings passed as TEnv to createRouter<TEnv>()
303
318
  interface AppBindings {
304
319
  DB: D1Database;
305
320
  KV: KVNamespace;
306
321
  }
307
322
 
323
+ // Variables declared via module augmentation
308
324
  interface AppVariables {
309
325
  user?: { id: string; name: string };
310
326
  }
311
327
 
312
- type AppEnv = RouterEnv<AppBindings, AppVariables>;
313
-
314
- const router = createRouter<AppEnv>({
328
+ const router = createRouter<AppBindings>({
315
329
  document: Document,
316
330
  urls: urlpatterns,
317
331
  });
332
+
333
+ // Register types globally for implicit typing
334
+ declare global {
335
+ namespace RSCRouter {
336
+ interface Env extends AppBindings {}
337
+ interface Vars extends AppVariables {}
338
+ }
339
+ }
318
340
  ```
319
341
 
320
342
  ## Connection Warmup
@@ -344,3 +366,74 @@ const router = createRouter({
344
366
 
345
367
  The warmup request is relative to the current page path, so it works correctly
346
368
  with subpath deployments (reverse proxy, base path).
369
+
370
+ ## Telemetry
371
+
372
+ The router emits structured lifecycle events through a pluggable telemetry sink.
373
+ Zero overhead when not configured.
374
+
375
+ ```typescript
376
+ // Console sink for development
377
+ import { createRouter, createConsoleSink } from "@rangojs/router";
378
+
379
+ const router = createRouter({
380
+ document: Document,
381
+ urls: urlpatterns,
382
+ telemetry: createConsoleSink(),
383
+ });
384
+ ```
385
+
386
+ ```typescript
387
+ // OpenTelemetry for production
388
+ import { createRouter, createOTelSink } from "@rangojs/router";
389
+ import { trace } from "@opentelemetry/api";
390
+
391
+ const router = createRouter({
392
+ document: Document,
393
+ urls: urlpatterns,
394
+ telemetry: createOTelSink(trace.getTracer("my-app")),
395
+ });
396
+ ```
397
+
398
+ ```typescript
399
+ // Custom sink
400
+ const router = createRouter({
401
+ telemetry: {
402
+ emit(event) {
403
+ // Send to any observability backend
404
+ myTracer.record(event);
405
+ },
406
+ },
407
+ });
408
+ ```
409
+
410
+ Events emitted: `request.start/end/error`, `loader.start/end/error`,
411
+ `handler.error`, `cache.decision`, `revalidation.decision`.
412
+
413
+ ## SSR Streaming Policy
414
+
415
+ Control whether HTML SSR responses stream progressively or wait for all content:
416
+
417
+ ```typescript
418
+ import { createRouter, type SSRStreamMode } from "@rangojs/router";
419
+
420
+ const router = createRouter({
421
+ ssr: {
422
+ resolveStreaming: ({ request }) => {
423
+ const ua = request.headers.get("user-agent") ?? "";
424
+ // Bots that can't process streamed HTML get a fully resolved page
425
+ if (/Googlebot|bingbot/i.test(ua)) return "allReady";
426
+ return "stream";
427
+ },
428
+ },
429
+ });
430
+ ```
431
+
432
+ `SSRStreamMode` is `"stream" | "allReady"`:
433
+
434
+ - `"stream"` (default) — flush HTML as React renders. Suspense fallbacks appear first, then resolved content streams in. Best for real users (fastest TTFB).
435
+ - `"allReady"` — await `stream.allReady` before flushing. The full page arrives in one shot. Use for bots that cannot execute JavaScript or process chunked HTML.
436
+
437
+ 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.
438
+
439
+ When `resolveStreaming` is not configured, the default is `"stream"`.
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: tailwind
3
+ description: Set up Tailwind CSS v4 with the Document component and CSS imports
4
+ argument-hint: [setup]
5
+ ---
6
+
7
+ # Tailwind CSS
8
+
9
+ Set up Tailwind CSS v4 with the Rango router. Styles are loaded through the Document component using Vite's `?url` CSS import.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pnpm add -D tailwindcss @tailwindcss/vite
15
+ ```
16
+
17
+ ## Vite Plugin
18
+
19
+ ```typescript
20
+ // vite.config.ts
21
+ import tailwindcss from "@tailwindcss/vite";
22
+
23
+ export default defineConfig({
24
+ plugins: [
25
+ tailwindcss(),
26
+ // ... other plugins
27
+ ],
28
+ });
29
+ ```
30
+
31
+ ## CSS Entry Point
32
+
33
+ ```css
34
+ /* src/index.css */
35
+ @import "tailwindcss";
36
+ ```
37
+
38
+ ## Document Component
39
+
40
+ Import the CSS file with `?url` to get a hashed URL, then preload and link it in `<head>`:
41
+
42
+ ```tsx
43
+ // src/document.tsx
44
+ "use client";
45
+
46
+ import type { ReactNode } from "react";
47
+ import { MetaTags } from "@rangojs/router/client";
48
+ import styles from "./index.css?url";
49
+
50
+ export function Document({ children }: { children: ReactNode }) {
51
+ return (
52
+ <html lang="en">
53
+ <head>
54
+ <link rel="preload" href={styles} as="style" />
55
+ <link rel="stylesheet" href={styles} />
56
+ <MetaTags />
57
+ </head>
58
+ <body className="font-sans antialiased text-slate-900 bg-slate-50">
59
+ {children}
60
+ </body>
61
+ </html>
62
+ );
63
+ }
64
+ ```
65
+
66
+ The `?url` suffix tells Vite to return the processed CSS file's URL instead of injecting it as a side effect. This gives you a stable, hashed asset path that works in both development and production.
67
+
68
+ ## Customizing the Theme
69
+
70
+ Tailwind v4 uses CSS `@theme` for customization:
71
+
72
+ ```css
73
+ /* src/index.css */
74
+ @import "tailwindcss";
75
+
76
+ @theme {
77
+ --font-sans: "Inter", system-ui, sans-serif;
78
+ --color-primary: #3b82f6;
79
+ --color-secondary: #64748b;
80
+ --breakpoint-3xl: 1920px;
81
+ }
82
+ ```
83
+
84
+ ## Dark Mode
85
+
86
+ Combine with the Rango theme system (see `/theme`):
87
+
88
+ ```typescript
89
+ const router = createRouter({
90
+ document: Document,
91
+ urls: urlpatterns,
92
+ theme: { attribute: "class" },
93
+ });
94
+ ```
95
+
96
+ Then use Tailwind's `dark:` variant which reads the `class` attribute:
97
+
98
+ ```tsx
99
+ <div className="bg-white dark:bg-slate-900 text-slate-900 dark:text-white">
100
+ Content
101
+ </div>
102
+ ```
103
+
104
+ ## With Custom Fonts
105
+
106
+ Use `@fontsource-variable` for self-hosted fonts bundled by Vite (see `/fonts` for all options):
107
+
108
+ ```bash
109
+ pnpm add @fontsource-variable/inter
110
+ ```
111
+
112
+ ```css
113
+ /* src/index.css */
114
+ @import "@fontsource-variable/inter";
115
+ @import "tailwindcss";
116
+
117
+ @theme {
118
+ --font-sans: "Inter Variable", system-ui, sans-serif;
119
+ }
120
+ ```
121
+
122
+ No extra `<link>` tags needed in the Document -- Vite bundles the font files from `node_modules` automatically.
123
+
124
+ ## Notes
125
+
126
+ - `?url` import is required -- bare CSS imports inject styles as a side effect and do not work with SSR streaming
127
+ - `<link rel="preload" as="style">` eliminates render-blocking by starting the download early
128
+ - Tailwind v4 does not need a `tailwind.config.js` -- use `@theme` in CSS instead
129
+ - The `@tailwindcss/vite` plugin handles content detection automatically
@@ -25,31 +25,32 @@ const router = createRouter<Env>({
25
25
  document: Document,
26
26
  urls: urlpatterns,
27
27
  theme: {
28
- defaultTheme: "system", // "light" | "dark" | "system"
28
+ defaultTheme: "system", // "light" | "dark" | "system"
29
29
  themes: ["light", "dark"],
30
- attribute: "class", // or "data-theme"
30
+ attribute: "class", // or "data-theme"
31
31
  storageKey: "theme",
32
- }
32
+ },
33
33
  });
34
34
  ```
35
35
 
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
- const currentTheme = ctx.theme; // read from cookie
43
+ export const SettingsLoader = createLoader(async (ctx) => {
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