@rangojs/router 0.16.0 → 0.17.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 (245) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +7 -4
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +1 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/types/cache-types.d.ts +14 -10
  67. package/dist/types/types/handler-context.d.ts +14 -0
  68. package/dist/types/types/loader-types.d.ts +8 -6
  69. package/dist/types/urls/response-types.d.ts +3 -12
  70. package/dist/types/use-loader.d.ts +12 -3
  71. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  72. package/dist/types/vite/rango.d.ts +1 -1
  73. package/dist/vite/index.js +1381 -1346
  74. package/package.json +2 -2
  75. package/skills/api-client/SKILL.md +22 -10
  76. package/skills/breadcrumbs/SKILL.md +65 -22
  77. package/skills/bundle-analysis/SKILL.md +22 -22
  78. package/skills/cache-guide/SKILL.md +207 -97
  79. package/skills/caching/SKILL.md +295 -93
  80. package/skills/catalog.json +16 -16
  81. package/skills/client-urls/SKILL.md +53 -31
  82. package/skills/cloudflare/SKILL.md +72 -20
  83. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  84. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  85. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  86. package/skills/comparison/SKILL.md +5 -0
  87. package/skills/comparison/references/framework-comparison.md +39 -33
  88. package/skills/composability/SKILL.md +38 -29
  89. package/skills/css/SKILL.md +10 -13
  90. package/skills/debug-manifest/SKILL.md +74 -31
  91. package/skills/defer-hydration/SKILL.md +2 -1
  92. package/skills/deployment-caching/SKILL.md +6 -1
  93. package/skills/document-cache/SKILL.md +92 -28
  94. package/skills/fonts/SKILL.md +16 -8
  95. package/skills/handler-use/SKILL.md +53 -17
  96. package/skills/hooks/SKILL.md +28 -20
  97. package/skills/hooks/data.md +23 -3
  98. package/skills/hooks/handle-and-actions.md +31 -16
  99. package/skills/hooks/navigation.md +39 -10
  100. package/skills/hooks/outlets.md +13 -6
  101. package/skills/hooks/state.md +37 -15
  102. package/skills/hooks/urls.md +7 -5
  103. package/skills/host-router/SKILL.md +64 -12
  104. package/skills/i18n/SKILL.md +60 -34
  105. package/skills/intercept/SKILL.md +161 -26
  106. package/skills/layout/SKILL.md +106 -50
  107. package/skills/links/SKILL.md +51 -19
  108. package/skills/loader/SKILL.md +242 -106
  109. package/skills/middleware/SKILL.md +119 -23
  110. package/skills/migrate-nextjs/SKILL.md +100 -45
  111. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  112. package/skills/migrate-react-router/SKILL.md +49 -38
  113. package/skills/migrate-react-router/component-migration.md +25 -6
  114. package/skills/migrate-react-router/data-and-actions.md +18 -5
  115. package/skills/migrate-react-router/route-mapping.md +12 -6
  116. package/skills/mime-routes/SKILL.md +12 -7
  117. package/skills/observability/SKILL.md +28 -14
  118. package/skills/parallel/SKILL.md +75 -39
  119. package/skills/ppr/SKILL.md +345 -150
  120. package/skills/prerender/SKILL.md +189 -100
  121. package/skills/rango/SKILL.md +65 -34
  122. package/skills/react-compiler/SKILL.md +6 -5
  123. package/skills/response-routes/SKILL.md +133 -25
  124. package/skills/route/SKILL.md +165 -60
  125. package/skills/router-setup/SKILL.md +418 -240
  126. package/skills/scripts/SKILL.md +31 -20
  127. package/skills/server-actions/SKILL.md +220 -59
  128. package/skills/shell-manifest/SKILL.md +12 -7
  129. package/skills/streams-and-websockets/SKILL.md +32 -14
  130. package/skills/tailwind/SKILL.md +37 -33
  131. package/skills/testing/SKILL.md +68 -32
  132. package/skills/testing/cache-prerender.md +55 -29
  133. package/skills/testing/client-components.md +59 -6
  134. package/skills/testing/e2e-parity.md +12 -2
  135. package/skills/testing/flight.md +19 -11
  136. package/skills/testing/handles.md +8 -5
  137. package/skills/testing/loader.md +8 -5
  138. package/skills/testing/middleware.md +8 -3
  139. package/skills/testing/predicates.md +120 -0
  140. package/skills/testing/render-handler.md +70 -22
  141. package/skills/testing/response-routes.md +11 -4
  142. package/skills/testing/reverse-and-types.md +20 -14
  143. package/skills/testing/server-actions.md +12 -3
  144. package/skills/testing/server-tree.md +9 -0
  145. package/skills/testing/setup.md +9 -5
  146. package/skills/theme/SKILL.md +106 -29
  147. package/skills/typesafety/SKILL.md +20 -9
  148. package/skills/typesafety/env-and-bindings.md +126 -47
  149. package/skills/typesafety/generated-files-and-cli.md +26 -14
  150. package/skills/typesafety/params-and-search.md +44 -19
  151. package/skills/typesafety/route-types.md +50 -14
  152. package/skills/use-cache/SKILL.md +200 -82
  153. package/skills/vercel/SKILL.md +21 -11
  154. package/skills/view-transitions/SKILL.md +41 -20
  155. package/src/__augment-tests__/augmented.check.ts +26 -1
  156. package/src/browser/index.ts +2 -2
  157. package/src/browser/navigation-bridge.ts +3 -4
  158. package/src/browser/navigation-transaction.ts +22 -2
  159. package/src/browser/partial-update.ts +35 -25
  160. package/src/browser/react/ScrollRestoration.tsx +6 -6
  161. package/src/browser/rsc-router.tsx +1 -1
  162. package/src/browser/scroll-restoration.ts +53 -6
  163. package/src/cache/cache-exec-scope.ts +37 -4
  164. package/src/cache/cache-key-utils.ts +1 -6
  165. package/src/cache/cache-runtime.ts +230 -61
  166. package/src/cache/cache-scope.ts +76 -12
  167. package/src/cache/document-cache.ts +15 -0
  168. package/src/cache/handle-capture.ts +90 -10
  169. package/src/cache/handle-snapshot.ts +132 -28
  170. package/src/cache/segment-codec.ts +26 -7
  171. package/src/cache/tag-invalidation.ts +4 -3
  172. package/src/cache/taint.ts +1 -1
  173. package/src/cache/types.ts +16 -3
  174. package/src/client.rsc.tsx +10 -9
  175. package/src/client.tsx +5 -4
  176. package/src/components/DefaultDocument.tsx +7 -2
  177. package/src/context-var.ts +6 -5
  178. package/src/escape-script.ts +1 -1
  179. package/src/handles/MetaTags.tsx +13 -11
  180. package/src/handles/Scripts.tsx +11 -10
  181. package/src/handles/meta.ts +2 -2
  182. package/src/handles/script.ts +17 -16
  183. package/src/href-client.ts +1 -1
  184. package/src/html.ts +12 -0
  185. package/src/index.rsc.ts +5 -0
  186. package/src/index.ts +1 -0
  187. package/src/loader-store.ts +7 -4
  188. package/src/prerender/build-shell-capture.ts +8 -11
  189. package/src/reverse.ts +44 -4
  190. package/src/route-definition/dsl-helpers.ts +117 -30
  191. package/src/route-definition/helpers-types.ts +33 -9
  192. package/src/route-definition/resolve-handler-use.ts +121 -6
  193. package/src/route-types.ts +0 -3
  194. package/src/router/debug-manifest.ts +10 -2
  195. package/src/router/error-handling.ts +51 -15
  196. package/src/router/handler-context.ts +34 -25
  197. package/src/router/intercept-resolution.ts +125 -39
  198. package/src/router/loader-resolution.ts +56 -11
  199. package/src/router/match-api.ts +12 -17
  200. package/src/router/match-middleware/background-revalidation.ts +161 -96
  201. package/src/router/match-middleware/cache-lookup.ts +93 -10
  202. package/src/router/match-middleware/cache-store.ts +17 -90
  203. package/src/router/middleware-types.ts +3 -5
  204. package/src/router/middleware.ts +52 -9
  205. package/src/router/prerender-match.ts +53 -19
  206. package/src/router/route-snapshot.ts +11 -1
  207. package/src/router/router-context.ts +2 -0
  208. package/src/router/router-interfaces.ts +25 -2
  209. package/src/router/router-options.ts +8 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -12
  211. package/src/router/segment-resolution/loader-cache.ts +205 -45
  212. package/src/router/segment-resolution/loader-mask.ts +12 -22
  213. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  214. package/src/router/segment-resolution/mask-nested.ts +7 -7
  215. package/src/router/segment-resolution/revalidation.ts +34 -6
  216. package/src/router/segment-wrappers.ts +4 -0
  217. package/src/rsc/render-pipeline.ts +3 -0
  218. package/src/rsc/response-route-handler.ts +11 -3
  219. package/src/rsc/rsc-rendering.ts +8 -45
  220. package/src/rsc/shell-capture-constants.ts +13 -0
  221. package/src/rsc/shell-capture.ts +111 -90
  222. package/src/rsc/ssr-setup.ts +14 -8
  223. package/src/rsc/types.ts +11 -0
  224. package/src/server/context.ts +88 -11
  225. package/src/server/cookie-store.ts +5 -4
  226. package/src/server/handle-store.ts +232 -7
  227. package/src/server/request-context.ts +35 -30
  228. package/src/ssr/index.tsx +40 -7
  229. package/src/static-handler.ts +4 -1
  230. package/src/testing/flight-runtime.d.ts +21 -0
  231. package/src/testing/generated-routes.ts +2 -2
  232. package/src/testing/internal/context.ts +1 -1
  233. package/src/testing/internal/flight-client-globals.ts +7 -5
  234. package/src/testing/render-route.tsx +85 -24
  235. package/src/testing/run-loader.ts +5 -1
  236. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  237. package/src/testing/vitest.ts +62 -1
  238. package/src/theme/ThemeScript.tsx +3 -3
  239. package/src/types/cache-types.ts +14 -10
  240. package/src/types/handler-context.ts +14 -0
  241. package/src/types/loader-types.ts +8 -6
  242. package/src/urls/response-types.ts +3 -23
  243. package/src/use-loader.tsx +12 -3
  244. package/src/vite/plugins/use-cache-transform.ts +118 -74
  245. package/src/vite/rango.ts +7 -3
package/src/ssr/index.tsx CHANGED
@@ -27,6 +27,7 @@ interface RenderToReadableStreamOptions {
27
27
  nonce?: string;
28
28
  formState?: unknown;
29
29
  progressiveChunkSize?: number;
30
+ onError?: (error: unknown) => void;
30
31
  }
31
32
 
32
33
  /**
@@ -124,6 +125,13 @@ export interface SSRRenderOptions {
124
125
  * prerender pass (host-agnostic bare captures).
125
126
  */
126
127
  origin?: string;
128
+
129
+ /**
130
+ * Called for each error Fizz reports through its onError: a component that
131
+ * threw inside a Suspense boundary, which leaves the boundary errored in an
132
+ * otherwise completed document. React's default console.error is kept.
133
+ */
134
+ onError?: (error: unknown) => void;
127
135
  }
128
136
 
129
137
  /**
@@ -407,6 +415,12 @@ interface ShellCaptureOptions {
407
415
  * capture, resume, and browser hydration.
408
416
  */
409
417
  origin?: string;
418
+ /**
419
+ * Called for each component error the prerender reports (an errored
420
+ * boundary in the prelude), never for the capture's own abort. The caller
421
+ * refuses to store the shell (issue #915).
422
+ */
423
+ onError?: (error: unknown) => void;
410
424
  }
411
425
 
412
426
  /**
@@ -437,6 +451,8 @@ interface ShellResumeOptions {
437
451
  search?: string;
438
452
  /** The HIT request's origin — same host as the capture's (key-scoped). */
439
453
  origin?: string;
454
+ /** Called for each component error the resumed holes report. */
455
+ onError?: (error: unknown) => void;
440
456
  }
441
457
 
442
458
  /**
@@ -601,7 +617,14 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
601
617
  rscStream: ReadableStream<Uint8Array>,
602
618
  options?: SSRRenderOptions,
603
619
  ): Promise<ReadableStream<Uint8Array>> {
604
- const { nonce, formState, streamMode, search, origin } = options ?? {};
620
+ const {
621
+ nonce,
622
+ formState,
623
+ streamMode,
624
+ search,
625
+ origin,
626
+ onError: onRenderError,
627
+ } = options ?? {};
605
628
 
606
629
  try {
607
630
  // Tee the stream:
@@ -659,6 +682,13 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
659
682
  formState,
660
683
  nonce,
661
684
  ...(progressiveChunkSize !== undefined && { progressiveChunkSize }),
685
+ ...(onRenderError && {
686
+ onError: (error: unknown) => {
687
+ onRenderError(error);
688
+ // What React's default onError does (it only logs).
689
+ console.error(error);
690
+ },
691
+ }),
662
692
  }),
663
693
  );
664
694
 
@@ -797,6 +827,7 @@ export function createShellCaptureHandler<TEnv = unknown>(
797
827
  if (error === abortReason) {
798
828
  return;
799
829
  }
830
+ opts.onError?.(error);
800
831
  reportRenderError(onError, error);
801
832
  },
802
833
  });
@@ -877,11 +908,10 @@ export function createShellCaptureHandler<TEnv = unknown>(
877
908
 
878
909
  // Sanity gate: a prelude with no `<body` is the no-shell failure mode.
879
910
  // 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
911
+ // later request re-captures. The dominant real-world cause is a masked
912
+ // live-loader read with no boundary above it (lane rule: see
913
+ // resolveLoaderData, loader-cache.ts), which root-postpones above
914
+ // <body>. Root-postponing layouts and hung handles degrade the same way. shell-capture.ts logs a once-per-key
885
915
  // warning so the eternal-MISS shape is diagnosable.
886
916
  if (!new TextDecoder().decode(prelude).includes("<body")) {
887
917
  return null;
@@ -982,7 +1012,10 @@ export function createShellResumeHandler<TEnv = unknown>(
982
1012
  // resumed stream and need the per-request nonce.
983
1013
  const resumed = await runWithPreinitNonce(nonce, () =>
984
1014
  resume(<SsrRoot />, JSON.parse(postponed), {
985
- onError: (error) => reportRenderError(onError, error),
1015
+ onError: (error) => {
1016
+ opts.onError?.(error);
1017
+ reportRenderError(onError, error);
1018
+ },
986
1019
  nonce,
987
1020
  }),
988
1021
  );
@@ -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
+ }
@@ -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
 
@@ -38,6 +38,7 @@ import {
38
38
  type RequestContext,
39
39
  } from "../server/request-context.js";
40
40
  import { createReverseFunction } from "../router/handler-context.js";
41
+ import { NOCACHE_SYMBOL } from "../cache/taint.js";
41
42
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
42
43
  import type { LoaderContext, LoaderDefinition } from "../types.js";
43
44
  import type { ContextVar } from "../context-var.js";
@@ -268,7 +269,7 @@ function runWithLoaderContext<R>(
268
269
  );
269
270
  }) as TestLoaderContext["reverse"]);
270
271
 
271
- const loaderCtx: TestLoaderContext = {
272
+ const loaderCtx: TestLoaderContext & { _routeName?: string } = {
272
273
  params: opts.params ?? {},
273
274
  routeParams: (opts.params ?? {}) as Record<string, string>,
274
275
  request: reqCtx.request,
@@ -324,6 +325,7 @@ function runWithLoaderContext<R>(
324
325
  body: opts.body,
325
326
  formData: opts.formData,
326
327
  reverse: reverse as TestLoaderContext["reverse"],
328
+ _routeName: reqCtx._routeName,
327
329
  rendered:
328
330
  opts.rendered !== undefined && opts.rendered !== false
329
331
  ? async () => {
@@ -344,6 +346,8 @@ function runWithLoaderContext<R>(
344
346
  },
345
347
  };
346
348
 
349
+ // Request-scoped for "use cache" keys, as production brands it (#940).
350
+ (loaderCtx as Record<symbol, unknown>)[NOCACHE_SYMBOL] = true;
347
351
  return fn(loaderCtx);
348
352
  });
349
353
  }
@@ -1,24 +1,127 @@
1
- // Stub for `@vitejs/plugin-rsc/rsc` and the split `/rsc/server`, `/rsc/client`
2
- // entries, shipped so consumers do not have to write a per-file `vi.mock(...)`.
3
- // Importing a router internal transitively pulls this module, whose real
4
- // top-level body imports Vite virtuals that do not resolve in plain node. The
5
- // unit/integration primitives (dispatch/runLoader/runMiddleware) never render
6
- // RSC, so empty fns suffice.
7
- export const createFromReadableStream = (): never => {
8
- throw new Error("plugin-rsc stub: createFromReadableStream not available");
9
- };
10
- export const renderToReadableStream = (): never => {
11
- throw new Error("plugin-rsc stub: renderToReadableStream not available");
12
- };
1
+ /// <reference path="../flight-runtime.d.ts" />
2
+ /**
3
+ * Stub for `@vitejs/plugin-rsc/rsc` and the split `/rsc/server`, `/rsc/client`
4
+ * entries, shipped so consumers do not have to write a per-file `vi.mock(...)`.
5
+ * The real entries top-level import Vite virtuals that do not resolve in plain
6
+ * node.
7
+ *
8
+ * Under the `react-server` condition (the Flight project) the "use cache"
9
+ * codec runs on real Flight: the vendored react-server-dom `server.edge` /
10
+ * `client.edge` builds that plugin-rsc's `react/rsc/server` and
11
+ * `react/rsc/client` wrap, with the same manifests and the same server-side
12
+ * reference loader (`@vitejs/plugin-rsc/core/rsc` `setRequireModule`), so
13
+ * keys, writes, hits, value round trips and error rows match a build. The
14
+ * wrappers are not reused: their manifests read `import.meta.env.DEV`, which
15
+ * is undefined in a module node loads directly (vitest externalizes
16
+ * node_modules).
17
+ *
18
+ * Without the condition React's server build refuses to load, so the codec
19
+ * functions throw and a "use cache" call runs uncached. Any other load failure
20
+ * (a moved vendored path, a missing peer) throws. The action entries stay
21
+ * inert either way: no primitive decodes an action request.
22
+ */
23
+
24
+ /** React's server build throws this on load without the react-server condition. */
25
+ const MISSING_REACT_SERVER_CONDITION =
26
+ /"react-server" condition must be enabled/;
27
+
28
+ async function loadFlightRuntime() {
29
+ // The server build first, alone. `@vitejs/plugin-rsc/core/rsc` statically
30
+ // imports it, and importing that ESM module after the CJS build failed to
31
+ // load leaks an unhandled rejection from Node's module loader even when every
32
+ // import() is caught (Node 22 and 24). So core/rsc loads only once the server
33
+ // build has.
34
+ const server =
35
+ await import("@vitejs/plugin-rsc/vendor/react-server-dom/server.edge").catch(
36
+ (error: unknown) => {
37
+ if (
38
+ error instanceof Error &&
39
+ MISSING_REACT_SERVER_CONDITION.test(error.message)
40
+ ) {
41
+ return undefined;
42
+ }
43
+ throw error;
44
+ },
45
+ );
46
+ if (!server) return undefined;
47
+ const [client, core] = await Promise.all([
48
+ import("@vitejs/plugin-rsc/vendor/react-server-dom/client.edge"),
49
+ import("@vitejs/plugin-rsc/core/rsc"),
50
+ ]);
51
+ await import("../internal/flight-client-globals.js");
52
+ // Resolves the `$$decode-client:` / `$$preserve:` ids a cached value's client
53
+ // and server references decode to; any other id would load a server module.
54
+ core.setRequireModule({
55
+ load: (id) => {
56
+ throw new Error(
57
+ `plugin-rsc stub: server reference module "${id}" is not loadable in a test`,
58
+ );
59
+ },
60
+ });
61
+ return { server, client, core };
62
+ }
63
+
64
+ const runtime = await loadFlightRuntime();
65
+
66
+ /** `<module id>#<export name>` -> manifest entry, as plugin-rsc builds it. */
67
+ function referenceManifest(prefix: string): Record<string, unknown> {
68
+ return new Proxy(
69
+ {},
70
+ {
71
+ get(_target, $$id) {
72
+ if (typeof $$id !== "string") return undefined;
73
+ const [id, name] = $$id.split("#");
74
+ return { id: prefix + id, name, chunks: [], async: true };
75
+ },
76
+ },
77
+ );
78
+ }
79
+
80
+ const CLIENT_MANIFEST = referenceManifest("");
81
+
82
+ function unavailable(name: string): never {
83
+ throw new Error(
84
+ `plugin-rsc stub: ${name} needs the react-server condition (the Flight test project)`,
85
+ );
86
+ }
87
+
88
+ export const renderToReadableStream = (
89
+ value: unknown,
90
+ options?: object,
91
+ ): ReadableStream<Uint8Array> =>
92
+ runtime
93
+ ? runtime.server.renderToReadableStream(value, CLIENT_MANIFEST, options)
94
+ : unavailable("renderToReadableStream");
95
+ export const createTemporaryReferenceSet = (): unknown =>
96
+ runtime ? runtime.server.createTemporaryReferenceSet() : {};
97
+ export const createFromReadableStream = (
98
+ stream: ReadableStream<Uint8Array>,
99
+ options?: object,
100
+ extraOptions?: { preserveServerReferences?: boolean },
101
+ ): Promise<unknown> =>
102
+ runtime
103
+ ? runtime.client.createFromReadableStream(stream, {
104
+ serverConsumerManifest: {
105
+ serverModuleMap: referenceManifest(
106
+ extraOptions?.preserveServerReferences
107
+ ? "$$server:$$preserve:"
108
+ : "$$server:",
109
+ ),
110
+ moduleMap: runtime.core.createServerDecodeClientManifest(),
111
+ },
112
+ ...options,
113
+ })
114
+ : unavailable("createFromReadableStream");
115
+ export const encodeReply = (
116
+ value: unknown,
117
+ options?: object,
118
+ ): Promise<string | FormData> =>
119
+ runtime
120
+ ? runtime.client.encodeReply(value, options)
121
+ : unavailable("encodeReply");
122
+ export const createClientTemporaryReferenceSet = (): unknown =>
123
+ runtime ? runtime.client.createTemporaryReferenceSet() : {};
13
124
  export const loadServerAction = (): undefined => undefined;
14
125
  export const decodeReply = (): undefined => undefined;
15
126
  export const decodeAction = (): undefined => undefined;
16
127
  export const decodeFormState = (): undefined => undefined;
17
- export const createTemporaryReferenceSet = (): Record<string, never> => ({});
18
- export const encodeReply = (): never => {
19
- throw new Error("plugin-rsc stub: encodeReply not available");
20
- };
21
- export const createClientTemporaryReferenceSet = (): Record<
22
- string,
23
- never
24
- > => ({});
@@ -78,6 +78,10 @@
78
78
 
79
79
  import { createRequire } from "node:module";
80
80
  import { fileURLToPath, pathToFileURL } from "node:url";
81
+ import {
82
+ isUseCacheCandidate,
83
+ transformUseCache,
84
+ } from "../vite/plugins/use-cache-transform.js";
81
85
 
82
86
  /** A single Vite/Vitest resolve alias entry. Structurally a Vite `Alias`. */
83
87
  export interface TestAlias {
@@ -262,6 +266,7 @@ interface FlightTransformPlugin {
262
266
  * import { defineConfig } from "vitest/config";
263
267
  * import {
264
268
  * rangoUseClientTransform,
269
+ * rangoUseCacheTransform,
265
270
  * rangoTestAliases,
266
271
  * rangoInlineDeps,
267
272
  * } from "@rangojs/router/testing/vitest";
@@ -271,7 +276,7 @@ interface FlightTransformPlugin {
271
276
  * process.env.NODE_ENV = "production";
272
277
  *
273
278
  * export default defineConfig({
274
- * plugins: [rangoUseClientTransform()],
279
+ * plugins: [rangoUseClientTransform(), rangoUseCacheTransform()],
275
280
  * resolve: {
276
281
  * conditions: ["react-server"],
277
282
  * // Bare `@rangojs/router` -> its react-server build, so a handler/component
@@ -340,3 +345,59 @@ export function rangoUseClientTransform(): FlightTransformPlugin {
340
345
  },
341
346
  };
342
347
  }
348
+
349
+ /** The Vite plugin shape {@link rangoUseCacheTransform} returns. */
350
+ interface UseCacheTransformPlugin {
351
+ name: string;
352
+ enforce: "post";
353
+ configResolved(config: { root: string }): void;
354
+ transform(
355
+ this: { warn(message: string): void },
356
+ code: string,
357
+ id: string,
358
+ ): Promise<{ code: string; map: unknown } | undefined>;
359
+ }
360
+
361
+ /**
362
+ * A Vite plugin that applies the rango plugin's `"use cache"` transform in a
363
+ * Vitest project, so a function written with the directive is wrapped with
364
+ * `registerCachedFunction` as in dev and a build. `rango()` runs that transform
365
+ * only in its `rsc` environment and Vitest transforms in `ssr`, so without this
366
+ * plugin the directive is an inert string and the function runs on every call.
367
+ *
368
+ * Add it to the react-server project next to {@link rangoUseClientTransform}
369
+ * (see that function's config). There, with a seeded `cacheStore`,
370
+ * `renderHandler` / `runLoader` observe real hits. In the node project the
371
+ * wrap applies too, but the stubbed Flight serializer cannot write an entry.
372
+ *
373
+ * Ids are the ones `vite dev` emits: `<path relative to the Vitest root>#<name>`
374
+ * (a function-level directive gives `src/data.ts#$$hoist_0_getData`), not the
375
+ * build's hashed path, so store keys stay readable. `configResolved` sets that
376
+ * root. If `@vitejs/plugin-rsc/transforms` fails to load, the transform throws
377
+ * instead of leaving the module unwrapped (which the rango plugin does).
378
+ */
379
+ export function rangoUseCacheTransform(): UseCacheTransformPlugin {
380
+ let root = process.cwd();
381
+ return {
382
+ name: "rango:testing-use-cache",
383
+ enforce: "post",
384
+ configResolved(config) {
385
+ root = config.root;
386
+ },
387
+ async transform(code, id) {
388
+ if (!isUseCacheCandidate(code, id)) return undefined;
389
+ return transformUseCache(code, id, {
390
+ root,
391
+ isBuild: false,
392
+ warn: (message) => this.warn(message),
393
+ onTransformsError: (error) => {
394
+ throw new Error(
395
+ `rangoUseCacheTransform: @vitejs/plugin-rsc/transforms failed to ` +
396
+ `load, so "use cache" functions in ${id} would run unwrapped.`,
397
+ { cause: error },
398
+ );
399
+ },
400
+ });
401
+ },
402
+ };
403
+ }