@rangojs/router 0.0.0-experimental.2
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/CLAUDE.md +7 -0
- package/README.md +19 -0
- package/dist/vite/index.js +1298 -0
- package/package.json +140 -0
- package/skills/caching/SKILL.md +319 -0
- package/skills/document-cache/SKILL.md +152 -0
- package/skills/hooks/SKILL.md +359 -0
- package/skills/intercept/SKILL.md +292 -0
- package/skills/layout/SKILL.md +216 -0
- package/skills/loader/SKILL.md +365 -0
- package/skills/middleware/SKILL.md +442 -0
- package/skills/parallel/SKILL.md +255 -0
- package/skills/route/SKILL.md +141 -0
- package/skills/router-setup/SKILL.md +403 -0
- package/skills/theme/SKILL.md +54 -0
- package/skills/typesafety/SKILL.md +352 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/component-utils.test.ts +76 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/__tests__/urls.test.tsx +436 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +893 -0
- package/src/browser/navigation-client.ts +162 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +559 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +275 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-href.tsx +208 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +164 -0
- package/src/browser/rsc-router.tsx +353 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +464 -0
- package/src/cache/__tests__/document-cache.test.ts +522 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +387 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +621 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +193 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href-context.ts +33 -0
- package/src/href.ts +177 -0
- package/src/index.rsc.ts +79 -0
- package/src/index.ts +87 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1371 -0
- package/src/route-map-builder.ts +146 -0
- package/src/route-types.ts +198 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +158 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +138 -0
- package/src/router/match-context.ts +264 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +266 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +214 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +272 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3876 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +1060 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +237 -0
- package/src/segment-system.tsx +456 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +417 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +146 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +234 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/__tests__/theme.test.ts +120 -0
- package/src/theme/constants.ts +55 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1561 -0
- package/src/urls.ts +726 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +787 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: middleware
|
|
3
|
+
description: Define middleware for authentication, logging, and request processing in rsc-router
|
|
4
|
+
argument-hint: [middleware-name]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Middleware
|
|
8
|
+
|
|
9
|
+
Middleware runs before/after route handlers using the onion model.
|
|
10
|
+
|
|
11
|
+
## Router-Level Middleware
|
|
12
|
+
|
|
13
|
+
Register middleware on the router with `.use()`:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// router.tsx
|
|
17
|
+
import { createRSCRouter } from "rsc-router/server";
|
|
18
|
+
|
|
19
|
+
const router = createRSCRouter<AppEnv>({ document: Document })
|
|
20
|
+
// Global middleware (runs for ALL routes)
|
|
21
|
+
.use(loggerMiddleware)
|
|
22
|
+
.use(requestIdMiddleware)
|
|
23
|
+
|
|
24
|
+
// Pattern-based middleware (runs for matching paths)
|
|
25
|
+
.use("/admin/*", adminAuthMiddleware)
|
|
26
|
+
.use("/api/*", rateLimitMiddleware)
|
|
27
|
+
.use("/api/:version/*", apiVersionMiddleware)
|
|
28
|
+
|
|
29
|
+
// Routes with scoped middleware
|
|
30
|
+
.routes("/shop", shopRoutes)
|
|
31
|
+
.use(shopAuthMiddleware) // Only runs for /shop/* routes
|
|
32
|
+
.map(() => import("./handlers/shop"));
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Pattern matching:**
|
|
36
|
+
- `*` - Match all routes
|
|
37
|
+
- `/admin/*` - Match `/admin` and anything under it
|
|
38
|
+
- `/users/:id` - Match `/users/123`, extract `{ id: "123" }`
|
|
39
|
+
- `/api/:version/*` - Match `/api/v1/users`, extract `{ version: "v1" }`
|
|
40
|
+
|
|
41
|
+
## Middleware Scoping Rules
|
|
42
|
+
|
|
43
|
+
**Key insight:** `.map()` returns the **router** (not the builder), so after `.map()` you're back to global scope.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const router = createRSCRouter<AppEnv>({ document: Document })
|
|
47
|
+
// GLOBAL - no mount prefix
|
|
48
|
+
.use(loggerMiddleware) // Runs for ALL routes
|
|
49
|
+
|
|
50
|
+
// SCOPED to /shop/*
|
|
51
|
+
.routes("/shop", shopRoutes)
|
|
52
|
+
.use(shopAuthMiddleware) // Only /shop/*
|
|
53
|
+
.use("/cart/*", cartMiddleware) // Only /shop/cart/*
|
|
54
|
+
.map(() => import("./handlers/shop"))
|
|
55
|
+
|
|
56
|
+
// GLOBAL again - .map() returned router, not builder
|
|
57
|
+
.use(anotherMiddleware) // Runs for ALL routes (including /admin)
|
|
58
|
+
|
|
59
|
+
// SCOPED to /admin/*
|
|
60
|
+
.routes("/admin", adminRoutes)
|
|
61
|
+
.use(adminAuthMiddleware) // Only /admin/*
|
|
62
|
+
.map(() => import("./handlers/admin"))
|
|
63
|
+
|
|
64
|
+
// GLOBAL again
|
|
65
|
+
.use(finalMiddleware); // Runs for ALL routes
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**The chain structure:**
|
|
69
|
+
```
|
|
70
|
+
router.use() → returns router (global middleware)
|
|
71
|
+
router.routes() → returns builder (scoped to mount prefix)
|
|
72
|
+
builder.use() → returns builder (middleware scoped to mount prefix)
|
|
73
|
+
builder.map() → returns router (ends scope, back to global)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Example execution order:**
|
|
77
|
+
```typescript
|
|
78
|
+
.use(A) // Global: all routes
|
|
79
|
+
.routes("/shop", shop)
|
|
80
|
+
.use(B) // Scoped: /shop/*
|
|
81
|
+
.use("/vip/*", C) // Scoped: /shop/vip/*
|
|
82
|
+
.map(...)
|
|
83
|
+
.use(D) // Global: all routes
|
|
84
|
+
.routes("/admin", admin)
|
|
85
|
+
.use(E) // Scoped: /admin/*
|
|
86
|
+
.map(...)
|
|
87
|
+
.use(F) // Global: all routes
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Result:
|
|
91
|
+
- `/shop/products` → A, B, D, F
|
|
92
|
+
- `/shop/vip/lounge` → A, B, C, D, F
|
|
93
|
+
- `/admin/dashboard` → A, D, E, F
|
|
94
|
+
- `/` (home) → A, D, F
|
|
95
|
+
|
|
96
|
+
## Handler-Level Middleware
|
|
97
|
+
|
|
98
|
+
Register middleware within route handlers using the `middleware()` helper:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { map } from "rsc-router/server";
|
|
102
|
+
|
|
103
|
+
export default map<typeof routes>(({ route, middleware }) => [
|
|
104
|
+
// Handler-wide middleware
|
|
105
|
+
middleware(async (ctx, next) => {
|
|
106
|
+
console.log("Request started:", ctx.pathname);
|
|
107
|
+
const start = Date.now();
|
|
108
|
+
|
|
109
|
+
await next();
|
|
110
|
+
|
|
111
|
+
const duration = Date.now() - start;
|
|
112
|
+
ctx.header("X-Response-Time", `${duration}ms`);
|
|
113
|
+
}),
|
|
114
|
+
|
|
115
|
+
route("index", IndexHandler),
|
|
116
|
+
]);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Route-Specific Middleware
|
|
120
|
+
|
|
121
|
+
Apply middleware to individual routes:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
export default map<typeof routes>(({ route, middleware }) => [
|
|
125
|
+
// No middleware
|
|
126
|
+
route("public", PublicPage),
|
|
127
|
+
|
|
128
|
+
// With route-specific middleware
|
|
129
|
+
route(
|
|
130
|
+
"dashboard",
|
|
131
|
+
(ctx) => <DashboardPage user={ctx.get("user")} />,
|
|
132
|
+
() => [
|
|
133
|
+
middleware(async (ctx, next) => {
|
|
134
|
+
const user = await getUser(ctx.request);
|
|
135
|
+
if (!user) throw redirect("/login");
|
|
136
|
+
ctx.set("user", user);
|
|
137
|
+
await next();
|
|
138
|
+
}),
|
|
139
|
+
]
|
|
140
|
+
),
|
|
141
|
+
]);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Middleware Context API
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
middleware(async (ctx, next) => {
|
|
148
|
+
// Request info
|
|
149
|
+
ctx.request // Raw Request object
|
|
150
|
+
ctx.url // URL object
|
|
151
|
+
ctx.pathname // Current path
|
|
152
|
+
ctx.searchParams // URLSearchParams
|
|
153
|
+
ctx.params // Route parameters (from pattern)
|
|
154
|
+
ctx.env // Platform bindings (Cloudflare, etc.)
|
|
155
|
+
|
|
156
|
+
// Variable storage (share data with handlers)
|
|
157
|
+
ctx.set("user", { id: "123", name: "John" });
|
|
158
|
+
ctx.get("user"); // Retrieve stored value
|
|
159
|
+
|
|
160
|
+
// Response headers
|
|
161
|
+
ctx.header("X-Custom", "value");
|
|
162
|
+
|
|
163
|
+
// Cookies
|
|
164
|
+
ctx.cookie("session"); // Read request cookie
|
|
165
|
+
ctx.cookies(); // Read all cookies as object
|
|
166
|
+
ctx.setCookie("auth", "token", { httpOnly: true, secure: true });
|
|
167
|
+
ctx.deleteCookie("old-session");
|
|
168
|
+
|
|
169
|
+
await next();
|
|
170
|
+
|
|
171
|
+
// After handler - access response
|
|
172
|
+
ctx.res; // Response object (available after next())
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Authentication Middleware
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// middleware/auth.ts
|
|
180
|
+
import { redirect } from "rsc-router/server";
|
|
181
|
+
|
|
182
|
+
export const authMiddleware = async (ctx, next) => {
|
|
183
|
+
const session = ctx.request.headers.get("Authorization");
|
|
184
|
+
|
|
185
|
+
if (!session) {
|
|
186
|
+
throw redirect("/login");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const user = await verifySession(session);
|
|
190
|
+
if (!user) {
|
|
191
|
+
throw redirect("/login");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
ctx.set("user", user);
|
|
195
|
+
await next();
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Usage in handler
|
|
199
|
+
export default map<typeof routes>(({ route, layout, middleware }) => [
|
|
200
|
+
middleware(authMiddleware),
|
|
201
|
+
|
|
202
|
+
layout(<DashboardLayout />, () => [
|
|
203
|
+
route("dashboard.index", (ctx) => {
|
|
204
|
+
const user = ctx.get("user"); // From middleware
|
|
205
|
+
return <Dashboard user={user} />;
|
|
206
|
+
}),
|
|
207
|
+
]),
|
|
208
|
+
]);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Multiple Middleware (Onion Model)
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
export default map<typeof routes>(({ route, middleware }) => [
|
|
215
|
+
middleware(loggerMiddleware), // 1st: Enter first, exit last
|
|
216
|
+
middleware(authMiddleware), // 2nd: Enter second, exit second
|
|
217
|
+
middleware(rateLimitMiddleware), // 3rd: Enter third, exit first
|
|
218
|
+
|
|
219
|
+
route("api", ApiHandler), // Handler runs in the middle
|
|
220
|
+
]);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Execution order:
|
|
224
|
+
```
|
|
225
|
+
loggerMiddleware (enter)
|
|
226
|
+
authMiddleware (enter)
|
|
227
|
+
rateLimitMiddleware (enter)
|
|
228
|
+
ApiHandler executes
|
|
229
|
+
rateLimitMiddleware (exit)
|
|
230
|
+
authMiddleware (exit)
|
|
231
|
+
loggerMiddleware (exit)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Scoped Middleware in Layouts
|
|
235
|
+
|
|
236
|
+
Apply middleware to layout children:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
export default map<typeof routes>(({ route, layout, middleware }) => [
|
|
240
|
+
// Public routes (no auth)
|
|
241
|
+
route("index", HomePage),
|
|
242
|
+
route("about", AboutPage),
|
|
243
|
+
|
|
244
|
+
// Protected routes - middleware applies to layout and all children
|
|
245
|
+
layout(<DashboardLayout />, () => [
|
|
246
|
+
middleware(authMiddleware), // Runs for all dashboard routes
|
|
247
|
+
|
|
248
|
+
route("dashboard.index", DashboardPage),
|
|
249
|
+
route("dashboard.settings", SettingsPage),
|
|
250
|
+
]),
|
|
251
|
+
]);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Loader Middleware
|
|
255
|
+
|
|
256
|
+
Middleware specific to a loader:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { createLoader } from "rsc-router/server";
|
|
260
|
+
|
|
261
|
+
export const UserProfileLoader = createLoader(
|
|
262
|
+
async (ctx) => {
|
|
263
|
+
const userId = ctx.get("userId"); // From loader middleware
|
|
264
|
+
return db.users.findUnique({ where: { id: userId } });
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
middleware: [
|
|
268
|
+
async (ctx, next) => {
|
|
269
|
+
const userId = ctx.params.id;
|
|
270
|
+
ctx.set("userId", userId);
|
|
271
|
+
await next();
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Short-Circuit Middleware
|
|
279
|
+
|
|
280
|
+
Stop execution early by returning a Response or throwing:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// Return Response to short-circuit
|
|
284
|
+
export const maintenanceMiddleware = async (ctx, next) => {
|
|
285
|
+
if (isMaintenanceMode()) {
|
|
286
|
+
return new Response("Under Maintenance", { status: 503 });
|
|
287
|
+
}
|
|
288
|
+
await next();
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Throw redirect
|
|
292
|
+
export const redirectMiddleware = async (ctx, next) => {
|
|
293
|
+
if (ctx.pathname === "/old-path") {
|
|
294
|
+
throw redirect("/new-path");
|
|
295
|
+
}
|
|
296
|
+
await next();
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Return early for auth
|
|
300
|
+
export const authGuard = async (ctx, next) => {
|
|
301
|
+
if (!isAuthenticated(ctx)) {
|
|
302
|
+
return new Response(null, {
|
|
303
|
+
status: 302,
|
|
304
|
+
headers: { Location: "/login" },
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
await next();
|
|
308
|
+
};
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Error Handling Middleware
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
export const errorMiddleware = async (ctx, next) => {
|
|
315
|
+
try {
|
|
316
|
+
await next();
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (error instanceof AuthError) {
|
|
319
|
+
throw redirect("/login");
|
|
320
|
+
}
|
|
321
|
+
if (error instanceof NotFoundError) {
|
|
322
|
+
throw notFound();
|
|
323
|
+
}
|
|
324
|
+
// Re-throw other errors
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## CORS Middleware Example
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
export const corsMiddleware = async (ctx, next) => {
|
|
334
|
+
ctx.header("Access-Control-Allow-Origin", "*");
|
|
335
|
+
ctx.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
|
|
336
|
+
ctx.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
337
|
+
|
|
338
|
+
if (ctx.method === "OPTIONS") {
|
|
339
|
+
return new Response(null, { status: 204 });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
await next();
|
|
343
|
+
};
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Rate Limiting Middleware Example
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
const rateLimiter = new Map<string, number[]>();
|
|
350
|
+
|
|
351
|
+
export const rateLimitMiddleware = async (ctx, next) => {
|
|
352
|
+
const ip = ctx.request.headers.get("CF-Connecting-IP") ?? "unknown";
|
|
353
|
+
const now = Date.now();
|
|
354
|
+
const windowMs = 60000; // 1 minute
|
|
355
|
+
const maxRequests = 100;
|
|
356
|
+
|
|
357
|
+
const requests = rateLimiter.get(ip) ?? [];
|
|
358
|
+
const recentRequests = requests.filter((t) => now - t < windowMs);
|
|
359
|
+
|
|
360
|
+
if (recentRequests.length >= maxRequests) {
|
|
361
|
+
ctx.header("Retry-After", "60");
|
|
362
|
+
return new Response("Too Many Requests", { status: 429 });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
recentRequests.push(now);
|
|
366
|
+
rateLimiter.set(ip, recentRequests);
|
|
367
|
+
|
|
368
|
+
await next();
|
|
369
|
+
};
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Type-Safe Route Middleware
|
|
373
|
+
|
|
374
|
+
Use `RouteMiddlewareFn` for type-safe params:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
import type { RouteMiddlewareFn } from "rsc-router";
|
|
378
|
+
import type { shopRoutes } from "./routes/shop";
|
|
379
|
+
|
|
380
|
+
// Middleware with typed params from route definition
|
|
381
|
+
export const productMiddleware: RouteMiddlewareFn<
|
|
382
|
+
typeof shopRoutes,
|
|
383
|
+
"shop.product" // Route has :slug param
|
|
384
|
+
> = async (ctx, next) => {
|
|
385
|
+
// ctx.params.slug is typed as string
|
|
386
|
+
console.log("Viewing product:", ctx.params.slug);
|
|
387
|
+
await next();
|
|
388
|
+
};
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## Middleware Type Signatures
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
// Basic middleware function
|
|
395
|
+
type MiddlewareFn<TEnv = any, TParams = Record<string, string>> = (
|
|
396
|
+
ctx: MiddlewareContext<TEnv, TParams>,
|
|
397
|
+
next: () => Promise<Response>
|
|
398
|
+
) => Response | Promise<Response> | void | Promise<void>;
|
|
399
|
+
|
|
400
|
+
// Middleware context
|
|
401
|
+
interface MiddlewareContext<TEnv, TParams> {
|
|
402
|
+
request: Request;
|
|
403
|
+
url: URL;
|
|
404
|
+
pathname: string;
|
|
405
|
+
searchParams: URLSearchParams;
|
|
406
|
+
env: TEnv;
|
|
407
|
+
params: TParams;
|
|
408
|
+
res: Response;
|
|
409
|
+
|
|
410
|
+
cookie(name: string): string | undefined;
|
|
411
|
+
cookies(): Record<string, string>;
|
|
412
|
+
setCookie(name: string, value: string, options?: CookieOptions): void;
|
|
413
|
+
deleteCookie(name: string, options?: CookieOptions): void;
|
|
414
|
+
|
|
415
|
+
get<K extends string>(key: K): any;
|
|
416
|
+
set<K extends string>(key: K, value: any): void;
|
|
417
|
+
header(name: string, value: string): void;
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Cookie Options
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
interface CookieOptions {
|
|
425
|
+
domain?: string;
|
|
426
|
+
path?: string;
|
|
427
|
+
expires?: Date;
|
|
428
|
+
maxAge?: number;
|
|
429
|
+
httpOnly?: boolean;
|
|
430
|
+
secure?: boolean;
|
|
431
|
+
sameSite?: "strict" | "lax" | "none";
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Example
|
|
435
|
+
ctx.setCookie("session", token, {
|
|
436
|
+
httpOnly: true,
|
|
437
|
+
secure: true,
|
|
438
|
+
sameSite: "lax",
|
|
439
|
+
maxAge: 60 * 60 * 24 * 7, // 1 week
|
|
440
|
+
path: "/",
|
|
441
|
+
});
|
|
442
|
+
```
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: parallel
|
|
3
|
+
description: Define parallel routes for multi-column layouts, sidebars, and modal slots in rsc-router
|
|
4
|
+
argument-hint: [@slot-name]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Parallel Routes
|
|
8
|
+
|
|
9
|
+
Parallel routes render multiple components simultaneously in named slots.
|
|
10
|
+
|
|
11
|
+
## Basic Parallel Routes
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { map } from "rsc-router/server";
|
|
15
|
+
import { ParallelOutlet } from "rsc-router";
|
|
16
|
+
|
|
17
|
+
// Handler definition
|
|
18
|
+
export default map<typeof routes>(({ route, layout, parallel }) => [
|
|
19
|
+
layout(
|
|
20
|
+
() => (
|
|
21
|
+
<DashboardLayout>
|
|
22
|
+
<aside>
|
|
23
|
+
<ParallelOutlet name="@sidebar" />
|
|
24
|
+
</aside>
|
|
25
|
+
<main>
|
|
26
|
+
<Outlet />
|
|
27
|
+
</main>
|
|
28
|
+
<div className="notifications">
|
|
29
|
+
<ParallelOutlet name="@notifications" />
|
|
30
|
+
</div>
|
|
31
|
+
</DashboardLayout>
|
|
32
|
+
),
|
|
33
|
+
() => [
|
|
34
|
+
parallel({
|
|
35
|
+
"@sidebar": () => <Sidebar />,
|
|
36
|
+
"@notifications": () => <NotificationPanel />,
|
|
37
|
+
}),
|
|
38
|
+
|
|
39
|
+
route("dashboard.index", DashboardIndex),
|
|
40
|
+
route("dashboard.analytics", Analytics),
|
|
41
|
+
]
|
|
42
|
+
),
|
|
43
|
+
]);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Parallel Routes with Context
|
|
47
|
+
|
|
48
|
+
Access route params and loaders in parallel slots:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
parallel({
|
|
52
|
+
"@sidebar": (ctx) => {
|
|
53
|
+
const user = ctx.get("user");
|
|
54
|
+
return <UserSidebar user={user} />;
|
|
55
|
+
},
|
|
56
|
+
"@details": async (ctx) => {
|
|
57
|
+
const data = await ctx.use(DetailsLoader);
|
|
58
|
+
return <DetailsPanel data={data} productId={ctx.params.id} />;
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Parallel Routes with Configuration
|
|
64
|
+
|
|
65
|
+
Add loaders, loading states, and revalidation to slots:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
parallel(
|
|
69
|
+
{
|
|
70
|
+
"@sidebar": async (ctx) => {
|
|
71
|
+
const categories = await ctx.use(CategoriesLoader);
|
|
72
|
+
return <CategorySidebar categories={categories} />;
|
|
73
|
+
},
|
|
74
|
+
"@cart": async (ctx) => {
|
|
75
|
+
const cart = await ctx.use(CartLoader);
|
|
76
|
+
return <CartPreview cart={cart} />;
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
() => [
|
|
80
|
+
loader(CategoriesLoader),
|
|
81
|
+
loader(CartLoader),
|
|
82
|
+
loading(<SidebarSkeleton />),
|
|
83
|
+
revalidate(({ actionId }) => actionId?.includes("cart") ?? false),
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Independent Slot Revalidation
|
|
89
|
+
|
|
90
|
+
Each parallel slot can control its own revalidation:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
layout(<DashboardLayout />, () => [
|
|
94
|
+
// Sidebar only revalidates on sidebar actions
|
|
95
|
+
parallel(
|
|
96
|
+
{ "@sidebar": SidebarComponent },
|
|
97
|
+
() => [
|
|
98
|
+
revalidate(({ actionId }) => actionId?.includes("sidebar") ?? false),
|
|
99
|
+
]
|
|
100
|
+
),
|
|
101
|
+
|
|
102
|
+
// Main content revalidates on route changes
|
|
103
|
+
parallel(
|
|
104
|
+
{ "@main": MainComponent },
|
|
105
|
+
() => [
|
|
106
|
+
revalidate(({ currentParams, nextParams }) =>
|
|
107
|
+
currentParams.id !== nextParams.id
|
|
108
|
+
),
|
|
109
|
+
]
|
|
110
|
+
),
|
|
111
|
+
|
|
112
|
+
route("dashboard.index", DashboardIndex),
|
|
113
|
+
])
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Parallel Routes for Modals
|
|
117
|
+
|
|
118
|
+
Use parallel routes with intercept for modal patterns:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
layout(
|
|
122
|
+
() => (
|
|
123
|
+
<ShopLayout>
|
|
124
|
+
<Outlet />
|
|
125
|
+
<ParallelOutlet name="@modal" />
|
|
126
|
+
</ShopLayout>
|
|
127
|
+
),
|
|
128
|
+
() => [
|
|
129
|
+
parallel({
|
|
130
|
+
"@modal": () => null, // Empty by default
|
|
131
|
+
}),
|
|
132
|
+
|
|
133
|
+
// Intercept product detail into modal
|
|
134
|
+
intercept(
|
|
135
|
+
"@modal",
|
|
136
|
+
"products.detail",
|
|
137
|
+
(ctx) => <ProductModal id={ctx.params.id} />,
|
|
138
|
+
() => [
|
|
139
|
+
loader(ProductLoader),
|
|
140
|
+
loading(<ProductModalSkeleton />),
|
|
141
|
+
]
|
|
142
|
+
),
|
|
143
|
+
|
|
144
|
+
route("products.index", ProductList),
|
|
145
|
+
route("products.detail", ProductDetail),
|
|
146
|
+
]
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Conditional Parallel Content
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
parallel({
|
|
154
|
+
"@sidebar": (ctx) => {
|
|
155
|
+
const user = ctx.get("user");
|
|
156
|
+
if (!user) return null; // Hide for guests
|
|
157
|
+
if (user.role === "admin") {
|
|
158
|
+
return <AdminSidebar />;
|
|
159
|
+
}
|
|
160
|
+
return <UserSidebar />;
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Parallel Routes with Error Boundaries
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
parallel(
|
|
169
|
+
{
|
|
170
|
+
"@sidebar": async (ctx) => {
|
|
171
|
+
const data = await ctx.use(SidebarLoader);
|
|
172
|
+
return <Sidebar data={data} />;
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
() => [
|
|
176
|
+
loader(SidebarLoader),
|
|
177
|
+
errorBoundary(({ error, reset }) => (
|
|
178
|
+
<div className="sidebar-error">
|
|
179
|
+
<p>Sidebar failed to load</p>
|
|
180
|
+
<button onClick={reset}>Retry</button>
|
|
181
|
+
</div>
|
|
182
|
+
)),
|
|
183
|
+
]
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## ParallelOutlet Component
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { ParallelOutlet } from "rsc-router";
|
|
191
|
+
|
|
192
|
+
function Layout() {
|
|
193
|
+
return (
|
|
194
|
+
<div className="layout">
|
|
195
|
+
{/* Named slot - renders content from parallel() */}
|
|
196
|
+
<ParallelOutlet name="@sidebar" />
|
|
197
|
+
|
|
198
|
+
{/* Main route content */}
|
|
199
|
+
<Outlet />
|
|
200
|
+
|
|
201
|
+
{/* Another slot */}
|
|
202
|
+
<ParallelOutlet name="@footer" />
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Dashboard Example
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// Complete dashboard with multiple panels
|
|
212
|
+
export default map<typeof routes>(({ route, layout, parallel, loader }) => [
|
|
213
|
+
layout(
|
|
214
|
+
() => (
|
|
215
|
+
<div className="dashboard-grid">
|
|
216
|
+
<header>
|
|
217
|
+
<ParallelOutlet name="@header" />
|
|
218
|
+
</header>
|
|
219
|
+
<nav>
|
|
220
|
+
<ParallelOutlet name="@nav" />
|
|
221
|
+
</nav>
|
|
222
|
+
<main>
|
|
223
|
+
<Outlet />
|
|
224
|
+
</main>
|
|
225
|
+
<aside>
|
|
226
|
+
<ParallelOutlet name="@activity" />
|
|
227
|
+
</aside>
|
|
228
|
+
</div>
|
|
229
|
+
),
|
|
230
|
+
() => [
|
|
231
|
+
parallel({
|
|
232
|
+
"@header": () => <DashboardHeader />,
|
|
233
|
+
"@nav": (ctx) => <Navigation user={ctx.get("user")} />,
|
|
234
|
+
"@activity": async (ctx) => {
|
|
235
|
+
const activity = await ctx.use(ActivityLoader);
|
|
236
|
+
return <ActivityFeed items={activity} />;
|
|
237
|
+
},
|
|
238
|
+
}, () => [
|
|
239
|
+
loader(ActivityLoader),
|
|
240
|
+
revalidate(({ actionId }) => actionId?.includes("activity") ?? false),
|
|
241
|
+
]),
|
|
242
|
+
|
|
243
|
+
route("dashboard.index", DashboardHome),
|
|
244
|
+
route("dashboard.projects", Projects),
|
|
245
|
+
route("dashboard.settings", Settings),
|
|
246
|
+
]
|
|
247
|
+
),
|
|
248
|
+
]);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Slot Naming Convention
|
|
252
|
+
|
|
253
|
+
- Prefix with `@` (e.g., `@sidebar`, `@modal`, `@header`)
|
|
254
|
+
- Use lowercase with hyphens for multi-word names (`@user-panel`)
|
|
255
|
+
- Common names: `@sidebar`, `@modal`, `@header`, `@footer`, `@notifications`, `@breadcrumbs`
|