@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/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 PrefixedRoutes,
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 types are accumulated through the chain
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
- ): RouteBuilder<
323
- RouteDefinition,
324
- TEnv,
325
- TRoutes & PrefixedRoutes<T, SanitizePrefix<TPrefix>>
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
- ): RouteBuilder<RouteDefinition, TEnv, TRoutes & T>;
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
- * router.href("shop.cart"); // "/shop/cart"
361
- * router.href("shop.products.detail", { slug: "widget" }); // "/shop/product/widget"
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 PrefixedRoutes<shopRoutes, "shop">
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
- * router.href("shop.cart");
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 function (component)
515
- // Note: We cannot validate "use client" at runtime since it's a bundler directive.
516
- // If a server component is passed, React will throw during rendering with a
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 with prefixes
3406
- // This enables type-safe router.href() calls
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
- mergedRouteMap[prefixedKey] = prefixedPattern;
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
- | Promise<{ default: () => Array<AllUseItems> }>
3444
- | Promise<() => Array<AllUseItems>>
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
@@ -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<TEnv = unknown>(
64
- options: CreateRSCHandlerOptions<TEnv>
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<TEnv = unknown> {
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) {