@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
@@ -36,6 +36,13 @@ import {
36
36
  collectClientRevalidationDecisions,
37
37
  setActiveInterceptTargets,
38
38
  } from "../client-urls/navigation.js";
39
+ import {
40
+ decideCommitGatedOff,
41
+ mergeSegmentParams,
42
+ type TransitionWhenActionInput,
43
+ } from "./transition-when.js";
44
+ import { buildHistoryState } from "./history-state.js";
45
+ import type { TransitionWhenKind } from "../types/segments.js";
39
46
 
40
47
  function toScrollPayload(
41
48
  scroll: boolean | undefined,
@@ -43,9 +50,16 @@ function toScrollPayload(
43
50
  return { enabled: scroll !== false ? scroll : false };
44
51
  }
45
52
 
53
+ /**
54
+ * Hold/animate decision for a commit: a non-intercept segment carries a
55
+ * transition, no intercept is presented, and this navigation's
56
+ * transition({ when }) decision did not gate it off.
57
+ */
46
58
  export function shouldStartViewTransition(
47
- segments: ResolvedSegment[],
59
+ segments: readonly ResolvedSegment[],
60
+ transitionGatedOff: boolean = false,
48
61
  ): boolean {
62
+ if (transitionGatedOff) return false;
49
63
  let hasIntercept = false;
50
64
  let hasTransition = false;
51
65
  for (const s of segments) {
@@ -107,6 +121,8 @@ export interface CommitOverrides {
107
121
  interceptSourceUrl?: string;
108
122
  /** Server-set location state to merge into history.pushState */
109
123
  serverState?: Record<string, unknown>;
124
+ /** The committed route's name (payload metadata), remembered per history entry. */
125
+ routeName?: string;
110
126
  }
111
127
 
112
128
  /**
@@ -132,10 +148,21 @@ export type UpdateMode =
132
148
  * boundaries do not animate the identical repaint.
133
149
  */
134
150
  optimisticPresented?: boolean;
151
+ /**
152
+ * A router.refresh() (or cross-tab refresh) of the current URL:
153
+ * transition({ when }) sees kind "revalidate", not "replace".
154
+ */
155
+ refresh?: boolean;
135
156
  }
136
157
  | { type: "leave-intercept"; interceptSourceUrl?: string }
137
158
  | { type: "stale-revalidation"; interceptSourceUrl?: string }
138
- | { type: "action"; interceptSourceUrl?: string; actionId?: string };
159
+ | {
160
+ type: "action";
161
+ interceptSourceUrl?: string;
162
+ actionId?: string;
163
+ /** The action's FormData and result, for transition({ when }) `action`. */
164
+ action?: TransitionWhenActionInput;
165
+ };
139
166
 
140
167
  /**
141
168
  * Type for the fetchPartialUpdate function
@@ -213,6 +240,54 @@ export function createPartialUpdater(
213
240
  }
214
241
  }
215
242
 
243
+ // transition({ when }). Every decision below runs before tx.commit()
244
+ // moves the store, so `from` is still the location being left:
245
+ // segmentState.currentUrl is the committed URL even on a popstate cache
246
+ // miss, where tx.currentUrl and window.location already point at the
247
+ // target.
248
+ const whenKind: TransitionWhenKind =
249
+ mode.type === "action"
250
+ ? "action"
251
+ : mode.type === "stale-revalidation" ||
252
+ (mode.type === "navigate" && mode.refresh)
253
+ ? "revalidate"
254
+ : tx.traversal
255
+ ? "pop"
256
+ : tx.replace
257
+ ? "replace"
258
+ : "push";
259
+ // Decide once per navigation (browser/transition-when.ts): reuse the
260
+ // decision the optimistic clientUrls() swap made; otherwise decide over
261
+ // every segment this commit presents, kept or re-sent (#989). The
262
+ // destination state is what the commit will leave on the entry.
263
+ const decideGatedOff = (
264
+ segmentsToCommit: readonly ResolvedSegment[],
265
+ metadata: {
266
+ params?: Record<string, string>;
267
+ routeName?: string;
268
+ locationState?: Record<string, unknown>;
269
+ },
270
+ ): boolean =>
271
+ tx.transitionGatedOff ??
272
+ // A stale revalidation the user navigated away from is discarded after
273
+ // the render (history-key check below): its predicate must not run.
274
+ ((mode.type === "stale-revalidation" &&
275
+ store.getHistoryKey() !== historyKeyAtStart) ||
276
+ decideCommitGatedOff(store, segmentsToCommit, whenKind, {
277
+ to: () => ({
278
+ url,
279
+ params: metadata.params ?? mergeSegmentParams(segmentsToCommit),
280
+ routeName: metadata.routeName,
281
+ state: tx.traversal
282
+ ? { ...window.history.state, ...metadata.locationState }
283
+ : buildHistoryState(tx.state, undefined, metadata.locationState),
284
+ }),
285
+ action:
286
+ mode.type === "action"
287
+ ? { ...mode.action, id: mode.actionId }
288
+ : undefined,
289
+ }));
290
+
216
291
  const targetCache =
217
292
  mode.type === "navigate" && mode.targetCacheSegments?.length
218
293
  ? mode.targetCacheSegments
@@ -349,13 +424,19 @@ export function createPartialUpdater(
349
424
  "[Browser] No diff but navigating with cached segments - rendering target route",
350
425
  );
351
426
 
427
+ const cachedGatedOff = decideGatedOff(
428
+ existingSegments,
429
+ payload.metadata,
430
+ );
352
431
  const newTree = await renderSegments(existingSegments, {
353
432
  forceAwait: true,
433
+ transitionGatedOff: cachedGatedOff,
354
434
  });
355
435
 
356
436
  const { scroll: commitScroll } = tx.commit(
357
437
  matchedIds,
358
438
  existingSegments,
439
+ { routeName: payload.metadata.routeName },
359
440
  );
360
441
 
361
442
  if (mode.targetCacheHandleData) {
@@ -376,7 +457,7 @@ export function createPartialUpdater(
376
457
  scroll: toScrollPayload(commitScroll),
377
458
  };
378
459
 
379
- if (shouldStartViewTransition(existingSegments)) {
460
+ if (shouldStartViewTransition(existingSegments, cachedGatedOff)) {
380
461
  commitInTransition(onUpdate, existingSegments, cachedUpdate, [
381
462
  "navigation",
382
463
  ]);
@@ -400,6 +481,7 @@ export function createPartialUpdater(
400
481
  const { scroll: leaveScroll } = tx.commit(
401
482
  matchedIds,
402
483
  existingSegments,
484
+ { routeName: payload.metadata.routeName },
403
485
  );
404
486
 
405
487
  onUpdate({
@@ -415,7 +497,9 @@ export function createPartialUpdater(
415
497
  debugLog(
416
498
  "[Browser] No changes - all revalidations returned false, keeping existing UI",
417
499
  );
418
- tx.commit(matchedIds, existingSegments);
500
+ tx.commit(matchedIds, existingSegments, {
501
+ routeName: payload.metadata.routeName,
502
+ });
419
503
  debugLog("[Browser] Navigation complete (no re-render)");
420
504
  return;
421
505
  }
@@ -477,7 +561,13 @@ export function createPartialUpdater(
477
561
  return;
478
562
  }
479
563
 
564
+ // Decide before the tree is built: on a gated-off commit every segment
565
+ // keeps its <ViewTransition> with "none" classes, so the decision must
566
+ // reach renderSegments. The response carries no decision, so a reused
567
+ // prefetch is decided against the real source.
568
+ const gatedOff = decideGatedOff(reconciled.segments, payload.metadata);
480
569
  const renderOptions = {
570
+ transitionGatedOff: gatedOff,
481
571
  isAction: mode.type === "action",
482
572
  // forceAwait unwraps the ROUTER loader promises during render so they
483
573
  // land without a loading()/fallback frame. A fully-prefetched nav has
@@ -557,16 +647,15 @@ export function createPartialUpdater(
557
647
 
558
648
  const allSegmentIds = matchedIds;
559
649
  const serverLocationState = payload.metadata?.locationState;
560
- const overrides: CommitOverrides | undefined = isInterceptResponse
561
- ? {
562
- scroll: false,
563
- intercept: true,
564
- interceptSourceUrl: effectiveInterceptSource,
565
- ...(serverLocationState && { serverState: serverLocationState }),
566
- }
567
- : serverLocationState
568
- ? { serverState: serverLocationState }
569
- : undefined;
650
+ const overrides: CommitOverrides = {
651
+ routeName: payload.metadata?.routeName,
652
+ ...(isInterceptResponse && {
653
+ scroll: false,
654
+ intercept: true,
655
+ interceptSourceUrl: effectiveInterceptSource,
656
+ }),
657
+ ...(serverLocationState && { serverState: serverLocationState }),
658
+ };
570
659
  const { scroll: navScroll } = tx.commit(
571
660
  allSegmentIds,
572
661
  reconciled.segments,
@@ -585,7 +674,10 @@ export function createPartialUpdater(
585
674
 
586
675
  debugLog("[partial-update] updating document");
587
676
 
588
- const hasTransition = shouldStartViewTransition(reconciled.segments);
677
+ const hasTransition = shouldStartViewTransition(
678
+ reconciled.segments,
679
+ gatedOff,
680
+ );
589
681
  const optimisticPresented =
590
682
  mode.type === "navigate" && mode.optimisticPresented === true;
591
683
  // [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. Reports which reconciled
@@ -600,6 +692,7 @@ export function createPartialUpdater(
600
692
  withTransition: reconciled.segments
601
693
  .filter((s) => s.transition)
602
694
  .map((s) => s.id),
695
+ transitionGatedOff: gatedOff,
603
696
  all: reconciled.segments.map((s) => s.id),
604
697
  });
605
698
  }
@@ -617,7 +710,10 @@ export function createPartialUpdater(
617
710
  metadata: payload.metadata!,
618
711
  scroll: scrollPayload,
619
712
  };
620
- if (mode.type === "action" || mode.type === "stale-revalidation") {
713
+ if (
714
+ !gatedOff &&
715
+ (mode.type === "action" || mode.type === "stale-revalidation")
716
+ ) {
621
717
  commitInTransition(
622
718
  onUpdate,
623
719
  reconciled.mainSegments,
@@ -633,7 +729,10 @@ export function createPartialUpdater(
633
729
  ? ["navigation", OPTIMISTIC_COMMIT_TRANSITION_TYPE]
634
730
  : ["navigation"],
635
731
  );
636
- } else if (fullyPrefetched || isSameStructureNav || optimisticPresented) {
732
+ } else if (
733
+ !gatedOff &&
734
+ (fullyPrefetched || isSameStructureNav || optimisticPresented)
735
+ ) {
637
736
  // Content-hold commit, two triggers. Fully-prefetched nav: the payload
638
737
  // is fully resolved (forceAwait above), so the transition commits
639
738
  // synchronously — no fallback flash. Same-structure nav: the re-run
@@ -663,10 +762,14 @@ export function createPartialUpdater(
663
762
  optimisticPresented ? [OPTIMISTIC_COMMIT_TRANSITION_TYPE] : [],
664
763
  );
665
764
  } else {
666
- // Cold/partially-prefetched nav that mounts NEW segments: normal
667
- // commit so fallbacks stream like a first load and the click has
668
- // visible feedback. Explicit transition() routes keep the
669
- // content-hold via the hasTransition branch above (the opt-in).
765
+ // Cold/partially-prefetched nav that mounts NEW segments, or any
766
+ // commit transition({ when }) gated off (navigation, action refetch,
767
+ // stale revalidation): normal commit so fallbacks stream
768
+ // like a first load and the click has visible feedback. A gated-off
769
+ // segment keeps its key, so its loading() fallback comes from its
770
+ // boundary re-suspending on this urgent commit, not from a remount
771
+ // (#995). Explicit transition() routes keep the content-hold via the
772
+ // hasTransition branch above (the opt-in).
670
773
  onUpdate(update);
671
774
  }
672
775
 
@@ -684,7 +787,10 @@ export function createPartialUpdater(
684
787
 
685
788
  const segmentIds = segments.map((s: ResolvedSegment) => s.id);
686
789
 
687
- const newTree = await renderSegments(segments);
790
+ const fullGatedOff = decideGatedOff(segments, payload.metadata ?? {});
791
+ const newTree = await renderSegments(segments, {
792
+ transitionGatedOff: fullGatedOff,
793
+ });
688
794
 
689
795
  if (signal?.aborted) {
690
796
  debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
@@ -692,13 +798,15 @@ export function createPartialUpdater(
692
798
  }
693
799
 
694
800
  const fullUpdateServerState = payload.metadata?.locationState;
695
- const { scroll: fullScroll } = fullUpdateServerState
696
- ? tx.commit(segmentIds, segments, {
697
- serverState: fullUpdateServerState,
698
- })
699
- : tx.commit(segmentIds, segments);
801
+ const { scroll: fullScroll } = tx.commit(segmentIds, segments, {
802
+ routeName: payload.metadata?.routeName,
803
+ ...(fullUpdateServerState && { serverState: fullUpdateServerState }),
804
+ });
700
805
 
701
- const fullHasTransition = shouldStartViewTransition(segments);
806
+ const fullHasTransition = shouldStartViewTransition(
807
+ segments,
808
+ fullGatedOff,
809
+ );
702
810
  const fullScrollPayload = toScrollPayload(fullScroll);
703
811
  const fullUpdate: NavigationUpdate = {
704
812
  root: newTree,
@@ -720,13 +828,16 @@ export function createPartialUpdater(
720
828
  );
721
829
  return;
722
830
  }
723
- commitInTransition(
724
- onUpdate,
725
- segments,
726
- fullUpdate,
727
- fullHasTransition ? ["action"] : [],
728
- );
729
- } else if (mode.type === "action") {
831
+ if (fullGatedOff) onUpdate(fullUpdate);
832
+ else {
833
+ commitInTransition(
834
+ onUpdate,
835
+ segments,
836
+ fullUpdate,
837
+ fullHasTransition ? ["action"] : [],
838
+ );
839
+ }
840
+ } else if (mode.type === "action" && !fullGatedOff) {
730
841
  commitInTransition(
731
842
  onUpdate,
732
843
  segments,
@@ -15,11 +15,11 @@
15
15
  * - Source-scoped: built by `buildSourceKey(rangoState, sourceHref, target)`
16
16
  * — shape `rangoState\0sourceHref\0/target?...`. Embeds the Rango state
17
17
  * (so rotation invalidates source-scoped entries too), the source href, and
18
- * the exact source segment tree. Populated when the
19
- * server tags a response with `X-RSC-Prefetch-Scope: source` (intercept
20
- * modals etc.), OR when a Link opts in with `prefetchKey=":source"` — in
21
- * both cases so source-sensitive responses cannot bleed into navigations
22
- * from other pages.
18
+ * the exact source segment tree. Populated when the server tags a
19
+ * response with `X-RSC-Prefetch-Scope: source` (a route an intercept
20
+ * targets, matched or not), OR when a Link opts in with
21
+ * `prefetchKey=":source"` — in both cases so source-sensitive responses
22
+ * cannot bleed into navigations from other pages.
23
23
  *
24
24
  * Also tracks in-flight prefetch promises. Each promise resolves to the
25
25
  * decoded prefetch entry (or null), letting navigation adopt a
@@ -172,7 +172,7 @@ let generation = 0;
172
172
  * rotation invalidates source-scoped entries alongside wildcard ones,
173
173
  * plus the source page href so the key is unique per originating page.
174
174
  * Populated either when the server tags a response with
175
- * `X-RSC-Prefetch-Scope: source` (intercept modals, etc.) or when a
175
+ * `X-RSC-Prefetch-Scope: source` (a route an intercept targets) or when a
176
176
  * Link opts in via `prefetchKey=":source"`.
177
177
  *
178
178
  * Source-scoped keys retain `_rsc_segments` because their exact source tree is
@@ -189,8 +189,8 @@ function buildPrefetchUrl(
189
189
  * - Otherwise: dual inflight registration under both `wildcardKey` and
190
190
  * `sourceKey` so same-source navigations adopt directly via their own
191
191
  * source key. Storage key is chosen at response time from the
192
- * `X-RSC-Prefetch-Scope` header — `"source"` → `sourceKey` (intercept
193
- * modals etc.), anything else → `wildcardKey`. The entry records its scope
192
+ * `X-RSC-Prefetch-Scope` header — `"source"` → `sourceKey` (a route an
193
+ * intercept targets), anything else → `wildcardKey`. The entry records its scope
194
194
  * so cross-source navigations that adopted via `wildcardKey` can bail out
195
195
  * in `navigation-client.ts` when the adopted entry turns out source-scoped.
196
196
  */
@@ -13,30 +13,23 @@ import React, {
13
13
  } from "react";
14
14
  import { NavigationStoreContext } from "./context.js";
15
15
  import { LinkContext } from "./use-link-status.js";
16
- import type { NavigateOptions } from "../types.js";
16
+ import type { HistoryState } from "../types.js";
17
17
  import {
18
18
  isHashOnlyNavigation,
19
19
  isPrefetchScopeDisabled,
20
20
  subscribeToPrefetchScopeChange,
21
21
  } from "../link-interceptor.js";
22
22
  import { subscribeToLocationChange } from "../event-controller.js";
23
- import {
24
- isLocationStateEntry,
25
- type LocationStateEntry,
26
- resolveLocationStateEntries,
27
- } from "./location-state.js";
23
+ import { isLocationStateDefinition } from "./location-state-shared.js";
28
24
 
29
- /**
30
- * State prop type for Link component.
31
- * - LocationStateEntry[]: Type-safe state entries via createLocationState()
32
- * - StateOrGetter: Plain state object or click-time getter function
33
- * - Record<string, unknown>: Plain state object passed to history.pushState
34
- */
35
25
  export type StateOrGetter<T = unknown> = T | (() => T);
36
26
 
37
- export type LinkState =
38
- | LocationStateEntry[]
39
- | StateOrGetter<Record<string, unknown>>;
27
+ /**
28
+ * State prop type for Link component: the `router.push()` state type
29
+ * ({@link HistoryState}: `LocationStateEntry[]` from createLocationState(), or
30
+ * plain structured-clone-safe state), or a getter for it called at click time.
31
+ */
32
+ export type LinkState = StateOrGetter<HistoryState>;
40
33
 
41
34
  import {
42
35
  prefetchDirect,
@@ -93,6 +86,14 @@ export interface LinkProps extends Omit<
93
86
  * @default true
94
87
  */
95
88
  revalidate?: boolean;
89
+ /**
90
+ * Set to `false` to present this navigation without a transition (urgent
91
+ * commit, `<ViewTransition>` classes `"none"`); no `transition({ when })`
92
+ * predicate is called. See `NavigateOptions.transition`.
93
+ *
94
+ * @default true
95
+ */
96
+ transition?: boolean;
96
97
  /**
97
98
  * Prefetch strategy for the link destination. When omitted, falls back to
98
99
  * the router-wide default (`createRouter({ defaultPrefetch })`: `"none"` in
@@ -118,8 +119,11 @@ export interface LinkProps extends Omit<
118
119
  * therefore serve the wrong diff to a navigation from a different
119
120
  * source.
120
121
  *
121
- * Intercept responses are auto-scoped to the source via a server-side
122
- * tag, so `":source"` is only needed for custom revalidation logic.
122
+ * A route an intercept targets is scoped to the source automatically
123
+ * (server-side `x-rsc-prefetch-scope: source`), whether or not the
124
+ * intercept applies from this page: the modal and the full page are
125
+ * both source-specific. Every other route stays shared, so `":source"`
126
+ * is only needed for custom revalidation logic.
123
127
  *
124
128
  * @example
125
129
  * ```tsx
@@ -226,6 +230,7 @@ export const Link: ForwardRefExoticComponent<
226
230
  scroll = true,
227
231
  reloadDocument = false,
228
232
  revalidate,
233
+ transition,
229
234
  prefetch,
230
235
  prefetchKey,
231
236
  state,
@@ -334,28 +339,26 @@ export const Link: ForwardRefExoticComponent<
334
339
  // Stop propagation to prevent link-interceptor from also handling this
335
340
  e.stopPropagation();
336
341
 
342
+ // Call a click-time getter; typed entries are resolved (and, in dev,
343
+ // checked) by the bridge's resolveNavigationState, like router.push().
344
+ // In dev an uncalled definition passes through to that check instead of
345
+ // being called as a getter.
337
346
  const currentState = stateRef.current;
338
- let resolvedState: unknown;
339
-
340
- if (
341
- Array.isArray(currentState) &&
342
- currentState.length > 0 &&
343
- isLocationStateEntry(currentState[0])
344
- ) {
345
- resolvedState = resolveLocationStateEntries(
346
- currentState as LocationStateEntry[],
347
- );
348
- } else if (typeof currentState === "function") {
349
- resolvedState = currentState();
350
- } else if (currentState != null) {
351
- resolvedState = currentState;
352
- }
347
+ const state =
348
+ typeof currentState === "function" &&
349
+ !(
350
+ process.env.NODE_ENV !== "production" &&
351
+ isLocationStateDefinition(currentState)
352
+ )
353
+ ? currentState()
354
+ : (currentState ?? undefined);
353
355
 
354
356
  ctx.navigate(resolvedTo, {
355
357
  replace,
356
358
  scroll,
357
- state: resolvedState,
359
+ state,
358
360
  revalidate,
361
+ transition,
359
362
  });
360
363
  },
361
364
  [
@@ -365,6 +368,7 @@ export const Link: ForwardRefExoticComponent<
365
368
  replace,
366
369
  scroll,
367
370
  revalidate,
371
+ transition,
368
372
  ctx,
369
373
  onClick,
370
374
  ],
@@ -487,6 +491,7 @@ export const Link: ForwardRefExoticComponent<
487
491
  data-scroll={scroll === false ? "false" : undefined}
488
492
  data-replace={replace ? "true" : undefined}
489
493
  data-revalidate={revalidate === false ? "false" : undefined}
494
+ data-transition={transition === false ? "false" : undefined}
490
495
  {...props}
491
496
  >
492
497
  <LinkContext.Provider value={resolvedTo}>{children}</LinkContext.Provider>
@@ -17,7 +17,7 @@ import {
17
17
  import type {
18
18
  NavigationStore,
19
19
  NavigationUpdate,
20
- NavigateOptions,
20
+ NavigateOptionsInternal,
21
21
  NavigationBridge,
22
22
  } from "../types.js";
23
23
  import type { EventController } from "../event-controller.js";
@@ -320,7 +320,7 @@ export function NavigationProvider({
320
320
  * Navigate to a URL (delegates to bridge)
321
321
  */
322
322
  const navigate = useCallback(
323
- async (url: string, options?: NavigateOptions): Promise<void> => {
323
+ async (url: string, options?: NavigateOptionsInternal): Promise<void> => {
324
324
  await bridge.navigate(url, options);
325
325
  },
326
326
  [],
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { createContext, type Context } from "react";
4
- import type { NavigationStore, NavigateOptions } from "../types.js";
4
+ import type { NavigationStore, NavigateOptionsInternal } from "../types.js";
5
5
  import type { EventController } from "../event-controller.js";
6
6
  import type { PrefetchStrategy } from "../../router/prefetch-default.js";
7
7
 
@@ -34,7 +34,7 @@ export interface NavigationStoreContextValue {
34
34
  * @param options - Navigation options (replace, scroll)
35
35
  * @returns Promise that resolves when navigation is complete
36
36
  */
37
- navigate: (url: string, options?: NavigateOptions) => Promise<void>;
37
+ navigate: (url: string, options?: NavigateOptionsInternal) => Promise<void>;
38
38
 
39
39
  /**
40
40
  * Refresh the current route