@rangojs/router 0.17.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 (293) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/event-controller.d.ts +3 -3
  4. package/dist/types/browser/history-state.d.ts +18 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  6. package/dist/types/browser/partial-update.d.ts +16 -1
  7. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  8. package/dist/types/browser/react/Link.d.ts +19 -9
  9. package/dist/types/browser/react/context.d.ts +2 -2
  10. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  11. package/dist/types/browser/react/location-state.d.ts +1 -1
  12. package/dist/types/browser/transition-when.d.ts +80 -0
  13. package/dist/types/browser/types.d.ts +104 -9
  14. package/dist/types/build/generate-manifest.d.ts +8 -0
  15. package/dist/types/cache/background-task.d.ts +24 -2
  16. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +48 -2
  18. package/dist/types/cache/cache-policy.d.ts +18 -1
  19. package/dist/types/cache/cache-runtime.d.ts +7 -0
  20. package/dist/types/cache/cache-scope.d.ts +192 -26
  21. package/dist/types/cache/cache-tag.d.ts +116 -11
  22. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  23. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  24. package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
  25. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  26. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  27. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
  28. package/dist/types/cache/document-cache.d.ts +10 -0
  29. package/dist/types/cache/handle-snapshot.d.ts +41 -8
  30. package/dist/types/cache/index.d.ts +1 -0
  31. package/dist/types/cache/invalidation-order.d.ts +23 -0
  32. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  33. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  34. package/dist/types/cache/shell-memo.d.ts +178 -0
  35. package/dist/types/cache/shell-snapshot.d.ts +141 -105
  36. package/dist/types/cache/tag-invalidation.d.ts +54 -3
  37. package/dist/types/cache/types.d.ts +267 -96
  38. package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
  39. package/dist/types/client-urls/navigation.d.ts +28 -2
  40. package/dist/types/client-urls/server-projection.d.ts +5 -2
  41. package/dist/types/client-urls/types.d.ts +5 -5
  42. package/dist/types/client.d.ts +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +1 -1
  45. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  46. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  47. package/dist/types/route-name.d.ts +7 -0
  48. package/dist/types/router/error-handling.d.ts +1 -1
  49. package/dist/types/router/instrument.d.ts +8 -0
  50. package/dist/types/router/intercept-resolution.d.ts +2 -0
  51. package/dist/types/router/match-context.d.ts +5 -0
  52. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  53. package/dist/types/router/middleware-types.d.ts +6 -1
  54. package/dist/types/router/router-options.d.ts +4 -1
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
  56. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  57. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
  59. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  60. package/dist/types/router/transition-when.d.ts +24 -10
  61. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  62. package/dist/types/rsc/full-payload.d.ts +21 -0
  63. package/dist/types/rsc/handler.d.ts +1 -1
  64. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  65. package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
  66. package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
  67. package/dist/types/rsc/shell-capture.d.ts +202 -74
  68. package/dist/types/rsc/shell-serve.d.ts +144 -50
  69. package/dist/types/rsc/types.d.ts +9 -0
  70. package/dist/types/segment-system.d.ts +7 -0
  71. package/dist/types/server/context.d.ts +155 -33
  72. package/dist/types/server/cookie-store.d.ts +33 -0
  73. package/dist/types/server/handle-store.d.ts +52 -1
  74. package/dist/types/server/request-context.d.ts +210 -63
  75. package/dist/types/server/request-headers.d.ts +44 -0
  76. package/dist/types/server.d.ts +1 -0
  77. package/dist/types/ssr/index.d.ts +9 -0
  78. package/dist/types/ssr/ssr-root.d.ts +9 -8
  79. package/dist/types/testing/e2e/index.d.ts +1 -1
  80. package/dist/types/testing/flight.entry.d.ts +7 -0
  81. package/dist/types/testing/index.d.ts +3 -2
  82. package/dist/types/testing/internal/action-id.d.ts +7 -0
  83. package/dist/types/testing/location-state-key.d.ts +24 -0
  84. package/dist/types/testing/render-route.d.ts +17 -1
  85. package/dist/types/testing/run-loader.d.ts +18 -3
  86. package/dist/types/testing/run-transition-when.d.ts +44 -69
  87. package/dist/types/testing/serve-shell-request.d.ts +133 -0
  88. package/dist/types/testing/shell-status.d.ts +44 -19
  89. package/dist/types/transition-when-ref.d.ts +79 -0
  90. package/dist/types/types/cache-types.d.ts +15 -3
  91. package/dist/types/types/handler-context.d.ts +14 -1
  92. package/dist/types/types/index.d.ts +1 -1
  93. package/dist/types/types/segments.d.ts +86 -71
  94. package/dist/types/urls/path-helper-types.d.ts +3 -4
  95. package/dist/types/urls/pattern-types.d.ts +21 -18
  96. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  97. package/dist/types/vite/router-discovery.d.ts +29 -2
  98. package/dist/vite/index.js +1005 -105
  99. package/package.json +1 -1
  100. package/skills/breadcrumbs/SKILL.md +17 -7
  101. package/skills/cache-guide/SKILL.md +175 -76
  102. package/skills/caching/SKILL.md +361 -33
  103. package/skills/catalog.json +1 -1
  104. package/skills/client-urls/SKILL.md +12 -7
  105. package/skills/cloudflare/SKILL.md +15 -0
  106. package/skills/comparison/references/framework-comparison.md +52 -37
  107. package/skills/defer-hydration/SKILL.md +50 -30
  108. package/skills/deployment-caching/SKILL.md +5 -2
  109. package/skills/document-cache/SKILL.md +32 -2
  110. package/skills/handler-use/SKILL.md +2 -0
  111. package/skills/hooks/SKILL.md +3 -1
  112. package/skills/hooks/handle-and-actions.md +3 -1
  113. package/skills/hooks/navigation.md +23 -2
  114. package/skills/hooks/state.md +101 -7
  115. package/skills/intercept/SKILL.md +29 -12
  116. package/skills/loader/SKILL.md +197 -44
  117. package/skills/middleware/SKILL.md +34 -9
  118. package/skills/migrate-nextjs/SKILL.md +68 -37
  119. package/skills/migrate-react-router/SKILL.md +2 -1
  120. package/skills/migrate-react-router/component-migration.md +3 -1
  121. package/skills/migrate-react-router/data-and-actions.md +3 -1
  122. package/skills/migrate-react-router/route-mapping.md +7 -4
  123. package/skills/observability/SKILL.md +65 -0
  124. package/skills/parallel/SKILL.md +13 -8
  125. package/skills/ppr/SKILL.md +715 -371
  126. package/skills/prerender/SKILL.md +30 -17
  127. package/skills/rango/SKILL.md +72 -34
  128. package/skills/route/SKILL.md +14 -7
  129. package/skills/router-setup/SKILL.md +4 -2
  130. package/skills/scripts/SKILL.md +8 -1
  131. package/skills/server-actions/SKILL.md +11 -0
  132. package/skills/shell-manifest/SKILL.md +21 -12
  133. package/skills/testing/SKILL.md +49 -43
  134. package/skills/testing/cache-prerender.md +15 -14
  135. package/skills/testing/client-components.md +45 -3
  136. package/skills/testing/loader.md +19 -1
  137. package/skills/testing/middleware.md +1 -0
  138. package/skills/testing/predicates.md +49 -49
  139. package/skills/testing/render-handler.md +1 -0
  140. package/skills/testing/serve-shell-request.md +202 -0
  141. package/skills/testing/server-actions.md +9 -3
  142. package/skills/theme/SKILL.md +56 -4
  143. package/skills/typesafety/env-and-bindings.md +18 -2
  144. package/skills/use-cache/SKILL.md +91 -23
  145. package/skills/vercel/SKILL.md +14 -2
  146. package/skills/view-transitions/SKILL.md +150 -29
  147. package/src/__augment-tests__/augmented.check.ts +23 -0
  148. package/src/browser/cookie-name.ts +23 -0
  149. package/src/browser/event-controller.ts +4 -4
  150. package/src/browser/history-state.ts +75 -1
  151. package/src/browser/link-interceptor.ts +4 -0
  152. package/src/browser/navigation-bridge.ts +77 -18
  153. package/src/browser/navigation-client.ts +4 -3
  154. package/src/browser/navigation-store.ts +43 -1
  155. package/src/browser/navigation-transaction.ts +31 -11
  156. package/src/browser/partial-update.ts +146 -35
  157. package/src/browser/prefetch/cache.ts +6 -6
  158. package/src/browser/prefetch/fetch.ts +2 -2
  159. package/src/browser/react/Link.tsx +38 -33
  160. package/src/browser/react/NavigationProvider.tsx +2 -2
  161. package/src/browser/react/context.ts +2 -2
  162. package/src/browser/react/location-state-shared.ts +241 -49
  163. package/src/browser/react/location-state.ts +5 -2
  164. package/src/browser/rsc-router.tsx +9 -0
  165. package/src/browser/server-action-bridge.ts +84 -15
  166. package/src/browser/transition-when.ts +205 -0
  167. package/src/browser/types.ts +124 -10
  168. package/src/build/generate-manifest.ts +77 -1
  169. package/src/cache/background-task.ts +55 -2
  170. package/src/cache/cache-exec-scope.ts +48 -1
  171. package/src/cache/cache-key-utils.ts +62 -2
  172. package/src/cache/cache-policy.ts +31 -4
  173. package/src/cache/cache-runtime.ts +209 -129
  174. package/src/cache/cache-scope.ts +573 -69
  175. package/src/cache/cache-tag.ts +475 -19
  176. package/src/cache/cf/cf-base64.ts +36 -4
  177. package/src/cache/cf/cf-cache-constants.ts +12 -0
  178. package/src/cache/cf/cf-cache-store.ts +1094 -376
  179. package/src/cache/cf/cf-cache-types.ts +32 -4
  180. package/src/cache/cf/cf-shell-frame.ts +258 -0
  181. package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
  182. package/src/cache/document-cache.ts +53 -7
  183. package/src/cache/handle-snapshot.ts +53 -9
  184. package/src/cache/index.ts +2 -0
  185. package/src/cache/invalidation-order.ts +84 -0
  186. package/src/cache/isolate-tag-memo.ts +308 -0
  187. package/src/cache/request-tag-mask.ts +147 -0
  188. package/src/cache/shell-memo.ts +341 -0
  189. package/src/cache/shell-snapshot.ts +249 -277
  190. package/src/cache/tag-invalidation.ts +109 -18
  191. package/src/cache/types.ts +274 -102
  192. package/src/cache/vercel/vercel-cache-store.ts +561 -74
  193. package/src/client-urls/client-root.tsx +8 -4
  194. package/src/client-urls/client-urls.ts +8 -6
  195. package/src/client-urls/navigation.ts +55 -4
  196. package/src/client-urls/server-projection.ts +31 -10
  197. package/src/client-urls/types.ts +12 -5
  198. package/src/client.tsx +2 -0
  199. package/src/host/cookie-handler.ts +2 -1
  200. package/src/index.rsc.ts +3 -0
  201. package/src/index.ts +3 -0
  202. package/src/loader-redirect.tsx +7 -3
  203. package/src/prerender/build-shell-capture.ts +173 -110
  204. package/src/route-definition/dsl-helpers.ts +6 -3
  205. package/src/route-definition/helpers-types.ts +11 -11
  206. package/src/route-name.ts +12 -0
  207. package/src/router/content-negotiation.ts +4 -1
  208. package/src/router/error-handling.ts +5 -2
  209. package/src/router/handler-context.ts +42 -5
  210. package/src/router/instrument.ts +54 -2
  211. package/src/router/intercept-resolution.ts +30 -8
  212. package/src/router/loader-resolution.ts +89 -33
  213. package/src/router/logging.ts +5 -3
  214. package/src/router/match-api.ts +89 -42
  215. package/src/router/match-context.ts +5 -0
  216. package/src/router/match-handlers.ts +14 -29
  217. package/src/router/match-middleware/background-revalidation.ts +3 -5
  218. package/src/router/match-middleware/cache-lookup.ts +52 -19
  219. package/src/router/match-middleware/cache-store.ts +36 -10
  220. package/src/router/match-result.ts +1 -0
  221. package/src/router/middleware-types.ts +6 -1
  222. package/src/router/middleware.ts +20 -4
  223. package/src/router/navigation-snapshot.ts +6 -5
  224. package/src/router/prerender-match.ts +10 -0
  225. package/src/router/request-classification.ts +7 -4
  226. package/src/router/router-options.ts +4 -1
  227. package/src/router/segment-resolution/fresh.ts +18 -13
  228. package/src/router/segment-resolution/helpers.ts +4 -2
  229. package/src/router/segment-resolution/loader-cache.ts +366 -70
  230. package/src/router/segment-resolution/loader-mask.ts +3 -4
  231. package/src/router/segment-resolution/loader-snapshot.ts +215 -126
  232. package/src/router/segment-resolution/mask-nested.ts +161 -42
  233. package/src/router/segment-resolution/revalidation.ts +27 -13
  234. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  235. package/src/router/telemetry.ts +6 -3
  236. package/src/router/transition-when.ts +34 -52
  237. package/src/router.ts +2 -1
  238. package/src/rsc/attach-transition-when.ts +47 -0
  239. package/src/rsc/full-payload.ts +47 -3
  240. package/src/rsc/handler.ts +24 -10
  241. package/src/rsc/loader-fetch.ts +2 -1
  242. package/src/rsc/manifest-init.ts +4 -0
  243. package/src/rsc/origin-guard.ts +8 -5
  244. package/src/rsc/progressive-enhancement.ts +10 -32
  245. package/src/rsc/response-cache-serve.ts +69 -78
  246. package/src/rsc/response-route-handler.ts +2 -2
  247. package/src/rsc/rsc-rendering.ts +712 -338
  248. package/src/rsc/server-action.ts +8 -20
  249. package/src/rsc/shell-build-manifest.ts +49 -29
  250. package/src/rsc/shell-capture-constants.ts +182 -12
  251. package/src/rsc/shell-capture.ts +1126 -612
  252. package/src/rsc/shell-serve.ts +331 -85
  253. package/src/rsc/ssr-setup.ts +3 -2
  254. package/src/rsc/types.ts +9 -0
  255. package/src/segment-system.tsx +40 -7
  256. package/src/server/context.ts +328 -72
  257. package/src/server/cookie-store.ts +184 -133
  258. package/src/server/handle-store.ts +245 -27
  259. package/src/server/request-context.ts +364 -100
  260. package/src/server/request-headers.ts +134 -0
  261. package/src/server.ts +4 -0
  262. package/src/ssr/index.tsx +96 -37
  263. package/src/ssr/ssr-root.tsx +9 -8
  264. package/src/testing/dispatch.ts +4 -7
  265. package/src/testing/e2e/index.ts +1 -0
  266. package/src/testing/flight.entry.ts +14 -0
  267. package/src/testing/index.ts +5 -0
  268. package/src/testing/internal/action-id.ts +25 -0
  269. package/src/testing/internal/context.ts +1 -1
  270. package/src/testing/location-state-key.ts +45 -0
  271. package/src/testing/render-handler.ts +1 -1
  272. package/src/testing/render-route.tsx +109 -16
  273. package/src/testing/run-client-revalidate.ts +3 -19
  274. package/src/testing/run-loader.ts +67 -5
  275. package/src/testing/run-transition-when.ts +109 -172
  276. package/src/testing/serve-shell-request.ts +381 -0
  277. package/src/testing/shell-status.ts +73 -47
  278. package/src/theme/ThemeProvider.tsx +7 -6
  279. package/src/transition-when-ref.ts +220 -0
  280. package/src/types/cache-types.ts +15 -3
  281. package/src/types/handler-context.ts +14 -1
  282. package/src/types/index.ts +3 -0
  283. package/src/types/segments.ts +94 -91
  284. package/src/urls/path-helper-types.ts +3 -4
  285. package/src/urls/pattern-types.ts +21 -18
  286. package/src/vite/discovery/discover-routers.ts +3 -0
  287. package/src/vite/discovery/shell-prerender-phase.ts +9 -2
  288. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  289. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  290. package/src/vite/rango.ts +2 -0
  291. package/src/vite/router-discovery.ts +89 -9
  292. package/dist/types/rsc/transition-gate.d.ts +0 -23
  293. package/src/rsc/transition-gate.ts +0 -86
@@ -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
 
@@ -39,6 +39,7 @@ import {
39
39
  } from "./intercept-utils.js";
40
40
  import { createAppShellRef } from "./app-shell.js";
41
41
  import { bootLog, IS_BROWSER_DEBUG } from "./logging.js";
42
+ import { stripShellMissMarker } from "./history-state.js";
42
43
  import { setActiveInterceptTargets } from "../client-urls/navigation.js";
43
44
 
44
45
  // Vite HMR types are provided by vite/client
@@ -169,6 +170,10 @@ export async function initBrowserApp(
169
170
  } = options;
170
171
 
171
172
  bootLog("initBrowserApp start");
173
+ // Before any location read (the history key, the navigation store, the
174
+ // event controller): the server rendered the marked request for the clean
175
+ // URL, so the client starts from the same one and hydration agrees.
176
+ stripShellMissMarker();
172
177
  bootLog("flight decode: awaiting initial payload from document stream");
173
178
  const initialPayload =
174
179
  await deps.createFromReadableStream<RscPayload>(rscStream);
@@ -221,6 +226,10 @@ export async function initBrowserApp(
221
226
  ...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
222
227
  });
223
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
+
224
233
  // Register the active store on the module-level handle and wire the
225
234
  // jar-divergence observer before any getRangoState() read can detect a
226
235
  // cross-tab/server rotation. There is no global store singleton, so this
@@ -2,6 +2,7 @@ import type {
2
2
  ServerActionBridge,
3
3
  ServerActionBridgeConfig,
4
4
  RscPayload,
5
+ ResolvedSegment,
5
6
  } from "./types.js";
6
7
  import { createPartialUpdater } from "./partial-update.js";
7
8
  import { enterActionFence, exitActionFence } from "./action-fence.js";
@@ -38,6 +39,11 @@ import { mergeLocationState } from "./history-state.js";
38
39
  import { classifyActionOutcome } from "./action-coordinator.js";
39
40
  import { getAppVersion } from "./app-version.js";
40
41
  import { collectClientRevalidationDecisions } from "../client-urls/navigation.js";
42
+ import {
43
+ decideCommitGatedOff,
44
+ findActionFormData,
45
+ type TransitionWhenActionInput,
46
+ } from "./transition-when.js";
41
47
  import { CLIENT_REVALIDATION_HEADER } from "../client-urls/revalidation-protocol.js";
42
48
 
43
49
  // Polyfill Symbol.dispose/asyncDispose for Safari and older browsers
@@ -137,6 +143,20 @@ export function createServerActionBridge(
137
143
  getVersion: getAppVersion,
138
144
  });
139
145
 
146
+ /**
147
+ * transition({ when }) for an action commit: the location on screen,
148
+ * refreshed by the response's params and route name when it carries them.
149
+ */
150
+ const decideActionGatedOff = (
151
+ segments: readonly ResolvedSegment[],
152
+ metadata: RscPayload["metadata"] | undefined,
153
+ action: TransitionWhenActionInput,
154
+ ): boolean =>
155
+ decideCommitGatedOff(store, segments, "action", {
156
+ from: metadata,
157
+ action,
158
+ });
159
+
140
160
  /**
141
161
  * Refetch current route via a navigation transaction.
142
162
  * Encapsulates the repeated pattern of creating a navTx + fetchPartialUpdate
@@ -146,6 +166,8 @@ export function createServerActionBridge(
146
166
  segments?: string[];
147
167
  interceptSourceUrl?: string | null;
148
168
  actionId?: string;
169
+ /** transition({ when }) `action` fields for the refetch commit. */
170
+ action?: TransitionWhenActionInput;
149
171
  }): Promise<void> {
150
172
  const src = opts?.interceptSourceUrl ?? null;
151
173
  const navTx = createNavigationTransaction(
@@ -169,6 +191,7 @@ export function createServerActionBridge(
169
191
  type: "action" as const,
170
192
  ...(src ? { interceptSourceUrl: src } : {}),
171
193
  ...(opts?.actionId !== undefined ? { actionId: opts.actionId } : {}),
194
+ ...(opts?.action ? { action: opts.action } : {}),
172
195
  },
173
196
  );
174
197
  } finally {
@@ -560,8 +583,22 @@ export function createServerActionBridge(
560
583
  // Reconcile error segments with cached tree
561
584
  const errorResult = reconcileErrorSegments(cachedSegments, segments);
562
585
 
586
+ // transition({ when }) decides the error-boundary commit too, with
587
+ // `action.error` set.
588
+ const errorGatedOff = decideActionGatedOff(
589
+ errorResult.segments,
590
+ metadata,
591
+ {
592
+ id,
593
+ formData: findActionFormData(args),
594
+ error:
595
+ returnValue && !returnValue.ok ? returnValue.data : undefined,
596
+ },
597
+ );
598
+
563
599
  // Render the full tree with error segment merged with parent layouts
564
600
  const errorTree = await renderSegments(errorResult.mainSegments, {
601
+ transitionGatedOff: errorGatedOff,
565
602
  isAction: true,
566
603
  interceptSegments:
567
604
  errorResult.interceptSegments.length > 0
@@ -605,11 +642,13 @@ export function createServerActionBridge(
605
642
  errorResult.segments,
606
643
  currentHandleData,
607
644
  );
645
+ store.rememberDisplayedEntry();
608
646
 
609
- // Update UI with error boundary
610
- startTransition(() => {
611
- onUpdate({ root: errorTree, metadata: metadata! });
612
- });
647
+ // Update UI with error boundary: urgent when transition({ when })
648
+ // gated it off, like every gated-off commit.
649
+ const errorUpdate = { root: errorTree, metadata: metadata! };
650
+ if (errorGatedOff) onUpdate(errorUpdate);
651
+ else startTransition(() => onUpdate(errorUpdate));
613
652
 
614
653
  // Throw the error so the action promise rejects
615
654
  if (returnValue && !returnValue.ok) {
@@ -697,6 +736,13 @@ export function createServerActionBridge(
697
736
  if (scenario.type !== "navigated-away") {
698
737
  applyActionLocationState(handle, metadata?.locationState);
699
738
  }
739
+ // transition({ when }) `action` for this action's commit, whichever
740
+ // lane applies it (normal, or a refetch below).
741
+ const whenAction: TransitionWhenActionInput = {
742
+ id,
743
+ formData: findActionFormData(args),
744
+ result: returnData,
745
+ };
700
746
 
701
747
  switch (scenario.type) {
702
748
  case "navigated-away": {
@@ -712,13 +758,15 @@ export function createServerActionBridge(
712
758
  // Invalidation is deferred to finalizeAction(); here we only trigger
713
759
  // the revalidation refetch of the new route (suppressed on keep).
714
760
  if (!scenario.onInterceptRoute && !keepCache) {
715
- refetchRoute({ actionId: id }).catch((error) => {
716
- if (isBackgroundSuppressible(error)) return;
717
- console.error(
718
- "[Browser] Background revalidation failed:",
719
- error,
720
- );
721
- });
761
+ refetchRoute({ actionId: id, action: whenAction }).catch(
762
+ (error) => {
763
+ if (isBackgroundSuppressible(error)) return;
764
+ console.error(
765
+ "[Browser] Background revalidation failed:",
766
+ error,
767
+ );
768
+ },
769
+ );
722
770
  }
723
771
  break;
724
772
  }
@@ -730,6 +778,7 @@ export function createServerActionBridge(
730
778
  await refetchRoute({
731
779
  interceptSourceUrl: store.getInterceptSourceUrl(),
732
780
  actionId: id,
781
+ action: whenAction,
733
782
  });
734
783
  }
735
784
  break;
@@ -743,7 +792,11 @@ export function createServerActionBridge(
743
792
  // resolving last must discharge a directive-free sibling's repair.
744
793
  // See the keep row in docs/design/rango-state-cookie.md (the all-keep
745
794
  // edge, and the benign re-mark-stale-after-refetch end-state delta).
746
- await refetchRoute({ interceptSourceUrl, actionId: id });
795
+ await refetchRoute({
796
+ interceptSourceUrl,
797
+ actionId: id,
798
+ action: whenAction,
799
+ });
747
800
  break;
748
801
  }
749
802
 
@@ -766,6 +819,7 @@ export function createServerActionBridge(
766
819
  segments: segmentsToSend,
767
820
  interceptSourceUrl,
768
821
  actionId: id,
822
+ action: whenAction,
769
823
  });
770
824
  break;
771
825
  }
@@ -791,8 +845,22 @@ export function createServerActionBridge(
791
845
  }
792
846
 
793
847
  case "normal": {
848
+ // transition({ when }) for the action commit (kind "action"): the
849
+ // page does not move, so `to` is `from`. False commits urgently (a
850
+ // re-suspending segment streams its loading()) and switches every
851
+ // <ViewTransition> class to "none"; true keeps the startTransition.
852
+ // This commit runs after awaits, outside the transition React opens
853
+ // for a form action / useActionState call, so React does not force
854
+ // a transition on it. The tree shape never changes, so
855
+ // useActionState and other client state survive (#995).
856
+ const gatedOff = decideActionGatedOff(
857
+ fullSegments,
858
+ metadata,
859
+ whenAction,
860
+ );
794
861
  // Prepare new tree (await loader data resolution)
795
862
  const newTree = await renderSegments(reconciled.mainSegments, {
863
+ transitionGatedOff: gatedOff,
796
864
  isAction: true,
797
865
  interceptSegments:
798
866
  reconciled.interceptSegments.length > 0
@@ -832,10 +900,11 @@ export function createServerActionBridge(
832
900
  fullSegments,
833
901
  currentHandleData,
834
902
  );
903
+ store.rememberDisplayedEntry(metadata?.routeName);
835
904
 
836
- startTransition(() => {
837
- onUpdate({ root: newTree, metadata: metadata! });
838
- });
905
+ const actionUpdate = { root: newTree, metadata: metadata! };
906
+ if (gatedOff) onUpdate(actionUpdate);
907
+ else startTransition(() => onUpdate(actionUpdate));
839
908
  // Invalidation deferred to finalizeAction() (runs after this caches
840
909
  // the fresh segments), suppressed when the action called
841
910
  // keepClientCache().