@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
@@ -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 }) {
@@ -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;
@@ -117,6 +117,14 @@ export type LoadFunction<T> = (options?: LoadOptions) => Promise<T>;
117
117
 
118
118
  export interface UseLoaderResult<T> {
119
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
+ */
120
128
  isLoading: boolean;
121
129
  error: Error | null;
122
130
  load: LoadFunction<T>;
@@ -125,6 +133,7 @@ export interface UseLoaderResult<T> {
125
133
 
126
134
  export interface UseFetchLoaderResult<T> {
127
135
  data: T | undefined;
136
+ /** Same family-keyed contract as {@link UseLoaderResult.isLoading}. */
128
137
  isLoading: boolean;
129
138
  error: Error | null;
130
139
  load: LoadFunction<T>;
@@ -560,7 +569,7 @@ function useLoaderInternal<T>(
560
569
  * @example Basic usage - accessing route loader data
561
570
  * ```tsx
562
571
  * "use client";
563
- * import { useLoader } from "rsc-router/client";
572
+ * import { useLoader } from "@rangojs/router/client";
564
573
  * import { CartLoader } from "../loaders/cart";
565
574
  *
566
575
  * // In route definition: loader(CartLoader)
@@ -608,7 +617,7 @@ export function useLoader<T>(
608
617
  * @example On-demand fetching
609
618
  * ```tsx
610
619
  * "use client";
611
- * import { useFetchLoader } from "rsc-router/client";
620
+ * import { useFetchLoader } from "@rangojs/router/client";
612
621
  * import { SearchLoader } from "../loaders/search";
613
622
  *
614
623
  * export function SearchResults() {
@@ -670,7 +679,7 @@ export function useFetchLoader<T>(
670
679
  * @example
671
680
  * ```tsx
672
681
  * "use client";
673
- * import { useLoader, useRefreshLoaders } from "rsc-router/client";
682
+ * import { useLoader, useRefreshLoaders } from "@rangojs/router/client";
674
683
  *
675
684
  * function Profile() {
676
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) {
package/src/vite/rango.ts CHANGED
@@ -45,7 +45,7 @@ import { createVersionInjectorPlugin } from "./plugins/version-injector.js";
45
45
  import { createCjsToEsmPlugin } from "./plugins/cjs-to-esm.js";
46
46
  import { createRouterDiscoveryPlugin } from "./router-discovery.js";
47
47
  import { performanceTracksPlugin } from "./plugins/performance-tracks.js";
48
- import { createRangoDebugger, NS } from "./debug.js";
48
+ import { createCounter, createRangoDebugger, NS } from "./debug.js";
49
49
 
50
50
  const debugConfig = createRangoDebugger(NS.config);
51
51
 
@@ -100,7 +100,7 @@ export { hasUseClientDirective };
100
100
  * plugins: [
101
101
  * react(),
102
102
  * rango({ preset: 'cloudflare' }),
103
- * cloudflare({ viteEnvironment: { name: 'rsc' } }),
103
+ * cloudflare({ viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] } }),
104
104
  * ],
105
105
  * });
106
106
  * ```
@@ -565,7 +565,11 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
565
565
  plugins.push(createLoaderDirectiveGuardPlugin());
566
566
  plugins.push(createLoaderScanStubPlugin());
567
567
  plugins.push(exposeActionId());
568
- plugins.push(useCacheTransform());
568
+ plugins.push(
569
+ useCacheTransform(
570
+ createCounter(createRangoDebugger(NS.transform), "use-cache"),
571
+ ),
572
+ );
569
573
  plugins.push(exposeInternalIds());
570
574
  plugins.push(exposeRouterId());
571
575
  plugins.push(createVersionPlugin());