@rangojs/router 0.16.0 → 0.18.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 (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
package/src/ssr/index.tsx CHANGED
@@ -2,7 +2,11 @@ import React from "react";
2
2
  import { createSsrRootComponent, deserializeSsrPayload } from "./ssr-root.js";
3
3
  import { injectRSCPayloadEager } from "./inject-rsc-eager.js";
4
4
  import { runWithPreinitNonce } from "./preinit-client-references.js";
5
- import { SHELL_CAPTURE_MAX_WAIT_MS } from "../rsc/shell-capture-constants.js";
5
+ import {
6
+ POST_QUIESCE_TASK_HOPS,
7
+ SHELL_CAPTURE_MAX_WAIT_MS,
8
+ } from "../rsc/shell-capture-constants.js";
9
+ import { isThenable } from "../handles/is-thenable.js";
6
10
  import type { ErrorPhase } from "../types.js";
7
11
  import type { HeadScriptsOption } from "../vite/plugin-types.js";
8
12
 
@@ -27,6 +31,7 @@ interface RenderToReadableStreamOptions {
27
31
  nonce?: string;
28
32
  formState?: unknown;
29
33
  progressiveChunkSize?: number;
34
+ onError?: (error: unknown) => void;
30
35
  }
31
36
 
32
37
  /**
@@ -124,6 +129,13 @@ export interface SSRRenderOptions {
124
129
  * prerender pass (host-agnostic bare captures).
125
130
  */
126
131
  origin?: string;
132
+
133
+ /**
134
+ * Called for each error Fizz reports through its onError: a component that
135
+ * threw inside a Suspense boundary, which leaves the boundary errored in an
136
+ * otherwise completed document. React's default console.error is kept.
137
+ */
138
+ onError?: (error: unknown) => void;
127
139
  }
128
140
 
129
141
  /**
@@ -229,28 +241,64 @@ export interface SSRDependencies<TEnv = unknown> {
229
241
  }
230
242
 
231
243
  /**
232
- * Fixed number of macrotask hops between `quiesce` resolving and the abort. These
233
- * give React's fizz worker turns to flush the settled shell into the prelude and
234
- * mark still-pending boundaries as POSTPONED (rather than errored) before
235
- * controller.abort() lands. Not a wall-clock wait.
236
- *
237
- * Why 16 and not the original 2: under the REPLAY-ONLY capture model
238
- * (docs/design/ppr-shell-resume.md), the capture Flight render serializes ring-3
239
- * cached segments that are ALREADY serialized, so it emits the whole shell payload
240
- * in the first tick and the gate declares quiesce almost immediately (~a few ms).
241
- * On the old fresh-execution path the Flight dribbled out as handlers ran, so
242
- * Flight-quiet effectively meant "the shell has rendered" and 2 hops sufficed.
243
- *
244
- * Hops alone are NOT render-readiness: fizz cannot emit even <html> until the
245
- * payload root settles, which waits on every referenced client-module LOAD —
246
- * real module-runner I/O in dev (100ms+ cold), which no fixed count of near-
247
- * zero-cost task hops can buy. captureShellHTML therefore awaits the payload-
248
- * settled signal (SsrRootOptions.onPayloadSettled, deadline-bounded) between
249
- * quiesce and these hops; the hops then only flush the settled tree and mark
250
- * pending boundaries POSTPONED. Still task-based (masked loaders never emit,
251
- * so more hops never lets a hole settle). Bounded by maxWaitMs end to end.
244
+ * One set per running capture, collecting the client-reference module loads
245
+ * requested while it runs (captureClientLoads). The Flight client resolves a
246
+ * client component used as an element type lazily, so the payload settles
247
+ * while its module is still loading, and a shell that renders it outside any
248
+ * Suspense boundary cannot complete until the load does. On the
249
+ * cloudflare-basic build a cold load (the module, its imports, its CSS
250
+ * virtual) settled at +890.1ms, after the payload (+873.4ms) and 1ms before
251
+ * the abort, and uninstrumented builds lost that race (issue #949). The
252
+ * Flight client asks the loader again for every import row, even for a
253
+ * module another render is already loading, so a capture sees each load it
254
+ * needs; one a concurrent render requests meanwhile is collected too and is
255
+ * the same bounded I/O. The sets are per capture, their contents isolate-wide:
256
+ * a load that never settles can only hold the captures running when it was
257
+ * requested, each until its deadline.
258
+ */
259
+ const activeCaptureClientLoads = new Set<Set<PromiseLike<unknown>>>();
260
+
261
+ /** The loader wrapper {@link captureClientLoads} last installed. */
262
+ let recordingClientReferenceLoader: ((id: string) => unknown) | undefined;
263
+
264
+ /**
265
+ * Start collecting the client-reference loads requested during a capture;
266
+ * call the returned `stop` when the capture ends. Wraps plugin-rsc's SSR
267
+ * loader (`globalThis.__vite_rsc_client_require__`, which
268
+ * `__vite_rsc_require__` reads per call) on the first capture, so an isolate
269
+ * that never captures keeps the bare loader. Wraps again when another wrapper
270
+ * replaced this one (the build shell phase bridges hashed ids the same way,
271
+ * vite/discovery/shell-prerender-phase.ts). Collects nothing without the
272
+ * loader.
252
273
  */
253
- const POST_QUIESCE_TASK_HOPS = 16;
274
+ function captureClientLoads(): {
275
+ loads: Set<PromiseLike<unknown>>;
276
+ stop: () => void;
277
+ } {
278
+ const g = globalThis as {
279
+ __vite_rsc_client_require__?: (id: string) => unknown;
280
+ };
281
+ const load = g.__vite_rsc_client_require__;
282
+ if (typeof load === "function" && load !== recordingClientReferenceLoader) {
283
+ recordingClientReferenceLoader = (id) => {
284
+ const loading = load(id);
285
+ // plugin-rsc memoizes one promise per id and the Flight client stamps
286
+ // status "fulfilled" on it once loaded: a warm module is not collected.
287
+ if (
288
+ activeCaptureClientLoads.size > 0 &&
289
+ isThenable(loading) &&
290
+ (loading as { status?: string }).status !== "fulfilled"
291
+ ) {
292
+ for (const loads of activeCaptureClientLoads) loads.add(loading);
293
+ }
294
+ return loading;
295
+ };
296
+ g.__vite_rsc_client_require__ = recordingClientReferenceLoader;
297
+ }
298
+ const loads = new Set<PromiseLike<unknown>>();
299
+ activeCaptureClientLoads.add(loads);
300
+ return { loads, stop: () => activeCaptureClientLoads.delete(loads) };
301
+ }
254
302
 
255
303
  /**
256
304
  * Route an SSR error through the deps.onError notification callback with the
@@ -407,6 +455,19 @@ interface ShellCaptureOptions {
407
455
  * capture, resume, and browser hydration.
408
456
  */
409
457
  origin?: string;
458
+ /**
459
+ * Called for each component error the prerender reports (an errored
460
+ * boundary in the prelude), never for the capture's own abort. The caller
461
+ * refuses to store the shell (issue #915).
462
+ */
463
+ onError?: (error: unknown) => void;
464
+ /**
465
+ * Called with React's errorInfo for each task still pending when the
466
+ * capture's own abort froze the prelude; its componentStack is computed
467
+ * when read. The dev no-shell warning prints the stacks: one of them is what
468
+ * suspended above <body> (issue #941).
469
+ */
470
+ onAbortedTask?: (errorInfo: { componentStack?: string } | undefined) => void;
410
471
  }
411
472
 
412
473
  /**
@@ -437,6 +498,8 @@ interface ShellResumeOptions {
437
498
  search?: string;
438
499
  /** The HIT request's origin — same host as the capture's (key-scoped). */
439
500
  origin?: string;
501
+ /** Called for each component error the resumed holes report. */
502
+ onError?: (error: unknown) => void;
440
503
  }
441
504
 
442
505
  /**
@@ -601,7 +664,14 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
601
664
  rscStream: ReadableStream<Uint8Array>,
602
665
  options?: SSRRenderOptions,
603
666
  ): Promise<ReadableStream<Uint8Array>> {
604
- const { nonce, formState, streamMode, search, origin } = options ?? {};
667
+ const {
668
+ nonce,
669
+ formState,
670
+ streamMode,
671
+ search,
672
+ origin,
673
+ onError: onRenderError,
674
+ } = options ?? {};
605
675
 
606
676
  try {
607
677
  // Tee the stream:
@@ -659,6 +729,13 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
659
729
  formState,
660
730
  nonce,
661
731
  ...(progressiveChunkSize !== undefined && { progressiveChunkSize }),
732
+ ...(onRenderError && {
733
+ onError: (error: unknown) => {
734
+ onRenderError(error);
735
+ // What React's default onError does (it only logs).
736
+ console.error(error);
737
+ },
738
+ }),
662
739
  }),
663
740
  );
664
741
 
@@ -731,12 +808,13 @@ export function createShellCaptureHandler<TEnv = unknown>(
731
808
  const deadline = (await isDebuggerAttached())
732
809
  ? { promise: new Promise<void>(() => {}), cancel: () => {} }
733
810
  : createCancelableTimeout(maxWaitMs);
811
+ const clientLoads = captureClientLoads();
734
812
  try {
735
813
  // No nonce (nonce'd requests never reach capture); no formState.
736
- // payloadSettled: fires when the Flight payload root settles — i.e.
737
- // every client-module load the payload references completed and fizz
738
- // can actually emit the tree. The abort below gates on it (bounded by
739
- // the same deadline): Flight byte-quiet alone is NOT render-readiness.
814
+ // payloadSettled: fires when the Flight payload root settles and fizz
815
+ // can emit the tree. The abort below gates on it and on the
816
+ // client-reference loads in flight in the isolate (bounded by the same
817
+ // deadline): Flight byte-quiet alone is NOT render-readiness.
740
818
  let settlePayload!: () => void;
741
819
  const payloadSettled = new Promise<void>((resolve) => {
742
820
  settlePayload = resolve;
@@ -793,10 +871,12 @@ export function createShellCaptureHandler<TEnv = unknown>(
793
871
  // render errors are NOT our sentinel and still surface through
794
872
  // deps.onError, the same channel renderHTML uses. See
795
873
  // docs/design/ppr-shell-resume.md.
796
- onError: (error: unknown) => {
874
+ onError: (error: unknown, errorInfo?: { componentStack?: string }) => {
797
875
  if (error === abortReason) {
876
+ opts.onAbortedTask?.(errorInfo);
798
877
  return;
799
878
  }
879
+ opts.onError?.(error);
800
880
  reportRenderError(onError, error);
801
881
  },
802
882
  });
@@ -815,25 +895,34 @@ export function createShellCaptureHandler<TEnv = unknown>(
815
895
  // shell that never goes quiet (a root postpone / hung handle).
816
896
  await Promise.race([opts.quiesce, deadline.promise]);
817
897
  // Then wait for fizz RENDER-READINESS, bounded by the same deadline:
818
- // the payload root settles only after every client-module load the
819
- // payload references completed (real module-runner I/O in dev; 100ms+
820
- // on a cold graph). Flight byte-quiet does NOT imply this — a fully
898
+ // the payload root settles, then the client-reference module loads the
899
+ // capture started settle — a client component used as an element type
900
+ // resolves lazily, after the payload (real module-runner I/O in dev;
901
+ // 100ms+ on a cold graph). Flight byte-quiet does NOT imply this — a fully
821
902
  // REPLAYED (prerendered) route's Flight stream finishes in ~1-3ms and
822
903
  // an abort taken on quiet-plus-task-hops alone landed BEFORE fizz could
823
904
  // emit <html>, freezing a zero-byte prelude: the eternal-MISS shape
824
905
  // this route class showed on every cold graph (dev cold boots, GH
825
906
  // runners) while ordinary routes — whose live handler execution keeps
826
- // Flight noisy long enough — never hit it. Masked-loader holes do not
827
- // block payload settlement (they postpone below the root), so this
828
- // await costs a genuinely hole-y shell nothing; a payload that NEVER
829
- // settles (hung handles) degrades at the deadline exactly as before.
830
- // A prerender that SETTLES first (early success or a hard rejection)
831
- // ends the wait immediately — fizz is already done either way.
907
+ // Flight noisy long enough — never hit it. Holes the payload carries
908
+ // block neither wait (masked live loaders and loader-nested promises postpone
909
+ // below the root and load no module; the Flight input stays frozen),
910
+ // so this await costs a genuinely hole-y shell nothing; a payload that
911
+ // NEVER settles (hung handles) degrades at the deadline exactly as
912
+ // before. A prerender that SETTLES first (early success or a hard
913
+ // rejection) ends the wait immediately — fizz is already done either
914
+ // way.
832
915
  const prerenderSettled = prerenderPromise.then(
833
916
  () => {},
834
917
  () => {},
835
918
  );
836
- await Promise.race([payloadSettled, prerenderSettled, deadline.promise]);
919
+ // The capture's loads all started when their import rows were parsed,
920
+ // before the Flight input froze.
921
+ await Promise.race([
922
+ payloadSettled.then(() => Promise.allSettled(clientLoads.loads)),
923
+ prerenderSettled,
924
+ deadline.promise,
925
+ ]);
837
926
  // Fixed task hops before the abort: give React's fizz worker turns to flush
838
927
  // the now-complete shell and mark the still-pending boundaries as POSTPONED
839
928
  // rather than errored. Deterministic (the byte set is already frozen and
@@ -877,11 +966,10 @@ export function createShellCaptureHandler<TEnv = unknown>(
877
966
 
878
967
  // Sanity gate: a prelude with no `<body` is the no-shell failure mode.
879
968
  // Return null and store nothing; the request falls back to axis 1 and a
880
- // later request re-captures. The dominant real-world cause is a loader
881
- // route WITHOUT a route-level loading() boundary: renderSegments' loading-
882
- // less branch awaits loader data at TREE-BUILD, so the masked loader pins
883
- // the whole tree above <body> (root postpone). Root-postponing layouts and
884
- // hung handles degrade the same way. shell-capture.ts logs a once-per-key
969
+ // later request re-captures. The dominant real-world cause is a masked
970
+ // live-loader read with no boundary above it (lane rule: see
971
+ // resolveLoaderData, loader-cache.ts), which root-postpones above
972
+ // <body>. Root-postponing layouts and hung handles degrade the same way. shell-capture.ts logs a once-per-key
885
973
  // warning so the eternal-MISS shape is diagnosable.
886
974
  if (!new TextDecoder().decode(prelude).includes("<body")) {
887
975
  return null;
@@ -893,6 +981,7 @@ export function createShellCaptureHandler<TEnv = unknown>(
893
981
  };
894
982
  } finally {
895
983
  deadline.cancel();
984
+ clientLoads.stop();
896
985
  }
897
986
  };
898
987
  }
@@ -982,7 +1071,10 @@ export function createShellResumeHandler<TEnv = unknown>(
982
1071
  // resumed stream and need the per-request nonce.
983
1072
  const resumed = await runWithPreinitNonce(nonce, () =>
984
1073
  resume(<SsrRoot />, JSON.parse(postponed), {
985
- onError: (error) => reportRenderError(onError, error),
1074
+ onError: (error) => {
1075
+ opts.onError?.(error);
1076
+ reportRenderError(onError, error);
1077
+ },
986
1078
  nonce,
987
1079
  }),
988
1080
  );
@@ -189,14 +189,15 @@ export interface SsrRootOptions {
189
189
  nonce?: string;
190
190
  /**
191
191
  * Fires once when the Flight payload root settles (resolve OR reject) — the
192
- * signal that every client-module load the payload references completed and
193
- * fizz can start emitting the tree. The capture pass gates its abort on
194
- * this: a fully REPLAYED (prerendered) route's Flight stream goes
195
- * byte-quiet in ~1-3ms, but fizz cannot render even <html> until the
196
- * module loads finish (real module-runner I/O in dev; 100ms+ on a cold
197
- * graph), so an abort gated on Flight quiet alone fires first and freezes
198
- * a zero-byte prelude. Masked-loader holes do NOT block this signal —
199
- * they postpone below the root.
192
+ * signal that fizz can start emitting the tree. A client component used as
193
+ * an element type resolves lazily, so its module load can still be in
194
+ * flight (captureShellHTML waits for those separately, issue #949). The
195
+ * capture pass gates its abort on this: a fully REPLAYED (prerendered)
196
+ * route's Flight stream goes byte-quiet in ~1-3ms, but fizz cannot render
197
+ * even <html> until the module loads finish (real module-runner I/O in dev;
198
+ * 100ms+ on a cold graph), so an abort gated on Flight quiet alone fires
199
+ * first and freezes a zero-byte prelude. Masked-loader holes do NOT block
200
+ * this signal — they postpone below the root.
200
201
  */
201
202
  onPayloadSettled?: () => void;
202
203
  /**
@@ -21,11 +21,14 @@
21
21
  * ```ts
22
22
  * export const DocsNav = Static((ctx) => <Nav docs={readDocsSync()} />);
23
23
  * export const DocShell = Static((ctx) => <Shell />);
24
+ * // Prerender/Static must be exported consts; an inline call inside path()
25
+ * // gets no build-injected $$id and throws.
26
+ * export const DocPage = Prerender(getParams, DocPageHandler);
24
27
  *
25
28
  * urls(({ path, layout }) => [
26
29
  * layout(DocsNav, () => [
27
30
  * path("/getting-started", DocShell, { name: "doc.gs" }),
28
- * path("/:slug", Prerender(getParams, DocPageHandler)),
31
+ * path("/:slug", DocPage),
29
32
  * ]),
30
33
  * ]);
31
34
  * ```
@@ -19,6 +19,9 @@ declare module "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge" {
19
19
  options?: { onError?: (error: unknown) => string | void },
20
20
  ): ReadableStream<Uint8Array>;
21
21
 
22
+ /** Server-side temporary-reference set (a WeakMap). */
23
+ export function createTemporaryReferenceSet(): unknown;
24
+
22
25
  /**
23
26
  * Tag a value as a client reference. Mutates `impl` in place (defining
24
27
  * `$$typeof`/`$$id`/`$$async`) and returns it, so a server tree that imports
@@ -55,3 +58,21 @@ declare module "@vitejs/plugin-rsc/core/browser" {
55
58
  load: (id: string) => unknown;
56
59
  }): void;
57
60
  }
61
+
62
+ /**
63
+ * The vendored react-server-dom CLIENT build the RSC environment decodes with
64
+ * (plugin-rsc `react/rsc/client`). The plugin-rsc stub runs the "use cache"
65
+ * codec on it under the react-server condition.
66
+ */
67
+ declare module "@vitejs/plugin-rsc/vendor/react-server-dom/client.edge" {
68
+ export function createFromReadableStream<T = unknown>(
69
+ stream: ReadableStream<Uint8Array>,
70
+ options?: object,
71
+ ): Promise<T>;
72
+ export function encodeReply(
73
+ value: unknown,
74
+ options?: object,
75
+ ): Promise<string | FormData>;
76
+ /** Client-side temporary-reference set (a Map). */
77
+ export function createTemporaryReferenceSet(): unknown;
78
+ }
@@ -18,6 +18,11 @@
18
18
  * element tree, so you can assert typed prop fidelity across the client boundary
19
19
  * (a `Date` comes back a `Date`) and detect inlined-vs-island. Serialize +
20
20
  * deserialize only — no hydration/interaction (that is the e2e tier).
21
+ *
22
+ * `serveShellRequest` serves one request through the router's production
23
+ * handler, so a `ppr` route's MISS runs the real shell capture and a later
24
+ * request is a real HIT; only the HTML step is stubbed. `resetShellTestState`
25
+ * clears the per-isolate PPR state between tests.
21
26
  */
22
27
 
23
28
  export {
@@ -50,3 +55,12 @@ export type {
50
55
  RenderHandlerResult,
51
56
  StateCookieSeed,
52
57
  } from "./render-handler.js";
58
+
59
+ export {
60
+ serveShellRequest,
61
+ resetShellTestState,
62
+ } from "./serve-shell-request.js";
63
+ export type {
64
+ ServeShellRequestOptions,
65
+ ServeShellRequestResult,
66
+ } from "./serve-shell-request.js";
@@ -158,10 +158,10 @@ export async function diffGeneratedRoutes(
158
158
  *
159
159
  * @example
160
160
  * ```ts
161
- * import generated from "./router.named-routes.gen";
161
+ * import { NamedRoutes } from "./router.named-routes.gen";
162
162
  * import { router } from "./router";
163
163
  *
164
- * await assertGeneratedRoutesMatch(router, generated);
164
+ * await assertGeneratedRoutesMatch(router, NamedRoutes);
165
165
  * ```
166
166
  */
167
167
  export async function assertGeneratedRoutesMatch(
@@ -347,7 +347,7 @@ export function buildRunSnapshot<TEnv>(
347
347
  *
348
348
  * @example
349
349
  * ```ts
350
- * const { result, cookies, response, thrown } = await runInRequestContext(
350
+ * const { cookies, headers, response, thrown } = await runInRequestContext(
351
351
  * () => loginAction(input), // sets a session cookie, then `throw redirect("/app")`
352
352
  * {
353
353
  * env,
@@ -7,9 +7,11 @@
7
7
  * (@vitejs/plugin-rsc `core/plugin.js`). That transform does NOT run in a bare
8
8
  * Vitest process, so the vendored client's free `__webpack_require__` /
9
9
  * `__webpack_chunk_load__` references would be undefined. We provide minimal
10
- * shims: `__webpack_require__` routes to the loader installed via
11
- * `setRequireModule`, and `__webpack_chunk_load__` is a no-op (renderServerTree
12
- * serializes with empty `chunks`, so no chunk fetch ever happens).
10
+ * shims: `__webpack_require__` forwards to `__vite_rsc_require__` (the build's
11
+ * rename), which plugin-rsc's `setRequireModule` installs (core/browser from
12
+ * renderServerTree, core/rsc from the plugin-rsc stub), and
13
+ * `__webpack_chunk_load__` is a no-op (the test manifests use empty `chunks`,
14
+ * so no chunk fetch ever happens).
13
15
  *
14
16
  * MUST be imported (for side effect) BEFORE `@vitejs/plugin-rsc/react/browser`,
15
17
  * which is why flight-tree.ts lists it first.
@@ -17,11 +19,11 @@
17
19
  const g = globalThis as unknown as {
18
20
  __webpack_require__?: ((id: string) => unknown) & { u?: unknown };
19
21
  __webpack_chunk_load__?: (chunkId: string) => Promise<unknown>;
20
- __vite_rsc_client_require__?: (id: string) => unknown;
22
+ __vite_rsc_require__?: (id: string) => unknown;
21
23
  };
22
24
 
23
25
  if (!g.__webpack_require__) {
24
- g.__webpack_require__ = (id: string) => g.__vite_rsc_client_require__!(id);
26
+ g.__webpack_require__ = (id: string) => g.__vite_rsc_require__!(id);
25
27
  }
26
28
  if (!g.__webpack_chunk_load__) {
27
29
  g.__webpack_chunk_load__ = async () => {};
@@ -23,10 +23,13 @@
23
23
  * SEEDED directly into client context (see the `loaders` / `locationState` /
24
24
  * `handles` / `outletPending` options) — nothing is executed on the server.
25
25
  * This exercises the context read path, not the run path.
26
- * - navigate() commits synchronously, so it does NOT drive the navigation
27
- * lifecycle: useNavigation().state, useLinkStatus().pending, and
28
- * useAction().state stay "idle". Assert pending/loading/submitting transition
29
- * states with renderServerTree / e2e instead (navigate() warns once if used).
26
+ * - navigate() does NOT drive the navigation lifecycle: useNavigation().state,
27
+ * useLinkStatus().pending, and useAction().state stay "idle". Assert
28
+ * pending/loading/submitting transition states with renderServerTree / e2e
29
+ * instead (navigate() warns once if used). The one pending state it does
30
+ * model is the held-navigation useLoader().isLoading: a `transition` spec
31
+ * plus a pending Promise seeded via navigate(url, { loaders }) commits
32
+ * through production's commitInTransition (browser/partial-update.ts).
30
33
  * What it DOES cover: client hooks that read NavigationProvider /
31
34
  * OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
32
35
  * useRouter, usePathname, useSearchParams, Outlet/useOutlet nesting and seeded
@@ -48,7 +51,15 @@ import {
48
51
  } from "../browser/navigation-store.js";
49
52
  import { createEventController } from "../browser/event-controller.js";
50
53
  import { resolveDeferredHandleValues } from "../handles/deferred-resolution.js";
51
- import type { NavigationStore, NavigationBridge } from "../browser/types.js";
54
+ import {
55
+ commitInTransition,
56
+ shouldStartViewTransition,
57
+ } from "../browser/partial-update.js";
58
+ import type {
59
+ NavigationStore,
60
+ NavigationBridge,
61
+ UpdateSubscriber,
62
+ } from "../browser/types.js";
52
63
  import type { EventController } from "../browser/event-controller.js";
53
64
  import type { ResolvedSegment, RscMetadata } from "../browser/types.js";
54
65
  import { NavigationProvider } from "../browser/react/NavigationProvider.js";
@@ -57,7 +68,7 @@ import {
57
68
  buildParamsFromMatch,
58
69
  } from "../router/pattern-matching.js";
59
70
  import { normalizeBasename } from "../router/basename.js";
60
- import type { LoaderDefinition } from "../types.js";
71
+ import type { LoaderDefinition, TransitionConfig } from "../types.js";
61
72
  import type { LocationStateDefinition } from "../browser/react/location-state-shared.js";
62
73
  import type { Handle } from "../handle.js";
63
74
  import type { ThemeConfig } from "../theme/types.js";
@@ -130,6 +141,14 @@ export interface RenderRouteSpec {
130
141
  loaderIds?: string[];
131
142
  /** Optional route name (informational; not used for matching). */
132
143
  name?: string;
144
+ /**
145
+ * The `transition()` config this node declares (`{}` for a bare
146
+ * `transition()`), attached to its segment as the DSL does. Puts the chain in
147
+ * a transition scope: param-agnostic keys (segment-system.tsx) and a held
148
+ * navigate() commit, so a pending loader seed passed to
149
+ * `router.navigate(url, { loaders })` pins `useLoader().isLoading`.
150
+ */
151
+ transition?: TransitionConfig;
133
152
  }
134
153
 
135
154
  /**
@@ -303,8 +322,26 @@ export interface TestRouterHandle {
303
322
  * updates params + location, and re-renders the segment tree. This is a
304
323
  * client-only navigation: no server fetch occurs, so only the components in
305
324
  * `routes` can be reached.
325
+ *
326
+ * `options.loaders` seeds loader data for THIS navigation, merged over the
327
+ * render-time seeds (the data a real navigation's response would carry). A
328
+ * pending Promise suspends the read like a streaming Flight chunk. When a
329
+ * spec in the chain has `transition`, the commit goes through production's
330
+ * commitInTransition: the reader stays on screen with
331
+ * `useLoader().isLoading === true` until the promise settles. Settle it
332
+ * inside RTL's `act()` to flush the commit. Without `transition` the commit
333
+ * is urgent and the read suspends to the nearest Suspense boundary.
334
+ *
335
+ * @example
336
+ * await router.navigate("/products/2", { loaders: [[ProductLoader, next]] });
337
+ * // reader: isLoading true, old data
338
+ * await act(async () => resolve({ name: "Product 2" }));
339
+ * // reader: isLoading false, new data
306
340
  */
307
- navigate(url: string): Promise<void>;
341
+ navigate(
342
+ url: string,
343
+ options?: Pick<RenderRouteOptions, "loaders">,
344
+ ): Promise<void>;
308
345
  /** The current committed pathname. */
309
346
  pathname(): string;
310
347
  /** The current committed params. */
@@ -413,6 +450,7 @@ function buildSegments(
413
450
  belongsToRoute: true,
414
451
  };
415
452
  if (mount) node.mountPath = mount;
453
+ if (spec.transition) node.transition = spec.transition;
416
454
  if (isLeaf && spec.layout) {
417
455
  const Layout = spec.layout;
418
456
  node.layout = <Layout />;
@@ -464,10 +502,17 @@ export async function renderRoute(
464
502
  const initialUrl = requestUrl ?? staticPrefix(leaf.path) ?? "/";
465
503
  const url = new URL(initialUrl, TEST_ORIGIN);
466
504
 
467
- const loaderData: Record<string, unknown> = { ...(options.loaderData ?? {}) };
468
- for (const [loader, data] of options.loaders ?? []) {
469
- loaderData[ensureSyntheticId(loader as object, "$$id")] = data;
470
- }
505
+ const seedLoaders = (
506
+ base: Record<string, unknown>,
507
+ loaders: RenderRouteOptions["loaders"],
508
+ ): Record<string, unknown> => {
509
+ const out = { ...base };
510
+ for (const [loader, data] of loaders ?? []) {
511
+ out[ensureSyntheticId(loader as object, "$$id")] = data;
512
+ }
513
+ return out;
514
+ };
515
+ const loaderData = seedLoaders(options.loaderData ?? {}, options.loaders);
471
516
 
472
517
  if (typeof window !== "undefined") {
473
518
  const stateObj: Record<string, unknown> = {};
@@ -550,25 +595,33 @@ export async function renderRoute(
550
595
  );
551
596
 
552
597
  let warnedNavLifecycle = false;
553
- const navigate = async (target: string): Promise<void> => {
554
- // renderRoute commits navigations synchronously (no server fetch, no Flight
555
- // stream), so it never drives the navigation lifecycle. The transition state
556
- // useNavigation()/useLinkStatus()/useAction() read stays "idle" — asserting a
557
- // pending/loading/submitting state here proves nothing. Warn once (per render)
558
- // under the test runner so that false-confidence trap is loud, not silent.
598
+ const navigate = async (
599
+ target: string,
600
+ navOptions?: Pick<RenderRouteOptions, "loaders">,
601
+ ): Promise<void> => {
602
+ // No server fetch, so the navigation lifecycle never starts: the state
603
+ // useNavigation()/useLinkStatus()/useAction() read stays "idle" — asserting
604
+ // a pending/loading/submitting state here proves nothing. Warn once (per
605
+ // render) under the test runner so that false-confidence trap is loud.
559
606
  if (isUnderTestRunner() && !warnedNavLifecycle) {
560
607
  warnedNavLifecycle = true;
561
608
  console.warn(
562
- "renderRoute: navigate()/useRouter().push commit synchronously and do " +
563
- "NOT drive the navigation lifecycle. useNavigation().state, " +
564
- 'useLinkStatus().pending, and useAction().state stay "idle" here. ' +
565
- "Assert params/pathname/content after navigate(); use renderServerTree " +
566
- "or e2e to assert pending/loading/submitting transition states.",
609
+ "renderRoute: navigate()/useRouter().push do NOT drive the navigation " +
610
+ "lifecycle. useNavigation().state, useLinkStatus().pending, and " +
611
+ 'useAction().state stay "idle" here. Assert params/pathname/content ' +
612
+ "after navigate(); use renderServerTree or e2e to assert " +
613
+ "pending/loading/submitting transition states. A held " +
614
+ "useLoader().isLoading is modeled: see navigate(url, { loaders }).",
567
615
  );
568
616
  }
569
617
  const nextUrl = new URL(target, TEST_ORIGIN);
570
618
  const match = resolve(nextUrl.pathname);
571
- const segments = buildSegments(routes, match.params, loaderData, mount);
619
+ const segments = buildSegments(
620
+ routes,
621
+ match.params,
622
+ seedLoaders(loaderData, navOptions?.loaders),
623
+ mount,
624
+ );
572
625
  const metadata = makeMetadata(nextUrl.pathname, segments, match.params);
573
626
  const root = await renderSegments(segments, {
574
627
  outletPending: options.outletPending,
@@ -577,8 +630,16 @@ export async function renderRoute(
577
630
  eventController.setParams(match.params);
578
631
  store.setCurrentUrl(nextUrl.href);
579
632
  store.setSegmentIds(segments.map((s) => s.id));
633
+ const emit: UpdateSubscriber = (update) => store.emitUpdate(update);
580
634
  await act(async () => {
581
- store.emitUpdate({ root, metadata });
635
+ // Production's transition() lane (browser/partial-update.ts). Its
636
+ // same-structure / fully-prefetched / optimistic hold lanes are not
637
+ // modeled: without transition() the commit stays urgent.
638
+ if (shouldStartViewTransition(segments)) {
639
+ commitInTransition(emit, segments, { root, metadata }, ["navigation"]);
640
+ } else {
641
+ emit({ root, metadata });
642
+ }
582
643
  });
583
644
  };
584
645