@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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 +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
package/skills/loader/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loader
|
|
3
3
|
description: Define data loaders for fetching data in routes with createLoader
|
|
4
|
-
argument-hint: [
|
|
4
|
+
argument-hint: [loader]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Data Loaders with loader()
|
|
@@ -13,7 +13,9 @@ Loaders fetch data on the server and stream it to the client.
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { createLoader } from "@rangojs/router";
|
|
15
15
|
|
|
16
|
-
export const ProductLoader = createLoader(
|
|
16
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
17
|
+
"use server";
|
|
18
|
+
|
|
17
19
|
const product = await ctx.env.DB.prepare(
|
|
18
20
|
"SELECT * FROM products WHERE slug = ?",
|
|
19
21
|
)
|
|
@@ -30,19 +32,19 @@ All of the following are equivalent and fully supported by the Vite transform:
|
|
|
30
32
|
|
|
31
33
|
```typescript
|
|
32
34
|
// Direct export (most common)
|
|
33
|
-
export const ProductLoader = createLoader(
|
|
35
|
+
export const ProductLoader = createLoader(handler);
|
|
34
36
|
|
|
35
37
|
// Separate declaration + named export
|
|
36
|
-
const ProductLoader = createLoader(
|
|
38
|
+
const ProductLoader = createLoader(handler);
|
|
37
39
|
export { ProductLoader };
|
|
38
40
|
|
|
39
41
|
// Aliased export
|
|
40
|
-
const InternalLoader = createLoader(
|
|
42
|
+
const InternalLoader = createLoader(handler);
|
|
41
43
|
export { InternalLoader as ProductLoader };
|
|
42
44
|
|
|
43
45
|
// Aliased import
|
|
44
46
|
import { createLoader as cl } from "@rangojs/router";
|
|
45
|
-
export const ProductLoader = cl(
|
|
47
|
+
export const ProductLoader = cl(handler);
|
|
46
48
|
```
|
|
47
49
|
|
|
48
50
|
The `export const` form and the `const + export { }` form both work for
|
|
@@ -66,7 +68,7 @@ export const urlpatterns = urls(({ path, loader }) => [
|
|
|
66
68
|
### In Server Components
|
|
67
69
|
|
|
68
70
|
```typescript
|
|
69
|
-
import { useLoader } from "@rangojs/router";
|
|
71
|
+
import { useLoader } from "@rangojs/router/client";
|
|
70
72
|
import { ProductLoader } from "./loaders/product";
|
|
71
73
|
|
|
72
74
|
async function ProductPage() {
|
|
@@ -79,12 +81,12 @@ async function ProductPage() {
|
|
|
79
81
|
|
|
80
82
|
```typescript
|
|
81
83
|
"use client";
|
|
82
|
-
import {
|
|
84
|
+
import { useLoader } from "@rangojs/router/client";
|
|
83
85
|
import { ProductLoader } from "./loaders/product";
|
|
84
86
|
|
|
85
87
|
function ProductDetails() {
|
|
86
|
-
const {
|
|
87
|
-
return <div>{product.description}</div>;
|
|
88
|
+
const { data } = useLoader(ProductLoader);
|
|
89
|
+
return <div>{data.product.description}</div>;
|
|
88
90
|
}
|
|
89
91
|
```
|
|
90
92
|
|
|
@@ -93,10 +95,15 @@ function ProductDetails() {
|
|
|
93
95
|
Loaders receive the same context as route handlers:
|
|
94
96
|
|
|
95
97
|
```typescript
|
|
96
|
-
export const ProductLoader = createLoader(
|
|
97
|
-
|
|
98
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
99
|
+
"use server";
|
|
100
|
+
|
|
101
|
+
// URL params (may include client-provided overrides for fetchable loaders)
|
|
98
102
|
const { slug } = ctx.params;
|
|
99
103
|
|
|
104
|
+
// Server-trusted route params (from URL pattern matching, cannot be overridden)
|
|
105
|
+
const { slug: trustedSlug } = ctx.routeParams;
|
|
106
|
+
|
|
100
107
|
// Query params
|
|
101
108
|
const variant = ctx.url.searchParams.get("variant");
|
|
102
109
|
|
|
@@ -113,6 +120,33 @@ export const ProductLoader = createLoader("product", async (ctx) => {
|
|
|
113
120
|
});
|
|
114
121
|
```
|
|
115
122
|
|
|
123
|
+
### params vs routeParams
|
|
124
|
+
|
|
125
|
+
- `ctx.params` — merged route params + explicit loader params. For fetchable
|
|
126
|
+
loaders called with `load(Loader, { params: { ... } })`, explicit params
|
|
127
|
+
override route-matched params.
|
|
128
|
+
- `ctx.routeParams` — server-trusted route params from URL pattern matching.
|
|
129
|
+
Cannot be overridden by client-provided params.
|
|
130
|
+
|
|
131
|
+
Use `ctx.routeParams` when you need trusted route identity for authorization
|
|
132
|
+
or resource scoping:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
export const OrderLoader = createLoader(async (ctx) => {
|
|
136
|
+
"use server";
|
|
137
|
+
|
|
138
|
+
// Use routeParams for auth checks — client cannot spoof the URL-matched ID
|
|
139
|
+
const { orderId } = ctx.routeParams;
|
|
140
|
+
const user = ctx.get("user");
|
|
141
|
+
|
|
142
|
+
const order = await db.orders.get(orderId);
|
|
143
|
+
if (order.userId !== user.id)
|
|
144
|
+
throw new Response("Forbidden", { status: 403 });
|
|
145
|
+
|
|
146
|
+
return { order };
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
116
150
|
## Loader with Children
|
|
117
151
|
|
|
118
152
|
Add caching or revalidation to specific loaders:
|
|
@@ -134,6 +168,157 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
134
168
|
]);
|
|
135
169
|
```
|
|
136
170
|
|
|
171
|
+
### Revalidation Contracts for Loader Dependencies
|
|
172
|
+
|
|
173
|
+
If a loader reads `ctx.get()` data produced by an outer handler/layout, share
|
|
174
|
+
the same named revalidation contract across producer and consumer segments.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// revalidation-contracts.ts
|
|
178
|
+
export const revalidateAccountScope = ({ actionId }) =>
|
|
179
|
+
actionId?.includes("src/actions/account.ts#") ?? false;
|
|
180
|
+
|
|
181
|
+
layout(AccountLayout, () => [
|
|
182
|
+
revalidate(revalidateAccountScope), // producer reruns
|
|
183
|
+
path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
|
|
184
|
+
loader(OrdersLoader, () => [
|
|
185
|
+
revalidate(revalidateAccountScope), // consumer reruns
|
|
186
|
+
]),
|
|
187
|
+
]),
|
|
188
|
+
]);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
For segments that depend on multiple upstream domains, compose multiple
|
|
192
|
+
contracts on both sides.
|
|
193
|
+
|
|
194
|
+
To keep loader route trees concise, export helper wrappers:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { revalidate } from "@rangojs/router";
|
|
198
|
+
|
|
199
|
+
export const revalidateAccount = () => [revalidate(revalidateAccountScope)];
|
|
200
|
+
|
|
201
|
+
layout(AccountLayout, () => [
|
|
202
|
+
revalidateAccount(),
|
|
203
|
+
path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
|
|
204
|
+
loader(OrdersLoader, () => [revalidateAccount()]),
|
|
205
|
+
]),
|
|
206
|
+
]);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Loaders: The Live Data Layer
|
|
210
|
+
|
|
211
|
+
Loaders are the live data layer of the router. They resolve fresh on every
|
|
212
|
+
request, even when the route's UI segments are served from cache. This is a
|
|
213
|
+
core design principle — route-level `cache()` caches rendered components but
|
|
214
|
+
never caches loader data. Loaders are excluded at storage time and re-resolved
|
|
215
|
+
on retrieval.
|
|
216
|
+
|
|
217
|
+
This means `cache()` gives you cached UI + fresh data by default. Pre-rendering
|
|
218
|
+
follows the same rule: at build time, loaders are skipped entirely (there is no
|
|
219
|
+
real request context), and at runtime the worker resolves them fresh against
|
|
220
|
+
the live database.
|
|
221
|
+
|
|
222
|
+
### Opting a Loader into Caching
|
|
223
|
+
|
|
224
|
+
To cache a specific loader's data, attach a `cache()` child:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
loader(ProductLoader, () => [cache({ ttl: 300 })]),
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The loader's data is cached independently from the route's segment cache,
|
|
231
|
+
using the same `SegmentCacheStore` (app-level or per-loader override).
|
|
232
|
+
|
|
233
|
+
Values are serialized through RSC Flight, so loaders can return ReactNode,
|
|
234
|
+
Promises, null, and any RSC-serializable type — all round-trip correctly
|
|
235
|
+
through the cache.
|
|
236
|
+
|
|
237
|
+
### Cache Key
|
|
238
|
+
|
|
239
|
+
The default cache key is `loader:{loaderId}:{pathname}:{sortedParams}`.
|
|
240
|
+
This can be customized at two levels:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// Full override — key function replaces the default entirely
|
|
244
|
+
loader(ProductLoader, () => [
|
|
245
|
+
cache({
|
|
246
|
+
ttl: 300,
|
|
247
|
+
key: (ctx) => `product:${ctx.params.slug}:${cookies().get("locale")?.value ?? "en"}`,
|
|
248
|
+
}),
|
|
249
|
+
]),
|
|
250
|
+
|
|
251
|
+
// Store-level keyGenerator — modifies the default key (e.g., adds a region prefix)
|
|
252
|
+
// Set in the store configuration, applies to all entries in that store
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Resolution priority (same as route-level `cache()`):
|
|
256
|
+
|
|
257
|
+
1. `key(ctx)` from cache options — full override
|
|
258
|
+
2. `store.keyGenerator(ctx, defaultKey)` — store-level modification
|
|
259
|
+
3. Default key — `loader:{id}:{pathname}:{params}`
|
|
260
|
+
|
|
261
|
+
If a custom key function throws, it falls back to the default key silently
|
|
262
|
+
(logged to console.error).
|
|
263
|
+
|
|
264
|
+
### Tags for Invalidation
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// Static tags
|
|
268
|
+
loader(ProductLoader, () => [
|
|
269
|
+
cache({ ttl: 300, tags: ["products", "catalog"] }),
|
|
270
|
+
]),
|
|
271
|
+
|
|
272
|
+
// Dynamic tags
|
|
273
|
+
loader(ProductLoader, () => [
|
|
274
|
+
cache({
|
|
275
|
+
ttl: 300,
|
|
276
|
+
tags: (ctx) => [`product:${ctx.params.slug}`, "products"],
|
|
277
|
+
}),
|
|
278
|
+
]),
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Stale-While-Revalidate
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
loader(ProductLoader, () => [
|
|
285
|
+
cache({ ttl: 60, swr: 300 }),
|
|
286
|
+
]),
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
During the SWR window (60-360s), stale data is returned immediately while
|
|
290
|
+
fresh data is fetched in the background via `waitUntil`. After the SWR window
|
|
291
|
+
expires (360s+), the entry is treated as a cache miss.
|
|
292
|
+
|
|
293
|
+
### Conditional Caching
|
|
294
|
+
|
|
295
|
+
Skip the cache at runtime based on request properties:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
loader(ProductLoader, () => [
|
|
299
|
+
cache({
|
|
300
|
+
ttl: 300,
|
|
301
|
+
condition: (ctx) => !ctx.request.headers.has("authorization"),
|
|
302
|
+
}),
|
|
303
|
+
]),
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
When `condition` returns false, the loader runs fresh and the cache is bypassed
|
|
307
|
+
entirely (no read, no write).
|
|
308
|
+
|
|
309
|
+
### Per-Loader Store Override
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const hotStore = new MemorySegmentCacheStore({ defaults: { ttl: 10 } });
|
|
313
|
+
|
|
314
|
+
loader(PricingLoader, () => [
|
|
315
|
+
cache({ store: hotStore }),
|
|
316
|
+
]),
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Without an explicit store, the loader uses the app-level store from the
|
|
320
|
+
handler config (`cache.store`).
|
|
321
|
+
|
|
137
322
|
## Multiple Loaders
|
|
138
323
|
|
|
139
324
|
Routes can have multiple loaders that run in parallel:
|
|
@@ -232,6 +417,31 @@ export const SearchLoader = createLoader(async (ctx) => {
|
|
|
232
417
|
}, true); // true = fetchable
|
|
233
418
|
```
|
|
234
419
|
|
|
420
|
+
### Fetchable Loader with Middleware
|
|
421
|
+
|
|
422
|
+
Pass an options object instead of `true` to attach per-loader middleware.
|
|
423
|
+
This middleware runs only on `_rsc_loader` fetch requests (client-side
|
|
424
|
+
`load()` / `useFetchLoader()` calls), not during SSR `ctx.use()` execution:
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
import { createLoader } from "@rangojs/router";
|
|
428
|
+
import { authMiddleware } from "../middleware/auth";
|
|
429
|
+
import { rateLimitMiddleware } from "../middleware/rate-limit";
|
|
430
|
+
|
|
431
|
+
export const ProtectedLoader = createLoader(
|
|
432
|
+
async (ctx) => {
|
|
433
|
+
"use server";
|
|
434
|
+
|
|
435
|
+
const user = ctx.get("user");
|
|
436
|
+
return { orders: await db.orders.list(user.id) };
|
|
437
|
+
},
|
|
438
|
+
{ middleware: [authMiddleware, rateLimitMiddleware] },
|
|
439
|
+
);
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The middleware uses the same `MiddlewareFn` signature as route/app middleware,
|
|
443
|
+
so you can reuse existing middleware functions directly.
|
|
444
|
+
|
|
235
445
|
Fetchable loaders support both GET and POST (PUT, PATCH, DELETE) from the client.
|
|
236
446
|
The `load()` function auto-detects the body type:
|
|
237
447
|
|
|
@@ -288,7 +498,9 @@ Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
|
|
|
288
498
|
// loaders/shop.ts
|
|
289
499
|
import { createLoader } from "@rangojs/router";
|
|
290
500
|
|
|
291
|
-
export const ProductLoader = createLoader(
|
|
501
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
502
|
+
"use server";
|
|
503
|
+
|
|
292
504
|
const product = await ctx.env.DB
|
|
293
505
|
.prepare("SELECT * FROM products WHERE slug = ?")
|
|
294
506
|
.bind(ctx.params.slug)
|
|
@@ -301,7 +513,9 @@ export const ProductLoader = createLoader("product", async (ctx) => {
|
|
|
301
513
|
return { product };
|
|
302
514
|
});
|
|
303
515
|
|
|
304
|
-
export const CartLoader = createLoader(
|
|
516
|
+
export const CartLoader = createLoader(async (ctx) => {
|
|
517
|
+
"use server";
|
|
518
|
+
|
|
305
519
|
const user = ctx.get("user");
|
|
306
520
|
if (!user) return { cart: null };
|
|
307
521
|
|
|
@@ -325,7 +539,7 @@ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalid
|
|
|
325
539
|
]);
|
|
326
540
|
|
|
327
541
|
// pages/product.tsx
|
|
328
|
-
import { useLoader } from "@rangojs/router";
|
|
542
|
+
import { useLoader } from "@rangojs/router/client";
|
|
329
543
|
import { ProductLoader, CartLoader } from "./loaders/shop";
|
|
330
544
|
|
|
331
545
|
async function ProductPage() {
|
|
@@ -8,12 +8,93 @@ argument-hint: [middleware-name]
|
|
|
8
8
|
|
|
9
9
|
Middleware runs before/after route handlers using the onion model.
|
|
10
10
|
|
|
11
|
+
## Execution Model
|
|
12
|
+
|
|
13
|
+
Canonical semantics reference:
|
|
14
|
+
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
15
|
+
|
|
16
|
+
There are two levels of middleware with different execution scopes:
|
|
17
|
+
|
|
18
|
+
### Global middleware (`router.use()`)
|
|
19
|
+
|
|
20
|
+
Registered on the router instance. Wraps the **entire request**, including server actions, rendering, and progressive enhancement (PE) re-renders.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
const router = createRouter<AppEnv>({})
|
|
24
|
+
.use(loggerMiddleware) // all routes
|
|
25
|
+
.use("/admin/*", authMiddleware) // pattern-scoped
|
|
26
|
+
.routes(urlpatterns);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Route middleware (`middleware()` in `urls()`)
|
|
30
|
+
|
|
31
|
+
Registered inside `urls()` callback. Wraps **rendering only** -- it does NOT wrap server action execution. Actions run before route middleware, so when route middleware executes during post-action revalidation, it can observe state that the action set (cookies, context variables, headers).
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Request flow (with action):
|
|
35
|
+
global mw -> action executes -> route mw -> layout -> handler -> loaders
|
|
36
|
+
|
|
37
|
+
Request flow (no action):
|
|
38
|
+
global mw -> route mw -> layout -> handler -> loaders
|
|
39
|
+
|
|
40
|
+
Progressive enhancement (no-JS form POST):
|
|
41
|
+
global mw -> action executes -> route mw -> full page re-render
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The contract is: **route middleware wraps rendering regardless of transport** (JS-enabled RSC stream or no-JS HTML). During PE re-render, route middleware observes action-set state (cookies, context variables) the same way it does during JS-enabled post-action revalidation.
|
|
45
|
+
|
|
46
|
+
Revalidation is still partial. Route middleware wraps the render pass that
|
|
47
|
+
does happen, but it does not force unrelated outer segments to recompute.
|
|
48
|
+
If a child segment depends on data established by an outer handler/layout,
|
|
49
|
+
revalidate that outer segment too, or have the child guard/reload the
|
|
50
|
+
data itself.
|
|
51
|
+
|
|
52
|
+
### Revalidation Contracts with Middleware-Backed Trees
|
|
53
|
+
|
|
54
|
+
Middleware can establish request-level context (`ctx.set`) for segments that
|
|
55
|
+
execute in the current render pass. It does not change partial revalidation
|
|
56
|
+
boundaries between handler/layout/parallel segments.
|
|
57
|
+
|
|
58
|
+
For shared segment data, use named revalidation contracts on both the producer
|
|
59
|
+
and consumer segments, even when middleware is present in the chain.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
export const revalidateCartData = ({ actionId }) =>
|
|
63
|
+
actionId?.includes("src/actions/cart.ts#") ?? false;
|
|
64
|
+
|
|
65
|
+
layout(CartLayout, () => [
|
|
66
|
+
middleware(cartRenderMiddleware),
|
|
67
|
+
revalidate(revalidateCartData), // producer reruns
|
|
68
|
+
parallel(
|
|
69
|
+
{ "@cart": CartSummary },
|
|
70
|
+
() => [revalidate(revalidateCartData)], // consumer reruns
|
|
71
|
+
),
|
|
72
|
+
]);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
You can package those contracts as importable helpers to avoid repeating
|
|
76
|
+
`revalidate(...)` at each segment:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { revalidate } from "@rangojs/router";
|
|
80
|
+
|
|
81
|
+
export const revalidateCart = () => [revalidate(revalidateCartData)];
|
|
82
|
+
|
|
83
|
+
layout(CartLayout, () => [
|
|
84
|
+
middleware(cartRenderMiddleware),
|
|
85
|
+
revalidateCart(),
|
|
86
|
+
parallel({ "@cart": CartSummary }, () => [revalidateCart()]),
|
|
87
|
+
]);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Route middleware is the right place for per-route concerns that affect rendering (setting context variables for handlers, adding response headers, reading cookies set by actions). It is NOT the right place for action guards -- use global middleware for that.
|
|
91
|
+
|
|
11
92
|
## Basic Middleware
|
|
12
93
|
|
|
13
94
|
```typescript
|
|
14
|
-
import {
|
|
95
|
+
import type { Middleware } from "@rangojs/router";
|
|
15
96
|
|
|
16
|
-
export const authMiddleware =
|
|
97
|
+
export const authMiddleware: Middleware = async (ctx, next) => {
|
|
17
98
|
const token = ctx.request.headers.get("Authorization");
|
|
18
99
|
|
|
19
100
|
if (!token) {
|
|
@@ -24,7 +105,7 @@ export const authMiddleware = createMiddleware(async (ctx, next) => {
|
|
|
24
105
|
ctx.set("user", user);
|
|
25
106
|
|
|
26
107
|
await next();
|
|
27
|
-
}
|
|
108
|
+
};
|
|
28
109
|
```
|
|
29
110
|
|
|
30
111
|
## Using Middleware in Routes
|
|
@@ -68,7 +149,7 @@ layout(<ShopLayout />, () => [
|
|
|
68
149
|
## Middleware Context
|
|
69
150
|
|
|
70
151
|
```typescript
|
|
71
|
-
export const myMiddleware =
|
|
152
|
+
export const myMiddleware: Middleware = async (ctx, next) => {
|
|
72
153
|
// Access request
|
|
73
154
|
ctx.request; // Request object
|
|
74
155
|
ctx.url; // Parsed URL
|
|
@@ -86,7 +167,7 @@ export const myMiddleware = createMiddleware(async (ctx, next) => {
|
|
|
86
167
|
|
|
87
168
|
// After handler (response intercepting)
|
|
88
169
|
console.log("Handler completed");
|
|
89
|
-
}
|
|
170
|
+
};
|
|
90
171
|
```
|
|
91
172
|
|
|
92
173
|
### Typed context variables in middleware
|
|
@@ -94,19 +175,20 @@ export const myMiddleware = createMiddleware(async (ctx, next) => {
|
|
|
94
175
|
Use `createVar<T>()` for type-safe data sharing between middleware and handlers:
|
|
95
176
|
|
|
96
177
|
```typescript
|
|
97
|
-
import {
|
|
178
|
+
import { createVar } from "@rangojs/router";
|
|
179
|
+
import type { Middleware } from "@rangojs/router";
|
|
98
180
|
|
|
99
181
|
interface AuthUser { id: string; email: string; role: string }
|
|
100
182
|
export const CurrentUser = createVar<AuthUser>();
|
|
101
183
|
|
|
102
|
-
export const authMiddleware =
|
|
184
|
+
export const authMiddleware: Middleware = async (ctx, next) => {
|
|
103
185
|
const token = ctx.request.headers.get("Authorization");
|
|
104
186
|
if (!token) throw new Response("Unauthorized", { status: 401 });
|
|
105
187
|
|
|
106
188
|
const user = await verifyToken(token);
|
|
107
189
|
ctx.set(CurrentUser, user); // type-checked
|
|
108
190
|
await next();
|
|
109
|
-
}
|
|
191
|
+
};
|
|
110
192
|
|
|
111
193
|
// In a handler -- typed read
|
|
112
194
|
import { CurrentUser } from "./middleware";
|
|
@@ -124,17 +206,14 @@ data; use RSCRouter.Vars for app-wide middleware state.
|
|
|
124
206
|
## Redirect with State in Middleware
|
|
125
207
|
|
|
126
208
|
```typescript
|
|
127
|
-
import {
|
|
128
|
-
|
|
129
|
-
redirect,
|
|
130
|
-
createLocationState,
|
|
131
|
-
} from "@rangojs/router";
|
|
209
|
+
import { redirect, createLocationState } from "@rangojs/router";
|
|
210
|
+
import type { Middleware } from "@rangojs/router";
|
|
132
211
|
|
|
133
212
|
export const FlashMessage = createLocationState<{ text: string }>({
|
|
134
213
|
flash: true,
|
|
135
214
|
});
|
|
136
215
|
|
|
137
|
-
export const requireAuthMiddleware =
|
|
216
|
+
export const requireAuthMiddleware: Middleware = async (ctx, next) => {
|
|
138
217
|
const token = ctx.request.headers.get("Authorization");
|
|
139
218
|
if (!token) {
|
|
140
219
|
return redirect("/login", {
|
|
@@ -142,7 +221,7 @@ export const requireAuthMiddleware = createMiddleware(async (ctx, next) => {
|
|
|
142
221
|
});
|
|
143
222
|
}
|
|
144
223
|
await next();
|
|
145
|
-
}
|
|
224
|
+
};
|
|
146
225
|
```
|
|
147
226
|
|
|
148
227
|
Read the flash on the target page with `useLocationState(FlashMessage)`. The `{ flash: true }` option makes it auto-clear after first render. See `/hooks`.
|
|
@@ -150,7 +229,7 @@ Read the flash on the target page with `useLocationState(FlashMessage)`. The `{
|
|
|
150
229
|
## Authentication Middleware
|
|
151
230
|
|
|
152
231
|
```typescript
|
|
153
|
-
export const requireAuthMiddleware =
|
|
232
|
+
export const requireAuthMiddleware: Middleware = async (ctx, next) => {
|
|
154
233
|
const user = ctx.get("user");
|
|
155
234
|
|
|
156
235
|
if (!user) {
|
|
@@ -158,9 +237,9 @@ export const requireAuthMiddleware = createMiddleware(async (ctx, next) => {
|
|
|
158
237
|
}
|
|
159
238
|
|
|
160
239
|
await next();
|
|
161
|
-
}
|
|
240
|
+
};
|
|
162
241
|
|
|
163
|
-
export const permissionsMiddleware =
|
|
242
|
+
export const permissionsMiddleware: Middleware = async (ctx, next) => {
|
|
164
243
|
const user = ctx.get("user");
|
|
165
244
|
const requiredPermission = "admin";
|
|
166
245
|
|
|
@@ -169,13 +248,13 @@ export const permissionsMiddleware = createMiddleware(async (ctx, next) => {
|
|
|
169
248
|
}
|
|
170
249
|
|
|
171
250
|
await next();
|
|
172
|
-
}
|
|
251
|
+
};
|
|
173
252
|
```
|
|
174
253
|
|
|
175
254
|
## Logger Middleware
|
|
176
255
|
|
|
177
256
|
```typescript
|
|
178
|
-
export const loggerMiddleware =
|
|
257
|
+
export const loggerMiddleware: Middleware = async (ctx, next) => {
|
|
179
258
|
const start = Date.now();
|
|
180
259
|
|
|
181
260
|
console.log(`[${ctx.request.method}] ${ctx.url.pathname}`);
|
|
@@ -184,13 +263,13 @@ export const loggerMiddleware = createMiddleware(async (ctx, next) => {
|
|
|
184
263
|
|
|
185
264
|
const duration = Date.now() - start;
|
|
186
265
|
console.log(`[${ctx.request.method}] ${ctx.url.pathname} - ${duration}ms`);
|
|
187
|
-
}
|
|
266
|
+
};
|
|
188
267
|
```
|
|
189
268
|
|
|
190
269
|
## Rate Limiting Middleware
|
|
191
270
|
|
|
192
271
|
```typescript
|
|
193
|
-
export const rateLimitMiddleware =
|
|
272
|
+
export const rateLimitMiddleware: Middleware = async (ctx, next) => {
|
|
194
273
|
const ip = ctx.request.headers.get("CF-Connecting-IP") ?? "unknown";
|
|
195
274
|
const key = `rate-limit:${ip}`;
|
|
196
275
|
|
|
@@ -206,32 +285,32 @@ export const rateLimitMiddleware = createMiddleware(async (ctx, next) => {
|
|
|
206
285
|
});
|
|
207
286
|
|
|
208
287
|
await next();
|
|
209
|
-
}
|
|
288
|
+
};
|
|
210
289
|
```
|
|
211
290
|
|
|
212
291
|
## Complete Example
|
|
213
292
|
|
|
214
293
|
```typescript
|
|
215
294
|
// middleware/index.ts
|
|
216
|
-
import {
|
|
295
|
+
import type { Middleware } from "@rangojs/router";
|
|
217
296
|
|
|
218
|
-
export const loggerMiddleware =
|
|
297
|
+
export const loggerMiddleware: Middleware = async (ctx, next) => {
|
|
219
298
|
console.log(`[${ctx.request.method}] ${ctx.url.pathname}`);
|
|
220
299
|
await next();
|
|
221
|
-
}
|
|
300
|
+
};
|
|
222
301
|
|
|
223
|
-
export const mockAuthMiddleware =
|
|
302
|
+
export const mockAuthMiddleware: Middleware = async (ctx, next) => {
|
|
224
303
|
// Mock user for development
|
|
225
304
|
ctx.set("user", { id: "1", name: "Demo User" });
|
|
226
305
|
await next();
|
|
227
|
-
}
|
|
306
|
+
};
|
|
228
307
|
|
|
229
|
-
export const requireAuthMiddleware =
|
|
308
|
+
export const requireAuthMiddleware: Middleware = async (ctx, next) => {
|
|
230
309
|
if (!ctx.get("user")) {
|
|
231
310
|
throw new Response("Unauthorized", { status: 401 });
|
|
232
311
|
}
|
|
233
312
|
await next();
|
|
234
|
-
}
|
|
313
|
+
};
|
|
235
314
|
|
|
236
315
|
// urls.tsx
|
|
237
316
|
import { urls } from "@rangojs/router";
|
package/skills/parallel/SKILL.md
CHANGED
|
@@ -8,6 +8,9 @@ argument-hint: [@slot-name]
|
|
|
8
8
|
|
|
9
9
|
Parallel routes render multiple components simultaneously in named slots.
|
|
10
10
|
|
|
11
|
+
Canonical semantics reference:
|
|
12
|
+
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
13
|
+
|
|
11
14
|
## Basic Parallel Routes
|
|
12
15
|
|
|
13
16
|
```typescript
|
|
@@ -56,8 +59,21 @@ parallel({
|
|
|
56
59
|
|
|
57
60
|
## Reading Handler Data
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
Parallels can read `ctx.set()` values from their parent handler or layout
|
|
63
|
+
via `ctx.get()`. The handler always executes before its parallels
|
|
64
|
+
(handler-first).
|
|
65
|
+
|
|
66
|
+
Visibility follows tree structure:
|
|
67
|
+
|
|
68
|
+
- Layout-level parallels see layout data, but not path handler data
|
|
69
|
+
(the path is a separate entry).
|
|
70
|
+
- Parallels inside a path (or its orphan layouts) see both layout and
|
|
71
|
+
path handler data.
|
|
72
|
+
|
|
73
|
+
This applies to full render passes. During partial action revalidation,
|
|
74
|
+
only revalidated segments are recomputed. If a parallel depends on data
|
|
75
|
+
set by an outer handler or layout, revalidate that outer segment too, or
|
|
76
|
+
have the parallel reload/guard the data itself.
|
|
61
77
|
|
|
62
78
|
```typescript
|
|
63
79
|
path("/dashboard/:id", (ctx) => {
|
|
@@ -142,6 +158,45 @@ parallel(
|
|
|
142
158
|
)
|
|
143
159
|
```
|
|
144
160
|
|
|
161
|
+
Revalidating only the parallel does not re-run outer handlers/layouts.
|
|
162
|
+
If the slot reads `ctx.get()` data established above it, opt the outer
|
|
163
|
+
segment into revalidation as well.
|
|
164
|
+
|
|
165
|
+
### Revalidation Contracts for Parallel Dependencies
|
|
166
|
+
|
|
167
|
+
Prefer named revalidation contracts shared by both the upstream producer and
|
|
168
|
+
the parallel consumer:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// revalidation-contracts.ts
|
|
172
|
+
export const revalidateCartData = ({ actionId }) =>
|
|
173
|
+
actionId?.includes("src/actions/cart.ts#") ?? false;
|
|
174
|
+
|
|
175
|
+
layout(CartLayout, () => [
|
|
176
|
+
revalidate(revalidateCartData), // producer reruns
|
|
177
|
+
parallel(
|
|
178
|
+
{ "@cart": CartSummary },
|
|
179
|
+
() => [revalidate(revalidateCartData)], // consumer reruns
|
|
180
|
+
),
|
|
181
|
+
]);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
If the slot consumes multiple upstream domains, compose the contracts on both
|
|
185
|
+
segments.
|
|
186
|
+
|
|
187
|
+
Handoff helper style also works:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { revalidate } from "@rangojs/router";
|
|
191
|
+
|
|
192
|
+
export const revalidateCart = () => [revalidate(revalidateCartData)];
|
|
193
|
+
|
|
194
|
+
layout(CartLayout, () => [
|
|
195
|
+
revalidateCart(),
|
|
196
|
+
parallel({ "@cart": CartSummary }, () => [revalidateCart()]),
|
|
197
|
+
]);
|
|
198
|
+
```
|
|
199
|
+
|
|
145
200
|
## Named Outlets
|
|
146
201
|
|
|
147
202
|
Use `ParallelOutlet` to render slots in layouts:
|