@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 +1 -1
- package/src/router.ts +42 -0
- package/src/rsc/handler.ts +69 -5
- package/src/vite/index.ts +21 -181
- package/src/vite/package-resolution.ts +152 -0
package/package.json
CHANGED
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
|
|
package/src/rsc/handler.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
302
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
507
|
+
exclude: excludeDeps,
|
|
648
508
|
esbuildOptions: sharedEsbuildOptions,
|
|
649
509
|
},
|
|
650
510
|
},
|
|
651
511
|
rsc: {
|
|
652
|
-
// RSC environment
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
}
|