@ivogt/rsc-router 0.0.0-experimental.12 → 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 +34 -34
- package/package.json +1 -1
- package/src/__tests__/component-utils.test.ts +76 -0
- package/src/cache/cf/cf-cache-store.ts +1 -1
- package/src/component-utils.ts +76 -0
- 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 +212 -36
- package/src/rsc/handler.ts +5 -4
- package/src/rsc/types.ts +5 -2
- package/src/server.ts +7 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +7 -7
- package/src/vite/expose-action-id.ts +2 -2
- package/src/vite/expose-handle-id.ts +4 -4
- package/src/vite/expose-loader-id.ts +7 -7
- package/src/vite/expose-location-state-id.ts +4 -4
- package/src/vite/index.ts +11 -11
- package/src/vite/package-resolution.ts +3 -3
- package/src/vite/version.d.ts +1 -1
- package/src/vite/virtual-entries.ts +10 -10
package/src/router.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ComponentType } from "react";
|
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
3
|
import { CacheScope, createCacheScope } from "./cache/cache-scope.js";
|
|
4
4
|
import type { SegmentCacheStore } from "./cache/types.js";
|
|
5
|
+
import { assertClientComponent } from "./component-utils.js";
|
|
5
6
|
import { DefaultDocument } from "./components/DefaultDocument.js";
|
|
6
7
|
import { DefaultErrorFallback } from "./default-error-boundary.js";
|
|
7
8
|
import {
|
|
@@ -13,10 +14,14 @@ import {
|
|
|
13
14
|
import {
|
|
14
15
|
createHref,
|
|
15
16
|
type HrefFunction,
|
|
16
|
-
type
|
|
17
|
-
type SanitizePrefix,
|
|
17
|
+
type PrefixRoutePatterns,
|
|
18
18
|
} from "./href.js";
|
|
19
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";
|
|
20
25
|
import type { AllUseItems } from "./route-types.js";
|
|
21
26
|
import {
|
|
22
27
|
EntryData,
|
|
@@ -264,14 +269,131 @@ export interface RSCRouterOptions<TEnv = any> {
|
|
|
264
269
|
| ((env: TEnv) => { store: SegmentCacheStore; enabled?: boolean });
|
|
265
270
|
}
|
|
266
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
|
+
|
|
267
387
|
/**
|
|
268
388
|
* Router builder for chaining .use() and .map()
|
|
269
389
|
* TRoutes accumulates all registered route types through the chain
|
|
390
|
+
* TLocalRoutes contains the routes for the current .routes() call (for inline handler typing)
|
|
270
391
|
*/
|
|
271
392
|
interface RouteBuilder<
|
|
272
393
|
T extends RouteDefinition,
|
|
273
394
|
TEnv,
|
|
274
395
|
TRoutes extends Record<string, string>,
|
|
396
|
+
TLocalRoutes extends Record<string, string> = Record<string, string>,
|
|
275
397
|
> {
|
|
276
398
|
/**
|
|
277
399
|
* Add middleware scoped to this mount
|
|
@@ -288,8 +410,34 @@ interface RouteBuilder<
|
|
|
288
410
|
use(
|
|
289
411
|
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
290
412
|
middleware?: MiddlewareFn<TEnv>
|
|
291
|
-
): RouteBuilder<T, TEnv, TRoutes>;
|
|
413
|
+
): RouteBuilder<T, TEnv, TRoutes, TLocalRoutes>;
|
|
292
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
|
|
293
441
|
map(
|
|
294
442
|
handler: () =>
|
|
295
443
|
| Array<AllUseItems>
|
|
@@ -314,24 +462,40 @@ export interface RSCRouter<
|
|
|
314
462
|
> {
|
|
315
463
|
/**
|
|
316
464
|
* Register routes with a prefix
|
|
317
|
-
* 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
|
|
318
469
|
*/
|
|
319
470
|
routes<TPrefix extends string, T extends ResolvedRouteMap<any>>(
|
|
320
471
|
prefix: TPrefix,
|
|
321
472
|
routes: T
|
|
322
|
-
):
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
+
};
|
|
327
484
|
|
|
328
485
|
/**
|
|
329
486
|
* Register routes without a prefix
|
|
330
487
|
* Route types are accumulated through the chain
|
|
488
|
+
*
|
|
489
|
+
* @throws Compile-time error if route keys conflict with previously registered routes
|
|
331
490
|
*/
|
|
332
491
|
routes<T extends ResolvedRouteMap<any>>(
|
|
333
492
|
routes: T
|
|
334
|
-
):
|
|
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
|
+
};
|
|
335
499
|
|
|
336
500
|
/**
|
|
337
501
|
* Add global middleware that runs on all routes
|
|
@@ -354,11 +518,13 @@ export interface RSCRouter<
|
|
|
354
518
|
/**
|
|
355
519
|
* Type-safe URL builder for registered routes
|
|
356
520
|
* Types are inferred from the accumulated route registrations
|
|
521
|
+
* Route keys stay unchanged regardless of mount prefix.
|
|
357
522
|
*
|
|
358
523
|
* @example
|
|
359
524
|
* ```typescript
|
|
360
|
-
*
|
|
361
|
-
* 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"
|
|
362
528
|
* ```
|
|
363
529
|
*/
|
|
364
530
|
href: HrefFunction<TRoutes>;
|
|
@@ -476,14 +642,16 @@ export interface RSCRouter<
|
|
|
476
642
|
* });
|
|
477
643
|
*
|
|
478
644
|
* // Route types accumulate through the chain - no module augmentation needed!
|
|
645
|
+
* // Keys stay unchanged, only URL patterns get the prefix
|
|
479
646
|
* router
|
|
480
647
|
* .routes(homeRoutes) // accumulates homeRoutes
|
|
481
648
|
* .map(() => import('./home'))
|
|
482
|
-
* .routes('/shop', shopRoutes) // accumulates
|
|
649
|
+
* .routes('/shop', shopRoutes) // accumulates shopRoutes with prefixed URLs
|
|
483
650
|
* .map(() => import('./shop'));
|
|
484
651
|
*
|
|
485
652
|
* // router.href now has type-safe autocomplete for all registered routes
|
|
486
|
-
*
|
|
653
|
+
* // Given shopRoutes = { cart: "/cart" }, href uses original key:
|
|
654
|
+
* router.href("cart"); // "/shop/cart"
|
|
487
655
|
* ```
|
|
488
656
|
*/
|
|
489
657
|
export function createRSCRouter<TEnv = any>(
|
|
@@ -511,16 +679,9 @@ export function createRSCRouter<TEnv = any>(
|
|
|
511
679
|
invokeOnError(onError, error, phase, context, "Router");
|
|
512
680
|
}
|
|
513
681
|
|
|
514
|
-
// Validate document is a
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
// "Functions cannot be passed to Client Components" error.
|
|
518
|
-
if (documentOption !== undefined && typeof documentOption !== "function") {
|
|
519
|
-
throw new Error(
|
|
520
|
-
`document must be a client component function with "use client" directive. ` +
|
|
521
|
-
`Make sure to pass the component itself, not a JSX element: ` +
|
|
522
|
-
`document: MyDocument (correct) vs document: <MyDocument /> (incorrect)`
|
|
523
|
-
);
|
|
682
|
+
// Validate document is a client component
|
|
683
|
+
if (documentOption !== undefined) {
|
|
684
|
+
assertClientComponent(documentOption, "document");
|
|
524
685
|
}
|
|
525
686
|
|
|
526
687
|
// Use default document if none provided (keeps internal name as rootLayout)
|
|
@@ -3399,15 +3560,13 @@ export function createRSCRouter<TEnv = any>(
|
|
|
3399
3560
|
function createRouteBuilder<TNewRoutes extends Record<string, string>>(
|
|
3400
3561
|
prefix: string,
|
|
3401
3562
|
routes: TNewRoutes
|
|
3402
|
-
): RouteBuilder<RouteDefinition, TEnv, TNewRoutes> {
|
|
3563
|
+
): RouteBuilder<RouteDefinition, TEnv, any, TNewRoutes> {
|
|
3403
3564
|
const currentMountIndex = mountIndex++;
|
|
3404
3565
|
|
|
3405
|
-
// Merge routes into the href map
|
|
3406
|
-
//
|
|
3566
|
+
// Merge routes into the href map
|
|
3567
|
+
// Keys stay unchanged for composability - only URL patterns get prefixed
|
|
3407
3568
|
const routeEntries = routes as Record<string, string>;
|
|
3408
3569
|
for (const [key, pattern] of Object.entries(routeEntries)) {
|
|
3409
|
-
// Build prefixed key: "shop" + "cart" -> "shop.cart"
|
|
3410
|
-
const prefixedKey = prefix ? `${prefix.slice(1)}.${key}` : key;
|
|
3411
3570
|
// Build prefixed pattern: "/shop" + "/cart" -> "/shop/cart"
|
|
3412
3571
|
const prefixedPattern =
|
|
3413
3572
|
prefix && pattern !== "/"
|
|
@@ -3415,7 +3574,18 @@ export function createRSCRouter<TEnv = any>(
|
|
|
3415
3574
|
: prefix && pattern === "/"
|
|
3416
3575
|
? prefix
|
|
3417
3576
|
: pattern;
|
|
3418
|
-
|
|
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;
|
|
3419
3589
|
}
|
|
3420
3590
|
|
|
3421
3591
|
// Auto-register route map for runtime href() usage
|
|
@@ -3427,7 +3597,7 @@ export function createRSCRouter<TEnv = any>(
|
|
|
3427
3597
|
| undefined;
|
|
3428
3598
|
|
|
3429
3599
|
// Create builder object so .use() can return it
|
|
3430
|
-
const builder: RouteBuilder<RouteDefinition, TEnv, TNewRoutes> = {
|
|
3600
|
+
const builder: RouteBuilder<RouteDefinition, TEnv, any, TNewRoutes> = {
|
|
3431
3601
|
use(
|
|
3432
3602
|
patternOrMiddleware: string | MiddlewareFn<TEnv>,
|
|
3433
3603
|
middleware?: MiddlewareFn<TEnv>
|
|
@@ -3438,16 +3608,22 @@ export function createRSCRouter<TEnv = any>(
|
|
|
3438
3608
|
},
|
|
3439
3609
|
|
|
3440
3610
|
map(
|
|
3441
|
-
handler:
|
|
3442
|
-
| Array<AllUseItems>
|
|
3443
|
-
|
|
|
3444
|
-
|
|
3611
|
+
handler:
|
|
3612
|
+
| ((helpers: InlineRouteHelpers<TNewRoutes, TEnv>) => Array<AllUseItems>)
|
|
3613
|
+
| (() =>
|
|
3614
|
+
| Array<AllUseItems>
|
|
3615
|
+
| Promise<{ default: () => Array<AllUseItems> }>
|
|
3616
|
+
| Promise<() => Array<AllUseItems>>)
|
|
3445
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
|
|
3446
3622
|
routesEntries.push({
|
|
3447
3623
|
prefix,
|
|
3448
3624
|
routes: routes as ResolvedRouteMap<any>,
|
|
3449
3625
|
trailingSlash: trailingSlashConfig,
|
|
3450
|
-
handler,
|
|
3626
|
+
handler: handler as any,
|
|
3451
3627
|
mountIndex: currentMountIndex,
|
|
3452
3628
|
});
|
|
3453
3629
|
// Return router with accumulated types
|
package/src/rsc/handler.ts
CHANGED
|
@@ -32,7 +32,7 @@ import type {
|
|
|
32
32
|
} from "./types.js";
|
|
33
33
|
import { hasBodyContent, createResponseWithMergedHeaders } from "./helpers.js";
|
|
34
34
|
import { generateNonce } from "./nonce.js";
|
|
35
|
-
import { VERSION } from "rsc-router:version";
|
|
35
|
+
import { VERSION } from "@ivogt/rsc-router:version";
|
|
36
36
|
import type { ErrorPhase } from "../types.js";
|
|
37
37
|
import { invokeOnError } from "../router/error-handling.js";
|
|
38
38
|
|
|
@@ -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.
|
package/src/server.ts
CHANGED
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
createHref,
|
|
31
31
|
type HrefFunction,
|
|
32
32
|
type PrefixedRoutes,
|
|
33
|
+
type PrefixRoutePatterns,
|
|
33
34
|
type ParamsFor,
|
|
34
35
|
type SanitizePrefix,
|
|
35
36
|
type MergeRoutes,
|
|
@@ -82,6 +83,12 @@ export {
|
|
|
82
83
|
sanitizeError,
|
|
83
84
|
} from "./errors.js";
|
|
84
85
|
|
|
86
|
+
// Component utilities
|
|
87
|
+
export {
|
|
88
|
+
isClientComponent,
|
|
89
|
+
assertClientComponent,
|
|
90
|
+
} from "./component-utils.js";
|
|
91
|
+
|
|
85
92
|
// Types (re-exported for convenience)
|
|
86
93
|
export type {
|
|
87
94
|
RouterEnv,
|
|
@@ -4,7 +4,7 @@ import { describe, it, expect } from "vitest";
|
|
|
4
4
|
* Mock function to test createLoader detection patterns
|
|
5
5
|
*/
|
|
6
6
|
function hasCreateLoaderImport(code: string): boolean {
|
|
7
|
-
const pattern = /import\s*\{[^}]*\bcreateLoader\b[^}]*\}\s*from\s*["']rsc-router(?:\/server)?["']/;
|
|
7
|
+
const pattern = /import\s*\{[^}]*\bcreateLoader\b[^}]*\}\s*from\s*["']@ivogt\/rsc-router(?:\/server)?["']/;
|
|
8
8
|
return pattern.test(code);
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -24,22 +24,22 @@ function extractLoaderExports(code: string): string[] {
|
|
|
24
24
|
describe("exposeLoaderId plugin", () => {
|
|
25
25
|
describe("hasCreateLoaderImport", () => {
|
|
26
26
|
it("should detect direct import from rsc-router", () => {
|
|
27
|
-
const code = `import { createLoader } from "rsc-router";`;
|
|
27
|
+
const code = `import { createLoader } from "@ivogt/rsc-router";`;
|
|
28
28
|
expect(hasCreateLoaderImport(code)).toBe(true);
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
it("should detect import from rsc-router/server", () => {
|
|
32
|
-
const code = `import { createLoader } from "rsc-router/server";`;
|
|
32
|
+
const code = `import { createLoader } from "@ivogt/rsc-router/server";`;
|
|
33
33
|
expect(hasCreateLoaderImport(code)).toBe(true);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
it("should detect createLoader with other imports", () => {
|
|
37
|
-
const code = `import { map, createLoader, route } from "rsc-router";`;
|
|
37
|
+
const code = `import { map, createLoader, route } from "@ivogt/rsc-router";`;
|
|
38
38
|
expect(hasCreateLoaderImport(code)).toBe(true);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it("should NOT detect aliased import", () => {
|
|
42
|
-
const code = `import { createLoader as cl } from "rsc-router";`;
|
|
42
|
+
const code = `import { createLoader as cl } from "@ivogt/rsc-router";`;
|
|
43
43
|
// Our simple pattern doesn't support aliasing - this is intentional
|
|
44
44
|
expect(hasCreateLoaderImport(code)).toBe(true); // Still matches the word
|
|
45
45
|
});
|
|
@@ -50,12 +50,12 @@ describe("exposeLoaderId plugin", () => {
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
it("should NOT detect default import", () => {
|
|
53
|
-
const code = `import createLoader from "rsc-router";`;
|
|
53
|
+
const code = `import createLoader from "@ivogt/rsc-router";`;
|
|
54
54
|
expect(hasCreateLoaderImport(code)).toBe(false);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
it("should NOT detect namespace import", () => {
|
|
58
|
-
const code = `import * as router from "rsc-router";`;
|
|
58
|
+
const code = `import * as router from "@ivogt/rsc-router";`;
|
|
59
59
|
expect(hasCreateLoaderImport(code)).toBe(false);
|
|
60
60
|
});
|
|
61
61
|
});
|
|
@@ -229,7 +229,7 @@ export function exposeActionId(): Plugin {
|
|
|
229
229
|
let rscPluginApi: RscPluginApi | undefined;
|
|
230
230
|
|
|
231
231
|
return {
|
|
232
|
-
name: "rsc-router:expose-action-id",
|
|
232
|
+
name: "@ivogt/rsc-router:expose-action-id",
|
|
233
233
|
// Run after all other plugins (including RSC plugin's transforms)
|
|
234
234
|
enforce: "post",
|
|
235
235
|
|
|
@@ -251,7 +251,7 @@ export function exposeActionId(): Plugin {
|
|
|
251
251
|
if (!rscPluginApi) {
|
|
252
252
|
throw new Error(
|
|
253
253
|
"[rsc-router] Could not find @vitejs/plugin-rsc. " +
|
|
254
|
-
"rsc-router requires the Vite RSC plugin.\n" +
|
|
254
|
+
"@ivogt/rsc-router requires the Vite RSC plugin.\n" +
|
|
255
255
|
"The RSC plugin should be included automatically. If you disabled it with\n" +
|
|
256
256
|
"rscRouter({ rsc: false }), add rsc() before rscRouter() in your config."
|
|
257
257
|
);
|
|
@@ -25,9 +25,9 @@ function hashHandleId(filePath: string, exportName: string): string {
|
|
|
25
25
|
* Check if file imports createHandle from rsc-router
|
|
26
26
|
*/
|
|
27
27
|
function hasCreateHandleImport(code: string): boolean {
|
|
28
|
-
// Match: import { createHandle } from "rsc-router" or "rsc-router/..."
|
|
28
|
+
// Match: import { createHandle } from "@ivogt/rsc-router" or "@ivogt/rsc-router/..."
|
|
29
29
|
const pattern =
|
|
30
|
-
/import\s*\{[^}]*\bcreateHandle\b[^}]*\}\s*from\s*["']rsc-router(?:\/[^"']+)?["']/;
|
|
30
|
+
/import\s*\{[^}]*\bcreateHandle\b[^}]*\}\s*from\s*["']@ivogt\/rsc-router(?:\/[^"']+)?["']/;
|
|
31
31
|
return pattern.test(code);
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -167,7 +167,7 @@ function transformHandleExports(
|
|
|
167
167
|
* The name is auto-generated from file path + export name.
|
|
168
168
|
*
|
|
169
169
|
* Requirements:
|
|
170
|
-
* - Must use direct import: import { createHandle } from "rsc-router"
|
|
170
|
+
* - Must use direct import: import { createHandle } from "@ivogt/rsc-router"
|
|
171
171
|
* - Must use named export: export const MyHandle = createHandle(...)
|
|
172
172
|
*/
|
|
173
173
|
export function exposeHandleId(): Plugin {
|
|
@@ -175,7 +175,7 @@ export function exposeHandleId(): Plugin {
|
|
|
175
175
|
let isBuild = false;
|
|
176
176
|
|
|
177
177
|
return {
|
|
178
|
-
name: "rsc-router:expose-handle-id",
|
|
178
|
+
name: "@ivogt/rsc-router:expose-handle-id",
|
|
179
179
|
enforce: "post",
|
|
180
180
|
|
|
181
181
|
configResolved(resolvedConfig) {
|
|
@@ -25,10 +25,10 @@ function hashLoaderId(filePath: string, exportName: string): string {
|
|
|
25
25
|
* Check if file imports createLoader from rsc-router
|
|
26
26
|
*/
|
|
27
27
|
function hasCreateLoaderImport(code: string): boolean {
|
|
28
|
-
// Match: import { createLoader } from "rsc-router" or "rsc-router/server"
|
|
28
|
+
// Match: import { createLoader } from "@ivogt/rsc-router" or "@ivogt/rsc-router/server"
|
|
29
29
|
// Must be exact - no aliasing support
|
|
30
30
|
const pattern =
|
|
31
|
-
/import\s*\{[^}]*\bcreateLoader\b[^}]*\}\s*from\s*["']rsc-router(?:\/server)?["']/;
|
|
31
|
+
/import\s*\{[^}]*\bcreateLoader\b[^}]*\}\s*from\s*["']@ivogt\/rsc-router(?:\/server)?["']/;
|
|
32
32
|
return pattern.test(code);
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -176,7 +176,7 @@ let manifestGenerated = false;
|
|
|
176
176
|
* The manifest can be imported by the RSC handler to get all loaders.
|
|
177
177
|
*
|
|
178
178
|
* Requirements:
|
|
179
|
-
* - Must use direct import: import { createLoader } from "rsc-router"
|
|
179
|
+
* - Must use direct import: import { createLoader } from "@ivogt/rsc-router"
|
|
180
180
|
* - No aliasing support (import { createLoader as cl } won't work)
|
|
181
181
|
* - Must use named export: export const MyLoader = createLoader(...)
|
|
182
182
|
*/
|
|
@@ -194,7 +194,7 @@ export function exposeLoaderId(): Plugin {
|
|
|
194
194
|
const pendingLoaderScans = new Map<string, Promise<void>>();
|
|
195
195
|
|
|
196
196
|
return {
|
|
197
|
-
name: "rsc-router:expose-loader-id",
|
|
197
|
+
name: "@ivogt/rsc-router:expose-loader-id",
|
|
198
198
|
enforce: "post",
|
|
199
199
|
|
|
200
200
|
configResolved(resolvedConfig) {
|
|
@@ -276,7 +276,7 @@ export function exposeLoaderId(): Plugin {
|
|
|
276
276
|
if (!isBuild) {
|
|
277
277
|
// Dev mode: empty map - use fallback path parsing in loader registry
|
|
278
278
|
// IDs in dev mode are "filePath#exportName" format for easier debugging
|
|
279
|
-
return `import { setLoaderImports } from "rsc-router/server";
|
|
279
|
+
return `import { setLoaderImports } from "@ivogt/rsc-router/server";
|
|
280
280
|
|
|
281
281
|
// Dev mode: empty map, loaders are resolved dynamically via path parsing
|
|
282
282
|
setLoaderImports({});
|
|
@@ -297,14 +297,14 @@ setLoaderImports({});
|
|
|
297
297
|
|
|
298
298
|
// If no loaders discovered, set empty map
|
|
299
299
|
if (lazyImports.length === 0) {
|
|
300
|
-
return `import { setLoaderImports } from "rsc-router/server";
|
|
300
|
+
return `import { setLoaderImports } from "@ivogt/rsc-router/server";
|
|
301
301
|
|
|
302
302
|
// No fetchable loaders discovered during build
|
|
303
303
|
setLoaderImports({});
|
|
304
304
|
`;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
const code = `import { setLoaderImports } from "rsc-router/server";
|
|
307
|
+
const code = `import { setLoaderImports } from "@ivogt/rsc-router/server";
|
|
308
308
|
|
|
309
309
|
// Lazy import map - loaders are loaded on-demand when first requested
|
|
310
310
|
setLoaderImports({
|
|
@@ -25,9 +25,9 @@ function hashLocationStateKey(filePath: string, exportName: string): string {
|
|
|
25
25
|
* Check if file imports createLocationState from rsc-router
|
|
26
26
|
*/
|
|
27
27
|
function hasCreateLocationStateImport(code: string): boolean {
|
|
28
|
-
// Match: import { createLocationState } from "rsc-router" or "rsc-router/client"
|
|
28
|
+
// Match: import { createLocationState } from "@ivogt/rsc-router" or "@ivogt/rsc-router/client"
|
|
29
29
|
const pattern =
|
|
30
|
-
/import\s*\{[^}]*\bcreateLocationState\b[^}]*\}\s*from\s*["']rsc-router(?:\/[^"']+)?["']/;
|
|
30
|
+
/import\s*\{[^}]*\bcreateLocationState\b[^}]*\}\s*from\s*["']@ivogt\/rsc-router(?:\/[^"']+)?["']/;
|
|
31
31
|
return pattern.test(code);
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -135,7 +135,7 @@ function transformLocationStateExports(
|
|
|
135
135
|
* The key is auto-generated from file path + export name.
|
|
136
136
|
*
|
|
137
137
|
* Requirements:
|
|
138
|
-
* - Must use direct import: import { createLocationState } from "rsc-router"
|
|
138
|
+
* - Must use direct import: import { createLocationState } from "@ivogt/rsc-router"
|
|
139
139
|
* - Must use named export: export const MyState = createLocationState(...)
|
|
140
140
|
*/
|
|
141
141
|
export function exposeLocationStateId(): Plugin {
|
|
@@ -143,7 +143,7 @@ export function exposeLocationStateId(): Plugin {
|
|
|
143
143
|
let isBuild = false;
|
|
144
144
|
|
|
145
145
|
return {
|
|
146
|
-
name: "rsc-router:expose-location-state-id",
|
|
146
|
+
name: "@ivogt/rsc-router:expose-location-state-id",
|
|
147
147
|
enforce: "post",
|
|
148
148
|
|
|
149
149
|
configResolved(resolvedConfig) {
|