@ivogt/rsc-router 0.0.0-experimental.13 → 0.0.0-experimental.14
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/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/src/href.ts +13 -9
- package/src/route-definition.ts +24 -0
- package/src/route-map-builder.ts +16 -10
- package/src/router/manifest.ts +34 -12
- package/src/router.ts +208 -26
- package/src/rsc/handler.ts +4 -3
- package/src/rsc/types.ts +5 -2
- package/src/server.ts +1 -0
package/dist/vite/index.js
CHANGED
|
@@ -675,7 +675,7 @@ import { resolve } from "node:path";
|
|
|
675
675
|
// package.json
|
|
676
676
|
var package_default = {
|
|
677
677
|
name: "@ivogt/rsc-router",
|
|
678
|
-
version: "0.0.0-experimental.
|
|
678
|
+
version: "0.0.0-experimental.14",
|
|
679
679
|
type: "module",
|
|
680
680
|
description: "Type-safe RSC router with partial rendering support",
|
|
681
681
|
author: "Ivo Todorov",
|
package/package.json
CHANGED
package/src/href.ts
CHANGED
|
@@ -8,19 +8,22 @@ export type SanitizePrefix<T extends string> = T extends `/${infer P}` ? P : T;
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Helper type to merge multiple route definitions into a single accumulated type.
|
|
11
|
-
*
|
|
11
|
+
* Note: When using createRSCRouter, types accumulate automatically through the
|
|
12
|
+
* builder chain, so this type is typically not needed.
|
|
12
13
|
*
|
|
13
14
|
* @example
|
|
14
15
|
* ```typescript
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* // Manual type merging (rarely needed):
|
|
17
17
|
* type AppRoutes = MergeRoutes<[
|
|
18
18
|
* typeof homeRoutes,
|
|
19
|
-
*
|
|
20
|
-
* PrefixedRoutes<typeof shopRoutes, "shop">,
|
|
19
|
+
* PrefixRoutePatterns<typeof blogRoutes, "/blog">,
|
|
21
20
|
* ]>;
|
|
22
21
|
*
|
|
23
|
-
*
|
|
22
|
+
* // Preferred: Let router accumulate types automatically
|
|
23
|
+
* const router = createRSCRouter<AppEnv>()
|
|
24
|
+
* .routes(homeRoutes).map(...)
|
|
25
|
+
* .routes("/blog", blogRoutes).map(...);
|
|
26
|
+
* type AppRoutes = typeof router.routeMap;
|
|
24
27
|
* ```
|
|
25
28
|
*/
|
|
26
29
|
export type MergeRoutes<T extends Record<string, string>[]> = T extends [
|
|
@@ -111,9 +114,10 @@ export type HrefFunction<TRoutes extends Record<string, string>> = {
|
|
|
111
114
|
*
|
|
112
115
|
* @example
|
|
113
116
|
* ```typescript
|
|
114
|
-
*
|
|
115
|
-
* href(
|
|
116
|
-
* href("
|
|
117
|
+
* // Given routes: { cart: "/shop/cart", detail: "/shop/product/:slug" }
|
|
118
|
+
* const href = createHref(routeMap);
|
|
119
|
+
* href("cart"); // "/shop/cart"
|
|
120
|
+
* href("detail", { slug: "my-product" }); // "/shop/product/my-product"
|
|
117
121
|
* ```
|
|
118
122
|
*/
|
|
119
123
|
export function createHref<TRoutes extends Record<string, string>>(
|
package/src/route-definition.ts
CHANGED
|
@@ -1248,6 +1248,30 @@ export function map<const T extends RouteDefinition, TEnv = DefaultEnv>(
|
|
|
1248
1248
|
};
|
|
1249
1249
|
}
|
|
1250
1250
|
|
|
1251
|
+
/**
|
|
1252
|
+
* Create RouteHelpers for inline route definitions
|
|
1253
|
+
* Used internally by router.map() for inline handler syntax
|
|
1254
|
+
*/
|
|
1255
|
+
export function createRouteHelpers<
|
|
1256
|
+
T extends RouteDefinition,
|
|
1257
|
+
TEnv,
|
|
1258
|
+
>(): RouteHelpers<T, TEnv> {
|
|
1259
|
+
return {
|
|
1260
|
+
route: createRouteHelper<T, TEnv>(),
|
|
1261
|
+
layout: createLayoutHelper<TEnv>(),
|
|
1262
|
+
parallel: createParallelHelper<TEnv>(),
|
|
1263
|
+
intercept: createInterceptHelper<T, TEnv>(),
|
|
1264
|
+
middleware: createMiddlewareHelper<TEnv>(),
|
|
1265
|
+
revalidate: createRevalidateHelper<TEnv>(),
|
|
1266
|
+
loader: createLoaderHelper<TEnv>(),
|
|
1267
|
+
loading: createLoadingHelper(),
|
|
1268
|
+
errorBoundary: createErrorBoundaryHelper<TEnv>(),
|
|
1269
|
+
notFoundBoundary: createNotFoundBoundaryHelper<TEnv>(),
|
|
1270
|
+
when: createWhenHelper(),
|
|
1271
|
+
cache: createCacheHelper(),
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1251
1275
|
/**
|
|
1252
1276
|
* Create a loader definition
|
|
1253
1277
|
*
|
package/src/route-map-builder.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import type {
|
|
26
|
+
import type { PrefixRoutePatterns } from "./href.js";
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Route map builder interface
|
|
@@ -37,12 +37,14 @@ export interface RouteMapBuilder<TRoutes extends Record<string, string> = {}> {
|
|
|
37
37
|
add<T extends Record<string, string>>(routes: T): RouteMapBuilder<TRoutes & T>;
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Add routes with prefix
|
|
40
|
+
* Add routes with prefix (only URL patterns are prefixed, keys stay unchanged)
|
|
41
|
+
* @param routes - Route definitions to add
|
|
42
|
+
* @param prefix - URL prefix WITHOUT leading slash (e.g., "blog" not "/blog")
|
|
41
43
|
*/
|
|
42
44
|
add<T extends Record<string, string>, P extends string>(
|
|
43
45
|
routes: T,
|
|
44
46
|
prefix: P
|
|
45
|
-
): RouteMapBuilder<TRoutes &
|
|
47
|
+
): RouteMapBuilder<TRoutes & PrefixRoutePatterns<T, `/${P}`>>;
|
|
46
48
|
|
|
47
49
|
/**
|
|
48
50
|
* The accumulated route map (for typeof extraction in module augmentation)
|
|
@@ -52,25 +54,29 @@ export interface RouteMapBuilder<TRoutes extends Record<string, string> = {}> {
|
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* Add routes to a map with optional prefix
|
|
57
|
+
* Keys stay unchanged for composability - only URL patterns get prefixed.
|
|
55
58
|
*
|
|
56
59
|
* @param routeMap - The map to add routes to
|
|
57
60
|
* @param routes - Routes to add
|
|
58
|
-
* @param prefix - Optional prefix for keys
|
|
61
|
+
* @param prefix - Optional prefix for URL paths WITHOUT leading slash (keys stay unchanged)
|
|
59
62
|
*/
|
|
60
63
|
function addRoutes(
|
|
61
64
|
routeMap: Record<string, string>,
|
|
62
65
|
routes: Record<string, string>,
|
|
63
66
|
prefix: string = ""
|
|
64
67
|
): void {
|
|
68
|
+
// Normalize prefix: remove leading slash if accidentally provided
|
|
69
|
+
const normalizedPrefix = prefix.startsWith("/") ? prefix.slice(1) : prefix;
|
|
70
|
+
|
|
65
71
|
for (const [key, pattern] of Object.entries(routes)) {
|
|
66
|
-
const prefixedKey = prefix ? `${prefix}.${key}` : key;
|
|
67
72
|
const prefixedPattern =
|
|
68
|
-
|
|
69
|
-
? `/${
|
|
70
|
-
:
|
|
71
|
-
? `/${
|
|
73
|
+
normalizedPrefix && pattern !== "/"
|
|
74
|
+
? `/${normalizedPrefix}${pattern}`
|
|
75
|
+
: normalizedPrefix && pattern === "/"
|
|
76
|
+
? `/${normalizedPrefix}`
|
|
72
77
|
: pattern;
|
|
73
|
-
|
|
78
|
+
// Use original key - enables reusable route modules
|
|
79
|
+
routeMap[key] = prefixedPattern;
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
|
package/src/router/manifest.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { invariant, RouteNotFoundError } from "../errors";
|
|
8
|
+
import { createRouteHelpers } from "../route-definition";
|
|
8
9
|
import { getContext, type EntryData, type MetricsStore } from "../server/context";
|
|
10
|
+
import MapRootLayout from "../server/root-layout";
|
|
9
11
|
import type { RouteEntry } from "../types";
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -63,19 +65,39 @@ export async function loadManifest(
|
|
|
63
65
|
Store.namespace || namespaceWithMount,
|
|
64
66
|
Store.parent,
|
|
65
67
|
async () => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
// Create helpers - inline handlers use them, lazy handlers ignore them
|
|
69
|
+
const helpers = createRouteHelpers();
|
|
70
|
+
|
|
71
|
+
// Call handler with helpers - works for both inline and lazy
|
|
72
|
+
const result = entry.handler(helpers);
|
|
73
|
+
|
|
74
|
+
// Handle based on return type
|
|
75
|
+
if (result instanceof Promise) {
|
|
76
|
+
// Lazy: () => import(...) - returns Promise
|
|
77
|
+
const load = await result;
|
|
78
|
+
if (
|
|
79
|
+
load &&
|
|
80
|
+
load !== null &&
|
|
81
|
+
typeof load === "object" &&
|
|
82
|
+
"default" in load
|
|
83
|
+
) {
|
|
84
|
+
// Promise<{ default: () => Array }> - e.g., dynamic import
|
|
85
|
+
// Pass helpers - functions that need them will use them,
|
|
86
|
+
// functions from route-definition's map() will ignore them
|
|
87
|
+
return load.default(helpers);
|
|
88
|
+
}
|
|
89
|
+
if (typeof load === "function") {
|
|
90
|
+
// Promise<() => Array>
|
|
91
|
+
return load(helpers);
|
|
92
|
+
}
|
|
93
|
+
// Promise<Array> - direct array from async handler
|
|
94
|
+
return load;
|
|
77
95
|
}
|
|
78
|
-
|
|
96
|
+
|
|
97
|
+
// Inline: ({ route }) => [...] - returns Array directly
|
|
98
|
+
// Wrap with layout (like map() from route-definition does)
|
|
99
|
+
// Flatten nested arrays from layout/route definitions
|
|
100
|
+
return [helpers.layout(MapRootLayout, () => result)].flat(3);
|
|
79
101
|
}
|
|
80
102
|
);
|
|
81
103
|
|
package/src/router.ts
CHANGED
|
@@ -14,10 +14,14 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
createHref,
|
|
16
16
|
type HrefFunction,
|
|
17
|
-
type
|
|
18
|
-
type SanitizePrefix,
|
|
17
|
+
type PrefixRoutePatterns,
|
|
19
18
|
} from "./href.js";
|
|
20
19
|
import { registerRouteMap } from "./route-map-builder.js";
|
|
20
|
+
import {
|
|
21
|
+
createRouteHelpers,
|
|
22
|
+
type RouteHelpers,
|
|
23
|
+
} from "./route-definition.js";
|
|
24
|
+
import MapRootLayout from "./server/root-layout.js";
|
|
21
25
|
import type { AllUseItems } from "./route-types.js";
|
|
22
26
|
import {
|
|
23
27
|
EntryData,
|
|
@@ -265,14 +269,131 @@ export interface RSCRouterOptions<TEnv = any> {
|
|
|
265
269
|
| ((env: TEnv) => { store: SegmentCacheStore; enabled?: boolean });
|
|
266
270
|
}
|
|
267
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Type-level detection of conflicting route keys.
|
|
274
|
+
* Extracts keys that exist in both TExisting and TNew but with different URL patterns.
|
|
275
|
+
* Returns `never` if no conflicts exist.
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```typescript
|
|
279
|
+
* ConflictingKeys<{ a: "/a" }, { a: "/b" }> // "a" (conflict - same key, different URLs)
|
|
280
|
+
* ConflictingKeys<{ a: "/a" }, { a: "/a" }> // never (no conflict - same key and URL)
|
|
281
|
+
* ConflictingKeys<{ a: "/a" }, { b: "/b" }> // never (no conflict - different keys)
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
type ConflictingKeys<
|
|
285
|
+
TExisting extends Record<string, string>,
|
|
286
|
+
TNew extends Record<string, string>
|
|
287
|
+
> = {
|
|
288
|
+
[K in keyof TExisting & keyof TNew]: TExisting[K] extends TNew[K]
|
|
289
|
+
? TNew[K] extends TExisting[K]
|
|
290
|
+
? never // Same value, no conflict
|
|
291
|
+
: K // Different values, conflict
|
|
292
|
+
: K; // Different values, conflict
|
|
293
|
+
}[keyof TExisting & keyof TNew];
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Simplified route helpers for inline route definitions.
|
|
297
|
+
* Uses TRoutes (Record<string, string>) instead of RouteDefinition.
|
|
298
|
+
*
|
|
299
|
+
* Note: Some helpers use `any` for context types as a trade-off for simpler usage.
|
|
300
|
+
* The main type safety is in the `route` helper which enforces valid route names.
|
|
301
|
+
* For full type safety, use the standard map() API with separate handler files.
|
|
302
|
+
*/
|
|
303
|
+
type InlineRouteHelpers<
|
|
304
|
+
TRoutes extends Record<string, string>,
|
|
305
|
+
TEnv,
|
|
306
|
+
> = {
|
|
307
|
+
/**
|
|
308
|
+
* Define a route handler for a specific route pattern
|
|
309
|
+
*/
|
|
310
|
+
route: <K extends keyof TRoutes & string>(
|
|
311
|
+
name: K,
|
|
312
|
+
handler:
|
|
313
|
+
| ((ctx: HandlerContext<{}, TEnv>) => ReactNode | Promise<ReactNode>)
|
|
314
|
+
| ReactNode
|
|
315
|
+
) => AllUseItems;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Define a layout that wraps child routes
|
|
319
|
+
*/
|
|
320
|
+
layout: (
|
|
321
|
+
component: ReactNode | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>),
|
|
322
|
+
use?: () => AllUseItems[]
|
|
323
|
+
) => AllUseItems;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Define parallel routes
|
|
327
|
+
*/
|
|
328
|
+
parallel: (
|
|
329
|
+
slots: Record<`@${string}`, ReactNode | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)>,
|
|
330
|
+
use?: () => AllUseItems[]
|
|
331
|
+
) => AllUseItems;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Define route middleware
|
|
335
|
+
*/
|
|
336
|
+
middleware: (fn: (ctx: any, next: () => Promise<void>) => Promise<void>) => AllUseItems;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Define revalidation handlers
|
|
340
|
+
*/
|
|
341
|
+
revalidate: (fn: (ctx: any) => boolean | Promise<boolean>) => AllUseItems;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Define data loaders
|
|
345
|
+
*/
|
|
346
|
+
loader: (loader: any, use?: () => AllUseItems[]) => AllUseItems;
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Define loading states
|
|
350
|
+
*/
|
|
351
|
+
loading: (component: ReactNode) => AllUseItems;
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Define error boundaries
|
|
355
|
+
*/
|
|
356
|
+
errorBoundary: (
|
|
357
|
+
handler: ReactNode | ((props: { error: Error }) => ReactNode)
|
|
358
|
+
) => AllUseItems;
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Define not found boundaries
|
|
362
|
+
*/
|
|
363
|
+
notFoundBoundary: (
|
|
364
|
+
handler: ReactNode | ((props: { pathname: string }) => ReactNode)
|
|
365
|
+
) => AllUseItems;
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Define intercept routes
|
|
369
|
+
*/
|
|
370
|
+
intercept: (
|
|
371
|
+
name: string,
|
|
372
|
+
handler: ReactNode | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>),
|
|
373
|
+
use?: () => AllUseItems[]
|
|
374
|
+
) => AllUseItems;
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Define when conditions for intercepts
|
|
378
|
+
*/
|
|
379
|
+
when: (condition: (ctx: any) => boolean | Promise<boolean>) => AllUseItems;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Define cache configuration
|
|
383
|
+
*/
|
|
384
|
+
cache: (config: { ttl?: number; swr?: number } | false, use?: () => AllUseItems[]) => AllUseItems;
|
|
385
|
+
};
|
|
386
|
+
|
|
268
387
|
/**
|
|
269
388
|
* Router builder for chaining .use() and .map()
|
|
270
389
|
* TRoutes accumulates all registered route types through the chain
|
|
390
|
+
* TLocalRoutes contains the routes for the current .routes() call (for inline handler typing)
|
|
271
391
|
*/
|
|
272
392
|
interface RouteBuilder<
|
|
273
393
|
T extends RouteDefinition,
|
|
274
394
|
TEnv,
|
|
275
395
|
TRoutes extends Record<string, string>,
|
|
396
|
+
TLocalRoutes extends Record<string, string> = Record<string, string>,
|
|
276
397
|
> {
|
|
277
398
|
/**
|
|
278
399
|
* Add middleware scoped to this mount
|
|
@@ -289,8 +410,34 @@ interface RouteBuilder<
|
|
|
289
410
|
use(
|
|
290
411
|
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
291
412
|
middleware?: MiddlewareFn<TEnv>
|
|
292
|
-
): RouteBuilder<T, TEnv, TRoutes>;
|
|
413
|
+
): RouteBuilder<T, TEnv, TRoutes, TLocalRoutes>;
|
|
293
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Map routes to handlers
|
|
417
|
+
*
|
|
418
|
+
* Supports two patterns:
|
|
419
|
+
*
|
|
420
|
+
* 1. Lazy loading (code-split):
|
|
421
|
+
* ```typescript
|
|
422
|
+
* .routes(homeRoutes)
|
|
423
|
+
* .map(() => import("./handlers/home"))
|
|
424
|
+
* ```
|
|
425
|
+
*
|
|
426
|
+
* 2. Inline definition:
|
|
427
|
+
* ```typescript
|
|
428
|
+
* .routes({ index: "/", about: "/about" })
|
|
429
|
+
* .map(({ route }) => [
|
|
430
|
+
* route("index", () => <HomePage />),
|
|
431
|
+
* route("about", () => <AboutPage />),
|
|
432
|
+
* ])
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
// Inline definition overload - handler receives helpers (must be first for correct inference)
|
|
436
|
+
// Uses TLocalRoutes so route names don't need the prefix
|
|
437
|
+
map<H extends (helpers: InlineRouteHelpers<TLocalRoutes, TEnv>) => Array<AllUseItems>>(
|
|
438
|
+
handler: H
|
|
439
|
+
): RSCRouter<TEnv, TRoutes>;
|
|
440
|
+
// Lazy loading overload - no parameters
|
|
294
441
|
map(
|
|
295
442
|
handler: () =>
|
|
296
443
|
| Array<AllUseItems>
|
|
@@ -315,24 +462,40 @@ export interface RSCRouter<
|
|
|
315
462
|
> {
|
|
316
463
|
/**
|
|
317
464
|
* Register routes with a prefix
|
|
318
|
-
* Route
|
|
465
|
+
* Route keys stay unchanged, only URL patterns get the prefix applied.
|
|
466
|
+
* This enables composable route modules that work regardless of mount point.
|
|
467
|
+
*
|
|
468
|
+
* @throws Compile-time error if route keys conflict with previously registered routes
|
|
319
469
|
*/
|
|
320
470
|
routes<TPrefix extends string, T extends ResolvedRouteMap<any>>(
|
|
321
471
|
prefix: TPrefix,
|
|
322
472
|
routes: T
|
|
323
|
-
):
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
473
|
+
): ConflictingKeys<TRoutes, T> extends never
|
|
474
|
+
? RouteBuilder<
|
|
475
|
+
RouteDefinition,
|
|
476
|
+
TEnv,
|
|
477
|
+
TRoutes & PrefixRoutePatterns<T, TPrefix>,
|
|
478
|
+
T
|
|
479
|
+
>
|
|
480
|
+
: {
|
|
481
|
+
__error: `Route key conflict! Keys [${ConflictingKeys<TRoutes, T> & string}] already exist with different URL patterns.`;
|
|
482
|
+
hint: "Use unique key names for each route definition.";
|
|
483
|
+
};
|
|
328
484
|
|
|
329
485
|
/**
|
|
330
486
|
* Register routes without a prefix
|
|
331
487
|
* Route types are accumulated through the chain
|
|
488
|
+
*
|
|
489
|
+
* @throws Compile-time error if route keys conflict with previously registered routes
|
|
332
490
|
*/
|
|
333
491
|
routes<T extends ResolvedRouteMap<any>>(
|
|
334
492
|
routes: T
|
|
335
|
-
):
|
|
493
|
+
): ConflictingKeys<TRoutes, T> extends never
|
|
494
|
+
? RouteBuilder<RouteDefinition, TEnv, TRoutes & T, T>
|
|
495
|
+
: {
|
|
496
|
+
__error: `Route key conflict! Keys [${ConflictingKeys<TRoutes, T> & string}] already exist with different URL patterns.`;
|
|
497
|
+
hint: "Use unique key names for each route definition.";
|
|
498
|
+
};
|
|
336
499
|
|
|
337
500
|
/**
|
|
338
501
|
* Add global middleware that runs on all routes
|
|
@@ -355,11 +518,13 @@ export interface RSCRouter<
|
|
|
355
518
|
/**
|
|
356
519
|
* Type-safe URL builder for registered routes
|
|
357
520
|
* Types are inferred from the accumulated route registrations
|
|
521
|
+
* Route keys stay unchanged regardless of mount prefix.
|
|
358
522
|
*
|
|
359
523
|
* @example
|
|
360
524
|
* ```typescript
|
|
361
|
-
*
|
|
362
|
-
* router.href("
|
|
525
|
+
* // Given: .routes("/shop", { cart: "/cart", detail: "/product/:slug" })
|
|
526
|
+
* router.href("cart"); // "/shop/cart"
|
|
527
|
+
* router.href("detail", { slug: "widget" }); // "/shop/product/widget"
|
|
363
528
|
* ```
|
|
364
529
|
*/
|
|
365
530
|
href: HrefFunction<TRoutes>;
|
|
@@ -477,14 +642,16 @@ export interface RSCRouter<
|
|
|
477
642
|
* });
|
|
478
643
|
*
|
|
479
644
|
* // Route types accumulate through the chain - no module augmentation needed!
|
|
645
|
+
* // Keys stay unchanged, only URL patterns get the prefix
|
|
480
646
|
* router
|
|
481
647
|
* .routes(homeRoutes) // accumulates homeRoutes
|
|
482
648
|
* .map(() => import('./home'))
|
|
483
|
-
* .routes('/shop', shopRoutes) // accumulates
|
|
649
|
+
* .routes('/shop', shopRoutes) // accumulates shopRoutes with prefixed URLs
|
|
484
650
|
* .map(() => import('./shop'));
|
|
485
651
|
*
|
|
486
652
|
* // router.href now has type-safe autocomplete for all registered routes
|
|
487
|
-
*
|
|
653
|
+
* // Given shopRoutes = { cart: "/cart" }, href uses original key:
|
|
654
|
+
* router.href("cart"); // "/shop/cart"
|
|
488
655
|
* ```
|
|
489
656
|
*/
|
|
490
657
|
export function createRSCRouter<TEnv = any>(
|
|
@@ -3393,15 +3560,13 @@ export function createRSCRouter<TEnv = any>(
|
|
|
3393
3560
|
function createRouteBuilder<TNewRoutes extends Record<string, string>>(
|
|
3394
3561
|
prefix: string,
|
|
3395
3562
|
routes: TNewRoutes
|
|
3396
|
-
): RouteBuilder<RouteDefinition, TEnv, TNewRoutes> {
|
|
3563
|
+
): RouteBuilder<RouteDefinition, TEnv, any, TNewRoutes> {
|
|
3397
3564
|
const currentMountIndex = mountIndex++;
|
|
3398
3565
|
|
|
3399
|
-
// Merge routes into the href map
|
|
3400
|
-
//
|
|
3566
|
+
// Merge routes into the href map
|
|
3567
|
+
// Keys stay unchanged for composability - only URL patterns get prefixed
|
|
3401
3568
|
const routeEntries = routes as Record<string, string>;
|
|
3402
3569
|
for (const [key, pattern] of Object.entries(routeEntries)) {
|
|
3403
|
-
// Build prefixed key: "shop" + "cart" -> "shop.cart"
|
|
3404
|
-
const prefixedKey = prefix ? `${prefix.slice(1)}.${key}` : key;
|
|
3405
3570
|
// Build prefixed pattern: "/shop" + "/cart" -> "/shop/cart"
|
|
3406
3571
|
const prefixedPattern =
|
|
3407
3572
|
prefix && pattern !== "/"
|
|
@@ -3409,7 +3574,18 @@ export function createRSCRouter<TEnv = any>(
|
|
|
3409
3574
|
: prefix && pattern === "/"
|
|
3410
3575
|
? prefix
|
|
3411
3576
|
: pattern;
|
|
3412
|
-
|
|
3577
|
+
|
|
3578
|
+
// Runtime validation: warn if key already exists with different pattern
|
|
3579
|
+
const existingPattern = mergedRouteMap[key];
|
|
3580
|
+
if (existingPattern !== undefined && existingPattern !== prefixedPattern) {
|
|
3581
|
+
console.warn(
|
|
3582
|
+
`[rsc-router] Route key conflict: "${key}" already maps to "${existingPattern}", ` +
|
|
3583
|
+
`overwriting with "${prefixedPattern}". Use unique key names to avoid this.`
|
|
3584
|
+
);
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
// Use original key - enables reusable route modules
|
|
3588
|
+
mergedRouteMap[key] = prefixedPattern;
|
|
3413
3589
|
}
|
|
3414
3590
|
|
|
3415
3591
|
// Auto-register route map for runtime href() usage
|
|
@@ -3421,7 +3597,7 @@ export function createRSCRouter<TEnv = any>(
|
|
|
3421
3597
|
| undefined;
|
|
3422
3598
|
|
|
3423
3599
|
// Create builder object so .use() can return it
|
|
3424
|
-
const builder: RouteBuilder<RouteDefinition, TEnv, TNewRoutes> = {
|
|
3600
|
+
const builder: RouteBuilder<RouteDefinition, TEnv, any, TNewRoutes> = {
|
|
3425
3601
|
use(
|
|
3426
3602
|
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
3427
3603
|
middleware?: MiddlewareFn<TEnv>
|
|
@@ -3432,16 +3608,22 @@ export function createRSCRouter<TEnv = any>(
|
|
|
3432
3608
|
},
|
|
3433
3609
|
|
|
3434
3610
|
map(
|
|
3435
|
-
handler:
|
|
3436
|
-
| Array<AllUseItems>
|
|
3437
|
-
|
|
|
3438
|
-
|
|
3611
|
+
handler:
|
|
3612
|
+
| ((helpers: InlineRouteHelpers<TNewRoutes, TEnv>) => Array<AllUseItems>)
|
|
3613
|
+
| (() =>
|
|
3614
|
+
| Array<AllUseItems>
|
|
3615
|
+
| Promise<{ default: () => Array<AllUseItems> }>
|
|
3616
|
+
| Promise<() => Array<AllUseItems>>)
|
|
3439
3617
|
) {
|
|
3618
|
+
// Store handler as-is - detection happens at call time based on return type
|
|
3619
|
+
// Both patterns use the same signature:
|
|
3620
|
+
// - Inline: ({ route }) => [...] - receives helpers, returns Array
|
|
3621
|
+
// - Lazy: () => import(...) - ignores helpers, returns Promise
|
|
3440
3622
|
routesEntries.push({
|
|
3441
3623
|
prefix,
|
|
3442
3624
|
routes: routes as ResolvedRouteMap<any>,
|
|
3443
3625
|
trailingSlash: trailingSlashConfig,
|
|
3444
|
-
handler,
|
|
3626
|
+
handler: handler as any,
|
|
3445
3627
|
mountIndex: currentMountIndex,
|
|
3446
3628
|
});
|
|
3447
3629
|
// Return router with accumulated types
|
package/src/rsc/handler.ts
CHANGED
|
@@ -60,9 +60,10 @@ import { invokeOnError } from "../router/error-handling.js";
|
|
|
60
60
|
* });
|
|
61
61
|
* ```
|
|
62
62
|
*/
|
|
63
|
-
export function createRSCHandler<
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
export function createRSCHandler<
|
|
64
|
+
TEnv = unknown,
|
|
65
|
+
TRoutes extends Record<string, string> = Record<string, string>,
|
|
66
|
+
>(options: CreateRSCHandlerOptions<TEnv, TRoutes>) {
|
|
66
67
|
const { router, version = VERSION, nonce: nonceProvider } = options;
|
|
67
68
|
|
|
68
69
|
// Use provided deps or default to @vitejs/plugin-rsc/rsc exports
|
package/src/rsc/types.ts
CHANGED
|
@@ -140,11 +140,14 @@ export type NonceProvider<TEnv = unknown> = (
|
|
|
140
140
|
/**
|
|
141
141
|
* Options for creating an RSC handler
|
|
142
142
|
*/
|
|
143
|
-
export interface CreateRSCHandlerOptions<
|
|
143
|
+
export interface CreateRSCHandlerOptions<
|
|
144
|
+
TEnv = unknown,
|
|
145
|
+
TRoutes extends Record<string, string> = Record<string, string>,
|
|
146
|
+
> {
|
|
144
147
|
/**
|
|
145
148
|
* The RSC router instance
|
|
146
149
|
*/
|
|
147
|
-
router: RSCRouter<TEnv>;
|
|
150
|
+
router: RSCRouter<TEnv, TRoutes>;
|
|
148
151
|
|
|
149
152
|
/**
|
|
150
153
|
* RSC dependencies from @vitejs/plugin-rsc/rsc.
|