@ivogt/rsc-router 0.0.0-experimental.5 → 0.0.0-experimental.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ivogt/rsc-router",
3
- "version": "0.0.0-experimental.5",
3
+ "version": "0.0.0-experimental.7",
4
4
  "type": "module",
5
5
  "description": "Type-safe RSC router with partial rendering support",
6
6
  "author": "Ivo Todorov",
package/src/router.ts CHANGED
@@ -160,6 +160,39 @@ export interface RSCRouterOptions<TEnv = any> {
160
160
  */
161
161
  defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler;
162
162
 
163
+ /**
164
+ * Component to render when no route matches the requested URL.
165
+ *
166
+ * This is rendered within your document/app shell with a 404 status code.
167
+ * Use this for a custom 404 page that maintains your app's look and feel.
168
+ *
169
+ * If not provided, a default "Page not found" component is rendered.
170
+ *
171
+ * Can be a static ReactNode or a function receiving the pathname.
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * // Simple static component
176
+ * const router = createRSCRouter<AppEnv>({
177
+ * document: AppShell,
178
+ * notFound: <NotFound404 />,
179
+ * });
180
+ *
181
+ * // Dynamic component with pathname
182
+ * const router = createRSCRouter<AppEnv>({
183
+ * document: AppShell,
184
+ * notFound: ({ pathname }) => (
185
+ * <div>
186
+ * <h1>404 - Not Found</h1>
187
+ * <p>No page exists at {pathname}</p>
188
+ * <a href="/">Go home</a>
189
+ * </div>
190
+ * ),
191
+ * });
192
+ * ```
193
+ */
194
+ notFound?: ReactNode | ((props: { pathname: string }) => ReactNode);
195
+
163
196
  /**
164
197
  * Callback invoked when an error occurs during request handling.
165
198
  *
@@ -368,6 +401,11 @@ export interface RSCRouter<
368
401
  */
369
402
  readonly cache?: RSCRouterOptions<TEnv>["cache"];
370
403
 
404
+ /**
405
+ * Not found component to render when no route matches (for internal use by RSC handler)
406
+ */
407
+ readonly notFound?: RSCRouterOptions<TEnv>["notFound"];
408
+
371
409
  /**
372
410
  * App-level middleware entries (for internal use by RSC handler)
373
411
  * These wrap the entire request/response cycle
@@ -456,6 +494,7 @@ export function createRSCRouter<TEnv = any>(
456
494
  document: documentOption,
457
495
  defaultErrorBoundary,
458
496
  defaultNotFoundBoundary,
497
+ notFound,
459
498
  onError,
460
499
  cache,
461
500
  } = options;
@@ -3471,6 +3510,9 @@ export function createRSCRouter<TEnv = any>(
3471
3510
  // Expose cache configuration for RSC handler
3472
3511
  cache,
3473
3512
 
3513
+ // Expose notFound component for RSC handler
3514
+ notFound,
3515
+
3474
3516
  // Expose global middleware for RSC handler
3475
3517
  middleware: globalMiddleware,
3476
3518
 
@@ -7,6 +7,7 @@
7
7
  * and progressive enhancement (no-JS form submissions).
8
8
  */
9
9
 
10
+ import { createElement } from "react";
10
11
  import { renderSegments } from "../segment-system.js";
11
12
  import { RouteNotFoundError } from "../errors.js";
12
13
  import { getLoaderLazy } from "../server/loader-registry.js";
@@ -291,22 +292,85 @@ export function createRSCHandler<TEnv = unknown>(
291
292
  // ============================================================================
292
293
  // REGULAR RSC RENDERING (Navigation)
293
294
  // ============================================================================
294
- return handleRscRendering(request, env, url, isPartial, handleStore, nonce);
295
+ // Note: Must use "return await" for try/catch to catch async rejections
296
+ return await handleRscRendering(request, env, url, isPartial, handleStore, nonce);
295
297
  } catch (error) {
296
298
  // Check if middleware/handler returned Response
297
299
  if (error instanceof Response) {
298
300
  return error;
299
301
  }
300
302
 
301
- // Return 404 for unmatched routes instead of 500
302
- if (error instanceof RouteNotFoundError) {
303
+ // Render 404 page for unmatched routes
304
+ // Check both instanceof and error.name for cross-bundle compatibility
305
+ const isRouteNotFound =
306
+ error instanceof RouteNotFoundError ||
307
+ (error instanceof Error && error.name === "RouteNotFoundError");
308
+ if (isRouteNotFound) {
303
309
  callOnError(error, "routing", {
304
310
  request,
305
311
  url,
306
312
  env,
307
- handledByBoundary: false,
313
+ handledByBoundary: true, // Handled by notFound component
314
+ });
315
+
316
+ // Get notFound component from router options or use default
317
+ const notFoundOption = router.notFound;
318
+ const notFoundComponent =
319
+ typeof notFoundOption === "function"
320
+ ? notFoundOption({ pathname: url.pathname })
321
+ : notFoundOption ?? createElement("h1", null, "Not Found");
322
+
323
+ // Create a simple segment for the 404 page
324
+ const notFoundSegment = {
325
+ id: "notFound",
326
+ namespace: "notFound",
327
+ type: "route" as const,
328
+ index: 0,
329
+ component: notFoundComponent,
330
+ params: {},
331
+ };
332
+
333
+ // Render with rootLayout to maintain app shell
334
+ const root = await renderSegments([notFoundSegment], {
335
+ rootLayout: router.rootLayout,
336
+ });
337
+
338
+ const payload: RscPayload = {
339
+ root,
340
+ metadata: {
341
+ pathname: url.pathname,
342
+ segments: [notFoundSegment],
343
+ matched: [],
344
+ diff: [],
345
+ isPartial: false,
346
+ handles: handleStore.stream(),
347
+ version,
348
+ },
349
+ };
350
+
351
+ const rscStream = renderToReadableStream(payload);
352
+
353
+ // Determine if this is an RSC request or HTML request
354
+ const isRscRequest =
355
+ (!request.headers.get("accept")?.includes("text/html") &&
356
+ !url.searchParams.has("__html")) ||
357
+ url.searchParams.has("__rsc");
358
+
359
+ if (isRscRequest) {
360
+ return createResponseWithMergedHeaders(rscStream, {
361
+ status: 404,
362
+ headers: { "content-type": "text/x-component;charset=utf-8" },
363
+ });
364
+ }
365
+
366
+ // Delegate to SSR for HTML response
367
+ const ssrModule = await loadSSRModule();
368
+ const htmlStream = await ssrModule.renderHTML(rscStream, { nonce });
369
+
370
+ return createResponseWithMergedHeaders(htmlStream, {
371
+ status: 404,
372
+ headers: { "content-type": "text/html;charset=utf-8" },
308
373
  });
309
- return createResponseWithMergedHeaders("Not Found", { status: 404 });
310
374
  }
311
375
 
312
376
  // Report unhandled errors
package/src/vite/index.ts CHANGED
@@ -12,6 +12,12 @@ import {
12
12
  getVirtualVersionContent,
13
13
  VIRTUAL_IDS,
14
14
  } from "./virtual-entries.ts";
15
+ import {
16
+ getExcludeDeps,
17
+ getPackageAliases,
18
+ getPublishedPackageName,
19
+ isWorkspaceDevelopment,
20
+ } from "./package-resolution.ts";
15
21
 
16
22
  // Re-export plugins
17
23
  export { exposeActionId } from "./expose-action-id.ts";
@@ -21,26 +27,6 @@ export { exposeLocationStateId } from "./expose-location-state-id.ts";
21
27
 
22
28
  // Virtual module type declarations in ./version.d.ts
23
29
 
24
- /**
25
- * Modules that must be excluded from Vite's dependency optimization.
26
- *
27
- * When rsc-router is installed from npm, Vite's dep optimizer bundles these modules
28
- * into separate chunks. However, @vitejs/plugin-rsc creates virtual proxy modules
29
- * for client components that import from the original source paths.
30
- *
31
- * This creates two different module instances:
32
- * 1. Bundled version in /node_modules/.vite/deps/
33
- * 2. Original source via plugin-rsc proxy
34
- *
35
- * When both instances create React Contexts (e.g., OutletContext), React sees them
36
- * as different contexts, causing useContext to return undefined even when a Provider
37
- * exists in the tree.
38
- *
39
- * By excluding these modules, we ensure a single module instance is used everywhere.
40
- *
41
- * We include both the scoped package name (@ivogt/rsc-router) and the aliased paths
42
- * (rsc-router) because Vite's optimizer runs before alias resolution.
43
- */
44
30
  /**
45
31
  * esbuild plugin to provide rsc-router:version virtual module during optimization.
46
32
  * This is needed because esbuild runs during Vite's dependency optimization phase,
@@ -68,118 +54,6 @@ const sharedEsbuildOptions = {
68
54
  plugins: [versionEsbuildPlugin],
69
55
  };
70
56
 
71
- const RSC_ROUTER_EXCLUDE_DEPS = [
72
- // Scoped package paths
73
- "@ivogt/rsc-router",
74
- "@ivogt/rsc-router/browser",
75
- "@ivogt/rsc-router/client",
76
- "@ivogt/rsc-router/server",
77
- "@ivogt/rsc-router/rsc",
78
- "@ivogt/rsc-router/ssr",
79
- "@ivogt/rsc-router/internal/deps/browser",
80
- "@ivogt/rsc-router/internal/deps/html-stream-client",
81
- "@ivogt/rsc-router/internal/deps/ssr",
82
- "@ivogt/rsc-router/internal/deps/rsc",
83
- // Aliased paths (before alias resolution)
84
- "rsc-router/browser",
85
- "rsc-router/client",
86
- "rsc-router/server",
87
- "rsc-router/rsc",
88
- "rsc-router/ssr",
89
- "rsc-router/internal/deps/browser",
90
- "rsc-router/internal/deps/html-stream-client",
91
- "rsc-router/internal/deps/ssr",
92
- "rsc-router/internal/deps/rsc",
93
- ];
94
-
95
- /**
96
- * Plugin to transform CJS react-server-dom vendor files to ESM.
97
- * The @vitejs/plugin-rsc package ships client.browser.js as CommonJS
98
- * which doesn't work in the browser. This transforms both:
99
- * 1. The entry point (client.browser.js) to re-export from the CJS file
100
- * 2. The actual CJS file content to ESM syntax
101
- */
102
- function createCjsToEsmPlugin(): Plugin {
103
- return {
104
- name: "rsc-router:cjs-to-esm",
105
- enforce: "pre",
106
- transform(code, id) {
107
- const cleanId = id.split("?")[0];
108
-
109
- // Transform the client.browser.js entry point to re-export from CJS
110
- if (
111
- cleanId.includes("vendor/react-server-dom/client.browser.js") ||
112
- cleanId.includes("vendor\\react-server-dom\\client.browser.js")
113
- ) {
114
- const isProd = process.env.NODE_ENV === "production";
115
- const cjsFile = isProd
116
- ? "./cjs/react-server-dom-webpack-client.browser.production.js"
117
- : "./cjs/react-server-dom-webpack-client.browser.development.js";
118
-
119
- return {
120
- code: `export * from "${cjsFile}";`,
121
- map: null,
122
- };
123
- }
124
-
125
- // Transform the actual CJS files to ESM
126
- if (
127
- (cleanId.includes("vendor/react-server-dom/cjs/") ||
128
- cleanId.includes("vendor\\react-server-dom\\cjs\\")) &&
129
- cleanId.includes("client.browser")
130
- ) {
131
- let transformed = code;
132
-
133
- // Extract the license comment to preserve it
134
- const licenseMatch = transformed.match(/^\/\*\*[\s\S]*?\*\//);
135
- const license = licenseMatch ? licenseMatch[0] : "";
136
- if (license) {
137
- transformed = transformed.slice(license.length);
138
- }
139
-
140
- // Remove "use strict" and the conditional IIFE wrapper
141
- // Pattern: "use strict"; "production" !== process.env.NODE_ENV && (function() { ... })();
142
- transformed = transformed.replace(
143
- /^\s*["']use strict["'];\s*["']production["']\s*!==\s*process\.env\.NODE_ENV\s*&&\s*\(function\s*\(\)\s*\{/,
144
- ""
145
- );
146
-
147
- // Remove the closing of the conditional IIFE at the end: })();
148
- transformed = transformed.replace(/\}\)\(\);?\s*$/, "");
149
-
150
- // Replace require('react') and require('react-dom') with imports
151
- // The pattern spans multiple lines with whitespace
152
- transformed = transformed.replace(
153
- /var\s+React\s*=\s*require\s*\(\s*["']react["']\s*\)\s*,[\s\n]+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
154
- 'import React from "react";\nimport ReactDOM from "react-dom";\nvar '
155
- );
156
-
157
- // Transform exports.xyz = function() to export function xyz()
158
- transformed = transformed.replace(
159
- /exports\.(\w+)\s*=\s*function\s*\(/g,
160
- "export function $1("
161
- );
162
-
163
- // Transform exports.xyz = value to export const xyz = value
164
- transformed = transformed.replace(
165
- /exports\.(\w+)\s*=/g,
166
- "export const $1 ="
167
- );
168
-
169
- // Reconstruct with license at the top
170
- transformed = license + "\n" + transformed;
171
-
172
- return {
173
- code: transformed,
174
- map: null,
175
- };
176
- }
177
-
178
- return null;
179
- },
180
- };
181
- }
182
-
183
57
  /**
184
58
  * RSC plugin entry points configuration.
185
59
  * All entries use virtual modules by default. Specify a path to use a custom entry file.
@@ -558,6 +432,10 @@ export async function rscRouter(
558
432
 
559
433
  const plugins: PluginOption[] = [];
560
434
 
435
+ // Get package resolution info (workspace vs npm install)
436
+ const rscRouterAliases = getPackageAliases();
437
+ const excludeDeps = getExcludeDeps();
438
+
561
439
  // Track RSC entry path for version injection
562
440
  let rscEntryPath: string | null = null;
563
441
 
@@ -584,29 +462,11 @@ export async function rscRouter(
584
462
  // Exclude rsc-router modules from optimization to prevent module duplication
585
463
  // This ensures the same Context instance is used by both browser entry and RSC proxy modules
586
464
  optimizeDeps: {
587
- exclude: RSC_ROUTER_EXCLUDE_DEPS,
465
+ exclude: excludeDeps,
588
466
  esbuildOptions: sharedEsbuildOptions,
589
467
  },
590
468
  resolve: {
591
- alias: {
592
- // Map rsc-router/* to @ivogt/rsc-router/* for virtual entries
593
- // This allows the package to work when published under a scoped name
594
- "rsc-router/internal/deps/browser":
595
- "@ivogt/rsc-router/internal/deps/browser",
596
- "rsc-router/internal/deps/ssr":
597
- "@ivogt/rsc-router/internal/deps/ssr",
598
- "rsc-router/internal/deps/rsc":
599
- "@ivogt/rsc-router/internal/deps/rsc",
600
- "rsc-router/internal/deps/html-stream-client":
601
- "@ivogt/rsc-router/internal/deps/html-stream-client",
602
- "rsc-router/internal/deps/html-stream-server":
603
- "@ivogt/rsc-router/internal/deps/html-stream-server",
604
- "rsc-router/browser": "@ivogt/rsc-router/browser",
605
- "rsc-router/client": "@ivogt/rsc-router/client",
606
- "rsc-router/server": "@ivogt/rsc-router/server",
607
- "rsc-router/rsc": "@ivogt/rsc-router/rsc",
608
- "rsc-router/ssr": "@ivogt/rsc-router/ssr",
609
- },
469
+ alias: rscRouterAliases,
610
470
  },
611
471
  environments: {
612
472
  client: {
@@ -621,7 +481,7 @@ export async function rscRouter(
621
481
  // Exclude rsc-router modules to ensure same Context instance
622
482
  optimizeDeps: {
623
483
  include: ["rsc-html-stream/client"],
624
- exclude: RSC_ROUTER_EXCLUDE_DEPS,
484
+ exclude: excludeDeps,
625
485
  esbuildOptions: sharedEsbuildOptions,
626
486
  },
627
487
  },
@@ -644,13 +504,15 @@ export async function rscRouter(
644
504
  "react/jsx-runtime",
645
505
  "rsc-html-stream/server",
646
506
  ],
647
- exclude: RSC_ROUTER_EXCLUDE_DEPS,
507
+ exclude: excludeDeps,
648
508
  esbuildOptions: sharedEsbuildOptions,
649
509
  },
650
510
  },
651
511
  rsc: {
652
- // RSC environment also needs esbuild options for version virtual module
512
+ // RSC environment needs exclude list and esbuild options
513
+ // Exclude rsc-router modules to prevent createContext in RSC environment
653
514
  optimizeDeps: {
515
+ exclude: excludeDeps,
654
516
  esbuildOptions: sharedEsbuildOptions,
655
517
  },
656
518
  },
@@ -716,29 +578,11 @@ export async function rscRouter(
716
578
  // Exclude rsc-router modules from optimization to prevent module duplication
717
579
  // This ensures the same Context instance is used by both browser entry and RSC proxy modules
718
580
  optimizeDeps: {
719
- exclude: RSC_ROUTER_EXCLUDE_DEPS,
581
+ exclude: excludeDeps,
720
582
  esbuildOptions: sharedEsbuildOptions,
721
583
  },
722
584
  resolve: {
723
- alias: {
724
- // Map rsc-router/* to @ivogt/rsc-router/* for virtual entries
725
- // This allows the package to work when published under a scoped name
726
- "rsc-router/internal/deps/browser":
727
- "@ivogt/rsc-router/internal/deps/browser",
728
- "rsc-router/internal/deps/ssr":
729
- "@ivogt/rsc-router/internal/deps/ssr",
730
- "rsc-router/internal/deps/rsc":
731
- "@ivogt/rsc-router/internal/deps/rsc",
732
- "rsc-router/internal/deps/html-stream-client":
733
- "@ivogt/rsc-router/internal/deps/html-stream-client",
734
- "rsc-router/internal/deps/html-stream-server":
735
- "@ivogt/rsc-router/internal/deps/html-stream-server",
736
- "rsc-router/browser": "@ivogt/rsc-router/browser",
737
- "rsc-router/client": "@ivogt/rsc-router/client",
738
- "rsc-router/server": "@ivogt/rsc-router/server",
739
- "rsc-router/rsc": "@ivogt/rsc-router/rsc",
740
- "rsc-router/ssr": "@ivogt/rsc-router/ssr",
741
- },
585
+ alias: rscRouterAliases,
742
586
  },
743
587
  environments: {
744
588
  client: {
@@ -751,7 +595,7 @@ export async function rscRouter(
751
595
  },
752
596
  // Always exclude rsc-router modules, conditionally add virtual entry
753
597
  optimizeDeps: {
754
- exclude: RSC_ROUTER_EXCLUDE_DEPS,
598
+ exclude: excludeDeps,
755
599
  esbuildOptions: sharedEsbuildOptions,
756
600
  ...(useVirtualClient && {
757
601
  // Tell Vite to scan the virtual entry for dependencies
@@ -765,7 +609,7 @@ export async function rscRouter(
765
609
  entries: [VIRTUAL_IDS.ssr],
766
610
  // Pre-bundle React for SSR to ensure single instance
767
611
  include: ["react", "react-dom/server.edge", "react/jsx-runtime"],
768
- exclude: RSC_ROUTER_EXCLUDE_DEPS,
612
+ exclude: excludeDeps,
769
613
  esbuildOptions: sharedEsbuildOptions,
770
614
  },
771
615
  },
@@ -834,10 +678,6 @@ export async function rscRouter(
834
678
  plugins.push(createVersionInjectorPlugin(rscEntryPath));
835
679
  }
836
680
 
837
- // Add CJS-to-ESM transform for @vitejs/plugin-rsc vendor files
838
- // This must be added to transform the CommonJS client.browser.js to ESM
839
- plugins.push(createCjsToEsmPlugin());
840
-
841
681
  return plugins;
842
682
  }
843
683
 
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Package Resolution Utilities
3
+ *
4
+ * Handles detection of workspace vs npm install context and generates
5
+ * appropriate aliases and exclude lists for Vite configuration.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { resolve, dirname } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ // Get the directory of this file to find package.json
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+
15
+ /**
16
+ * Read the package name from package.json
17
+ * This allows the name to change without updating hardcoded strings
18
+ */
19
+ function getPackageName(): string {
20
+ try {
21
+ // Navigate from src/vite/ to package root
22
+ const packageJsonPath = resolve(__dirname, "../../package.json");
23
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
24
+ return packageJson.name;
25
+ } catch {
26
+ // Fallback to known name if package.json read fails
27
+ return "@ivogt/rsc-router";
28
+ }
29
+ }
30
+
31
+ /**
32
+ * The canonical name used in virtual entries (without scope)
33
+ */
34
+ const VIRTUAL_PACKAGE_NAME = "rsc-router";
35
+
36
+ /**
37
+ * Cached package name from package.json
38
+ */
39
+ let _packageName: string | null = null;
40
+
41
+ /**
42
+ * Get the published package name (e.g., "@ivogt/rsc-router")
43
+ */
44
+ export function getPublishedPackageName(): string {
45
+ if (_packageName === null) {
46
+ _packageName = getPackageName();
47
+ }
48
+ return _packageName;
49
+ }
50
+
51
+ /**
52
+ * Check if the package is installed from npm (scoped) vs workspace (unscoped)
53
+ *
54
+ * In workspace development:
55
+ * - Package is installed as "rsc-router" via pnpm workspace alias
56
+ * - The scoped name (@ivogt/rsc-router) doesn't exist in node_modules
57
+ *
58
+ * When installed from npm:
59
+ * - Package is installed as "@ivogt/rsc-router"
60
+ * - We need aliases to map "rsc-router/*" to "@ivogt/rsc-router/*"
61
+ */
62
+ export function isInstalledFromNpm(): boolean {
63
+ const packageName = getPublishedPackageName();
64
+ // Check if the scoped package exists in node_modules
65
+ return existsSync(resolve(process.cwd(), "node_modules", packageName));
66
+ }
67
+
68
+ /**
69
+ * Check if we're in a monorepo/workspace development context
70
+ */
71
+ export function isWorkspaceDevelopment(): boolean {
72
+ return !isInstalledFromNpm();
73
+ }
74
+
75
+ /**
76
+ * Subpaths that need to be excluded from Vite's dependency optimization
77
+ * and potentially aliased
78
+ */
79
+ const PACKAGE_SUBPATHS = [
80
+ "",
81
+ "/browser",
82
+ "/client",
83
+ "/server",
84
+ "/rsc",
85
+ "/ssr",
86
+ "/internal/deps/browser",
87
+ "/internal/deps/html-stream-client",
88
+ "/internal/deps/ssr",
89
+ "/internal/deps/rsc",
90
+ ] as const;
91
+
92
+ /**
93
+ * Generate the list of modules to exclude from Vite's dependency optimization.
94
+ *
95
+ * We include both the published name and the virtual name because
96
+ * Vite's optimizer runs before alias resolution.
97
+ */
98
+ export function getExcludeDeps(): string[] {
99
+ const packageName = getPublishedPackageName();
100
+ const excludes: string[] = [];
101
+
102
+ for (const subpath of PACKAGE_SUBPATHS) {
103
+ // Add scoped package paths
104
+ excludes.push(`${packageName}${subpath}`);
105
+ // Add virtual/aliased paths (before alias resolution)
106
+ if (packageName !== VIRTUAL_PACKAGE_NAME) {
107
+ excludes.push(`${VIRTUAL_PACKAGE_NAME}${subpath}`);
108
+ }
109
+ }
110
+
111
+ return excludes;
112
+ }
113
+
114
+ /**
115
+ * Subpaths that need aliasing (subset of PACKAGE_SUBPATHS)
116
+ */
117
+ const ALIAS_SUBPATHS = [
118
+ "/internal/deps/browser",
119
+ "/internal/deps/ssr",
120
+ "/internal/deps/rsc",
121
+ "/internal/deps/html-stream-client",
122
+ "/internal/deps/html-stream-server",
123
+ "/browser",
124
+ "/client",
125
+ "/server",
126
+ "/rsc",
127
+ "/ssr",
128
+ ] as const;
129
+
130
+ /**
131
+ * Generate aliases to map virtual package paths to the actual published package.
132
+ *
133
+ * Only needed when installed from npm, where the package is under @ivogt/rsc-router
134
+ * but virtual entries import from rsc-router/*.
135
+ *
136
+ * Returns empty object in workspace development where rsc-router resolves directly.
137
+ */
138
+ export function getPackageAliases(): Record<string, string> {
139
+ if (isWorkspaceDevelopment()) {
140
+ // No aliases needed - rsc-router resolves directly
141
+ return {};
142
+ }
143
+
144
+ const packageName = getPublishedPackageName();
145
+ const aliases: Record<string, string> = {};
146
+
147
+ for (const subpath of ALIAS_SUBPATHS) {
148
+ aliases[`${VIRTUAL_PACKAGE_NAME}${subpath}`] = `${packageName}${subpath}`;
149
+ }
150
+
151
+ return aliases;
152
+ }