@rangojs/router 0.18.0 → 0.19.0

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 (244) hide show
  1. package/dist/types/browser/event-controller.d.ts +3 -3
  2. package/dist/types/browser/history-state.d.ts +2 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  4. package/dist/types/browser/partial-update.d.ts +16 -1
  5. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  6. package/dist/types/browser/react/Link.d.ts +19 -9
  7. package/dist/types/browser/react/context.d.ts +2 -2
  8. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  9. package/dist/types/browser/react/location-state.d.ts +1 -1
  10. package/dist/types/browser/transition-when.d.ts +80 -0
  11. package/dist/types/browser/types.d.ts +104 -9
  12. package/dist/types/build/generate-manifest.d.ts +8 -0
  13. package/dist/types/cache/background-task.d.ts +24 -2
  14. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +44 -13
  16. package/dist/types/cache/cache-policy.d.ts +1 -1
  17. package/dist/types/cache/cache-runtime.d.ts +7 -0
  18. package/dist/types/cache/cache-scope.d.ts +117 -35
  19. package/dist/types/cache/cache-tag.d.ts +24 -4
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
  21. package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
  22. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
  23. package/dist/types/cache/document-cache.d.ts +10 -0
  24. package/dist/types/cache/handle-snapshot.d.ts +40 -8
  25. package/dist/types/cache/invalidation-order.d.ts +23 -0
  26. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +88 -141
  28. package/dist/types/cache/tag-invalidation.d.ts +38 -14
  29. package/dist/types/cache/types.d.ts +68 -61
  30. package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
  31. package/dist/types/client-urls/navigation.d.ts +28 -2
  32. package/dist/types/client-urls/server-projection.d.ts +5 -2
  33. package/dist/types/client-urls/types.d.ts +5 -5
  34. package/dist/types/client.d.ts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  38. package/dist/types/route-name.d.ts +7 -0
  39. package/dist/types/router/error-handling.d.ts +1 -1
  40. package/dist/types/router/intercept-resolution.d.ts +2 -0
  41. package/dist/types/router/match-context.d.ts +5 -0
  42. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  43. package/dist/types/router/router-options.d.ts +4 -1
  44. package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
  45. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  46. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
  47. package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
  48. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  49. package/dist/types/router/transition-when.d.ts +24 -10
  50. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  51. package/dist/types/rsc/full-payload.d.ts +14 -7
  52. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  53. package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
  54. package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
  55. package/dist/types/rsc/shell-capture.d.ts +78 -81
  56. package/dist/types/rsc/shell-serve.d.ts +19 -48
  57. package/dist/types/rsc/types.d.ts +2 -0
  58. package/dist/types/segment-system.d.ts +7 -0
  59. package/dist/types/server/context.d.ts +89 -61
  60. package/dist/types/server/cookie-store.d.ts +30 -18
  61. package/dist/types/server/handle-store.d.ts +52 -1
  62. package/dist/types/server/request-context.d.ts +66 -45
  63. package/dist/types/server/request-headers.d.ts +44 -0
  64. package/dist/types/server.d.ts +1 -0
  65. package/dist/types/testing/e2e/index.d.ts +1 -1
  66. package/dist/types/testing/index.d.ts +3 -2
  67. package/dist/types/testing/internal/action-id.d.ts +7 -0
  68. package/dist/types/testing/location-state-key.d.ts +24 -0
  69. package/dist/types/testing/render-route.d.ts +17 -1
  70. package/dist/types/testing/run-transition-when.d.ts +44 -69
  71. package/dist/types/testing/serve-shell-request.d.ts +7 -6
  72. package/dist/types/testing/shell-status.d.ts +34 -22
  73. package/dist/types/transition-when-ref.d.ts +79 -0
  74. package/dist/types/types/cache-types.d.ts +15 -3
  75. package/dist/types/types/index.d.ts +1 -1
  76. package/dist/types/types/segments.d.ts +86 -71
  77. package/dist/types/urls/path-helper-types.d.ts +3 -4
  78. package/dist/types/urls/pattern-types.d.ts +3 -4
  79. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  80. package/dist/vite/index.js +960 -97
  81. package/package.json +1 -1
  82. package/skills/cache-guide/SKILL.md +76 -49
  83. package/skills/caching/SKILL.md +167 -69
  84. package/skills/client-urls/SKILL.md +1 -1
  85. package/skills/comparison/references/framework-comparison.md +8 -5
  86. package/skills/document-cache/SKILL.md +24 -0
  87. package/skills/hooks/SKILL.md +3 -1
  88. package/skills/hooks/navigation.md +23 -2
  89. package/skills/hooks/state.md +98 -6
  90. package/skills/intercept/SKILL.md +29 -12
  91. package/skills/loader/SKILL.md +52 -21
  92. package/skills/middleware/SKILL.md +18 -0
  93. package/skills/migrate-nextjs/SKILL.md +3 -3
  94. package/skills/observability/SKILL.md +6 -5
  95. package/skills/parallel/SKILL.md +1 -1
  96. package/skills/ppr/SKILL.md +129 -77
  97. package/skills/prerender/SKILL.md +3 -3
  98. package/skills/rango/SKILL.md +25 -6
  99. package/skills/route/SKILL.md +2 -0
  100. package/skills/testing/SKILL.md +27 -25
  101. package/skills/testing/cache-prerender.md +9 -9
  102. package/skills/testing/client-components.md +45 -3
  103. package/skills/testing/loader.md +3 -2
  104. package/skills/testing/middleware.md +1 -0
  105. package/skills/testing/predicates.md +49 -49
  106. package/skills/testing/render-handler.md +1 -0
  107. package/skills/testing/serve-shell-request.md +14 -0
  108. package/skills/testing/server-actions.md +9 -3
  109. package/skills/theme/SKILL.md +8 -4
  110. package/skills/typesafety/env-and-bindings.md +8 -0
  111. package/skills/use-cache/SKILL.md +64 -19
  112. package/skills/view-transitions/SKILL.md +150 -29
  113. package/src/__augment-tests__/augmented.check.ts +23 -0
  114. package/src/browser/event-controller.ts +4 -4
  115. package/src/browser/history-state.ts +52 -1
  116. package/src/browser/link-interceptor.ts +4 -0
  117. package/src/browser/navigation-bridge.ts +77 -18
  118. package/src/browser/navigation-client.ts +4 -3
  119. package/src/browser/navigation-store.ts +43 -1
  120. package/src/browser/navigation-transaction.ts +31 -11
  121. package/src/browser/partial-update.ts +146 -35
  122. package/src/browser/prefetch/cache.ts +6 -6
  123. package/src/browser/prefetch/fetch.ts +2 -2
  124. package/src/browser/react/Link.tsx +38 -33
  125. package/src/browser/react/NavigationProvider.tsx +2 -2
  126. package/src/browser/react/context.ts +2 -2
  127. package/src/browser/react/location-state-shared.ts +241 -49
  128. package/src/browser/react/location-state.ts +5 -2
  129. package/src/browser/rsc-router.tsx +4 -0
  130. package/src/browser/server-action-bridge.ts +84 -15
  131. package/src/browser/transition-when.ts +205 -0
  132. package/src/browser/types.ts +124 -10
  133. package/src/build/generate-manifest.ts +77 -1
  134. package/src/cache/background-task.ts +55 -2
  135. package/src/cache/cache-exec-scope.ts +48 -1
  136. package/src/cache/cache-key-utils.ts +56 -16
  137. package/src/cache/cache-policy.ts +9 -4
  138. package/src/cache/cache-runtime.ts +197 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +105 -13
  141. package/src/cache/cf/cf-cache-store.ts +152 -76
  142. package/src/cache/cf/cf-shell-frame.ts +2 -2
  143. package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
  144. package/src/cache/document-cache.ts +53 -7
  145. package/src/cache/handle-snapshot.ts +52 -9
  146. package/src/cache/invalidation-order.ts +84 -0
  147. package/src/cache/request-tag-mask.ts +147 -0
  148. package/src/cache/shell-snapshot.ts +130 -348
  149. package/src/cache/tag-invalidation.ts +57 -51
  150. package/src/cache/types.ts +71 -67
  151. package/src/cache/vercel/vercel-cache-store.ts +61 -58
  152. package/src/client-urls/client-root.tsx +8 -4
  153. package/src/client-urls/client-urls.ts +8 -6
  154. package/src/client-urls/navigation.ts +55 -4
  155. package/src/client-urls/server-projection.ts +31 -10
  156. package/src/client-urls/types.ts +12 -5
  157. package/src/client.tsx +2 -0
  158. package/src/host/cookie-handler.ts +2 -1
  159. package/src/index.rsc.ts +3 -0
  160. package/src/index.ts +3 -0
  161. package/src/loader-redirect.tsx +7 -3
  162. package/src/prerender/build-shell-capture.ts +155 -146
  163. package/src/route-definition/dsl-helpers.ts +2 -0
  164. package/src/route-definition/helpers-types.ts +11 -11
  165. package/src/route-name.ts +12 -0
  166. package/src/router/content-negotiation.ts +4 -1
  167. package/src/router/error-handling.ts +5 -2
  168. package/src/router/handler-context.ts +10 -7
  169. package/src/router/intercept-resolution.ts +11 -1
  170. package/src/router/loader-resolution.ts +28 -3
  171. package/src/router/logging.ts +5 -3
  172. package/src/router/match-api.ts +62 -34
  173. package/src/router/match-context.ts +5 -0
  174. package/src/router/match-handlers.ts +9 -29
  175. package/src/router/match-middleware/background-revalidation.ts +3 -5
  176. package/src/router/match-middleware/cache-lookup.ts +35 -19
  177. package/src/router/match-middleware/cache-store.ts +15 -9
  178. package/src/router/match-result.ts +1 -0
  179. package/src/router/middleware.ts +2 -5
  180. package/src/router/navigation-snapshot.ts +6 -5
  181. package/src/router/prerender-match.ts +4 -0
  182. package/src/router/request-classification.ts +7 -4
  183. package/src/router/router-options.ts +4 -1
  184. package/src/router/segment-resolution/fresh.ts +0 -4
  185. package/src/router/segment-resolution/loader-cache.ts +150 -25
  186. package/src/router/segment-resolution/loader-mask.ts +3 -4
  187. package/src/router/segment-resolution/loader-snapshot.ts +15 -78
  188. package/src/router/segment-resolution/mask-nested.ts +47 -22
  189. package/src/router/segment-resolution/revalidation.ts +0 -3
  190. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  191. package/src/router/telemetry.ts +6 -3
  192. package/src/router/transition-when.ts +34 -52
  193. package/src/router.ts +2 -1
  194. package/src/rsc/attach-transition-when.ts +47 -0
  195. package/src/rsc/full-payload.ts +30 -10
  196. package/src/rsc/handler.ts +12 -10
  197. package/src/rsc/loader-fetch.ts +2 -1
  198. package/src/rsc/manifest-init.ts +4 -0
  199. package/src/rsc/origin-guard.ts +8 -5
  200. package/src/rsc/progressive-enhancement.ts +7 -30
  201. package/src/rsc/response-cache-serve.ts +68 -80
  202. package/src/rsc/response-route-handler.ts +2 -2
  203. package/src/rsc/rsc-rendering.ts +206 -252
  204. package/src/rsc/server-action.ts +8 -20
  205. package/src/rsc/shell-build-manifest.ts +22 -20
  206. package/src/rsc/shell-capture-constants.ts +136 -20
  207. package/src/rsc/shell-capture.ts +352 -589
  208. package/src/rsc/shell-serve.ts +73 -109
  209. package/src/rsc/ssr-setup.ts +3 -2
  210. package/src/rsc/types.ts +2 -0
  211. package/src/segment-system.tsx +40 -7
  212. package/src/server/context.ts +189 -124
  213. package/src/server/cookie-store.ts +153 -127
  214. package/src/server/handle-store.ts +245 -27
  215. package/src/server/request-context.ts +123 -84
  216. package/src/server/request-headers.ts +134 -0
  217. package/src/server.ts +4 -0
  218. package/src/testing/dispatch.ts +4 -7
  219. package/src/testing/e2e/index.ts +1 -0
  220. package/src/testing/index.ts +5 -0
  221. package/src/testing/internal/action-id.ts +25 -0
  222. package/src/testing/internal/context.ts +1 -1
  223. package/src/testing/location-state-key.ts +45 -0
  224. package/src/testing/render-handler.ts +1 -1
  225. package/src/testing/render-route.tsx +109 -16
  226. package/src/testing/run-client-revalidate.ts +3 -19
  227. package/src/testing/run-loader.ts +5 -1
  228. package/src/testing/run-transition-when.ts +109 -172
  229. package/src/testing/serve-shell-request.ts +10 -7
  230. package/src/testing/shell-status.ts +60 -56
  231. package/src/transition-when-ref.ts +220 -0
  232. package/src/types/cache-types.ts +15 -3
  233. package/src/types/index.ts +3 -0
  234. package/src/types/segments.ts +94 -91
  235. package/src/urls/path-helper-types.ts +3 -4
  236. package/src/urls/pattern-types.ts +3 -4
  237. package/src/vite/discovery/discover-routers.ts +3 -0
  238. package/src/vite/discovery/shell-prerender-phase.ts +0 -2
  239. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  240. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  241. package/src/vite/rango.ts +2 -0
  242. package/src/vite/router-discovery.ts +75 -5
  243. package/dist/types/rsc/transition-gate.d.ts +0 -23
  244. package/src/rsc/transition-gate.ts +0 -86
@@ -13,30 +13,23 @@ import React, {
13
13
  } from "react";
14
14
  import { NavigationStoreContext } from "./context.js";
15
15
  import { LinkContext } from "./use-link-status.js";
16
- import type { NavigateOptions } from "../types.js";
16
+ import type { HistoryState } from "../types.js";
17
17
  import {
18
18
  isHashOnlyNavigation,
19
19
  isPrefetchScopeDisabled,
20
20
  subscribeToPrefetchScopeChange,
21
21
  } from "../link-interceptor.js";
22
22
  import { subscribeToLocationChange } from "../event-controller.js";
23
- import {
24
- isLocationStateEntry,
25
- type LocationStateEntry,
26
- resolveLocationStateEntries,
27
- } from "./location-state.js";
23
+ import { isLocationStateDefinition } from "./location-state-shared.js";
28
24
 
29
- /**
30
- * State prop type for Link component.
31
- * - LocationStateEntry[]: Type-safe state entries via createLocationState()
32
- * - StateOrGetter: Plain state object or click-time getter function
33
- * - Record<string, unknown>: Plain state object passed to history.pushState
34
- */
35
25
  export type StateOrGetter<T = unknown> = T | (() => T);
36
26
 
37
- export type LinkState =
38
- | LocationStateEntry[]
39
- | StateOrGetter<Record<string, unknown>>;
27
+ /**
28
+ * State prop type for Link component: the `router.push()` state type
29
+ * ({@link HistoryState}: `LocationStateEntry[]` from createLocationState(), or
30
+ * plain structured-clone-safe state), or a getter for it called at click time.
31
+ */
32
+ export type LinkState = StateOrGetter<HistoryState>;
40
33
 
41
34
  import {
42
35
  prefetchDirect,
@@ -93,6 +86,14 @@ export interface LinkProps extends Omit<
93
86
  * @default true
94
87
  */
95
88
  revalidate?: boolean;
89
+ /**
90
+ * Set to `false` to present this navigation without a transition (urgent
91
+ * commit, `<ViewTransition>` classes `"none"`); no `transition({ when })`
92
+ * predicate is called. See `NavigateOptions.transition`.
93
+ *
94
+ * @default true
95
+ */
96
+ transition?: boolean;
96
97
  /**
97
98
  * Prefetch strategy for the link destination. When omitted, falls back to
98
99
  * the router-wide default (`createRouter({ defaultPrefetch })`: `"none"` in
@@ -118,8 +119,11 @@ export interface LinkProps extends Omit<
118
119
  * therefore serve the wrong diff to a navigation from a different
119
120
  * source.
120
121
  *
121
- * Intercept responses are auto-scoped to the source via a server-side
122
- * tag, so `":source"` is only needed for custom revalidation logic.
122
+ * A route an intercept targets is scoped to the source automatically
123
+ * (server-side `x-rsc-prefetch-scope: source`), whether or not the
124
+ * intercept applies from this page: the modal and the full page are
125
+ * both source-specific. Every other route stays shared, so `":source"`
126
+ * is only needed for custom revalidation logic.
123
127
  *
124
128
  * @example
125
129
  * ```tsx
@@ -226,6 +230,7 @@ export const Link: ForwardRefExoticComponent<
226
230
  scroll = true,
227
231
  reloadDocument = false,
228
232
  revalidate,
233
+ transition,
229
234
  prefetch,
230
235
  prefetchKey,
231
236
  state,
@@ -334,28 +339,26 @@ export const Link: ForwardRefExoticComponent<
334
339
  // Stop propagation to prevent link-interceptor from also handling this
335
340
  e.stopPropagation();
336
341
 
342
+ // Call a click-time getter; typed entries are resolved (and, in dev,
343
+ // checked) by the bridge's resolveNavigationState, like router.push().
344
+ // In dev an uncalled definition passes through to that check instead of
345
+ // being called as a getter.
337
346
  const currentState = stateRef.current;
338
- let resolvedState: unknown;
339
-
340
- if (
341
- Array.isArray(currentState) &&
342
- currentState.length > 0 &&
343
- isLocationStateEntry(currentState[0])
344
- ) {
345
- resolvedState = resolveLocationStateEntries(
346
- currentState as LocationStateEntry[],
347
- );
348
- } else if (typeof currentState === "function") {
349
- resolvedState = currentState();
350
- } else if (currentState != null) {
351
- resolvedState = currentState;
352
- }
347
+ const state =
348
+ typeof currentState === "function" &&
349
+ !(
350
+ process.env.NODE_ENV !== "production" &&
351
+ isLocationStateDefinition(currentState)
352
+ )
353
+ ? currentState()
354
+ : (currentState ?? undefined);
353
355
 
354
356
  ctx.navigate(resolvedTo, {
355
357
  replace,
356
358
  scroll,
357
- state: resolvedState,
359
+ state,
358
360
  revalidate,
361
+ transition,
359
362
  });
360
363
  },
361
364
  [
@@ -365,6 +368,7 @@ export const Link: ForwardRefExoticComponent<
365
368
  replace,
366
369
  scroll,
367
370
  revalidate,
371
+ transition,
368
372
  ctx,
369
373
  onClick,
370
374
  ],
@@ -487,6 +491,7 @@ export const Link: ForwardRefExoticComponent<
487
491
  data-scroll={scroll === false ? "false" : undefined}
488
492
  data-replace={replace ? "true" : undefined}
489
493
  data-revalidate={revalidate === false ? "false" : undefined}
494
+ data-transition={transition === false ? "false" : undefined}
490
495
  {...props}
491
496
  >
492
497
  <LinkContext.Provider value={resolvedTo}>{children}</LinkContext.Provider>
@@ -17,7 +17,7 @@ import {
17
17
  import type {
18
18
  NavigationStore,
19
19
  NavigationUpdate,
20
- NavigateOptions,
20
+ NavigateOptionsInternal,
21
21
  NavigationBridge,
22
22
  } from "../types.js";
23
23
  import type { EventController } from "../event-controller.js";
@@ -320,7 +320,7 @@ export function NavigationProvider({
320
320
  * Navigate to a URL (delegates to bridge)
321
321
  */
322
322
  const navigate = useCallback(
323
- async (url: string, options?: NavigateOptions): Promise<void> => {
323
+ async (url: string, options?: NavigateOptionsInternal): Promise<void> => {
324
324
  await bridge.navigate(url, options);
325
325
  },
326
326
  [],
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { createContext, type Context } from "react";
4
- import type { NavigationStore, NavigateOptions } from "../types.js";
4
+ import type { NavigationStore, NavigateOptionsInternal } from "../types.js";
5
5
  import type { EventController } from "../event-controller.js";
6
6
  import type { PrefetchStrategy } from "../../router/prefetch-default.js";
7
7
 
@@ -34,7 +34,7 @@ export interface NavigationStoreContextValue {
34
34
  * @param options - Navigation options (replace, scroll)
35
35
  * @returns Promise that resolves when navigation is complete
36
36
  */
37
- navigate: (url: string, options?: NavigateOptions) => Promise<void>;
37
+ navigate: (url: string, options?: NavigateOptionsInternal) => Promise<void>;
38
38
 
39
39
  /**
40
40
  * Refresh the current route
@@ -27,17 +27,64 @@ type IsAny<T> = 0 extends 1 & T ? true : false;
27
27
  type IsUnknown<T> =
28
28
  IsAny<T> extends true ? false : unknown extends T ? true : false;
29
29
 
30
+ type UnknownReason =
31
+ "`unknown` cannot be verified as serializable; give it a concrete type";
32
+ type FunctionReason = "functions cannot be stored in location state";
33
+ type ConstructorReason =
34
+ "class constructors cannot be stored in location state";
35
+ type SymbolReason = "symbols cannot be stored in location state";
36
+ type ReactReason =
37
+ "React/RSC content cannot be stored in location state; store plain data and render it on arrival";
38
+ type DepthReason =
39
+ "nested too deep to locate the unsafe field; it is below this path";
40
+
30
41
  /**
31
42
  * Branded error surfaced when a value that cannot live in location state is
32
43
  * used. Location state is written into `history.state`, which uses the
33
44
  * structured clone algorithm; React elements, functions, and symbols throw a
34
- * `DataCloneError` at runtime. Carries a human-readable reason so the compile
35
- * error explains the fix.
45
+ * `DataCloneError` at runtime. Carries the reason and the offending field's
46
+ * path (`items[].info.values`; `<root>` for the value itself) so the compile
47
+ * error names the field to fix.
36
48
  */
37
- export type LocationStateUnsafe<Reason extends string> = {
49
+ export type LocationStateUnsafe<
50
+ Reason extends string,
51
+ Path extends string = "<root>",
52
+ > = {
38
53
  readonly __rango_location_state_unsafe: Reason;
54
+ readonly __rango_location_state_path: Path;
39
55
  };
40
56
 
57
+ /**
58
+ * Why a value cannot be structured-cloned, for the kinds that fail on their
59
+ * own (not through something nested in them), or `never`. Distributes over a
60
+ * union, one reason per unsafe member.
61
+ */
62
+ type UnsafeReason<T> = T extends LocationStateUnsafeFn
63
+ ? FunctionReason
64
+ : T extends LocationStateUnsafeCtor
65
+ ? ConstructorReason
66
+ : T extends symbol
67
+ ? SymbolReason
68
+ : T extends ReactElement
69
+ ? ReactReason
70
+ : never;
71
+
72
+ /** Primitives and structured-clone built-ins, stored as-is. */
73
+ type LocationStatePassthrough =
74
+ | string
75
+ | number
76
+ | boolean
77
+ | bigint
78
+ | null
79
+ | undefined
80
+ | Date
81
+ | RegExp
82
+ | ArrayBuffer
83
+ | ArrayBufferView
84
+ | Blob
85
+ | File
86
+ | FormData;
87
+
41
88
  /**
42
89
  * Maps `T` to itself when it is safe to store in location state, or to a branded
43
90
  * {@link LocationStateUnsafe} error for the disallowed parts: `unknown`, React
@@ -50,48 +97,134 @@ export type LocationStateUnsafe<Reason extends string> = {
50
97
  */
51
98
  export type LocationStateSafe<T> =
52
99
  IsUnknown<T> extends true
53
- ? LocationStateUnsafe<"location state needs an explicit, concrete type; `unknown` cannot be verified as serializable">
54
- : T extends LocationStateUnsafeFn
55
- ? LocationStateUnsafe<"functions cannot be stored in location state">
56
- : T extends LocationStateUnsafeCtor
57
- ? LocationStateUnsafe<"class constructors cannot be stored in location state">
58
- : T extends symbol
59
- ? LocationStateUnsafe<"symbols cannot be stored in location state">
60
- : T extends ReactElement
61
- ? LocationStateUnsafe<"React/RSC content cannot be stored in location state; store plain data and render it on arrival">
62
- : T extends string | number | boolean | bigint | null | undefined
63
- ? T
64
- : T extends
65
- | Date
66
- | RegExp
67
- | ArrayBuffer
68
- | ArrayBufferView
69
- | Blob
70
- | File
71
- | FormData
72
- ? T
73
- : T extends ReadonlyMap<infer K, infer V>
74
- ? ReadonlyMap<LocationStateSafe<K>, LocationStateSafe<V>>
75
- : T extends ReadonlySet<infer V>
76
- ? ReadonlySet<LocationStateSafe<V>>
77
- : T extends readonly unknown[]
78
- ? { [K in keyof T]: LocationStateSafe<T[K]> }
79
- : T extends object
80
- ? { [K in keyof T]: LocationStateSafe<T[K]> }
81
- : T;
100
+ ? LocationStateUnsafe<UnknownReason>
101
+ : T extends unknown
102
+ ? [UnsafeReason<T>] extends [never]
103
+ ? T extends LocationStatePassthrough
104
+ ? T
105
+ : T extends ReadonlyMap<infer K, infer V>
106
+ ? ReadonlyMap<LocationStateSafe<K>, LocationStateSafe<V>>
107
+ : T extends ReadonlySet<infer V>
108
+ ? ReadonlySet<LocationStateSafe<V>>
109
+ : T extends object
110
+ ? { [K in keyof T]: LocationStateSafe<T[K]> }
111
+ : T
112
+ : LocationStateUnsafe<UnsafeReason<T>>
113
+ : never;
114
+
115
+ type AtPath<P extends string> = P extends "" ? "<root>" : P;
116
+
117
+ /** `a.b` for a named key; `a[string]` / `a[number]` for an index signature. */
118
+ type ChildPath<P extends string, K> = string extends K
119
+ ? `${P}[string]`
120
+ : number extends K
121
+ ? `${P}[number]`
122
+ : K extends string | number
123
+ ? P extends ""
124
+ ? `${K}`
125
+ : `${P}.${K}`
126
+ : `${P}[symbol]`;
127
+
128
+ type UnsafeAt<R, P extends string> = R extends string
129
+ ? LocationStateUnsafe<R, AtPath<P>>
130
+ : never;
131
+
132
+ /**
133
+ * Walks `T` to the fields {@link LocationStateSafe} rejects and returns one
134
+ * {@link LocationStateUnsafe} brand per offending path (a union when several
135
+ * fail). Reports the shallowest failures: a value that is itself unsafe stops
136
+ * the walk, and an object with unsafe fields of its own reports only those, so
137
+ * a class instance or DOM node stops at its methods instead of walking its
138
+ * whole object graph (`{ el: HTMLElement }` took 8.4M instantiations and hit
139
+ * TS2589 before). Descends only into children that fail the cached
140
+ * `LocationStateSafe` check, and stops at depth 8.
141
+ */
142
+ type LocateUnsafe<
143
+ T,
144
+ P extends string,
145
+ D extends unknown[],
146
+ > = D["length"] extends 8
147
+ ? LocationStateUnsafe<DepthReason, AtPath<P>>
148
+ : IsUnknown<T> extends true
149
+ ? LocationStateUnsafe<UnknownReason, AtPath<P>>
150
+ : [UnsafeReason<T>] extends [never]
151
+ ? LocateNested<T, P, D>
152
+ : UnsafeAt<UnsafeReason<T>, P>;
153
+
154
+ type LocateNested<
155
+ T,
156
+ P extends string,
157
+ D extends unknown[],
158
+ > = T extends LocationStatePassthrough
159
+ ? never
160
+ : T extends ReadonlyMap<infer K, infer V>
161
+ ? LocateChild<K, `${P}<key>`, D> | LocateChild<V, `${P}<value>`, D>
162
+ : T extends ReadonlySet<infer V>
163
+ ? LocateChild<V, `${P}[]`, D>
164
+ : T extends readonly unknown[]
165
+ ? number extends T["length"]
166
+ ? LocateChild<T[number], `${P}[]`, D>
167
+ : {
168
+ [K in keyof T & `${number}`]-?: LocateChild<
169
+ T[K],
170
+ `${P}[${K}]`,
171
+ D
172
+ >;
173
+ }[keyof T & `${number}`]
174
+ : T extends object
175
+ ? LocateFields<T, P, D, OwnUnsafeKeys<T>>
176
+ : never;
177
+
178
+ /**
179
+ * Keys whose value is unsafe itself, not through something nested in it.
180
+ * `any` is never one: it passes LocationStateSafe, so reporting it would
181
+ * leave nothing to report and hide its siblings' paths.
182
+ */
183
+ type OwnUnsafeKeys<T> = {
184
+ [K in keyof T]-?: IsAny<T[K]> extends true
185
+ ? never
186
+ : IsUnknown<T[K]> extends true
187
+ ? K
188
+ : [UnsafeReason<T[K]>] extends [never]
189
+ ? never
190
+ : K;
191
+ }[keyof T];
192
+
193
+ type LocateFields<
194
+ T,
195
+ P extends string,
196
+ D extends unknown[],
197
+ Own extends keyof T,
198
+ > = [Own] extends [never]
199
+ ? { [K in keyof T]-?: LocateChild<T[K], ChildPath<P, K>, D> }[keyof T]
200
+ : { [K in Own]-?: LocateChild<T[K], ChildPath<P, K>, D> }[Own];
201
+
202
+ type LocateChild<T, P extends string, D extends unknown[]> = [T] extends [
203
+ LocationStateSafe<T>,
204
+ ]
205
+ ? never
206
+ : LocateUnsafe<T, P, [...D, unknown]>;
207
+
208
+ type GenericReason =
209
+ "location state must be serializable: React/RSC content, functions, and symbols cannot be stored — pass plain data and render it on arrival";
82
210
 
83
211
  /**
84
212
  * `unknown` (a no-op) when `T` is safe to store in location state, otherwise a
85
- * branded {@link LocationStateUnsafe} object. Intersected into the value
86
- * parameter of a definition's call and `write()` so POSTING RSC content (or any
87
- * non-serializable value) is a compile error whose text carries the reason —
88
- * without a `TState extends ...` self-constraint, which TypeScript rejects as
89
- * circular (TS2313). For safe `T`, `value & unknown` collapses back to `value`,
90
- * so valid usage is unchanged.
213
+ * branded {@link LocationStateUnsafe} object naming the reason and the path of
214
+ * each offending field, e.g. `LocationStateUnsafe<"...", "items[].info.values">`.
215
+ * Intersected into the value parameter of a definition's call and `write()` so
216
+ * POSTING RSC content (or any non-serializable value) is a compile error whose
217
+ * text carries the reason and the field — without a `TState extends ...`
218
+ * self-constraint, which TypeScript rejects as circular (TS2313). For safe `T`,
219
+ * `value & unknown` collapses back to `value`, so valid usage is unchanged.
91
220
  */
92
221
  export type ValidateLocationState<T> = [T] extends [LocationStateSafe<T>]
93
222
  ? unknown
94
- : LocationStateUnsafe<"location state must be serializable: React/RSC content, functions, and symbols cannot be stored — pass plain data and render it on arrival">;
223
+ : OrGenericUnsafe<LocateUnsafe<T, "", []>>;
224
+
225
+ type OrGenericUnsafe<U> = [U] extends [never]
226
+ ? LocationStateUnsafe<GenericReason>
227
+ : U;
95
228
 
96
229
  /**
97
230
  * Type-safe location state definition
@@ -101,7 +234,10 @@ export type ValidateLocationState<T> = [T] extends [LocationStateSafe<T>]
101
234
  */
102
235
  export interface LocationStateDefinition<TArgs extends unknown[], TState> {
103
236
  (...args: TArgs): LocationStateEntry;
104
- /** Injected by Vite plugin - do not set manually */
237
+ /**
238
+ * Injected by Vite plugin - do not set manually. Unit tests without the
239
+ * plugin use withLocationStateKey() from @rangojs/router/testing.
240
+ */
105
241
  __rsc_ls_key: string;
106
242
  /** Whether this state auto-clears after first read */
107
243
  readonly __rsc_ls_flash: boolean;
@@ -117,6 +253,13 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
117
253
  * want React to manage subscription/hydration for you.
118
254
  */
119
255
  read(): TState | undefined;
256
+ /**
257
+ * Read this definition's slot from a location snapshot instead of the
258
+ * current entry: a transition({ when }) context's `from` / `to`, or any
259
+ * `{ state }` holding a `history.state` object. A snapshot read never clears
260
+ * flash state.
261
+ */
262
+ read(location: { readonly state: unknown }): TState | undefined;
120
263
  /**
121
264
  * Statically write the value into the current history entry under this
122
265
  * definition's key, preserving any other keys already on history.state
@@ -187,6 +330,26 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
187
330
  * ProductState.delete();
188
331
  * ```
189
332
  */
333
+ let onCurrentStateReplaced: (() => void) | undefined;
334
+
335
+ /**
336
+ * @internal The navigation store registers here (navigation-store.ts) to
337
+ * refresh its per-entry memory: back/forward reads the entry being left from
338
+ * that memory (at popstate history.state already belongs to the destination),
339
+ * so a write after the entry's commit must reach it.
340
+ */
341
+ export function setCurrentHistoryStateListener(
342
+ listener: (() => void) | undefined,
343
+ ): void {
344
+ onCurrentStateReplaced = listener;
345
+ }
346
+
347
+ /** Replace the current entry's history.state in place (same URL). */
348
+ export function replaceCurrentHistoryState(state: unknown): void {
349
+ window.history.replaceState(state, "", window.location.href);
350
+ onCurrentStateReplaced?.();
351
+ }
352
+
190
353
  export function createLocationState<TState>(
191
354
  options?: LocationStateOptions,
192
355
  ): LocationStateDefinition<
@@ -196,12 +359,17 @@ export function createLocationState<TState>(
196
359
  const flash = options?.flash ?? false;
197
360
  let _key: string | undefined;
198
361
 
362
+ // Dev and test throw; production folds the check away (the plugin always
363
+ // injects the key there). Without it, reads and writes silently target
364
+ // history.state["undefined"].
199
365
  function getKey(): string {
200
- if (!_key && process.env.NODE_ENV === "development") {
366
+ if (!_key && process.env.NODE_ENV !== "production") {
201
367
  throw new Error(
202
368
  "[rango] createLocationState key not set. " +
203
369
  "Make sure the exposeInternalIds Vite plugin is enabled and " +
204
- "the state is exported with: export const MyState = createLocationState(...)",
370
+ "the state is exported with: export const MyState = createLocationState(...). " +
371
+ "In a unit test without the plugin, assign one with " +
372
+ "withLocationStateKey(MyState) from @rangojs/router/testing.",
205
373
  );
206
374
  }
207
375
  return _key!;
@@ -239,7 +407,13 @@ export function createLocationState<TState>(
239
407
  });
240
408
 
241
409
  Object.defineProperty(fn, "read", {
242
- value: (): TState | undefined => {
410
+ value: (location?: { readonly state: unknown }): TState | undefined => {
411
+ if (location !== undefined) {
412
+ const state = location.state;
413
+ return state !== null && typeof state === "object"
414
+ ? ((state as Record<string, unknown>)[getKey()] as TState | undefined)
415
+ : undefined;
416
+ }
243
417
  if (typeof window === "undefined") return undefined;
244
418
  return window.history.state?.[getKey()] as TState | undefined;
245
419
  },
@@ -263,11 +437,7 @@ export function createLocationState<TState>(
263
437
  const existing = window.history.state;
264
438
  const current =
265
439
  existing !== null && typeof existing === "object" ? existing : {};
266
- window.history.replaceState(
267
- { ...current, [key]: value },
268
- "",
269
- window.location.href,
270
- );
440
+ replaceCurrentHistoryState({ ...current, [key]: value });
271
441
  },
272
442
  enumerable: true,
273
443
  });
@@ -290,7 +460,7 @@ export function createLocationState<TState>(
290
460
  return;
291
461
  const next = { ...current };
292
462
  delete next[key];
293
- window.history.replaceState(next, "", window.location.href);
463
+ replaceCurrentHistoryState(next);
294
464
  },
295
465
  enumerable: true,
296
466
  });
@@ -301,6 +471,28 @@ export function createLocationState<TState>(
301
471
  >;
302
472
  }
303
473
 
474
+ /**
475
+ * A definition's key, or undefined when none is set, without the missing-key
476
+ * throw of the `__rsc_ls_key` getter. For dev checks and test helpers.
477
+ */
478
+ export function peekLocationStateKey(definition: object): string | undefined {
479
+ try {
480
+ return (definition as { __rsc_ls_key?: string }).__rsc_ls_key || undefined;
481
+ } catch {
482
+ return undefined;
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Check if a value is a LocationStateDefinition (a createLocationState()
488
+ * result, callable, not an entry).
489
+ */
490
+ export function isLocationStateDefinition(
491
+ value: unknown,
492
+ ): value is LocationStateDefinition<unknown[], unknown> {
493
+ return typeof value === "function" && "__rsc_ls_flash" in value;
494
+ }
495
+
304
496
  /**
305
497
  * Check if a value is a LocationStateEntry
306
498
  */
@@ -1,7 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useEffect, useRef } from "react";
4
- import type { LocationStateDefinition } from "./location-state-shared.js";
4
+ import {
5
+ replaceCurrentHistoryState,
6
+ type LocationStateDefinition,
7
+ } from "./location-state-shared.js";
5
8
 
6
9
  export {
7
10
  createLocationState,
@@ -124,7 +127,7 @@ export function useLocationState<TArgs extends unknown[], TState>(
124
127
  if (isFlash && key && state !== undefined) {
125
128
  const cleaned = { ...window.history.state };
126
129
  delete cleaned[key];
127
- window.history.replaceState(cleaned, "", window.location.href);
130
+ replaceCurrentHistoryState(cleaned);
128
131
  }
129
132
  }, [isFlash, key, state]);
130
133
 
@@ -226,6 +226,10 @@ export async function initBrowserApp(
226
226
  ...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
227
227
  });
228
228
 
229
+ // The document's entry: the transition({ when }) source of the first
230
+ // navigation, and of a later back/forward that leaves it.
231
+ store.rememberDisplayedEntry(initialPayload.metadata?.routeName);
232
+
229
233
  // Register the active store on the module-level handle and wire the
230
234
  // jar-divergence observer before any getRangoState() read can detect a
231
235
  // cross-tab/server rotation. There is no global store singleton, so this