@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
@@ -1,12 +1,19 @@
1
1
  # Route Types
2
2
 
3
+ Route names come from the `name` option on `path()`. Names are typed in two
4
+ places: the `urls()` value itself (local names, patterns and response payloads
5
+ as phantom types) and the generated `router.named-routes.gen.ts` (the global
6
+ name map behind `Handler<"name">`, `ctx.reverse()` and `href()`). See
7
+ [`./generated-files-and-cli.md`](./generated-files-and-cli.md) for which surface
8
+ feeds which API.
9
+
3
10
  ## Route Definition with Type-Safe Names
4
11
 
5
12
  ```typescript
6
13
  // urls.tsx
7
14
  import { urls } from "@rangojs/router";
8
15
 
9
- export const urlpatterns = urls(({ path, layout }) => [
16
+ export const urlpatterns = urls(({ path }) => [
10
17
  path("/", HomePage, { name: "home" }),
11
18
  path("/products", ProductsPage, { name: "products" }),
12
19
  path("/product/:slug", ProductPage, { name: "product" }),
@@ -17,7 +24,7 @@ export const urlpatterns = urls(({ path, layout }) => [
17
24
  // Route names are inferred from the { name } option
18
25
  ```
19
26
 
20
- ## Type-Safe href()
27
+ ## Type-Safe URL Generation
21
28
 
22
29
  ### Server: ctx.reverse with route names
23
30
 
@@ -36,8 +43,15 @@ export const ProductHandler: Handler<"shop.product"> = (ctx) => {
36
43
  };
37
44
  ```
38
45
 
39
- For type-safe local names, generate a route types file with `npx rango generate urls/shop.tsx`
40
- and pass it as the second generic to `Handler` or `Prerender`:
46
+ Without a local route map, `.name` calls accept any string (the include scope
47
+ is only known at runtime). Middleware and response-route handlers are the
48
+ exception: their `ctx.reverse` (`GlobalReverseFunction`) has no include scope,
49
+ so it takes global names only and a `.name` call there is a type error. Global names are checked against the generated
50
+ map once it exists. For type-safe local names, generate a route types file with
51
+ `npx rango generate urls/shop.tsx` and pass its `routes` type as the second
52
+ generic to `Handler` or `Prerender`. With a local map, local names also
53
+ require their params (mount params such as `:tenantId` may still be passed as
54
+ extras):
41
55
 
42
56
  ```typescript
43
57
  import type { Handler } from "@rangojs/router";
@@ -61,6 +75,7 @@ import { href, useHref, Link } from "@rangojs/router/client";
61
75
  // href() validates absolute paths via PatternToPath types
62
76
  href("/about"); // Valid path
63
77
  href("/blog/hello"); // Matches /blog/:slug
78
+ href("/blog/hello?page=2#top"); // Query and hash suffixes are allowed
64
79
 
65
80
  // useHref() auto-prefixes with include() mount
66
81
  function ShopNav() {
@@ -69,6 +84,10 @@ function ShopNav() {
69
84
  }
70
85
  ```
71
86
 
87
+ `href()` is compile-time only: at runtime it returns the path unchanged. It
88
+ does not resolve route names. `useHref()` prepends the mount but its argument
89
+ is typed as any `/${string}`, not checked against routes.
90
+
72
91
  `href()` and the `Rango.Path` type read from `RegisteredRoutes` when you augment
73
92
  it, otherwise from the auto-generated `GeneratedRouteMap` — so `rango generate`
74
93
  alone type-checks `href()` paths with no manual augmentation. The augmentation
@@ -76,6 +95,7 @@ below is only needed for **`Rango.PathResponse`** (response-payload inference),
76
95
  `GeneratedRouteMap` cannot provide:
77
96
 
78
97
  ```typescript
98
+ // router.tsx, below `export const router = createRouter(...).routes(urlpatterns)`
79
99
  // The alias is required: an interface heritage clause cannot take a `typeof`
80
100
  // type query directly (TS1109).
81
101
  type AppRoutes = typeof router.routeMap;
@@ -200,15 +220,31 @@ a rename is a type error in one place instead of silent drift:
200
220
  ```ts
201
221
  import { addToCart, removeFromCart } from "./actions/cart";
202
222
  import * as CartActions from "./actions/cart";
203
-
204
- revalidate((ctx) => ctx.isAction(addToCart) || undefined); // one action
205
- revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
206
- revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any action in the module
207
- revalidate((ctx) => ctx.isAction({ addToCart, removeFromCart }) || undefined); // object form
223
+ import * as OrderActions from "./actions/order";
224
+
225
+ // On a loader: after an action, re-run only for cart actions; on navigation,
226
+ // keep the default.
227
+ revalidate((ctx) => (ctx.isAction() ? ctx.isAction(CartActions) : undefined));
228
+
229
+ // Matcher forms (each returns a boolean):
230
+ ctx.isAction(); // any action at all
231
+ ctx.isAction(addToCart); // one action
232
+ ctx.isAction(addToCart, removeFromCart); // several
233
+ ctx.isAction(CartActions); // any action in the module
234
+ ctx.isAction({ addToCart, removeFromCart }); // object form
235
+ ctx.isAction({ Cart: CartActions, Order: OrderActions }); // grouped namespaces
208
236
  ```
209
237
 
210
- `ctx.isAction()` (only available on the revalidate predicate's context) returns a
211
- raw boolean — combine with `|| undefined` for the "revalidate on match, else
212
- defer" intent. It resolves the reference the same way the router derives
213
- `actionId` (`$id` in production, `$$id` in dev), so matching
214
- works in both modes. `actionId` stays available for advanced cases.
238
+ `ctx.isAction()` (only available on the revalidate predicate's argument)
239
+ returns a raw boolean and is `false` on plain navigation. Wrap it by what the
240
+ segment's default already does: `|| undefined` adds a re-render on a parent
241
+ layout or layout-level parallel (skipped after actions by default); on a loader
242
+ or route segment, which already re-run after every action, `|| undefined`
243
+ changes nothing, so narrow with the ternary above. Avoid a bare
244
+ `ctx.isAction(X)`: it is a hard `false` on navigation too. See `/loader` →
245
+ "`revalidate()` return shapes". It resolves
246
+ the reference the same way the router derives `actionId` (`$id ?? $$id`: the
247
+ file-path `$id` injected in a production RSC build, otherwise React's `$$id`),
248
+ so matching works in dev and production. `actionId` stays available for
249
+ advanced cases. In `clientUrls()` groups the predicate runs in the browser,
250
+ where `actionId` is the hashed form (see `/client-urls`).
@@ -6,14 +6,21 @@ argument-hint: [profile-name]
6
6
 
7
7
  # "use cache" Directive
8
8
 
9
- Function-level caching for async server functions and RSC components. Caches
10
- return values with TTL + stale-while-revalidate. Complementary to the route-level
11
- `cache()` DSL and build-time `Static()`/`Prerender()`.
9
+ Function-level caching for async server functions and RSC components. Put the
10
+ directive in a function (or at the top of a module) and its return value is
11
+ cached with a TTL and a stale-while-revalidate window, keyed by its arguments.
12
+ Use it when one data fetch or one component is expensive and is reused across
13
+ routes or requests; the code around the call keeps running.
14
+
15
+ It complements the route-level `cache()` DSL (a whole segment subtree) and
16
+ build-time `Static()`/`Prerender()`. `"use cache"` needs an app-level store
17
+ (`createRouter({ cache })`); without one the function runs uncached.
12
18
 
13
19
  ## Not this skill if…
14
20
 
15
21
  - You want to cache a whole route or a rendered subtree of segments — that is
16
22
  the segment-level `cache()` DSL: see `/caching`.
23
+ - You want a value fresh on every request — use a loader (see `/loader`).
17
24
  - You are unsure which cache layer you need — start at `/cache-guide`.
18
25
 
19
26
  ## Basic Usage
@@ -56,6 +63,16 @@ export async function ProductCard({ id }: { id: string }) {
56
63
  }
57
64
  ```
58
65
 
66
+ ### Results that are not cached
67
+
68
+ A result Flight cannot encode cleanly is not cached: an async child component
69
+ that throws, a promise in the result or in a handle push that rejects, a
70
+ function or a class instance. Storing it would bake the failure into every hit
71
+ until expiry. Instead the write is skipped and reported to `onError` (phase
72
+ `"cache"`, category `cache-write`, or `stale-revalidation` for a background
73
+ refresh): a miss still returns the live result, the next call runs the
74
+ function again, and a stale entry keeps serving.
75
+
59
76
  ## Named Cache Profiles
60
77
 
61
78
  Define profiles in createRouter. Profile names map to `"use cache: <name>"` in
@@ -85,11 +102,17 @@ createRouter({
85
102
  mutation-related cached data; incidental TTL staleness on an ordinary action
86
103
  stays SWR so the action is not turned into a synchronous cache-refresh barrier.
87
104
  For strong read-your-own-writes after a mutation, prefer `updateTag()` (a hard
88
- purge, so the action's own re-render is a fresh foreground miss).
105
+ purge, so the action's own re-render is a fresh foreground miss). Only
106
+ `"use cache"` honors this flag; `cache()` boundaries and cached loaders ignore
107
+ it.
108
+ - `default` is built in as `{ ttl: 900, swr: 1800 }`; defining `default`
109
+ overrides it.
110
+ - Profile names must match `[a-zA-Z0-9_-]+`, and `ttl`/`swr` must be finite and
111
+ non-negative; `createRouter()` throws otherwise.
89
112
  - Unknown profile names throw at runtime, on the first invocation of the cached
90
113
  function (the Vite transform does not validate names at build/boot). The error
91
- is actionable -- it names the missing profile and shows the `createRouter({
92
- cacheProfiles: { ... } })` entry to add.
114
+ names the missing profile and shows the `createRouter({ cacheProfiles })` entry
115
+ to add.
93
116
 
94
117
  ## Cache Key
95
118
 
@@ -97,8 +120,20 @@ cacheProfiles: { ... } })` entry to add.
97
120
  use-cache:{functionId}:{serializedArgs}
98
121
  ```
99
122
 
100
- - `functionId` -- stable ID from Vite transform (module path + export name).
101
- - `serializedArgs` -- key-generating arguments serialized via RSC `encodeReply()`.
123
+ - `functionId` -- stable ID from Vite transform (module path + export name in
124
+ dev, a hash of both in production builds).
125
+ - `serializedArgs` -- key-generating arguments: a stable JSON encoding when every
126
+ argument is JSON-safe, otherwise serialized via RSC `encodeReply()`.
127
+
128
+ React elements (`header`, `children` slots) and client or server references
129
+ are left out of the key: the first call's rendered slot is part of the cached
130
+ output, and a hit returns it for any slot passed.
131
+
132
+ If any other argument cannot be serialized, top-level or nested (a function, a
133
+ class instance such as a database client, a symbol), the call runs uncached and
134
+ warns once per function in dev. Flight's encoder writes the same placeholder
135
+ for every such value, so keying on it would serve one caller's result to every
136
+ other caller. Pass the serializable values the function needs.
102
137
 
103
138
  When there are no key-generating arguments, the key has no trailing colon -- it is
104
139
  just `use-cache:{functionId}`.
@@ -117,16 +152,48 @@ params excluded). The same cached function called with `ctx` on different routes
117
152
  param combinations, hosts, response types, or query variants therefore produces
118
153
  distinct cache entries -- not one shared entry.
119
154
 
120
- ## Tainted Arguments (ctx, env, req)
121
-
122
- Request-scoped objects are branded with `Symbol.for('rango:nocache')` at creation.
123
- When detected:
155
+ ## Request-Scoped Arguments (ctx, Request, env)
156
+
157
+ Three request-scoped arguments are kept out of the serialized arguments:
158
+
159
+ - **`ctx`** is branded with `Symbol.for('rango:nocache')` at creation. Its route
160
+ fields are folded into the key, and its handle pushes are captured and
161
+ replayed (below). Every ctx is branded: handler, loader, middleware,
162
+ response-route and the request context. A loader ctx keys exactly like the
163
+ handler ctx of the same request. A middleware ctx keys by host, pathname,
164
+ its params and search, plus the route name once the route is matched (a
165
+ global middleware before `next()` has none yet). A ctx carrying a request
166
+ body (a fetchable loader called with `method: "POST"` and a `body` or form
167
+ data) runs the call uncached and warns in dev: the body is not in the key.
168
+ The method is not in the key either.
169
+ - **A `Request`** (such as `ctx.request`) keys by its URL: host, pathname and
170
+ the user-facing sorted search params, with the same internal-param exclusion
171
+ and `cache.searchParams` filter as the URL-keyed tiers. Headers, cookies and
172
+ the method are not in the key; read what you need outside and pass the values.
173
+ - **The request's `env`** (the object `ctx.env` returns) is left out of the key:
174
+ it is fixed per deployment. A binding passed on its own (`env.DB`) is a class
175
+ instance and runs uncached; pass `env` instead.
176
+
177
+ When a `ctx` is detected:
124
178
 
125
179
  1. **Excluded from cache key** -- request-scoped, not meaningful for keying.
126
180
  (The route-identifying fields read off `ctx` are still folded in -- see
127
181
  "Route context is folded into the key" above.)
128
- 2. **Handle data captured on miss** -- side effects via `ctx.use(Handle)` are recorded.
129
- 3. **Handle data replayed on hit** -- restored into the current request's HandleStore.
182
+ 2. **Handle data captured on miss** -- side effects via `ctx.use(Handle)` are recorded:
183
+ the function's own pushes (and those of cached functions it calls), not the
184
+ handler's or loaders' pushes into the same request.
185
+ 3. **Handle data replayed on hit** -- appended to the calling segment (for a
186
+ loader ctx, the segment that declares the loader), after what the handler
187
+ and loaders already pushed, as if the body had run (a layout and its page
188
+ calling the same function each get their copy). A
189
+ stale hit's background refresh records its pushes into the refreshed entry only.
190
+ 4. **A loader the function reads shows its pushes once** -- pushes from a loader
191
+ read with `ctx.use(Loader)` inside the function are recorded under that
192
+ loader. Loaders stay live, so if the handler or a DSL loader also reads it,
193
+ that live run's pushes are the ones on the page: a live run before the hit
194
+ makes the replay skip them, and one after it replaces the replayed values in
195
+ place. A loader read only inside the function is replayed once, also when
196
+ a layout and its page both call the function.
130
197
 
131
198
  ```typescript
132
199
  export async function getProductData(ctx) {
@@ -136,6 +203,17 @@ export async function getProductData(ctx) {
136
203
  return await db.query("SELECT * FROM products");
137
204
  }
138
205
  // On hit: return value restored, breadcrumb replayed.
206
+
207
+ export async function getProduct(ctx) {
208
+ "use cache";
209
+ const category = await ctx.use(CategoryLoader); // pushes a category crumb
210
+ return db.product(ctx.params.id, category.id);
211
+ }
212
+ // Handler: `await getProduct(ctx); await ctx.use(CategoryLoader);`
213
+ // On hit: one category crumb, from the handler's live CategoryLoader run.
214
+
215
+ export const ProductLoader = createLoader(async (ctx) => getProduct(ctx));
216
+ // A loader ctx keys like the handler ctx: one entry per route, params and query.
139
217
  ```
140
218
 
141
219
  ## Request-Scoped Guards
@@ -153,10 +231,35 @@ const locale = cookies().get("locale")?.value ?? "en";
153
231
  const data = await getCachedData(locale); // locale is now in the cache key
154
232
  ```
155
233
 
156
- The guard does not reach into LOADER bodies consumed inside the cached
157
- function (`await ctx.use(loader)`): loaders always run fresh, so their reads
158
- are exempt — but the CONSUMED VALUE is captured into the shared cache entry
159
- like any other computed data. Same rule across `cache()` and the PPR shell:
234
+ `ctx.get()` of a **non-cacheable variable** (`createVar({ cache: false })`, or
235
+ a value written with `ctx.set(key, value, { cache: false })`) throws the same
236
+ way, whether it goes through `getRequestContext().get()` or a `ctx` passed in
237
+ (handler, loader, middleware or response-route).
238
+ The key does not include the value, so the first caller's value would be
239
+ served to later callers. Read it before the call and pass it in:
240
+
241
+ ```typescript
242
+ const Tenant = createVar<string>({ cache: false });
243
+
244
+ async function getNav(tenant: string) {
245
+ "use cache";
246
+ return loadNav(tenant); // tenant is part of the key
247
+ }
248
+
249
+ // in a handler or loader, outside the cached function
250
+ const nav = await getNav(ctx.get(Tenant)!);
251
+ ```
252
+
253
+ Ordinary (cacheable) variables stay readable. Calling the cached function from
254
+ a loader does not exempt it: the loader re-runs on every request, but the
255
+ cached body does not.
256
+
257
+ The non-cacheable variable guard does not reach into LOADER bodies consumed
258
+ inside the cached function (`await ctx.use(loader)`): loaders always run
259
+ fresh, so their variable reads are exempt — but the CONSUMED VALUE is captured
260
+ into the shared cache entry like any other computed data. (`cookies()` and
261
+ `headers()` still throw there: that guard follows the cached body's whole
262
+ async chain, loaders included.) Same rule across `cache()` and the PPR shell:
160
263
  handler/cached-scope consumption = baked copy, client-side `useLoader` = live
161
264
  (the consumption-lane rule, `/rango` → Invariants).
162
265
 
@@ -165,15 +268,17 @@ handler/cached-scope consumption = baked copy, client-side `useLoader` = live
165
268
  These ctx methods **throw** inside a `"use cache"` function because their effects
166
269
  are lost on cache hit (the function body is skipped):
167
270
 
168
- - `ctx.set()` for passing values to children
169
- - `ctx.header()`
271
+ - `ctx.set()` for passing values to children (handler and middleware ctx)
272
+ - `ctx.headers.set()` and the other mutating `Headers` methods (handler and
273
+ middleware ctx), and a middleware's `ctx.header()`
274
+ - cookie writes and the request-context writers `header()`, `setCookie()`,
275
+ `deleteCookie()`, `setStatus()`, `onResponse()`
170
276
  - `ctx.setTheme()`
171
277
  - `ctx.setLocationState()`
172
- - `ctx.onResponse()`
173
278
 
174
- `ctx.get()` is **not** exec-guarded inside `"use cache"` -- it is a read, so it is
175
- safe. (It only throws when reading a non-cacheable variable inside the separate
176
- route-level `cache()` DSL boundary.)
279
+ `ctx.get()` is a read, not a side effect: it stays allowed for ordinary
280
+ variables and throws only for a non-cacheable one (see "Read Guards" above).
281
+ The same non-cacheable read throws inside a route-level `cache()` boundary.
177
282
 
178
283
  The error message recommends two alternatives:
179
284
 
@@ -257,9 +362,8 @@ On cache miss, the function executes and the result is serialized inline (blocki
257
362
  The cache **store write** (`setItem`) is deferred to `waitUntil` and does NOT block
258
363
  the response.
259
364
 
260
- On stale hit, stale data is returned immediately. Background revalidation (re-execute
261
-
262
- - store) runs entirely inside `waitUntil`.
365
+ On stale hit, stale data is returned immediately. Background revalidation
366
+ (re-execute + store write) runs entirely inside `waitUntil`.
263
367
 
264
368
  | Phase | Blocks response? |
265
369
  | ------------------------------------ | ---------------- |
@@ -356,75 +460,89 @@ decrypt; without it they fail until the entry expires or revalidates.
356
460
 
357
461
  ## Vite Transform
358
462
 
359
- The `rango:use-cache` Vite plugin detects the directive and wraps exports with
360
- `registerCachedFunction()`:
463
+ The `@rangojs/router:use-cache` Vite plugin (part of `rango()`) detects the directive and wraps each cached
464
+ function with `registerCachedFunction(fn, functionId, profileName)` from
465
+ `@rangojs/router/cache-runtime`. You never call it yourself. Simplified output:
361
466
 
362
467
  ```typescript
363
- // Input
364
- "use cache"
468
+ // Input (file-level directive)
469
+ "use cache";
365
470
  export async function getProducts() { ... }
366
471
 
367
- // Output
368
- import { registerCachedFunction } from '@rangojs/router/cache-runtime';
472
+ // Output (simplified)
473
+ import { registerCachedFunction } from "@rangojs/router/cache-runtime";
369
474
  export const getProducts = registerCachedFunction(
370
475
  async function getProducts() { ... },
371
- "src/data/products.ts#getProducts",
372
- "default"
476
+ "src/data/products.ts#getProducts", // a hash of this in production builds
477
+ "default",
373
478
  );
374
479
  ```
375
480
 
376
- Function-level directives are hoisted:
481
+ A function-level directive (`"use cache: short"` inside the body) is hoisted out
482
+ of the function and wrapped the same way, with the profile name from the
483
+ directive.
377
484
 
378
- ```typescript
379
- // Input
380
- export async function getProducts() {
381
- "use cache: short";
382
- return await db.query("...");
383
- }
485
+ File-level rules:
384
486
 
385
- // Output
386
- const __rango_cached_getProducts = registerCachedFunction(
387
- async function getProducts() {
388
- return await db.query("...");
389
- },
390
- "src/data/products.ts#getProducts",
391
- "short",
392
- );
393
- export async function getProducts() {
394
- return __rango_cached_getProducts();
395
- }
396
- ```
487
+ - Only exports that are statically confirmed functions are wrapped
488
+ (`export async function foo() {}`, `export const foo = async () => {}`). Any
489
+ other export (a constant, or a factory call like `makeCached(fn)`) fails the
490
+ build — move it to another module.
491
+ - The default export of a file named `layout.tsx` or `template.tsx` (any
492
+ `.ts/.tsx/.js/.jsx` extension) is not wrapped: it receives `children`, which
493
+ cannot be part of a cache key.
494
+ - Functions carrying their own `"use server"` directive are server actions, not
495
+ cached functions, and are left alone.
397
496
 
398
497
  ## Backing Store
399
498
 
400
- Writes to the same `SegmentCacheStore` as `cache()` DSL, `Static()`, and `Prerender()`.
401
- One store, one configuration.
402
-
403
- Cache entries (and `cacheProfiles`) can be tagged via `cache({ tags })` or runtime
404
- `cacheTag(...tags)`. `cacheTag` has two forms: inside a `"use cache"` function it
405
- tags that entry; called during a request render outside `"use cache"` it tags the
406
- request's document/shell artifact (rides `_requestTags`) instead of throwing. The
407
- built-in
408
- `MemorySegmentCacheStore` and `CFCacheStore` index by tag. Invalidate on demand
409
- with `updateTag(...tags)` (awaitable, read-your-own-writes; for server actions) or
410
- `revalidateTag(...tags)` (background, non-blocking; for route handlers/webhooks).
411
- Both hard-purge; the difference is awaitability, not stale-serving. For
412
- `CFCacheStore`, distributed invalidation needs a `kv` namespace (markers live in
413
- that same namespace). The separate `revalidate()` export is the client-update axis
414
- (which segments re-render on a navigation or action), not a cache bust.
499
+ `"use cache"` reads and writes the app-level store from `createRouter({ cache })`
500
+ — the same store `cache()` boundaries and cached loaders use. One store, one
501
+ configuration. (`Static()`/`Prerender()` output is not in this store; it is
502
+ built into the server bundle, see `/prerender`.)
503
+
504
+ Entries are tagged by the profile's `tags` plus any runtime `cacheTag(...tags)`
505
+ calls in the function body. `cacheTag` has two forms: inside a `"use cache"`
506
+ function it tags that entry; called during a request render outside
507
+ `"use cache"` it tags the request's `ppr` shell / document-cache entry instead
508
+ (see `/caching` → "Tag-Based Invalidation"). All built-in stores
509
+ (`MemorySegmentCacheStore`, `CFCacheStore`, `VercelCacheStore`) index by tag.
510
+ Invalidate on demand with `updateTag(...tags)` (awaitable,
511
+ read-your-own-writes; for server actions) or `revalidateTag(...tags)`
512
+ (background, non-blocking; for route handlers/webhooks). Both hard-purge; the
513
+ difference is awaitability, not stale-serving. For `CFCacheStore`, cross-colo
514
+ invalidation needs a `kv` namespace (markers live in that same namespace) or
515
+ `tagPurge`. The separate `revalidate()` export is the client-update axis (which
516
+ segments re-render on a navigation or action), not a cache bust.
517
+
518
+ A `"use cache"` entry's tags do not propagate to an enclosing `cache()` segment
519
+ entry: invalidating them re-runs the function on the next miss of the outer
520
+ boundary, not before (see `/cache-guide` → "Nesting rule").
415
521
 
416
522
  ## Interaction with Other Caching
417
523
 
418
- | Mechanism | Granularity | When | Use case |
419
- | -------------------- | ------------------ | ---------- | ----------------------------------------------- |
420
- | `"use cache"` | Function/component | Runtime | Cache individual data fetches or components |
421
- | `cache()` DSL | Route segment | Runtime | Cache entire route subtrees with children |
422
- | `cache({ ttl })` DSL | Route segment | Runtime | Cache a route subtree with explicit options |
423
- | `Static()` | Route segment | Build-time | Render once, never re-render |
424
- | `Prerender()` | Route segment | Build-time | Pre-render known params, optional live fallback |
425
-
426
- ## Dev Mode
427
-
428
- In development, the Vite transform still wraps functions, but the cache store is
429
- a `MemorySegmentCacheStore` that works locally. Functions cache normally in dev
430
- for testing cache behavior.
524
+ | Mechanism | Granularity | When | Use case |
525
+ | ---------------- | ------------------ | ---------- | ----------------------------------------------------- |
526
+ | `"use cache"` | Function/component | Runtime | Cache individual data fetches or components |
527
+ | loader `cache()` | One loader's data | Runtime | Cache a loader result while the page stays live |
528
+ | `cache()` DSL | Route segment tree | Runtime | Cache entire route subtrees with children |
529
+ | `ppr` | HTML shell | Runtime | Serve a cached HTML shell, resume live holes (`/ppr`) |
530
+ | `Static()` | One segment | Build-time | Render once at build, no params |
531
+ | `Prerender()` | Route segment tree | Build-time | Pre-render known params, optional live fallback |
532
+
533
+ Inside a `ppr` shell, a `"use cache"` value that renders as shell material is
534
+ pinned at capture time for the life of that shell (see `/ppr` → Pitfalls).
535
+
536
+ ## Dev Mode and tests
537
+
538
+ The transform runs in dev exactly as in production, but caching only happens
539
+ when the router has a store: there is no implicit dev store. Configure one (a
540
+ `MemorySegmentCacheStore` is the usual dev/test choice) to see cache hits
541
+ locally.
542
+
543
+ With no item-capable store configured, a `"use cache"` function simply runs on
544
+ every call. The request-scoped guards (`cookies()`, `ctx.set()`, …) are not
545
+ active on that uncached path, so a guard violation only shows up once a store is
546
+ configured — test with a store. Under Vitest, `runLoader`, `renderHandler`, and
547
+ the other `@rangojs/router/testing` helpers take `{ cacheStore, cacheProfiles }`
548
+ and warn when a cached function ran without one (see `/testing`).
@@ -6,7 +6,11 @@ argument-hint:
6
6
 
7
7
  # Vercel deployment
8
8
 
9
- The `vercel` preset builds like the `node` preset (Vercel runs Node Functions, not Workers): rango owns the RSC entry, folds `process.env.NODE_ENV` for the SSR/RSC build, and after `vite build` assembles a `.vercel/output` directory (Build Output API v3) from `dist/` — a single streaming Node Function plus the static client assets.
9
+ This skill covers deploying a Rango app to Vercel Functions: the `vercel`
10
+ preset, function settings, the Runtime Cache store, host routers, tracing, and
11
+ local validation. For router options in general see `/router-setup`.
12
+
13
+ The `vercel` preset builds like the `node` preset (Vercel runs Node Functions, not Workers): rango owns the RSC entry (it imports the named `router` export of your `createRouter()` module), folds `process.env.NODE_ENV` for the SSR/RSC build, and after `vite build` assembles a `.vercel/output` directory (Build Output API v3) from `dist/`: a single streaming Node Function plus the static client assets.
10
14
 
11
15
  ## Deployment boundary
12
16
 
@@ -30,27 +34,27 @@ checklist before adding shared-cache headers.
30
34
  ## Setup
31
35
 
32
36
  ```bash
33
- npm install @vercel/functions
37
+ pnpm add @vercel/functions
34
38
  ```
35
39
 
36
40
  ```typescript
37
41
  // vite.config.ts
38
42
  import { defineConfig } from "vite";
39
- import react from "@vitejs/plugin-react";
40
43
  import { rango } from "@rangojs/router/vite";
41
44
 
42
45
  export default defineConfig({
43
- plugins: [react(), rango({ preset: "vercel" })],
46
+ plugins: [rango({ preset: "vercel" })], // add react() first if you use @vitejs/plugin-react
44
47
  });
45
48
  ```
46
49
 
47
50
  `@vercel/functions` is required: it backs the generated function launcher (`waitUntil`) and `VercelCacheStore`. The build fails with a clear error if it is missing.
48
51
 
49
- `vite build` produces `.vercel/output`; deploy with the Vercel CLI (`vercel deploy --prebuilt`) or via Git integration.
52
+ `vite build` produces `.vercel/output`; deploy with the Vercel CLI (`vercel deploy --prebuilt`) or via Git integration. The generated routing serves files first (hashed assets under the assets directory get `Cache-Control: public, max-age=31536000, immutable`) and sends everything else to the function.
50
53
 
51
54
  ## Function configuration
52
55
 
53
- Per-function knobs go under `vercel` and are written into `.vc-config.json`:
56
+ Per-function knobs go under `vercel` and are written into `.vc-config.json`
57
+ (`functionName` also sets the `config.json` route):
54
58
 
55
59
  ```typescript
56
60
  rango({
@@ -60,11 +64,15 @@ rango({
60
64
  maxDuration: 30, // seconds, default
61
65
  memory: 1024, // MB (platform default when omitted)
62
66
  regions: ["fra1"], // pin regions (platform default when omitted)
63
- functionName: "index", // the <name>.func dir + config.json route
67
+ functionName: "index", // the <name>.func dir + config.json route (default)
64
68
  },
65
69
  });
66
70
  ```
67
71
 
72
+ `runtime` must be a `nodejs*` runtime. The preset emits a Node serverless
73
+ function, so the build fails on any other value; the Edge runtime is not
74
+ supported.
75
+
68
76
  ## Runtime Cache
69
77
 
70
78
  `VercelCacheStore` wraps the Vercel Runtime Cache for segment, item, response,
@@ -72,6 +80,7 @@ and PPR shell families. Locally (no `process.env.VERCEL`) fall back to an
72
80
  in-memory store so dev/preview work without the platform:
73
81
 
74
82
  ```typescript
83
+ import { createRouter } from "@rangojs/router";
75
84
  import {
76
85
  MemorySegmentCacheStore,
77
86
  VercelCacheStore,
@@ -97,7 +106,7 @@ function resolveCache() {
97
106
  export const router = createRouter({ cache: resolveCache }).routes(/* ... */);
98
107
  ```
99
108
 
100
- The cache factory receives `(env, ctx)`; on Vercel `env` is `process.env` and `ctx` is `{ waitUntil }`.
109
+ The cache factory receives `(env, ctx)`: on Vercel `env` is `process.env` and `ctx` is `{ waitUntil }`; off-platform `ctx` is `undefined`. Cache semantics and tag invalidation: `/caching`, `/cache-guide`.
101
110
 
102
111
  ## Host routers (multi-app)
103
112
 
@@ -121,7 +130,7 @@ hostRouter.host(["."]).lazy(() => import("./apps/site/handler.js"));
121
130
  export default hostRouter; // the instance
122
131
  ```
123
132
 
124
- `{ env, ctx }` is threaded unchanged from the function to each matched sub-app's handler and its `cache(env, ctx)` factory. See the `host-router` skill for sub-app structure and routing patterns.
133
+ `{ env, ctx }` is threaded unchanged from the function to each matched sub-app's handler and its `cache(env, ctx)` factory. See `/host-router` for sub-app structure and routing patterns.
125
134
 
126
135
  ## Tracing (custom spans)
127
136
 
@@ -138,12 +147,13 @@ registerOTel({ serviceName: "my-app" });
138
147
  export const tracing = createVercelTracing();
139
148
 
140
149
  // router.tsx — importing `tracing` runs instrumentation.ts (and registerOTel)
150
+ import { createRouter } from "@rangojs/router";
141
151
  import { tracing } from "./instrumentation.js";
142
152
  export const router = createRouter({ tracing }).routes(/* ... */);
143
153
  ```
144
154
 
145
- `createVercelTracing(opts?)` takes `{ enabled, spans, tracerName, tracer }` — same phase set as `createCloudflareTracing` (`rango.request/middleware/action/loader/render/ssr`). Caveats: Node-runtime only (Vercel custom spans are unsupported on Edge); `registerOTel()` must run before the first request; `@vercel/otel` is what unlocks Vercel Session Tracing + Trace Drains. The deploy bundles `@vercel/otel` and its `@opentelemetry/*` peers into the function (no `node_modules` at runtime), so they must be installed. See `examples/vercel-basic` for a worked hybrid setup and the `observability` skill for the cross-platform tracing model.
155
+ `createVercelTracing(opts?)` takes `{ enabled, spans, tracerName, tracer }` (`tracerName` defaults to `"rango"`; `tracer` overrides the global tracer) and emits the same phases as `createCloudflareTracing`: `rango.request`, `rango.middleware`, `rango.action`, `rango.loader`, `rango.handler`, `rango.render`, `rango.ssr`, `rango.response`, and `rango.background`. Caveats: Node-runtime only (Vercel custom spans are unsupported on Edge); `registerOTel()` must run before the first request; `@vercel/otel` is what unlocks Vercel Session Tracing + Trace Drains. The deploy bundles `@vercel/otel` and its `@opentelemetry/*` peers into the function (no `node_modules` at runtime), so they must be installed. The Rango repository's `examples/vercel-basic` (not shipped in this package) has a worked setup; `/observability` covers the cross-platform tracing model.
146
156
 
147
157
  ## Local validation without deploying
148
158
 
149
- `vite preview` serves the static client assets only. To preview the RSC **function**, serve the assembled `.vercel/output` behind filesystem-then-function routing — `examples/vercel-basic/scripts/preview.mjs` does this (and `pnpm preview:vercel` runs it). For a faithful deploy test (isolated filesystem, ESM, self-contained bundle), `examples/vercel-basic/scripts/smoke.mjs` serves it from a temp dir outside the repo. Both share `scripts/serve-vercel-output.mjs`.
159
+ `vite preview` does not run the assembled function. To exercise it locally, serve `.vercel/output` with filesystem-then-function routing (static files first, everything else to `functions/<name>.func/index.mjs`). The Rango repository's `examples/vercel-basic` (not shipped in this package) has two scripts for this: `scripts/preview.mjs` (`pnpm preview:vercel`) serves it in place, and `scripts/smoke.mjs` copies it to a temp dir outside the repo for a faithful deploy test (isolated filesystem, ESM, self-contained bundle). Both share `scripts/serve-vercel-output.mjs`.