@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.13221847

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 (298) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1531 -212
  4. package/dist/vite/index.js +3995 -2489
  5. package/package.json +57 -52
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +85 -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 +6 -4
  13. package/skills/hooks/SKILL.md +328 -70
  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 +62 -15
  18. package/skills/loader/SKILL.md +368 -42
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +14 -10
  21. package/skills/parallel/SKILL.md +137 -1
  22. package/skills/prerender/SKILL.md +366 -28
  23. package/skills/rango/SKILL.md +85 -21
  24. package/skills/response-routes/SKILL.md +136 -83
  25. package/skills/route/SKILL.md +195 -21
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/theme/SKILL.md +9 -8
  28. package/skills/typesafety/SKILL.md +240 -102
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +102 -4
  31. package/src/bin/rango.ts +312 -15
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +92 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +24 -4
  38. package/src/browser/logging.ts +11 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +266 -558
  41. package/src/browser/navigation-client.ts +132 -75
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +297 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +303 -309
  46. package/src/browser/prefetch/cache.ts +206 -0
  47. package/src/browser/prefetch/fetch.ts +144 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +48 -0
  50. package/src/browser/prefetch/queue.ts +128 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +190 -70
  53. package/src/browser/react/NavigationProvider.tsx +78 -11
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +6 -1
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +29 -70
  65. package/src/browser/react/use-link-status.ts +6 -5
  66. package/src/browser/react/use-navigation.ts +22 -63
  67. package/src/browser/react/use-params.ts +65 -0
  68. package/src/browser/react/use-pathname.ts +47 -0
  69. package/src/browser/react/use-router.ts +63 -0
  70. package/src/browser/react/use-search-params.ts +56 -0
  71. package/src/browser/react/use-segments.ts +80 -97
  72. package/src/browser/response-adapter.ts +73 -0
  73. package/src/browser/rsc-router.tsx +188 -57
  74. package/src/browser/scroll-restoration.ts +117 -44
  75. package/src/browser/segment-reconciler.ts +221 -0
  76. package/src/browser/segment-structure-assert.ts +16 -0
  77. package/src/browser/server-action-bridge.ts +488 -606
  78. package/src/browser/shallow.ts +6 -1
  79. package/src/browser/types.ts +116 -47
  80. package/src/browser/validate-redirect-origin.ts +29 -0
  81. package/src/build/generate-manifest.ts +63 -21
  82. package/src/build/generate-route-types.ts +36 -1038
  83. package/src/build/index.ts +2 -5
  84. package/src/build/route-trie.ts +38 -12
  85. package/src/build/route-types/ast-helpers.ts +25 -0
  86. package/src/build/route-types/ast-route-extraction.ts +98 -0
  87. package/src/build/route-types/codegen.ts +102 -0
  88. package/src/build/route-types/include-resolution.ts +411 -0
  89. package/src/build/route-types/param-extraction.ts +48 -0
  90. package/src/build/route-types/per-module-writer.ts +128 -0
  91. package/src/build/route-types/router-processing.ts +479 -0
  92. package/src/build/route-types/scan-filter.ts +78 -0
  93. package/src/build/runtime-discovery.ts +231 -0
  94. package/src/cache/background-task.ts +34 -0
  95. package/src/cache/cache-key-utils.ts +44 -0
  96. package/src/cache/cache-policy.ts +125 -0
  97. package/src/cache/cache-runtime.ts +342 -0
  98. package/src/cache/cache-scope.ts +122 -303
  99. package/src/cache/cf/cf-cache-store.ts +571 -17
  100. package/src/cache/cf/index.ts +13 -3
  101. package/src/cache/document-cache.ts +116 -77
  102. package/src/cache/handle-capture.ts +81 -0
  103. package/src/cache/handle-snapshot.ts +41 -0
  104. package/src/cache/index.ts +1 -15
  105. package/src/cache/memory-segment-store.ts +191 -13
  106. package/src/cache/profile-registry.ts +73 -0
  107. package/src/cache/read-through-swr.ts +134 -0
  108. package/src/cache/segment-codec.ts +256 -0
  109. package/src/cache/taint.ts +98 -0
  110. package/src/cache/types.ts +72 -122
  111. package/src/client.rsc.tsx +3 -1
  112. package/src/client.tsx +84 -126
  113. package/src/component-utils.ts +4 -4
  114. package/src/components/DefaultDocument.tsx +5 -1
  115. package/src/context-var.ts +86 -0
  116. package/src/debug.ts +19 -9
  117. package/src/errors.ts +77 -7
  118. package/src/handle.ts +12 -7
  119. package/src/handles/MetaTags.tsx +73 -20
  120. package/src/handles/breadcrumbs.ts +66 -0
  121. package/src/handles/index.ts +1 -0
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +21 -15
  124. package/src/host/errors.ts +8 -8
  125. package/src/host/index.ts +4 -7
  126. package/src/host/pattern-matcher.ts +27 -27
  127. package/src/host/router.ts +61 -39
  128. package/src/host/testing.ts +8 -8
  129. package/src/host/types.ts +15 -7
  130. package/src/host/utils.ts +1 -1
  131. package/src/href-client.ts +65 -45
  132. package/src/index.rsc.ts +104 -40
  133. package/src/index.ts +122 -67
  134. package/src/internal-debug.ts +9 -3
  135. package/src/loader.rsc.ts +18 -93
  136. package/src/loader.ts +26 -9
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +4 -2
  140. package/src/prerender/store.ts +121 -17
  141. package/src/prerender.ts +325 -20
  142. package/src/reverse.ts +144 -124
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +7 -4
  145. package/src/route-definition/dsl-helpers.ts +959 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1450
  151. package/src/route-map-builder.ts +87 -133
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +41 -6
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +160 -0
  158. package/src/router/handler-context.ts +324 -116
  159. package/src/router/intercept-resolution.ts +11 -4
  160. package/src/router/lazy-includes.ts +237 -0
  161. package/src/router/loader-resolution.ts +179 -133
  162. package/src/router/logging.ts +112 -6
  163. package/src/router/manifest.ts +58 -19
  164. package/src/router/match-api.ts +89 -88
  165. package/src/router/match-context.ts +4 -2
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +86 -89
  168. package/src/router/match-middleware/cache-lookup.ts +295 -49
  169. package/src/router/match-middleware/cache-store.ts +56 -13
  170. package/src/router/match-middleware/intercept-resolution.ts +45 -22
  171. package/src/router/match-middleware/segment-resolution.ts +20 -9
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +44 -21
  174. package/src/router/metrics.ts +240 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +327 -369
  178. package/src/router/pattern-matching.ts +169 -31
  179. package/src/router/prerender-match.ts +402 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +105 -14
  182. package/src/router/router-context.ts +40 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +677 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +199 -0
  189. package/src/router/segment-resolution/revalidation.ts +1296 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -1354
  192. package/src/router/segment-wrappers.ts +291 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +96 -29
  197. package/src/router/types.ts +15 -9
  198. package/src/router.ts +642 -2366
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +639 -1027
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +0 -20
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +237 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +38 -11
  215. package/src/search-params.ts +66 -54
  216. package/src/segment-system.tsx +165 -17
  217. package/src/server/context.ts +237 -54
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +11 -6
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +438 -71
  223. package/src/server.ts +26 -164
  224. package/src/ssr/index.tsx +101 -31
  225. package/src/static-handler.ts +22 -4
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +773 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +109 -0
  241. package/src/types/segments.ts +150 -0
  242. package/src/types.ts +1 -1795
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -1323
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +108 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -2259
  261. package/src/vite/plugin-types.ts +48 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -47
  266. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +8 -43
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +266 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +445 -0
  280. package/src/vite/router-discovery.ts +777 -0
  281. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +181 -9
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -43
  289. package/dist/vite/index.named-routes.gen.ts +0 -103
  290. package/src/browser/lru-cache.ts +0 -69
  291. package/src/browser/request-controller.ts +0 -164
  292. package/src/cache/memory-store.ts +0 -253
  293. package/src/href-context.ts +0 -33
  294. package/src/router.gen.ts +0 -6
  295. package/src/static-handler.gen.ts +0 -5
  296. package/src/urls.gen.ts +0 -8
  297. package/src/vite/expose-internal-ids.ts +0 -1167
  298. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -22,43 +22,62 @@ export const urlpatterns = urls(({ path, layout, include }) => [
22
22
  path("/about", AboutPage, { name: "about" }),
23
23
 
24
24
  // JSON API route (inline, alongside RSC routes)
25
- path.json("/api/status", (ctx) => ({
26
- status: "ok",
27
- timestamp: Date.now(),
28
- }), { name: "status" }),
25
+ path.json(
26
+ "/api/status",
27
+ (ctx) => ({
28
+ status: "ok",
29
+ timestamp: Date.now(),
30
+ }),
31
+ { name: "status" },
32
+ ),
29
33
 
30
34
  // Text route
31
- path.text("/robots.txt", (ctx) => {
32
- return "User-agent: *\nAllow: /\nDisallow: /api/\n";
33
- }, { name: "robots" }),
35
+ path.text(
36
+ "/robots.txt",
37
+ (ctx) => {
38
+ return "User-agent: *\nAllow: /\nDisallow: /api/\n";
39
+ },
40
+ { name: "robots" },
41
+ ),
34
42
 
35
43
  // Markdown route
36
- path.md("/docs/:slug.md", (ctx) => {
37
- return `# ${ctx.params.slug}\n\nDocumentation content here.`;
38
- }, { name: "docs" }),
44
+ path.md(
45
+ "/docs/:slug.md",
46
+ (ctx) => {
47
+ return `# ${ctx.params.slug}\n\nDocumentation content here.`;
48
+ },
49
+ { name: "docs" },
50
+ ),
39
51
 
40
52
  // Response route (full control, returns Response directly)
41
- path.image("/og/:slug.png", async (ctx) => {
42
- const image = await generateOgImage(ctx.params.slug);
43
- return new Response(image, {
44
- headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=86400" },
45
- });
46
- }, { name: "ogImage" }),
53
+ path.image(
54
+ "/og/:slug.png",
55
+ async (ctx) => {
56
+ const image = await generateOgImage(ctx.params.slug);
57
+ return new Response(image, {
58
+ headers: {
59
+ "Content-Type": "image/png",
60
+ "Cache-Control": "public, max-age=86400",
61
+ },
62
+ });
63
+ },
64
+ { name: "ogImage" },
65
+ ),
47
66
  ]);
48
67
  ```
49
68
 
50
69
  ## Available Tags
51
70
 
52
- | Tag | Usage | Handler returns | Auto-wrap |
53
- |-----|-------|-----------------|-----------|
54
- | `json` | `path.json()` | plain object/array | `{ data: T }` envelope |
55
- | `text` | `path.text()` | string | text/plain Response |
56
- | `html` | `path.html()` | string | text/html Response |
57
- | `xml` | `path.xml()` | string | application/xml Response |
58
- | `md` | `path.md()` | string | text/markdown Response |
59
- | `image` | `path.image()` | Response | pass-through |
60
- | `stream` | `path.stream()` | Response | pass-through |
61
- | `any` | `path.any()` | Response | pass-through |
71
+ | Tag | Usage | Handler returns | Auto-wrap |
72
+ | -------- | --------------- | ------------------ | ------------------------ |
73
+ | `json` | `path.json()` | plain object/array | `{ data: T }` envelope |
74
+ | `text` | `path.text()` | string | text/plain Response |
75
+ | `html` | `path.html()` | string | text/html Response |
76
+ | `xml` | `path.xml()` | string | application/xml Response |
77
+ | `md` | `path.md()` | string | text/markdown Response |
78
+ | `image` | `path.image()` | Response | pass-through |
79
+ | `stream` | `path.stream()` | Response | pass-through |
80
+ | `any` | `path.any()` | Response | pass-through |
62
81
 
63
82
  ## ResponseHandlerContext
64
83
 
@@ -67,14 +86,15 @@ Response route handlers receive a lighter context (no `ctx.use()`, no `ctx.res`)
67
86
  ```typescript
68
87
  interface ResponseHandlerContext<TParams, TEnv> {
69
88
  request: Request;
70
- params: TParams; // Typed from URL pattern
71
- env: Bindings; // Extracted from RouterEnv (DB, KV, etc.)
89
+ params: TParams; // Typed from URL pattern
90
+ env: TEnv; // Plain bindings (DB, KV, etc.)
72
91
  searchParams: URLSearchParams;
73
92
  url: URL;
74
93
  pathname: string;
75
- href: (name: string, params?: Record<string, string>) => string;
94
+ reverse: (name: string, params?: Record<string, string>) => string;
95
+ get: GetVariableFn; // Read middleware variables
76
96
  header: (name: string, value: string) => void;
77
- setCookie: (name: string, value: string, options?: CookieOptions) => void;
97
+ // Use cookies().set(name, value, opts) for cookie mutations (standalone API)
78
98
  }
79
99
  ```
80
100
 
@@ -84,31 +104,39 @@ String-returning handlers (json, text, html, xml, md) can set custom headers and
84
104
  without constructing a full Response:
85
105
 
86
106
  ```typescript
87
- path.md("/docs/:slug.md", (ctx) => {
88
- ctx.header("Cache-Control", "public, max-age=3600");
89
- ctx.setCookie("last-doc", ctx.params.slug, { path: "/" });
90
- return `# ${ctx.params.slug}\n\nContent here.`;
91
- }, { name: "docs" });
107
+ path.md(
108
+ "/docs/:slug.md",
109
+ (ctx) => {
110
+ ctx.header("Cache-Control", "public, max-age=3600");
111
+ cookies().set("last-doc", ctx.params.slug, { path: "/" });
112
+ return `# ${ctx.params.slug}\n\nContent here.`;
113
+ },
114
+ { name: "docs" },
115
+ );
92
116
  ```
93
117
 
94
- 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
95
119
  auto-wrapped Response. If the handler returns a `Response` directly, these are ignored
96
120
  (use the Response headers instead).
97
121
 
98
- ### Environment Type Extraction
122
+ ### Environment Access
99
123
 
100
- `env` extracts bindings from `RouterEnv`, not the full env:
124
+ `ctx.env` is always the plain bindings passed as TEnv to `createRouter<TEnv>()`:
101
125
 
102
126
  ```typescript
103
- type AppEnv = RouterEnv<{ DB: D1Database; KV: KVNamespace }, { user: User }>;
127
+ // createRouter<{ DB: D1Database; KV: KVNamespace }>({ ... })
104
128
 
105
129
  // In a response handler:
106
- path.json("/api/data", (ctx) => {
107
- ctx.env.DB; // D1Database (bindings extracted)
108
- ctx.env.KV; // KVNamespace
109
- // ctx.env.user -- NOT available (variables are not on response ctx.env)
110
- return { data: "ok" };
111
- }, { name: "data" });
130
+ path.json(
131
+ "/api/data",
132
+ (ctx) => {
133
+ ctx.env.DB; // D1Database (plain bindings)
134
+ ctx.env.KV; // KVNamespace
135
+ // Variables are accessed via ctx.get("key") or ctx.get(ContextVar)
136
+ return { data: "ok" };
137
+ },
138
+ { name: "data" },
139
+ );
112
140
  ```
113
141
 
114
142
  ## JSON Envelope
@@ -131,16 +159,22 @@ Throw `RouterError` to return structured error envelopes:
131
159
  ```typescript
132
160
  import { RouterError } from "@rangojs/router";
133
161
 
134
- path.json("/api/users/:id", (ctx) => {
135
- const user = users.get(ctx.params.id);
136
- if (!user) {
137
- throw new RouterError("NOT_FOUND", `User ${ctx.params.id} not found`, { status: 404 });
138
- }
139
- if (!hasPermission(ctx)) {
140
- throw new RouterError("FORBIDDEN", "Access denied", { status: 403 });
141
- }
142
- return user;
143
- }, { name: "user" });
162
+ path.json(
163
+ "/api/users/:id",
164
+ (ctx) => {
165
+ const user = users.get(ctx.params.id);
166
+ if (!user) {
167
+ throw new RouterError("NOT_FOUND", `User ${ctx.params.id} not found`, {
168
+ status: 404,
169
+ });
170
+ }
171
+ if (!hasPermission(ctx)) {
172
+ throw new RouterError("FORBIDDEN", "Access denied", { status: 403 });
173
+ }
174
+ return user;
175
+ },
176
+ { name: "user" },
177
+ );
144
178
  ```
145
179
 
146
180
  ### Returning Response Directly
@@ -148,15 +182,19 @@ path.json("/api/users/:id", (ctx) => {
148
182
  JSON handlers can return `Response` to bypass auto-wrap (custom status, headers, streaming):
149
183
 
150
184
  ```typescript
151
- path.json("/api/export", (ctx) => {
152
- const csv = generateCsv();
153
- return new Response(csv, {
154
- headers: {
155
- "Content-Type": "text/csv",
156
- "Content-Disposition": "attachment; filename=export.csv",
157
- },
158
- });
159
- }, { name: "export" });
185
+ path.json(
186
+ "/api/export",
187
+ (ctx) => {
188
+ const csv = generateCsv();
189
+ return new Response(csv, {
190
+ headers: {
191
+ "Content-Type": "text/csv",
192
+ "Content-Disposition": "attachment; filename=export.csv",
193
+ },
194
+ });
195
+ },
196
+ { name: "export" },
197
+ );
160
198
  ```
161
199
 
162
200
  ## Client-Side Type Safety
@@ -272,18 +310,29 @@ A self-contained module with RSC pages + JSON APIs, mountable via `include()`:
272
310
  import { urls, RouterError } from "@rangojs/router";
273
311
 
274
312
  export const blogApiPatterns = urls(({ path }) => [
275
- path.json("/stats", (ctx) => ({
276
- views: 1200, visitors: 450,
277
- }), { name: "stats" }),
278
-
279
- path.json("/:slug/likes", (ctx) => ({
280
- slug: ctx.params.slug,
281
- count: 42,
282
- }), { name: "likes" }),
283
-
284
- path.json("/:slug/comments", (ctx) => ([
285
- { id: "c1", body: "Great post", author: "alice" },
286
- ]), { name: "comments" }),
313
+ path.json(
314
+ "/stats",
315
+ (ctx) => ({
316
+ views: 1200,
317
+ visitors: 450,
318
+ }),
319
+ { name: "stats" },
320
+ ),
321
+
322
+ path.json(
323
+ "/:slug/likes",
324
+ (ctx) => ({
325
+ slug: ctx.params.slug,
326
+ count: 42,
327
+ }),
328
+ { name: "likes" },
329
+ ),
330
+
331
+ path.json(
332
+ "/:slug/comments",
333
+ (ctx) => [{ id: "c1", body: "Great post", author: "alice" }],
334
+ { name: "comments" },
335
+ ),
287
336
  ]);
288
337
 
289
338
  // blog/urls.tsx
@@ -333,13 +382,17 @@ Response route handlers inside a mounted module can reference local names:
333
382
 
334
383
  ```typescript
335
384
  // Inside blogApiPatterns handler
336
- path("/:slug/likes", (ctx) => {
337
- // ctx.reverse resolves names relative to the mount point
338
- const commentsUrl = ctx.reverse("comments", { slug: ctx.params.slug });
339
- // -> "/blog/api/my-post/comments"
340
-
341
- return { slug: ctx.params.slug, count: 42, commentsUrl };
342
- }, { name: "likes" });
385
+ path(
386
+ "/:slug/likes",
387
+ (ctx) => {
388
+ // ctx.reverse resolves names relative to the mount point
389
+ const commentsUrl = ctx.reverse("comments", { slug: ctx.params.slug });
390
+ // -> "/blog/api/my-post/comments"
391
+
392
+ return { slug: ctx.params.slug, count: 42, commentsUrl };
393
+ },
394
+ { name: "likes" },
395
+ );
343
396
  ```
344
397
 
345
398
  ## Content Negotiation
@@ -74,19 +74,19 @@ 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
79
  ```
80
80
 
81
81
  ### Typed Search Params
82
82
 
83
- Add a `search` schema to get typed `ctx.searchParams` instead of `URLSearchParams`:
83
+ Add a `search` schema to get typed `ctx.search`:
84
84
 
85
85
  ```typescript
86
86
  path("/search", SearchPage, {
87
87
  name: "search",
88
88
  search: { q: "string", page: "number?", sort: "string?" },
89
- })
89
+ });
90
90
  ```
91
91
 
92
92
  Use `Handler<"name">` for typed search params (resolves from the generated route map automatically):
@@ -95,23 +95,24 @@ Use `Handler<"name">` for typed search params (resolves from the generated route
95
95
  import type { Handler } from "@rangojs/router";
96
96
 
97
97
  export const SearchPage: Handler<"search"> = (ctx) => {
98
- // ctx.searchParams is typed: { q: string; page?: number; sort?: string }
99
- const { q, page, sort } = ctx.searchParams;
98
+ // ctx.search is typed: { q: string; page?: number; sort?: string }
99
+ const { q, page, sort } = ctx.search;
100
+ // ctx.searchParams is always URLSearchParams
100
101
  return <SearchResults q={q} page={page} sort={sort} />;
101
102
  };
102
103
  ```
103
104
 
104
105
  Supported types: `"string"`, `"number"`, `"boolean"`, with `?` suffix for optional.
105
- Required params default to zero values when missing (`""`, `0`, `false`).
106
- 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).
107
108
 
108
109
  Use `RouteSearchParams<"name">` and `RouteParams<"name">` to extract types for props:
109
110
 
110
111
  ```typescript
111
112
  import type { RouteSearchParams, RouteParams } from "@rangojs/router";
112
113
 
113
- type SP = RouteSearchParams<"search">; // { q: string; page?: number; sort?: string }
114
- type P = RouteParams<"blogPost">; // { slug: string }
114
+ type SP = RouteSearchParams<"search">; // { q: string; page?: number; sort?: string }
115
+ type P = RouteParams<"blogPost">; // { slug: string }
115
116
  ```
116
117
 
117
118
  ## Route Children
@@ -126,19 +127,193 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
126
127
  ])
127
128
  ```
128
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
+
129
294
  ## Handler Context
130
295
 
131
296
  Every handler receives a context object:
132
297
 
133
298
  ```typescript
134
299
  interface HandlerContext<TParams = {}, TEnv = DefaultEnv, TSearch = {}> {
135
- params: TParams; // URL parameters
136
- request: Request; // Original request
137
- searchParams: URLSearchParams | ResolveSearchSchema<TSearch>; // Query params (typed when search schema is set)
138
- url: URL; // Parsed URL
139
- env: TEnv; // Environment (bindings + variables)
140
- use<T>(handle: Handle<T>): T; // Access handles
141
- reverse(name: string, params?: Record<string, string>, search?: Record<string, unknown>): string; // URL generation
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
142
317
  }
143
318
  ```
144
319
 
@@ -152,8 +327,8 @@ path("/product/:slug", (ctx) => {
152
327
  // Access query params (untyped - use search schema for typed access)
153
328
  const tab = ctx.searchParams.get("tab");
154
329
 
155
- // Access environment
156
- const db = ctx.env.Bindings.DB;
330
+ // Access platform bindings
331
+ const db = ctx.env.DB;
157
332
 
158
333
  // Access handles
159
334
  const breadcrumbs = ctx.use(Breadcrumbs);
@@ -180,8 +355,7 @@ urls(({ path, layout }) => [
180
355
  ## Complete Example
181
356
 
182
357
  ```typescript
183
- import { urls } from "@rangojs/router";
184
- import { Breadcrumbs } from "./handles/breadcrumbs";
358
+ import { urls, Breadcrumbs } from "@rangojs/router";
185
359
 
186
360
  export const urlpatterns = urls(({ path, layout, loader, loading }) => [
187
361
  // Simple route