@rangojs/router 0.16.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 +255 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- 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 +132 -19
- 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-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +54 -13
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/segment-codec.d.ts +10 -3
- 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/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +220 -44
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +9 -3
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
- package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
- package/dist/types/rsc/shell-capture.d.ts +151 -19
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +17 -0
- package/dist/types/server/context.d.ts +139 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +182 -53
- package/dist/types/ssr/index.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- 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/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +28 -1
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1420 -1348
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +75 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +306 -124
- package/skills/caching/SKILL.md +530 -98
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +65 -38
- package/skills/cloudflare/SKILL.md +87 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +75 -57
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +52 -31
- package/skills/deployment-caching/SKILL.md +11 -3
- package/skills/document-cache/SKILL.md +98 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +55 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +34 -17
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +39 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +386 -128
- package/skills/middleware/SKILL.md +129 -26
- package/skills/migrate-nextjs/SKILL.md +156 -70
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +51 -39
- package/skills/migrate-react-router/component-migration.md +28 -7
- package/skills/migrate-react-router/data-and-actions.md +21 -6
- package/skills/migrate-react-router/route-mapping.md +19 -10
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +92 -14
- package/skills/parallel/SKILL.md +88 -47
- package/skills/ppr/SKILL.md +866 -379
- package/skills/prerender/SKILL.md +210 -108
- package/skills/rango/SKILL.md +115 -65
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +170 -60
- package/skills/router-setup/SKILL.md +420 -240
- package/skills/scripts/SKILL.md +39 -21
- package/skills/server-actions/SKILL.md +231 -59
- package/skills/shell-manifest/SKILL.md +28 -14
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +73 -33
- package/skills/testing/cache-prerender.md +60 -33
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +26 -6
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +155 -30
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +137 -50
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +224 -83
- package/skills/vercel/SKILL.md +35 -13
- package/skills/view-transitions/SKILL.md +42 -21
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +6 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +23 -8
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +264 -68
- package/src/cache/cache-scope.ts +419 -43
- 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/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +133 -28
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +108 -22
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +225 -44
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +73 -22
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +121 -33
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +71 -28
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +141 -43
- package/src/router/loader-resolution.ts +115 -39
- package/src/router/match-api.ts +40 -26
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +110 -10
- package/src/router/match-middleware/cache-store.ts +48 -101
- package/src/router/middleware-types.ts +9 -6
- package/src/router/middleware.ts +75 -13
- package/src/router/prerender-match.ts +59 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +62 -21
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +407 -76
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +208 -56
- package/src/router/segment-resolution/mask-nested.ts +138 -44
- package/src/router/segment-resolution/revalidation.ts +61 -16
- package/src/router/segment-wrappers.ts +4 -0
- 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/render-pipeline.ts +3 -0
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +608 -225
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +73 -6
- package/src/rsc/shell-capture.ts +1068 -296
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +18 -0
- package/src/server/context.ts +300 -32
- package/src/server/cookie-store.ts +87 -61
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +297 -67
- package/src/ssr/index.tsx +136 -44
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +68 -6
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +28 -1
- package/src/types/loader-types.ts +8 -6
- package/src/urls/pattern-types.ts +22 -18
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
- package/src/vite/router-discovery.ts +14 -4
|
@@ -15,7 +15,7 @@ storefront homepage (shell HIT, production build, M-series laptop): a single
|
|
|
15
15
|
was dead for ~11s. Fast paint, frozen page.
|
|
16
16
|
|
|
17
17
|
The obvious fix — wrap the heavy subtree in `<Suspense>` so it hydrates later —
|
|
18
|
-
|
|
18
|
+
can trade the paint away: when that boundary postpones under shell capture, the
|
|
19
19
|
frozen prelude ships an empty `<main>`. This recipe gets both: the full body
|
|
20
20
|
HTML in the prelude AND its hydration off the initial task, released on first
|
|
21
21
|
idle at retry-lane priority.
|
|
@@ -28,11 +28,14 @@ idle at retry-lane priority.
|
|
|
28
28
|
| interactive (menu click works) | ~3.5s | ~1.6s | ~1.6s |
|
|
29
29
|
|
|
30
30
|
Measure your own page before and after — the win depends on how much of the
|
|
31
|
-
hydration cost lives under the boundary (see "Verifying and measuring").
|
|
31
|
+
hydration cost lives under the boundary (see "Verifying and measuring"). These
|
|
32
|
+
numbers were measured before PPR captures baked handler output; see the next
|
|
33
|
+
section for what that changed.
|
|
32
34
|
|
|
33
35
|
## The recipe
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
About 50 lines of plain React, no rango imports. Copy it into a client module in
|
|
38
|
+
your app:
|
|
36
39
|
|
|
37
40
|
```tsx
|
|
38
41
|
"use client";
|
|
@@ -101,23 +104,39 @@ inside hydrates after first idle.
|
|
|
101
104
|
|
|
102
105
|
## Why `fallback={children}` is load-bearing
|
|
103
106
|
|
|
104
|
-
This is not a style choice
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
107
|
+
This is not a style choice. The recipe was built when the shell capture
|
|
108
|
+
postponed any big `<Suspense>` subtree: the capture froze on Flight byte-quiet
|
|
109
|
+
(`FLIGHT_QUIET_HOPS` in `src/rsc/shell-capture.ts`) while the handler output
|
|
110
|
+
was still rendering, so a boundary over the page body became a hole no matter
|
|
111
|
+
where it sat. Verified both ways at the time: wrapping the client island from
|
|
112
|
+
outside AND placing the boundary inside the island both baked
|
|
113
|
+
`<!--$?--><template id="B:…">` into `<main>`, i.e. an empty body in the frozen
|
|
114
|
+
prelude. With the content as the fallback, the postpone became the delivery
|
|
115
|
+
mechanism: the shell baked the fallback, and the fallback was the body.
|
|
116
|
+
|
|
117
|
+
PPR captures now wait for the whole handler layer before freezing anything:
|
|
118
|
+
the capture records the page's segments first (async server components
|
|
119
|
+
rendered, every promise in the handler output settled) and renders its Flight
|
|
120
|
+
payload from that record (`settleCaptureRecord` in `src/rsc/shell-capture.ts`).
|
|
121
|
+
A boundary whose content is only handler output is therefore expected to bake
|
|
122
|
+
its content as a completed boundary instead of postponing. What still
|
|
123
|
+
postpones at capture is a boundary over live data: a loader without
|
|
124
|
+
`ssr: false` read with `useLoader` under `loading()` or an inline
|
|
125
|
+
`<Suspense>`, or a promise nested in an `ssr: false` loader's value. Run the
|
|
126
|
+
prelude check in "Verifying and measuring" on your page rather than assuming
|
|
127
|
+
either shape.
|
|
128
|
+
|
|
129
|
+
`fallback={children}` stays the safe choice either way:
|
|
130
|
+
|
|
131
|
+
- If the boundary still postpones (live data inside it), the fallback is what
|
|
132
|
+
the prelude shows, so it must be the content.
|
|
133
|
+
- The sync-update trap below: a client render of the boundary shows the
|
|
134
|
+
fallback. With `fallback={children}` that is a visual no-op; with
|
|
135
|
+
`fallback={null}` it blanks the page.
|
|
136
|
+
|
|
137
|
+
The hydration deferral does not depend on which shape you get: `HydrationGate`
|
|
138
|
+
suspends during hydration, and React keeps the server DOM of the boundary in
|
|
139
|
+
place until the gate releases.
|
|
121
140
|
|
|
122
141
|
## Why the client gate is free
|
|
123
142
|
|
|
@@ -163,13 +182,14 @@ release on interaction instead (see Variations).
|
|
|
163
182
|
|
|
164
183
|
## Known cost: the body rides twice (measure it, don't guess)
|
|
165
184
|
|
|
166
|
-
|
|
167
|
-
baked fallback in the prelude, once as the
|
|
168
|
-
re-renders and re-ships it; there is no
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
185
|
+
When the boundary postpones at capture, a shell HIT carries the gated
|
|
186
|
+
subtree's HTML twice — once as the baked fallback in the prelude, once as the
|
|
187
|
+
resumed hole content (the resume re-renders and re-ships it; there is no
|
|
188
|
+
bake-through). Homepage measurement (postponing boundary): 234KB → 302KB
|
|
189
|
+
gzipped (+68KB, +29%; raw +1.05MB). It is post-paint bandwidth, not
|
|
190
|
+
render-blocking — the visible prelude streams first — but it is real bytes on
|
|
191
|
+
every document GET. Weigh it per page; on a small body the recipe may not pay
|
|
192
|
+
for itself. A boundary that bakes its content ships it once.
|
|
173
193
|
|
|
174
194
|
## Verifying and measuring
|
|
175
195
|
|
|
@@ -229,7 +249,8 @@ this exact space. The recipe survives that future; a primitive might not.
|
|
|
229
249
|
|
|
230
250
|
## Related
|
|
231
251
|
|
|
232
|
-
- `/ppr` — the shell/hole mechanics this recipe rides on
|
|
233
|
-
|
|
234
|
-
- `src/rsc/shell-capture.ts` — the
|
|
235
|
-
(`FLIGHT_QUIET_HOPS`) that
|
|
252
|
+
- `/ppr` — the shell/hole mechanics this recipe rides on: what bakes into the
|
|
253
|
+
shell, and which boundaries stay holes
|
|
254
|
+
- `src/rsc/shell-capture.ts` — the record-first capture (`settleCaptureRecord`)
|
|
255
|
+
and the byte-quiet window (`FLIGHT_QUIET_HOPS`) that decide what the prelude
|
|
256
|
+
holds
|
|
@@ -10,6 +10,9 @@ Start with one question: **does the request reach Rango before shared bytes are
|
|
|
10
10
|
served?** That boundary decides whether middleware runs, whether loaders stay
|
|
11
11
|
live, and which invalidation system owns the result.
|
|
12
12
|
|
|
13
|
+
This skill is about where each cache sits relative to your deployment platform.
|
|
14
|
+
To choose between the in-app layers themselves, start at `/cache-guide`.
|
|
15
|
+
|
|
13
16
|
## The execution matrix
|
|
14
17
|
|
|
15
18
|
| Mechanism | Stored artifact | Function/worker runs on a hit? | Rango middleware on a hit? | What stays live? |
|
|
@@ -53,8 +56,11 @@ There are two shell producers:
|
|
|
53
56
|
Both producers feed the same in-function serve path. Build-time shells are
|
|
54
57
|
content-hashed modules in the server bundle, not CDN-served HTML files.
|
|
55
58
|
|
|
56
|
-
`ppr.ttl
|
|
57
|
-
|
|
59
|
+
`ppr.ttl` and `ppr.swr` set the shell entry's lifetime, capped by the route
|
|
60
|
+
`cache()` entry it was captured from (the shorter wins). Its tags are
|
|
61
|
+
`ppr.tags` plus the tags its content recorded. A route `cache({ key })` or a
|
|
62
|
+
store `keyGenerator` stores one shell per partition value. None of this emits
|
|
63
|
+
HTTP `Cache-Control` or configures a platform CDN.
|
|
58
64
|
|
|
59
65
|
## Platform deployment shapes
|
|
60
66
|
|
|
@@ -173,4 +179,6 @@ provably public and shared.
|
|
|
173
179
|
- `/document-cache` — store-backed complete-response middleware
|
|
174
180
|
- `/vercel` — Vercel Build Output preset and Runtime Cache wiring
|
|
175
181
|
- `/cloudflare` — Worker deployment and bindings
|
|
176
|
-
- `/cache-guide` —
|
|
182
|
+
- `/cache-guide` — choosing between the in-app cache layers
|
|
183
|
+
- `/caching` — `cache()` segments, stores, and tag invalidation
|
|
184
|
+
- `/use-cache` — function-level `"use cache"`
|
|
@@ -6,13 +6,21 @@ argument-hint: [setup]
|
|
|
6
6
|
|
|
7
7
|
# Store-backed Document Cache
|
|
8
8
|
|
|
9
|
-
Caches complete HTTP responses (HTML
|
|
10
|
-
|
|
9
|
+
Caches complete HTTP responses (HTML documents and RSC payloads) in the
|
|
10
|
+
app-level cache store. Routes opt in by sending a `Cache-Control` header with
|
|
11
|
+
`s-maxage`; `stale-while-revalidate` adds a stale window. On a hit, nothing
|
|
12
|
+
below the middleware runs — no route middleware, handlers, or loaders.
|
|
13
|
+
|
|
14
|
+
Use it for pages whose **whole** response is public and identical for every
|
|
15
|
+
visitor sharing a URL, when you want the reuse to happen inside the app (so
|
|
16
|
+
outer middleware still runs and invalidation stays in Rango's tag system).
|
|
11
17
|
|
|
12
18
|
This middleware runs **inside** the worker/function. It is not itself a platform
|
|
13
|
-
CDN cache.
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
CDN cache. It works with every built-in store: with `CFCacheStore` the response
|
|
20
|
+
family uses Cloudflare's Cache API (and KV when configured), with
|
|
21
|
+
`VercelCacheStore` it uses Vercel Runtime Cache, and `MemorySegmentCacheStore`
|
|
22
|
+
keeps responses in process memory. The request still reaches Rango before the
|
|
23
|
+
middleware can return a store hit.
|
|
16
24
|
|
|
17
25
|
## Not this skill if…
|
|
18
26
|
|
|
@@ -27,9 +35,9 @@ reaches Rango before the middleware can return a store hit.
|
|
|
27
35
|
## Setup
|
|
28
36
|
|
|
29
37
|
Document caching is a middleware. Add `createDocumentCacheMiddleware()` to the
|
|
30
|
-
router with `.use()`.
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
router with `.use()`. It reads and writes the app-level store you configure on
|
|
39
|
+
`createRouter({ cache })`; the store is not a middleware option. Without an app
|
|
40
|
+
store, the middleware passes every request through.
|
|
33
41
|
|
|
34
42
|
```typescript
|
|
35
43
|
import { createRouter } from "@rangojs/router";
|
|
@@ -39,7 +47,7 @@ import {
|
|
|
39
47
|
} from "@rangojs/router/cache";
|
|
40
48
|
import { urlpatterns } from "./urls";
|
|
41
49
|
|
|
42
|
-
const router = createRouter<AppBindings>({
|
|
50
|
+
export const router = createRouter<AppBindings>({
|
|
43
51
|
document: Document,
|
|
44
52
|
urls: urlpatterns,
|
|
45
53
|
// App-level cache store. The document cache middleware uses this store's
|
|
@@ -53,13 +61,14 @@ router.use(
|
|
|
53
61
|
debug: process.env.NODE_ENV === "development",
|
|
54
62
|
}),
|
|
55
63
|
);
|
|
56
|
-
|
|
57
|
-
export default router;
|
|
58
64
|
```
|
|
59
65
|
|
|
66
|
+
Middleware registered before it runs on every request, hits included; everything
|
|
67
|
+
after it (and all route middleware) runs only on a miss.
|
|
68
|
+
|
|
60
69
|
## Route Opt-In with Cache-Control
|
|
61
70
|
|
|
62
|
-
Routes opt
|
|
71
|
+
Routes opt in to document caching by setting a `Cache-Control` response header
|
|
63
72
|
with `s-maxage`. The middleware caches responses whose `Cache-Control` includes
|
|
64
73
|
`s-maxage`; `stale-while-revalidate` enables background revalidation (SWR).
|
|
65
74
|
|
|
@@ -86,6 +95,21 @@ function BlogPostHandler(ctx) {
|
|
|
86
95
|
// Dashboard sets no Cache-Control header, so it is never document-cached.
|
|
87
96
|
```
|
|
88
97
|
|
|
98
|
+
A handler inside a `cache()` boundary or on a `ppr` route cannot write response
|
|
99
|
+
headers (the write throws). Set the header from route middleware instead — it
|
|
100
|
+
runs on every request that reaches the route:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
cache({ ttl: 60 }, () => [
|
|
104
|
+
path("/blog", BlogIndex, { name: "blog" }, () => [
|
|
105
|
+
middleware(async (ctx, next) => {
|
|
106
|
+
ctx.header("Cache-Control", "s-maxage=300, stale-while-revalidate=3600");
|
|
107
|
+
await next();
|
|
108
|
+
}),
|
|
109
|
+
]),
|
|
110
|
+
]);
|
|
111
|
+
```
|
|
112
|
+
|
|
89
113
|
## Document Cache Options
|
|
90
114
|
|
|
91
115
|
`createDocumentCacheMiddleware(options?)` accepts:
|
|
@@ -95,10 +119,11 @@ createDocumentCacheMiddleware({
|
|
|
95
119
|
// Skip specific paths (matched by pathname prefix)
|
|
96
120
|
skipPaths: ["/api", "/admin"],
|
|
97
121
|
|
|
98
|
-
// Custom cache key
|
|
99
|
-
|
|
122
|
+
// Custom cache key base. Replaces host + pathname + search entirely, so
|
|
123
|
+
// include the host yourself when one deployment serves several domains.
|
|
124
|
+
keyGenerator: (url) => `${url.host}${url.pathname}`,
|
|
100
125
|
|
|
101
|
-
// Conditional caching, evaluated per request
|
|
126
|
+
// Conditional caching, evaluated per request (receives the middleware ctx)
|
|
102
127
|
isEnabled: (ctx) => !ctx.request.headers.has("x-preview"),
|
|
103
128
|
|
|
104
129
|
// Debug logging (HIT, MISS, STALE, REVALIDATED)
|
|
@@ -150,18 +175,26 @@ header or logs to identify an actual CDN hit.
|
|
|
150
175
|
|
|
151
176
|
## Cache Key Generation
|
|
152
177
|
|
|
153
|
-
|
|
178
|
+
The default key is the host, pathname, and sorted search string (internal
|
|
179
|
+
`_rsc*` params removed, `cache.searchParams` filtering applied), followed by
|
|
180
|
+
suffixes that separate response variants:
|
|
154
181
|
|
|
155
|
-
- HTML requests: `{pathname}:html`
|
|
156
|
-
- RSC
|
|
182
|
+
- HTML document requests: `{host}{pathname}[?search]:html`
|
|
183
|
+
- RSC requests (client navigations and full-document Flight fetches):
|
|
184
|
+
`{host}{pathname}[?search]:rsc`
|
|
185
|
+
- Client navigations additionally add a hash of the segments the client already
|
|
186
|
+
has (`:{segmentHash}`), so navigations from pages with different layouts get
|
|
187
|
+
different cached payloads, and a `:fragments` marker when the client accepts
|
|
188
|
+
fragment envelopes.
|
|
157
189
|
|
|
158
|
-
|
|
190
|
+
A custom `keyGenerator(url)` replaces only the `{host}{pathname}[?search]` part;
|
|
191
|
+
the variant suffixes are still appended.
|
|
159
192
|
|
|
160
193
|
## What Gets Cached
|
|
161
194
|
|
|
162
195
|
- Full HTML responses (document requests)
|
|
163
196
|
- RSC payloads (client navigation)
|
|
164
|
-
- Only 200
|
|
197
|
+
- Only `GET` 200 responses whose `Cache-Control` includes `s-maxage`
|
|
165
198
|
|
|
166
199
|
## What's NOT Cached
|
|
167
200
|
|
|
@@ -169,7 +202,41 @@ Segment hash ensures different cached responses for navigations from different s
|
|
|
169
202
|
- Loader requests (`_rsc_loader`)
|
|
170
203
|
- Non-GET requests
|
|
171
204
|
- Responses without an `s-maxage` `Cache-Control` directive
|
|
205
|
+
- Responses whose `Cache-Control` also contains `private`, `no-store`, or an
|
|
206
|
+
unqualified `no-cache` (those win over `s-maxage` in a shared cache)
|
|
207
|
+
- Responses carrying `Set-Cookie` (a per-client value must not be replayed to
|
|
208
|
+
everyone). That includes a response whose request called
|
|
209
|
+
`updateTag()`/`revalidateTag()` on a store that keeps per-isolate memos
|
|
210
|
+
(`CFCacheStore`, `VercelCacheStore`): it carries the fresh-reads cookie
|
|
211
|
+
(`rango-state-fresh` by default), so it is not stored.
|
|
172
212
|
- Non-200 responses
|
|
213
|
+
- A 200 whose render reported an error: a component that threw after the
|
|
214
|
+
response started streaming, such as an async server component whose fetch
|
|
215
|
+
failed. The status is already 200, so the body carries the error (an error
|
|
216
|
+
row in the RSC payload, an errored Suspense boundary in the HTML). The write
|
|
217
|
+
is skipped and logged as `[DocumentCache] cache write`; the error goes to
|
|
218
|
+
`onError` as `cache-write` unless the router already reported that same
|
|
219
|
+
error during the render. The current request still gets the errored render,
|
|
220
|
+
the next request renders fresh, and on a stale refresh the stale entry keeps
|
|
221
|
+
serving.
|
|
222
|
+
|
|
223
|
+
## Invalidation
|
|
224
|
+
|
|
225
|
+
Entries expire by `s-maxage` (+ `stale-while-revalidate`). They are also tagged
|
|
226
|
+
with the request's tag set, so `updateTag()`/`revalidateTag()` evict them:
|
|
227
|
+
|
|
228
|
+
- a render-callable `cacheTag("...")` in any server component on the page;
|
|
229
|
+
- tags of `cache()` segments, cached loaders, and `"use cache"` functions the
|
|
230
|
+
render read. A `cache()` segment HIT contributes the tags its content
|
|
231
|
+
recorded when it was written (render-callable calls and `"use cache"` reads
|
|
232
|
+
inside it), so a document stored over the HIT stays evictable by them. A
|
|
233
|
+
cached loader's HIT likewise contributes the tags its body recorded.
|
|
234
|
+
|
|
235
|
+
Tags are snapshotted after the response body has fully streamed, so tags from
|
|
236
|
+
Suspense-streamed content are included. A `Prerender()` route's build-time
|
|
237
|
+
segments contribute no tags (only its live loaders can), so such an entry
|
|
238
|
+
usually expires by TTL only.
|
|
239
|
+
See `/caching` → "Tag-Based Invalidation".
|
|
173
240
|
|
|
174
241
|
## Complete Example
|
|
175
242
|
|
|
@@ -179,7 +246,7 @@ import { createRouter } from "@rangojs/router";
|
|
|
179
246
|
import { createDocumentCacheMiddleware, CFCacheStore } from "@rangojs/router/cache";
|
|
180
247
|
import { urlpatterns } from "./urls";
|
|
181
248
|
|
|
182
|
-
const router = createRouter<AppBindings>({
|
|
249
|
+
export const router = createRouter<AppBindings>({
|
|
183
250
|
document: Document,
|
|
184
251
|
urls: urlpatterns,
|
|
185
252
|
cache: (_env, ctx) => ({ store: new CFCacheStore({ ctx: ctx! }) }),
|
|
@@ -192,7 +259,6 @@ router.use(
|
|
|
192
259
|
}),
|
|
193
260
|
);
|
|
194
261
|
|
|
195
|
-
export default router;
|
|
196
262
|
|
|
197
263
|
// urls.tsx
|
|
198
264
|
import { urls } from "@rangojs/router";
|
|
@@ -225,14 +291,18 @@ function BlogPost(ctx) {
|
|
|
225
291
|
}
|
|
226
292
|
```
|
|
227
293
|
|
|
294
|
+
Note that `BlogPostLoader` output is frozen into the cached response along with
|
|
295
|
+
everything else.
|
|
296
|
+
|
|
228
297
|
## Document Cache vs Segment Cache
|
|
229
298
|
|
|
230
|
-
| Feature | Document Cache
|
|
231
|
-
| ------------ |
|
|
232
|
-
| Granularity | Full response
|
|
233
|
-
| Opt-in | `Cache-Control` `s-maxage`
|
|
234
|
-
|
|
|
235
|
-
|
|
|
299
|
+
| Feature | Document Cache | Segment Cache (`cache()`) |
|
|
300
|
+
| ------------ | ------------------------------------- | --------------------------------- |
|
|
301
|
+
| Granularity | Full response | Individual segments |
|
|
302
|
+
| Opt-in | `Cache-Control` `s-maxage` | `cache({ ttl, swr })` |
|
|
303
|
+
| Loaders | Frozen with the response | Always run fresh |
|
|
304
|
+
| Use case | Static pages | Dynamic compositions |
|
|
305
|
+
| Key includes | host + URL + variant (+ segment hash) | host + pathname + params + search |
|
|
236
306
|
|
|
237
307
|
Use document cache for mostly-static pages. Use segment cache when different parts of a page have different cache requirements.
|
|
238
308
|
|
package/skills/fonts/SKILL.md
CHANGED
|
@@ -6,7 +6,10 @@ argument-hint: [provider]
|
|
|
6
6
|
|
|
7
7
|
# Fonts
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Three ways to load web fonts in a Rango app: a hosted provider (Google Fonts),
|
|
10
|
+
self-hosted files in `public/`, or Fontsource packages bundled by Vite (the
|
|
11
|
+
recommended default). Font links go in the Document `<head>` next to your
|
|
12
|
+
stylesheet, which follows the `?url` + `precedence` pattern from `/css`.
|
|
10
13
|
|
|
11
14
|
## Google Fonts
|
|
12
15
|
|
|
@@ -15,7 +18,7 @@ Load web fonts in the Document component with `<link rel="preload">` for optimal
|
|
|
15
18
|
"use client";
|
|
16
19
|
|
|
17
20
|
import type { ReactNode } from "react";
|
|
18
|
-
import {
|
|
21
|
+
import { Html } from "@rangojs/router/client";
|
|
19
22
|
import styles from "./index.css?url";
|
|
20
23
|
|
|
21
24
|
export function Document({ children }: { children: ReactNode }) {
|
|
@@ -30,16 +33,17 @@ export function Document({ children }: { children: ReactNode }) {
|
|
|
30
33
|
crossOrigin="anonymous"
|
|
31
34
|
/>
|
|
32
35
|
|
|
33
|
-
{/*
|
|
36
|
+
{/* Font stylesheet first, so app styles can override it */}
|
|
34
37
|
<link
|
|
35
38
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
36
39
|
rel="stylesheet"
|
|
40
|
+
precedence="default"
|
|
37
41
|
/>
|
|
38
42
|
|
|
39
43
|
{/* App styles */}
|
|
40
44
|
<link rel="preload" href={styles} as="style" />
|
|
41
|
-
<link rel="stylesheet" href={styles} />
|
|
42
|
-
<
|
|
45
|
+
<link rel="stylesheet" href={styles} precedence="default" />
|
|
46
|
+
<Html.Meta />
|
|
43
47
|
</head>
|
|
44
48
|
<body>{children}</body>
|
|
45
49
|
</html>
|
|
@@ -60,6 +64,8 @@ Or with Tailwind (see `/tailwind`):
|
|
|
60
64
|
|
|
61
65
|
```css
|
|
62
66
|
/* src/index.css */
|
|
67
|
+
@import "tailwindcss";
|
|
68
|
+
|
|
63
69
|
@theme {
|
|
64
70
|
--font-sans: "Inter", sans-serif;
|
|
65
71
|
}
|
|
@@ -92,7 +98,9 @@ body {
|
|
|
92
98
|
}
|
|
93
99
|
```
|
|
94
100
|
|
|
95
|
-
Preload the most critical weight in the Document
|
|
101
|
+
Preload the most critical weight in the Document (same imports as the Google
|
|
102
|
+
Fonts example above). Font preloads need `crossOrigin` even for same-origin
|
|
103
|
+
files, or the browser fetches the font twice:
|
|
96
104
|
|
|
97
105
|
```tsx
|
|
98
106
|
export function Document({ children }: { children: ReactNode }) {
|
|
@@ -107,8 +115,8 @@ export function Document({ children }: { children: ReactNode }) {
|
|
|
107
115
|
crossOrigin="anonymous"
|
|
108
116
|
/>
|
|
109
117
|
<link rel="preload" href={styles} as="style" />
|
|
110
|
-
<link rel="stylesheet" href={styles} />
|
|
111
|
-
<
|
|
118
|
+
<link rel="stylesheet" href={styles} precedence="default" />
|
|
119
|
+
<Html.Meta />
|
|
112
120
|
</head>
|
|
113
121
|
<body>{children}</body>
|
|
114
122
|
</html>
|
|
@@ -6,7 +6,7 @@ argument-hint: "[handler]"
|
|
|
6
6
|
|
|
7
7
|
# Handler-Attached `.use`
|
|
8
8
|
|
|
9
|
-
A handler function (or branded `Static`/`Prerender`/`Passthrough` definition) can carry its own defaults via a `.use` callback that returns an array of `use` items (loader, middleware, parallel, intercept, layout, loading, etc.). The mount-site DSL (`path()`, `layout()`, `parallel()`, `intercept()`) merges those defaults with any explicit `use()` callback supplied at the registration site.
|
|
9
|
+
A handler function (or branded `Static`/`Prerender`/`Passthrough` definition) can carry its own defaults via a `.use` callback that returns an array of `use` items (loader, middleware, parallel, intercept, layout, loading, etc.). The mount-site DSL (`path()`, `layout()`, `parallel()`, `intercept()`) merges those defaults with any explicit `use()` callback supplied at the registration site. Loader definitions from `createLoader()` accept a `.use` too, limited to `revalidate` and `cache` (see "Loaders" below).
|
|
10
10
|
|
|
11
11
|
This lets handlers be **self-contained, reusable units** — a page brings its own loader, a layout brings its own middleware, a parallel slot brings its own data + skeleton — without forcing every caller to wire the same items at every mount site.
|
|
12
12
|
|
|
@@ -51,13 +51,16 @@ Now `ProductPage` carries its loader, loading state, and response-header middlew
|
|
|
51
51
|
|
|
52
52
|
`handler.use()` is the same callback shape regardless of where the handler runs, but the runtime validates that the items it returns are valid for the mount site. Driven by `MOUNT_SITE_ALLOWED_TYPES` in [resolve-handler-use.ts](../../src/route-definition/resolve-handler-use.ts):
|
|
53
53
|
|
|
54
|
-
| Mount site | Allowed item types
|
|
55
|
-
| ------------------------------------------------- |
|
|
56
|
-
| `path()`
|
|
57
|
-
| `layout()` | All of the above, plus `route
|
|
58
|
-
| `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition`
|
|
59
|
-
| `intercept()` | `middleware`, `
|
|
60
|
-
| Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache`
|
|
54
|
+
| Mount site | Allowed item types |
|
|
55
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
56
|
+
| `path()` | `layout`, `parallel`, `intercept`, `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `cache`, `transition` |
|
|
57
|
+
| `layout()` | All of the above, plus `route` (nested `path()` items) and `include` |
|
|
58
|
+
| `parallel()` (per slot) | `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `transition` |
|
|
59
|
+
| `intercept()` | `middleware`, `loader`, `loading`, `layout` (modal chrome, without its own `use()` items), `route`, `transition`. Conditional activation is config-only: `intercept(..., { when })`. Anything else throws with a pointer: `revalidate` goes on the intercept's loader (`loader(Def, () => [revalidate(...)])`), `errorBoundary`/`notFoundBoundary` on the enclosing layout, `cache` on the target route |
|
|
60
|
+
| Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
|
|
61
|
+
| `loader()` (a `createLoader()` definition) | `revalidate`, `cache` |
|
|
62
|
+
|
|
63
|
+
`route` is the item type a `path()` call produces.
|
|
61
64
|
|
|
62
65
|
For per-item semantics see the dedicated skills: [middleware](../middleware/SKILL.md), [loader](../loader/SKILL.md), [parallel](../parallel/SKILL.md), [intercept](../intercept/SKILL.md), [layout](../layout/SKILL.md), [view-transitions](../view-transitions/SKILL.md).
|
|
63
66
|
|
|
@@ -68,7 +71,13 @@ handler.use() returned middleware() which is not valid inside parallel().
|
|
|
68
71
|
Allowed types: revalidate, loader, loading, errorBoundary, notFoundBoundary, transition.
|
|
69
72
|
```
|
|
70
73
|
|
|
71
|
-
The narrowest contract is `parallel()` — slots cannot bring their own middleware or layout; only data, loading, error/notFound boundaries, revalidation, and transitions.
|
|
74
|
+
The narrowest contract for a handler is `parallel()` — slots cannot bring their own middleware or layout; only data, loading, error/notFound boundaries, revalidation, and transitions.
|
|
75
|
+
|
|
76
|
+
This runtime check applies to items returned by `handler.use()`. Items in an explicit mount-site `use()` callback are constrained by the TypeScript use-item types (e.g. a `parallel()` callback is typed to parallel items), not by this runtime allow-list. `intercept()` is the exception: it checks its merged items, explicit `use()` and `handler.use()` together, against the same list at runtime (`validateInterceptUseItems`), and a rejected item throws with a pointer instead:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
errorBoundary() is not valid inside intercept("@modal", "product") use() (including the handler's .use): put it on the enclosing layout or path; it handles the intercept's handler and loader errors.
|
|
80
|
+
```
|
|
72
81
|
|
|
73
82
|
## Composition with explicit `use()`
|
|
74
83
|
|
|
@@ -175,6 +184,8 @@ parallel({
|
|
|
175
184
|
});
|
|
176
185
|
```
|
|
177
186
|
|
|
187
|
+
On a `ppr` route, a slot's `loading()` over handler output does not make a hole. A slot handler that awaits `ctx.use(CartLoader)` is handler output: it runs at capture, its value is baked into the shared shell, and every HIT replays it without running the handler. A `cookies()`/`headers()` read in that loader refuses the capture, so the route stays on MISS. For a per-request slot, read the loader with `useLoader()` in a `"use client"` component under the slot's `loading()` (see `/ppr` → "The layout-with-loaders playbook").
|
|
188
|
+
|
|
178
189
|
### Two scopes for explicit `use` at the mount site: shared (broadcast) and slot-local
|
|
179
190
|
|
|
180
191
|
`parallel()` accepts an explicit `use()` callback that **broadcasts** to every slot in the call ([dsl-helpers.ts](../../src/route-definition/dsl-helpers.ts)). That's the right behavior for the items the parallel allow-list permits and that accumulate (`loader`, `revalidate`, `errorBoundary`, `notFoundBoundary`, `transition`) — every slot gets them. (Note: `middleware` is not allowed inside `parallel()`; see the allowed-types table above.)
|
|
@@ -281,7 +292,7 @@ AdminLayout.use = () => [
|
|
|
281
292
|
|
|
282
293
|
### Intercepts (`intercept()`)
|
|
283
294
|
|
|
284
|
-
Intercept handlers can carry their own middleware chain, loaders, and
|
|
295
|
+
Intercept handlers can carry their own middleware chain, loaders, `loading()`, and the modal chrome: a `layout(Chrome)` with no `use()` items of its own. Boundaries, `cache()` and `revalidate()` throw here (see the table above and [intercept](../intercept/SKILL.md)).
|
|
285
296
|
|
|
286
297
|
```typescript
|
|
287
298
|
const QuickViewModal: Handler = async (ctx) => {
|
|
@@ -295,6 +306,30 @@ QuickViewModal.use = () => [
|
|
|
295
306
|
];
|
|
296
307
|
```
|
|
297
308
|
|
|
309
|
+
### Loaders (`loader()`)
|
|
310
|
+
|
|
311
|
+
A loader definition can carry its own cache and revalidation defaults. They are
|
|
312
|
+
merged into every `loader(Def)` registration, before the explicit
|
|
313
|
+
`loader(Def, () => [...])` items:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { cache, createLoader, revalidate } from "@rangojs/router";
|
|
317
|
+
import * as CatalogActions from "./actions/catalog";
|
|
318
|
+
|
|
319
|
+
export const CategoriesLoader = createLoader(async () => fetchCategories());
|
|
320
|
+
CategoriesLoader.use = () => [
|
|
321
|
+
cache({ ttl: 300 }),
|
|
322
|
+
// Loaders re-run after every action by default. Re-run this one only for
|
|
323
|
+
// catalog actions; on navigation, undefined keeps the default.
|
|
324
|
+
revalidate((ctx) =>
|
|
325
|
+
ctx.isAction() ? ctx.isAction(CatalogActions) : undefined,
|
|
326
|
+
),
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
// Mount sites get the cache + revalidation defaults with no extra wiring.
|
|
330
|
+
layout(<ShopLayout />, () => [loader(CategoriesLoader) /* … */]);
|
|
331
|
+
```
|
|
332
|
+
|
|
298
333
|
## `loading()` is a single-assignment item — scope it correctly
|
|
299
334
|
|
|
300
335
|
Most `use` items accumulate when merged: `handler.use` `middleware()` runs _and_ explicit `middleware()` runs; both `loader()` registrations apply. `loading()` is different — it mutates `entry.loading` directly, last call wins ([dsl-helpers.ts `loading`](../../src/route-definition/dsl-helpers.ts)).
|
|
@@ -310,7 +345,7 @@ Cart.use = () => [loader(CartLoader), loading(<CartSkeleton />)];
|
|
|
310
345
|
const Notifs: Handler = async (ctx) => { /* … */ };
|
|
311
346
|
Notifs.use = () => [loader(NotifsLoader), loading(<NotifsSkeleton />)];
|
|
312
347
|
|
|
313
|
-
//
|
|
348
|
+
// @cart gets a custom skeleton; @notifs keeps its handler.use default.
|
|
314
349
|
parallel({
|
|
315
350
|
"@cart": {
|
|
316
351
|
handler: Cart,
|
|
@@ -319,7 +354,7 @@ parallel({
|
|
|
319
354
|
"@notifs": Notifs,
|
|
320
355
|
});
|
|
321
356
|
|
|
322
|
-
//
|
|
357
|
+
// Opt one slot out of streaming while siblings still stream the broadcast.
|
|
323
358
|
parallel(
|
|
324
359
|
{
|
|
325
360
|
"@cart": { handler: Cart, use: () => [loading(false)] },
|
|
@@ -333,7 +368,7 @@ Per-slot merge order is **handler.use → shared use → slot-local use**. Slot-
|
|
|
333
368
|
|
|
334
369
|
Other things to keep in mind about `loading()`:
|
|
335
370
|
|
|
336
|
-
- Any `loading()` (regardless of source) makes the segment a streaming unit. A handler that includes `loading()` in its `.use` opts every mount site into streaming by default. To opt back out, pass `loading(false)` at the mount site (`loading
|
|
371
|
+
- Any `loading()` (regardless of source) makes the segment a streaming unit. A handler that includes `loading()` in its `.use` opts every mount site into streaming by default. To opt back out, pass `loading(false)` at the mount site (`loading === false` handling in [segment-resolution/fresh.ts](../../src/router/segment-resolution/fresh.ts)) — use the slot descriptor form for parallel slots so the opt-out doesn't broadcast.
|
|
337
372
|
|
|
338
373
|
Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every mount site to stream by default. Use the slot descriptor's `use` for any per-slot intent at a `parallel()` call.
|
|
339
374
|
|
|
@@ -343,10 +378,13 @@ Rule of thumb: only put `loading()` in `handler.use` if you genuinely want every
|
|
|
343
378
|
- **Branded handlers** — `Static()`, `Prerender()`, and `Passthrough()` are positional constructors (not object-arg). Construct first, then attach `.use` to the returned definition:
|
|
344
379
|
|
|
345
380
|
```typescript
|
|
346
|
-
const ProductPage = Prerender(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
381
|
+
const ProductPage = Prerender(
|
|
382
|
+
async () => [{ slug: "shoes" }, { slug: "jacket" }], // getParams
|
|
383
|
+
async (ctx) => {
|
|
384
|
+
const product = await fetchProduct(ctx.params.slug);
|
|
385
|
+
return <ProductView product={product} />;
|
|
386
|
+
},
|
|
387
|
+
);
|
|
350
388
|
ProductPage.use = () => [loader(ProductLoader)];
|
|
351
389
|
```
|
|
352
390
|
|