@ivogt/rsc-router 0.0.0-experimental.1 → 0.0.0-experimental.11
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/expose-action-id.d.ts +18 -0
- package/dist/expose-handle-id.d.ts +19 -0
- package/dist/expose-loader-id.d.ts +16 -0
- package/dist/expose-location-state-id.d.ts +19 -0
- package/dist/index.d.ts +123 -0
- package/dist/package-resolution.d.ts +42 -0
- package/dist/virtual-entries.d.ts +24 -0
- package/dist/vite/index.js +1286 -0
- package/package.json +5 -2
- package/src/router.ts +42 -0
- package/src/rsc/handler.ts +69 -5
- package/src/server/root-layout.tsx +1 -1
- package/src/vite/index.ts +166 -4
- package/src/vite/package-resolution.ts +125 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ivogt/rsc-router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Type-safe RSC router with partial rendering support",
|
|
6
6
|
"author": "Ivo Todorov",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"./vite": {
|
|
57
57
|
"types": "./src/vite/index.ts",
|
|
58
|
-
"import": "./
|
|
58
|
+
"import": "./dist/vite/index.js"
|
|
59
59
|
},
|
|
60
60
|
"./types": {
|
|
61
61
|
"types": "./src/vite/version.d.ts"
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
},
|
|
90
90
|
"files": [
|
|
91
91
|
"src",
|
|
92
|
+
"dist",
|
|
92
93
|
"README.md"
|
|
93
94
|
],
|
|
94
95
|
"peerDependencies": {
|
|
@@ -117,11 +118,13 @@
|
|
|
117
118
|
"@types/react-dom": "^19.2.3",
|
|
118
119
|
"react": "^19.2.1",
|
|
119
120
|
"react-dom": "^19.2.1",
|
|
121
|
+
"esbuild": "^0.27.0",
|
|
120
122
|
"tinyexec": "^0.3.2",
|
|
121
123
|
"typescript": "^5.3.0",
|
|
122
124
|
"vitest": "^2.1.8"
|
|
123
125
|
},
|
|
124
126
|
"scripts": {
|
|
127
|
+
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external",
|
|
125
128
|
"typecheck": "tsc --noEmit",
|
|
126
129
|
"test": "playwright test",
|
|
127
130
|
"test:ui": "playwright test --ui",
|
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,6 +27,33 @@ export { exposeLocationStateId } from "./expose-location-state-id.ts";
|
|
|
21
27
|
|
|
22
28
|
// Virtual module type declarations in ./version.d.ts
|
|
23
29
|
|
|
30
|
+
/**
|
|
31
|
+
* esbuild plugin to provide rsc-router:version virtual module during optimization.
|
|
32
|
+
* This is needed because esbuild runs during Vite's dependency optimization phase,
|
|
33
|
+
* before Vite's plugin system can handle virtual modules.
|
|
34
|
+
*/
|
|
35
|
+
const versionEsbuildPlugin = {
|
|
36
|
+
name: "rsc-router-version",
|
|
37
|
+
setup(build: any) {
|
|
38
|
+
build.onResolve({ filter: /^rsc-router:version$/ }, (args: any) => ({
|
|
39
|
+
path: args.path,
|
|
40
|
+
namespace: "rsc-router-virtual",
|
|
41
|
+
}));
|
|
42
|
+
build.onLoad({ filter: /.*/, namespace: "rsc-router-virtual" }, () => ({
|
|
43
|
+
contents: `export const VERSION = "dev";`,
|
|
44
|
+
loader: "js",
|
|
45
|
+
}));
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Shared esbuild options for dependency optimization.
|
|
51
|
+
* Includes the version stub plugin for all environments.
|
|
52
|
+
*/
|
|
53
|
+
const sharedEsbuildOptions = {
|
|
54
|
+
plugins: [versionEsbuildPlugin],
|
|
55
|
+
};
|
|
56
|
+
|
|
24
57
|
/**
|
|
25
58
|
* RSC plugin entry points configuration.
|
|
26
59
|
* All entries use virtual modules by default. Specify a path to use a custom entry file.
|
|
@@ -399,6 +432,10 @@ export async function rscRouter(
|
|
|
399
432
|
|
|
400
433
|
const plugins: PluginOption[] = [];
|
|
401
434
|
|
|
435
|
+
// Get package resolution info (workspace vs npm install)
|
|
436
|
+
const rscRouterAliases = getPackageAliases();
|
|
437
|
+
const excludeDeps = getExcludeDeps();
|
|
438
|
+
|
|
402
439
|
// Track RSC entry path for version injection
|
|
403
440
|
let rscEntryPath: string | null = null;
|
|
404
441
|
|
|
@@ -422,6 +459,15 @@ export async function rscRouter(
|
|
|
422
459
|
config() {
|
|
423
460
|
// Configure environments for cloudflare deployment
|
|
424
461
|
return {
|
|
462
|
+
// Exclude rsc-router modules from optimization to prevent module duplication
|
|
463
|
+
// This ensures the same Context instance is used by both browser entry and RSC proxy modules
|
|
464
|
+
optimizeDeps: {
|
|
465
|
+
exclude: excludeDeps,
|
|
466
|
+
esbuildOptions: sharedEsbuildOptions,
|
|
467
|
+
},
|
|
468
|
+
resolve: {
|
|
469
|
+
alias: rscRouterAliases,
|
|
470
|
+
},
|
|
425
471
|
environments: {
|
|
426
472
|
client: {
|
|
427
473
|
build: {
|
|
@@ -432,8 +478,11 @@ export async function rscRouter(
|
|
|
432
478
|
},
|
|
433
479
|
},
|
|
434
480
|
// Pre-bundle rsc-html-stream to prevent discovery during first request
|
|
481
|
+
// Exclude rsc-router modules to ensure same Context instance
|
|
435
482
|
optimizeDeps: {
|
|
436
483
|
include: ["rsc-html-stream/client"],
|
|
484
|
+
exclude: excludeDeps,
|
|
485
|
+
esbuildOptions: sharedEsbuildOptions,
|
|
437
486
|
},
|
|
438
487
|
},
|
|
439
488
|
ssr: {
|
|
@@ -446,6 +495,7 @@ export async function rscRouter(
|
|
|
446
495
|
dedupe: ["react", "react-dom"],
|
|
447
496
|
},
|
|
448
497
|
// Pre-bundle SSR entry and React for proper module linking with childEnvironments
|
|
498
|
+
// Exclude rsc-router modules to ensure same Context instance
|
|
449
499
|
optimizeDeps: {
|
|
450
500
|
entries: [finalEntries.ssr],
|
|
451
501
|
include: [
|
|
@@ -454,6 +504,16 @@ export async function rscRouter(
|
|
|
454
504
|
"react/jsx-runtime",
|
|
455
505
|
"rsc-html-stream/server",
|
|
456
506
|
],
|
|
507
|
+
exclude: excludeDeps,
|
|
508
|
+
esbuildOptions: sharedEsbuildOptions,
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
rsc: {
|
|
512
|
+
// RSC environment needs exclude list and esbuild options
|
|
513
|
+
// Exclude rsc-router modules to prevent createContext in RSC environment
|
|
514
|
+
optimizeDeps: {
|
|
515
|
+
exclude: excludeDeps,
|
|
516
|
+
esbuildOptions: sharedEsbuildOptions,
|
|
457
517
|
},
|
|
458
518
|
},
|
|
459
519
|
},
|
|
@@ -515,6 +575,15 @@ export async function rscRouter(
|
|
|
515
575
|
const useVirtualRSC = finalEntries.rsc === VIRTUAL_IDS.rsc;
|
|
516
576
|
|
|
517
577
|
return {
|
|
578
|
+
// Exclude rsc-router modules from optimization to prevent module duplication
|
|
579
|
+
// This ensures the same Context instance is used by both browser entry and RSC proxy modules
|
|
580
|
+
optimizeDeps: {
|
|
581
|
+
exclude: excludeDeps,
|
|
582
|
+
esbuildOptions: sharedEsbuildOptions,
|
|
583
|
+
},
|
|
584
|
+
resolve: {
|
|
585
|
+
alias: rscRouterAliases,
|
|
586
|
+
},
|
|
518
587
|
environments: {
|
|
519
588
|
client: {
|
|
520
589
|
build: {
|
|
@@ -524,12 +593,15 @@ export async function rscRouter(
|
|
|
524
593
|
},
|
|
525
594
|
},
|
|
526
595
|
},
|
|
527
|
-
|
|
528
|
-
|
|
596
|
+
// Always exclude rsc-router modules, conditionally add virtual entry
|
|
597
|
+
optimizeDeps: {
|
|
598
|
+
exclude: excludeDeps,
|
|
599
|
+
esbuildOptions: sharedEsbuildOptions,
|
|
600
|
+
...(useVirtualClient && {
|
|
529
601
|
// Tell Vite to scan the virtual entry for dependencies
|
|
530
602
|
entries: [VIRTUAL_IDS.browser],
|
|
531
|
-
},
|
|
532
|
-
}
|
|
603
|
+
}),
|
|
604
|
+
},
|
|
533
605
|
},
|
|
534
606
|
...(useVirtualSSR && {
|
|
535
607
|
ssr: {
|
|
@@ -537,6 +609,8 @@ export async function rscRouter(
|
|
|
537
609
|
entries: [VIRTUAL_IDS.ssr],
|
|
538
610
|
// Pre-bundle React for SSR to ensure single instance
|
|
539
611
|
include: ["react", "react-dom/server.edge", "react/jsx-runtime"],
|
|
612
|
+
exclude: excludeDeps,
|
|
613
|
+
esbuildOptions: sharedEsbuildOptions,
|
|
540
614
|
},
|
|
541
615
|
},
|
|
542
616
|
}),
|
|
@@ -546,6 +620,7 @@ export async function rscRouter(
|
|
|
546
620
|
entries: [VIRTUAL_IDS.rsc],
|
|
547
621
|
// Pre-bundle React for RSC to ensure single instance
|
|
548
622
|
include: ["react", "react/jsx-runtime"],
|
|
623
|
+
esbuildOptions: sharedEsbuildOptions,
|
|
549
624
|
},
|
|
550
625
|
},
|
|
551
626
|
}),
|
|
@@ -603,6 +678,93 @@ export async function rscRouter(
|
|
|
603
678
|
plugins.push(createVersionInjectorPlugin(rscEntryPath));
|
|
604
679
|
}
|
|
605
680
|
|
|
681
|
+
// Transform CJS vendor files to ESM for browser compatibility
|
|
682
|
+
// optimizeDeps.include doesn't work because the file is loaded after initial optimization
|
|
683
|
+
plugins.push(createCjsToEsmPlugin());
|
|
684
|
+
|
|
606
685
|
return plugins;
|
|
607
686
|
}
|
|
608
687
|
|
|
688
|
+
/**
|
|
689
|
+
* Transform CJS vendor files from @vitejs/plugin-rsc to ESM for browser compatibility.
|
|
690
|
+
* The react-server-dom vendor files are shipped as CJS which doesn't work in browsers.
|
|
691
|
+
*/
|
|
692
|
+
function createCjsToEsmPlugin(): Plugin {
|
|
693
|
+
return {
|
|
694
|
+
name: "rsc-router:cjs-to-esm",
|
|
695
|
+
enforce: "pre",
|
|
696
|
+
transform(code, id) {
|
|
697
|
+
const cleanId = id.split("?")[0];
|
|
698
|
+
|
|
699
|
+
// Transform the client.browser.js entry point to re-export from CJS
|
|
700
|
+
if (
|
|
701
|
+
cleanId.includes("vendor/react-server-dom/client.browser.js") ||
|
|
702
|
+
cleanId.includes("vendor\\react-server-dom\\client.browser.js")
|
|
703
|
+
) {
|
|
704
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
705
|
+
const cjsFile = isProd
|
|
706
|
+
? "./cjs/react-server-dom-webpack-client.browser.production.js"
|
|
707
|
+
: "./cjs/react-server-dom-webpack-client.browser.development.js";
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
code: `export * from "${cjsFile}";`,
|
|
711
|
+
map: null,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Transform the actual CJS files to ESM
|
|
716
|
+
if (
|
|
717
|
+
(cleanId.includes("vendor/react-server-dom/cjs/") ||
|
|
718
|
+
cleanId.includes("vendor\\react-server-dom\\cjs\\")) &&
|
|
719
|
+
cleanId.includes("client.browser")
|
|
720
|
+
) {
|
|
721
|
+
let transformed = code;
|
|
722
|
+
|
|
723
|
+
// Extract the license comment to preserve it
|
|
724
|
+
const licenseMatch = transformed.match(/^\/\*\*[\s\S]*?\*\//);
|
|
725
|
+
const license = licenseMatch ? licenseMatch[0] : "";
|
|
726
|
+
if (license) {
|
|
727
|
+
transformed = transformed.slice(license.length);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Remove "use strict" and the conditional IIFE wrapper
|
|
731
|
+
transformed = transformed.replace(
|
|
732
|
+
/^\s*["']use strict["'];\s*["']production["']\s*!==\s*process\.env\.NODE_ENV\s*&&\s*\(function\s*\(\)\s*\{/,
|
|
733
|
+
""
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
// Remove the closing of the conditional IIFE at the end
|
|
737
|
+
transformed = transformed.replace(/\}\)\(\);?\s*$/, "");
|
|
738
|
+
|
|
739
|
+
// Replace require('react') and require('react-dom') with imports
|
|
740
|
+
transformed = transformed.replace(
|
|
741
|
+
/var\s+React\s*=\s*require\s*\(\s*["']react["']\s*\)\s*,[\s\n]+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
|
|
742
|
+
'import React from "react";\nimport ReactDOM from "react-dom";\nvar '
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
// Transform exports.xyz = function() to export function xyz()
|
|
746
|
+
transformed = transformed.replace(
|
|
747
|
+
/exports\.(\w+)\s*=\s*function\s*\(/g,
|
|
748
|
+
"export function $1("
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
// Transform exports.xyz = value to export const xyz = value
|
|
752
|
+
transformed = transformed.replace(
|
|
753
|
+
/exports\.(\w+)\s*=/g,
|
|
754
|
+
"export const $1 ="
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
// Reconstruct with license at the top
|
|
758
|
+
transformed = license + "\n" + transformed;
|
|
759
|
+
|
|
760
|
+
return {
|
|
761
|
+
code: transformed,
|
|
762
|
+
map: null,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return null;
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
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 } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The canonical name used in virtual entries (without scope)
|
|
14
|
+
*/
|
|
15
|
+
const VIRTUAL_PACKAGE_NAME = "rsc-router";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the published package name (e.g., "@ivogt/rsc-router")
|
|
19
|
+
*/
|
|
20
|
+
export function getPublishedPackageName(): string {
|
|
21
|
+
return packageJson.name;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if the package is installed from npm (scoped) vs workspace (unscoped)
|
|
26
|
+
*
|
|
27
|
+
* In workspace development:
|
|
28
|
+
* - Package is installed as "rsc-router" via pnpm workspace alias
|
|
29
|
+
* - The scoped name (@ivogt/rsc-router) doesn't exist in node_modules
|
|
30
|
+
*
|
|
31
|
+
* When installed from npm:
|
|
32
|
+
* - Package is installed as "@ivogt/rsc-router"
|
|
33
|
+
* - We need aliases to map "rsc-router/*" to "@ivogt/rsc-router/*"
|
|
34
|
+
*/
|
|
35
|
+
export function isInstalledFromNpm(): boolean {
|
|
36
|
+
const packageName = getPublishedPackageName();
|
|
37
|
+
// Check if the scoped package exists in node_modules
|
|
38
|
+
return existsSync(resolve(process.cwd(), "node_modules", packageName));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if we're in a monorepo/workspace development context
|
|
43
|
+
*/
|
|
44
|
+
export function isWorkspaceDevelopment(): boolean {
|
|
45
|
+
return !isInstalledFromNpm();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Subpaths that need to be excluded from Vite's dependency optimization
|
|
50
|
+
* and potentially aliased
|
|
51
|
+
*/
|
|
52
|
+
const PACKAGE_SUBPATHS = [
|
|
53
|
+
"",
|
|
54
|
+
"/browser",
|
|
55
|
+
"/client",
|
|
56
|
+
"/server",
|
|
57
|
+
"/rsc",
|
|
58
|
+
"/ssr",
|
|
59
|
+
"/internal/deps/browser",
|
|
60
|
+
"/internal/deps/html-stream-client",
|
|
61
|
+
"/internal/deps/ssr",
|
|
62
|
+
"/internal/deps/rsc",
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate the list of modules to exclude from Vite's dependency optimization.
|
|
67
|
+
*
|
|
68
|
+
* We include both the published name and the virtual name because
|
|
69
|
+
* Vite's optimizer runs before alias resolution.
|
|
70
|
+
*/
|
|
71
|
+
export function getExcludeDeps(): string[] {
|
|
72
|
+
const packageName = getPublishedPackageName();
|
|
73
|
+
const excludes: string[] = [];
|
|
74
|
+
|
|
75
|
+
for (const subpath of PACKAGE_SUBPATHS) {
|
|
76
|
+
// Add scoped package paths
|
|
77
|
+
excludes.push(`${packageName}${subpath}`);
|
|
78
|
+
// Add virtual/aliased paths (before alias resolution)
|
|
79
|
+
if (packageName !== VIRTUAL_PACKAGE_NAME) {
|
|
80
|
+
excludes.push(`${VIRTUAL_PACKAGE_NAME}${subpath}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return excludes;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Subpaths that need aliasing (subset of PACKAGE_SUBPATHS)
|
|
89
|
+
*/
|
|
90
|
+
const ALIAS_SUBPATHS = [
|
|
91
|
+
"/internal/deps/browser",
|
|
92
|
+
"/internal/deps/ssr",
|
|
93
|
+
"/internal/deps/rsc",
|
|
94
|
+
"/internal/deps/html-stream-client",
|
|
95
|
+
"/internal/deps/html-stream-server",
|
|
96
|
+
"/browser",
|
|
97
|
+
"/client",
|
|
98
|
+
"/server",
|
|
99
|
+
"/rsc",
|
|
100
|
+
"/ssr",
|
|
101
|
+
] as const;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Generate aliases to map virtual package paths to the actual published package.
|
|
105
|
+
*
|
|
106
|
+
* Only needed when installed from npm, where the package is under @ivogt/rsc-router
|
|
107
|
+
* but virtual entries import from rsc-router/*.
|
|
108
|
+
*
|
|
109
|
+
* Returns empty object in workspace development where rsc-router resolves directly.
|
|
110
|
+
*/
|
|
111
|
+
export function getPackageAliases(): Record<string, string> {
|
|
112
|
+
if (isWorkspaceDevelopment()) {
|
|
113
|
+
// No aliases needed - rsc-router resolves directly
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const packageName = getPublishedPackageName();
|
|
118
|
+
const aliases: Record<string, string> = {};
|
|
119
|
+
|
|
120
|
+
for (const subpath of ALIAS_SUBPATHS) {
|
|
121
|
+
aliases[`${VIRTUAL_PACKAGE_NAME}${subpath}`] = `${packageName}${subpath}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return aliases;
|
|
125
|
+
}
|