@rangojs/router 0.15.1 → 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 (249) 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 +52 -0
  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 +36 -10
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +3 -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/thenable-status.d.ts +9 -0
  67. package/dist/types/types/cache-types.d.ts +14 -10
  68. package/dist/types/types/handler-context.d.ts +14 -0
  69. package/dist/types/types/loader-types.d.ts +8 -6
  70. package/dist/types/urls/response-types.d.ts +3 -12
  71. package/dist/types/use-loader.d.ts +12 -3
  72. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  73. package/dist/types/vite/rango.d.ts +1 -1
  74. package/dist/vite/index.js +884 -768
  75. package/package.json +4 -4
  76. package/skills/api-client/SKILL.md +22 -10
  77. package/skills/breadcrumbs/SKILL.md +65 -22
  78. package/skills/bundle-analysis/SKILL.md +22 -22
  79. package/skills/cache-guide/SKILL.md +207 -97
  80. package/skills/caching/SKILL.md +295 -93
  81. package/skills/catalog.json +16 -16
  82. package/skills/client-urls/SKILL.md +53 -31
  83. package/skills/cloudflare/SKILL.md +72 -20
  84. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  85. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  86. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  87. package/skills/comparison/SKILL.md +5 -0
  88. package/skills/comparison/references/framework-comparison.md +39 -33
  89. package/skills/composability/SKILL.md +38 -29
  90. package/skills/css/SKILL.md +10 -13
  91. package/skills/debug-manifest/SKILL.md +74 -31
  92. package/skills/defer-hydration/SKILL.md +2 -1
  93. package/skills/deployment-caching/SKILL.md +6 -1
  94. package/skills/document-cache/SKILL.md +92 -28
  95. package/skills/fonts/SKILL.md +16 -8
  96. package/skills/handler-use/SKILL.md +53 -17
  97. package/skills/hooks/SKILL.md +28 -20
  98. package/skills/hooks/data.md +43 -1
  99. package/skills/hooks/handle-and-actions.md +31 -16
  100. package/skills/hooks/navigation.md +39 -10
  101. package/skills/hooks/outlets.md +13 -6
  102. package/skills/hooks/state.md +37 -15
  103. package/skills/hooks/urls.md +7 -5
  104. package/skills/host-router/SKILL.md +64 -12
  105. package/skills/i18n/SKILL.md +60 -34
  106. package/skills/intercept/SKILL.md +161 -26
  107. package/skills/layout/SKILL.md +106 -50
  108. package/skills/links/SKILL.md +51 -19
  109. package/skills/loader/SKILL.md +242 -106
  110. package/skills/middleware/SKILL.md +119 -23
  111. package/skills/migrate-nextjs/SKILL.md +100 -45
  112. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  113. package/skills/migrate-react-router/SKILL.md +49 -38
  114. package/skills/migrate-react-router/component-migration.md +25 -6
  115. package/skills/migrate-react-router/data-and-actions.md +18 -5
  116. package/skills/migrate-react-router/route-mapping.md +12 -6
  117. package/skills/mime-routes/SKILL.md +12 -7
  118. package/skills/observability/SKILL.md +28 -14
  119. package/skills/parallel/SKILL.md +75 -39
  120. package/skills/ppr/SKILL.md +345 -150
  121. package/skills/prerender/SKILL.md +189 -100
  122. package/skills/rango/SKILL.md +65 -34
  123. package/skills/react-compiler/SKILL.md +6 -5
  124. package/skills/response-routes/SKILL.md +133 -25
  125. package/skills/route/SKILL.md +165 -60
  126. package/skills/router-setup/SKILL.md +418 -240
  127. package/skills/scripts/SKILL.md +31 -20
  128. package/skills/server-actions/SKILL.md +220 -59
  129. package/skills/shell-manifest/SKILL.md +12 -7
  130. package/skills/streams-and-websockets/SKILL.md +32 -14
  131. package/skills/tailwind/SKILL.md +37 -33
  132. package/skills/testing/SKILL.md +68 -32
  133. package/skills/testing/cache-prerender.md +55 -29
  134. package/skills/testing/client-components.md +59 -6
  135. package/skills/testing/e2e-parity.md +12 -2
  136. package/skills/testing/flight.md +19 -11
  137. package/skills/testing/handles.md +8 -5
  138. package/skills/testing/loader.md +8 -5
  139. package/skills/testing/middleware.md +8 -3
  140. package/skills/testing/predicates.md +120 -0
  141. package/skills/testing/render-handler.md +70 -22
  142. package/skills/testing/response-routes.md +11 -4
  143. package/skills/testing/reverse-and-types.md +20 -14
  144. package/skills/testing/server-actions.md +12 -3
  145. package/skills/testing/server-tree.md +9 -0
  146. package/skills/testing/setup.md +9 -5
  147. package/skills/theme/SKILL.md +106 -29
  148. package/skills/typesafety/SKILL.md +20 -9
  149. package/skills/typesafety/env-and-bindings.md +126 -47
  150. package/skills/typesafety/generated-files-and-cli.md +26 -14
  151. package/skills/typesafety/params-and-search.md +44 -19
  152. package/skills/typesafety/route-types.md +50 -14
  153. package/skills/use-cache/SKILL.md +200 -82
  154. package/skills/vercel/SKILL.md +21 -11
  155. package/skills/view-transitions/SKILL.md +41 -20
  156. package/src/__augment-tests__/augmented.check.ts +26 -1
  157. package/src/browser/index.ts +2 -2
  158. package/src/browser/navigation-bridge.ts +3 -4
  159. package/src/browser/navigation-transaction.ts +22 -2
  160. package/src/browser/partial-update.ts +75 -80
  161. package/src/browser/react/ScrollRestoration.tsx +6 -6
  162. package/src/browser/rsc-router.tsx +1 -1
  163. package/src/browser/scroll-restoration.ts +53 -6
  164. package/src/cache/cache-exec-scope.ts +37 -4
  165. package/src/cache/cache-key-utils.ts +1 -6
  166. package/src/cache/cache-runtime.ts +230 -61
  167. package/src/cache/cache-scope.ts +76 -12
  168. package/src/cache/document-cache.ts +15 -0
  169. package/src/cache/handle-capture.ts +90 -10
  170. package/src/cache/handle-snapshot.ts +132 -28
  171. package/src/cache/segment-codec.ts +26 -7
  172. package/src/cache/tag-invalidation.ts +4 -3
  173. package/src/cache/taint.ts +1 -1
  174. package/src/cache/types.ts +16 -3
  175. package/src/client.rsc.tsx +10 -9
  176. package/src/client.tsx +5 -4
  177. package/src/components/DefaultDocument.tsx +7 -2
  178. package/src/context-var.ts +6 -5
  179. package/src/escape-script.ts +1 -1
  180. package/src/handles/MetaTags.tsx +13 -11
  181. package/src/handles/Scripts.tsx +11 -10
  182. package/src/handles/meta.ts +2 -2
  183. package/src/handles/script.ts +17 -16
  184. package/src/href-client.ts +1 -1
  185. package/src/html.ts +12 -0
  186. package/src/index.rsc.ts +5 -0
  187. package/src/index.ts +1 -0
  188. package/src/loader-store.ts +106 -0
  189. package/src/prerender/build-shell-capture.ts +8 -11
  190. package/src/reverse.ts +44 -4
  191. package/src/route-definition/dsl-helpers.ts +134 -42
  192. package/src/route-definition/helpers-types.ts +36 -10
  193. package/src/route-definition/resolve-handler-use.ts +121 -6
  194. package/src/route-types.ts +2 -3
  195. package/src/router/debug-manifest.ts +10 -2
  196. package/src/router/error-handling.ts +51 -15
  197. package/src/router/handler-context.ts +34 -25
  198. package/src/router/intercept-resolution.ts +125 -39
  199. package/src/router/loader-resolution.ts +56 -11
  200. package/src/router/match-api.ts +12 -17
  201. package/src/router/match-middleware/background-revalidation.ts +161 -96
  202. package/src/router/match-middleware/cache-lookup.ts +93 -10
  203. package/src/router/match-middleware/cache-store.ts +17 -90
  204. package/src/router/middleware-types.ts +3 -5
  205. package/src/router/middleware.ts +52 -9
  206. package/src/router/prerender-match.ts +53 -19
  207. package/src/router/route-snapshot.ts +11 -1
  208. package/src/router/router-context.ts +2 -0
  209. package/src/router/router-interfaces.ts +25 -2
  210. package/src/router/router-options.ts +8 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -12
  212. package/src/router/segment-resolution/loader-cache.ts +205 -45
  213. package/src/router/segment-resolution/loader-mask.ts +12 -22
  214. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  215. package/src/router/segment-resolution/mask-nested.ts +7 -7
  216. package/src/router/segment-resolution/revalidation.ts +34 -6
  217. package/src/router/segment-wrappers.ts +4 -0
  218. package/src/rsc/render-pipeline.ts +3 -0
  219. package/src/rsc/response-route-handler.ts +11 -3
  220. package/src/rsc/rsc-rendering.ts +8 -45
  221. package/src/rsc/shell-capture-constants.ts +13 -0
  222. package/src/rsc/shell-capture.ts +111 -90
  223. package/src/rsc/ssr-setup.ts +14 -8
  224. package/src/rsc/types.ts +11 -0
  225. package/src/server/context.ts +88 -11
  226. package/src/server/cookie-store.ts +5 -4
  227. package/src/server/handle-store.ts +232 -7
  228. package/src/server/request-context.ts +35 -30
  229. package/src/ssr/index.tsx +40 -7
  230. package/src/ssr-suspension-warning.ts +2 -13
  231. package/src/static-handler.ts +4 -1
  232. package/src/testing/flight-runtime.d.ts +21 -0
  233. package/src/testing/generated-routes.ts +2 -2
  234. package/src/testing/internal/context.ts +1 -1
  235. package/src/testing/internal/flight-client-globals.ts +7 -5
  236. package/src/testing/render-route.tsx +85 -24
  237. package/src/testing/run-loader.ts +5 -1
  238. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  239. package/src/testing/vitest.ts +62 -1
  240. package/src/theme/ThemeProvider.tsx +14 -12
  241. package/src/theme/ThemeScript.tsx +3 -3
  242. package/src/thenable-status.ts +16 -0
  243. package/src/types/cache-types.ts +14 -10
  244. package/src/types/handler-context.ts +14 -0
  245. package/src/types/loader-types.ts +8 -6
  246. package/src/urls/response-types.ts +3 -23
  247. package/src/use-loader.tsx +24 -4
  248. package/src/vite/plugins/use-cache-transform.ts +118 -74
  249. package/src/vite/rango.ts +7 -3
@@ -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
+ }
@@ -114,8 +114,6 @@ export function ThemeProvider({
114
114
  initialTheme,
115
115
  children,
116
116
  }: ThemeProviderProps): React.ReactNode {
117
- const [mounted, setMounted] = useState(false);
118
-
119
117
  // HYDRATION PARITY: this initializer is the server (SSR/resume) render AND
120
118
  // the client's hydration render — both must produce the same value. It must
121
119
  // NEVER read cookie/localStorage: whenever the payload's initialTheme
@@ -132,7 +130,6 @@ export function ThemeProvider({
132
130
  const [systemTheme, setSystemTheme] = useState<ResolvedTheme>("light");
133
131
 
134
132
  useEffect(() => {
135
- setMounted(true);
136
133
  setSystemTheme(getSystemTheme());
137
134
  // Re-sync state from an EXPLICITLY stored theme after mount. initialTheme
138
135
  // comes from the payload and can legitimately differ from the visitor's
@@ -229,18 +226,16 @@ export function ThemeProvider({
229
226
  };
230
227
  }, [config]);
231
228
 
229
+ // No pre-mount special case: `theme` and `systemTheme` ("light" until the
230
+ // mount effect) are identical on the server and in the hydration render, so
231
+ // this is hydration-safe, and a concrete defaultTheme without an initialTheme
232
+ // no longer flips at mount.
232
233
  const resolvedTheme: ResolvedTheme = useMemo(() => {
233
- if (!mounted) {
234
- if (initialTheme && initialTheme !== "system") {
235
- return initialTheme as ResolvedTheme;
236
- }
237
- return "light";
238
- }
239
234
  if (theme === "system" && config.enableSystem) {
240
235
  return systemTheme;
241
236
  }
242
237
  return theme as ResolvedTheme;
243
- }, [theme, systemTheme, config.enableSystem, mounted, initialTheme]);
238
+ }, [theme, systemTheme, config.enableSystem]);
244
239
 
245
240
  const themes = useMemo(() => {
246
241
  if (config.enableSystem) {
@@ -249,16 +244,23 @@ export function ThemeProvider({
249
244
  return config.themes;
250
245
  }, [config.themes, config.enableSystem]);
251
246
 
247
+ // Keyed on the exposed field values only: a NEW context object with
248
+ // unchanged fields propagates into every dehydrated Suspense boundary still
249
+ // streaming in (React cannot see their consumers) and makes React
250
+ // client-render those boundaries instead of adopting the server HTML their
251
+ // $RC script delivers. A field that genuinely changes at mount (dark system
252
+ // theme, stored override) must still publish; that case stays open
253
+ // (CHANGELOG).
252
254
  const contextValue: ThemeContextValue = useMemo(
253
255
  () => ({
254
256
  theme,
255
257
  setTheme,
256
258
  resolvedTheme,
257
- systemTheme: mounted ? systemTheme : "light",
259
+ systemTheme,
258
260
  themes,
259
261
  config,
260
262
  }),
261
- [theme, setTheme, resolvedTheme, systemTheme, themes, config, mounted],
263
+ [theme, setTheme, resolvedTheme, systemTheme, themes, config],
262
264
  );
263
265
 
264
266
  return (
@@ -8,14 +8,14 @@
8
8
  *
9
9
  * Must be placed in the <head> element of your document, before any stylesheets.
10
10
  *
11
- * Note: when theme is enabled in the router config, `<MetaTags />` ALREADY
11
+ * Note: when theme is enabled in the router config, `<Html.Meta />` ALREADY
12
12
  * renders this FOUC script. Use `<ThemeScript />` only if you do NOT render
13
- * `<MetaTags />`. Rendering both is safe — the inline script guards the
13
+ * `<Html.Meta />`. Rendering both is safe — the inline script guards the
14
14
  * matchMedia listener registration against double-running — but it is redundant.
15
15
  *
16
16
  * @example
17
17
  * ```tsx
18
- * // In your document component. Use ThemeScript only when you do not render MetaTags.
18
+ * // In your document component. Use ThemeScript only when you do not render Html.Meta.
19
19
  * import { ThemeScript } from "@rangojs/router/theme";
20
20
  *
21
21
  * export function Document({ children }) {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Thenable statuses React's `use()` unwraps without suspending: React's
3
+ * re-check after `.then()` handles "fulfilled", and Flight chunks in the
4
+ * resolved_* states initialize synchronously inside that same call. Anything
5
+ * else (pending, blocked, an unstamped native promise) suspends at least once.
6
+ * Shared by the dev SSR-suspension diagnostic and the held-navigation
7
+ * isLoading registration (loader-store.ts), which must agree on "settled".
8
+ */
9
+ export function unwrapsSynchronously(stream: Promise<unknown>): boolean {
10
+ const status = (stream as { status?: string }).status;
11
+ return (
12
+ status === "fulfilled" ||
13
+ status === "resolved_model" ||
14
+ status === "resolved_module"
15
+ );
16
+ }
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Context passed to cache condition/key/tags functions.
2
+ * Context passed to cache condition/key/tags functions (the full
3
+ * RequestContext).
3
4
  *
4
- * This is a subset of RequestContext that's guaranteed to be available
5
- * during cache key generation (before middleware runs).
6
- *
7
- * Note: While the full RequestContext is passed, middleware-set variables
8
- * read via `ctx.get()` may not be populated yet since cache lookup happens
9
- * before middleware execution.
5
+ * Ordering: global middleware wraps the whole request, and on RSC routes route
6
+ * middleware wraps the match/render pipeline (rsc/handler.ts executeRender), so
7
+ * variables they set before `next()` are readable via `ctx.get()`. On response
8
+ * routes the cache lookup runs BEFORE route middleware, which wraps only the
9
+ * handler run on a miss (rsc/response-route-handler.ts); only global-middleware
10
+ * variables are populated there.
10
11
  */
11
12
  export type { RequestContext as CacheContext } from "../server/request-context.js";
12
13
 
@@ -109,7 +110,8 @@ export interface CacheOptions<TEnv = unknown> {
109
110
  * Return false to skip cache for this request (always fetch fresh).
110
111
  *
111
112
  * Has access to full RequestContext including env, request, params, cookies, etc.
112
- * Note: Middleware-set variables read via `ctx.get()` may not be populated yet.
113
+ * Runs after global and route middleware on RSC routes; on response routes,
114
+ * before route middleware (see CacheContext).
113
115
  *
114
116
  * @example
115
117
  * ```typescript
@@ -131,7 +133,8 @@ export interface CacheOptions<TEnv = unknown> {
131
133
  * Bypasses default key generation AND store's keyGenerator.
132
134
  *
133
135
  * Has access to full RequestContext including env, request, params, cookies, etc.
134
- * Note: Middleware-set variables read via `ctx.get()` may not be populated yet.
136
+ * Runs after global and route middleware on RSC routes; on response routes,
137
+ * before route middleware (see CacheContext).
135
138
  *
136
139
  * @example
137
140
  * ```typescript
@@ -153,7 +156,8 @@ export interface CacheOptions<TEnv = unknown> {
153
156
  * Tags for cache invalidation.
154
157
  * Can be a static array or a function that returns tags.
155
158
  *
156
- * The built-in `MemorySegmentCacheStore` and `CFCacheStore` index by tag.
159
+ * The built-in `MemorySegmentCacheStore`, `CFCacheStore`, and
160
+ * `VercelCacheStore` index by tag.
157
161
  * Invalidate on demand with `updateTag(...tags)` (awaitable, read-your-own-writes;
158
162
  * for server actions) or `revalidateTag(...tags)` (background hard-purge, not
159
163
  * awaited; for route handlers / webhooks). For `CFCacheStore`, distributed
@@ -455,6 +455,20 @@ export type InternalHandlerContext<
455
455
  * table (so a loader cannot await its own in-flight memoized promise).
456
456
  */
457
457
  _loaderCacheOriginalUse?: (item: any) => any;
458
+ /**
459
+ * @internal A loader-cache HIT asks before replaying a loader's recorded
460
+ * pushes: true when that loader has not run in this request and no other
461
+ * replay delivered it; its later run then replaces the replayed values
462
+ * (HandleStore.pushReplayed). One copy per loader per request. See
463
+ * setupLoaderAccess.
464
+ */
465
+ _claimLoaderPushes?: (loaderId: string) => boolean;
466
+ /**
467
+ * @internal Run a loader on a fresh executor (own memo map): a loader-cache
468
+ * stale refresh, whose diverted pushes must not take a dependency's run
469
+ * from the page. See setupLoaderAccess.
470
+ */
471
+ _runLoaderIsolated?: (loader: any) => Promise<any>;
458
472
  };
459
473
 
460
474
  /**
@@ -28,9 +28,10 @@ import type { RequestScope } from "./request-scope.js";
28
28
  * return await db.cart.get(user.id);
29
29
  * });
30
30
  *
31
- * // With typed params:
32
- * const ProductLoader = createLoader<Product, { slug: string }>(async (ctx) => {
33
- * const { slug } = ctx.params; // slug is typed as string
31
+ * // createLoader's one type parameter is the data type; params are
32
+ * // Record<string, string | undefined>:
33
+ * const ProductLoader = createLoader<Product>(async (ctx) => {
34
+ * const { slug } = ctx.params; // string | undefined
34
35
  * return await db.products.findBySlug(slug);
35
36
  * });
36
37
  * ```
@@ -243,9 +244,10 @@ export type LoadOptions =
243
244
  * return await db.cart.get(ctx.get("user").id);
244
245
  * });
245
246
  *
246
- * // With typed params:
247
- * export const ProductLoader = createLoader<Product, { slug: string }>(async (ctx) => {
248
- * const { slug } = ctx.params; // slug is typed as string
247
+ * // createLoader's one type parameter is the data type; params are
248
+ * // Record<string, string | undefined>:
249
+ * export const ProductLoader = createLoader<Product>(async (ctx) => {
250
+ * const { slug } = ctx.params; // string | undefined
249
251
  * return await db.products.findBySlug(slug);
250
252
  * });
251
253
  *
@@ -1,5 +1,5 @@
1
1
  import type { ContextVar } from "../context-var.js";
2
- import type { ReverseFunction } from "../reverse.js";
2
+ import type { GlobalReverseFunction } from "../reverse.js";
3
3
  import type {
4
4
  DefaultReverseRouteMap,
5
5
  DefaultVars,
@@ -7,27 +7,6 @@ import type {
7
7
  import type { UseItems, ResponseRouteUseItem } from "../route-types.js";
8
8
  import type { RequestScope } from "../types/request-scope.js";
9
9
 
10
- /**
11
- * Reverse function for response handler contexts.
12
- * Global names get autocomplete and param validation from the generated route map.
13
- * Local `.name` calls are accepted but not validated (scope unknown at type level).
14
- */
15
- type ResponseReverseFunction = [DefaultReverseRouteMap] extends [
16
- Record<string, string>,
17
- ]
18
- ? (
19
- name: string,
20
- params?: Record<string, string>,
21
- search?: Record<string, unknown>,
22
- ) => string
23
- : ReverseFunction<DefaultReverseRouteMap> & {
24
- (
25
- name: `.${string}`,
26
- params?: Record<string, string>,
27
- search?: Record<string, unknown>,
28
- ): string;
29
- };
30
-
31
10
  /**
32
11
  * Symbol marking a route as a response route (non-RSC).
33
12
  * Stored on PathOptions and UrlPatterns to signal the trie to short-circuit.
@@ -99,7 +78,8 @@ export interface ResponseHandlerContext<
99
78
  params: TParams;
100
79
  /** @internal Phantom property for params type invariance. Prevents mounting handlers on wrong routes. */
101
80
  readonly _paramCheck?: (params: TParams) => TParams;
102
- reverse: ResponseReverseFunction;
81
+ /** Global route names only; see {@link GlobalReverseFunction}. */
82
+ reverse: GlobalReverseFunction<DefaultReverseRouteMap>;
103
83
  /** Read a variable set by middleware via ctx.set(key, value) or ctx.set(ContextVar, value). */
104
84
  get: {
105
85
  <T>(contextVar: ContextVar<T>): T | undefined;
@@ -8,6 +8,7 @@ import {
8
8
  useContext,
9
9
  useEffect,
10
10
  useMemo,
11
+ useOptimistic,
11
12
  useRef,
12
13
  useState,
13
14
  type ReactNode,
@@ -116,6 +117,14 @@ export type LoadFunction<T> = (options?: LoadOptions) => Promise<T>;
116
117
 
117
118
  export interface UseLoaderResult<T> {
118
119
  data: T;
120
+ /**
121
+ * True while this loader family (`loader.$$id`) is in flight: a `load()` /
122
+ * group refresh, or a held navigation still streaming a re-run of this
123
+ * loader. Family-keyed, not segment-keyed: a layout `useLoader` of the same
124
+ * `createLoader` the child route is re-running also reports true, even if
125
+ * the layout copy is not replaced. Not used for the initial streamed read
126
+ * (that suspends).
127
+ */
119
128
  isLoading: boolean;
120
129
  error: Error | null;
121
130
  load: LoadFunction<T>;
@@ -124,6 +133,7 @@ export interface UseLoaderResult<T> {
124
133
 
125
134
  export interface UseFetchLoaderResult<T> {
126
135
  data: T | undefined;
136
+ /** Same family-keyed contract as {@link UseLoaderResult.isLoading}. */
127
137
  isLoading: boolean;
128
138
  error: Error | null;
129
139
  load: LoadFunction<T>;
@@ -261,6 +271,13 @@ function useLoaderInternal<T>(
261
271
  }
262
272
  }, [loaderId, bucketKey]);
263
273
 
274
+ // Held-navigation pin: a transition commit whose stream for this loader is
275
+ // still pending announces it (loader-store.ts announcePendingStreams, which
276
+ // documents the lane discipline); this optimistic update renders urgently
277
+ // on the held content and React reverts it when that transition commits,
278
+ // i.e. together with the new data.
279
+ const [streamPending, setStreamPending] = useOptimistic(false);
280
+
264
281
  const [sharedState, setSharedState] = useState<{
265
282
  bucketKey: string;
266
283
  snapshot: LoaderEntry;
@@ -295,6 +312,9 @@ function useLoaderInternal<T>(
295
312
  ephemeral: !hasContextData,
296
313
  group: hasGroups ? groupList : undefined,
297
314
  refetch: hasGroups ? groupRefetch : undefined,
315
+ onStreamPending: hasContextData
316
+ ? () => startTransition(() => setStreamPending(true))
317
+ : undefined,
298
318
  },
299
319
  );
300
320
  // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional:
@@ -362,7 +382,7 @@ function useLoaderInternal<T>(
362
382
  : sharedSnapshot.hasValue
363
383
  ? (sharedSnapshot.value as T | undefined)
364
384
  : contextData;
365
- const isLoading = localIsLoading || sharedSnapshot.isLoading;
385
+ const isLoading = localIsLoading || sharedSnapshot.isLoading || streamPending;
366
386
  const error = localError ?? sharedSnapshot.error;
367
387
 
368
388
  const throwOnError = options?.throwOnError ?? true;
@@ -549,7 +569,7 @@ function useLoaderInternal<T>(
549
569
  * @example Basic usage - accessing route loader data
550
570
  * ```tsx
551
571
  * "use client";
552
- * import { useLoader } from "rsc-router/client";
572
+ * import { useLoader } from "@rangojs/router/client";
553
573
  * import { CartLoader } from "../loaders/cart";
554
574
  *
555
575
  * // In route definition: loader(CartLoader)
@@ -597,7 +617,7 @@ export function useLoader<T>(
597
617
  * @example On-demand fetching
598
618
  * ```tsx
599
619
  * "use client";
600
- * import { useFetchLoader } from "rsc-router/client";
620
+ * import { useFetchLoader } from "@rangojs/router/client";
601
621
  * import { SearchLoader } from "../loaders/search";
602
622
  *
603
623
  * export function SearchResults() {
@@ -659,7 +679,7 @@ export function useFetchLoader<T>(
659
679
  * @example
660
680
  * ```tsx
661
681
  * "use client";
662
- * import { useLoader, useRefreshLoaders } from "rsc-router/client";
682
+ * import { useLoader, useRefreshLoaders } from "@rangojs/router/client";
663
683
  *
664
684
  * function Profile() {
665
685
  * const { data } = useLoader(ProfileLoader, { key: userId, refreshGroup: "account" });
@@ -16,14 +16,18 @@
16
16
  * - transformHoistInlineDirective() for function-level hoisting
17
17
  */
18
18
 
19
- import type { Plugin } from "vite";
19
+ import type { Plugin, Rollup } from "vite";
20
20
  import type { ModuleExportMeta } from "@vitejs/plugin-rsc/transforms";
21
21
  import path from "node:path";
22
- import MagicString from "magic-string";
23
22
  import { normalizePath, hashId } from "./expose-id-utils.js";
24
- import { createRangoDebugger, createCounter, NS } from "../debug.js";
23
+ import type { Counter } from "../debug.js";
25
24
 
26
- const debug = createRangoDebugger(NS.transform);
25
+ // No static import of `magic-string` or `../debug.js` (the `debug` package):
26
+ // `rangoUseCacheTransform()` bundles this module into `dist/testing/vitest.js`,
27
+ // whose static imports are node builtins only, and every package it needs
28
+ // loads lazily, as `rangoUseClientTransform()`'s `vite` and plugin-rsc
29
+ // imports do. src/__tests__/installed-vitest-config.test.ts loads that file
30
+ // from a fixture holding only `package.json` and `dist/`.
27
31
 
28
32
  const CACHE_RUNTIME_IMPORT = "@rangojs/router/cache-runtime";
29
33
 
@@ -33,12 +37,13 @@ const LAYOUT_TEMPLATE_PATTERN = /\/(layout|template)\.(tsx?|jsx?)$/;
33
37
 
34
38
  export const USE_CACHE_DIRECTIVE_RE: RegExp = /^use cache(:\s*[\w-]+)?$/;
35
39
 
36
- export function useCacheTransform(): Plugin {
40
+ /**
41
+ * @param counter `rango:transform` timing counter, created by `rango()`
42
+ * (`createCounter` in ../debug.js); omitted when debugging is off.
43
+ */
44
+ export function useCacheTransform(counter?: Counter): Plugin {
37
45
  let projectRoot = "";
38
46
  let isBuild = false;
39
- let rscTransforms: typeof import("@vitejs/plugin-rsc/transforms") | null =
40
- null;
41
- const counter = createCounter(debug, "use-cache");
42
47
 
43
48
  return {
44
49
  name: "@rangojs/router:use-cache",
@@ -56,75 +61,15 @@ export function useCacheTransform(): Plugin {
56
61
  async transform(code, id) {
57
62
  // Only process in RSC environment
58
63
  if (this.environment?.name !== "rsc") return;
59
-
60
- // Quick bail: no "use cache" in source
61
- if (!code.includes("use cache")) return;
62
-
63
- // Skip node_modules and virtual modules
64
- if (id.includes("/node_modules/") || id.startsWith("\0")) return;
65
-
66
- // Only JS/TS files
67
- if (!/\.(tsx?|jsx?|mjs)$/.test(id)) return;
64
+ if (!isUseCacheCandidate(code, id)) return;
68
65
 
69
66
  const start = counter ? performance.now() : 0;
70
67
  try {
71
- if (!rscTransforms) {
72
- try {
73
- rscTransforms = await import("@vitejs/plugin-rsc/transforms");
74
- } catch {
75
- return;
76
- }
77
- }
78
-
79
- const {
80
- hasDirective,
81
- transformWrapExport,
82
- transformHoistInlineDirective,
83
- } = rscTransforms;
84
-
85
- let ast: any;
86
- try {
87
- const { parseAst } = await import("vite");
88
- ast = parseAst(code, { lang: "tsx" });
89
- } catch {
90
- return;
91
- }
92
-
93
- // plugin-rsc 0.5.34 `matchDirective` does `stmt.directive.match(...)`
94
- // after `"directive" in node`. Vite/oxc parseAst now emits
95
- // `directive: null` on ordinary ExpressionStatements, so a file that
96
- // mixes a `"use cache"` function with a sibling handler whose first
97
- // statement is an expression throws and the wrap is dropped.
98
- stripNullDirectiveFields(ast);
99
-
100
- const filePath = normalizePath(path.relative(projectRoot, id));
101
- const isLayoutOrTemplate = LAYOUT_TEMPLATE_PATTERN.test(id);
102
-
103
- if (hasDirective(ast.body, "use cache")) {
104
- return transformFileLevelUseCache(
105
- code,
106
- ast,
107
- filePath,
108
- id,
109
- isBuild,
110
- isLayoutOrTemplate,
111
- transformWrapExport,
112
- hasDirective,
113
- );
114
- }
115
-
116
- const functionResult = transformFunctionLevelUseCache(
117
- code,
118
- ast,
119
- filePath,
120
- id,
68
+ return await transformUseCache(code, id, {
69
+ root: projectRoot,
121
70
  isBuild,
122
- transformHoistInlineDirective,
123
- );
124
-
125
- warnOnNearMissDirectives(ast, id, this.warn.bind(this));
126
-
127
- if (functionResult) return functionResult;
71
+ warn: (message) => this.warn(message),
72
+ });
128
73
  } finally {
129
74
  counter?.record(id, performance.now() - start);
130
75
  }
@@ -132,7 +77,105 @@ export function useCacheTransform(): Plugin {
132
77
  };
133
78
  }
134
79
 
135
- function transformFileLevelUseCache(
80
+ /** Options for {@link transformUseCache}. */
81
+ export interface UseCacheTransformOptions {
82
+ /** Root the ids are relative to (Vite's `config.root`). */
83
+ root: string;
84
+ /** Build ids hash the path (`hashId`); dev ids are `<path>#<export>`. */
85
+ isBuild: boolean;
86
+ /** Sink for the near-miss directive warning. */
87
+ warn: (message: string) => void;
88
+ /**
89
+ * Called when `@vitejs/plugin-rsc/transforms` fails to load; the module is
90
+ * then left unwrapped. The rango plugin omits it (skip silently).
91
+ */
92
+ onTransformsError?: (error: unknown) => void;
93
+ }
94
+
95
+ /** Whether a module may carry a `"use cache"` directive the transform wraps. */
96
+ export function isUseCacheCandidate(code: string, id: string): boolean {
97
+ return (
98
+ // Quick bail: no "use cache" in source
99
+ code.includes("use cache") &&
100
+ // Skip node_modules and virtual modules
101
+ !id.includes("/node_modules/") &&
102
+ !id.startsWith("\0") &&
103
+ // Only JS/TS files
104
+ /\.(tsx?|jsx?|mjs)$/.test(id)
105
+ );
106
+ }
107
+
108
+ let rscTransforms: typeof import("@vitejs/plugin-rsc/transforms") | null = null;
109
+
110
+ /**
111
+ * Wrap a module's `"use cache"` functions with `registerCachedFunction`. The
112
+ * rango plugin runs it in the rsc environment; `rangoUseCacheTransform()`
113
+ * (testing/vitest.ts) runs it in a Vitest project, whose environment is `ssr`.
114
+ * Call it only for an {@link isUseCacheCandidate} module.
115
+ */
116
+ export async function transformUseCache(
117
+ code: string,
118
+ id: string,
119
+ options: UseCacheTransformOptions,
120
+ ): Promise<{ code: string; map: Rollup.ExistingRawSourceMap } | undefined> {
121
+ if (!rscTransforms) {
122
+ try {
123
+ rscTransforms = await import("@vitejs/plugin-rsc/transforms");
124
+ } catch (error) {
125
+ options.onTransformsError?.(error);
126
+ return;
127
+ }
128
+ }
129
+
130
+ const { hasDirective, transformWrapExport, transformHoistInlineDirective } =
131
+ rscTransforms;
132
+
133
+ let ast: any;
134
+ try {
135
+ const { parseAst } = await import("vite");
136
+ ast = parseAst(code, { lang: "tsx" });
137
+ } catch {
138
+ return;
139
+ }
140
+
141
+ // plugin-rsc 0.5.34 `matchDirective` does `stmt.directive.match(...)`
142
+ // after `"directive" in node`. Vite/oxc parseAst now emits
143
+ // `directive: null` on ordinary ExpressionStatements, so a file that
144
+ // mixes a `"use cache"` function with a sibling handler whose first
145
+ // statement is an expression throws and the wrap is dropped.
146
+ stripNullDirectiveFields(ast);
147
+
148
+ const filePath = normalizePath(path.relative(options.root, id));
149
+ const isLayoutOrTemplate = LAYOUT_TEMPLATE_PATTERN.test(id);
150
+
151
+ if (hasDirective(ast.body, "use cache")) {
152
+ return transformFileLevelUseCache(
153
+ code,
154
+ ast,
155
+ filePath,
156
+ id,
157
+ options.isBuild,
158
+ isLayoutOrTemplate,
159
+ transformWrapExport,
160
+ hasDirective,
161
+ );
162
+ }
163
+
164
+ const functionResult = transformFunctionLevelUseCache(
165
+ code,
166
+ ast,
167
+ filePath,
168
+ id,
169
+ options.isBuild,
170
+ transformHoistInlineDirective,
171
+ );
172
+
173
+ warnOnNearMissDirectives(ast, id, options.warn);
174
+
175
+ if (functionResult) return functionResult;
176
+ }
177
+
178
+ async function transformFileLevelUseCache(
136
179
  code: string,
137
180
  ast: any,
138
181
  filePath: string,
@@ -193,6 +236,7 @@ function transformFileLevelUseCache(
193
236
  }
194
237
 
195
238
  if (exportNames.length === 0) {
239
+ const { default: MagicString } = await import("magic-string");
196
240
  const s = new MagicString(code);
197
241
  const directive = findFileLevelDirective(ast);
198
242
  if (directive) {