@rangojs/router 0.0.0-experimental.9 → 0.0.0-experimental.a5f27bd5

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 (299) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1531 -155
  4. package/dist/vite/index.js +4440 -2170
  5. package/package.json +60 -54
  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 +6 -4
  13. package/skills/hooks/SKILL.md +333 -71
  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 +74 -15
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +15 -11
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +405 -45
  23. package/skills/rango/SKILL.md +85 -21
  24. package/skills/response-routes/SKILL.md +144 -91
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/theme/SKILL.md +9 -8
  28. package/skills/typesafety/SKILL.md +316 -87
  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 +87 -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 +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +285 -553
  41. package/src/browser/navigation-client.ts +123 -73
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +295 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +261 -309
  46. package/src/browser/prefetch/cache.ts +154 -0
  47. package/src/browser/prefetch/fetch.ts +135 -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 +88 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +182 -70
  53. package/src/browser/react/NavigationProvider.tsx +51 -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 +106 -27
  74. package/src/browser/scroll-restoration.ts +92 -16
  75. package/src/browser/segment-reconciler.ts +216 -0
  76. package/src/browser/segment-structure-assert.ts +16 -0
  77. package/src/browser/server-action-bridge.ts +504 -599
  78. package/src/browser/shallow.ts +6 -1
  79. package/src/browser/types.ts +107 -47
  80. package/src/browser/validate-redirect-origin.ts +29 -0
  81. package/src/build/generate-manifest.ts +82 -21
  82. package/src/build/generate-route-types.ts +36 -752
  83. package/src/build/index.ts +6 -5
  84. package/src/build/route-trie.ts +39 -13
  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 +469 -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 +338 -0
  98. package/src/cache/cache-scope.ts +120 -301
  99. package/src/cache/cf/cf-cache-store.ts +119 -7
  100. package/src/cache/cf/index.ts +8 -2
  101. package/src/cache/document-cache.ts +101 -72
  102. package/src/cache/handle-capture.ts +81 -0
  103. package/src/cache/handle-snapshot.ts +41 -0
  104. package/src/cache/index.ts +0 -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 +17 -7
  117. package/src/errors.ts +77 -7
  118. package/src/handle.ts +15 -10
  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 +133 -21
  133. package/src/index.ts +164 -52
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +25 -143
  136. package/src/loader.ts +27 -10
  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 +158 -13
  141. package/src/prerender.ts +333 -26
  142. package/src/reverse.ts +184 -121
  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 +934 -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 -1431
  151. package/src/route-map-builder.ts +156 -123
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +48 -9
  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 +158 -0
  158. package/src/router/handler-context.ts +374 -81
  159. package/src/router/intercept-resolution.ts +24 -16
  160. package/src/router/lazy-includes.ts +234 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +248 -0
  163. package/src/router/manifest.ts +83 -32
  164. package/src/router/match-api.ts +118 -119
  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 +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +336 -84
  169. package/src/router/match-middleware/cache-store.ts +43 -24
  170. package/src/router/match-middleware/intercept-resolution.ts +45 -20
  171. package/src/router/match-middleware/segment-resolution.ts +16 -8
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -28
  174. package/src/router/metrics.ts +235 -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 +324 -367
  178. package/src/router/pattern-matching.ts +197 -41
  179. package/src/router/prerender-match.ts +402 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +36 -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 +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1239 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -1315
  192. package/src/router/segment-wrappers.ts +289 -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 +16 -9
  198. package/src/router.ts +590 -1983
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +661 -1015
  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 +230 -0
  216. package/src/segment-system.tsx +25 -13
  217. package/src/server/context.ts +173 -48
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  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 +430 -70
  223. package/src/server.ts +35 -155
  224. package/src/ssr/index.tsx +100 -31
  225. package/src/static-handler.ts +114 -0
  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 +687 -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 +102 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1757
  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 -1282
  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 +110 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -1963
  261. package/src/vite/plugin-types.ts +131 -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 -51
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  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 +254 -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 +510 -0
  280. package/src/vite/router-discovery.ts +785 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  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/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/router.gen.ts +0 -6
  294. package/src/urls.gen.ts +0 -8
  295. package/src/vite/expose-handle-id.ts +0 -209
  296. package/src/vite/expose-loader-id.ts +0 -426
  297. package/src/vite/expose-location-state-id.ts +0 -177
  298. package/src/vite/expose-prerender-handler-id.ts +0 -429
  299. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -26,7 +26,12 @@ export function shallow<T>(a: T, b: T): boolean {
26
26
 
27
27
  // Check each key's value with Object.is
28
28
  for (const key of keysA) {
29
- if (!Object.is((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) {
29
+ if (
30
+ !Object.is(
31
+ (a as Record<string, unknown>)[key],
32
+ (b as Record<string, unknown>)[key],
33
+ )
34
+ ) {
30
35
  return false;
31
36
  }
32
37
  }
@@ -8,10 +8,10 @@ import type { RenderSegmentsOptions } from "../segment-system.js";
8
8
  // ============================================================================
9
9
 
10
10
  /**
11
- * RSC payload received from server
11
+ * RSC payload received from server.
12
+ * The tree is reconstructed from metadata.segments by the browser bridges.
12
13
  */
13
14
  export interface RscPayload<TMetadata = RscMetadata> {
14
- root: ReactNode | Promise<ReactNode> | null;
15
15
  metadata?: TMetadata;
16
16
  returnValue?: ActionResult;
17
17
  formState?: unknown;
@@ -36,6 +36,8 @@ export interface RscMetadata {
36
36
  isError?: boolean;
37
37
  matched?: string[];
38
38
  diff?: string[];
39
+ /** Merged route params from the matched route */
40
+ params?: Record<string, string>;
39
41
  /**
40
42
  * State of named slots for this route match
41
43
  * Key is slot name (e.g., "@modal"), value is slot state
@@ -53,6 +55,11 @@ export interface RscMetadata {
53
55
  * Used to detect version mismatches after HMR/deployment.
54
56
  */
55
57
  version?: string;
58
+ /**
59
+ * TTL in milliseconds for the client-side in-memory prefetch cache.
60
+ * Sent on initial render so the browser can configure its cache duration.
61
+ */
62
+ prefetchCacheTTL?: number;
56
63
  /**
57
64
  * Theme configuration from router.
58
65
  * Included when theme is enabled in router config.
@@ -65,6 +72,10 @@ export interface RscMetadata {
65
72
  initialTheme?: Theme;
66
73
  /** Whether connection warmup is enabled */
67
74
  warmupEnabled?: boolean;
75
+ /** Server-side redirect with optional state (for partial requests) */
76
+ redirect?: { url: string };
77
+ /** Server-set location state to include in history.pushState */
78
+ locationState?: Record<string, unknown>;
68
79
  }
69
80
 
70
81
  /**
@@ -115,7 +126,7 @@ export interface NavigationState {
115
126
  /** Whether RSC data is currently streaming (initial load or navigation) */
116
127
  isStreaming: boolean;
117
128
 
118
- /** Current location (updated optimistically) */
129
+ /** Current location */
119
130
  location: NavigationLocation;
120
131
 
121
132
  /** URL being navigated to (null when idle) */
@@ -172,7 +183,7 @@ export type ActionStateListener = (state: TrackedActionState) => void;
172
183
 
173
184
  /**
174
185
  * Cache interface for storing segments
175
- * Compatible with both Map and LRUCache
186
+ * Compatible with Map
176
187
  *
177
188
  * @internal This type is an implementation detail and may change without notice.
178
189
  */
@@ -209,9 +220,11 @@ export interface NavigationUpdate {
209
220
  /**
210
221
  * State value for navigate/Link
211
222
  * - LocationStateEntry[]: Type-safe state entries (recommended)
212
- * - unknown: Legacy format for backwards compatibility
223
+ * - unknown: Plain state format (object or getter function)
213
224
  */
214
- export type HistoryState = import("./react/location-state-shared.js").LocationStateEntry[] | unknown;
225
+ export type HistoryState =
226
+ | import("./react/location-state-shared.js").LocationStateEntry[]
227
+ | unknown;
215
228
 
216
229
  /**
217
230
  * Options for navigation operations
@@ -219,6 +232,25 @@ export type HistoryState = import("./react/location-state-shared.js").LocationSt
219
232
  export interface NavigateOptions {
220
233
  replace?: boolean;
221
234
  scroll?: boolean;
235
+ /**
236
+ * Whether to revalidate server data on navigation.
237
+ * Set to `false` to skip the RSC server fetch and only update the URL.
238
+ *
239
+ * Only takes effect when the pathname stays the same (search param / hash changes).
240
+ * If the pathname changes, this option is ignored and a full navigation occurs.
241
+ *
242
+ * All location-aware hooks (`useSearchParams`, `useNavigation`, etc.) still update.
243
+ * Server components do not re-render.
244
+ *
245
+ * @default true
246
+ *
247
+ * @example
248
+ * ```tsx
249
+ * router.push("/products?color=blue", { revalidate: false });
250
+ * router.replace("/products?page=3", { revalidate: false });
251
+ * ```
252
+ */
253
+ revalidate?: boolean;
222
254
  /**
223
255
  * State to pass to history.pushState/replaceState
224
256
  * Accessible via useLocationState() hook.
@@ -226,19 +258,67 @@ export interface NavigateOptions {
226
258
  * @example
227
259
  * ```tsx
228
260
  * // Type-safe state (recommended)
229
- * const ProductState = createLocationState<{ name: string }>("product");
261
+ * const ProductState = createLocationState<{ name: string }>();
230
262
  * navigate("/product/123", { state: [ProductState({ name: "Widget" })] });
231
263
  *
264
+ * // Type-safe just-in-time state (getter called at navigation time)
265
+ * navigate("/product/123", {
266
+ * state: [ProductState(() => ({ name: computeName() }))],
267
+ * });
268
+ *
232
269
  * // Multiple states
233
270
  * navigate("/checkout", { state: [ProductState(p), CartState(c)] });
234
271
  *
235
- * // Legacy format (backwards compatible)
272
+ * // Plain static state
236
273
  * navigate("/product", { state: { from: "list" } });
274
+ *
275
+ * // Plain just-in-time state
276
+ * navigate("/product", { state: () => ({ from: window.location.pathname }) });
237
277
  * ```
238
278
  */
239
279
  state?: HistoryState;
240
280
  }
241
281
 
282
+ /** @internal Extended options used only within the navigation bridge */
283
+ export interface NavigateOptionsInternal extends NavigateOptions {
284
+ /** Skip segment cache (used by redirect-with-state to force re-render) */
285
+ _skipCache?: boolean;
286
+ }
287
+
288
+ /**
289
+ * Options for useRouter push/replace methods.
290
+ * Same as NavigateOptions but without `replace` (implicit in push vs replace).
291
+ */
292
+ export type RouterNavigateOptions = Omit<NavigateOptions, "replace">;
293
+
294
+ /**
295
+ * Router instance returned by useRouter hook.
296
+ * Provides stable action methods that never cause re-renders.
297
+ */
298
+ export interface RouterInstance {
299
+ /** Navigate to a URL, pushing a new entry to the history stack */
300
+ push(url: string, options?: RouterNavigateOptions): Promise<void>;
301
+ /** Navigate to a URL, replacing the current history entry */
302
+ replace(url: string, options?: RouterNavigateOptions): Promise<void>;
303
+ /** Refresh the current route (re-fetch server data, preserve client state) */
304
+ refresh(): Promise<void>;
305
+ /** Prefetch a URL for faster client-side transition */
306
+ prefetch(url: string): void;
307
+ /** Go back in browser history */
308
+ back(): void;
309
+ /** Go forward in browser history */
310
+ forward(): void;
311
+ }
312
+
313
+ /**
314
+ * URLSearchParams without mutation methods.
315
+ * Matches Next.js convention for useSearchParams return type.
316
+ */
317
+ export type ReadonlyURLSearchParams = Omit<
318
+ URLSearchParams,
319
+ "append" | "delete" | "set" | "sort"
320
+ >;
321
+
242
322
  // ============================================================================
243
323
  // RSC Browser Dependencies
244
324
  // ============================================================================
@@ -252,15 +332,15 @@ export interface NavigateOptions {
252
332
  export interface RscBrowserDependencies {
253
333
  createFromFetch: <T>(
254
334
  response: Promise<Response>,
255
- options?: { temporaryReferences?: any }
335
+ options?: { temporaryReferences?: any },
256
336
  ) => Promise<T>;
257
337
  createFromReadableStream: <T>(stream: ReadableStream) => Promise<T>;
258
338
  encodeReply: (
259
339
  args: any[],
260
- options?: { temporaryReferences?: any }
340
+ options?: { temporaryReferences?: any },
261
341
  ) => Promise<FormData | string>;
262
342
  setServerCallback: (
263
- callback: (id: string, args: any[]) => Promise<any>
343
+ callback: (id: string, args: any[]) => Promise<any>,
264
344
  ) => void;
265
345
  createTemporaryReferenceSet: () => any;
266
346
  }
@@ -312,11 +392,13 @@ export interface NavigationStore {
312
392
  cacheSegmentsForHistory(
313
393
  historyKey: string,
314
394
  segments: ResolvedSegment[],
315
- handleData?: HandleData
395
+ handleData?: HandleData,
316
396
  ): void;
317
397
  getCachedSegments(
318
- historyKey: string
319
- ): { segments: ResolvedSegment[]; stale: boolean; handleData?: HandleData } | undefined;
398
+ historyKey: string,
399
+ ):
400
+ | { segments: ResolvedSegment[]; stale: boolean; handleData?: HandleData }
401
+ | undefined;
320
402
  hasHistoryCache(historyKey: string): boolean;
321
403
  updateCacheHandleData(historyKey: string, handleData: HandleData): void;
322
404
  markCacheAsStale(): void;
@@ -340,39 +422,10 @@ export interface NavigationStore {
340
422
  setActionState(actionId: string, state: Partial<TrackedActionState>): void;
341
423
  subscribeToAction(
342
424
  actionId: string,
343
- listener: ActionStateListener
425
+ listener: ActionStateListener,
344
426
  ): () => void;
345
427
  }
346
428
 
347
- // ============================================================================
348
- // Request Controller Types
349
- // ============================================================================
350
-
351
- /**
352
- * Disposable abort controller with automatic cleanup
353
- */
354
- export interface DisposableAbortController extends Disposable {
355
- controller: AbortController;
356
- }
357
-
358
- /**
359
- * Request controller for managing concurrent requests
360
- *
361
- * Separates navigation requests (aborted on new navigation) from
362
- * action requests (complete independently of navigation).
363
- */
364
- export interface RequestController {
365
- create(): AbortController;
366
- createDisposable(): DisposableAbortController;
367
- /** Create a disposable controller for actions (not aborted by navigation) */
368
- createActionDisposable(): DisposableAbortController;
369
- /** Abort all navigation requests (not actions) */
370
- abortAll(): void;
371
- /** Abort all action requests (used for error handling) */
372
- abortAllActions(): void;
373
- remove(controller: AbortController): void;
374
- }
375
-
376
429
  // ============================================================================
377
430
  // Navigation Client Types
378
431
  // ============================================================================
@@ -430,7 +483,6 @@ export interface LinkInterceptorOptions {
430
483
  */
431
484
  export interface ServerActionBridge {
432
485
  register(): void;
433
- unregister(): void;
434
486
  }
435
487
 
436
488
  /**
@@ -443,7 +495,7 @@ export interface ServerActionBridgeConfig {
443
495
  onUpdate: UpdateSubscriber;
444
496
  renderSegments: (
445
497
  segments: ResolvedSegment[],
446
- options?: RenderSegmentsOptions
498
+ options?: RenderSegmentsOptions,
447
499
  ) => Promise<ReactNode> | ReactNode;
448
500
  }
449
501
 
@@ -470,9 +522,17 @@ export interface NavigationBridgeConfig {
470
522
  onUpdate: UpdateSubscriber;
471
523
  renderSegments: (
472
524
  segments: ResolvedSegment[],
473
- options?: RenderSegmentsOptions
525
+ options?: RenderSegmentsOptions,
474
526
  ) => Promise<ReactNode> | ReactNode;
475
527
  }
476
528
 
477
529
  // Re-export ResolvedSegment for convenience
478
530
  export type { ResolvedSegment };
531
+
532
+ /**
533
+ * Token for tracking an active stream.
534
+ * Call end() when the stream completes.
535
+ */
536
+ export interface StreamingToken {
537
+ end(): void;
538
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Validate that a client-consumed redirect URL (from headers or Flight payload)
3
+ * targets the same origin as the current page. Prevents open-redirect attacks
4
+ * via crafted responses.
5
+ *
6
+ * @returns The canonical (normalized) URL string on success, or null if blocked.
7
+ */
8
+ export function validateRedirectOrigin(
9
+ url: string,
10
+ currentOrigin: string,
11
+ ): string | null {
12
+ try {
13
+ const target = new URL(url, currentOrigin);
14
+ if (target.origin !== currentOrigin) {
15
+ console.error(
16
+ `[rango] Redirect blocked: origin mismatch (${target.origin})`,
17
+ );
18
+ return null;
19
+ }
20
+ // Return pathname+search+hash for relative inputs, full href for absolute.
21
+ // This normalizes protocol-relative and other ambiguous forms.
22
+ return target.href.startsWith(currentOrigin)
23
+ ? target.href
24
+ : target.pathname + target.search + target.hash;
25
+ } catch {
26
+ console.error(`[rango] Redirect blocked: invalid URL "${url}"`);
27
+ return null;
28
+ }
29
+ }
@@ -43,14 +43,14 @@ export interface GeneratedManifest {
43
43
  routeManifest: Record<string, string>;
44
44
  /** Route name → trailing slash mode for trie redirect handling */
45
45
  routeTrailingSlash?: Record<string, string>;
46
- /** Route names using createPrerenderHandler (for dev-mode Node.js delegation) */
46
+ /** Route names using Prerender (for dev-mode Node.js delegation) */
47
47
  prerenderRoutes?: string[];
48
48
  /** Route names with passthrough: true (handler kept in bundle for live fallback) */
49
49
  passthroughRoutes?: string[];
50
50
  /** Route name → response type for non-RSC routes */
51
51
  responseTypeRoutes?: Record<string, string>;
52
- /** Generation timestamp */
53
- generatedAt: string;
52
+ /** Route name -> search schema descriptor for typed URL helpers */
53
+ routeSearchSchemas?: Record<string, Record<string, string>>;
54
54
  }
55
55
 
56
56
  /**
@@ -62,7 +62,7 @@ function buildPrefixTreeNode(
62
62
  namePrefix: string | undefined,
63
63
  patterns: UrlPatterns<any>,
64
64
  routeManifest: Record<string, string>,
65
- routeAncestry: Record<string, string[]>, // internal: feeds trie building, not exported
65
+ routeAncestry: Record<string, string[]>, // internal: feeds trie building, not exported
66
66
  mountIndex: number,
67
67
  visited: Set<unknown> = new Set(),
68
68
  routeTrailingSlash?: Record<string, string>,
@@ -70,10 +70,11 @@ function buildPrefixTreeNode(
70
70
  prerenderDefs?: Record<string, any>,
71
71
  passthroughRoutes?: string[],
72
72
  responseTypeRoutes?: Record<string, string>,
73
+ routeSearchSchemas?: Record<string, Record<string, string>>,
73
74
  ): PrefixTreeNode {
74
75
  if (visited.has(patterns)) {
75
76
  console.warn(
76
- `[@rangojs/router] Circular include detected at prefix "${urlPrefix}". Skipping.`
77
+ `[@rangojs/router] Circular include detected at prefix "${urlPrefix}". Skipping.`,
77
78
  );
78
79
  return {
79
80
  staticPrefix: extractStaticPrefix(urlPrefix),
@@ -89,6 +90,7 @@ function buildPrefixTreeNode(
89
90
  const patternsMap = new Map<string, string>();
90
91
  const patternsByPrefix = new Map<string, Map<string, string>>();
91
92
  const trailingSlashMap = new Map<string, TrailingSlashMode>();
93
+ const searchSchemasMap = new Map<string, Record<string, string>>();
92
94
  const trackedIncludes: TrackedInclude[] = [];
93
95
 
94
96
  RSCRouterContext.run(
@@ -97,6 +99,7 @@ function buildPrefixTreeNode(
97
99
  patterns: patternsMap,
98
100
  patternsByPrefix,
99
101
  trailingSlash: trailingSlashMap,
102
+ searchSchemas: searchSchemasMap,
100
103
  namespace: "build",
101
104
  parent: null,
102
105
  counters: {},
@@ -114,7 +117,7 @@ function buildPrefixTreeNode(
114
117
  }
115
118
  return patterns.handler() as AllUseItems[];
116
119
  });
117
- }
120
+ },
118
121
  );
119
122
 
120
123
  // Collect route names defined in this include (routes have prefixes applied)
@@ -130,6 +133,11 @@ function buildPrefixTreeNode(
130
133
  routeTrailingSlash[name] = mode;
131
134
  }
132
135
  }
136
+ if (routeSearchSchemas) {
137
+ for (const [name, schema] of searchSchemasMap.entries()) {
138
+ routeSearchSchemas[name] = schema;
139
+ }
140
+ }
133
141
 
134
142
  // Capture ancestry from manifest entries' parent chains
135
143
  captureAncestry(manifest, routeAncestry);
@@ -142,7 +150,10 @@ function buildPrefixTreeNode(
142
150
  if (prerenderDefs && entry.prerenderDef) {
143
151
  prerenderDefs[name] = entry.prerenderDef;
144
152
  }
145
- if (passthroughRoutes && entry.prerenderDef?.options?.passthrough === true) {
153
+ if (
154
+ passthroughRoutes &&
155
+ entry.prerenderDef?.options?.passthrough === true
156
+ ) {
146
157
  passthroughRoutes.push(name);
147
158
  }
148
159
  }
@@ -177,6 +188,7 @@ function buildPrefixTreeNode(
177
188
  prerenderDefs,
178
189
  passthroughRoutes,
179
190
  responseTypeRoutes,
191
+ routeSearchSchemas,
180
192
  );
181
193
 
182
194
  const existing = children[include.fullPrefix];
@@ -188,6 +200,11 @@ function buildPrefixTreeNode(
188
200
  }
189
201
  }
190
202
 
203
+ // Remove from visited so sibling branches can reuse the same patterns
204
+ // without false circular-include detection. Only ancestors in the current
205
+ // recursion path should trigger the cycle guard.
206
+ visited.delete(patterns);
207
+
191
208
  return {
192
209
  staticPrefix: extractStaticPrefix(urlPrefix),
193
210
  fullPrefix: urlPrefix,
@@ -218,11 +235,20 @@ function captureAncestry(
218
235
  }
219
236
 
220
237
  /**
221
- * Generate manifest from UrlPatterns
238
+ * Internal manifest result including build-pipeline-only fields.
239
+ * Not part of the public API — use generateManifest() for the public surface.
240
+ */
241
+ export interface FullManifest extends GeneratedManifest {
242
+ _routeAncestry: Record<string, string[]>;
243
+ _prerenderDefs?: Record<string, any>;
244
+ }
245
+
246
+ /**
247
+ * Generate manifest from UrlPatterns (public API).
222
248
  *
223
- * This runs all patterns (including lazy ones) at build time to extract:
224
- * - The complete prefix tree for short-circuit optimization
225
- * - The complete route manifest for href()
249
+ * Returns only the public GeneratedManifest fields. Internal build pipeline
250
+ * consumers that need _routeAncestry or _prerenderDefs should use
251
+ * generateManifestFull() instead.
226
252
  *
227
253
  * @example
228
254
  * ```typescript
@@ -240,7 +266,26 @@ function captureAncestry(
240
266
  export function generateManifest<TEnv>(
241
267
  urlpatterns: UrlPatterns<TEnv, any>,
242
268
  mountIndex: number = 0,
243
- ): GeneratedManifest & { _routeAncestry: Record<string, string[]>; _prerenderDefs?: Record<string, any> } {
269
+ ): GeneratedManifest {
270
+ const {
271
+ _routeAncestry: _,
272
+ _prerenderDefs: __,
273
+ ...publicManifest
274
+ } = generateManifestFull(urlpatterns, mountIndex);
275
+ return publicManifest;
276
+ }
277
+
278
+ /**
279
+ * Generate manifest with internal build-pipeline fields.
280
+ *
281
+ * Used by the Vite plugin (discover-routers via dynamic import through
282
+ * @rangojs/router/build), manifest-init (direct import), and trie
283
+ * building. Not intended for external use.
284
+ */
285
+ export function generateManifestFull<TEnv>(
286
+ urlpatterns: UrlPatterns<TEnv, any>,
287
+ mountIndex: number = 0,
288
+ ): FullManifest {
244
289
  const routeManifest: Record<string, string> = {};
245
290
  const routeAncestry: Record<string, string[]> = {};
246
291
  const prefixTree: Record<string, PrefixTreeNode> = {};
@@ -250,6 +295,7 @@ export function generateManifest<TEnv>(
250
295
  const patternsMap = new Map<string, string>();
251
296
  const patternsByPrefix = new Map<string, Map<string, string>>();
252
297
  const trailingSlashMap = new Map<string, TrailingSlashMode>();
298
+ const searchSchemasMap = new Map<string, Record<string, string>>();
253
299
  const trackedIncludes: TrackedInclude[] = [];
254
300
 
255
301
  RSCRouterContext.run(
@@ -258,6 +304,7 @@ export function generateManifest<TEnv>(
258
304
  patterns: patternsMap,
259
305
  patternsByPrefix,
260
306
  trailingSlash: trailingSlashMap,
307
+ searchSchemas: searchSchemasMap,
261
308
  namespace: "build",
262
309
  parent: null,
263
310
  counters: {},
@@ -270,7 +317,7 @@ export function generateManifest<TEnv>(
270
317
  helpers.layout(MapRootLayout, () => {
271
318
  return urlpatterns.handler() as AllUseItems[];
272
319
  });
273
- }
320
+ },
274
321
  );
275
322
 
276
323
  // Collect root-level routes and trailing slash config
@@ -281,6 +328,10 @@ export function generateManifest<TEnv>(
281
328
  for (const [name, mode] of trailingSlashMap.entries()) {
282
329
  routeTrailingSlash[name] = mode;
283
330
  }
331
+ const routeSearchSchemas: Record<string, Record<string, string>> = {};
332
+ for (const [name, schema] of searchSchemasMap.entries()) {
333
+ routeSearchSchemas[name] = schema;
334
+ }
284
335
 
285
336
  // Capture ancestry from manifest entries' parent chains
286
337
  captureAncestry(manifest, routeAncestry);
@@ -323,6 +374,7 @@ export function generateManifest<TEnv>(
323
374
  prerenderDefs,
324
375
  passthroughRoutes,
325
376
  responseTypeRoutes,
377
+ routeSearchSchemas,
326
378
  );
327
379
 
328
380
  const existing = prefixTree[include.fullPrefix];
@@ -337,15 +389,25 @@ export function generateManifest<TEnv>(
337
389
  return {
338
390
  prefixTree,
339
391
  routeManifest,
340
- routeTrailingSlash: Object.keys(routeTrailingSlash).length > 0 ? routeTrailingSlash : undefined,
392
+ routeTrailingSlash:
393
+ Object.keys(routeTrailingSlash).length > 0
394
+ ? routeTrailingSlash
395
+ : undefined,
341
396
  prerenderRoutes: prerenderRoutes.length > 0 ? prerenderRoutes : undefined,
342
- passthroughRoutes: passthroughRoutes.length > 0 ? passthroughRoutes : undefined,
343
- responseTypeRoutes: Object.keys(responseTypeRoutes).length > 0 ? responseTypeRoutes : undefined,
344
- generatedAt: new Date().toISOString(),
345
- // Internal: routeAncestry is used only for trie building, not exported
397
+ passthroughRoutes:
398
+ passthroughRoutes.length > 0 ? passthroughRoutes : undefined,
399
+ responseTypeRoutes:
400
+ Object.keys(responseTypeRoutes).length > 0
401
+ ? responseTypeRoutes
402
+ : undefined,
403
+ routeSearchSchemas:
404
+ Object.keys(routeSearchSchemas).length > 0
405
+ ? routeSearchSchemas
406
+ : undefined,
346
407
  _routeAncestry: routeAncestry,
347
408
  // Internal: prerender handler definitions for build-time getParams() access
348
- _prerenderDefs: Object.keys(prerenderDefs).length > 0 ? prerenderDefs : undefined,
409
+ _prerenderDefs:
410
+ Object.keys(prerenderDefs).length > 0 ? prerenderDefs : undefined,
349
411
  };
350
412
  }
351
413
 
@@ -359,13 +421,12 @@ export function generateManifest<TEnv>(
359
421
  * ```
360
422
  */
361
423
  export function generateManifestCode<TEnv>(
362
- urlpatterns: UrlPatterns<TEnv, any>
424
+ urlpatterns: UrlPatterns<TEnv, any>,
363
425
  ): string {
364
426
  const manifest = generateManifest(urlpatterns);
365
427
 
366
428
  return `/**
367
429
  * Auto-generated route manifest
368
- * Generated at: ${manifest.generatedAt}
369
430
  *
370
431
  * DO NOT EDIT - This file is generated by @rangojs/router
371
432
  */