@rangojs/router 0.18.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 (244) hide show
  1. package/dist/types/browser/event-controller.d.ts +3 -3
  2. package/dist/types/browser/history-state.d.ts +2 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  4. package/dist/types/browser/partial-update.d.ts +16 -1
  5. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  6. package/dist/types/browser/react/Link.d.ts +19 -9
  7. package/dist/types/browser/react/context.d.ts +2 -2
  8. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  9. package/dist/types/browser/react/location-state.d.ts +1 -1
  10. package/dist/types/browser/transition-when.d.ts +80 -0
  11. package/dist/types/browser/types.d.ts +104 -9
  12. package/dist/types/build/generate-manifest.d.ts +8 -0
  13. package/dist/types/cache/background-task.d.ts +24 -2
  14. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +44 -13
  16. package/dist/types/cache/cache-policy.d.ts +1 -1
  17. package/dist/types/cache/cache-runtime.d.ts +7 -0
  18. package/dist/types/cache/cache-scope.d.ts +117 -35
  19. package/dist/types/cache/cache-tag.d.ts +24 -4
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
  21. package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
  22. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
  23. package/dist/types/cache/document-cache.d.ts +10 -0
  24. package/dist/types/cache/handle-snapshot.d.ts +40 -8
  25. package/dist/types/cache/invalidation-order.d.ts +23 -0
  26. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +88 -141
  28. package/dist/types/cache/tag-invalidation.d.ts +38 -14
  29. package/dist/types/cache/types.d.ts +68 -61
  30. package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
  31. package/dist/types/client-urls/navigation.d.ts +28 -2
  32. package/dist/types/client-urls/server-projection.d.ts +5 -2
  33. package/dist/types/client-urls/types.d.ts +5 -5
  34. package/dist/types/client.d.ts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  38. package/dist/types/route-name.d.ts +7 -0
  39. package/dist/types/router/error-handling.d.ts +1 -1
  40. package/dist/types/router/intercept-resolution.d.ts +2 -0
  41. package/dist/types/router/match-context.d.ts +5 -0
  42. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  43. package/dist/types/router/router-options.d.ts +4 -1
  44. package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
  45. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  46. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
  47. package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
  48. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  49. package/dist/types/router/transition-when.d.ts +24 -10
  50. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  51. package/dist/types/rsc/full-payload.d.ts +14 -7
  52. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  53. package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
  54. package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
  55. package/dist/types/rsc/shell-capture.d.ts +78 -81
  56. package/dist/types/rsc/shell-serve.d.ts +19 -48
  57. package/dist/types/rsc/types.d.ts +2 -0
  58. package/dist/types/segment-system.d.ts +7 -0
  59. package/dist/types/server/context.d.ts +89 -61
  60. package/dist/types/server/cookie-store.d.ts +30 -18
  61. package/dist/types/server/handle-store.d.ts +52 -1
  62. package/dist/types/server/request-context.d.ts +66 -45
  63. package/dist/types/server/request-headers.d.ts +44 -0
  64. package/dist/types/server.d.ts +1 -0
  65. package/dist/types/testing/e2e/index.d.ts +1 -1
  66. package/dist/types/testing/index.d.ts +3 -2
  67. package/dist/types/testing/internal/action-id.d.ts +7 -0
  68. package/dist/types/testing/location-state-key.d.ts +24 -0
  69. package/dist/types/testing/render-route.d.ts +17 -1
  70. package/dist/types/testing/run-transition-when.d.ts +44 -69
  71. package/dist/types/testing/serve-shell-request.d.ts +7 -6
  72. package/dist/types/testing/shell-status.d.ts +34 -22
  73. package/dist/types/transition-when-ref.d.ts +79 -0
  74. package/dist/types/types/cache-types.d.ts +15 -3
  75. package/dist/types/types/index.d.ts +1 -1
  76. package/dist/types/types/segments.d.ts +86 -71
  77. package/dist/types/urls/path-helper-types.d.ts +3 -4
  78. package/dist/types/urls/pattern-types.d.ts +3 -4
  79. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  80. package/dist/vite/index.js +960 -97
  81. package/package.json +1 -1
  82. package/skills/cache-guide/SKILL.md +76 -49
  83. package/skills/caching/SKILL.md +167 -69
  84. package/skills/client-urls/SKILL.md +1 -1
  85. package/skills/comparison/references/framework-comparison.md +8 -5
  86. package/skills/document-cache/SKILL.md +24 -0
  87. package/skills/hooks/SKILL.md +3 -1
  88. package/skills/hooks/navigation.md +23 -2
  89. package/skills/hooks/state.md +98 -6
  90. package/skills/intercept/SKILL.md +29 -12
  91. package/skills/loader/SKILL.md +52 -21
  92. package/skills/middleware/SKILL.md +18 -0
  93. package/skills/migrate-nextjs/SKILL.md +3 -3
  94. package/skills/observability/SKILL.md +6 -5
  95. package/skills/parallel/SKILL.md +1 -1
  96. package/skills/ppr/SKILL.md +129 -77
  97. package/skills/prerender/SKILL.md +3 -3
  98. package/skills/rango/SKILL.md +25 -6
  99. package/skills/route/SKILL.md +2 -0
  100. package/skills/testing/SKILL.md +27 -25
  101. package/skills/testing/cache-prerender.md +9 -9
  102. package/skills/testing/client-components.md +45 -3
  103. package/skills/testing/loader.md +3 -2
  104. package/skills/testing/middleware.md +1 -0
  105. package/skills/testing/predicates.md +49 -49
  106. package/skills/testing/render-handler.md +1 -0
  107. package/skills/testing/serve-shell-request.md +14 -0
  108. package/skills/testing/server-actions.md +9 -3
  109. package/skills/theme/SKILL.md +8 -4
  110. package/skills/typesafety/env-and-bindings.md +8 -0
  111. package/skills/use-cache/SKILL.md +64 -19
  112. package/skills/view-transitions/SKILL.md +150 -29
  113. package/src/__augment-tests__/augmented.check.ts +23 -0
  114. package/src/browser/event-controller.ts +4 -4
  115. package/src/browser/history-state.ts +52 -1
  116. package/src/browser/link-interceptor.ts +4 -0
  117. package/src/browser/navigation-bridge.ts +77 -18
  118. package/src/browser/navigation-client.ts +4 -3
  119. package/src/browser/navigation-store.ts +43 -1
  120. package/src/browser/navigation-transaction.ts +31 -11
  121. package/src/browser/partial-update.ts +146 -35
  122. package/src/browser/prefetch/cache.ts +6 -6
  123. package/src/browser/prefetch/fetch.ts +2 -2
  124. package/src/browser/react/Link.tsx +38 -33
  125. package/src/browser/react/NavigationProvider.tsx +2 -2
  126. package/src/browser/react/context.ts +2 -2
  127. package/src/browser/react/location-state-shared.ts +241 -49
  128. package/src/browser/react/location-state.ts +5 -2
  129. package/src/browser/rsc-router.tsx +4 -0
  130. package/src/browser/server-action-bridge.ts +84 -15
  131. package/src/browser/transition-when.ts +205 -0
  132. package/src/browser/types.ts +124 -10
  133. package/src/build/generate-manifest.ts +77 -1
  134. package/src/cache/background-task.ts +55 -2
  135. package/src/cache/cache-exec-scope.ts +48 -1
  136. package/src/cache/cache-key-utils.ts +56 -16
  137. package/src/cache/cache-policy.ts +9 -4
  138. package/src/cache/cache-runtime.ts +197 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +105 -13
  141. package/src/cache/cf/cf-cache-store.ts +152 -76
  142. package/src/cache/cf/cf-shell-frame.ts +2 -2
  143. package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
  144. package/src/cache/document-cache.ts +53 -7
  145. package/src/cache/handle-snapshot.ts +52 -9
  146. package/src/cache/invalidation-order.ts +84 -0
  147. package/src/cache/request-tag-mask.ts +147 -0
  148. package/src/cache/shell-snapshot.ts +130 -348
  149. package/src/cache/tag-invalidation.ts +57 -51
  150. package/src/cache/types.ts +71 -67
  151. package/src/cache/vercel/vercel-cache-store.ts +61 -58
  152. package/src/client-urls/client-root.tsx +8 -4
  153. package/src/client-urls/client-urls.ts +8 -6
  154. package/src/client-urls/navigation.ts +55 -4
  155. package/src/client-urls/server-projection.ts +31 -10
  156. package/src/client-urls/types.ts +12 -5
  157. package/src/client.tsx +2 -0
  158. package/src/host/cookie-handler.ts +2 -1
  159. package/src/index.rsc.ts +3 -0
  160. package/src/index.ts +3 -0
  161. package/src/loader-redirect.tsx +7 -3
  162. package/src/prerender/build-shell-capture.ts +155 -146
  163. package/src/route-definition/dsl-helpers.ts +2 -0
  164. package/src/route-definition/helpers-types.ts +11 -11
  165. package/src/route-name.ts +12 -0
  166. package/src/router/content-negotiation.ts +4 -1
  167. package/src/router/error-handling.ts +5 -2
  168. package/src/router/handler-context.ts +10 -7
  169. package/src/router/intercept-resolution.ts +11 -1
  170. package/src/router/loader-resolution.ts +28 -3
  171. package/src/router/logging.ts +5 -3
  172. package/src/router/match-api.ts +62 -34
  173. package/src/router/match-context.ts +5 -0
  174. package/src/router/match-handlers.ts +9 -29
  175. package/src/router/match-middleware/background-revalidation.ts +3 -5
  176. package/src/router/match-middleware/cache-lookup.ts +35 -19
  177. package/src/router/match-middleware/cache-store.ts +15 -9
  178. package/src/router/match-result.ts +1 -0
  179. package/src/router/middleware.ts +2 -5
  180. package/src/router/navigation-snapshot.ts +6 -5
  181. package/src/router/prerender-match.ts +4 -0
  182. package/src/router/request-classification.ts +7 -4
  183. package/src/router/router-options.ts +4 -1
  184. package/src/router/segment-resolution/fresh.ts +0 -4
  185. package/src/router/segment-resolution/loader-cache.ts +150 -25
  186. package/src/router/segment-resolution/loader-mask.ts +3 -4
  187. package/src/router/segment-resolution/loader-snapshot.ts +15 -78
  188. package/src/router/segment-resolution/mask-nested.ts +47 -22
  189. package/src/router/segment-resolution/revalidation.ts +0 -3
  190. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  191. package/src/router/telemetry.ts +6 -3
  192. package/src/router/transition-when.ts +34 -52
  193. package/src/router.ts +2 -1
  194. package/src/rsc/attach-transition-when.ts +47 -0
  195. package/src/rsc/full-payload.ts +30 -10
  196. package/src/rsc/handler.ts +12 -10
  197. package/src/rsc/loader-fetch.ts +2 -1
  198. package/src/rsc/manifest-init.ts +4 -0
  199. package/src/rsc/origin-guard.ts +8 -5
  200. package/src/rsc/progressive-enhancement.ts +7 -30
  201. package/src/rsc/response-cache-serve.ts +68 -80
  202. package/src/rsc/response-route-handler.ts +2 -2
  203. package/src/rsc/rsc-rendering.ts +206 -252
  204. package/src/rsc/server-action.ts +8 -20
  205. package/src/rsc/shell-build-manifest.ts +22 -20
  206. package/src/rsc/shell-capture-constants.ts +136 -20
  207. package/src/rsc/shell-capture.ts +352 -589
  208. package/src/rsc/shell-serve.ts +73 -109
  209. package/src/rsc/ssr-setup.ts +3 -2
  210. package/src/rsc/types.ts +2 -0
  211. package/src/segment-system.tsx +40 -7
  212. package/src/server/context.ts +189 -124
  213. package/src/server/cookie-store.ts +153 -127
  214. package/src/server/handle-store.ts +245 -27
  215. package/src/server/request-context.ts +123 -84
  216. package/src/server/request-headers.ts +134 -0
  217. package/src/server.ts +4 -0
  218. package/src/testing/dispatch.ts +4 -7
  219. package/src/testing/e2e/index.ts +1 -0
  220. package/src/testing/index.ts +5 -0
  221. package/src/testing/internal/action-id.ts +25 -0
  222. package/src/testing/internal/context.ts +1 -1
  223. package/src/testing/location-state-key.ts +45 -0
  224. package/src/testing/render-handler.ts +1 -1
  225. package/src/testing/render-route.tsx +109 -16
  226. package/src/testing/run-client-revalidate.ts +3 -19
  227. package/src/testing/run-loader.ts +5 -1
  228. package/src/testing/run-transition-when.ts +109 -172
  229. package/src/testing/serve-shell-request.ts +10 -7
  230. package/src/testing/shell-status.ts +60 -56
  231. package/src/transition-when-ref.ts +220 -0
  232. package/src/types/cache-types.ts +15 -3
  233. package/src/types/index.ts +3 -0
  234. package/src/types/segments.ts +94 -91
  235. package/src/urls/path-helper-types.ts +3 -4
  236. package/src/urls/pattern-types.ts +3 -4
  237. package/src/vite/discovery/discover-routers.ts +3 -0
  238. package/src/vite/discovery/shell-prerender-phase.ts +0 -2
  239. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  240. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  241. package/src/vite/rango.ts +2 -0
  242. package/src/vite/router-discovery.ts +75 -5
  243. package/dist/types/rsc/transition-gate.d.ts +0 -23
  244. package/src/rsc/transition-gate.ts +0 -86
@@ -0,0 +1,45 @@
1
+ import {
2
+ peekLocationStateKey,
3
+ type LocationStateDefinition,
4
+ } from "../browser/react/location-state-shared.js";
5
+
6
+ // Typed history.state slots must carry this prefix: buildHistoryState spreads
7
+ // only `__rsc_ls_*` keys onto history.state (history-state.ts).
8
+ const KEY_PREFIX = "__rsc_ls_";
9
+
10
+ let syntheticKeyCounter = 0;
11
+
12
+ /**
13
+ * Assign a location-state key to a `createLocationState()` definition in a unit
14
+ * test that runs without the rango Vite plugin (which injects the key in dev
15
+ * and build). Outside production, a definition without a key throws on first
16
+ * use (`MyState(value)`, `useLocationState(MyState)`, `.read()`).
17
+ *
18
+ * - `withLocationStateKey(GridState, "GridState")` sets `__rsc_ls_GridState`
19
+ * (a name that already starts with `__rsc_ls_` is used as-is).
20
+ * - Without a name, a key already set (by the plugin or an earlier call) is
21
+ * kept; otherwise the definition gets a synthetic `__rsc_ls_test_<n>` key,
22
+ * which then stays on it.
23
+ *
24
+ * Returns the definition.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * import { withLocationStateKey } from "@rangojs/router/testing";
29
+ * import { GridState } from "../src/location-states";
30
+ *
31
+ * withLocationStateKey(GridState);
32
+ * ```
33
+ */
34
+ export function withLocationStateKey<
35
+ TDefinition extends LocationStateDefinition<any, any>,
36
+ >(definition: TDefinition, name?: string): TDefinition {
37
+ if (name !== undefined) {
38
+ definition.__rsc_ls_key = name.startsWith(KEY_PREFIX)
39
+ ? name
40
+ : KEY_PREFIX + name;
41
+ } else if (!peekLocationStateKey(definition)) {
42
+ definition.__rsc_ls_key = `${KEY_PREFIX}test_${syntheticKeyCounter++}`;
43
+ }
44
+ return definition;
45
+ }
@@ -338,7 +338,7 @@ export async function renderHandler<TEnv = any>(
338
338
  }
339
339
  });
340
340
 
341
- const cookies = { ...reqCtx.cookies() };
341
+ const cookies = reqCtx._readCookies();
342
342
  const responseSource = didThrow
343
343
  ? thrown
344
344
  : out instanceof Response
@@ -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,
@@ -51,6 +51,7 @@ import type { CacheProfile } from "../cache/profile-registry.js";
51
51
  import type { PartialCacheOptions } from "../types/cache-types.js";
52
52
  import type { HandlerContext } from "../types/handler-context.js";
53
53
  import { resolveLoaderData } from "../router/segment-resolution/loader-cache.js";
54
+ import { armLoaderTagSets } from "../cache/cache-tag.js";
54
55
  import {
55
56
  createTestRequestContext,
56
57
  buildRunSnapshot,
@@ -385,7 +386,9 @@ function cacheLoaderId(loader: RunnableLoader<unknown>): string {
385
386
  /**
386
387
  * Call the loader: directly, or through its `cache()` binding (the funnel a
387
388
  * route's DSL loaders take, loader-cache.ts resolveLoaderData), whose MISS
388
- * reads the binding through the handler ctx's use().
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).
389
392
  */
390
393
  function invokeLoader<T>(
391
394
  loader: RunnableLoader<T>,
@@ -395,6 +398,7 @@ function invokeLoader<T>(
395
398
  ): Promise<T> {
396
399
  const run = () => Promise.resolve(loaderFn(loaderCtx));
397
400
  if (!opts.cache) return run();
401
+ armLoaderTagSets();
398
402
  const handlerCtx = {
399
403
  params: loaderCtx.params,
400
404
  use: run,
@@ -1,195 +1,132 @@
1
1
  /**
2
- * runTransitionWhen — unit-test a transition({ when }) predicate in isolation.
2
+ * runTransitionWhen — unit-test a transition({ when }) browser predicate.
3
3
  *
4
- * Runs the SAME server functions the router uses — the PPR pre-handler evaluator
5
- * when `ppr: true`, applyViewTransitionDefault (which strips the server-only
6
- * function), and gateTransitions. So the
7
- * predicate sees exactly the navigation/action metadata it would at runtime
8
- * (currentUrl/currentParams/fromRouteName, nextUrl/nextParams/toRouteName,
9
- * actionId/actionUrl/actionResult/formData/method, get/env), and `kept` reflects
10
- * whether the transition would apply this request. The result also exposes the
11
- * assembled `whenContext` so tests can assert the exact fields without reaching
12
- * into private request-context state.
4
+ * Builds the context and evaluates the predicate through the SAME browser
5
+ * functions the router uses at navigation time (browser/transition-when.ts):
6
+ * a throw counts as false and is logged. `from`/`to` take a URL string or a
7
+ * partial RouteLocation; a `state` given as location-state entries
8
+ * (`[Def(value)]`, what Link/push take) is stored the way a push stores it,
9
+ * so `Def.read(ctx.to)` reads it back. Accepts the predicate or a whole
10
+ * TransitionConfig.
13
11
  *
14
- * This is the public way to exercise a transition gate: the full
15
- * match -> render pipeline that wires these together only runs under real RSC
16
- * rendering (which the Flight primitives do not drive), so without this primitive
17
- * a consumer could not test their predicate through @rangojs/router/testing.
18
- *
19
- * Synchronous: a transition predicate returns a boolean and the gate has no I/O.
12
+ * Synchronous: a transition predicate returns a boolean and has no I/O.
20
13
  */
21
14
 
22
15
  import {
23
- runWithRequestContext,
24
- type RequestContext,
25
- } from "../server/request-context.js";
26
- import { applyViewTransitionDefault } from "../router/segment-resolution/view-transition-default.js";
27
- import { gateTransitions } from "../rsc/transition-gate.js";
28
- import { createTestRequestContext, type VarsInit } from "./internal/context.js";
16
+ createTransitionWhenContext,
17
+ evaluateTransitionWhen,
18
+ type RouteLocationInput,
19
+ } from "../browser/transition-when.js";
20
+ import {
21
+ buildHistoryState,
22
+ resolveNavigationState,
23
+ } from "../browser/history-state.js";
24
+ import { isLocationStateEntry } from "../browser/react/location-state-shared.js";
25
+ import { resolveTestActionId } from "./internal/action-id.js";
29
26
  import type {
30
- ResolvedSegment,
27
+ RouteLocation,
31
28
  TransitionConfig,
32
29
  TransitionWhenContext,
30
+ TransitionWhenFn,
31
+ TransitionWhenKind,
33
32
  } from "../types/segments.js";
34
- import type { OnErrorCallback } from "../types/error-types.js";
35
- import type { EntryData } from "../server/context.js";
36
- import { evaluatePprTransitionWhen } from "../router/transition-when.js";
37
- import { invokeOnError } from "../router/error-handling.js";
38
- import { toURL } from "./to-url.js";
39
33
 
40
- /**
41
- * Options for runTransitionWhen. All navigation/action fields are optional and
42
- * default to "absent", matching what the gate sees for an initial full load with
43
- * no action: omit `currentUrl`/`currentParams`/`fromRouteName` to model the
44
- * navigation source being unavailable, and omit the `action*` fields to model a
45
- * plain (non-action) navigation.
46
- */
47
- export interface RunTransitionWhenOptions<TEnv = any> {
48
- /** The navigation TARGET request (drives `nextUrl`): a Request or URL/path string. Defaults to `http://localhost/`. */
49
- request?: Request | string;
50
- /** Route params for the target (`nextParams`). */
51
- params?: Record<string, string>;
52
- /** Target route name (`toRouteName`). */
53
- toRouteName?: string;
54
- /** Environment bindings surfaced as `env` (and `ctx.env`). */
55
- env?: TEnv;
56
- /** Variables readable via the predicate's `get()`. With `ppr`, these model pre-handler middleware/input state. */
57
- vars?: VarsInit;
58
- /** Navigation SOURCE url (`currentUrl`): a URL or path string. */
59
- currentUrl?: string | URL;
60
- /** Source route params (`currentParams`). */
61
- currentParams?: Record<string, string>;
62
- /** Source route name (`fromRouteName`). */
63
- fromRouteName?: string;
64
- /** Id of the action that triggered a revalidation (`actionId`). */
65
- actionId?: string;
66
- /** Url the action was submitted from (`actionUrl`). */
67
- actionUrl?: string | URL;
68
- /** The action's return value (`actionResult`). */
69
- actionResult?: unknown;
70
- /** FormData from a form action (`formData`). */
34
+ type ImportedAction = (...args: never[]) => unknown;
35
+
36
+ /** A navigation side: a URL, or a partial RouteLocation. */
37
+ export type RunTransitionWhenLocation =
38
+ | string
39
+ | URL
40
+ | (Partial<Omit<RouteLocation, "url">> & { url?: string | URL });
41
+
42
+ /** The action fields of a `kind: "action"` context. */
43
+ export interface RunTransitionWhenAction {
44
+ /** The triggering action: an imported server action or its actionId. */
45
+ ref?: ImportedAction | string;
71
46
  formData?: FormData;
72
- /** Receives an error thrown by the predicate (the gate reports to `router.onError`, phase `"rendering"`). */
73
- onError?: OnErrorCallback;
74
- /** Model a route with `ppr`, where the predicate runs before route handlers and cache lookup. */
75
- ppr?: boolean;
47
+ /** The action's return value on success. */
48
+ result?: unknown;
49
+ /** What a failed action threw (the error lane). */
50
+ error?: unknown;
76
51
  }
77
52
 
78
- /**
79
- * Result of runTransitionWhen.
80
- */
81
- export interface RunTransitionWhenResult<TEnv = any> {
82
- /** True if the transition would apply this request (predicate returned non-false, or there is no `when`). */
83
- kept: boolean;
84
- /** Convenience inverse of `kept`. */
85
- dropped: boolean;
53
+ export interface RunTransitionWhenOptions {
54
+ /** Defaults to `"action"` when `action` is given, `"push"` otherwise. */
55
+ kind?: TransitionWhenKind;
56
+ /** The committed location being left. Defaults to `http://localhost/`. */
57
+ from?: RunTransitionWhenLocation;
58
+ /** The destination. Defaults to `from`; always `from` for "action" and "revalidate". */
59
+ to?: RunTransitionWhenLocation;
86
60
  /**
87
- * The production-assembled predicate context. Undefined when the config has
88
- * no `when` predicate.
61
+ * The triggering action (like runClientRevalidate's): an imported server
62
+ * action (id via `$id ?? $$id`), an actionId string, or
63
+ * `{ ref, formData, result, error }`.
89
64
  */
90
- whenContext?: TransitionWhenContext<Record<string, string>, TEnv>;
91
- /** The underlying RequestContext, for additional assertions (`ctx.get(...)`, etc.). */
92
- ctx: RequestContext<TEnv>;
65
+ action?: ImportedAction | string | RunTransitionWhenAction;
93
66
  }
94
67
 
95
- export function runTransitionWhen<TEnv = any>(
96
- config: TransitionConfig,
97
- opts: RunTransitionWhenOptions<TEnv> = {},
98
- ): RunTransitionWhenResult<TEnv> {
99
- const { ctx } = createTestRequestContext<TEnv>({
100
- env: opts.env,
101
- request: opts.request,
102
- vars: opts.vars,
103
- params: opts.params,
104
- });
105
- const reqCtx = ctx as unknown as RequestContext<TEnv>;
68
+ export interface RunTransitionWhenResult {
69
+ /** True when the navigation holds (the predicate did not return false or throw, or there is none). */
70
+ applied: boolean;
71
+ /** Inverse of `applied`. */
72
+ gatedOff: boolean;
73
+ /** The context the predicate received. */
74
+ context: TransitionWhenContext;
75
+ }
106
76
 
107
- // Target route name (the public field the gate reads for `toRouteName`).
108
- if (opts.toRouteName !== undefined)
109
- reqCtx.routeName = opts.toRouteName as RequestContext<TEnv>["routeName"];
110
- // Source (match-time) data the gate reads for currentUrl/currentParams/fromRouteName.
111
- if (opts.currentUrl !== undefined)
112
- reqCtx._gateCurrentUrl = toURL(opts.currentUrl, reqCtx.url);
113
- if (opts.currentParams !== undefined)
114
- reqCtx._gateCurrentParams = opts.currentParams;
115
- if (opts.fromRouteName !== undefined)
116
- reqCtx._prevRouteKey = opts.fromRouteName;
117
- // Action data the gate reads at the action-bearing call sites.
118
- if (opts.actionId !== undefined) reqCtx._gateActionId = opts.actionId;
119
- if (opts.actionUrl !== undefined)
120
- reqCtx._gateActionUrl = toURL(opts.actionUrl, reqCtx.url);
121
- if (opts.actionResult !== undefined)
122
- reqCtx._gateActionResult = opts.actionResult;
123
- if (opts.formData !== undefined) reqCtx._gateFormData = opts.formData;
77
+ const DEFAULT_URL = "http://localhost/";
124
78
 
125
- let whenContext:
126
- | TransitionWhenContext<Record<string, string>, TEnv>
127
- | undefined;
128
- const when = config.when;
129
- const configForGate: TransitionConfig = when
130
- ? {
131
- ...config,
132
- when: (c) => {
133
- whenContext = c as TransitionWhenContext<
134
- Record<string, string>,
135
- TEnv
136
- >;
137
- return when(c);
138
- },
139
- }
140
- : config;
79
+ function toStoredState(state: unknown): unknown {
80
+ if (
81
+ Array.isArray(state) &&
82
+ state.length > 0 &&
83
+ isLocationStateEntry(state[0])
84
+ ) {
85
+ return buildHistoryState(resolveNavigationState(state));
86
+ }
87
+ return state;
88
+ }
89
+
90
+ function toLocation(
91
+ value: RunTransitionWhenLocation | undefined,
92
+ fallback: string | URL,
93
+ ): RouteLocationInput {
94
+ if (value === undefined) return { url: fallback };
95
+ if (typeof value === "string" || value instanceof URL) return { url: value };
96
+ return {
97
+ url: value.url ?? fallback,
98
+ params: value.params,
99
+ routeName: value.routeName,
100
+ state: toStoredState(value.state),
101
+ };
102
+ }
141
103
 
142
- return runWithRequestContext(reqCtx, () => {
143
- if (opts.ppr) {
144
- const entry = {
145
- type: "route",
146
- id: "tx-when-entry",
147
- shortCode: "tx-when-seg",
148
- parent: null,
149
- transition: configForGate,
150
- layout: [],
151
- parallel: {},
152
- } as unknown as EntryData;
153
- evaluatePprTransitionWhen(
154
- [entry],
155
- reqCtx,
156
- { params: reqCtx.params, routeName: opts.toRouteName },
157
- (error, segmentId) =>
158
- invokeOnError(
159
- opts.onError,
160
- error,
161
- "rendering",
162
- {
163
- request: reqCtx.request,
164
- url: reqCtx.url,
165
- params: reqCtx.params,
166
- segmentId,
167
- },
168
- "RSC",
169
- ),
170
- );
171
- }
172
- // Ordinary predicates are collected here and evaluated by the gate. PPR
173
- // predicates keep the decision already evaluated before this step.
174
- const serialized = applyViewTransitionDefault(
175
- configForGate,
176
- undefined,
177
- "tx-when-seg",
178
- );
179
- const segment = {
180
- id: "tx-when-seg",
181
- namespace: "r",
182
- type: "route",
183
- index: 0,
184
- component: null,
185
- transition: serialized,
186
- } as ResolvedSegment;
187
- const [gatedSegment] = gateTransitions(
188
- [segment],
189
- reqCtx as Parameters<typeof gateTransitions>[1],
190
- opts.onError,
191
- );
192
- const kept = gatedSegment.transition !== undefined;
193
- return { kept, dropped: !kept, whenContext, ctx: reqCtx };
104
+ export function runTransitionWhen(
105
+ whenOrConfig: TransitionWhenFn | TransitionConfig,
106
+ opts: RunTransitionWhenOptions = {},
107
+ ): RunTransitionWhenResult {
108
+ const actionOption =
109
+ typeof opts.action === "function" || typeof opts.action === "string"
110
+ ? { ref: opts.action }
111
+ : opts.action;
112
+ const from = toLocation(opts.from, DEFAULT_URL);
113
+ const context = createTransitionWhenContext({
114
+ kind: opts.kind ?? (actionOption ? "action" : "push"),
115
+ from,
116
+ to: opts.to === undefined ? undefined : toLocation(opts.to, from.url),
117
+ ...(actionOption
118
+ ? {
119
+ action: {
120
+ id: resolveTestActionId(actionOption.ref, "runTransitionWhen"),
121
+ formData: actionOption.formData,
122
+ result: actionOption.result,
123
+ error: actionOption.error,
124
+ },
125
+ }
126
+ : {}),
194
127
  });
128
+ const when =
129
+ typeof whenOrConfig === "function" ? whenOrConfig : whenOrConfig.when;
130
+ const applied = when ? evaluateTransitionWhen(when, context) : true;
131
+ return { applied, gatedOff: !applied, context };
195
132
  }