@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
@@ -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;
@@ -11,12 +11,20 @@
11
11
  import type { UrlPatterns } from "../urls.js";
12
12
  import type { AllUseItems } from "../route-types.js";
13
13
  import { extractStaticPrefix } from "../router/pattern-matching.js";
14
- import { RangoContext, runWithPrefixes } from "../server/context.js";
14
+ import {
15
+ getParallelSlotEntries,
16
+ RangoContext,
17
+ runWithPrefixes,
18
+ } from "../server/context.js";
15
19
  import type { EntryData, TrackedInclude } from "../server/context.js";
16
20
  import type { TrailingSlashMode } from "../types.js";
17
21
  import { createRouteHelpers } from "../route-definition.js";
18
22
  import MapRootLayout from "../server/root-layout.js";
19
23
  import { collectFallbackClientRefs } from "./collect-fallback-refs.js";
24
+ import {
25
+ createTransitionWhenError,
26
+ transitionWhenProblem,
27
+ } from "../transition-when-ref.js";
20
28
  import {
21
29
  isIncludeProvider,
22
30
  resolveIncludeModule,
@@ -59,6 +67,57 @@ export interface GeneratedManifest {
59
67
  routeSearchSchemas?: Record<string, Record<string, string>>;
60
68
  }
61
69
 
70
+ /**
71
+ * Fail discovery on an invalid transition({ when }) anywhere in the entry
72
+ * chains of the evaluated routes: the route itself, its ancestors, their
73
+ * orphan layouts, parallel slots and intercepts. Strict: a plain function is
74
+ * rejected, because discovery evaluates the tree in the react-server graph
75
+ * where a "use client" import is a client reference (transition-when-ref.ts).
76
+ * The error names the first route found through the entry.
77
+ */
78
+ function validateTransitionWhens(
79
+ manifest: Map<string, EntryData>,
80
+ patterns: Map<string, string>,
81
+ ): void {
82
+ const seen = new Set<object>();
83
+ for (const [routeName, routeEntry] of manifest) {
84
+ const route = {
85
+ routeName,
86
+ pattern:
87
+ patterns.get(routeName) ??
88
+ (routeEntry.type === "route" ? routeEntry.pattern : undefined),
89
+ };
90
+ const check = (when: unknown, entryType: string): void => {
91
+ const problem = transitionWhenProblem(when, true);
92
+ if (problem) {
93
+ throw createTransitionWhenError(when, problem, {
94
+ ...route,
95
+ entryType,
96
+ });
97
+ }
98
+ };
99
+ const visit = (entry: EntryData): void => {
100
+ if (seen.has(entry)) return;
101
+ seen.add(entry);
102
+ check(entry.transition?.when, entry.type);
103
+ for (const orphan of entry.layout) visit(orphan);
104
+ for (const { entry: slot } of getParallelSlotEntries(entry.parallel)) {
105
+ visit(slot);
106
+ }
107
+ for (const intercept of entry.intercept) {
108
+ check(intercept.transition?.when, "intercept");
109
+ }
110
+ };
111
+ for (
112
+ let entry: EntryData | null | undefined = routeEntry;
113
+ entry;
114
+ entry = entry.parent ?? entry.orphanOwner
115
+ ) {
116
+ visit(entry);
117
+ }
118
+ }
119
+ }
120
+
62
121
  /**
63
122
  * Build prefix tree node by running the patterns with proper context.
64
123
  * Uses a visited set to detect circular includes and prevent infinite recursion.
@@ -116,6 +175,7 @@ async function buildPrefixTreeNode(
116
175
  responseTypeRoutes?: Record<string, string>,
117
176
  routeSearchSchemas?: Record<string, Record<string, string>>,
118
177
  routerId?: string,
178
+ validateTransitionWhen?: boolean,
119
179
  ): Promise<PrefixTreeNode> {
120
180
  // Resolve an async include provider (`() => import("./routes")`) so its routes
121
181
  // are walked into the build-time manifest/types/href. Runtime matching still
@@ -174,6 +234,8 @@ async function buildPrefixTreeNode(
174
234
  },
175
235
  );
176
236
 
237
+ if (validateTransitionWhen) validateTransitionWhens(manifest, patternsMap);
238
+
177
239
  // Collect route names defined in this include (routes have prefixes applied)
178
240
  const routes = [...patternsMap.keys()];
179
241
  Object.assign(routeManifest, Object.fromEntries(patternsMap));
@@ -230,6 +292,7 @@ async function buildPrefixTreeNode(
230
292
  responseTypeRoutes,
231
293
  routeSearchSchemas,
232
294
  routerId,
295
+ validateTransitionWhen,
233
296
  ),
234
297
  );
235
298
 
@@ -313,6 +376,14 @@ export async function generateManifestFull<TEnv>(
313
376
  * EntryData map built below is local; this is the only seam that surfaces it.
314
377
  */
315
378
  collectClientFallbackRef?: (refKey: string) => void;
379
+ /**
380
+ * Fail on an invalid transition({ when }): a non-function, or a plain
381
+ * (server) function where a client reference is required. Only the Vite
382
+ * plugin's discovery passes it: it evaluates the tree in the react-server
383
+ * graph, the one place a "use client" import is told apart from a server
384
+ * function (transition-when-ref.ts).
385
+ */
386
+ validateTransitionWhen?: boolean;
316
387
  },
317
388
  ): Promise<FullManifest> {
318
389
  const routeManifest: Record<string, string> = {};
@@ -351,6 +422,10 @@ export async function generateManifestFull<TEnv>(
351
422
  },
352
423
  );
353
424
 
425
+ if (options?.validateTransitionWhen) {
426
+ validateTransitionWhens(manifest, patternsMap);
427
+ }
428
+
354
429
  // Surface the "use client" components registered as error/notFound fallbacks
355
430
  // (route-tree errorBoundary()/notFoundBoundary() helpers, stored on EntryData).
356
431
  // The boundary may be a handler function and/or wrap the client boundary in
@@ -413,6 +488,7 @@ export async function generateManifestFull<TEnv>(
413
488
  responseTypeRoutes,
414
489
  routeSearchSchemas,
415
490
  options?.routerId,
491
+ options?.validateTransitionWhen,
416
492
  ),
417
493
  );
418
494
 
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Background Task Runner
3
3
  *
4
- * Unified helper for scheduling async work via waitUntil.
5
- * When waitUntil is unavailable, falls back to blocking or skipping.
4
+ * Unified helper for scheduling async work via waitUntil, and the two bounded
5
+ * waits the PPR shell capture puts on background work (raceDeadline,
6
+ * settleGrowing). When waitUntil is unavailable, falls back to blocking or
7
+ * skipping.
6
8
  */
7
9
 
8
10
  interface WaitUntilHost {
@@ -32,3 +34,54 @@ export function runBackground(
32
34
  return task();
33
35
  }
34
36
  }
37
+
38
+ /**
39
+ * Race `work` against an absolute deadline (epoch ms), keeping its value. The
40
+ * losing work is not cancelled (a router match has no abort signal) and its
41
+ * late rejection is swallowed. The timer is unref'd, so a pending deadline
42
+ * never keeps a Node dev process alive.
43
+ */
44
+ export async function raceDeadline<T>(
45
+ work: Promise<T>,
46
+ deadline: number,
47
+ ): Promise<{ done: true; value: T } | { done: false }> {
48
+ work.catch(() => {});
49
+ let timer: ReturnType<typeof setTimeout> | undefined;
50
+ const expired = new Promise<{ done: false }>((resolve) => {
51
+ timer = setTimeout(
52
+ () => resolve({ done: false }),
53
+ Math.max(0, deadline - Date.now()),
54
+ );
55
+ (timer as { unref?: () => void }).unref?.();
56
+ });
57
+ try {
58
+ return await Promise.race([
59
+ work.then((value) => ({ done: true as const, value })),
60
+ expired,
61
+ ]);
62
+ } finally {
63
+ if (timer) clearTimeout(timer);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Wait until every promise in `list` has settled, including ones appended
69
+ * while waiting (a settled write can schedule a nested one: cacheRoute
70
+ * schedules its store.set in a second waitUntil), or until `deadline` (epoch
71
+ * ms). Resolves true when the list drained, false at the deadline. Never
72
+ * mutates `list`: a later call waits for what is still pending.
73
+ */
74
+ export async function settleGrowing(
75
+ list: readonly Promise<unknown>[],
76
+ deadline: number,
77
+ ): Promise<boolean> {
78
+ let seen = 0;
79
+ while (list.length > seen) {
80
+ if (Date.now() >= deadline) return false;
81
+ const batch = list.slice(seen);
82
+ seen = list.length;
83
+ const settled = await raceDeadline(Promise.allSettled(batch), deadline);
84
+ if (!settled.done) return false;
85
+ }
86
+ return true;
87
+ }
@@ -38,6 +38,53 @@ export interface CacheExecScope {
38
38
 
39
39
  const cacheExecStorage = new AsyncLocalStorage<CacheExecScope>();
40
40
 
41
+ // Global key: the value must survive HMR re-evaluation of this module, like
42
+ // the other request-scoped ALS instances (server/context.ts RangoContext).
43
+ const IDENTITY_EXEMPT_KEY = Symbol.for("rangojs-router:identity-exempt");
44
+ const identityExemptStorage: AsyncLocalStorage<boolean> = ((globalThis as any)[
45
+ IDENTITY_EXEMPT_KEY
46
+ ] ??= new AsyncLocalStorage<boolean>());
47
+
48
+ /**
49
+ * Run a callback whose identity reads never reach a shared entry, so
50
+ * guardIdentityRead (server/context.ts) lets them through and records
51
+ * nothing (#976):
52
+ *
53
+ * - a cache's own decisions and metadata: a `key()`, a store `keyGenerator`
54
+ * (cache-policy.ts resolveCacheKey), a `condition()` (cache-scope.ts
55
+ * conditionAllows, loader-cache.ts) and a `tags()` function
56
+ * (cache-policy.ts resolveTagsOption). The value picks the entry, whether
57
+ * one is used, or how it is invalidated; it is never rendered. Reading the
58
+ * request there is the documented way to partition an entry;
59
+ * - the router's `onError` (router/error-handling.ts invokeOnError): it
60
+ * observes, and a render error can reach it inside a capture or a cached
61
+ * scope.
62
+ *
63
+ * Scar: a ppr capture resolves its doc record key, and the record's tags,
64
+ * again under the capture context; once `ctx.request.headers` was guarded, a
65
+ * keyGenerator or tags() reading it refused every capture of the route.
66
+ *
67
+ * The exemption is async-local, so it would follow everything the callback
68
+ * starts. A "use cache" body (runWithCacheExecScope), a loader body
69
+ * (server/context.ts runInsideLoaderBodyScope) and a segment funnel
70
+ * (getContext().runWithStore) end it on entry (endIdentityExempt). Scar: a
71
+ * key() that awaited a "use cache" function reading cookies() stored the
72
+ * first visitor's cookie and served it to the next.
73
+ */
74
+ export function runIdentityExempt<T>(fn: () => T): T {
75
+ return identityExemptStorage.run(true, fn);
76
+ }
77
+
78
+ /** True inside runIdentityExempt, and not in a scope started from it. */
79
+ export function isInsideIdentityExempt(): boolean {
80
+ return identityExemptStorage.getStore() === true;
81
+ }
82
+
83
+ /** Run `fn` with the identity exemption ended (see runIdentityExempt). */
84
+ export function endIdentityExempt<T>(fn: () => T): T {
85
+ return isInsideIdentityExempt() ? identityExemptStorage.run(false, fn) : fn();
86
+ }
87
+
41
88
  /**
42
89
  * Run fn with the "use cache" execution scope active. Continuations spawned
43
90
  * from fn's synchronous kickoff inherit the scope; parallel chains do not.
@@ -48,7 +95,7 @@ export function runWithCacheExecScope<T>(
48
95
  fn: () => T,
49
96
  scope: CacheExecScope = createCacheExecScope(),
50
97
  ): T {
51
- return cacheExecStorage.run(scope, fn);
98
+ return endIdentityExempt(() => cacheExecStorage.run(scope, fn));
52
99
  }
53
100
 
54
101
  /** A scope nested in the calling chain's current one, if any. */