@rangojs/router 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- package/dist/types/cache/cache-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +98 -14
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-snapshot.d.ts +4 -3
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/types.d.ts +207 -43
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +40 -11
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +21 -10
- package/dist/types/router/segment-resolution/mask-nested.d.ts +65 -24
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +53 -6
- package/dist/types/rsc/shell-capture.d.ts +146 -15
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/types.d.ts +7 -0
- package/dist/types/server/context.d.ts +109 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/request-context.d.ts +159 -33
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- package/dist/types/types/handler-context.d.ts +14 -1
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +46 -9
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +123 -51
- package/skills/caching/SKILL.md +253 -23
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +11 -6
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +44 -32
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +8 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/state.md +3 -1
- package/skills/loader/SKILL.md +161 -39
- package/skills/middleware/SKILL.md +16 -9
- package/skills/migrate-nextjs/SKILL.md +67 -36
- package/skills/migrate-react-router/SKILL.md +2 -1
- package/skills/migrate-react-router/component-migration.md +3 -1
- package/skills/migrate-react-router/data-and-actions.md +3 -1
- package/skills/migrate-react-router/route-mapping.md +7 -4
- package/skills/observability/SKILL.md +64 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +655 -363
- package/skills/prerender/SKILL.md +27 -14
- package/skills/rango/SKILL.md +52 -33
- package/skills/route/SKILL.md +12 -7
- package/skills/router-setup/SKILL.md +4 -2
- package/skills/scripts/SKILL.md +8 -1
- package/skills/server-actions/SKILL.md +11 -0
- package/skills/shell-manifest/SKILL.md +21 -12
- package/skills/testing/SKILL.md +25 -21
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/loader.md +18 -1
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/theme/SKILL.md +52 -4
- package/skills/typesafety/env-and-bindings.md +10 -2
- package/skills/use-cache/SKILL.md +39 -16
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/rsc-router.tsx +5 -0
- package/src/cache/cache-key-utils.ts +22 -2
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +34 -7
- package/src/cache/cache-scope.ts +345 -33
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/handle-snapshot.ts +4 -3
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +104 -19
- package/src/cache/types.ts +211 -43
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/prerender/build-shell-capture.ts +65 -11
- package/src/route-definition/dsl-helpers.ts +4 -3
- package/src/router/handler-context.ts +38 -4
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +19 -7
- package/src/router/loader-resolution.ts +63 -32
- package/src/router/match-api.ts +28 -9
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/cache-lookup.ts +17 -0
- package/src/router/match-middleware/cache-store.ts +31 -11
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +23 -4
- package/src/router/prerender-match.ts +6 -0
- package/src/router/segment-resolution/fresh.ts +18 -9
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +227 -56
- package/src/router/segment-resolution/loader-snapshot.ts +203 -51
- package/src/router/segment-resolution/mask-nested.ts +136 -42
- package/src/router/segment-resolution/revalidation.ts +27 -10
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/rsc-rendering.ts +601 -181
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +60 -6
- package/src/rsc/shell-capture.ts +1001 -250
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/types.ts +7 -0
- package/src/server/context.ts +219 -28
- package/src/server/cookie-store.ts +87 -62
- package/src/server/request-context.ts +263 -38
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/run-loader.ts +63 -5
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/types/handler-context.ts +14 -1
- package/src/urls/pattern-types.ts +22 -18
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/router-discovery.ts +14 -4
|
@@ -20,8 +20,9 @@ The handler code and its imports are removed from the production bundle.
|
|
|
20
20
|
|
|
21
21
|
## Not this skill if…
|
|
22
22
|
|
|
23
|
-
- You want a cached HTML shell captured at runtime, with holes
|
|
24
|
-
|
|
23
|
+
- You want a cached HTML shell captured at runtime, with holes staying live
|
|
24
|
+
per request (loaders without `ssr: false` run per request; `ssr: false`
|
|
25
|
+
loaders bake) — see `/ppr`.
|
|
25
26
|
- You want runtime segment caching with TTL/SWR — that is the `cache()` DSL:
|
|
26
27
|
see `/caching`. Prerender is the same cache filled at build time.
|
|
27
28
|
- You are unsure which cache layer you need — start at `/cache-guide`.
|
|
@@ -244,15 +245,14 @@ In client and SSR environments, ALL prerender handlers are always stubbed.
|
|
|
244
245
|
## Sub-use Semantics
|
|
245
246
|
|
|
246
247
|
Everything inside the path's use() callback is part of the pre-rendered route
|
|
247
|
-
subtree
|
|
248
|
-
the exception — they stay live:
|
|
248
|
+
subtree and gets pre-rendered. Loaders are the exception — they stay live:
|
|
249
249
|
|
|
250
250
|
```typescript
|
|
251
251
|
path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
|
|
252
|
-
layout(<PostLayout />, () => [ // inside
|
|
252
|
+
layout(<PostLayout />, () => [ // inside the route -> pre-rendered
|
|
253
253
|
loader(PostMetaLoader), // live at runtime, bundled normally
|
|
254
254
|
]),
|
|
255
|
-
parallel({ "@sidebar": BlogSidebar }), // inside
|
|
255
|
+
parallel({ "@sidebar": BlogSidebar }), // inside the route -> pre-rendered
|
|
256
256
|
])
|
|
257
257
|
```
|
|
258
258
|
|
|
@@ -327,9 +327,16 @@ implicitly re-run outer prerender-derived handlers/layouts.
|
|
|
327
327
|
## Prerender + PPR Build Shells
|
|
328
328
|
|
|
329
329
|
A `Prerender` page may also declare `ppr` on the path option. The build still
|
|
330
|
-
stores the Flight payload first. After that,
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
stores the Flight payload first. After that, the build-time shell capture
|
|
331
|
+
tries to bake the HTML shell for each generated URL so the first document
|
|
332
|
+
request can be an `x-rango-shell: HIT`. A route partitioned by
|
|
333
|
+
`cache({ key })` or a store `keyGenerator` never serves that build shell (the
|
|
334
|
+
build captured one partition); each partition captures at runtime, a
|
|
335
|
+
once-per-route warning says so, and a `keyGenerator` that returns the default
|
|
336
|
+
key unchanged keeps it. When the route's `cache()` scope (its own, or one
|
|
337
|
+
inherited from a layout) refuses a request — `cache(false)`, or a
|
|
338
|
+
`condition()` that returns false — that request gets no shell at all, the
|
|
339
|
+
build shell included: it renders like a cache miss.
|
|
333
340
|
|
|
334
341
|
That shell capture is request-shaped enough to run middleware safely:
|
|
335
342
|
|
|
@@ -362,14 +369,15 @@ entry STILL serves and a runtime recapture is scheduled that upgrades it in plac
|
|
|
362
369
|
(SWR is the upgrade path from build entry → fresher runtime entry). Because the
|
|
363
370
|
`Prerender` handler is evicted from the production bundle, that recapture never
|
|
364
371
|
re-runs the handler — it replays the same build-time segments and only refreshes
|
|
365
|
-
`cache()`-scoped data baked into the shell
|
|
366
|
-
`
|
|
372
|
+
what the capture reads again: `cache()`-scoped data baked into the shell, and
|
|
373
|
+
the data of `ssr: false` loaders, which run at every capture. **If the shell
|
|
374
|
+
bakes neither, `ttl` has nothing to refresh** — reach for `updateTag` (or a
|
|
367
375
|
redeploy) instead of a shorter `ttl`.
|
|
368
376
|
|
|
369
|
-
`ctx.dynamic()` opts a request off the shell
|
|
377
|
+
`ctx.dynamic()` opts a request off the shell ONLY. A `Prerender` route has
|
|
370
378
|
no live handler to fall back to (it was evicted), so a `dynamic()` request still
|
|
371
|
-
serves the build-baked segments — fresh loaders
|
|
372
|
-
|
|
379
|
+
serves the build-baked segments — fresh loaders, not a fresh handler render.
|
|
380
|
+
There is no "fully dynamic" render for a prerendered route.
|
|
373
381
|
|
|
374
382
|
## Dev Mode
|
|
375
383
|
|
|
@@ -635,6 +643,11 @@ Passthrough entries are logged distinctly:
|
|
|
635
643
|
Loaders on pre-rendered routes run at request time. They are bundled normally
|
|
636
644
|
and need `cache()` for caching. Do not use build-only APIs in loaders.
|
|
637
645
|
|
|
646
|
+
The exception is an `ssr: false` loader on a `Prerender` + `ppr` route: the
|
|
647
|
+
build-time shell capture runs it and bakes its settled data into the shell,
|
|
648
|
+
and a shell HIT runs it only when its return carries promises (`/ppr` → "The
|
|
649
|
+
bake lane"). The Flight payload collection itself still skips loaders.
|
|
650
|
+
|
|
638
651
|
### Build-time handle data is frozen
|
|
639
652
|
|
|
640
653
|
Handle values pushed via `ctx.use()` DURING pre-rendering (handler pushes at
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -26,7 +26,9 @@ with the shape, then pick a primitive.
|
|
|
26
26
|
inside a cached render. They run **in parallel** right after middleware and
|
|
27
27
|
**stream**, so data latency overlaps first paint instead of blocking it (a
|
|
28
28
|
cache hit streams UI instantly while loaders resolve fresh alongside). Opt into
|
|
29
|
-
caching explicitly. See `/loader` → "Parallel and streaming".
|
|
29
|
+
caching explicitly. See `/loader` → "Parallel and streaming". The one
|
|
30
|
+
exception is a `ppr` route's `ssr: false` loader: it bakes into the shell
|
|
31
|
+
and runs on a shell HIT only when its return carries promises (`/ppr`).
|
|
30
32
|
- **One identity, one store** — loaders, handles, cached fns, and actions are all
|
|
31
33
|
`path#export`; all caches share one store. Entries expire by TTL/SWR, and are
|
|
32
34
|
tagged via `cache({ tags })` or runtime `cacheTag(...tags)`; built-in stores
|
|
@@ -68,8 +70,9 @@ Reach for the next rung only when the one above doesn't fit — the higher rungs
|
|
|
68
70
|
are immune to partial-revalidation staleness by construction.
|
|
69
71
|
|
|
70
72
|
1. **A loader** (`loader()` + `useLoader()`). Loaders resolve fresh on every
|
|
71
|
-
pass — full renders, action revalidations, cache hits
|
|
72
|
-
|
|
73
|
+
pass — full renders, action revalidations, cache hits, PPR shell HITs (on a
|
|
74
|
+
`ppr` route, keep it off `ssr: false`, which bakes it into the shell).
|
|
75
|
+
Nothing to keep in sync. If the data can be a loader, make it a loader.
|
|
73
76
|
2. **Middleware `ctx.set()`**. Route middleware wraps every render pass,
|
|
74
77
|
including post-action revalidation and PE re-renders, so its variables are
|
|
75
78
|
never stale. Right for request-shaped context: auth, session, locale.
|
|
@@ -114,7 +117,7 @@ stated, greppable contract.
|
|
|
114
117
|
| guarantee loader output in the SSR HTML | `loader(L, { ssr: false })` | /loader |
|
|
115
118
|
| pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
|
|
116
119
|
| feed live loaders from a cached shell | replayed handle + `ctx.rendered()` | /shell-manifest |
|
|
117
|
-
| cache
|
|
120
|
+
| cache HTML shell, live loaders as holes | `ppr` path option | /ppr |
|
|
118
121
|
| choose in-function vs CDN caching | deployment cache boundary | /deployment-caching |
|
|
119
122
|
| stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()` | /streams-and-websockets |
|
|
120
123
|
|
|
@@ -123,15 +126,26 @@ stated, greppable contract.
|
|
|
123
126
|
- `path()`/`include()` are always visible in `urls()`; config helpers are extractable.
|
|
124
127
|
- **Cache decides freshness; `revalidate()` decides client-update.** Orthogonal; compose.
|
|
125
128
|
- Loaders resolve fresh every request (even inside `cache()`) and never run twice/request.
|
|
129
|
+
The exception is a `ppr` route's `ssr: false` loader: it bakes into the shell
|
|
130
|
+
and runs on a shell HIT only when its return carries promises.
|
|
126
131
|
- **The consumption-lane rule.** For every shared artifact (`cache()`,
|
|
127
132
|
`"use cache"`, the PPR shell): server-side handler consumption
|
|
128
|
-
(`await ctx.use(loader)`) yields a BAKED copy —
|
|
129
|
-
(`cookies()`/`headers()
|
|
130
|
-
|
|
131
|
-
"
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
(`await ctx.use(loader)`) yields a BAKED copy — the capture-time value
|
|
134
|
+
freezes into the shared artifact. Identity reads (`cookies()`/`headers()`,
|
|
135
|
+
a `{ cache: false }` variable) inside that loader are permitted under
|
|
136
|
+
`cache()`/`"use cache"` (a documented footgun; see `/caching` → "Cache
|
|
137
|
+
purity & tainted objects") but REFUSE a PPR capture: the route serves
|
|
138
|
+
uncached until the read moves to a live loader. Client-side consumption
|
|
139
|
+
(`useLoader` in a `"use client"` component) is the LIVE lane. DSL
|
|
140
|
+
`loader()` segments follow their PPR lane (only `ssr: false` bakes; see
|
|
141
|
+
`/ppr` → The loader lane rule).
|
|
142
|
+
- **A PPR shell HIT never runs a handler.** Everything a handler produces
|
|
143
|
+
(promises it passes under `<Suspense>`, async server components, handle
|
|
144
|
+
pushes, loaders it awaits) is shell material, as under `cache()`; a HIT
|
|
145
|
+
runs middleware and live loaders only (an `ssr: false` loader runs there
|
|
146
|
+
only when its return carries promises). Live data belongs in a loader without
|
|
147
|
+
`ssr: false`, read with `useLoader` under `loading()` or an inline
|
|
148
|
+
`<Suspense>`.
|
|
135
149
|
- Inside `"use cache"`: `cookies()`/`headers()` and `ctx` side-effects
|
|
136
150
|
(`set`/`header`/`setTheme`/`onResponse`/`setLocationState`) throw; `ctx.use(Handle)`
|
|
137
151
|
is captured on miss and replayed on hit. A non-cacheable variable read
|
|
@@ -162,17 +176,22 @@ stated, greppable contract.
|
|
|
162
176
|
Same words, different jobs — this is the most common source of the
|
|
163
177
|
`revalidate()`-is-caching misread.
|
|
164
178
|
|
|
165
|
-
| You may know | Maps to Rango
|
|
166
|
-
| --------------------------------------- |
|
|
167
|
-
| Next.js `export const revalidate = N` | **
|
|
168
|
-
| Next.js `revalidateTag` / `updateTag` | **
|
|
169
|
-
| React Router / Remix `shouldRevalidate` | **
|
|
170
|
-
| HTTP `Cache-Control` / ISR | Deployment layer
|
|
171
|
-
| Next.js PPR (partial prerendering) | HTML shell layer
|
|
172
|
-
| Remix/RR `loader` | live data
|
|
179
|
+
| You may know | Maps to Rango | Watch out |
|
|
180
|
+
| --------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
181
|
+
| Next.js `export const revalidate = N` | **Value freshness** (cache) | Same word, opposite meaning. Next's `revalidate` is time-based cache expiry; Rango's `revalidate()` is **partial-render selection**. Use `cache({ ttl })` for the Next behavior. |
|
|
182
|
+
| Next.js `revalidateTag` / `updateTag` | **Value freshness** (cache) | Cache busting by tag. Tag via `cache({ tags })` / `cacheTag(...tags)`; invalidate with `updateTag(...tags)` (awaitable, read-your-own-writes) or `revalidateTag(...tags)` (background, non-blocking). Built-in stores index by tag. No `revalidatePath` (path-based busting); use tags. |
|
|
183
|
+
| React Router / Remix `shouldRevalidate` | **Partial-render selection** | This is the correct mental model for Rango's `revalidate()`. |
|
|
184
|
+
| HTTP `Cache-Control` / ISR | Deployment layer | Complete-response deployment layer. A CDN hit bypasses Rango entirely; the store-backed middleware does not. See `/deployment-caching` and `/document-cache`. |
|
|
185
|
+
| Next.js PPR (partial prerendering) | HTML shell layer | Same React primitive, different transport: Rango serves shells in-function after middleware. Ordinary `ppr` captures at runtime; `Prerender + ppr` captures at build. See `/ppr`, `/prerender`, and `/deployment-caching`. |
|
|
186
|
+
| Remix/RR `loader` | live data | Like Rango loaders, fresh per request — but Rango loaders run in parallel and stream (latency overlaps first paint), and can opt into caching on demand. |
|
|
173
187
|
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
Next.js PPR and Rango's `ppr` also differ in the hole model: in Rango only
|
|
189
|
+
loaders make holes. A handler's own promise under `<Suspense>` bakes into the
|
|
190
|
+
shell, as under `cache()`.
|
|
191
|
+
|
|
192
|
+
See `/cache-guide` for the cache decision guide, `/loader` and `/route` for
|
|
193
|
+
`revalidate()` (partial-render selection), and `/document-cache` for the edge
|
|
194
|
+
layer.
|
|
176
195
|
|
|
177
196
|
## Canonical shape
|
|
178
197
|
|
|
@@ -277,18 +296,18 @@ Grouped by concern — read when you need to…
|
|
|
277
296
|
|
|
278
297
|
**Data & caching** — fetch, mutate, and cache:
|
|
279
298
|
|
|
280
|
-
| Skill | Description
|
|
281
|
-
| --------------------- |
|
|
282
|
-
| `/loader` | Data loaders with `createLoader()` and `revalidate()`
|
|
283
|
-
| `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation
|
|
284
|
-
| `/caching` | Segment caching with memory or KV stores
|
|
285
|
-
| `/use-cache` | Function-level caching with `"use cache"` directive
|
|
286
|
-
| `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide
|
|
287
|
-
| `/document-cache` | Store-backed complete-response middleware using Cache-Control policy
|
|
288
|
-
| `/deployment-caching` | Choose between in-function caches, store-backed responses, and an external CDN cache
|
|
289
|
-
| `/ppr` | PPR shell caching: cached shell served instantly, live holes resumed — a hole is a `loading()`
|
|
290
|
-
| `/prerender` | Pre-render route segments at build time (Passthrough live fallback)
|
|
291
|
-
| `/shell-manifest` | Replayed handles as cache metadata read by live loaders (frozen shell, batched live holes)
|
|
299
|
+
| Skill | Description |
|
|
300
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
301
|
+
| `/loader` | Data loaders with `createLoader()` and `revalidate()` |
|
|
302
|
+
| `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation |
|
|
303
|
+
| `/caching` | Segment caching with memory or KV stores |
|
|
304
|
+
| `/use-cache` | Function-level caching with `"use cache"` directive |
|
|
305
|
+
| `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
|
|
306
|
+
| `/document-cache` | Store-backed complete-response middleware using Cache-Control policy |
|
|
307
|
+
| `/deployment-caching` | Choose between in-function caches, store-backed responses, and an external CDN cache |
|
|
308
|
+
| `/ppr` | PPR shell caching: cached shell served instantly, live holes resumed — a hole is a live loader read under `loading()` or an inline `<Suspense>`; handler output bakes |
|
|
309
|
+
| `/prerender` | Pre-render route segments at build time (Passthrough live fallback) |
|
|
310
|
+
| `/shell-manifest` | Replayed handles as cache metadata read by live loaders (frozen shell, batched live holes) |
|
|
292
311
|
|
|
293
312
|
**Client & presentation** — build the client-side UX:
|
|
294
313
|
|
package/skills/route/SKILL.md
CHANGED
|
@@ -329,12 +329,17 @@ cache({ ttl: 60 }, () => [
|
|
|
329
329
|
]);
|
|
330
330
|
```
|
|
331
331
|
|
|
332
|
-
The same read throws inside a `"use cache"` function body, where the value
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
332
|
+
The same read throws inside a `"use cache"` function body, where the value would
|
|
333
|
+
be stored without being part of the key — pass it in as an argument instead. The
|
|
334
|
+
guard does not follow derived values (a string copied out of `Session` and
|
|
335
|
+
cached elsewhere is not tracked). Under `cache()`, loaders are exempt — a DSL
|
|
336
|
+
loader re-runs on every hit — but a `"use cache"` function a loader calls is
|
|
337
|
+
not, and neither is a loader bound with its own `cache()` without a `key()` (any
|
|
338
|
+
`key()` switches the check off, so it must include the value; `/loader` → "Cache
|
|
339
|
+
Key"). On a `ppr` route the exemption does not cover a loader the shell capture
|
|
340
|
+
waits for (`ssr: false`, or awaited by a handler via `ctx.use()`): the read
|
|
341
|
+
refuses the capture. Cacheable vars (the default) can be read freely inside
|
|
342
|
+
cache scopes. See `/cache-guide` → "Context Variable Cache Safety".
|
|
338
343
|
|
|
339
344
|
### Revalidation Contracts for Handler Data
|
|
340
345
|
|
|
@@ -543,7 +548,7 @@ interface HandlerContext<TParams = {}, TEnv = DefaultEnv, TSearch = {}> {
|
|
|
543
548
|
build: boolean; // true while pre-rendering at build time
|
|
544
549
|
dev: boolean; // true under Vite dev
|
|
545
550
|
dynamic(): void; // opt this request out of PPR shell capture (see /ppr)
|
|
546
|
-
theme?: Theme; // plus setTheme?() — only when the router enables themes (/theme)
|
|
551
|
+
readonly theme?: Theme; // plus setTheme?() — only when the router enables themes; a read is guarded like cookies() (/theme)
|
|
547
552
|
}
|
|
548
553
|
```
|
|
549
554
|
|
|
@@ -163,7 +163,7 @@ interface RangoOptions<TEnv> {
|
|
|
163
163
|
viewTransition?: "auto" | false; // Router-placed <ViewTransition> default (/view-transitions)
|
|
164
164
|
warmup?: boolean; // Connection warmup after idle (default true)
|
|
165
165
|
strictMode?: boolean; // Hydrate inside <React.StrictMode> (default true)
|
|
166
|
-
stateCookiePrefix?: string; // Prefix of the state cookie `{prefix}_{routerId}` (default "rango-state")
|
|
166
|
+
stateCookiePrefix?: string; // Prefix of the state cookie `{prefix}_{routerId}` and the fresh-reads cookie `{prefix}-fresh` (default "rango-state")
|
|
167
167
|
|
|
168
168
|
// --- Request handling ---
|
|
169
169
|
nonce?: (
|
|
@@ -752,7 +752,9 @@ the check.
|
|
|
752
752
|
render counts in development. Production behavior is unchanged.
|
|
753
753
|
- `stateCookiePrefix`: rename the state cookie that keys the client caches. The
|
|
754
754
|
`_{routerId}` suffix is always kept so sibling apps on one origin do not
|
|
755
|
-
collide.
|
|
755
|
+
collide. The prefix also names the fresh-reads cookie `{prefix}-fresh`
|
|
756
|
+
(`rango-state-fresh` by default) that `updateTag()`/`revalidateTag()` set
|
|
757
|
+
on a store with per-isolate memos; a consent-manager list needs both names.
|
|
756
758
|
- `nonce: () => true`: generate a CSP nonce per request and apply it to the
|
|
757
759
|
router's inline scripts. Return a string to supply your own, or
|
|
758
760
|
`false`/`""` to skip it for one request. Middleware reads it with
|
package/skills/scripts/SKILL.md
CHANGED
|
@@ -114,7 +114,10 @@ during navigation.
|
|
|
114
114
|
> means it is silently dropped. If a loader must contribute an inline script
|
|
115
115
|
> to the document, register it `loader(Def, { ssr: false })` so the
|
|
116
116
|
> document render awaits the push; otherwise push from a handler (or use an
|
|
117
|
-
> `async` config, which stays reactive).
|
|
117
|
+
> `async` config, which stays reactive). On a `ppr` route a loader without
|
|
118
|
+
> `ssr: false` never runs during the shell capture, so its inline/ordered
|
|
119
|
+
> script is dropped on every shell HIT; with `ssr: false` the push bakes into
|
|
120
|
+
> the shell.
|
|
118
121
|
|
|
119
122
|
**Nonce caveat for soft-nav async.** The "nonce is applied automatically" claim
|
|
120
123
|
holds for DOCUMENT-RENDERED scripts (they carry the nonce in the SSR HTML). An
|
|
@@ -185,6 +188,10 @@ Otherwise allow the vendor hosts. For GTM/GA4 (Google's wildcards): `script-src
|
|
|
185
188
|
`frame-src https://*.googletagmanager.com` for the GTM `<noscript>` iframe. See
|
|
186
189
|
[Google's CSP guide](https://developers.google.com/tag-platform/security/guides/csp).
|
|
187
190
|
|
|
191
|
+
A per-request nonce also takes a `ppr` route off its shell: a shared shell
|
|
192
|
+
cannot carry one request's nonce, so the route renders normally (no shell) and
|
|
193
|
+
warns once per key. See `/ppr` → "What always renders without a shell".
|
|
194
|
+
|
|
188
195
|
## Not covered (do it yourself)
|
|
189
196
|
|
|
190
197
|
- **`onLoad` / `onReady` / `onError`** — callbacks can't cross the server handle
|
|
@@ -37,6 +37,17 @@ Actions mutate state; route handlers and loaders read the latest state. After
|
|
|
37
37
|
an action finishes, Rango performs a server-side revalidation render for the
|
|
38
38
|
matched route so the UI receives fresh segment output and loader data.
|
|
39
39
|
|
|
40
|
+
Segments under a route `cache()` re-render fresh on that render (actions skip
|
|
41
|
+
the route cache lookup and write). A loader with its own `cache()` and a
|
|
42
|
+
`"use cache"` read return their stored entry, and a `ppr` route's stored shell
|
|
43
|
+
is not evicted, unless the action invalidates their tags.
|
|
44
|
+
`await updateTag(...tags)` purges them before the revalidation render, so the
|
|
45
|
+
action's own response reads fresh (`revalidateTag()` gives that render fresh
|
|
46
|
+
reads too; only its durable write runs in the background). On a store that keeps
|
|
47
|
+
per-isolate memos (`CFCacheStore`, `VercelCacheStore`) the response also sets
|
|
48
|
+
the fresh-reads cookie (`rango-state-fresh` by default), so the same user's next
|
|
49
|
+
requests skip those memos (see `/caching` → "The fresh-reads cookie").
|
|
50
|
+
|
|
40
51
|
The main control point is `revalidate((ctx) => ...)` on the segment that owns
|
|
41
52
|
the data. Match specific actions by imported reference with `ctx.isAction()`;
|
|
42
53
|
use raw `actionId` only when you intentionally need path or directory matching.
|
|
@@ -9,8 +9,8 @@ argument-hint:
|
|
|
9
9
|
Use this when a cached or prerendered shell has dynamic holes, and the live
|
|
10
10
|
data layer needs to know **what the shell actually contains** — which
|
|
11
11
|
products, which slots, which keys. The frozen render describes itself
|
|
12
|
-
through a handle; loaders (
|
|
13
|
-
exactly the dynamic data the shell needs, in one batch.
|
|
12
|
+
through a handle; live loaders (no `ssr: false`) read that description and
|
|
13
|
+
fetch exactly the dynamic data the shell needs, in one batch.
|
|
14
14
|
|
|
15
15
|
Canonical case: a prerendered product list where prices must stay live.
|
|
16
16
|
|
|
@@ -32,16 +32,19 @@ batched.
|
|
|
32
32
|
## The mechanism (three features composed)
|
|
33
33
|
|
|
34
34
|
1. **Handles record data at render time.** The handler pushes to a handle
|
|
35
|
-
(`ctx.use(Handle)`) while it renders — at build time for `Prerender`, on
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
(`ctx.use(Handle)`) while it renders — at build time for `Prerender`, on the
|
|
36
|
+
cache miss for `cache()`. (Loader bodies can push handles too — see `/loader`
|
|
37
|
+
— but a live loader's push is request-time and is NOT part of the replayed
|
|
38
|
+
artifact; a manifest handle must be pushed by the code that gets frozen with
|
|
39
|
+
the shell. An `ssr: false` loader on a `ppr` route is the exception: its
|
|
40
|
+
settled pushes are recorded with the shell. On a HIT, one whose return has no
|
|
41
|
+
promises does not run (unless the capture marked it to run), so the recorded
|
|
42
|
+
pushes are what the page shows; one whose return carries promises runs on
|
|
43
|
+
every HIT, and its pushes replace the recorded ones — `/ppr` "On a shell
|
|
44
|
+
HIT".)
|
|
42
45
|
2. **Replay on every hit.** Handle data is stored with the Flight payload
|
|
43
|
-
and replayed into the handle store on cache
|
|
44
|
-
does not re-run, but its pushes do.
|
|
46
|
+
and replayed into the handle store on cache, prerender and `ppr` shell
|
|
47
|
+
hits — handler code does not re-run, but its pushes do.
|
|
45
48
|
3. **Loaders read after the render barrier.** A DSL loader can
|
|
46
49
|
`await ctx.rendered()` (waits for all non-loader segments to settle —
|
|
47
50
|
fresh render or replay alike), then `ctx.get(Handle)` returns the
|
|
@@ -185,7 +188,13 @@ const prices = await runLoader(PriceLoader, {
|
|
|
185
188
|
|
|
186
189
|
This tests the loader's post-barrier logic. The real
|
|
187
190
|
push → store → replay → barrier wiring is covered at the e2e tier (dev +
|
|
188
|
-
production), like every cache-path behavior.
|
|
191
|
+
production), like every cache-path behavior. On a `ppr` route,
|
|
192
|
+
`serveShellRequest(router, url, { cacheStore })` from
|
|
193
|
+
`@rangojs/router/testing/flight` runs the real capture and HIT in a unit test
|
|
194
|
+
(react-server Vitest): serve the URL twice with the same store; the second
|
|
195
|
+
response is a HIT, and its `flight` carries the live loader's output for the
|
|
196
|
+
replayed ids. Call `resetShellTestState()` in `beforeEach`. Recipe:
|
|
197
|
+
`/testing` → `serve-shell-request.md`.
|
|
189
198
|
|
|
190
199
|
## Related
|
|
191
200
|
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: testing
|
|
3
|
-
description: Test @rangojs/router apps — unit (loaders, middleware, server actions, handles, transition/clientUrls predicates), client components (renderRoute), RSC/Flight (renderServerTree, renderHandler), integration (dispatch), and e2e (dev+prod parity, progressive enhancement). Use when writing a unit test for a loader, middleware, action, or handler, setting up vitest for a Rango app, or testing a route end-to-end in dev and production.
|
|
3
|
+
description: Test @rangojs/router apps — unit (loaders, middleware, server actions, handles, transition/clientUrls predicates), client components (renderRoute), RSC/Flight (renderServerTree, renderHandler, serveShellRequest for PPR shells), integration (dispatch), and e2e (dev+prod parity, progressive enhancement). Use when writing a unit test for a loader, middleware, action, or handler, setting up vitest for a Rango app, or testing a route end-to-end in dev and production.
|
|
4
4
|
argument-hint: [layer]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -108,8 +108,10 @@ Each primitive links to its sub-file (API + recipe + caveats).
|
|
|
108
108
|
| navigation, hydration, PE parity, view transitions, real SSR | e2e | [`createRangoE2E` -> `parityDescribe`/`expectParity`](./e2e-parity.md) | `@rangojs/router/testing/e2e` |
|
|
109
109
|
| a `"use cache"` hit in a handler (the body runs once across renders; the value round-trips) | RSC unit | [`renderHandler` + seeded `cacheStore`](./render-handler.md#a-use-cache-hit) | `@rangojs/router/testing/flight` |
|
|
110
110
|
| a `"use cache"` hit in a loader (the same, in the react-server project) | RSC unit | [`runLoader` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
|
|
111
|
+
| a loader's own `cache()` (HIT, `updateTag()` of a tag its body recorded) | RSC unit | [`runLoader` + `cache` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
|
|
111
112
|
| cache hit/miss/stale, prerender (= a cache hit by design) | e2e + signal | [`assertCacheStatus` (header) / `assertCacheDecision` (telemetry sink)](./cache-prerender.md) | `@rangojs/router/testing[/e2e]` |
|
|
112
|
-
|
|
|
113
|
+
| a `ppr` route's shell: MISS -> capture -> HIT, what the shell froze vs what a HIT renders live, tag eviction, navigation replay | RSC unit | [`serveShellRequest`](./serve-shell-request.md) | `@rangojs/router/testing/flight` |
|
|
114
|
+
| PPR shell HIT/MISS in real HTML (`ppr` path option), shell store key / getShell after flush | e2e + signal | [`assertShellStatus` / `shellCacheKey` + store](./cache-prerender.md#ppr-shell-and-navigation-replay) | `@rangojs/router/testing[/e2e]` |
|
|
113
115
|
| generated route map drift vs runtime | unit (node) | [`assertGeneratedRoutesMatch`](./reverse-and-types.md) | `@rangojs/router/testing` |
|
|
114
116
|
| a platform binding (`env.DB` / Durable Object / `env.R2`) | unit/integr. | [your own double via `env`](./bindings.md) | (any primitive's `env` option) |
|
|
115
117
|
|
|
@@ -125,7 +127,8 @@ Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
|
|
|
125
127
|
[`predicates.md`](./predicates.md), [`reverse-and-types.md`](./reverse-and-types.md)
|
|
126
128
|
- Unit (DOM): [`client-components.md`](./client-components.md)
|
|
127
129
|
- RSC unit: [`flight.md`](./flight.md), [`server-tree.md`](./server-tree.md),
|
|
128
|
-
[`render-handler.md`](./render-handler.md)
|
|
130
|
+
[`render-handler.md`](./render-handler.md),
|
|
131
|
+
[`serve-shell-request.md`](./serve-shell-request.md)
|
|
129
132
|
- Integration: [`response-routes.md`](./response-routes.md)
|
|
130
133
|
- E2E: [`e2e-parity.md`](./e2e-parity.md), [`cache-prerender.md`](./cache-prerender.md)
|
|
131
134
|
|
|
@@ -134,24 +137,25 @@ Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
|
|
|
134
137
|
Where each runtime export is documented (types such as `RunLoaderOptions` sit
|
|
135
138
|
beside their function):
|
|
136
139
|
|
|
137
|
-
| Entry | Exports | Documented in
|
|
138
|
-
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
139
|
-
| `@rangojs/router/testing` | `runLoader`, `runLoaderResult` | [`loader.md`](./loader.md)
|
|
140
|
-
| | `runMiddleware` | [`middleware.md`](./middleware.md)
|
|
141
|
-
| | `runInRequestContext`, `createTestRequestContext`, `runWithRequestContext`, `toRequest`, `seedVariables` | [`server-actions.md`](./server-actions.md)
|
|
142
|
-
| | `runTransitionWhen`, `runClientRevalidate` | [`predicates.md`](./predicates.md)
|
|
143
|
-
| | `collectHandle` | [`handles.md`](./handles.md)
|
|
144
|
-
| | `dispatch` | [`response-routes.md`](./response-routes.md)
|
|
145
|
-
| | `assertCacheStatus`, `assertCacheDecision`, `parseCacheHeader`, `createCacheSink`, `filterCacheDecisions` | [`cache-prerender.md`](./cache-prerender.md)
|
|
146
|
-
| | `assertShellStatus`, `parseShellStatus`, `shellCacheKey`, `SHELL_STATUS_HEADER`, `assertPprReplayStatus`, `parsePprReplayStatus`, `PPR_REPLAY_STATUS_HEADER` | [`cache-prerender.md`](./cache-prerender.md)
|
|
147
|
-
| | `assertGeneratedRoutesMatch`, `diffGeneratedRoutes` | [`reverse-and-types.md`](./reverse-and-types.md)
|
|
148
|
-
| `@rangojs/router/testing/dom` | `renderRoute` | [`client-components.md`](./client-components.md)
|
|
149
|
-
| `@rangojs/router/testing/flight` | `renderToFlightString`, `normalizeFlight`, `assertFlightRuntimeAvailable` | [`flight.md`](./flight.md)
|
|
150
|
-
| | `renderServerTree`, `findClientBoundaries`, `findElements`, `textContent`, `assertFlightTreeRuntimeAvailable` | [`server-tree.md`](./server-tree.md)
|
|
151
|
-
| | `renderHandler` | [`render-handler.md`](./render-handler.md)
|
|
152
|
-
|
|
|
153
|
-
| `@rangojs/router/testing/
|
|
154
|
-
| `@rangojs/router/testing/
|
|
140
|
+
| Entry | Exports | Documented in |
|
|
141
|
+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
|
|
142
|
+
| `@rangojs/router/testing` | `runLoader`, `runLoaderResult` | [`loader.md`](./loader.md) |
|
|
143
|
+
| | `runMiddleware` | [`middleware.md`](./middleware.md) |
|
|
144
|
+
| | `runInRequestContext`, `createTestRequestContext`, `runWithRequestContext`, `toRequest`, `seedVariables` | [`server-actions.md`](./server-actions.md) |
|
|
145
|
+
| | `runTransitionWhen`, `runClientRevalidate` | [`predicates.md`](./predicates.md) |
|
|
146
|
+
| | `collectHandle` | [`handles.md`](./handles.md) |
|
|
147
|
+
| | `dispatch` | [`response-routes.md`](./response-routes.md) |
|
|
148
|
+
| | `assertCacheStatus`, `assertCacheDecision`, `parseCacheHeader`, `createCacheSink`, `filterCacheDecisions` | [`cache-prerender.md`](./cache-prerender.md) |
|
|
149
|
+
| | `assertShellStatus`, `parseShellStatus`, `shellCacheKey`, `SHELL_STATUS_HEADER`, `assertPprReplayStatus`, `parsePprReplayStatus`, `PPR_REPLAY_STATUS_HEADER` | [`cache-prerender.md`](./cache-prerender.md) |
|
|
150
|
+
| | `assertGeneratedRoutesMatch`, `diffGeneratedRoutes` | [`reverse-and-types.md`](./reverse-and-types.md) |
|
|
151
|
+
| `@rangojs/router/testing/dom` | `renderRoute` | [`client-components.md`](./client-components.md) |
|
|
152
|
+
| `@rangojs/router/testing/flight` | `renderToFlightString`, `normalizeFlight`, `assertFlightRuntimeAvailable` | [`flight.md`](./flight.md) |
|
|
153
|
+
| | `renderServerTree`, `findClientBoundaries`, `findElements`, `textContent`, `assertFlightTreeRuntimeAvailable` | [`server-tree.md`](./server-tree.md) |
|
|
154
|
+
| | `renderHandler` | [`render-handler.md`](./render-handler.md) |
|
|
155
|
+
| | `serveShellRequest`, `resetShellTestState` | [`serve-shell-request.md`](./serve-shell-request.md) |
|
|
156
|
+
| `@rangojs/router/testing/flight-matchers` | `flightMatchers` (`toMatchFlight`, `toMatchFlightSnapshot`) | [`flight.md`](./flight.md) |
|
|
157
|
+
| `@rangojs/router/testing/vitest` | `rangoTestConfig`, `rangoTestAliases`, `rangoInlineDeps`, `rangoUseClientTransform`, `rangoUseCacheTransform` | [`setup.md`](./setup.md) |
|
|
158
|
+
| `@rangojs/router/testing/e2e` | `createRangoE2E` and its standalone helpers; the cache/shell status helpers above are re-exported here | [`e2e-parity.md`](./e2e-parity.md) |
|
|
155
159
|
|
|
156
160
|
## Pre-push checklist
|
|
157
161
|
|
|
@@ -119,17 +119,17 @@ expect(decision.segments?.[0].shouldRevalidate).toBe(true);
|
|
|
119
119
|
|
|
120
120
|
## PPR shell and navigation replay
|
|
121
121
|
|
|
122
|
-
**DSL:** `ppr: true | PartialPrerenderProps` on a page route (see `/ppr`). **Not** the same header as `X-Rango-Cache` — shell is
|
|
123
|
-
|
|
124
|
-
| Helper
|
|
125
|
-
|
|
|
126
|
-
| `assertShellStatus(res, "HIT" \| "MISS")`
|
|
127
|
-
| `parseShellStatus(res)`
|
|
128
|
-
| `shellCacheKey(url, searchParams?)`
|
|
129
|
-
| `SHELL_STATUS_HEADER`
|
|
130
|
-
| `assertPprReplayStatus(res, expected)`
|
|
131
|
-
| `parsePprReplayStatus(res)`
|
|
132
|
-
| `PPR_REPLAY_STATUS_HEADER`
|
|
122
|
+
**DSL:** `ppr: true | PartialPrerenderProps` on a page route (see `/ppr`). **Not** the same header as `X-Rango-Cache` — the shell is cached separately from the segments.
|
|
123
|
+
|
|
124
|
+
| Helper | Import | Role |
|
|
125
|
+
| ----------------------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
126
|
+
| `assertShellStatus(res, "HIT" \| "MISS")` | `@rangojs/router/testing` or `…/e2e` | Assert `x-rango-shell` on a **real document** Response |
|
|
127
|
+
| `parseShellStatus(res)` | same | `"HIT" \| "MISS" \| null` (null = header absent / unrecognized) |
|
|
128
|
+
| `shellCacheKey(url, searchParams?, partition?)` | same | Production shell store key (`host+pathname+sorted search+:shell`, then `\|partition`) for `store.getShell` / custom stores. Pass your router's `cache.searchParams` config as the second argument when it sets one — the production key applies it. For a route whose `cache({ key })` or store `keyGenerator` partitions its record, pass what that function returns for the request as `partition`: the shell is partitioned the same way. Under nested keyed `cache()` boundaries, pass their `key()` results as an array, outermost first. |
|
|
129
|
+
| `SHELL_STATUS_HEADER` | same | `"x-rango-shell"` constant |
|
|
130
|
+
| `assertPprReplayStatus(res, expected)` | same | Assert fresh/stale replay or a bounded bypass decision |
|
|
131
|
+
| `parsePprReplayStatus(res)` | same | Structured replay/bypass status, or null for an absent/unrecognized header |
|
|
132
|
+
| `PPR_REPLAY_STATUS_HEADER` | same | `"x-rango-ppr-replay"` constant |
|
|
133
133
|
|
|
134
134
|
### What unit can prove vs e2e
|
|
135
135
|
|
|
@@ -137,10 +137,11 @@ expect(decision.segments?.[0].shouldRevalidate).toBe(true);
|
|
|
137
137
|
| -------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
138
138
|
| **Unit** | store family + key identity | `MemorySegmentCacheStore` + `shellCacheKey(url)` + `putShell`/`getShell` / tag eviction — dogfood in `e2e/mini/test/shell-store-family.test.ts` and `src/testing/__tests__/shell-status.test.ts` |
|
|
139
139
|
| **Unit** | header helper contract | `assertShellStatus` on a Response that already carries the header (characterizes the helper; **never** invent a HIT to claim capture worked) |
|
|
140
|
+
| **Unit** | real capture → HIT, replay | `serveShellRequest(router, url, { cacheStore })` (react-server project); only the HTML step is stubbed — [`./serve-shell-request.md`](./serve-shell-request.md) |
|
|
140
141
|
| **E2E** | live MISS → capture → HIT | document GET, poll until `x-rango-shell: HIT` (background capture) — `e2e/shell-cache.test.ts` |
|
|
141
142
|
| **E2E** | partial replay decision | soft navigation to a warmed shell and `assertPprReplayStatus(response, { outcome: "HIT", freshness: "fresh" })` |
|
|
142
143
|
|
|
143
|
-
`dispatch` is RSC-free: it never runs shell serve/capture. `renderHandler` only surfaces `ctx.dynamic()` opt-out, not bake/serve.
|
|
144
|
+
`dispatch` is RSC-free: it never runs shell serve/capture. `renderHandler` only surfaces `ctx.dynamic()` opt-out, not bake/serve. The HIT contract — no handler runs, the handler output is the capture's copy, live loaders re-run — is asserted through `serveShellRequest`: count your handler's runs and a live loader's across a MISS and the HITs after it, and check that only the loader's moves ([`./serve-shell-request.md`](./serve-shell-request.md)). A route partitioned by `cache({ key })` or a store `keyGenerator` has a shell per partition: `serveShellRequest` reports the key the serve path resolved (`result.key`), and `shellCacheKey` takes the partition as its third argument.
|
|
144
145
|
|
|
145
146
|
### Recipe (e2e)
|
|
146
147
|
|
|
@@ -185,7 +186,7 @@ it("stores the shell under the production key", async () => {
|
|
|
185
186
|
## Caveats
|
|
186
187
|
|
|
187
188
|
- The `X-Rango-Cache` header is emitted ONLY when the gate is on: `createRouter({ debugCacheSignal: true })` or `process.env.RANGO_TEST_SIGNALS === "1"`. Off by default — zero production surface. With the gate off, `assertCacheStatus` throws a clear "header missing" error.
|
|
188
|
-
- `x-rango-shell` is always set on document GETs the serve path considered for a ppr route (no debug gate). Absent header =
|
|
189
|
+
- `x-rango-shell` is always set on document GETs the serve path considered for a ppr route (no debug gate). Absent header = a normal render (no shell): the request fell open, or the route has no `ppr`.
|
|
189
190
|
- v1 is COARSE: route-level, keyed by the route NAME (e.g. `product.detail`), NOT the URL pattern (`/products/:id`); not per-individual-segment. The signal is built from `ctx.routeKey`, so a pattern-shaped key never matches. (`parseCacheHeader` exposes the raw `{ routeKey: status }` map if you need it.)
|
|
190
191
|
- Prerender is indistinguishable from a cache hit by design — no static `.html`/`.rsc` files, the worker handles every request and looks up a stored Flight payload; the browser cannot tell. Do not assert "prerendered" from the DOM. Assert via the signal (`assertCacheStatus(res, seg, "prerendered")`) and run prerender assertions in PRODUCTION mode (the build-time artifacts only exist after `pnpm build`).
|
|
191
192
|
- In a Playwright e2e import the cache-status / shell-status helpers from the `/e2e` entry — the `@rangojs/router/testing` barrel is Vitest-only (it pulls a build-only virtual that does not resolve in a plain Playwright runner). Zero-prod-surface alternative for segment cache: the telemetry sink (`createCacheSink`/`filterCacheDecisions`), no header at all. Note: the non-RSC `dispatch()` primitive never emits either header — get the Response from the router's real RSC fetch path.
|
|
@@ -194,5 +195,5 @@ it("stores the shell under the production key", async () => {
|
|
|
194
195
|
## See also
|
|
195
196
|
|
|
196
197
|
- `/caching`, `/prerender`, `/use-cache`, `/ppr` — the DSL this tests
|
|
197
|
-
- Siblings: [`./e2e-parity.md`](./e2e-parity.md), [`./response-routes.md`](./response-routes.md)
|
|
198
|
+
- Siblings: [`./e2e-parity.md`](./e2e-parity.md), [`./response-routes.md`](./response-routes.md), [`./serve-shell-request.md`](./serve-shell-request.md)
|
|
198
199
|
- Long-form prose: [docs/testing.md](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/testing.md) — section "Cache, SWR, and prerender"
|
package/skills/testing/loader.md
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
| `use` | `UseResolver` | Dynamic resolver for `ctx.use(OtherLoader)` composition. `loaders` wins when both match. |
|
|
28
28
|
| `cacheStore` | `SegmentCacheStore` | Cache store backing `use cache` functions. Without one, a cached function bypasses and runs uncached (its taint/profile guards never fire). |
|
|
29
29
|
| `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles, the `createRouter({ cacheProfiles })` shape. |
|
|
30
|
+
| `cache` | `PartialCacheOptions` | Run the loader through the `cache()` a route binds it with (`loader(Loader, () => [cache({...})])` takes the same options), against `cache.store`, else `cacheStore`. See the caveat below. |
|
|
30
31
|
| `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a loader calling `invalidateClientCache()` rotates (the name is always seeded — default `rango-state_router_0`). |
|
|
31
32
|
| `rendered` | `boolean \| (() => void \| Promise<void>)` | Mock the `ctx.rendered()` render barrier so a loader that `await ctx.rendered()`s can be unit-tested. By default `ctx.rendered()` throws. `true` resolves immediately; a function controls timing/side effects. |
|
|
32
33
|
| `handles` | `ReadonlyArray<readonly [Handle<any, any>, unknown]>` | Seed the values `ctx.get(SomeHandle)` returns — the ACCUMULATED handle data read after `await ctx.rendered()`. Matched by handle reference. (Loader handle WRITES need no seed — see `runLoaderResult(...).handlePushes`.) |
|
|
@@ -122,8 +123,24 @@ it("asserts a loader's set-cookie + redirect (runLoaderResult)", async () => {
|
|
|
122
123
|
- A `createLoader()` definition imported through the CLIENT build has its body dropped — `runLoader` throws a clear error pointing to the `rangoTestConfig()` preset or the raw body. A router using `Prerender()`/`createLoader()`/`Static()` now constructs in a bare test (each assigns a runtime fallback `$$id`); only the whole router _file_ may still need the plugin (its page modules pull app deps / `virtual:` modules).
|
|
123
124
|
- No `cookies`/`headers` option: seed a cookie by passing a full Request with a Cookie header — `{ request: new Request(url, { headers: { Cookie: "sid=abc" } }) }`. (`search`/`method` are baked onto this request for you.)
|
|
124
125
|
- `ctx.search` (typed) defaults to `{}`; `opts.search` only sets the raw `ctx.searchParams`. Seed the typed object with `searchData`. (The harness seeds `searchData` verbatim — it does NOT run a typed-search SCHEMA, so schema parsing/validation is e2e.)
|
|
125
|
-
- `ctx.theme`/`ctx.setTheme` are NOT on the loader context —
|
|
126
|
+
- `ctx.theme`/`ctx.setTheme` are NOT on the loader context — they are handler/middleware-only. The `theme` option seeds the request context's theme config, so a loader body reads the visitor's theme with `getRequestContext().theme` (seed the cookie through `request`). That read is guarded like `cookies()` (#971): allowed in a loader body, but it throws inside a `"use cache"` function. `redirect()` does no basename prefixing unless you seed `basename`.
|
|
126
127
|
- A `"use cache"` hit needs real Flight, which loads only under the react-server condition. In the react-server project (a `*.rsc-test.ts` file, see [`./setup.md`](./setup.md)) with `cacheStore` seeded, the body runs once across `runLoader` calls and the value round-trips. The write is a background (`waitUntil`) task `runLoader` does not await: a call before it lands joins the still-running execution, a call after it reads the store (wait with `vi.waitFor` on a `cacheStore.setItem` spy). In the node project a call looks up the store but never writes, so every call runs the body. A function written with the directive needs `rangoUseCacheTransform()` in the project's `plugins` (see [`./render-handler.md`](./render-handler.md#a-use-cache-hit)).
|
|
128
|
+
- The `cache` option runs the loader through the production loader-cache read-through, so it needs the react-server project for the same reason. A HIT returns the stored value without running the body and records the tags the body recorded when the entry was written, so `updateTag()` of one (inside `runInRequestContext(fn, { cacheStore })`) drops the entry. There is no in-flight join here: wait for the write before the next call, or that call misses too. A raw body is keyed by its function reference. `runLoaderResult(...).handlePushes` lists the body's pushes, so a HIT reports none (the replay into the page's handle store is not modeled).
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const load = () =>
|
|
132
|
+
runLoader(productLoader, {
|
|
133
|
+
params: { id: "1" },
|
|
134
|
+
cacheStore,
|
|
135
|
+
cache: { ttl: 300 },
|
|
136
|
+
});
|
|
137
|
+
await load(); // MISS: the body runs and calls cacheTag("product:1")
|
|
138
|
+
await vi.waitFor(() => expect(setItem).toHaveBeenCalledTimes(1));
|
|
139
|
+
await load(); // HIT: the body does not run
|
|
140
|
+
await runInRequestContext(() => updateTag("product:1"), { cacheStore });
|
|
141
|
+
await load(); // MISS again
|
|
142
|
+
```
|
|
143
|
+
|
|
127
144
|
- Platform bindings are yours to double via `env` (see `./bindings.md`).
|
|
128
145
|
|
|
129
146
|
## See also
|