@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.
Files changed (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. 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`