@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126

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 (235) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/rango.js +3 -4
  3. package/dist/vite/index.js +315 -68
  4. package/package.json +19 -18
  5. package/skills/breadcrumbs/SKILL.md +60 -0
  6. package/skills/hooks/SKILL.md +2 -2
  7. package/skills/route/SKILL.md +6 -0
  8. package/skills/server-actions/SKILL.md +25 -1
  9. package/skills/testing/SKILL.md +17 -17
  10. package/skills/testing/cache-prerender.md +29 -3
  11. package/skills/testing/flight.md +13 -10
  12. package/skills/testing/render-handler.md +3 -0
  13. package/skills/testing/server-tree.md +1 -1
  14. package/skills/testing/setup.md +1 -1
  15. package/src/__internal.ts +0 -65
  16. package/src/browser/action-coordinator.ts +1 -1
  17. package/src/browser/action-fence.ts +10 -0
  18. package/src/browser/event-controller.ts +1 -83
  19. package/src/browser/navigation-store-handle.ts +3 -4
  20. package/src/browser/navigation-store.ts +0 -39
  21. package/src/browser/navigation-transaction.ts +0 -32
  22. package/src/browser/partial-update.ts +23 -84
  23. package/src/browser/prefetch/cache.ts +6 -45
  24. package/src/browser/prefetch/queue.ts +6 -3
  25. package/src/browser/rango-state.ts +2 -23
  26. package/src/browser/react/Link.tsx +0 -2
  27. package/src/browser/react/NavigationProvider.tsx +2 -1
  28. package/src/browser/react/ScrollRestoration.tsx +10 -6
  29. package/src/browser/react/filter-segment-order.ts +0 -2
  30. package/src/browser/react/index.ts +0 -45
  31. package/src/browser/react/location-state-shared.ts +0 -13
  32. package/src/browser/react/location-state.ts +0 -1
  33. package/src/browser/react/use-action.ts +6 -15
  34. package/src/browser/react/use-handle.ts +0 -5
  35. package/src/browser/react/use-link-status.ts +0 -4
  36. package/src/browser/react/use-navigation.ts +0 -3
  37. package/src/browser/react/use-params.ts +0 -2
  38. package/src/browser/react/use-router.ts +2 -1
  39. package/src/browser/react/use-search-params.ts +0 -5
  40. package/src/browser/react/use-segments.ts +0 -13
  41. package/src/browser/rsc-router.tsx +10 -3
  42. package/src/browser/server-action-bridge.ts +51 -3
  43. package/src/browser/types.ts +23 -5
  44. package/src/browser/validate-redirect-origin.ts +43 -16
  45. package/src/build/index.ts +8 -9
  46. package/src/build/route-trie.ts +46 -11
  47. package/src/build/route-types/param-extraction.ts +6 -3
  48. package/src/build/route-types/router-processing.ts +0 -8
  49. package/src/cache/cache-policy.ts +0 -54
  50. package/src/cache/cache-runtime.ts +48 -24
  51. package/src/cache/cache-scope.ts +0 -27
  52. package/src/cache/cache-tag.ts +0 -37
  53. package/src/cache/cf/cf-cache-store.ts +72 -45
  54. package/src/cache/cf/index.ts +0 -24
  55. package/src/cache/document-cache.ts +10 -36
  56. package/src/cache/handle-snapshot.ts +0 -40
  57. package/src/cache/index.ts +0 -27
  58. package/src/cache/memory-segment-store.ts +0 -52
  59. package/src/cache/profile-registry.ts +6 -30
  60. package/src/cache/read-through-swr.ts +41 -11
  61. package/src/cache/segment-codec.ts +0 -16
  62. package/src/cache/types.ts +0 -98
  63. package/src/client.rsc.tsx +4 -22
  64. package/src/client.tsx +19 -32
  65. package/src/context-var.ts +12 -0
  66. package/src/defer.ts +196 -0
  67. package/src/deps/ssr.ts +0 -1
  68. package/src/handle.ts +2 -12
  69. package/src/handles/MetaTags.tsx +0 -14
  70. package/src/handles/breadcrumbs.ts +16 -5
  71. package/src/handles/meta.ts +0 -39
  72. package/src/host/cookie-handler.ts +0 -36
  73. package/src/host/errors.ts +0 -24
  74. package/src/host/index.ts +6 -0
  75. package/src/host/pattern-matcher.ts +7 -50
  76. package/src/host/router.ts +1 -65
  77. package/src/host/testing.ts +0 -16
  78. package/src/host/types.ts +6 -2
  79. package/src/href-client.ts +0 -4
  80. package/src/index.rsc.ts +27 -2
  81. package/src/index.ts +7 -0
  82. package/src/internal-debug.ts +2 -4
  83. package/src/loader.rsc.ts +4 -15
  84. package/src/loader.ts +3 -9
  85. package/src/network-error-thrower.tsx +1 -6
  86. package/src/outlet-provider.tsx +1 -5
  87. package/src/prerender/param-hash.ts +10 -11
  88. package/src/prerender/store.ts +23 -30
  89. package/src/prerender.ts +34 -0
  90. package/src/redirect-origin.ts +100 -0
  91. package/src/root-error-boundary.tsx +1 -19
  92. package/src/route-content-wrapper.tsx +1 -44
  93. package/src/route-definition/dsl-helpers.ts +7 -19
  94. package/src/route-definition/helpers-types.ts +3 -3
  95. package/src/route-definition/redirect.ts +43 -9
  96. package/src/route-definition/resolve-handler-use.ts +6 -0
  97. package/src/route-map-builder.ts +0 -16
  98. package/src/router/content-negotiation.ts +0 -13
  99. package/src/router/error-handling.ts +12 -16
  100. package/src/router/find-match.ts +4 -31
  101. package/src/router/intercept-resolution.ts +10 -1
  102. package/src/router/lazy-includes.ts +1 -57
  103. package/src/router/loader-resolution.ts +25 -23
  104. package/src/router/logging.ts +0 -6
  105. package/src/router/manifest.ts +1 -25
  106. package/src/router/match-api.ts +0 -20
  107. package/src/router/match-context.ts +0 -22
  108. package/src/router/match-handlers.ts +0 -43
  109. package/src/router/match-middleware/background-revalidation.ts +0 -7
  110. package/src/router/match-middleware/cache-lookup.ts +96 -179
  111. package/src/router/match-middleware/cache-store.ts +0 -31
  112. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  113. package/src/router/match-middleware/segment-resolution.ts +0 -22
  114. package/src/router/match-pipelines.ts +1 -42
  115. package/src/router/match-result.ts +1 -52
  116. package/src/router/metrics.ts +0 -34
  117. package/src/router/middleware-types.ts +0 -116
  118. package/src/router/middleware.ts +77 -60
  119. package/src/router/navigation-snapshot.ts +0 -51
  120. package/src/router/params-util.ts +23 -0
  121. package/src/router/pattern-matching.ts +5 -56
  122. package/src/router/prerender-match.ts +56 -51
  123. package/src/router/request-classification.ts +1 -38
  124. package/src/router/revalidation.ts +14 -62
  125. package/src/router/route-snapshot.ts +0 -1
  126. package/src/router/router-context.ts +0 -27
  127. package/src/router/router-interfaces.ts +10 -0
  128. package/src/router/segment-resolution/fresh.ts +25 -57
  129. package/src/router/segment-resolution/helpers.ts +34 -0
  130. package/src/router/segment-resolution/loader-cache.ts +35 -23
  131. package/src/router/segment-resolution/revalidation.ts +188 -283
  132. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  133. package/src/router/segment-resolution.ts +4 -1
  134. package/src/router/segment-wrappers.ts +0 -3
  135. package/src/router/telemetry-otel.ts +0 -20
  136. package/src/router/telemetry.ts +0 -22
  137. package/src/router/timeout.ts +0 -20
  138. package/src/router/trie-matching.ts +66 -45
  139. package/src/router/types.ts +1 -63
  140. package/src/router/url-params.ts +0 -5
  141. package/src/router.ts +8 -11
  142. package/src/rsc/handler-context.ts +1 -0
  143. package/src/rsc/handler.ts +20 -4
  144. package/src/rsc/helpers.ts +71 -3
  145. package/src/rsc/json-route-result.ts +38 -0
  146. package/src/rsc/origin-guard.ts +9 -15
  147. package/src/rsc/progressive-enhancement.ts +10 -1
  148. package/src/rsc/redirect-guard.ts +99 -0
  149. package/src/rsc/response-route-handler.ts +23 -18
  150. package/src/rsc/rsc-rendering.ts +2 -7
  151. package/src/rsc/runtime-warnings.ts +14 -0
  152. package/src/rsc/server-action.ts +34 -29
  153. package/src/rsc/types.ts +6 -3
  154. package/src/search-params.ts +0 -16
  155. package/src/segment-loader-promise.ts +14 -2
  156. package/src/segment-system.tsx +79 -88
  157. package/src/server/handle-store.ts +7 -24
  158. package/src/server/loader-registry.ts +5 -24
  159. package/src/server/request-context.ts +29 -92
  160. package/src/ssr/index.tsx +14 -14
  161. package/src/static-handler.ts +2 -27
  162. package/src/testing/cache-status.ts +44 -48
  163. package/src/testing/collect-handle.ts +1 -24
  164. package/src/testing/dispatch.ts +43 -6
  165. package/src/testing/e2e/index.ts +1 -22
  166. package/src/testing/e2e/matchers.ts +0 -16
  167. package/src/testing/flight-matchers.ts +0 -13
  168. package/src/testing/flight-normalize.ts +3 -30
  169. package/src/testing/flight.ts +46 -48
  170. package/src/testing/generated-routes.ts +1 -41
  171. package/src/testing/index.ts +1 -21
  172. package/src/testing/internal/context.ts +3 -45
  173. package/src/testing/internal/seed-vars.ts +0 -26
  174. package/src/testing/render-handler.ts +31 -61
  175. package/src/testing/render-route.tsx +75 -103
  176. package/src/testing/run-loader.ts +0 -96
  177. package/src/testing/run-middleware.ts +0 -26
  178. package/src/theme/ThemeProvider.tsx +0 -52
  179. package/src/theme/ThemeScript.tsx +0 -6
  180. package/src/theme/constants.ts +0 -12
  181. package/src/theme/index.ts +0 -7
  182. package/src/theme/theme-context.ts +1 -5
  183. package/src/theme/theme-script.ts +0 -14
  184. package/src/theme/use-theme.ts +0 -3
  185. package/src/types/boundaries.ts +0 -35
  186. package/src/types/error-types.ts +25 -89
  187. package/src/types/global-namespace.ts +4 -14
  188. package/src/types/handler-context.ts +28 -9
  189. package/src/types/index.ts +0 -10
  190. package/src/types/request-scope.ts +0 -19
  191. package/src/types/route-config.ts +6 -50
  192. package/src/types/route-entry.ts +0 -6
  193. package/src/types/segments.ts +0 -13
  194. package/src/urls/include-helper.ts +0 -4
  195. package/src/urls/index.ts +0 -6
  196. package/src/urls/path-helper-types.ts +2 -2
  197. package/src/urls/path-helper.ts +0 -54
  198. package/src/urls/urls-function.ts +0 -13
  199. package/src/use-loader.tsx +0 -186
  200. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  201. package/src/vite/discovery/discover-routers.ts +28 -18
  202. package/src/vite/discovery/prerender-collection.ts +2 -4
  203. package/src/vite/discovery/state.ts +5 -0
  204. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  205. package/src/vite/plugin-types.ts +35 -9
  206. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  207. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  208. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  209. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  210. package/src/vite/plugins/expose-action-id.ts +2 -73
  211. package/src/vite/plugins/expose-id-utils.ts +0 -55
  212. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  213. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  214. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  215. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  216. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  217. package/src/vite/plugins/performance-tracks.ts +0 -3
  218. package/src/vite/plugins/refresh-cmd.ts +1 -1
  219. package/src/vite/plugins/use-cache-transform.ts +21 -46
  220. package/src/vite/plugins/version-injector.ts +0 -20
  221. package/src/vite/plugins/version-plugin.ts +1 -49
  222. package/src/vite/plugins/virtual-entries.ts +0 -15
  223. package/src/vite/rango.ts +2 -108
  224. package/src/vite/router-discovery.ts +9 -1
  225. package/src/vite/utils/ast-handler-extract.ts +0 -16
  226. package/src/vite/utils/bundle-analysis.ts +6 -13
  227. package/src/vite/utils/client-chunks.ts +0 -6
  228. package/src/vite/utils/forward-user-plugins.ts +0 -22
  229. package/src/vite/utils/manifest-utils.ts +0 -4
  230. package/src/vite/utils/package-resolution.ts +1 -73
  231. package/src/vite/utils/prerender-utils.ts +0 -35
  232. package/src/vite/utils/shared-utils.ts +3 -35
  233. package/src/browser/shallow.ts +0 -40
  234. package/src/handles/index.ts +0 -7
  235. package/src/router/middleware-cookies.ts +0 -55
@@ -31,6 +31,12 @@
31
31
  *
32
32
  * Replaces the previous browser HTTP cache approach which was unreliable
33
33
  * due to response draining race conditions and browser inconsistencies.
34
+ *
35
+ * State here lives in module-level singletons (cache, inflight, generation,
36
+ * cacheTTL, etc.) rather than a per-instance factory. This is correct because
37
+ * exactly one router is live per document — an SPA navigation crossing a
38
+ * host-router boundary forces a full document reload — so the singletons are
39
+ * effectively per-document. Unit tests reset them via clearPrefetchCache().
34
40
  */
35
41
 
36
42
  import { abortAllPrefetches } from "./queue.js";
@@ -61,9 +67,6 @@ export interface DecodedPrefetch {
61
67
  scope: "source" | "wildcard";
62
68
  }
63
69
 
64
- // Default TTL: 5 minutes. Overridden by initPrefetchCache() with
65
- // the server-configured prefetchCacheTTL from router options.
66
- // 0 disables the in-memory cache entirely.
67
70
  let cacheTTL = 300_000;
68
71
 
69
72
  /**
@@ -92,41 +95,12 @@ interface PrefetchCacheEntry {
92
95
  const cache = new Map<string, PrefetchCacheEntry>();
93
96
  const inflight = new Set<string>();
94
97
 
95
- /**
96
- * In-flight promise map. When a prefetch fetch+decode is in progress, its
97
- * Promise<DecodedPrefetch | null> is stored here so navigation can await it
98
- * instead of starting a duplicate request. Resolves to null when the prefetch
99
- * failed, was aborted, or carried a control header (reload/redirect) that the
100
- * navigation must re-fetch to act on.
101
- */
102
98
  const inflightPromises = new Map<string, Promise<DecodedPrefetch | null>>();
103
99
 
104
- /**
105
- * Alias map for in-flight promises registered under multiple keys (see
106
- * dual inflight in prefetch/fetch.ts). Records each key's sibling set so
107
- * that consuming or clearing any one key atomically removes every alias —
108
- * guaranteeing a single consumer for the shared decode.
109
- */
110
100
  const inflightAliases = new Map<string, string[]>();
111
101
 
112
- /**
113
- * Keys whose in-flight prefetch promise was adopted by a navigation (via
114
- * `consumeInflightPrefetch`). A `DecodedPrefetch` carries a single-use
115
- * `metadata.handles` async generator; the adopter drains it. The same entry is
116
- * also published to the `cache` map by `storePrefetch` when the fetch resolves
117
- * — which runs AFTER adoption (adoption only succeeds while the fetch is still
118
- * in flight, so the entry is not yet cached). Without this guard the adopted,
119
- * now-drained entry would be left in the cache and served to a later navigation
120
- * whose handle generator yields nothing, silently dropping that route's
121
- * breadcrumbs. Recording the adopted keys lets `storePrefetch` skip publishing
122
- * them, keeping the existing one-time-consumption contract (a consumed prefetch
123
- * is gone; the next navigation re-fetches).
124
- */
125
102
  const adoptedKeys = new Set<string>();
126
103
 
127
- // Generation counter incremented on each clearPrefetchCache(). Fetches that
128
- // started before a clear carry a stale generation and must not store their
129
- // response (the data may be stale due to a server action invalidation).
130
104
  let generation = 0;
131
105
 
132
106
  /**
@@ -306,9 +280,6 @@ export function markPrefetchInflight(key: string): void {
306
280
  inflight.add(key);
307
281
  }
308
282
 
309
- /**
310
- * Store the in-flight Promise for a prefetch so navigation can reuse it.
311
- */
312
283
  export function setInflightPromise(
313
284
  key: string,
314
285
  promise: Promise<DecodedPrefetch | null>,
@@ -337,20 +308,10 @@ export function clearPrefetchInflight(key: string): void {
337
308
  inflight.delete(k);
338
309
  inflightPromises.delete(k);
339
310
  inflightAliases.delete(k);
340
- // Clear any adopted marker too, so a fetch that failed before storePrefetch
341
- // (the marker's normal consumer) does not strand it across the next prefetch.
342
311
  adoptedKeys.delete(k);
343
312
  });
344
313
  }
345
314
 
346
- /**
347
- * Invalidate all prefetch state. Called when server actions mutate data.
348
- * Clears the in-memory cache, cancels in-flight prefetches, and rotates
349
- * the Rango state key so CDN-cached responses are also invalidated.
350
- *
351
- * Uses abortAllPrefetches (hard cancel) because in-flight responses
352
- * may contain stale data after a mutation.
353
- */
354
315
  export function clearPrefetchCache(): void {
355
316
  generation++;
356
317
  inflight.clear();
@@ -71,10 +71,13 @@ function scheduleDrain(): void {
71
71
  Promise.race([waitForViewportImages(), wait(IMAGE_WAIT_TIMEOUT)]),
72
72
  )
73
73
  .then(() => {
74
- drainScheduled = false;
75
- // Stale drain: a cancel/abort happened while we were waiting.
76
- // A fresh scheduleDrain will be called by whatever enqueues next.
74
+ // Stale drain: a cancel/abort happened while we were waiting, and a fresh
75
+ // scheduleDrain may already own drainScheduled for the new generation.
76
+ // Bail WITHOUT clearing the flag so we don't clobber the live wait's
77
+ // single-in-flight-drain coalescing (clearing it here would let the next
78
+ // enqueue start a third overlapping wait).
77
79
  if (gen !== drainGeneration) return;
80
+ drainScheduled = false;
78
81
  if (queue.length > 0) drain();
79
82
  });
80
83
  }
@@ -32,23 +32,13 @@ import {
32
32
  serializeStateCookie,
33
33
  } from "./cookie-name.js";
34
34
 
35
- // The resolved cookie name this document is bound to (server-resolved, read
36
- // from payload metadata at boot). Bare default until initRangoState runs.
37
35
  let cookieName: string = DEFAULT_STATE_COOKIE_PREFIX;
38
36
 
39
- // Build version for this document, used as the prefix of minted values.
40
37
  let currentVersion = "0";
41
38
 
42
- // Write-through mirror of the value. Authoritative only when the cookie is
43
- // unreadable. `cookieBacked` records whether the mirror was last confirmed
44
- // present in the jar, so a present->absent transition (an external clear) is
45
- // detected exactly once instead of re-firing on every subsequent read.
46
39
  let mirror: string | null = null;
47
40
  let cookieBacked = false;
48
41
 
49
- // External-rotation observer, registered by the store-handle wiring (so a
50
- // sibling tab's rotation or a server Set-Cookie marks the history cache stale).
51
- // Null until registered; self-rotations never call it.
52
42
  let externalRotationObserver: ((value: string) => void) | null = null;
53
43
 
54
44
  /**
@@ -81,7 +71,6 @@ function readCookie(name: string): CookieRead {
81
71
  } catch {
82
72
  return { readable: false, value: null };
83
73
  }
84
- // Shared parser with the server seat so both read the same jar entry.
85
74
  return { readable: true, value: getRawCookieValue(raw, name) };
86
75
  }
87
76
 
@@ -91,14 +80,9 @@ function writeCookie(name: string, value: string): void {
91
80
  typeof location !== "undefined" && location.protocol === "https:";
92
81
  try {
93
82
  document.cookie = serializeStateCookie(name, value, secure);
94
- } catch {
95
- // Write failures are silently absorbed; the mirror carries the value.
96
- }
83
+ } catch {}
97
84
  }
98
85
 
99
- // Mint a fresh value: same version, a timestamp strictly greater than the
100
- // current one (the in-memory mirror is the previous value). The monotonic guard
101
- // lives in mintStateValue, shared with the server seat.
102
86
  function mintValue(): string {
103
87
  return mintStateValue(currentVersion, mirror);
104
88
  }
@@ -195,9 +179,6 @@ export function invalidateRangoState(): void {
195
179
  writeCookie(cookieName, mirror);
196
180
  }
197
181
 
198
- // One-time migration: remove the legacy localStorage keys this mechanism used
199
- // before the cookie cutover. No value porting — a fresh cookie mint just misses
200
- // cleanly. Idempotent: scans for `rango-state` and `rango-state:{routerId}`.
201
182
  function cleanupLegacyStorage(): void {
202
183
  if (typeof localStorage === "undefined") return;
203
184
  try {
@@ -209,7 +190,5 @@ function cleanupLegacyStorage(): void {
209
190
  }
210
191
  }
211
192
  for (const key of toRemove) localStorage.removeItem(key);
212
- } catch {
213
- // localStorage unavailable; nothing to clean.
214
- }
193
+ } catch {}
215
194
  }
@@ -39,8 +39,6 @@ import {
39
39
  unobserveForPrefetch,
40
40
  } from "../prefetch/observer.js";
41
41
 
42
- // Touch device detection for adaptive strategy.
43
- // Checked once at module load (Link.tsx is "use client", runs only in browser).
44
42
  const isTouchDevice =
45
43
  typeof window !== "undefined" && window.matchMedia("(hover: none)").matches;
46
44
 
@@ -29,6 +29,7 @@ import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
29
29
  import { cancelAllPrefetches } from "../prefetch/queue.js";
30
30
  import { handleNavigationEnd } from "../scroll-restoration.js";
31
31
  import { createAppShellRef, type AppShellRef } from "../app-shell.js";
32
+ import { debugLog } from "../logging.js";
32
33
 
33
34
  /**
34
35
  * Process handles from an async generator, updating the event controller
@@ -70,7 +71,7 @@ async function processHandles(
70
71
  // This prevents handle data from cancelled navigations polluting
71
72
  // the current route's breadcrumbs (e.g., quick popstate after clicking a link).
72
73
  if (historyKey !== store.getHistoryKey()) {
73
- console.log(
74
+ debugLog(
74
75
  "[NavigationProvider] Stopping handle processing - user navigated away",
75
76
  );
76
77
  return;
@@ -14,17 +14,21 @@ export interface ScrollRestorationProps {
14
14
  * Return location.pathname to restore scroll based on path
15
15
  * (useful for keeping scroll position on the same page).
16
16
  *
17
+ * Provide a stable reference: a module-level function or one wrapped in
18
+ * useCallback. The init effect re-runs when getKey's identity changes, and
19
+ * teardown clears in-memory scroll positions — a fresh inline arrow on every
20
+ * parent render would discard unpersisted positions mid-session.
21
+ *
17
22
  * @example
18
23
  * ```tsx
24
+ * // Stable module-level getKey (recommended)
25
+ * const byPathname = (location) => location.pathname;
26
+ *
19
27
  * // Restore based on pathname (same URL = same scroll)
20
- * <ScrollRestoration
21
- * getKey={(location) => location.pathname}
22
- * />
28
+ * <ScrollRestoration getKey={byPathname} />
23
29
  *
24
30
  * // Restore based on unique history entry (default)
25
- * <ScrollRestoration
26
- * getKey={(location) => location.key}
27
- * />
31
+ * // <ScrollRestoration /> — omit getKey to use location.key
28
32
  * ```
29
33
  */
30
34
  getKey?: (location: {
@@ -46,8 +46,6 @@ export function filterSegmentOrder(matched: string[]): string[] {
46
46
  const slots = slotsByParent.get(id);
47
47
  if (slots) result.push(...slots);
48
48
  }
49
- // Defensive: any slot whose parent is missing from the filtered list still
50
- // gets included rather than silently dropped. Shouldn't happen in practice.
51
49
  for (const [parent, slots] of slotsByParent) {
52
50
  if (!nonSlotSet.has(parent)) result.push(...slots);
53
51
  }
@@ -1,49 +1,4 @@
1
- // React exports for browser navigation
2
-
3
- // Hook with Zustand-style selectors
4
- export { useNavigation } from "./use-navigation.js";
5
-
6
- // Router actions hook (stable reference, no re-renders)
7
- export { useRouter } from "./use-router.js";
8
-
9
- // URL hooks
10
- export { usePathname } from "./use-pathname.js";
11
- export { useSearchParams } from "./use-search-params.js";
12
- export { useParams } from "./use-params.js";
13
-
14
- // Action state tracking hook
15
- export { useAction, type TrackedActionState } from "./use-action.js";
16
-
17
- // Segments state hook
18
- export { useSegments, type SegmentsState } from "./use-segments.js";
19
-
20
- // Handle data hook
21
- export { useHandle } from "./use-handle.js";
22
-
23
- // Mount-aware reverse hook
24
- export { useReverse } from "./use-reverse.js";
25
-
26
- // Provider
27
1
  export {
28
2
  NavigationProvider,
29
3
  type NavigationProviderProps,
30
4
  } from "./NavigationProvider.js";
31
-
32
- // Context (for advanced usage)
33
- export {
34
- NavigationStoreContext,
35
- type NavigationStoreContextValue,
36
- } from "./context.js";
37
-
38
- // Link component
39
- export { Link, type LinkProps, type PrefetchStrategy } from "./Link.js";
40
-
41
- // Link status hook
42
- export { useLinkStatus, type LinkStatus } from "./use-link-status.js";
43
-
44
- // Scroll restoration
45
- export {
46
- ScrollRestoration,
47
- useScrollRestoration,
48
- type ScrollRestorationProps,
49
- } from "./ScrollRestoration.js";
@@ -1,8 +1,3 @@
1
- /**
2
- * Shared location state utilities - works in both RSC and client contexts
3
- * No "use client" directive so it can be imported from RSC
4
- */
5
-
6
1
  import type { ReactElement } from "react";
7
2
 
8
3
  /**
@@ -26,16 +21,8 @@ export interface LocationStateOptions {
26
21
 
27
22
  type LocationStateUnsafeFn = (...args: never[]) => unknown;
28
23
 
29
- // Broadest constructor signature (`abstract` covers both abstract and concrete
30
- // classes). A class passed as state has a `new` signature, not a call signature,
31
- // so it slips past LocationStateUnsafeFn; at runtime the lazy-getter path
32
- // (`typeof value === "function"`) then mistakes it for a getter and throws.
33
24
  type LocationStateUnsafeCtor = abstract new (...args: never[]) => unknown;
34
25
 
35
- // `unknown` cannot be verified serializable, so it is rejected (callers must
36
- // supply a concrete type). `any` deliberately defeats type checking and is NOT
37
- // guardable — it is assignable to the branded error too, so the check always
38
- // passes; it remains an explicit escape hatch.
39
26
  type IsAny<T> = 0 extends 1 & T ? true : false;
40
27
  type IsUnknown<T> =
41
28
  IsAny<T> extends true ? false : unknown extends T ? true : false;
@@ -3,7 +3,6 @@
3
3
  import { useState, useEffect, useRef } from "react";
4
4
  import type { LocationStateDefinition } from "./location-state-shared.js";
5
5
 
6
- // Re-export shared utilities and types
7
6
  export {
8
7
  createLocationState,
9
8
  isLocationStateEntry,
@@ -24,32 +24,24 @@ const DEFAULT_ACTION_STATE: TrackedActionState = {
24
24
  result: null,
25
25
  };
26
26
 
27
- /**
28
- * Normalize action ID - returns the ID as-is
29
- *
30
- * Server actions have IDs like "hash#actionName" or "src/actions.ts#actionName".
31
- * When using function references, we use the full ID for exact matching.
32
- * When using strings, the event controller supports suffix matching
33
- * (e.g., "addToCart" matches "hash#addToCart").
34
- */
35
- function normalizeActionId(actionId: string): string {
36
- return actionId;
37
- }
38
-
39
27
  /**
40
28
  * Extract action ID from a server action function or string.
41
29
  *
42
30
  * Actions passed as props from server components lose their metadata
43
31
  * during RSC serialization - use a string action name instead.
32
+ *
33
+ * The extracted $$id (e.g. "hash#actionName" or "src/actions.ts#actionName")
34
+ * is returned as-is. Suffix-vs-exact matching against this ID happens
35
+ * downstream in the event controller, not here.
44
36
  */
45
- export function getActionId(action: ServerActionFunction | string): string {
37
+ function getActionId(action: ServerActionFunction | string): string {
46
38
  invariant(
47
39
  typeof action === "function" || typeof action === "string",
48
40
  `useAction: action must be a function or string, got ${typeof action}`,
49
41
  );
50
42
  const actionId = (action as any)?.$$id;
51
43
  if (actionId) {
52
- return normalizeActionId(actionId);
44
+ return actionId;
53
45
  }
54
46
 
55
47
  // If action is a string, use it directly
@@ -162,7 +154,6 @@ export function useAction<T>(
162
154
  });
163
155
  const prevSelected = useRef(baseState);
164
156
  prevSelected.current = baseState;
165
- // useOptimistic allows immediate updates during transitions/actions
166
157
  const [optimisticState, setOptimisticState] = useOptimistic<
167
158
  T | TrackedActionState
168
159
  >(null!);
@@ -43,7 +43,6 @@ export function useHandle<T, A, S>(
43
43
  ): Rango.FlightSerialize<A> | S {
44
44
  const ctx = useContext(NavigationStoreContext);
45
45
 
46
- // Initial state from context event controller, or empty fallback without provider.
47
46
  const [value, setValue] = useState<Rango.FlightSerialize<A> | S>(() => {
48
47
  if (!ctx) {
49
48
  const collected = collectHandleData(
@@ -54,7 +53,6 @@ export function useHandle<T, A, S>(
54
53
  return selector ? selector(collected) : collected;
55
54
  }
56
55
 
57
- // On client, use event controller state
58
56
  const state = ctx.eventController.getHandleState();
59
57
  const collected = collectHandleData(
60
58
  handle,
@@ -65,15 +63,12 @@ export function useHandle<T, A, S>(
65
63
  });
66
64
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
67
65
 
68
- // Track previous value for shallow comparison
69
66
  const prevValueRef = useRef(value);
70
67
  prevValueRef.current = value;
71
68
 
72
- // Ref keeps the latest selector without re-subscribing on every render.
73
69
  const selectorRef = useRef(selector);
74
70
  selectorRef.current = selector;
75
71
 
76
- // Subscribe to handle data changes (client only)
77
72
  useEffect(() => {
78
73
  if (!ctx) return;
79
74
 
@@ -82,11 +82,9 @@ export function useLinkStatus(): LinkStatus {
82
82
  const linkTo = useContext(LinkContext);
83
83
  const ctx = useContext(NavigationStoreContext);
84
84
 
85
- // Get origin for URL normalization (stable across renders)
86
85
  const origin =
87
86
  typeof window !== "undefined" ? window.location.origin : "http://localhost";
88
87
 
89
- // Base state for useOptimistic
90
88
  const [basePending, setBasePending] = useState<boolean>(() => {
91
89
  if (!ctx || linkTo === null) {
92
90
  return false;
@@ -97,7 +95,6 @@ export function useLinkStatus(): LinkStatus {
97
95
 
98
96
  const prevPending = useRef(basePending);
99
97
 
100
- // useOptimistic allows immediate updates during transitions
101
98
  const [pending, setOptimisticPending] = useOptimistic(basePending);
102
99
 
103
100
  useEffect(() => {
@@ -105,7 +102,6 @@ export function useLinkStatus(): LinkStatus {
105
102
  return;
106
103
  }
107
104
 
108
- // Subscribe to navigation state changes
109
105
  return ctx.eventController.subscribe(() => {
110
106
  const state = ctx.eventController.getState();
111
107
  const isPending = isPendingFor(linkTo, state.pendingUrl, origin);
@@ -46,7 +46,6 @@ export function useNavigation<T>(
46
46
  throw new Error("useNavigation must be used within NavigationProvider");
47
47
  }
48
48
 
49
- // Base state for useOptimistic
50
49
  const [baseValue, setBaseValue] = useState<T | PublicNavigationState>(() => {
51
50
  const publicState = toPublicState(ctx.eventController.getState());
52
51
  return selector ? selector(publicState) : publicState;
@@ -59,7 +58,6 @@ export function useNavigation<T>(
59
58
  // parent transition (e.g. <Link> click) is still pending.
60
59
  const optimisticPinnedRef = useRef(false);
61
60
 
62
- // useOptimistic allows immediate updates during transitions/actions
63
61
  const [value, setOptimisticValue] = useOptimistic(baseValue);
64
62
 
65
63
  // Store selector in a ref so the subscription callback always uses the
@@ -72,7 +70,6 @@ export function useNavigation<T>(
72
70
 
73
71
  // Subscribe to event controller state changes (only runs on client)
74
72
  useEffect(() => {
75
- // Subscribe to updates from event controller
76
73
  return ctx.eventController.subscribe(() => {
77
74
  const currentState = ctx.eventController.getState();
78
75
  const publicState = toPublicState(currentState);
@@ -50,8 +50,6 @@ export function useParams<T>(
50
50
  });
51
51
 
52
52
  const prevValue = useRef(value);
53
- // Ref keeps the latest selector without re-subscribing. Event-driven by
54
- // design: value updates on store events, not on selector identity change.
55
53
  const selectorRef = useRef(selector);
56
54
  selectorRef.current = selector;
57
55
 
@@ -60,7 +60,7 @@ export function useRouter(): RouterInstance {
60
60
  return ctx.refresh();
61
61
  },
62
62
 
63
- prefetch(url: string): void {
63
+ prefetch(url: string, options?: { key?: ":source" }): void {
64
64
  const segmentState = ctx.store?.getSegmentState();
65
65
  if (segmentState) {
66
66
  prefetchDirect(
@@ -68,6 +68,7 @@ export function useRouter(): RouterInstance {
68
68
  segmentState.currentSegmentIds,
69
69
  getAppVersion(),
70
70
  ctx.store?.getRouterId?.(),
71
+ options?.key,
71
72
  );
72
73
  }
73
74
  },
@@ -24,9 +24,6 @@ import type { ReadonlyURLSearchParams } from "../types.js";
24
24
  export function useSearchParams(): ReadonlyURLSearchParams {
25
25
  const ctx = useContext(NavigationStoreContext);
26
26
 
27
- // Always initialize with empty URLSearchParams to match SSR output
28
- // and avoid hydration mismatch. The useEffect below syncs from
29
- // the real URL after hydration.
30
27
  const [searchParams, setSearchParams] = useState<ReadonlyURLSearchParams>(
31
28
  () => new URLSearchParams(),
32
29
  );
@@ -41,12 +38,10 @@ export function useSearchParams(): ReadonlyURLSearchParams {
41
38
  const nextSearch = location.searchParams.toString();
42
39
  if (nextSearch !== prevSearch.current) {
43
40
  prevSearch.current = nextSearch;
44
- // Create a snapshot so callers cannot mutate the source URLSearchParams
45
41
  setSearchParams(new URLSearchParams(nextSearch));
46
42
  }
47
43
  };
48
44
 
49
- // Sync on mount (picks up search params from browser URL)
50
45
  update();
51
46
 
52
47
  return ctx.eventController.subscribe(update);
@@ -86,31 +86,20 @@ export function useSegments<T>(
86
86
  const selectorRef = useRef(selector);
87
87
  selectorRef.current = selector;
88
88
 
89
- // Track selector identity to detect when the selector function changes.
90
- // Only then do we eagerly recompute during render to avoid staleness.
91
- // Without this guard, no-selector mode causes infinite re-renders because
92
- // buildSegmentsState creates fresh arrays that fail Object.is checks.
93
89
  const prevSelectorIdentity = useRef(selector);
94
90
 
95
- // Cache SegmentsState to stabilize nested references (path, segmentIds
96
- // arrays) so selectors returning composite values don't cause spurious
97
- // render-time setState calls.
98
91
  const segmentsCache = useRef<{
99
92
  location: URL;
100
93
  routeSegmentIds: string[];
101
94
  state: SegmentsState;
102
95
  } | null>(null);
103
96
 
104
- // Recompute selected value from current store state and apply selector.
105
- // Shared by the render-time eager check and the subscription callback.
106
97
  function recompute(
107
98
  sel: ((state: SegmentsState) => T) | undefined,
108
99
  ): T | SegmentsState {
109
100
  const location = ctx!.eventController.getLocation();
110
101
  const handleState = ctx!.eventController.getHandleState();
111
102
 
112
- // Reuse cached state when inputs haven't changed by reference,
113
- // keeping array/object references stable for composite selectors.
114
103
  const cache = segmentsCache.current;
115
104
  let segmentsState: SegmentsState;
116
105
  if (
@@ -165,8 +154,6 @@ export function useSegments<T>(
165
154
  unsubscribeNav();
166
155
  unsubscribeHandles();
167
156
  };
168
- // Stable subscription: selector changes are handled via selectorRef,
169
- // state comparison uses prevState ref. No re-subscribe needed.
170
157
  // eslint-disable-next-line react-hooks/exhaustive-deps
171
158
  }, []);
172
159
 
@@ -8,6 +8,7 @@ import {
8
8
  generateHistoryKey,
9
9
  } from "./navigation-store.js";
10
10
  import { createEventController } from "./event-controller.js";
11
+ import { validateRedirectOrigin } from "./validate-redirect-origin.js";
11
12
  import { createNavigationClient } from "./navigation-client.js";
12
13
  import { createServerActionBridge } from "./server-action-bridge.js";
13
14
  import { createNavigationBridge } from "./navigation-bridge.js";
@@ -178,8 +179,8 @@ export async function initBrowserApp(
178
179
 
179
180
  // Register the active store on the module-level handle and wire the
180
181
  // jar-divergence observer before any getRangoState() read can detect a
181
- // cross-tab/server rotation. The real boot path never populates the
182
- // getNavigationStore() singleton, so this handle is the live reference.
182
+ // cross-tab/server rotation. There is no global store singleton, so this
183
+ // handle is the live reference.
183
184
  registerNavigationStore(store);
184
185
 
185
186
  // Seed router identity from the initial SSR payload so the first
@@ -280,7 +281,13 @@ export async function initBrowserApp(
280
281
  renderSegments,
281
282
  onNavigate: (url, options) => {
282
283
  if (!navigateFn) {
283
- window.location.href = url;
284
+ // Navigation bridge not wired yet: hard-navigate, but re-validate
285
+ // same-origin defensively so this init-window fallback cannot become an
286
+ // open redirect (the normal path validates inside the navigation bridge).
287
+ const safe = validateRedirectOrigin(url, window.location.origin);
288
+ if (safe) {
289
+ window.location.href = safe;
290
+ }
284
291
  return Promise.resolve();
285
292
  }
286
293
  return navigateFn(url, options);