@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
@@ -2,8 +2,9 @@
2
2
  * Search parameter schema types and runtime utilities.
3
3
  *
4
4
  * Provides a lightweight schema system for typed query parameters.
5
- * When a route defines a `search` schema, ctx.searchParams becomes
6
- * a typed object with parsed values instead of raw URLSearchParams.
5
+ * When a route defines a `search` schema, ctx.search provides a typed
6
+ * object with parsed values. ctx.searchParams always remains a standard
7
+ * URLSearchParams instance.
7
8
  */
8
9
 
9
10
  // ============================================================================
@@ -54,16 +55,26 @@ type Simplify<T> = { [K in keyof T]: T[K] };
54
55
  /**
55
56
  * Resolve a SearchSchema to its typed object.
56
57
  *
58
+ * Both required and optional params resolve to `T | undefined` at the handler
59
+ * level. The required/optional distinction is a consumer-facing contract
60
+ * (e.g., for href() and reverse() autocomplete) — it tells callers which
61
+ * params the route expects, but the handler must still check for undefined
62
+ * since the framework cannot trust the client to send all required params.
63
+ *
57
64
  * @example
58
65
  * type S = { q: "string"; page: "number?"; sort: "string?" };
59
66
  * type R = ResolveSearchSchema<S>;
60
- * // { q: string; page?: number; sort?: string }
67
+ * // { q: string | undefined; page?: number; sort?: string }
61
68
  */
62
- export type ResolveSearchSchema<T extends SearchSchema> = Simplify<{
63
- [K in RequiredKeys<T> & string]: ResolveBaseType<BaseType<T[K]>>;
64
- } & {
65
- [K in OptionalKeys<T> & string]?: ResolveBaseType<BaseType<T[K]>>;
66
- }>;
69
+ export type ResolveSearchSchema<T extends SearchSchema> = Simplify<
70
+ {
71
+ [K in RequiredKeys<T> & string]:
72
+ | ResolveBaseType<BaseType<T[K]>>
73
+ | undefined;
74
+ } & {
75
+ [K in OptionalKeys<T> & string]?: ResolveBaseType<BaseType<T[K]>>;
76
+ }
77
+ >;
67
78
 
68
79
  // ============================================================================
69
80
  // Route-Level Type Extraction
@@ -87,19 +98,17 @@ type GlobalRouteMap = keyof RSCRouter.RegisteredRoutes extends never
87
98
  * // { q: string; page?: number }
88
99
  * ```
89
100
  */
90
- export type RouteSearchParams<
91
- TName extends string,
92
- TRouteMap = never,
93
- > = [TRouteMap] extends [never]
101
+ export type RouteSearchParams<TName extends string, TRouteMap = never> = [
102
+ TRouteMap,
103
+ ] extends [never]
94
104
  ? ExtractAndResolveSearch<GlobalRouteMap, TName>
95
105
  : ExtractAndResolveSearch<TRouteMap, TName>;
96
106
 
97
- type ExtractAndResolveSearch<TRouteMap, TName> =
98
- TName extends keyof TRouteMap
99
- ? TRouteMap[TName] extends { readonly search: infer S extends SearchSchema }
100
- ? ResolveSearchSchema<S>
101
- : {}
102
- : {};
107
+ type ExtractAndResolveSearch<TRouteMap, TName> = TName extends keyof TRouteMap
108
+ ? TRouteMap[TName] extends { readonly search: infer S extends SearchSchema }
109
+ ? ResolveSearchSchema<S>
110
+ : {}
111
+ : {};
103
112
 
104
113
  /**
105
114
  * Extract the route params type for a named route.
@@ -112,36 +121,46 @@ type ExtractAndResolveSearch<TRouteMap, TName> =
112
121
  * // { slug: string }
113
122
  * ```
114
123
  */
115
- export type RouteParams<
116
- TName extends string,
117
- TRouteMap = never,
118
- > = [TRouteMap] extends [never]
124
+ export type RouteParams<TName extends string, TRouteMap = never> = [
125
+ TRouteMap,
126
+ ] extends [never]
119
127
  ? ExtractRouteParamsFromMap<GlobalRouteMap, TName>
120
128
  : ExtractRouteParamsFromMap<TRouteMap, TName>;
121
129
 
122
- type ExtractRouteParamsFromMap<TRouteMap, TName> =
123
- TName extends keyof TRouteMap
124
- ? TRouteMap[TName] extends string
125
- ? ExtractParamsFromPattern<TRouteMap[TName]>
126
- : TRouteMap[TName] extends { readonly path: infer P extends string }
127
- ? ExtractParamsFromPattern<P>
128
- : {}
129
- : {};
130
+ type ExtractRouteParamsFromMap<TRouteMap, TName> = TName extends keyof TRouteMap
131
+ ? TRouteMap[TName] extends string
132
+ ? ExtractParamsFromPattern<TRouteMap[TName]>
133
+ : TRouteMap[TName] extends { readonly path: infer P extends string }
134
+ ? ExtractParamsFromPattern<P>
135
+ : {}
136
+ : {};
137
+
138
+ /** Parse "a|b|c" into "a" | "b" | "c" */
139
+ type ParseConstraint<T extends string> =
140
+ T extends `${infer First}|${infer Rest}` ? First | ParseConstraint<Rest> : T;
130
141
 
131
142
  /** Minimal inline param extraction (avoids importing from types.ts to prevent circular deps). */
132
143
  type ExtractParamsFromPattern<T extends string> =
133
144
  T extends `${string}:${infer Param}/${infer Rest}`
134
- ? Param extends `${infer Name}?`
135
- ? { [K in Name]?: string } & ExtractParamsFromPattern<`/${Rest}`>
136
- : Param extends `${infer Name}(${string})`
137
- ? { [K in Name]: string } & ExtractParamsFromPattern<`/${Rest}`>
138
- : { [K in Param]: string } & ExtractParamsFromPattern<`/${Rest}`>
145
+ ? Param extends `${infer Name}(${infer C})?`
146
+ ? {
147
+ [K in Name]?: ParseConstraint<C>;
148
+ } & ExtractParamsFromPattern<`/${Rest}`>
149
+ : Param extends `${infer Name}(${infer C})`
150
+ ? {
151
+ [K in Name]: ParseConstraint<C>;
152
+ } & ExtractParamsFromPattern<`/${Rest}`>
153
+ : Param extends `${infer Name}?`
154
+ ? { [K in Name]?: string } & ExtractParamsFromPattern<`/${Rest}`>
155
+ : { [K in Param]: string } & ExtractParamsFromPattern<`/${Rest}`>
139
156
  : T extends `${string}:${infer Param}`
140
- ? Param extends `${infer Name}?`
141
- ? { [K in Name]?: string }
142
- : Param extends `${infer Name}(${string})`
143
- ? { [K in Name]: string }
144
- : { [K in Param]: string }
157
+ ? Param extends `${infer Name}(${infer C})?`
158
+ ? { [K in Name]?: ParseConstraint<C> }
159
+ : Param extends `${infer Name}(${infer C})`
160
+ ? { [K in Name]: ParseConstraint<C> }
161
+ : Param extends `${infer Name}?`
162
+ ? { [K in Name]?: string }
163
+ : { [K in Param]: string }
145
164
  : {};
146
165
 
147
166
  // ============================================================================
@@ -155,7 +174,9 @@ type ExtractParamsFromPattern<T extends string> =
155
174
  * - `"number"` / `"number?"` - coerced via `Number()`; NaN treated as missing
156
175
  * - `"boolean"` / `"boolean?"` - `"true"` / `"1"` -> true, `"false"` / `"0"` / `""` -> false
157
176
  *
158
- * Missing required params are set to their zero value (empty string / 0 / false).
177
+ * Missing params (both required and optional) are omitted from the result
178
+ * (undefined). The required/optional distinction is a consumer-facing contract
179
+ * only — the handler must check for undefined.
159
180
  */
160
181
  export function parseSearchParams<T extends SearchSchema>(
161
182
  searchParams: URLSearchParams,
@@ -169,13 +190,7 @@ export function parseSearchParams<T extends SearchSchema>(
169
190
  const raw = searchParams.get(key);
170
191
 
171
192
  if (raw === null) {
172
- if (!isOptional) {
173
- // Required param missing: use zero value
174
- if (baseType === "string") result[key] = "";
175
- else if (baseType === "number") result[key] = 0;
176
- else if (baseType === "boolean") result[key] = false;
177
- }
178
- // Optional params are omitted (undefined)
193
+ // Missing params are omitted (undefined) regardless of required/optional
179
194
  continue;
180
195
  }
181
196
 
@@ -183,11 +198,10 @@ export function parseSearchParams<T extends SearchSchema>(
183
198
  result[key] = raw;
184
199
  } else if (baseType === "number") {
185
200
  const num = Number(raw);
186
- if (Number.isNaN(num)) {
187
- if (!isOptional) result[key] = 0;
188
- } else {
201
+ if (!Number.isNaN(num)) {
189
202
  result[key] = num;
190
203
  }
204
+ // NaN treated as missing (undefined)
191
205
  } else if (baseType === "boolean") {
192
206
  result[key] = raw === "true" || raw === "1";
193
207
  }
@@ -204,9 +218,7 @@ export function parseSearchParams<T extends SearchSchema>(
204
218
  * Serialize a typed search params object to a query string (without leading `?`).
205
219
  * Skips `undefined` and `null` values.
206
220
  */
207
- export function serializeSearchParams(
208
- params: Record<string, unknown>,
209
- ): string {
221
+ export function serializeSearchParams(params: Record<string, unknown>): string {
210
222
  const parts: string[] = [];
211
223
  for (const [key, value] of Object.entries(params)) {
212
224
  if (value === undefined || value === null) continue;
@@ -1,3 +1,4 @@
1
+ import * as React from "react";
1
2
  import { createElement, type ReactNode, type ComponentType } from "react";
2
3
  import { OutletProvider } from "./client.js";
3
4
  import { MountContextProvider } from "./browser/react/mount-context.js";
@@ -14,6 +15,66 @@ import {
14
15
  } from "./route-content-wrapper.js";
15
16
  import { RootErrorBoundary } from "./root-error-boundary.js";
16
17
 
18
+ // ViewTransition is only available in React experimental.
19
+ // Access via namespace import to avoid compile-time errors on stable React.
20
+ const ReactViewTransition: any =
21
+ "ViewTransition" in React ? (React as any).ViewTransition : null;
22
+
23
+ function restoreParallelLoaderMarkers(
24
+ segments: ResolvedSegment[],
25
+ ): ResolvedSegment[] {
26
+ const parallelLoadingByNamespace = new Map<string, ReactNode>();
27
+ let nextSegments: ResolvedSegment[] | null = null;
28
+
29
+ for (let i = 0; i < segments.length; i++) {
30
+ const segment = segments[i];
31
+
32
+ if (segment.type === "parallel") {
33
+ if (
34
+ segment.namespace &&
35
+ segment.loading !== undefined &&
36
+ segment.loading !== null &&
37
+ segment.loading !== false
38
+ ) {
39
+ parallelLoadingByNamespace.set(segment.namespace, segment.loading);
40
+ }
41
+ continue;
42
+ }
43
+
44
+ if (segment.type !== "loader" || segment.parallelLoading !== undefined) {
45
+ continue;
46
+ }
47
+
48
+ const parallelLoading = segment.namespace
49
+ ? parallelLoadingByNamespace.get(segment.namespace)
50
+ : undefined;
51
+ if (parallelLoading === undefined) {
52
+ continue;
53
+ }
54
+
55
+ if (!nextSegments) {
56
+ nextSegments = segments.slice();
57
+ }
58
+ nextSegments[i] = { ...segment, parallelLoading };
59
+ }
60
+
61
+ return nextSegments ?? segments;
62
+ }
63
+
64
+ function hasSameReferences(a: unknown[] | undefined, b: unknown[]): boolean {
65
+ if (!a || a.length !== b.length) {
66
+ return false;
67
+ }
68
+
69
+ for (let i = 0; i < a.length; i++) {
70
+ if (a[i] !== b[i]) {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ return true;
76
+ }
77
+
17
78
  /**
18
79
  * Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
19
80
  */
@@ -137,6 +198,10 @@ export async function renderSegments(
137
198
  } = options || {};
138
199
 
139
200
  const temporalLazyRefs: Promise<any>[] = [];
201
+ const normalizedSegments = restoreParallelLoaderMarkers(segments);
202
+ const normalizedInterceptSegments = interceptSegments
203
+ ? restoreParallelLoaderMarkers(interceptSegments)
204
+ : undefined;
140
205
 
141
206
  /**
142
207
  * Registers promises from lazy/async components for awaiting.
@@ -161,7 +226,7 @@ export async function renderSegments(
161
226
  );
162
227
  }
163
228
  // Separate segments by type, passing intercept segments for explicit injection
164
- const tree = segmentTreeWalk(segments, interceptSegments);
229
+ const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
165
230
  // Render content segments as siblings
166
231
  let content: ReactNode = null;
167
232
  for (const node of tree) {
@@ -210,7 +275,7 @@ export async function renderSegments(
210
275
  }
211
276
 
212
277
  let nodeContent: ReactNode =
213
- loading !== null && loading
278
+ loading !== null && loading !== undefined && loading !== false
214
279
  ? createElement(RouteContentWrapper, {
215
280
  key: `suspense-loading-${id}`,
216
281
  content:
@@ -221,6 +286,19 @@ export async function renderSegments(
221
286
  segmentId: id,
222
287
  })
223
288
  : registerLazyRef(resolvedComponent);
289
+
290
+ // Wrap with <ViewTransition> if transition config exists (React experimental only).
291
+ // An empty config ({}) creates a bare <ViewTransition> boundary that participates
292
+ // in transitions without adding custom animation classes. Named element-level
293
+ // <ViewTransition> components inside (with name/share props) morph independently
294
+ // from the parent's default cross-fade.
295
+ if (ReactViewTransition && node.segment.transition) {
296
+ nodeContent = createElement(ReactViewTransition, {
297
+ ...node.segment.transition,
298
+ children: nodeContent,
299
+ });
300
+ }
301
+
224
302
  // Common props for OutletProvider
225
303
  const outletContent: ReactNode =
226
304
  node.segment.type === "layout" ? content : null;
@@ -265,13 +343,90 @@ export async function renderSegments(
265
343
  children: nodeContent,
266
344
  });
267
345
  } else {
268
- // Has loaders but no loading skeleton - await loaders and render directly
269
- const resolvedData = await loaderDataPromise;
346
+ // Has loaders but no loading skeleton.
347
+ // Split: parallel-owned loaders stream (their parallel has loading()),
348
+ // layout-owned loaders are awaited (they gate the layout content).
349
+ const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
350
+ const parallelOwnedLoaders = loaderEntries.filter(
351
+ (l) => !!l.parallelLoading,
352
+ );
353
+
354
+ // Await only layout-owned loaders
355
+ const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
356
+ const layoutLoaderDataPromise =
357
+ layoutLoaders.length > 0
358
+ ? Promise.all(
359
+ layoutLoaders.map((l) =>
360
+ l.loaderData instanceof Promise
361
+ ? l.loaderData
362
+ : Promise.resolve(l.loaderData),
363
+ ),
364
+ )
365
+ : Promise.resolve([]);
366
+ const resolvedData = await layoutLoaderDataPromise;
270
367
  const { loaderData, errorFallback } = resolveLoaderData(
271
368
  resolvedData,
272
- loaderIds,
369
+ layoutLoaderIds,
273
370
  );
274
371
 
372
+ // Parallel-owned loaders: attach to their owning parallel segment
373
+ // as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
374
+ if (parallelOwnedLoaders.length > 0) {
375
+ const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
376
+
377
+ for (const loader of parallelOwnedLoaders) {
378
+ if (!loader.namespace) {
379
+ continue;
380
+ }
381
+ const existing = loadersByParallelNamespace.get(loader.namespace);
382
+ if (existing) {
383
+ existing.push(loader);
384
+ } else {
385
+ loadersByParallelNamespace.set(loader.namespace, [loader]);
386
+ }
387
+ }
388
+
389
+ for (const p of node.parallel) {
390
+ if (!p.loading || !p.namespace) {
391
+ continue;
392
+ }
393
+
394
+ const ownedLoaders = loadersByParallelNamespace.get(p.namespace);
395
+ if (!ownedLoaders || ownedLoaders.length === 0) {
396
+ continue;
397
+ }
398
+
399
+ const parallelLoaderIds = ownedLoaders.map((l) => l.loaderId!);
400
+ const parallelLoaderSources = ownedLoaders.map((l) => l.loaderData);
401
+ p.loaderIds = parallelLoaderIds;
402
+
403
+ const shouldReuseParallelPromise =
404
+ p.loaderDataPromise !== undefined &&
405
+ hasSameReferences(p.parallelLoaderSources, parallelLoaderSources);
406
+
407
+ const parallelLoaderDataPromise = shouldReuseParallelPromise
408
+ ? p.loaderDataPromise
409
+ : forceAwait || isAction
410
+ ? await Promise.all(
411
+ ownedLoaders.map((l) =>
412
+ l.loaderData instanceof Promise
413
+ ? l.loaderData
414
+ : Promise.resolve(l.loaderData),
415
+ ),
416
+ )
417
+ : Promise.all(
418
+ ownedLoaders.map((l) =>
419
+ l.loaderData instanceof Promise
420
+ ? l.loaderData
421
+ : Promise.resolve(l.loaderData),
422
+ ),
423
+ );
424
+
425
+ p.loaderDataPromise = parallelLoaderDataPromise;
426
+ p.parallelLoaderSources = parallelLoaderSources;
427
+ }
428
+ }
429
+
275
430
  content = createElement(OutletProvider, {
276
431
  key,
277
432
  content: outletContent,
@@ -407,20 +562,13 @@ function* segmentTreeWalk(
407
562
  }
408
563
  }
409
564
 
410
- // Sort segments by ID to ensure consistent root-to-leaf ordering
411
- // regardless of the order they arrive in the input array (which can differ
412
- // between document requests and actions)
413
- // Shorter IDs come first (closer to root), same length sorted lexicographically
414
- nonParallels.sort((a, b) => {
415
- if (a.id.length !== b.id.length) {
416
- return a.id.length - b.id.length;
417
- }
418
- return a.id.localeCompare(b.id);
419
- });
420
-
421
- // Iterate bottom-to-top using reverse() to process leaf segments first
565
+ // Segments arrive in root-to-leaf order from the server (resolveSegment
566
+ // and resolveSegmentWithRevalidation push segments in this order).
567
+ // All consumers (reconcileSegments, cache) preserve this order.
568
+ // No sorting needed iterate bottom-to-top to process leaf segments first.
422
569
  // This processes route/leaf layouts first, then parent layouts
423
570
  // Note: We reverse the array to iterate from end to start (bottom-to-top)
571
+
424
572
  for (let i = nonParallels.length - 1; i >= 0; i--) {
425
573
  const segment = nonParallels[i];
426
574