@rangojs/router 0.18.0 → 0.19.1

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 +37 -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 +48 -5
  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 +126 -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 +962 -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 +67 -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 +54 -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 +202 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +209 -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 +42 -5
  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 +152 -26
  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 +263 -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 +134 -87
  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
@@ -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().
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Browser-side transition({ when }) decisions.
3
+ *
4
+ * A navigation is decided ONCE, at the first commit that presents its
5
+ * destination: the canonical commit for a server route, the optimistic swap
6
+ * for a clientUrls() cross-route navigation (the canonical commit then reuses
7
+ * the decision carried on its transaction), the history-cache restore or
8
+ * refetch commit for back/forward, the action commit (error lane included).
9
+ *
10
+ * The decision is navigation-wide: every distinct predicate (by function
11
+ * identity) declared by a committed segment, kept or re-sent (#989), runs, and
12
+ * any false gates the whole navigation off. It never changes the rendered
13
+ * tree's shape: every segment keeps its key and its <ViewTransition> element
14
+ * (segment-system.tsx renders each class "none"), and the commit is urgent
15
+ * instead of held (partial-update.ts). That is what keeps component state
16
+ * across when true -> false -> true (#995). A response carries no decision,
17
+ * so a reused prefetch is decided against the real source.
18
+ */
19
+
20
+ import type {
21
+ ResolvedSegment,
22
+ RouteLocation,
23
+ TransitionWhenContext,
24
+ TransitionWhenFn,
25
+ TransitionWhenKind,
26
+ } from "../types/segments.js";
27
+ import { resolveTransitionWhen } from "../transition-when-ref.js";
28
+ import { makeIsAction } from "../router/is-action.js";
29
+ import { isInterceptSegment } from "./intercept-utils.js";
30
+ import type { NavigationStore } from "./types.js";
31
+
32
+ /** One side of the navigation before it is frozen into a RouteLocation. */
33
+ export interface RouteLocationInput {
34
+ url: string | URL;
35
+ params?: Readonly<Record<string, string>>;
36
+ routeName?: string;
37
+ state?: unknown;
38
+ }
39
+
40
+ /** The action fields of a `kind: "action"` context. */
41
+ export interface TransitionWhenActionInput {
42
+ id?: string;
43
+ formData?: FormData;
44
+ result?: unknown;
45
+ error?: unknown;
46
+ }
47
+
48
+ export interface TransitionDecisionInput {
49
+ kind: TransitionWhenKind;
50
+ from: RouteLocationInput;
51
+ /** The destination; ignored for "action" and "revalidate" (`to` is `from`). */
52
+ to?: RouteLocationInput;
53
+ action?: TransitionWhenActionInput;
54
+ }
55
+
56
+ const EMPTY_PARAMS: Readonly<Record<string, string>> = Object.freeze({});
57
+
58
+ function createRouteLocation(input: RouteLocationInput): RouteLocation {
59
+ return Object.freeze({
60
+ // A fresh URL: a predicate may not mutate the store's or the caller's.
61
+ url: new URL(
62
+ String(input.url),
63
+ typeof window !== "undefined"
64
+ ? window.location.href
65
+ : "http://localhost/",
66
+ ),
67
+ params: input.params ? Object.freeze({ ...input.params }) : EMPTY_PARAMS,
68
+ routeName: input.routeName,
69
+ state: input.state,
70
+ });
71
+ }
72
+
73
+ export function createTransitionWhenContext(
74
+ input: TransitionDecisionInput,
75
+ ): TransitionWhenContext {
76
+ const from = createRouteLocation(input.from);
77
+ const inAction = input.kind === "action";
78
+ const { id, formData, result, error } = input.action ?? {};
79
+ return Object.freeze({
80
+ kind: input.kind,
81
+ from,
82
+ to:
83
+ inAction || input.kind === "revalidate" || !input.to
84
+ ? from
85
+ : createRouteLocation(input.to),
86
+ isAction: makeIsAction(id, inAction),
87
+ ...(inAction && { action: Object.freeze({ id, formData, result, error }) }),
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Run one predicate. A throw counts as false (the navigation does not hold)
93
+ * and is logged; it never fails the navigation.
94
+ */
95
+ export function evaluateTransitionWhen(
96
+ fn: TransitionWhenFn,
97
+ ctx: TransitionWhenContext,
98
+ ): boolean {
99
+ try {
100
+ return fn(ctx) !== false;
101
+ } catch (error) {
102
+ console.error(
103
+ "[rango] transition({ when }) threw; the navigation does not hold.",
104
+ error,
105
+ );
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /** Distinct predicates of the segments that declare one, in segment order. */
111
+ export function collectTransitionWhens(
112
+ segments: readonly ResolvedSegment[],
113
+ ): Set<TransitionWhenFn> {
114
+ const fns = new Set<TransitionWhenFn>();
115
+ for (const segment of segments) {
116
+ const fn = resolveTransitionWhen(segment.transition?.when);
117
+ if (fn) fns.add(fn);
118
+ }
119
+ return fns;
120
+ }
121
+
122
+ /**
123
+ * Decide a commit: true when the navigation is gated off (some predicate
124
+ * returned false or threw). The context is built only when a predicate runs.
125
+ * An intercept commit is not evaluated: it never holds or animates
126
+ * (partial-update.ts shouldStartViewTransition). `extra` is a predicate not on
127
+ * a segment yet (the optimistic clientUrls() destination).
128
+ */
129
+ export function decideTransitionGatedOff(
130
+ segments: readonly ResolvedSegment[],
131
+ input: () => TransitionDecisionInput,
132
+ extra?: TransitionWhenFn,
133
+ ): boolean {
134
+ if (segments.some(isInterceptSegment)) return false;
135
+ const fns = collectTransitionWhens(segments);
136
+ if (extra) fns.add(extra);
137
+ if (fns.size === 0) return false;
138
+ const ctx = createTransitionWhenContext(input());
139
+ for (const fn of fns) if (!evaluateTransitionWhen(fn, ctx)) return true;
140
+ return false;
141
+ }
142
+
143
+ /** Merge route params the way popstate restores them (per-segment params). */
144
+ export function mergeSegmentParams(
145
+ segments: readonly ResolvedSegment[],
146
+ ): Record<string, string> {
147
+ const params: Record<string, string> = {};
148
+ for (const s of segments) if (s.params) Object.assign(params, s.params);
149
+ return params;
150
+ }
151
+
152
+ export interface CommitDecision {
153
+ /** Newer params / route name for the committed location (an action response's). */
154
+ from?: { params?: Record<string, string>; routeName?: string };
155
+ /** The destination, from the committed location; omit for "action" / "revalidate". */
156
+ to?: (from: RouteLocationInput) => RouteLocationInput;
157
+ action?: TransitionWhenActionInput;
158
+ extra?: TransitionWhenFn;
159
+ }
160
+
161
+ /**
162
+ * decideTransitionGatedOff with `from` read from the store: call it before
163
+ * the commit moves the store. `state` is the live history.state, except for
164
+ * back/forward: at popstate history.state already belongs to the destination,
165
+ * so the entry being left comes from the store's per-entry memory (undefined
166
+ * after a reload, never guessed).
167
+ */
168
+ export function decideCommitGatedOff(
169
+ store: NavigationStore,
170
+ segments: readonly ResolvedSegment[],
171
+ kind: TransitionWhenKind,
172
+ decision: CommitDecision = {},
173
+ ): boolean {
174
+ return decideTransitionGatedOff(
175
+ segments,
176
+ () => {
177
+ const memory = store.getHistoryEntryMemory();
178
+ const from: RouteLocationInput = {
179
+ url: store.getSegmentState().currentUrl || window.location.href,
180
+ params:
181
+ decision.from?.params ??
182
+ mergeSegmentParams(
183
+ store.getCachedSegments(store.getHistoryKey())?.segments ?? [],
184
+ ),
185
+ routeName: decision.from?.routeName ?? memory?.routeName,
186
+ state: kind === "pop" ? memory?.state : window.history.state,
187
+ };
188
+ return {
189
+ kind,
190
+ from,
191
+ to: decision.to?.(from),
192
+ action: decision.action,
193
+ };
194
+ },
195
+ decision.extra,
196
+ );
197
+ }
198
+
199
+ /** The FormData argument a server action was called with, if any. */
200
+ export function findActionFormData(
201
+ args: readonly unknown[],
202
+ ): FormData | undefined {
203
+ if (typeof FormData === "undefined") return undefined;
204
+ return args.find((arg): arg is FormData => arg instanceof FormData);
205
+ }
@@ -48,6 +48,12 @@ export interface RscMetadata {
48
48
  resolvedIds?: string[];
49
49
  /** Merged route params from the matched route */
50
50
  params?: Record<string, string>;
51
+ /**
52
+ * The matched route's name (include name prefix applied), when the route is
53
+ * named. The browser keeps it per history entry for transition({ when })'s
54
+ * `from.routeName` / `to.routeName` (browser/transition-when.ts).
55
+ */
56
+ routeName?: string;
51
57
  /**
52
58
  * State of named slots for this route match
53
59
  * Key is slot name (e.g., "@modal"), value is slot state
@@ -276,6 +282,18 @@ export interface SegmentState {
276
282
  currentSegmentIds: string[];
277
283
  }
278
284
 
285
+ /**
286
+ * What the router remembers about one history entry for transition({ when })
287
+ * (NavigationStore.rememberDisplayedEntry).
288
+ *
289
+ * @internal This type is an implementation detail and may change without notice.
290
+ */
291
+ export interface HistoryEntryMemory {
292
+ readonly routeName: string | undefined;
293
+ /** The entry's `history.state` as last recorded. */
294
+ readonly state: unknown;
295
+ }
296
+
279
297
  /**
280
298
  * Navigation update emitted when UI should re-render
281
299
  *
@@ -296,13 +314,67 @@ export interface NavigationUpdate {
296
314
  }
297
315
 
298
316
  /**
299
- * State value for navigate/Link
300
- * - LocationStateEntry[]: Type-safe state entries (recommended)
301
- * - unknown: Plain state format (object or getter function)
317
+ * Plain object accepted as {@link PlainHistoryState}. The `any` string index
318
+ * admits interface-typed values (an `unknown` index would reject interfaces,
319
+ * which carry no implicit index signature). The `never` members reject:
320
+ * - a single typed entry (`GridState(value)` without the array) — its
321
+ * `__rsc_ls_*` fields would be spread onto history.state and
322
+ * `useLocationState(GridState)` would read `undefined`;
323
+ * - functions, including a location-state definition passed uncalled
324
+ * (`Symbol.hasInstance` comes from `Function.prototype`), which throw
325
+ * `DataCloneError`;
326
+ * - arrays and other iterables (`Symbol.iterator`), so an array must match the
327
+ * typed-entry or plain-array member instead;
328
+ * - built-ins tagged with `Symbol.toStringTag` (`Promise`, `WeakMap`,
329
+ * `WeakSet`), which throw `DataCloneError`. The cloneable tagged ones
330
+ * (`Map`, `Set`, `ArrayBuffer`, typed arrays) have their own members.
331
+ *
332
+ * Members are not checked: a function, symbol, or React element inside a
333
+ * plain object still compiles, as does a top-level `ReactElement` or DOM node
334
+ * (an object with none of the keys above).
335
+ */
336
+ export interface PlainHistoryObject {
337
+ readonly [key: string]: any;
338
+ readonly __rsc_ls_key?: never;
339
+ readonly __rsc_ls_value?: never;
340
+ readonly __rsc_ls_lazy?: never;
341
+ readonly [Symbol.iterator]?: never;
342
+ readonly [Symbol.hasInstance]?: never;
343
+ readonly [Symbol.toStringTag]?: never;
344
+ }
345
+
346
+ /**
347
+ * Plain (untyped) navigation state, stored under `history.state.state` and
348
+ * read with `useLocationState<T>()`. A structured-clone-safe value: primitives,
349
+ * arrays, `Map`/`Set`, `ArrayBuffer`, typed arrays, and plain objects. Excludes
350
+ * functions, symbols, `Promise`/`WeakMap`/`WeakSet`, and objects carrying
351
+ * `__rsc_ls_*` keys (a typed entry passed without its array). Arrays, `Map`,
352
+ * and `Set` are checked element by element; plain object members are not
353
+ * checked (see {@link PlainHistoryObject}).
354
+ */
355
+ export type PlainHistoryState =
356
+ | string
357
+ | number
358
+ | boolean
359
+ | bigint
360
+ | null
361
+ | undefined
362
+ | readonly PlainHistoryState[]
363
+ | ReadonlyMap<PlainHistoryState, PlainHistoryState>
364
+ | ReadonlySet<PlainHistoryState>
365
+ | ArrayBuffer
366
+ | ArrayBufferView
367
+ | PlainHistoryObject;
368
+
369
+ /**
370
+ * State value for `router.push()` / `router.replace()` / `<Link state>`.
371
+ * - `readonly LocationStateEntry[]`: typed entries from `createLocationState()`
372
+ * definitions, e.g. `[GridState({ count: 3 })]` (recommended)
373
+ * - {@link PlainHistoryState}: plain structured-clone-safe state
302
374
  */
303
375
  export type HistoryState =
304
- | import("./react/location-state-shared.js").LocationStateEntry[]
305
- | unknown;
376
+ | readonly import("./react/location-state-shared.js").LocationStateEntry[]
377
+ | PlainHistoryState;
306
378
 
307
379
  /**
308
380
  * Options for navigation operations
@@ -329,6 +401,22 @@ export interface NavigateOptions {
329
401
  * ```
330
402
  */
331
403
  revalidate?: boolean;
404
+ /**
405
+ * Set to `false` to present this navigation without a transition: the
406
+ * commit is urgent and every `<ViewTransition>` class resolves to `"none"`,
407
+ * the same result as a `transition({ when })` predicate returning `false`.
408
+ * No `when` predicate is called. Back/forward, action and revalidation
409
+ * commits are unaffected (they are not started by this call).
410
+ *
411
+ * @default true (the route's transition config applies)
412
+ *
413
+ * @example
414
+ * ```tsx
415
+ * router.push("/photos/2", { transition: false });
416
+ * <Link to="/photos/2" transition={false}>Next</Link>
417
+ * ```
418
+ */
419
+ transition?: boolean;
332
420
  /**
333
421
  * State to pass to history.pushState/replaceState
334
422
  * Accessible via useLocationState() hook.
@@ -350,15 +438,25 @@ export interface NavigateOptions {
350
438
  * // Plain static state
351
439
  * navigate("/product", { state: { from: "list" } });
352
440
  *
353
- * // Plain just-in-time state
354
- * navigate("/product", { state: () => ({ from: window.location.pathname }) });
441
+ * // Compile errors (and a dev-mode runtime error for untyped callers):
442
+ * navigate("/product", { state: ProductState(p) }); // entry without the array
443
+ * navigate("/product", { state: [ProductState] }); // definition not called
355
444
  * ```
356
445
  */
357
446
  state?: HistoryState;
358
447
  }
359
448
 
360
- /** @internal Extended options used only within the navigation bridge */
361
- export interface NavigateOptionsInternal extends NavigateOptions {
449
+ /**
450
+ * @internal Extended options used only within the navigation bridge. `state`
451
+ * is widened to `unknown`: the redirect lanes pass state already resolved to
452
+ * a flat `history.state` record, and in dev Link passes an uncalled
453
+ * definition through to resolveNavigationState's check.
454
+ */
455
+ export interface NavigateOptionsInternal extends Omit<
456
+ NavigateOptions,
457
+ "state"
458
+ > {
459
+ state?: unknown;
362
460
  /** Skip segment cache (used by redirect-with-state to force re-render) */
363
461
  _skipCache?: boolean;
364
462
  }
@@ -465,6 +563,22 @@ export interface NavigationStore {
465
563
  // History-based segment cache (for back/forward navigation and partial merging)
466
564
  getHistoryKey(): string;
467
565
  setHistoryKey(key: string): void;
566
+
567
+ /**
568
+ * Per-history-entry memory for transition({ when }) sources: record the
569
+ * entry on screen (keyed by its `history.state.key`) with its current
570
+ * `history.state` and, when given, its route name (kept from the last
571
+ * record otherwise; every push/replace creates a new key, so a stale name
572
+ * never carries to another entry). Called after every commit, restore and
573
+ * state merge.
574
+ */
575
+ rememberDisplayedEntry(routeName?: string): void;
576
+ /**
577
+ * The memory of the entry on screen, or of `entryKey`. At popstate
578
+ * history.state already belongs to the destination, so back/forward reads
579
+ * the entry being LEFT from here. In-memory only: empty after a reload.
580
+ */
581
+ getHistoryEntryMemory(entryKey?: string): HistoryEntryMemory | undefined;
468
582
  /** Monotonic token of the most recently committed navigation. */
469
583
  getNavInstance(): number;
470
584
  cacheSegmentsForHistory(
@@ -631,7 +745,7 @@ export interface ServerActionBridgeConfig {
631
745
  * Navigation bridge for handling client-side navigation
632
746
  */
633
747
  export interface NavigationBridge {
634
- navigate(url: string, options?: NavigateOptions): Promise<void>;
748
+ navigate(url: string, options?: NavigateOptionsInternal): Promise<void>;
635
749
  refresh(): Promise<void>;
636
750
  handlePopstate(): Promise<void>;
637
751
  registerLinkInterception(): () => void;