@rangojs/router 0.0.0-experimental.8 → 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 -867
  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 -1133
  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
@@ -5,57 +5,184 @@
5
5
  */
6
6
 
7
7
  import type { HandlerContext, InternalHandlerContext } from "../types";
8
- import { getRequestContext } from "../server/request-context.js";
8
+ import { _getRequestContext } from "../server/request-context.js";
9
+ import { getSearchSchema, isRouteRootScoped } from "../route-map-builder.js";
10
+ import { parseSearchParams, serializeSearchParams } from "../search-params.js";
11
+ import { contextGet, contextSet } from "../context-var.js";
12
+ import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
13
+ import { isAutoGeneratedRouteName } from "../route-name.js";
14
+ import { PRERENDER_PASSTHROUGH } from "../prerender.js";
15
+
16
+ /**
17
+ * Strip internal _rsc* query params from a URL.
18
+ * Returns a new URL with only user-facing params.
19
+ */
20
+ export function stripInternalParams(url: URL): URL {
21
+ const clean = new URL(url);
22
+ for (const key of [...clean.searchParams.keys()]) {
23
+ if (key.startsWith("_rsc")) {
24
+ clean.searchParams.delete(key);
25
+ }
26
+ }
27
+ return clean;
28
+ }
9
29
 
10
30
  /**
11
31
  * Resolve route name with namespace prefix support.
12
- * Supports local names, absolute names (dot notation), and path-based URLs.
32
+ * Supports local names (dot-prefixed) and absolute names (global lookup).
33
+ *
34
+ * @param rootScoped - Explicit override for root-scope check. When undefined,
35
+ * falls back to the global scope registry, then to a heuristic.
13
36
  */
14
37
  function resolveRouteName(
15
38
  name: string,
16
39
  routeMap: Record<string, string>,
17
- currentRoutePrefix?: string
40
+ currentRoutePrefix?: string,
41
+ rootScoped?: boolean,
18
42
  ): string | undefined {
19
- // 1. Path-based - starts with /
20
- if (name.startsWith("/")) {
21
- return name;
22
- }
23
-
24
- // 2. Absolute name - already has a dot (e.g., "shop.cart")
25
- if (name.includes(".")) {
26
- return routeMap[name];
27
- }
43
+ // 1. Dot-prefixed (".article", ".author.posts") local resolution only.
44
+ // Resolves within the current include() scope using the mount prefix.
45
+ if (name.startsWith(".")) {
46
+ const lookupName = name.slice(1);
47
+ if (!currentRoutePrefix) return undefined;
28
48
 
29
- // 3. Local name - try with current prefix first, then fall back to direct lookup
30
- if (currentRoutePrefix) {
31
- // Extract the prefix from current route name
32
- // e.g., "blog.posts.detail" → prefix is "blog.posts"
49
+ // Extract the include prefix from current route name
50
+ // e.g., "magazine.author" -> prefix is "magazine"
33
51
  const lastDot = currentRoutePrefix.lastIndexOf(".");
34
- const prefix = lastDot > 0 ? currentRoutePrefix.substring(0, lastDot) : currentRoutePrefix;
52
+ const prefix =
53
+ lastDot > 0
54
+ ? currentRoutePrefix.substring(0, lastDot)
55
+ : currentRoutePrefix;
35
56
 
36
- // Try prefixed name
37
- const prefixedName = `${prefix}.${name}`;
57
+ // Try prefixed name at current level
58
+ const prefixedName = `${prefix}.${lookupName}`;
38
59
  if (routeMap[prefixedName] !== undefined) {
39
60
  return routeMap[prefixedName];
40
61
  }
41
62
 
42
- // If current route is a nested include, try parent prefixes
43
- // e.g., for "blog.posts.detail", try "blog.posts.index", then "blog.index"
63
+ // Walk up parent prefixes for nested includes
44
64
  let currentPrefix = prefix;
45
65
  while (currentPrefix.includes(".")) {
46
66
  const parentDot = currentPrefix.lastIndexOf(".");
47
67
  currentPrefix = currentPrefix.substring(0, parentDot);
48
- const parentPrefixedName = `${currentPrefix}.${name}`;
68
+ const parentPrefixedName = `${currentPrefix}.${lookupName}`;
49
69
  if (routeMap[parentPrefixedName] !== undefined) {
50
70
  return routeMap[parentPrefixedName];
51
71
  }
52
72
  }
73
+
74
+ // Fallback: try bare name at root scope only.
75
+ // Routes inside { name: "" } mounts are at root scope — their dot-local
76
+ // names can fall back to bare names (e.g., "sub.detail" reaching "flatIndex").
77
+ // Routes inside named mounts (e.g., { name: "magazine" }) are NOT at root
78
+ // scope — dot-local must not leak into unrelated global names.
79
+ //
80
+ // Resolution order: explicit param > scope registry > heuristic.
81
+ const isRootScoped =
82
+ rootScoped ??
83
+ isRouteRootScoped(currentRoutePrefix) ??
84
+ !currentRoutePrefix.includes(".");
85
+ if (isRootScoped) {
86
+ if (routeMap[lookupName] !== undefined) {
87
+ return routeMap[lookupName];
88
+ }
89
+ }
90
+
91
+ return undefined;
53
92
  }
54
93
 
55
- // Fall back to direct lookup (route without prefix)
94
+ // 2. Unprefixed ("magazine.index", "blog.post") global resolution only.
95
+ // Direct lookup in the full named-routes map.
56
96
  return routeMap[name];
57
97
  }
58
98
 
99
+ function createPrerenderPassthroughFn(
100
+ build: boolean,
101
+ isPassthroughRoute: boolean,
102
+ ): () => typeof PRERENDER_PASSTHROUGH {
103
+ return () => {
104
+ if (!build) {
105
+ throw new Error(
106
+ "ctx.passthrough() can only be called during build-time prerendering.",
107
+ );
108
+ }
109
+ if (!isPassthroughRoute) {
110
+ throw new Error(
111
+ "ctx.passthrough() is only available on routes declared with " +
112
+ "{ passthrough: true }. Remove the passthrough() call or add " +
113
+ "{ passthrough: true } to the Prerender options.",
114
+ );
115
+ }
116
+ return PRERENDER_PASSTHROUGH;
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Create a reverse function for URL generation from route names.
122
+ * Used by both HandlerContext and MiddlewareContext.
123
+ *
124
+ * When currentParams is provided, those params are used as defaults for URL
125
+ * generation. This enables auto-filling mount params from include() prefixes:
126
+ * inner handlers can call ctx.reverse(".sibling") without explicitly passing
127
+ * params that are already known from the current URL match.
128
+ * Explicitly passed hrefParams take priority over currentParams.
129
+ */
130
+ export function createReverseFunction(
131
+ routeMap: Record<string, string>,
132
+ currentRoutePrefix?: string,
133
+ currentParams?: Record<string, string>,
134
+ rootScoped?: boolean,
135
+ ): (
136
+ name: string,
137
+ hrefParams?: Record<string, string>,
138
+ search?: Record<string, unknown>,
139
+ ) => string {
140
+ return (name, hrefParams, search) => {
141
+ // Resolve route name with namespace support
142
+ const pattern = resolveRouteName(
143
+ name,
144
+ routeMap,
145
+ currentRoutePrefix,
146
+ rootScoped,
147
+ );
148
+
149
+ if (pattern === undefined) {
150
+ throw new Error(
151
+ `Unknown route: "${name}"${currentRoutePrefix ? ` (current route: ${currentRoutePrefix})` : ""}`,
152
+ );
153
+ }
154
+
155
+ let result = pattern;
156
+
157
+ // Merge current request params as defaults, explicit params override
158
+ const effectiveParams = currentParams
159
+ ? { ...currentParams, ...hrefParams }
160
+ : hrefParams;
161
+
162
+ // Substitute params (strip constraint and optional syntax: :param(a|b)? -> value)
163
+ if (effectiveParams) {
164
+ result = result.replace(
165
+ /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\??/g,
166
+ (_, key) => {
167
+ const value = effectiveParams[key];
168
+ if (value === undefined) {
169
+ throw new Error(`Missing param "${key}" for route "${name}"`);
170
+ }
171
+ return encodeURIComponent(value);
172
+ },
173
+ );
174
+ }
175
+
176
+ // Append search params as query string
177
+ if (search) {
178
+ const qs = serializeSearchParams(search);
179
+ if (qs) result += `?${qs}`;
180
+ }
181
+
182
+ return result;
183
+ };
184
+ }
185
+
59
186
  /**
60
187
  * Create HandlerContext with typed env/var/get/set
61
188
  */
@@ -65,49 +192,69 @@ export function createHandlerContext<TEnv>(
65
192
  searchParams: URLSearchParams,
66
193
  pathname: string,
67
194
  url: URL,
68
- bindings: any = {},
195
+ bindings: TEnv = {} as TEnv,
69
196
  routeMap: Record<string, string> = {},
70
- routeName?: string
197
+ routeName?: string,
198
+ responseType?: string,
199
+ isPassthroughRoute: boolean = false,
71
200
  ): InternalHandlerContext<any, TEnv> {
72
201
  // Get variables from request context - this is the unified context
73
202
  // shared between middleware and route handlers
74
- const requestContext = getRequestContext();
203
+ const requestContext = _getRequestContext();
75
204
  const variables: any = requestContext?.var ?? {};
76
205
 
77
- // Filter system parameters (starting with _rsc) from searchParams
78
- // This ensures handlers only see user-facing query params
79
- const cleanSearchParams = new URLSearchParams();
80
- searchParams.forEach((value, key) => {
81
- if (!key.startsWith("_rsc")) {
82
- cleanSearchParams.set(key, value);
83
- }
84
- });
85
-
86
- // Create clean URL without system params
87
- const cleanUrl = new URL(url);
88
- cleanUrl.search = cleanSearchParams.toString();
206
+ // If route has a search schema, parse URLSearchParams into typed object
207
+ const searchSchema = routeName ? getSearchSchema(routeName) : undefined;
208
+ const resolvedSearchParams = searchSchema
209
+ ? parseSearchParams(searchParams, searchSchema)
210
+ : searchParams;
89
211
 
90
212
  // Get stub response from request context for setting headers
91
- const stubResponse = requestContext?.res ?? new Response(null, { status: 200 });
213
+ const stubResponse =
214
+ requestContext?.res ?? new Response(null, { status: 200 });
92
215
 
93
- return {
216
+ // Guard mutating Headers methods so they throw inside "use cache" functions.
217
+ // Uses lazy `ctx` reference (assigned below) — only the specific handler ctx
218
+ // is stamped by cache-runtime, not the shared request context.
219
+ const MUTATING_HEADERS_METHODS = new Set(["set", "append", "delete"]);
220
+ let ctx: InternalHandlerContext<any, TEnv>;
221
+ const guardedHeaders = new Proxy(stubResponse.headers, {
222
+ get(target, prop, receiver) {
223
+ const value = Reflect.get(target, prop, receiver);
224
+ if (typeof value === "function") {
225
+ if (MUTATING_HEADERS_METHODS.has(prop as string)) {
226
+ return (...args: any[]) => {
227
+ assertNotInsideCacheExec(ctx, "headers");
228
+ return value.apply(target, args);
229
+ };
230
+ }
231
+ return value.bind(target);
232
+ }
233
+ return value;
234
+ },
235
+ });
236
+
237
+ ctx = {
94
238
  params,
239
+ build: false,
95
240
  request,
96
- searchParams: cleanSearchParams, // Filtered params
241
+ searchParams,
242
+ search: searchSchema ? resolvedSearchParams : {},
97
243
  pathname,
98
- url: cleanUrl, // Clean URL
244
+ url,
245
+ originalUrl: new URL(request.url),
99
246
  env: bindings,
100
247
  var: variables,
101
- get: ((key: string) => variables[key]) as HandlerContext<
248
+ get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as HandlerContext<
102
249
  any,
103
250
  TEnv
104
251
  >["get"],
105
- set: ((key: string, value: any) => {
106
- variables[key] = value;
252
+ set: ((keyOrVar: any, value: any) => {
253
+ assertNotInsideCacheExec(ctx, "set");
254
+ contextSet(variables, keyOrVar, value);
107
255
  }) as HandlerContext<any, TEnv>["set"],
108
- _originalRequest: request, // Raw request for advanced use
109
256
  res: stubResponse, // Stub response for setting headers
110
- headers: stubResponse.headers, // Shorthand for res.headers
257
+ headers: guardedHeaders, // Guarded shorthand for res.headers
111
258
  // Placeholder use() - will be replaced with actual implementation during request
112
259
  use: () => {
113
260
  throw new Error("ctx.use() called before loaders were initialized");
@@ -115,44 +262,190 @@ export function createHandlerContext<TEnv>(
115
262
  // Theme support (when enabled via router config)
116
263
  theme: requestContext?.theme,
117
264
  setTheme: requestContext?.setTheme,
118
- // Scoped href for URL generation
119
- href: (name: string, hrefParams?: Record<string, string>) => {
120
- // Path-based - return directly (optionally with param substitution)
121
- if (name.startsWith("/")) {
122
- if (hrefParams) {
123
- return name.replace(/:([^/]+)/g, (_, key) => {
124
- const value = hrefParams[key];
125
- if (value === undefined) {
126
- throw new Error(`Missing param "${key}" for path "${name}"`);
127
- }
128
- return encodeURIComponent(value);
129
- });
130
- }
131
- return name;
132
- }
133
-
134
- // Resolve route name with namespace support
135
- const pattern = resolveRouteName(name, routeMap, routeName);
136
-
137
- if (pattern === undefined) {
265
+ // Location state support (delegates to request context)
266
+ setLocationState(entries) {
267
+ if (!requestContext) {
138
268
  throw new Error(
139
- `Unknown route: "${name}"${routeName ? ` (current route: ${routeName})` : ""}`
269
+ "setLocationState() is not available outside a request context",
140
270
  );
141
271
  }
272
+ requestContext.setLocationState(entries);
273
+ },
274
+ routeName: (routeName && !isAutoGeneratedRouteName(routeName)
275
+ ? routeName
276
+ : undefined) as HandlerContext["routeName"],
277
+ // Scoped reverse for URL generation (auto-fills current request params).
278
+ // Resolve rootScoped eagerly so the reverse function is self-contained
279
+ // and does not depend on the global rootScopeRoutes registry at call time.
280
+ reverse: createReverseFunction(
281
+ routeMap,
282
+ routeName,
283
+ params,
284
+ routeName ? isRouteRootScoped(routeName) : undefined,
285
+ ),
286
+ passthrough: createPrerenderPassthroughFn(false, isPassthroughRoute),
287
+ _responseType: responseType,
288
+ _routeName: routeName,
289
+ };
290
+ // Brand with taint symbol so "use cache" excludes ctx from cache keys
291
+ (ctx as any)[NOCACHE_SYMBOL] = true;
292
+ return ctx;
293
+ }
142
294
 
143
- // If no params, return pattern directly
144
- if (!hrefParams) {
145
- return pattern;
146
- }
295
+ /**
296
+ * Create a PrerenderContext for Prerender() handlers at build time.
297
+ *
298
+ * Returns an InternalHandlerContext where params, pathname, url, searchParams,
299
+ * search, reverse, and use(handle) work. Request-time properties
300
+ * (request, env, headers, cookies, var, get, set, res) throw with a clear error.
301
+ */
302
+ export function createPrerenderContext<TEnv>(
303
+ params: Record<string, string>,
304
+ pathname: string,
305
+ routeMap: Record<string, string>,
306
+ routeName?: string,
307
+ buildVars?: Record<string, any>,
308
+ isPassthroughRoute?: boolean,
309
+ ): InternalHandlerContext<any, TEnv> {
310
+ const syntheticUrl = new URL(`http://prerender${pathname}`);
311
+ const variables = buildVars ?? {};
147
312
 
148
- // Substitute params
149
- return pattern.replace(/:([^/]+)/g, (_, key) => {
150
- const value = hrefParams[key];
151
- if (value === undefined) {
152
- throw new Error(`Missing param "${key}" for route "${name}"`);
153
- }
154
- return encodeURIComponent(value);
155
- });
313
+ function throwUnavailable(prop: string): never {
314
+ throw new Error(
315
+ `Property "${prop}" is not available during pre-rendering. ` +
316
+ `Fetch data directly in the handler or use a passthrough prerender handler.`,
317
+ );
318
+ }
319
+
320
+ return {
321
+ params,
322
+ build: true,
323
+ get request(): Request {
324
+ return throwUnavailable("request");
156
325
  },
157
- };
326
+ searchParams: syntheticUrl.searchParams,
327
+ search: {},
328
+ pathname,
329
+ url: syntheticUrl,
330
+ originalUrl: syntheticUrl,
331
+ get env(): TEnv {
332
+ return throwUnavailable("env");
333
+ },
334
+ get var(): any {
335
+ return throwUnavailable("var");
336
+ },
337
+ get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
338
+ set: ((keyOrVar: any, value: any) => {
339
+ contextSet(variables, keyOrVar, value);
340
+ }) as any,
341
+ get res(): Response {
342
+ return throwUnavailable("res");
343
+ },
344
+ get headers(): Headers {
345
+ return throwUnavailable("headers");
346
+ },
347
+ // Placeholder use() - replaced by setupBuildUse
348
+ use: () => {
349
+ throw new Error("ctx.use() called before build context was initialized");
350
+ },
351
+ theme: undefined,
352
+ setTheme: undefined,
353
+ routeName: (routeName && !isAutoGeneratedRouteName(routeName)
354
+ ? routeName
355
+ : undefined) as HandlerContext["routeName"],
356
+ setLocationState: () => {
357
+ throwUnavailable("setLocationState");
358
+ },
359
+ reverse: createReverseFunction(
360
+ routeMap,
361
+ routeName,
362
+ params,
363
+ routeName ? isRouteRootScoped(routeName) : undefined,
364
+ ),
365
+ passthrough: createPrerenderPassthroughFn(
366
+ true,
367
+ isPassthroughRoute === true,
368
+ ),
369
+ _routeName: routeName,
370
+ } as InternalHandlerContext<any, TEnv>;
371
+ }
372
+
373
+ /**
374
+ * Create a StaticContext for Static() handlers at build time.
375
+ *
376
+ * Returns an InternalHandlerContext where only reverse and use(handle) work.
377
+ * Static handlers have no URL, no params, no pathname — everything else throws.
378
+ */
379
+ export function createStaticContext<TEnv>(
380
+ routeMap: Record<string, string>,
381
+ routeName?: string,
382
+ ): InternalHandlerContext<any, TEnv> {
383
+ const variables: Record<string, any> = {};
384
+
385
+ function throwUnavailable(prop: string): never {
386
+ throw new Error(
387
+ `Property "${prop}" is not available in Static() handlers. ` +
388
+ `Static handlers render content without request context.`,
389
+ );
390
+ }
391
+
392
+ return {
393
+ get params(): any {
394
+ return throwUnavailable("params");
395
+ },
396
+ build: true,
397
+ get request(): Request {
398
+ return throwUnavailable("request");
399
+ },
400
+ get searchParams(): URLSearchParams {
401
+ return throwUnavailable("searchParams");
402
+ },
403
+ get search(): any {
404
+ return throwUnavailable("search");
405
+ },
406
+ get pathname(): string {
407
+ return throwUnavailable("pathname");
408
+ },
409
+ get url(): URL {
410
+ return throwUnavailable("url");
411
+ },
412
+ get originalUrl(): URL {
413
+ return throwUnavailable("originalUrl");
414
+ },
415
+ get env(): TEnv {
416
+ return throwUnavailable("env");
417
+ },
418
+ get var(): any {
419
+ return throwUnavailable("var");
420
+ },
421
+ get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
422
+ set: ((keyOrVar: any, value: any) => {
423
+ contextSet(variables, keyOrVar, value);
424
+ }) as any,
425
+ get res(): Response {
426
+ return throwUnavailable("res");
427
+ },
428
+ get headers(): Headers {
429
+ return throwUnavailable("headers");
430
+ },
431
+ // Placeholder use() - replaced by setupBuildUse
432
+ use: () => {
433
+ throw new Error("ctx.use() called before build context was initialized");
434
+ },
435
+ theme: undefined,
436
+ setTheme: undefined,
437
+ routeName: (routeName && !isAutoGeneratedRouteName(routeName)
438
+ ? routeName
439
+ : undefined) as HandlerContext["routeName"],
440
+ setLocationState: () => {
441
+ throwUnavailable("setLocationState");
442
+ },
443
+ reverse: createReverseFunction(
444
+ routeMap,
445
+ routeName,
446
+ undefined,
447
+ routeName ? isRouteRootScoped(routeName) : undefined,
448
+ ),
449
+ _routeName: routeName,
450
+ } as InternalHandlerContext<any, TEnv>;
158
451
  }