@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
@@ -30,6 +30,11 @@
30
30
  * model is the held-navigation useLoader().isLoading: a `transition` spec
31
31
  * plus a pending Promise seeded via navigate(url, { loaders }) commits
32
32
  * through production's commitInTransition (browser/partial-update.ts).
33
+ * - A spec's transition({ when }) decides every commit with production's
34
+ * browser decision: navigate() and useRouter().push model kind "push",
35
+ * useRouter().replace kind "replace", refresh() kind "revalidate".
36
+ * Back/forward, action and optimistic clientUrls() swap decisions are
37
+ * e2e territory.
33
38
  * What it DOES cover: client hooks that read NavigationProvider /
34
39
  * OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
35
40
  * useRouter, usePathname, useSearchParams, Outlet/useOutlet nesting and seeded
@@ -42,6 +47,7 @@
42
47
  * (the segment chain is wrapped in a MountContext exactly as in production).
43
48
  */
44
49
 
50
+ import { decideTransitionGatedOff } from "../browser/transition-when.js";
45
51
  import { useEffect, type ReactNode, type ComponentType } from "react";
46
52
  import type { RenderResult } from "@testing-library/react";
47
53
  import { renderSegments } from "../segment-system.js";
@@ -56,6 +62,7 @@ import {
56
62
  shouldStartViewTransition,
57
63
  } from "../browser/partial-update.js";
58
64
  import type {
65
+ NavigateOptionsInternal,
59
66
  NavigationStore,
60
67
  NavigationBridge,
61
68
  UpdateSubscriber,
@@ -68,8 +75,19 @@ import {
68
75
  buildParamsFromMatch,
69
76
  } from "../router/pattern-matching.js";
70
77
  import { normalizeBasename } from "../router/basename.js";
71
- import type { LoaderDefinition, TransitionConfig } from "../types.js";
78
+ import type {
79
+ LoaderDefinition,
80
+ TransitionConfig,
81
+ TransitionWhenKind,
82
+ } from "../types.js";
72
83
  import type { LocationStateDefinition } from "../browser/react/location-state-shared.js";
84
+ import {
85
+ buildHistoryState,
86
+ hasLocationState,
87
+ pushHistoryWithIdx,
88
+ resolveNavigationState,
89
+ } from "../browser/history-state.js";
90
+ import { withLocationStateKey } from "./location-state-key.js";
73
91
  import type { Handle } from "../handle.js";
74
92
  import type { ThemeConfig } from "../theme/types.js";
75
93
  import { resolveThemeConfig } from "../theme/constants.js";
@@ -93,18 +111,15 @@ export type HandleDataSeed = Record<string, Record<string, unknown[]>>;
93
111
  const syntheticIds = new WeakMap<object, string>();
94
112
  let syntheticIdCounter = 0;
95
113
 
96
- function ensureSyntheticId(
97
- handle: object,
98
- field: "$$id" | "__rsc_ls_key",
99
- ): string {
100
- const existing = (handle as Record<string, string>)[field];
114
+ function ensureSyntheticId(loader: object): string {
115
+ const existing = (loader as { $$id?: string }).$$id;
101
116
  if (existing) return existing;
102
- let id = syntheticIds.get(handle);
117
+ let id = syntheticIds.get(loader);
103
118
  if (!id) {
104
119
  id = `__rango_test_id_${syntheticIdCounter++}`;
105
- syntheticIds.set(handle, id);
120
+ syntheticIds.set(loader, id);
106
121
  }
107
- (handle as Record<string, string>)[field] = id;
122
+ (loader as { $$id?: string }).$$id = id;
108
123
  return id;
109
124
  }
110
125
 
@@ -337,11 +352,20 @@ export interface TestRouterHandle {
337
352
  * // reader: isLoading true, old data
338
353
  * await act(async () => resolve({ name: "Product 2" }));
339
354
  * // reader: isLoading false, new data
355
+ *
356
+ * `options.transition: false` is the per-navigation opt-out
357
+ * (`router.push(url, { transition: false })`, `<Link transition={false}>`):
358
+ * the commit is urgent and no transition({ when }) predicate is called.
340
359
  */
341
360
  navigate(
342
361
  url: string,
343
- options?: Pick<RenderRouteOptions, "loaders">,
362
+ options?: Pick<RenderRouteOptions, "loaders"> & { transition?: boolean },
344
363
  ): Promise<void>;
364
+ /**
365
+ * Re-render the current location, as router.refresh() does: a spec's
366
+ * transition({ when }) decides it with kind "revalidate" (`to` is `from`).
367
+ */
368
+ refresh(): Promise<void>;
345
369
  /** The current committed pathname. */
346
370
  pathname(): string;
347
371
  /** The current committed params. */
@@ -508,7 +532,7 @@ export async function renderRoute(
508
532
  ): Record<string, unknown> => {
509
533
  const out = { ...base };
510
534
  for (const [loader, data] of loaders ?? []) {
511
- out[ensureSyntheticId(loader as object, "$$id")] = data;
535
+ out[ensureSyntheticId(loader)] = data;
512
536
  }
513
537
  return out;
514
538
  };
@@ -517,7 +541,7 @@ export async function renderRoute(
517
541
  if (typeof window !== "undefined") {
518
542
  const stateObj: Record<string, unknown> = {};
519
543
  for (const [def, value] of options.locationState ?? []) {
520
- stateObj[ensureSyntheticId(def as object, "__rsc_ls_key")] = value;
544
+ stateObj[withLocationStateKey(def).__rsc_ls_key] = value;
521
545
  }
522
546
  window.history.replaceState(stateObj, "");
523
547
  }
@@ -597,8 +621,30 @@ export async function renderRoute(
597
621
  let warnedNavLifecycle = false;
598
622
  const navigate = async (
599
623
  target: string,
600
- navOptions?: Pick<RenderRouteOptions, "loaders">,
624
+ navOptions?: Pick<RenderRouteOptions, "loaders"> & {
625
+ history?: Pick<
626
+ NavigateOptionsInternal,
627
+ "state" | "replace" | "transition"
628
+ >;
629
+ kind?: TransitionWhenKind;
630
+ transition?: boolean;
631
+ },
601
632
  ): Promise<void> => {
633
+ // A useRouter().push/replace or <Link> navigation (`history`) writes its
634
+ // entry the way production does (navigation-bridge.ts navigate ->
635
+ // navigation-transaction.ts commit): the dev state check, typed entries
636
+ // spread onto history.state, the idx stamp, and the __rsc_locationstate
637
+ // event when the old or new entry carries location state. The URL stays
638
+ // put: renderRoute tracks location on the event controller, not
639
+ // window.location. router.navigate() and refresh() leave history alone.
640
+ const history = navOptions?.history;
641
+ const historyState = history
642
+ ? buildHistoryState(
643
+ history.state !== undefined
644
+ ? resolveNavigationState(history.state)
645
+ : undefined,
646
+ )
647
+ : undefined;
602
648
  // No server fetch, so the navigation lifecycle never starts: the state
603
649
  // useNavigation()/useLinkStatus()/useAction() read stays "idle" — asserting
604
650
  // a pending/loading/submitting state here proves nothing. Warn once (per
@@ -623,30 +669,76 @@ export async function renderRoute(
623
669
  mount,
624
670
  );
625
671
  const metadata = makeMetadata(nextUrl.pathname, segments, match.params);
672
+ // Production's browser-run transition({ when }) decision
673
+ // (browser/partial-update.ts): the committed location is the source.
674
+ const kind: TransitionWhenKind =
675
+ navOptions?.kind ?? (history?.replace ? "replace" : "push");
676
+ // `transition: false` gates off without calling a predicate
677
+ // (navigation-bridge.ts navigate).
678
+ const transitionOptOut =
679
+ (navOptions?.transition ?? history?.transition) === false;
680
+ const gatedOff =
681
+ transitionOptOut ||
682
+ decideTransitionGatedOff(segments, () => ({
683
+ kind,
684
+ from: {
685
+ url: eventController.getLocation().href,
686
+ params: eventController.getParams(),
687
+ routeName: leaf.name,
688
+ state: window.history.state,
689
+ },
690
+ to: {
691
+ url: nextUrl,
692
+ params: match.params,
693
+ routeName: leaf.name,
694
+ state: historyState ?? null,
695
+ },
696
+ }));
626
697
  const root = await renderSegments(segments, {
627
698
  outletPending: options.outletPending,
699
+ transitionGatedOff: gatedOff,
628
700
  });
629
701
  eventController.setLocation(nextUrl);
630
702
  eventController.setParams(match.params);
631
703
  store.setCurrentUrl(nextUrl.href);
632
704
  store.setSegmentIds(segments.map((s) => s.id));
705
+ let notifyLocationState = false;
706
+ if (history) {
707
+ notifyLocationState =
708
+ hasLocationState(window.history.state) ||
709
+ hasLocationState(historyState);
710
+ pushHistoryWithIdx(
711
+ historyState ?? null,
712
+ window.location.href,
713
+ history.replace ?? false,
714
+ );
715
+ }
633
716
  const emit: UpdateSubscriber = (update) => store.emitUpdate(update);
634
717
  await act(async () => {
635
718
  // Production's transition() lane (browser/partial-update.ts). Its
636
719
  // same-structure / fully-prefetched / optimistic hold lanes are not
637
720
  // modeled: without transition() the commit stays urgent.
638
- if (shouldStartViewTransition(segments)) {
721
+ if (shouldStartViewTransition(segments, gatedOff)) {
639
722
  commitInTransition(emit, segments, { root, metadata }, ["navigation"]);
640
723
  } else {
641
724
  emit({ root, metadata });
642
725
  }
726
+ if (notifyLocationState) {
727
+ window.dispatchEvent(new Event("__rsc_locationstate"));
728
+ }
643
729
  });
644
730
  };
645
731
 
732
+ const refresh = (): Promise<void> => {
733
+ const current = new URL(eventController.getLocation().href);
734
+ return navigate(current.pathname + current.search, { kind: "revalidate" });
735
+ };
736
+
646
737
  let prefetchRoot: HTMLElement | undefined;
647
738
  const bridge: NavigationBridge = {
648
- navigate: (target) => navigate(target),
649
- refresh: () => navigate(url.pathname + url.search),
739
+ navigate: (target, navigateOptions) =>
740
+ navigate(target, { history: navigateOptions ?? {} }),
741
+ refresh: () => refresh(),
650
742
  handlePopstate: async () => {},
651
743
  registerLinkInterception: () => () => {},
652
744
  registerDelegatedPrefetch: () =>
@@ -704,6 +796,7 @@ export async function renderRoute(
704
796
 
705
797
  const router: TestRouterHandle = {
706
798
  navigate,
799
+ refresh,
707
800
  pathname: () => new URL(eventController.getLocation().href).pathname,
708
801
  params: () => eventController.getParams(),
709
802
  store,
@@ -10,7 +10,8 @@
10
10
  * Synchronous: client revalidate functions must be sync.
11
11
  */
12
12
 
13
- import { makeIsAction, resolveActionRefId } from "../router/is-action.js";
13
+ import { makeIsAction } from "../router/is-action.js";
14
+ import { resolveTestActionId } from "./internal/action-id.js";
14
15
  import {
15
16
  lockedClientDefault,
16
17
  runClientRevalidateChain,
@@ -23,23 +24,6 @@ import type {
23
24
 
24
25
  const DEFAULT_URL = "http://localhost/";
25
26
 
26
- function resolveActionId(
27
- action: ((...args: never[]) => unknown) | string | undefined,
28
- ): string | undefined {
29
- if (action === undefined) return undefined;
30
- if (typeof action === "string") return action;
31
- const id = resolveActionRefId(action);
32
- if (id === undefined) {
33
- throw new Error(
34
- "runClientRevalidate: `action` must be a single imported server action " +
35
- "(carrying its build-injected id) or an actionId string. The passed " +
36
- "function has no $id/$$id — outside a built app, pass the id string " +
37
- 'your predicate should match (e.g. "src/actions/cart.ts#addToCart").',
38
- );
39
- }
40
- return id;
41
- }
42
-
43
27
  /**
44
28
  * Options for {@link runClientRevalidate}. Defaults model a same-URL
45
29
  * navigation with no action (locked default `false`).
@@ -80,7 +64,7 @@ export function runClientRevalidate(
80
64
  const currentParams = opts.currentParams ?? {};
81
65
  const nextParams = opts.nextParams ?? currentParams;
82
66
  const inAction = opts.action !== undefined;
83
- const actionId = resolveActionId(opts.action);
67
+ const actionId = resolveTestActionId(opts.action, "runClientRevalidate");
84
68
  const defaultShouldRevalidate = lockedClientDefault({
85
69
  actionRequest: opts.actionRequest ?? inAction,
86
70
  currentParams,
@@ -48,6 +48,10 @@ import { collectHandle } from "./collect-handle.js";
48
48
  import type { ThemeConfig } from "../theme/types.js";
49
49
  import type { SegmentCacheStore } from "../cache/types.js";
50
50
  import type { CacheProfile } from "../cache/profile-registry.js";
51
+ import type { PartialCacheOptions } from "../types/cache-types.js";
52
+ import type { HandlerContext } from "../types/handler-context.js";
53
+ import { resolveLoaderData } from "../router/segment-resolution/loader-cache.js";
54
+ import { armLoaderTagSets } from "../cache/cache-tag.js";
51
55
  import {
52
56
  createTestRequestContext,
53
57
  buildRunSnapshot,
@@ -111,9 +115,11 @@ export interface RunLoaderOptions<TEnv = any> {
111
115
  basename?: string;
112
116
  /**
113
117
  * Theme config in the same shape `createRouter({ theme })` takes (e.g. `true`
114
- * or `{ themes: [...] }`). Seeds the request's theme config so nested handler
115
- * or cache contexts created from this loader observe it. Loaders themselves do
116
- * not expose `ctx.theme`/`ctx.setTheme` (those are handler/middleware-only).
118
+ * or `{ themes: [...] }`). Seeds the request's theme config, so the loader
119
+ * body reads the visitor's theme (the Cookie on `request`, or the default)
120
+ * with `getRequestContext().theme`. That read is guarded like `cookies()`: it
121
+ * throws inside a `"use cache"` function (#971). The loader ctx has no
122
+ * `theme`/`setTheme` (those are handler/middleware-only).
117
123
  */
118
124
  theme?: ThemeConfig | true;
119
125
  /** Environment bindings surfaced as `ctx.env`. */
@@ -151,6 +157,18 @@ export interface RunLoaderOptions<TEnv = any> {
151
157
  cacheStore?: SegmentCacheStore;
152
158
  /** Cache profiles (the `createRouter({ cacheProfiles })` shape). */
153
159
  cacheProfiles?: Record<string, CacheProfile>;
160
+ /**
161
+ * Run the loader through the `cache()` a route binds it with — the options
162
+ * `loader(Loader, () => [cache({...})])` takes — using the production
163
+ * read-through against `cache.store`, else `cacheStore`. A HIT returns the
164
+ * stored value without running the body and records the tags the body
165
+ * recorded when the entry was written, so `updateTag()` of any of them drops
166
+ * the entry. The write is a background task the call does not await (spy
167
+ * on the store's `setItem` to wait for it). A raw loader body is keyed by
168
+ * its function reference. `handlePushes` lists the body's pushes, so a HIT
169
+ * reports none: the replay into the page's handle store is not modeled.
170
+ */
171
+ cache?: PartialCacheOptions;
154
172
  /**
155
173
  * Customize the rango state cookie a loader that calls
156
174
  * `invalidateClientCache()` rotates (the name is always seeded — default
@@ -352,6 +370,50 @@ function runWithLoaderContext<R>(
352
370
  });
353
371
  }
354
372
 
373
+ const rawLoaderIds = new WeakMap<object, string>();
374
+ let rawLoaderSeq = 0;
375
+
376
+ function cacheLoaderId(loader: RunnableLoader<unknown>): string {
377
+ if (typeof loader !== "function") return loader.$$id;
378
+ let id = rawLoaderIds.get(loader);
379
+ if (id === undefined) {
380
+ id = `runLoader#${++rawLoaderSeq}`;
381
+ rawLoaderIds.set(loader, id);
382
+ }
383
+ return id;
384
+ }
385
+
386
+ /**
387
+ * Call the loader: directly, or through its `cache()` binding (the funnel a
388
+ * route's DSL loaders take, loader-cache.ts resolveLoaderData), whose MISS
389
+ * reads the binding through the handler ctx's use(). A binding arms the
390
+ * per-execution loader tag sets first, as a match that resolves one does
391
+ * (match-api.ts, bindsLoaderCache).
392
+ */
393
+ function invokeLoader<T>(
394
+ loader: RunnableLoader<T>,
395
+ loaderFn: (ctx: TestLoaderContext) => Promise<T> | T,
396
+ opts: RunLoaderOptions,
397
+ loaderCtx: TestLoaderContext,
398
+ ): Promise<T> {
399
+ const run = () => Promise.resolve(loaderFn(loaderCtx));
400
+ if (!opts.cache) return run();
401
+ armLoaderTagSets();
402
+ const handlerCtx = {
403
+ params: loaderCtx.params,
404
+ use: run,
405
+ } as unknown as HandlerContext<any, any>;
406
+ return resolveLoaderData(
407
+ {
408
+ loader: { __brand: "loader", $$id: cacheLoaderId(loader) },
409
+ revalidate: [],
410
+ cache: { options: opts.cache },
411
+ },
412
+ handlerCtx,
413
+ loaderCtx.pathname,
414
+ );
415
+ }
416
+
355
417
  export async function runLoader<T>(
356
418
  loader: RunnableLoader<T>,
357
419
  opts: RunLoaderOptions = {},
@@ -359,7 +421,7 @@ export async function runLoader<T>(
359
421
  const loaderFn = resolveLoaderFn(loader);
360
422
  const { ctx } = createTestRequestContext(buildLoaderCtxOpts(opts));
361
423
  return runWithLoaderContext(ctx as RequestContext<any>, opts, (loaderCtx) =>
362
- Promise.resolve(loaderFn(loaderCtx)),
424
+ invokeLoader(loader, loaderFn, opts, loaderCtx),
363
425
  );
364
426
  }
365
427
 
@@ -413,7 +475,7 @@ export async function runLoaderResult<T>(
413
475
  result = await runWithLoaderContext(
414
476
  reqCtx,
415
477
  opts,
416
- (loaderCtx) => Promise.resolve(loaderFn(loaderCtx)),
478
+ (loaderCtx) => invokeLoader(loader, loaderFn, opts, loaderCtx),
417
479
  handlePushes,
418
480
  );
419
481
  } catch (error) {