@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19
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 +46 -8
- package/dist/bin/rango.js +105 -18
- package/dist/vite/index.js +227 -93
- package/package.json +15 -14
- package/skills/hooks/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +94 -1
- package/skills/middleware/SKILL.md +81 -0
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +187 -17
- package/skills/route/SKILL.md +42 -1
- package/skills/router-setup/SKILL.md +77 -0
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +38 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +25 -27
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +0 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +46 -13
- package/src/browser/navigation-client.ts +32 -61
- package/src/browser/navigation-store.ts +1 -31
- package/src/browser/navigation-transaction.ts +46 -207
- package/src/browser/partial-update.ts +102 -150
- package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
- package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
- package/src/browser/react/Link.tsx +28 -23
- package/src/browser/react/NavigationProvider.tsx +9 -1
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +1 -1
- package/src/browser/react/location-state.ts +2 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/use-action.ts +9 -1
- package/src/browser/react/use-handle.ts +3 -25
- package/src/browser/react/use-params.ts +2 -4
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +7 -60
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +29 -23
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +115 -96
- package/src/browser/types.ts +1 -31
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +5 -0
- package/src/build/generate-route-types.ts +2 -0
- 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 +45 -3
- 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 +132 -96
- package/src/cache/cache-scope.ts +71 -73
- package/src/cache/cf/cf-cache-store.ts +9 -4
- package/src/cache/document-cache.ts +72 -47
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/memory-segment-store.ts +18 -7
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +101 -112
- package/src/cache/taint.ts +26 -0
- package/src/client.tsx +53 -30
- 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/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +33 -1
- package/src/index.ts +27 -0
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +4 -3
- 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/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 +94 -15
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +1 -0
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +61 -7
- 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 +69 -4
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/middleware-types.ts +7 -0
- package/src/router/middleware.ts +93 -8
- package/src/router/pattern-matching.ts +41 -5
- package/src/router/prerender-match.ts +34 -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 +34 -0
- package/src/router/router-options.ts +200 -0
- package/src/router/segment-resolution/fresh.ts +123 -30
- package/src/router/segment-resolution/helpers.ts +19 -0
- package/src/router/segment-resolution/loader-cache.ts +37 -146
- package/src/router/segment-resolution/revalidation.ts +358 -94
- 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/types.ts +7 -1
- package/src/router.ts +155 -11
- package/src/rsc/handler-context.ts +11 -0
- package/src/rsc/handler.ts +380 -88
- package/src/rsc/helpers.ts +25 -16
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +232 -19
- package/src/rsc/response-route-handler.ts +37 -26
- package/src/rsc/rsc-rendering.ts +12 -5
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +134 -58
- package/src/rsc/types.ts +8 -0
- package/src/search-params.ts +22 -10
- package/src/server/context.ts +53 -5
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +66 -9
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +90 -9
- package/src/ssr/index.tsx +63 -27
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +1 -6
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +5 -0
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +9 -0
- package/src/types/handler-context.ts +35 -13
- package/src/types/loader-types.ts +7 -0
- package/src/types/route-entry.ts +28 -0
- 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 +27 -2
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +12 -4
- package/src/vite/discovery/bundle-postprocess.ts +12 -7
- package/src/vite/discovery/discover-routers.ts +30 -18
- package/src/vite/discovery/prerender-collection.ts +24 -27
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/rango.ts +3 -3
- package/src/vite/router-discovery.ts +99 -36
- package/src/vite/utils/prerender-utils.ts +21 -0
- package/src/vite/utils/shared-utils.ts +3 -1
- package/src/browser/request-controller.ts +0 -164
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.19",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -132,6 +132,15 @@
|
|
|
132
132
|
"access": "public",
|
|
133
133
|
"tag": "experimental"
|
|
134
134
|
},
|
|
135
|
+
"scripts": {
|
|
136
|
+
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
137
|
+
"prepublishOnly": "pnpm build",
|
|
138
|
+
"typecheck": "tsc --noEmit",
|
|
139
|
+
"test": "playwright test",
|
|
140
|
+
"test:ui": "playwright test --ui",
|
|
141
|
+
"test:unit": "vitest run",
|
|
142
|
+
"test:unit:watch": "vitest"
|
|
143
|
+
},
|
|
135
144
|
"dependencies": {
|
|
136
145
|
"@vitejs/plugin-rsc": "^0.5.14",
|
|
137
146
|
"magic-string": "^0.30.17",
|
|
@@ -141,12 +150,12 @@
|
|
|
141
150
|
"devDependencies": {
|
|
142
151
|
"@playwright/test": "^1.49.1",
|
|
143
152
|
"@types/node": "^24.10.1",
|
|
144
|
-
"@types/react": "
|
|
145
|
-
"@types/react-dom": "
|
|
153
|
+
"@types/react": "catalog:",
|
|
154
|
+
"@types/react-dom": "catalog:",
|
|
146
155
|
"esbuild": "^0.27.0",
|
|
147
156
|
"jiti": "^2.6.1",
|
|
148
|
-
"react": "
|
|
149
|
-
"react-dom": "
|
|
157
|
+
"react": "catalog:",
|
|
158
|
+
"react-dom": "catalog:",
|
|
150
159
|
"tinyexec": "^0.3.2",
|
|
151
160
|
"typescript": "^5.3.0",
|
|
152
161
|
"vitest": "^4.0.0"
|
|
@@ -164,13 +173,5 @@
|
|
|
164
173
|
"vite": {
|
|
165
174
|
"optional": true
|
|
166
175
|
}
|
|
167
|
-
},
|
|
168
|
-
"scripts": {
|
|
169
|
-
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
170
|
-
"typecheck": "tsc --noEmit",
|
|
171
|
-
"test": "playwright test",
|
|
172
|
-
"test:ui": "playwright test --ui",
|
|
173
|
-
"test:unit": "vitest run",
|
|
174
|
-
"test:unit:watch": "vitest"
|
|
175
176
|
}
|
|
176
|
-
}
|
|
177
|
+
}
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -484,7 +484,7 @@ Or via `ctx.setLocationState()` on any response:
|
|
|
484
484
|
|
|
485
485
|
```tsx
|
|
486
486
|
(ctx) => {
|
|
487
|
-
ctx.setLocationState(
|
|
487
|
+
ctx.setLocationState(FlashMessage({ text: "Welcome back!" }));
|
|
488
488
|
return <Dashboard />;
|
|
489
489
|
};
|
|
490
490
|
```
|
|
@@ -8,6 +8,9 @@ argument-hint: [@slot-name] [route-to-intercept]
|
|
|
8
8
|
|
|
9
9
|
Intercept routes render a different component during soft navigation (client-side) while preserving the background route. Hard navigation (direct URL) shows the full page.
|
|
10
10
|
|
|
11
|
+
Canonical semantics reference:
|
|
12
|
+
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
13
|
+
|
|
11
14
|
## Basic Intercept
|
|
12
15
|
|
|
13
16
|
```typescript
|
|
@@ -68,6 +71,78 @@ intercept(
|
|
|
68
71
|
)
|
|
69
72
|
```
|
|
70
73
|
|
|
74
|
+
## Intercept Middleware
|
|
75
|
+
|
|
76
|
+
Intercepts support their own middleware chain via the use callback. The full chain for an intercept request is:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
global mw (router.use) -> route mw (urls middleware()) -> intercept mw -> intercept handler -> intercept loaders
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
intercept(
|
|
84
|
+
"@modal",
|
|
85
|
+
"product",
|
|
86
|
+
<ProductModal />,
|
|
87
|
+
() => [
|
|
88
|
+
middleware(async (ctx, next) => {
|
|
89
|
+
// Runs only for this intercept, after global and route middleware
|
|
90
|
+
ctx.set("interceptSource", "modal");
|
|
91
|
+
await next();
|
|
92
|
+
}),
|
|
93
|
+
loader(ProductLoader),
|
|
94
|
+
]
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The intercept handler can read context variables set by all upstream middleware layers (global, route, and intercept-specific).
|
|
99
|
+
|
|
100
|
+
Handler/layout `ctx.set()` data follows the same rule as elsewhere:
|
|
101
|
+
intercepts see data produced in the current render pass, but partial
|
|
102
|
+
action revalidation only recomputes segments that actually revalidate.
|
|
103
|
+
If an intercept depends on data established by an outer layout/handler,
|
|
104
|
+
revalidate that outer segment too or reload/guard the data inside the
|
|
105
|
+
intercept.
|
|
106
|
+
|
|
107
|
+
### Revalidation Contracts for Intercept Dependencies
|
|
108
|
+
|
|
109
|
+
Use named revalidation contracts on both the outer producer and the intercept
|
|
110
|
+
consumer when they share `ctx.set()` data:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
export const revalidateProductShell = ({ actionId }) =>
|
|
114
|
+
actionId?.includes("src/actions/product.ts#") ?? false;
|
|
115
|
+
|
|
116
|
+
layout(ProductLayout, () => [
|
|
117
|
+
revalidate(revalidateProductShell), // producer reruns
|
|
118
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
119
|
+
revalidate(revalidateProductShell), // consumer reruns
|
|
120
|
+
loader(ProductLoader),
|
|
121
|
+
]),
|
|
122
|
+
]);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Compose multiple contracts if the intercept depends on multiple upstream
|
|
126
|
+
domains.
|
|
127
|
+
|
|
128
|
+
Helper handoff style keeps intercept trees terse:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { revalidate } from "@rangojs/router";
|
|
132
|
+
|
|
133
|
+
export const revalidateProduct = () => [
|
|
134
|
+
revalidate(revalidateProductShell),
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
layout(ProductLayout, () => [
|
|
138
|
+
revalidateProduct(),
|
|
139
|
+
intercept("@modal", "product", <ProductModal />, () => [
|
|
140
|
+
revalidateProduct(),
|
|
141
|
+
loader(ProductLoader),
|
|
142
|
+
]),
|
|
143
|
+
]);
|
|
144
|
+
```
|
|
145
|
+
|
|
71
146
|
## Conditional Intercept with when()
|
|
72
147
|
|
|
73
148
|
Only intercept based on navigation context:
|
|
@@ -166,6 +241,10 @@ Runtime behavior:
|
|
|
166
241
|
Loaders inside the intercept always run fresh at request time, same as regular
|
|
167
242
|
pre-rendered routes.
|
|
168
243
|
|
|
244
|
+
During action-driven partial revalidation, this same partial rule applies:
|
|
245
|
+
refreshing the intercept does not implicitly rebuild non-revalidated outer
|
|
246
|
+
segments.
|
|
247
|
+
|
|
169
248
|
## Complete Example
|
|
170
249
|
|
|
171
250
|
```typescript
|
package/skills/layout/SKILL.md
CHANGED
|
@@ -8,6 +8,9 @@ argument-hint: [component]
|
|
|
8
8
|
|
|
9
9
|
Layouts wrap child routes and persist during navigation within their scope.
|
|
10
10
|
|
|
11
|
+
Canonical semantics reference:
|
|
12
|
+
[docs/execution-model.md](../../docs/internal/execution-model.md)
|
|
13
|
+
|
|
11
14
|
## Basic Layout
|
|
12
15
|
|
|
13
16
|
```typescript
|
|
@@ -145,6 +148,13 @@ A layout as a child of `path()` wraps the route content and can read
|
|
|
145
148
|
data set by the route handler via `ctx.get()`. The handler always
|
|
146
149
|
executes before its children.
|
|
147
150
|
|
|
151
|
+
This handler-first guarantee applies to a single full render pass
|
|
152
|
+
(initial render, prerender, or full HTML re-render). During partial
|
|
153
|
+
action revalidation, only the segments that revalidate are recomputed.
|
|
154
|
+
If an orphan layout depends on data established by an outer handler or
|
|
155
|
+
layout, that outer segment must also revalidate, or the orphan must
|
|
156
|
+
guard/reload the data independently.
|
|
157
|
+
|
|
148
158
|
```typescript
|
|
149
159
|
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
150
160
|
|
|
@@ -175,8 +185,10 @@ urls(({ path, layout, parallel }) => [
|
|
|
175
185
|
])
|
|
176
186
|
```
|
|
177
187
|
|
|
178
|
-
Orphan layouts
|
|
179
|
-
|
|
188
|
+
Orphan layouts can call `ctx.get()` to read data set by their parent
|
|
189
|
+
handler. They can also call `ctx.set()`, though the primary pattern is
|
|
190
|
+
for route handlers and middleware to write context variables and for
|
|
191
|
+
orphan layouts to read them.
|
|
180
192
|
|
|
181
193
|
## Layout Revalidation
|
|
182
194
|
|
|
@@ -198,6 +210,54 @@ layout(<CartLayout />, () => [
|
|
|
198
210
|
])
|
|
199
211
|
```
|
|
200
212
|
|
|
213
|
+
If child segments read data that was established by this layout or by a
|
|
214
|
+
route handler above them, revalidate the outer segment too. Partial
|
|
215
|
+
revalidation does not re-run non-revalidated ancestors just to rebuild
|
|
216
|
+
their `ctx.set()` state.
|
|
217
|
+
|
|
218
|
+
### Revalidation Contracts
|
|
219
|
+
|
|
220
|
+
For shared upstream data, define named revalidation functions and reuse
|
|
221
|
+
them on both producer and consumer segments:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// revalidation-contracts.ts
|
|
225
|
+
export const revalidateCartData = ({ actionId }) =>
|
|
226
|
+
actionId?.includes("src/actions/cart.ts#addToCart") ?? false;
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
layout(<CartLayout />, () => [
|
|
231
|
+
revalidate(revalidateCartData), // producer
|
|
232
|
+
path("/cart", CartPage, { name: "cart" }, () => [
|
|
233
|
+
revalidate(revalidateCartData), // consumer
|
|
234
|
+
]),
|
|
235
|
+
]);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
If a segment depends on multiple upstream domains, compose multiple
|
|
239
|
+
contracts (`revalidateAuthData`, `revalidateCartData`, and so on).
|
|
240
|
+
|
|
241
|
+
You can also package them as importable handoff helpers:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// revalidation-contracts.ts
|
|
245
|
+
import { revalidate } from "@rangojs/router";
|
|
246
|
+
|
|
247
|
+
export const revalidateAuthData = ({ actionId }) =>
|
|
248
|
+
actionId?.includes("src/actions/auth.ts#") ?? false;
|
|
249
|
+
export const revalidateAuth = () => [revalidate(revalidateAuthData)];
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
layout(<ShellLayout />, () => [
|
|
254
|
+
revalidateAuth(),
|
|
255
|
+
path("/account", AccountPage, { name: "account" }, () => [
|
|
256
|
+
revalidateAuth(),
|
|
257
|
+
]),
|
|
258
|
+
]);
|
|
259
|
+
```
|
|
260
|
+
|
|
201
261
|
## Complete Example
|
|
202
262
|
|
|
203
263
|
```typescript
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -98,9 +98,12 @@ Loaders receive the same context as route handlers:
|
|
|
98
98
|
export const ProductLoader = createLoader(async (ctx) => {
|
|
99
99
|
"use server";
|
|
100
100
|
|
|
101
|
-
// URL params
|
|
101
|
+
// URL params (may include client-provided overrides for fetchable loaders)
|
|
102
102
|
const { slug } = ctx.params;
|
|
103
103
|
|
|
104
|
+
// Server-trusted route params (from URL pattern matching, cannot be overridden)
|
|
105
|
+
const { slug: trustedSlug } = ctx.routeParams;
|
|
106
|
+
|
|
104
107
|
// Query params
|
|
105
108
|
const variant = ctx.url.searchParams.get("variant");
|
|
106
109
|
|
|
@@ -117,6 +120,33 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
117
120
|
});
|
|
118
121
|
```
|
|
119
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
|
+
|
|
120
150
|
## Loader with Children
|
|
121
151
|
|
|
122
152
|
Add caching or revalidation to specific loaders:
|
|
@@ -138,6 +168,44 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
138
168
|
]);
|
|
139
169
|
```
|
|
140
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
|
+
|
|
141
209
|
## Loaders: The Live Data Layer
|
|
142
210
|
|
|
143
211
|
Loaders are the live data layer of the router. They resolve fresh on every
|
|
@@ -349,6 +417,31 @@ export const SearchLoader = createLoader(async (ctx) => {
|
|
|
349
417
|
}, true); // true = fetchable
|
|
350
418
|
```
|
|
351
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
|
+
|
|
352
445
|
Fetchable loaders support both GET and POST (PUT, PATCH, DELETE) from the client.
|
|
353
446
|
The `load()` function auto-detects the body type:
|
|
354
447
|
|
|
@@ -8,6 +8,87 @@ 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
|
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:
|