@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: prerender
|
|
3
|
-
description: Pre-render route segments at build time with Prerender and Passthrough live fallback
|
|
3
|
+
description: Pre-render route segments at build time with Prerender and Passthrough live fallback. Use when a page's content is mostly static and shouldn't render on every request, speeding up cold responses, or deciding which routes to prerender vs render live.
|
|
4
4
|
argument-hint: [passthrough]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -11,6 +11,13 @@ deserialization path, same segment system. The worker handles every request --
|
|
|
11
11
|
there are NO static .html or .rsc files served from assets. The worker reads
|
|
12
12
|
pre-computed Flight payloads instead of executing handler code.
|
|
13
13
|
|
|
14
|
+
## Not this skill if…
|
|
15
|
+
|
|
16
|
+
- You want a cached HTML shell captured at runtime, with holes and loaders
|
|
17
|
+
staying live per request — see `/ppr`.
|
|
18
|
+
- You want runtime segment caching with TTL/SWR — that is the `cache()` DSL:
|
|
19
|
+
see `/caching`. Prerender is the same cache filled at build time.
|
|
20
|
+
|
|
14
21
|
## API: Prerender
|
|
15
22
|
|
|
16
23
|
### Static Route (no params)
|
|
@@ -243,16 +250,16 @@ path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
|
|
|
243
250
|
|
|
244
251
|
## Interaction with DSL Items
|
|
245
252
|
|
|
246
|
-
| DSL item | Behavior with Prerender
|
|
247
|
-
| -------------- |
|
|
248
|
-
| `loader()` | Live at runtime, bundled normally. Use `cache()` for caching.
|
|
249
|
-
| `revalidate()` | Not allowed without Passthrough. Allowed with Passthrough.
|
|
250
|
-
| `cache()` | Orthogonal -- use on parent layouts and loaders.
|
|
251
|
-
| `layout()` | Child layouts inside path are pre-rendered. Parent layouts are live.
|
|
252
|
-
| `parallel()` | Parallel slots inside path are pre-rendered.
|
|
253
|
-
| `middleware()` | Skipped during pre-render (no request). Runs at request time for loaders.
|
|
254
|
-
| `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough.
|
|
255
|
-
| `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside main segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when
|
|
253
|
+
| DSL item | Behavior with Prerender |
|
|
254
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
255
|
+
| `loader()` | Live at runtime, bundled normally. Use `cache()` for caching. |
|
|
256
|
+
| `revalidate()` | Not allowed without Passthrough. Allowed with Passthrough. |
|
|
257
|
+
| `cache()` | Orthogonal -- use on parent layouts and loaders. |
|
|
258
|
+
| `layout()` | Child layouts inside path are pre-rendered. Parent layouts are live. |
|
|
259
|
+
| `parallel()` | Parallel slots inside path are pre-rendered. |
|
|
260
|
+
| `middleware()` | Skipped during pre-render (no request). Runs at request time for loaders. |
|
|
261
|
+
| `loading()` | Ignored without Passthrough. Works for live fallback with Passthrough. |
|
|
262
|
+
| `intercept()` | Pre-rendered at build time. Intercept variant stored under `/i` key alongside main segments. At runtime, the correct variant is served based on `ctx.isIntercept`. `when` config conditions are skipped at build time (all intercepts are pre-rendered unconditionally). |
|
|
256
263
|
|
|
257
264
|
When Passthrough revalidation is enabled, remember that revalidation is
|
|
258
265
|
still partial: opting a child segment into revalidation does not
|
|
@@ -609,12 +616,12 @@ At runtime, the cache-lookup middleware checks `ctx.isIntercept`:
|
|
|
609
616
|
(filtered by `namespace?.startsWith("intercept:")`) and sets up slots.
|
|
610
617
|
- **Direct navigation**: looks up `paramHash` (no suffix). Standard prerender path.
|
|
611
618
|
- **Intercept miss (no `/i` entry)**: falls through to the normal pipeline so
|
|
612
|
-
intercept-resolution middleware runs live. This handles `when
|
|
619
|
+
intercept-resolution middleware runs live. This handles `when` config conditions
|
|
613
620
|
that prevented pre-rendering.
|
|
614
621
|
|
|
615
|
-
The `when
|
|
622
|
+
The `when` config selector receives an `InterceptSelectorContext` with `from.pathname`
|
|
616
623
|
which is unknown at build time. All intercepts are pre-rendered unconditionally;
|
|
617
|
-
`when
|
|
624
|
+
`when` is evaluated at runtime by the intercept-resolution middleware.
|
|
618
625
|
|
|
619
626
|
### Example: Pre-rendered route with intercept
|
|
620
627
|
|
|
@@ -633,10 +640,13 @@ layout(ShopLayout, () => [
|
|
|
633
640
|
|
|
634
641
|
// Intercept detail from shop index into a modal.
|
|
635
642
|
// At build time, this is resolved and stored under the /i key.
|
|
636
|
-
intercept(
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
643
|
+
intercept(
|
|
644
|
+
"@modal",
|
|
645
|
+
".detail",
|
|
646
|
+
<ProductModal />,
|
|
647
|
+
{ when: ({ from }) => from.pathname === "/shop" },
|
|
648
|
+
() => [loader(ProductLoader)],
|
|
649
|
+
),
|
|
640
650
|
])
|
|
641
651
|
```
|
|
642
652
|
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rango
|
|
3
|
-
description: Overview of @rangojs/router and available skills
|
|
3
|
+
description: Overview of @rangojs/router and available skills. Use when unsure which skill to reach for, starting a new task in a Rango app, or asking "what can this router do".
|
|
4
4
|
argument-hint:
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -59,28 +59,73 @@ appears, not up front.
|
|
|
59
59
|
To decide where something can live: **does it define a URL? structure, stays in
|
|
60
60
|
`urls()`. Does it modify a node? config, compose freely.**
|
|
61
61
|
|
|
62
|
+
## Passing data down the tree
|
|
63
|
+
|
|
64
|
+
Four ways to get per-request data to a segment below you, ordered safest-first.
|
|
65
|
+
Reach for the next rung only when the one above doesn't fit — the higher rungs
|
|
66
|
+
are immune to partial-revalidation staleness by construction.
|
|
67
|
+
|
|
68
|
+
1. **A loader** (`loader()` + `useLoader()`). Loaders resolve fresh on every
|
|
69
|
+
pass — full renders, action revalidations, cache hits. Nothing to keep in
|
|
70
|
+
sync. If the data can be a loader, make it a loader.
|
|
71
|
+
2. **Middleware `ctx.set()`**. Route middleware wraps every render pass,
|
|
72
|
+
including post-action revalidation and PE re-renders, so its variables are
|
|
73
|
+
never stale. Right for request-shaped context: auth, session, locale.
|
|
74
|
+
3. **Handler `ctx.set()` to its own children** —
|
|
75
|
+
`path(handler, ..., () => [layout(...)])`. Orphan layouts and their
|
|
76
|
+
parallels belong to the route entry: on an action the whole entry re-runs
|
|
77
|
+
together by default (handler-first preserved), so the data stays consistent
|
|
78
|
+
with zero configuration. Right for data the page must compute anyway —
|
|
79
|
+
e.g. pagination, where the handler's search decides how many pages the
|
|
80
|
+
layout chrome renders. One rule: if you narrow the entry's revalidation
|
|
81
|
+
with a predicate that can return a hard `false`, put the same contract on
|
|
82
|
+
the entry's children too — a hard `false` on one side of a
|
|
83
|
+
producer/consumer pair desyncs it.
|
|
84
|
+
4. **Cross-entry sharing** — an outer `layout()` entry feeding descendants.
|
|
85
|
+
Outer entries do NOT revalidate on actions by default (the revalidation
|
|
86
|
+
trace calls this `action:parent-chain-skip`), so this rung always requires
|
|
87
|
+
a shared revalidation contract: the same named `revalidate()` function on
|
|
88
|
+
the producer and every consumer. See `/layout` → "Revalidation Contracts".
|
|
89
|
+
Before writing one, check whether the producer can move down a rung.
|
|
90
|
+
|
|
91
|
+
The failure mode this ladder prevents: a consumer re-runs, its producer
|
|
92
|
+
doesn't, `ctx.get()` reads `undefined`, and fallback UI silently replaces good
|
|
93
|
+
UI after an action. Rungs 1–3 make that unrepresentable; rung 4 makes it a
|
|
94
|
+
stated, greppable contract.
|
|
95
|
+
|
|
62
96
|
## Pick a primitive
|
|
63
97
|
|
|
64
|
-
| I need to…
|
|
65
|
-
|
|
|
66
|
-
| render data fresh every request
|
|
67
|
-
| cache a rendered subtree
|
|
68
|
-
| cache one function/component's result
|
|
69
|
-
| cache a loader's data
|
|
70
|
-
| re-render a segment after an action
|
|
71
|
-
| mutate
|
|
72
|
-
| debug a slow request
|
|
73
|
-
| share config across routes
|
|
74
|
-
| compose a sub-app / module
|
|
75
|
-
| modal / soft navigation
|
|
76
|
-
| pre-render a route at build time
|
|
77
|
-
|
|
|
98
|
+
| I need to… | Use | Skill |
|
|
99
|
+
| --------------------------------------- | ---------------------------------- | ----------------------- |
|
|
100
|
+
| render data fresh every request | `loader()` + `useLoader()` | /loader |
|
|
101
|
+
| cache a rendered subtree | `cache()` on a segment | /caching |
|
|
102
|
+
| cache one function/component's result | `"use cache"` | /use-cache |
|
|
103
|
+
| cache a loader's data | `loader(L, () => [cache()])` | /loader, /caching |
|
|
104
|
+
| re-render a segment after an action | `revalidate()` | /loader |
|
|
105
|
+
| mutate | `"use server"` action | /server-actions |
|
|
106
|
+
| debug a slow request | `debugPerformance` / telemetry | /observability |
|
|
107
|
+
| share config across routes | factory returning a helper array | /composability |
|
|
108
|
+
| compose a sub-app / module | `include()` | /route |
|
|
109
|
+
| modal / soft navigation | `intercept()` | /intercept |
|
|
110
|
+
| pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
|
|
111
|
+
| feed live loaders from a cached shell | replayed handle + `ctx.rendered()` | /shell-manifest |
|
|
112
|
+
| cache the HTML shell, keep loaders live | `ppr` path option | /ppr |
|
|
113
|
+
| stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()` | /streams-and-websockets |
|
|
78
114
|
|
|
79
115
|
## Invariants
|
|
80
116
|
|
|
81
117
|
- `path()`/`include()` are always visible in `urls()`; config helpers are extractable.
|
|
82
118
|
- **Cache decides freshness; `revalidate()` decides client-update.** Orthogonal; compose.
|
|
83
119
|
- Loaders resolve fresh every request (even inside `cache()`) and never run twice/request.
|
|
120
|
+
- **The consumption-lane rule.** For every shared artifact (`cache()`,
|
|
121
|
+
`"use cache"`, the PPR shell): server-side handler consumption
|
|
122
|
+
(`await ctx.use(loader)`) yields a BAKED copy — identity reads
|
|
123
|
+
(`cookies()`/`headers()`) are permitted there and the capture-time value
|
|
124
|
+
freezes into the shared artifact (a documented footgun; see `/caching` →
|
|
125
|
+
"Cache purity & tainted objects"). Client-side consumption (`useLoader` in
|
|
126
|
+
a `"use client"` component) is the LIVE lane. DSL `loader()` segments
|
|
127
|
+
follow their lane machinery (live under renderable `loading()`, bake
|
|
128
|
+
otherwise). Pinned by semantic-matrix row PPR3.
|
|
84
129
|
- Inside `"use cache"`: `cookies()`/`headers()` and `ctx` side-effects
|
|
85
130
|
(`set`/`header`/`setTheme`/`onResponse`/`setLocationState`) throw; `ctx.use(Handle)`
|
|
86
131
|
is captured on miss and replayed on hit. (The non-cacheable read guard is a
|
|
@@ -117,6 +162,7 @@ Same words, different jobs — this is the most common source of the
|
|
|
117
162
|
| Next.js `revalidateTag` / `updateTag` | **Axis 1** (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. |
|
|
118
163
|
| React Router / Remix `shouldRevalidate` | **Axis 2** | This is the correct mental model for Rango's `revalidate()`. |
|
|
119
164
|
| HTTP `Cache-Control` / ISR | **Axis 1** | Edge/document layer — see `/document-cache`. Separate from both `cache()` and `revalidate()`. |
|
|
165
|
+
| Next.js PPR (partial prerendering) | HTML shell layer | Same idea, different wiring: the opt-in `ppr` path option captures at runtime (no build-time default); holes are render-defined — `loading()` subtrees plus pending promises under a consumer's own `<Suspense>`. See `/ppr`. |
|
|
120
166
|
| 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. |
|
|
121
167
|
|
|
122
168
|
See `/cache-guide` for the axis-1 decision guide, `/loader` and `/route` for
|
|
@@ -185,6 +231,12 @@ resolve `dist/` outside `./vite`, and it may lag `src/`.
|
|
|
185
231
|
|
|
186
232
|
Grouped by concern — read when you need to…
|
|
187
233
|
|
|
234
|
+
**Positioning & evaluation**:
|
|
235
|
+
|
|
236
|
+
| Skill | Description |
|
|
237
|
+
| ------------- | ------------------------------------------------------------ |
|
|
238
|
+
| `/comparison` | Compare Rango with Next.js, TanStack Start, and Waku fairly. |
|
|
239
|
+
|
|
188
240
|
**Structure & routing** — shape URLs, layouts, navigation, and request processing:
|
|
189
241
|
|
|
190
242
|
| Skill | Description |
|
|
@@ -206,15 +258,17 @@ Grouped by concern — read when you need to…
|
|
|
206
258
|
|
|
207
259
|
**Data & caching** — fetch, mutate, and cache:
|
|
208
260
|
|
|
209
|
-
| Skill | Description
|
|
210
|
-
| ----------------- |
|
|
211
|
-
| `/loader` | Data loaders with `createLoader()` and `revalidate()`
|
|
212
|
-
| `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation
|
|
213
|
-
| `/caching` | Segment caching with memory or KV stores
|
|
214
|
-
| `/use-cache` | Function-level caching with `"use cache"` directive
|
|
215
|
-
| `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide
|
|
216
|
-
| `/document-cache` | Edge caching with Cache-Control headers
|
|
217
|
-
| `/
|
|
261
|
+
| Skill | Description |
|
|
262
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
263
|
+
| `/loader` | Data loaders with `createLoader()` and `revalidate()` |
|
|
264
|
+
| `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation |
|
|
265
|
+
| `/caching` | Segment caching with memory or KV stores |
|
|
266
|
+
| `/use-cache` | Function-level caching with `"use cache"` directive |
|
|
267
|
+
| `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
|
|
268
|
+
| `/document-cache` | Edge caching with Cache-Control headers |
|
|
269
|
+
| `/ppr` | PPR shell caching: cached shell served instantly, live holes resumed — a hole is a `loading()` subtree OR a pending promise under `<Suspense>` (no loader needed) |
|
|
270
|
+
| `/prerender` | Pre-render route segments at build time (Passthrough live fallback) |
|
|
271
|
+
| `/shell-manifest` | Replayed handles as cache metadata read by live loaders (frozen shell, batched live holes) |
|
|
218
272
|
|
|
219
273
|
**Client & presentation** — build the client-side UX:
|
|
220
274
|
|
|
@@ -228,6 +282,7 @@ Grouped by concern — read when you need to…
|
|
|
228
282
|
| `/scripts` | Inject third-party scripts (GTM/analytics) into head/body via the `Script` handle; nonce auto-applied to document-rendered scripts |
|
|
229
283
|
| `/tailwind` | Set up Tailwind CSS v4 with `?url` imports |
|
|
230
284
|
| `/view-transitions` | React View Transitions on layouts, routes, and parallel slots |
|
|
285
|
+
| `/defer-hydration` | Full body HTML in the PPR shell + hydration off the critical path (gated Suspense boundary, content-as-fallback) |
|
|
231
286
|
| `/breadcrumbs` | Built-in Breadcrumbs handle for breadcrumb navigation |
|
|
232
287
|
| `/react-compiler` | Enable React Compiler (opt-in) the vite-rsc way; client-only scope |
|
|
233
288
|
|
|
@@ -325,7 +380,11 @@ dotfiles, and existing `.gen.` files.
|
|
|
325
380
|
### Recursive includes
|
|
326
381
|
|
|
327
382
|
The generator follows `include()` calls across files, resolving imports to build
|
|
328
|
-
the full route tree.
|
|
383
|
+
the full route tree. It resolves both the eager form `include("/x", patterns)`
|
|
384
|
+
and the code-split async form `include("/x", () => import("./x"))` — for the
|
|
385
|
+
latter it walks the imported module's `export default urls(...)`, including any
|
|
386
|
+
nested `include()`s inside it — so a code-split route group is still fully typed
|
|
387
|
+
(see `/composability`). Circular includes are detected and warned about.
|
|
329
388
|
|
|
330
389
|
### First-wins deduplication
|
|
331
390
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: response-routes
|
|
3
|
-
description: Response routes (path.json, path.text, etc.) for non-RSC endpoints with typed responses
|
|
3
|
+
description: Response routes (path.json, path.text, etc.) for non-RSC endpoints with typed responses. Use when building a JSON/text API endpoint alongside your pages, or asking how to return raw JSON instead of RSC from a route.
|
|
4
4
|
argument-hint: [json|text|html|xml|md|image|stream]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -418,6 +418,20 @@ export const urlpatterns = urls(({ path, include }) => [
|
|
|
418
418
|
]);
|
|
419
419
|
```
|
|
420
420
|
|
|
421
|
+
A heavy module like this is a good code-split candidate. Pass an async provider
|
|
422
|
+
and the module — its handlers, response serializers, and any nested
|
|
423
|
+
`include()`s — loads on the first request under the prefix instead of at startup:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
// blog/urls.tsx: `export default blogPatterns`
|
|
427
|
+
include("/blog", () => import("./blog/urls"), { name: "blog" }),
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Named routes and response types still resolve through the split: `TRoutes` and
|
|
431
|
+
the `_responses` phantom are inferred from the resolved `urls()` value, so
|
|
432
|
+
`Rango.PathResponse<"/blog/api/stats">` and `ctx.reverse` are unchanged. See
|
|
433
|
+
`/composability`.
|
|
434
|
+
|
|
421
435
|
### Type safety after mounting
|
|
422
436
|
|
|
423
437
|
```typescript
|
package/skills/route/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: route
|
|
3
|
-
description: Define routes with path() in @rangojs/router
|
|
3
|
+
description: Define routes with path() in @rangojs/router. Use when creating a new page or route, or asking how to define a URL path and its handler.
|
|
4
4
|
argument-hint: [pattern]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -53,6 +53,49 @@ For the common pattern of an optional locale prefix
|
|
|
53
53
|
locale detection, fallback chains, URL generation with absent locale —
|
|
54
54
|
see `/i18n`.
|
|
55
55
|
|
|
56
|
+
### Named catch-all params (`:name+` / `:name*`)
|
|
57
|
+
|
|
58
|
+
A catch-all consumes the **rest of the path** and exposes it as a single
|
|
59
|
+
decoded string at `ctx.params.<name>`, with the internal `/` separators kept.
|
|
60
|
+
It must be the **last** segment of the pattern.
|
|
61
|
+
|
|
62
|
+
- `:name+` — **one-or-more** segments (Next `[...name]`, React-Router splat).
|
|
63
|
+
`/docs/:slug+` matches `/docs/a` and `/docs/a/b/c`, but **not** the bare
|
|
64
|
+
`/docs`.
|
|
65
|
+
- `:name*` — **zero-or-more** segments (Next `[[...name]]`). `/docs/:slug*`
|
|
66
|
+
additionally matches the bare `/docs`, binding `ctx.params.slug` to `""`.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
urls(({ path }) => [
|
|
70
|
+
// /shop/electronics/phones -> ctx.params.path === "electronics/phones"
|
|
71
|
+
path("/shop/:path+", ShopCatchAll, { name: "shopCatchAll" }),
|
|
72
|
+
|
|
73
|
+
// /docs -> ctx.params.slug === ""
|
|
74
|
+
// /docs/intro -> ctx.params.slug === "intro"
|
|
75
|
+
// /docs/a/b -> ctx.params.slug === "a/b"
|
|
76
|
+
path("/docs/:slug*", (ctx) => {
|
|
77
|
+
const parts = ctx.params.slug === "" ? [] : ctx.params.slug.split("/");
|
|
78
|
+
return <Docs segments={parts} />;
|
|
79
|
+
}, { name: "docs" }),
|
|
80
|
+
]);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`ctx.params.<name>` is always a `string` for a catch-all (never `undefined`) —
|
|
84
|
+
`:name*` binds `""` for the empty case, so read it directly. `reverse()` /
|
|
85
|
+
`ctx.reverse()` rebuild the URL with separators preserved:
|
|
86
|
+
`reverse("docs", { slug: "a/b" })` -> `/docs/a/b`.
|
|
87
|
+
|
|
88
|
+
The value is the URL-decoded remainder. `split("/")` recovers the segments in the
|
|
89
|
+
common case, but note that a segment containing an encoded slash (`%2F`) decodes
|
|
90
|
+
to a literal `/` and is therefore indistinguishable from a separator — the same
|
|
91
|
+
trade-off the bare `*` splat has. If you need to distinguish those, match on the
|
|
92
|
+
raw pathname instead.
|
|
93
|
+
|
|
94
|
+
The bare unnamed wildcard `path("/files/*", …)` still works and is read at
|
|
95
|
+
`ctx.params["*"]`; prefer a named catch-all when you want a typed param key.
|
|
96
|
+
Combining a modifier with `?`, a literal suffix, or a constraint
|
|
97
|
+
(`:slug*?`, `:slug*.html`, `:slug(a|b)+`) is rejected at build time.
|
|
98
|
+
|
|
56
99
|
## Route Handler Patterns
|
|
57
100
|
|
|
58
101
|
### Component Function
|
|
@@ -154,6 +197,13 @@ first. Use `ctx.set(key, value)` to share data with children, who read it
|
|
|
154
197
|
via `ctx.get(key)`. Caching wraps all segments together, so either all run
|
|
155
198
|
or none do.
|
|
156
199
|
|
|
200
|
+
This pattern is also safe under partial action revalidation: on an action,
|
|
201
|
+
the route entry re-runs as a unit by default — route segment, loaders, and
|
|
202
|
+
`belongsToRoute` children (orphan layouts, entry parallels) all seed
|
|
203
|
+
revalidate-true, with handler-first ordering preserved. Handler-set data
|
|
204
|
+
stays consistent with no configuration. See `/rango` → "Passing data down
|
|
205
|
+
the tree" for the safest-first ladder.
|
|
206
|
+
|
|
157
207
|
### Typed context variables with createVar
|
|
158
208
|
|
|
159
209
|
Use `createVar<T>()` to create a typed token for `ctx.set()`/`ctx.get()`.
|
|
@@ -240,9 +290,18 @@ Cacheable vars (the default) can be read freely inside cache scopes.
|
|
|
240
290
|
> decides hit/miss/ttl/swr independently and never reads `revalidate()`. See
|
|
241
291
|
> `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
|
|
242
292
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
293
|
+
With no `revalidate()` configured, an entry needs no contract: on an action
|
|
294
|
+
the route handler and its children re-run together by default, so handler
|
|
295
|
+
data stays consistent on its own. Contracts matter in two cases:
|
|
296
|
+
|
|
297
|
+
1. **You narrow the entry's revalidation** with a predicate that can return a
|
|
298
|
+
hard `false` (e.g. bare `ctx.isAction(X)`). A hard `false` on one side of a
|
|
299
|
+
producer/consumer pair desyncs it — the child re-runs by default and reads
|
|
300
|
+
`undefined`, or vice versa. Put the same named contract on the route and
|
|
301
|
+
its dependent children so they narrow together.
|
|
302
|
+
2. **The producer is an outer entry** (a standalone `layout()` above this
|
|
303
|
+
route). Outer entries skip action revalidation by default, so the shared
|
|
304
|
+
contract is mandatory — see `/layout` → "Revalidation Contracts".
|
|
246
305
|
|
|
247
306
|
```typescript
|
|
248
307
|
// revalidation-contracts.ts
|
|
@@ -420,6 +479,14 @@ urls(({ path, layout }) => [
|
|
|
420
479
|
])
|
|
421
480
|
```
|
|
422
481
|
|
|
482
|
+
For composing whole route MODULES, reach for `include()` — and prefer the
|
|
483
|
+
code-split form `include("/shop", () => import("./shop-patterns"))` for any
|
|
484
|
+
group that is a natural unit: it keeps the group off the cold-start path, and
|
|
485
|
+
measured first-hit cost scales with routes-per-chunk, so many small groups
|
|
486
|
+
beat one giant one. Sizing rules and the numbers behind them:
|
|
487
|
+
[skills/composability](../composability/SKILL.md) → "Sizing async include
|
|
488
|
+
groups (measured)".
|
|
489
|
+
|
|
423
490
|
## View Transitions
|
|
424
491
|
|
|
425
492
|
A route can configure its own `transition()` — the wrap goes around the route's component itself (routes are leaves; they have no separate default outlet channel). If the route component renders a `<ParallelOutlet />` directly, that slot remains inside the route's VT subtree, so prefer mounting parallel slots in a layout when combining intercept modals with route-level transitions. See [skills/view-transitions](../view-transitions/SKILL.md) for examples and the wrap-location rules across layouts, routes, and slots.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: router-setup
|
|
3
|
-
description: Create and configure the RSC router with createRouter
|
|
3
|
+
description: Create and configure the RSC router with createRouter. Use when bootstrapping a new Rango app, or configuring top-level router options like base path, cache store, or environment.
|
|
4
4
|
argument-hint: [option]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -60,8 +60,7 @@ urls(
|
|
|
60
60
|
cache, // Configure caching
|
|
61
61
|
middleware, // Add middleware
|
|
62
62
|
revalidate, // Control revalidation
|
|
63
|
-
intercept, // Intercept routes for modals
|
|
64
|
-
when, // Conditional rendering
|
|
63
|
+
intercept, // Intercept routes for modals (conditional via intercept(..., { when }))
|
|
65
64
|
errorBoundary, // Add an error boundary
|
|
66
65
|
notFoundBoundary, // Add a not-found boundary
|
|
67
66
|
transition, // Configure view transitions
|
|
@@ -399,6 +398,18 @@ export const urlpatterns = urls(({ path, include }) => [
|
|
|
399
398
|
]);
|
|
400
399
|
```
|
|
401
400
|
|
|
401
|
+
`include()` also accepts an async provider to code-split that group into its own
|
|
402
|
+
chunk, imported on the first request reaching the prefix instead of at startup:
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
// urls/shop.tsx: `export default shopPatterns`
|
|
406
|
+
include("/shop", () => import("./urls/shop"), { name: "shop" }),
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Build-time discovery still `await`s the provider, so route types, `href()`, and
|
|
410
|
+
prerender see every route in the split group. Reach for it when a group is a
|
|
411
|
+
large, independently-loadable unit — see `/composability`.
|
|
412
|
+
|
|
402
413
|
## Environment Types
|
|
403
414
|
|
|
404
415
|
```typescript
|
package/skills/scripts/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: scripts
|
|
3
|
-
description: Inject third-party scripts (GTM, analytics, widgets) into the document head/body via the Script handle
|
|
3
|
+
description: Inject third-party scripts (GTM, analytics, widgets) into the document head/body via the Script handle. Use when adding Google Tag Manager, an analytics snippet, or a third-party widget script to the page.
|
|
4
4
|
argument-hint: "[vendor]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: server-actions
|
|
3
|
-
description: Define and call server actions (`"use server"`) — forms, useActionState, useOptimistic, validation, error handling, redirects, revalidation
|
|
3
|
+
description: Define and call server actions (`"use server"`) — forms, useActionState, useOptimistic, validation, error handling, redirects, revalidation. Use when handling a form submission on the server, calling a server function from a client component, or needing optimistic UI after a mutation.
|
|
4
4
|
argument-hint: "[action]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -24,7 +24,8 @@ with no framework wrapper. All standard React hooks (`useActionState`,
|
|
|
24
24
|
Use loaders and route handlers for reads. Use actions for writes. After an
|
|
25
25
|
action runs, the matched route tree can partially re-render so handlers and
|
|
26
26
|
loaders that opt into revalidation see the new state — see "Revalidation"
|
|
27
|
-
below.
|
|
27
|
+
below. For the read-side APIs (`createLoader`, `useLoader`, `useFetchLoader`),
|
|
28
|
+
see `/loader`.
|
|
28
29
|
|
|
29
30
|
## Revalidation Model
|
|
30
31
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shell-manifest
|
|
3
|
+
description: Shell manifest pattern — replayed handles as cache metadata that live loaders read, e.g. a prerendered product list with batched live prices. Use when a cached/prerendered shell needs to feed IDs or metadata to a live loader for freshly-fetched data.
|
|
4
|
+
argument-hint:
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Shell Manifest — cache metadata for live loaders
|
|
8
|
+
|
|
9
|
+
Use this when a cached or prerendered shell has dynamic holes, and the live
|
|
10
|
+
data layer needs to know **what the shell actually contains** — which
|
|
11
|
+
products, which slots, which keys. The frozen render describes itself
|
|
12
|
+
through a handle; loaders (always live) read that description and fetch
|
|
13
|
+
exactly the dynamic data the shell needs, in one batch.
|
|
14
|
+
|
|
15
|
+
Canonical case: a prerendered product list where prices must stay live.
|
|
16
|
+
|
|
17
|
+
## The problem this solves
|
|
18
|
+
|
|
19
|
+
Any cached-shell-plus-live-holes design has a coordination gap: how does the
|
|
20
|
+
live layer know what the holes need?
|
|
21
|
+
|
|
22
|
+
- **Per-hole fetching** (each `<Price>` component fetching for itself) is the
|
|
23
|
+
N+1 default — N visible products, N queries.
|
|
24
|
+
- **A loader that re-queries the list** ("current top products") drifts from
|
|
25
|
+
a stale shell — right prices attached to wrong products.
|
|
26
|
+
|
|
27
|
+
The shell manifest closes the gap with a consistency guarantee: the loader
|
|
28
|
+
reads the ids the shell _actually rendered_, replayed from the same stored
|
|
29
|
+
artifact, so the holes can never desync from the shell and the query is
|
|
30
|
+
batched.
|
|
31
|
+
|
|
32
|
+
## The mechanism (three features composed)
|
|
33
|
+
|
|
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
|
+
the cache miss for `cache()`.
|
|
37
|
+
2. **Replay on every hit.** Handle data is stored with the Flight payload
|
|
38
|
+
and replayed into the handle store on cache/prerender hits — handler code
|
|
39
|
+
does not re-run, but its pushes do.
|
|
40
|
+
3. **Loaders read after the render barrier.** A DSL loader can
|
|
41
|
+
`await ctx.rendered()` (waits for all non-loader segments to settle —
|
|
42
|
+
fresh render or replay alike), then `ctx.use(Handle)` returns the
|
|
43
|
+
**collected** handle data.
|
|
44
|
+
|
|
45
|
+
Loaders are live by default, so the read happens on every request even when
|
|
46
|
+
the shell is a hit.
|
|
47
|
+
|
|
48
|
+
## Canonical example: prerendered list, live prices
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// handles/rendered-products.ts
|
|
52
|
+
import { createHandle } from "@rangojs/router";
|
|
53
|
+
|
|
54
|
+
// TData = string (one push per product id), collected to a flat string[]
|
|
55
|
+
export const RenderedProducts = createHandle<string, string[]>((segments) =>
|
|
56
|
+
segments.flat(),
|
|
57
|
+
);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// routes/products.tsx — the list is baked at build time; prices are not
|
|
62
|
+
import { Prerender } from "@rangojs/router";
|
|
63
|
+
import { RenderedProducts } from "../handles/rendered-products";
|
|
64
|
+
import { Price } from "../components/price";
|
|
65
|
+
|
|
66
|
+
export const ProductList = Prerender(
|
|
67
|
+
async () => [{ category: "espresso" }, { category: "filter" }],
|
|
68
|
+
async (ctx) => {
|
|
69
|
+
const products = await db.productsByCategory(ctx.params.category);
|
|
70
|
+
const track = ctx.use(RenderedProducts);
|
|
71
|
+
for (const p of products) track(p.id);
|
|
72
|
+
return (
|
|
73
|
+
<ul>
|
|
74
|
+
{products.map((p) => (
|
|
75
|
+
<li key={p.id}>
|
|
76
|
+
{p.name} <Price id={p.id} />
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// loaders/prices.ts — one batched query for exactly the rendered products
|
|
87
|
+
import { createLoader } from "@rangojs/router";
|
|
88
|
+
import { RenderedProducts } from "../handles/rendered-products";
|
|
89
|
+
|
|
90
|
+
export const PriceLoader = createLoader(async (ctx) => {
|
|
91
|
+
"use server";
|
|
92
|
+
await ctx.rendered();
|
|
93
|
+
const ids = ctx.use(RenderedProducts);
|
|
94
|
+
return db.pricesFor(ids); // Map<string, number> keyed by product id
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
// urls.tsx — wire the route and register the loader
|
|
100
|
+
path("/products/:category", ProductList, { name: "products" }, () => [
|
|
101
|
+
loader(PriceLoader),
|
|
102
|
+
]);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
// components/price.tsx — live hole in the frozen shell
|
|
107
|
+
"use client";
|
|
108
|
+
import { useLoader } from "@rangojs/router/client";
|
|
109
|
+
import { PriceLoader } from "../loaders/prices";
|
|
110
|
+
|
|
111
|
+
export function Price({ id }: { id: string }) {
|
|
112
|
+
const { data } = useLoader(PriceLoader);
|
|
113
|
+
return <span>{formatPrice(data[id])}</span>;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Request flow on a hit: stored payload replays (handler never runs) → handle
|
|
118
|
+
data lands in the store → render barrier resolves → `PriceLoader` reads the
|
|
119
|
+
replayed ids → one query → prices stream into `<Price>` components.
|
|
120
|
+
|
|
121
|
+
## Works with runtime cache() too
|
|
122
|
+
|
|
123
|
+
`Prerender` is build-time caching; the replay mechanism is identical for the
|
|
124
|
+
runtime segment cache. Wrap the route in `cache()` instead and the handler
|
|
125
|
+
pushes on the miss, replays on every hit:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
cache({ ttl: 600, tags: ["products"] }, () => [
|
|
129
|
+
path("/products/:category", ProductList, { name: "products" }, () => [
|
|
130
|
+
loader(PriceLoader),
|
|
131
|
+
]),
|
|
132
|
+
]);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Contract and gotchas
|
|
136
|
+
|
|
137
|
+
- **The manifest is exactly as fresh as the shell.** Replayed handle data is
|
|
138
|
+
frozen with the payload. To change _which_ products render, invalidate the
|
|
139
|
+
shell (`updateTag("products")`, TTL expiry, rebuild) — never treat the
|
|
140
|
+
loader as the refresh path for the list itself. This is the point:
|
|
141
|
+
shell and holes cannot desync because they share one artifact.
|
|
142
|
+
- **No request-scoped data in a manifest handle.** The handle data is baked
|
|
143
|
+
into a shared artifact — the same cross-user rule as any cached content.
|
|
144
|
+
Ids, slugs, slot names, variant keys: yes. Anything derived from
|
|
145
|
+
`cookies()`/`headers()`: no.
|
|
146
|
+
- **`ctx.rendered()` is experimental and DSL-loaders-only.** It throws in
|
|
147
|
+
fetchable/standalone loader calls that run outside a route render.
|
|
148
|
+
- **The reading loader serializes after the shell.** `await ctx.rendered()`
|
|
149
|
+
deliberately gives up loader/render parallelism — on a miss the loader
|
|
150
|
+
waits for segment resolution; on a hit (the common case for a cached
|
|
151
|
+
shell) replay is immediate and the wait is negligible. A
|
|
152
|
+
`debugPerformance` waterfall shows this loader after the render bar; for
|
|
153
|
+
this pattern that is the contract, not a regression.
|
|
154
|
+
- **`ctx.use(Handle)` before `await ctx.rendered()` throws** in a loader,
|
|
155
|
+
with an error saying to await the barrier first.
|
|
156
|
+
- **Deferred handle values are resolved before storage** (resolve-by-default),
|
|
157
|
+
so the manifest read always sees plain values, never promises.
|
|
158
|
+
|
|
159
|
+
## Testing
|
|
160
|
+
|
|
161
|
+
`runLoader` seeds the barrier and the collected handle value directly —
|
|
162
|
+
matched by handle reference (the same seeding style as loader deps):
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { runLoader } from "@rangojs/router/testing";
|
|
166
|
+
|
|
167
|
+
const prices = await runLoader(PriceLoader, {
|
|
168
|
+
rendered: true,
|
|
169
|
+
handles: [[RenderedProducts, ["widget-a", "widget-b"]]],
|
|
170
|
+
env: { DB: fakeDb },
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
This tests the loader's post-barrier logic. The real
|
|
175
|
+
push → store → replay → barrier wiring is covered at the e2e tier (dev +
|
|
176
|
+
production), like every cache-path behavior.
|
|
177
|
+
|
|
178
|
+
## Related
|
|
179
|
+
|
|
180
|
+
- `/prerender` — `Prerender`/`Passthrough`, build flow, passthrough fallback
|
|
181
|
+
- `/caching` — segment `cache()`, stores, tags
|
|
182
|
+
- `/loader` — loader context, `ctx.rendered()`, streaming
|
|
183
|
+
- `/hooks` — `useHandle` for reading handle data in client components
|
|
184
|
+
- `/rango` → "Passing data down the tree" — this pattern is the frozen→live
|
|
185
|
+
counterpart of that ladder
|