@reckona/mreact-router 0.0.100 → 0.0.102

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": "@reckona/mreact-router",
3
- "version": "0.0.100",
3
+ "version": "0.0.102",
4
4
  "description": "File-system app router, SSR, actions, and deployment adapters for mreact.",
5
5
  "keywords": [
6
6
  "jsx",
@@ -105,20 +105,20 @@
105
105
  },
106
106
  "dependencies": {
107
107
  "typescript": "^6.0.3",
108
- "@reckona/mreact": "0.0.100",
109
- "@reckona/mreact-compat": "0.0.100",
110
- "@reckona/mreact-devtools": "0.0.100",
111
- "@reckona/mreact-query": "0.0.100",
112
- "@reckona/mreact-compiler": "0.0.100",
113
- "@reckona/mreact-reactive-core": "0.0.100",
114
- "@reckona/mreact-reactive-dom": "0.0.100",
115
- "@reckona/mreact-server": "0.0.100",
116
- "@reckona/mreact-shared": "0.0.100"
108
+ "@reckona/mreact": "0.0.102",
109
+ "@reckona/mreact-compat": "0.0.102",
110
+ "@reckona/mreact-compiler": "0.0.102",
111
+ "@reckona/mreact-query": "0.0.102",
112
+ "@reckona/mreact-devtools": "0.0.102",
113
+ "@reckona/mreact-reactive-core": "0.0.102",
114
+ "@reckona/mreact-reactive-dom": "0.0.102",
115
+ "@reckona/mreact-server": "0.0.102",
116
+ "@reckona/mreact-shared": "0.0.102"
117
117
  },
118
118
  "peerDependencies": {
119
119
  "vite": ">=8 <9"
120
120
  },
121
121
  "optionalDependencies": {
122
- "@reckona/mreact-router-native": "0.0.100"
122
+ "@reckona/mreact-router-native": "0.0.102"
123
123
  }
124
124
  }
package/src/build.ts CHANGED
@@ -2380,6 +2380,17 @@ interface CloudflareBatchedRouteModules {
2380
2380
  entries: ReadonlyMap<string, CloudflareBatchedRouteModule>;
2381
2381
  }
2382
2382
 
2383
+ interface CloudflareBatchedComponentRoute {
2384
+ filename: string;
2385
+ routeId: string;
2386
+ }
2387
+
2388
+ interface CloudflareBatchedStringRoute {
2389
+ filename: string;
2390
+ routeId: string;
2391
+ shellFiles: CloudflareShellFile[];
2392
+ }
2393
+
2383
2394
  interface RouteRequestModuleBatchEntry {
2384
2395
  code: string;
2385
2396
  filename: string;
@@ -2437,10 +2448,44 @@ async function writeCloudflareRouteModules(options: {
2437
2448
  root: options.projectRoot,
2438
2449
  vitePlugins: options.vitePlugins,
2439
2450
  });
2451
+ const directComponentRoutes = await collectCloudflareDirectComponentRoutes({
2452
+ requiredRoutes,
2453
+ routesDir: options.routesDir,
2454
+ serverModules: options.serverModules,
2455
+ sourceAnalysis: options.sourceAnalysis,
2456
+ });
2457
+ const stringShellComponentRoutes = await collectCloudflareStringShellComponentRoutes({
2458
+ requiredRoutes,
2459
+ routesDir: options.routesDir,
2460
+ serverModules: options.serverModules,
2461
+ sourceAnalysis: options.sourceAnalysis,
2462
+ });
2463
+ const directComponentRouteIds = new Set(directComponentRoutes.map((route) => route.routeId));
2464
+ const stringShellComponentRouteIds = new Set(
2465
+ stringShellComponentRoutes.map((route) => route.routeId),
2466
+ );
2467
+ const directComponentModules = await buildCloudflareServerComponentModuleBatch({
2468
+ cacheDir: options.cacheDir,
2469
+ projectRoot: options.projectRoot,
2470
+ routes: directComponentRoutes,
2471
+ serverModules: options.serverModules,
2472
+ sourceAnalysis: options.sourceAnalysis,
2473
+ vitePlugins: options.vitePlugins,
2474
+ });
2475
+ const stringShellComponentModules = await buildCloudflareStringRouteComponentModuleBatch({
2476
+ cacheDir: options.cacheDir,
2477
+ projectRoot: options.projectRoot,
2478
+ routes: stringShellComponentRoutes,
2479
+ serverModules: options.serverModules,
2480
+ sourceAnalysis: options.sourceAnalysis,
2481
+ vitePlugins: options.vitePlugins,
2482
+ });
2440
2483
 
2441
2484
  await Promise.all([
2442
2485
  writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, serverRouteModules),
2443
2486
  writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, loaderRouteModules),
2487
+ writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, directComponentModules),
2488
+ writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, stringShellComponentModules),
2444
2489
  ]);
2445
2490
 
2446
2491
  const registryEntries = await mapWithBuildConcurrency(requiredRoutes, async ({ route, routeFile, routeId }) => {
@@ -2480,29 +2525,43 @@ async function writeCloudflareRouteModules(options: {
2480
2525
  : "string";
2481
2526
 
2482
2527
  try {
2483
- const componentOutput =
2484
- serverOutput === "stream"
2485
- ? await buildCloudflareStreamRouteComponentModule({
2486
- cacheDir: options.cacheDir,
2487
- filename: route.file,
2488
- projectRoot: options.projectRoot,
2489
- routesDir: options.routesDir,
2490
- serverModules: options.serverModules,
2491
- sourceAnalysis: options.sourceAnalysis,
2492
- vitePlugins: options.vitePlugins,
2493
- })
2494
- : await buildCloudflareStringRouteComponentModule({
2495
- cacheDir: options.cacheDir,
2496
- filename: route.file,
2497
- projectRoot: options.projectRoot,
2498
- routesDir: options.routesDir,
2499
- serverModules: options.serverModules,
2500
- sourceAnalysis: options.sourceAnalysis,
2501
- vitePlugins: options.vitePlugins,
2502
- });
2503
- const componentFile = `routes/${routeId}.${hashText(componentOutput).slice(0, 8)}.component.mjs`;
2528
+ const batchedComponent =
2529
+ stringShellComponentRouteIds.has(routeId)
2530
+ ? stringShellComponentModules.entries.get(routeId)
2531
+ : directComponentRouteIds.has(routeId)
2532
+ ? directComponentModules.entries.get(routeId)
2533
+ : undefined;
2534
+ let componentFile = batchedComponent?.fileName;
2535
+
2536
+ if (batchedComponent === undefined) {
2537
+ const componentOutput =
2538
+ serverOutput === "stream"
2539
+ ? await buildCloudflareStreamRouteComponentModule({
2540
+ cacheDir: options.cacheDir,
2541
+ filename: route.file,
2542
+ projectRoot: options.projectRoot,
2543
+ routesDir: options.routesDir,
2544
+ serverModules: options.serverModules,
2545
+ sourceAnalysis: options.sourceAnalysis,
2546
+ vitePlugins: options.vitePlugins,
2547
+ })
2548
+ : await buildCloudflareStringRouteComponentModule({
2549
+ cacheDir: options.cacheDir,
2550
+ filename: route.file,
2551
+ projectRoot: options.projectRoot,
2552
+ routesDir: options.routesDir,
2553
+ serverModules: options.serverModules,
2554
+ sourceAnalysis: options.sourceAnalysis,
2555
+ vitePlugins: options.vitePlugins,
2556
+ });
2557
+
2558
+ componentFile = `routes/${routeId}.${hashText(componentOutput).slice(0, 8)}.component.mjs`;
2559
+ await writeFile(join(options.cloudflareDir, componentFile), componentOutput);
2560
+ }
2504
2561
 
2505
- await writeFile(join(options.cloudflareDir, componentFile), componentOutput);
2562
+ if (componentFile === undefined) {
2563
+ throw new Error("Missing bundled Cloudflare component module.");
2564
+ }
2506
2565
 
2507
2566
  const componentImport = `./${componentFile.split("/").pop() ?? componentFile}`;
2508
2567
  routeModuleExports = [
@@ -2549,12 +2608,389 @@ async function writeCloudflareRouteModules(options: {
2549
2608
  return { registryFile: "route-modules.mjs" };
2550
2609
  }
2551
2610
 
2611
+ async function collectCloudflareDirectComponentRoutes(options: {
2612
+ requiredRoutes: readonly CloudflareRequiredRoute[];
2613
+ routesDir: string;
2614
+ serverModules: Record<string, BuiltServerModuleArtifact>;
2615
+ sourceAnalysis: BuildSourceAnalysisScope;
2616
+ }): Promise<CloudflareBatchedComponentRoute[]> {
2617
+ const routes = await Promise.all(
2618
+ options.requiredRoutes.map(async ({ route, routeFile, routeId }) => {
2619
+ if (route.kind !== "page") {
2620
+ return undefined;
2621
+ }
2622
+
2623
+ const serverOutput =
2624
+ options.serverModules[routeFile]?.analysis?.streamRoute === true ||
2625
+ options.sourceAnalysis.byRouteFile.get(routeFile)?.streamRoute === true
2626
+ ? "stream"
2627
+ : "string";
2628
+
2629
+ if (serverOutput !== "string") {
2630
+ return undefined;
2631
+ }
2632
+
2633
+ const shellFiles = await cloudflareShellFilesForPage(options.routesDir, route.file);
2634
+
2635
+ if (shellFiles.length > 0) {
2636
+ return undefined;
2637
+ }
2638
+
2639
+ return {
2640
+ filename: route.file,
2641
+ routeId,
2642
+ };
2643
+ }),
2644
+ );
2645
+
2646
+ return routes.filter((route): route is CloudflareBatchedComponentRoute => route !== undefined);
2647
+ }
2648
+
2649
+ async function collectCloudflareStringShellComponentRoutes(options: {
2650
+ requiredRoutes: readonly CloudflareRequiredRoute[];
2651
+ routesDir: string;
2652
+ serverModules: Record<string, BuiltServerModuleArtifact>;
2653
+ sourceAnalysis: BuildSourceAnalysisScope;
2654
+ }): Promise<CloudflareBatchedStringRoute[]> {
2655
+ const routes = await Promise.all(
2656
+ options.requiredRoutes.map(async ({ route, routeFile, routeId }) => {
2657
+ if (route.kind !== "page") {
2658
+ return undefined;
2659
+ }
2660
+
2661
+ const serverOutput =
2662
+ options.serverModules[routeFile]?.analysis?.streamRoute === true ||
2663
+ options.sourceAnalysis.byRouteFile.get(routeFile)?.streamRoute === true
2664
+ ? "stream"
2665
+ : "string";
2666
+
2667
+ if (serverOutput !== "string") {
2668
+ return undefined;
2669
+ }
2670
+
2671
+ const shellFiles = await cloudflareShellFilesForPage(options.routesDir, route.file);
2672
+
2673
+ if (shellFiles.length === 0) {
2674
+ return undefined;
2675
+ }
2676
+
2677
+ return {
2678
+ filename: route.file,
2679
+ routeId,
2680
+ shellFiles,
2681
+ };
2682
+ }),
2683
+ );
2684
+
2685
+ return routes.filter((route): route is CloudflareBatchedStringRoute => route !== undefined);
2686
+ }
2687
+
2552
2688
  interface CloudflareShellFile {
2553
2689
  file: string;
2554
2690
  id: string;
2555
2691
  kind: "layout" | "template";
2556
2692
  }
2557
2693
 
2694
+ async function buildCloudflareServerComponentModuleBatch(options: {
2695
+ cacheDir?: string | undefined;
2696
+ projectRoot: string;
2697
+ routes: readonly CloudflareBatchedComponentRoute[];
2698
+ serverModules: Record<string, BuiltServerModuleArtifact>;
2699
+ sourceAnalysis: BuildSourceAnalysisScope;
2700
+ vitePlugins?: readonly PluginOption[] | undefined;
2701
+ }): Promise<CloudflareBatchedRouteModules> {
2702
+ if (options.routes.length === 0) {
2703
+ return { chunks: [], entries: new Map() };
2704
+ }
2705
+
2706
+ const virtualModules = new Map<string, CloudflareVirtualModule>();
2707
+ const bundleEntries = await Promise.all(
2708
+ options.routes.map(async (route) => {
2709
+ const metadataModule = await buildCloudflareRouteMetadataExportModule({
2710
+ cacheDir: options.cacheDir,
2711
+ filename: route.filename,
2712
+ hasMetadata: buildSourceAnalysisForFile(
2713
+ options.sourceAnalysis,
2714
+ options.projectRoot,
2715
+ route.filename,
2716
+ )?.hasMetadata,
2717
+ root: options.projectRoot,
2718
+ vitePlugins: options.vitePlugins,
2719
+ });
2720
+ const metadataId = `mreact:metadata:${route.routeId}`;
2721
+
2722
+ if (metadataModule !== undefined) {
2723
+ virtualModules.set(metadataId, {
2724
+ contents: metadataModule,
2725
+ resolveDir: dirname(route.filename),
2726
+ });
2727
+ }
2728
+
2729
+ return {
2730
+ code: cloudflareServerComponentModuleEntry(
2731
+ route.filename,
2732
+ metadataModule === undefined ? undefined : metadataId,
2733
+ ),
2734
+ filename: `${route.filename}.mreact-cloudflare-component.js`,
2735
+ name: `${route.routeId}.component`,
2736
+ routeId: route.routeId,
2737
+ };
2738
+ }),
2739
+ );
2740
+ const output = await bundleRouterModules({
2741
+ cacheDir: options.cacheDir,
2742
+ chunkFileNames: "routes/chunks/[name].[hash].mjs",
2743
+ entries: bundleEntries,
2744
+ entryFileNames: "routes/[name].[hash].mjs",
2745
+ minify: true,
2746
+ platform: "node",
2747
+ plugins: [
2748
+ cloudflareVirtualModulesPlugin(virtualModules),
2749
+ cloudflareServerSourceTransformPlugin({
2750
+ projectRoot: options.projectRoot,
2751
+ serverOutput: "string",
2752
+ serverModules: options.serverModules,
2753
+ vitePlugins: options.vitePlugins,
2754
+ }),
2755
+ cloudflareWorkspaceRuntimePlugin(),
2756
+ ],
2757
+ root: options.projectRoot,
2758
+ target: "es2022",
2759
+ vitePlugins: options.vitePlugins,
2760
+ });
2761
+ const entriesByName = new Map(bundleEntries.map((entry) => [entry.name, entry]));
2762
+ const entries = new Map<string, CloudflareBatchedRouteModule>();
2763
+
2764
+ for (const chunk of output.chunks) {
2765
+ if (!chunk.isEntry) {
2766
+ continue;
2767
+ }
2768
+
2769
+ const entry = entriesByName.get(chunk.name);
2770
+
2771
+ if (entry === undefined) {
2772
+ continue;
2773
+ }
2774
+
2775
+ entries.set(entry.routeId, {
2776
+ code: chunk.code,
2777
+ fileName: chunk.fileName,
2778
+ });
2779
+ }
2780
+
2781
+ return { chunks: output.chunks, entries };
2782
+ }
2783
+
2784
+ async function buildCloudflareStringRouteComponentModuleBatch(options: {
2785
+ cacheDir?: string | undefined;
2786
+ projectRoot: string;
2787
+ routes: readonly CloudflareBatchedStringRoute[];
2788
+ serverModules: Record<string, BuiltServerModuleArtifact>;
2789
+ sourceAnalysis: BuildSourceAnalysisScope;
2790
+ vitePlugins?: readonly PluginOption[] | undefined;
2791
+ }): Promise<CloudflareBatchedRouteModules> {
2792
+ if (options.routes.length === 0) {
2793
+ return { chunks: [], entries: new Map() };
2794
+ }
2795
+
2796
+ const virtualModules = new Map<string, CloudflareVirtualModule>();
2797
+ const bundleEntries = await Promise.all(
2798
+ options.routes.map(async (route) => {
2799
+ const metadataImports: string[] = [];
2800
+ const pageMetadataId = `mreact:metadata:${route.routeId}:page`;
2801
+ const pageMetadataModule = await buildCloudflareRouteMetadataExportModule({
2802
+ cacheDir: options.cacheDir,
2803
+ filename: route.filename,
2804
+ hasMetadata: buildSourceAnalysisForFile(
2805
+ options.sourceAnalysis,
2806
+ options.projectRoot,
2807
+ route.filename,
2808
+ )?.hasMetadata,
2809
+ root: options.projectRoot,
2810
+ vitePlugins: options.vitePlugins,
2811
+ });
2812
+
2813
+ if (pageMetadataModule === undefined) {
2814
+ metadataImports.push("const pageMetadataModule = {};");
2815
+ } else {
2816
+ virtualModules.set(pageMetadataId, {
2817
+ contents: pageMetadataModule,
2818
+ resolveDir: dirname(route.filename),
2819
+ });
2820
+ metadataImports.push(`import * as pageMetadataModule from ${JSON.stringify(pageMetadataId)};`);
2821
+ }
2822
+
2823
+ const shellMetadataImports = await Promise.all(
2824
+ route.shellFiles.map(async (shell, index) => {
2825
+ const shellMetadataId = `mreact:metadata:${route.routeId}:shell:${index}`;
2826
+ const shellMetadataModule = await buildCloudflareRouteMetadataExportModule({
2827
+ cacheDir: options.cacheDir,
2828
+ filename: shell.file,
2829
+ hasMetadata: buildSourceAnalysisForFile(
2830
+ options.sourceAnalysis,
2831
+ options.projectRoot,
2832
+ shell.file,
2833
+ )?.hasMetadata,
2834
+ root: options.projectRoot,
2835
+ vitePlugins: options.vitePlugins,
2836
+ });
2837
+
2838
+ if (shellMetadataModule === undefined) {
2839
+ return `const shellMetadataModule${index} = {};`;
2840
+ }
2841
+
2842
+ virtualModules.set(shellMetadataId, {
2843
+ contents: shellMetadataModule,
2844
+ resolveDir: dirname(shell.file),
2845
+ });
2846
+ return `import * as shellMetadataModule${index} from ${JSON.stringify(shellMetadataId)};`;
2847
+ }),
2848
+ );
2849
+ metadataImports.push(...shellMetadataImports);
2850
+
2851
+ return {
2852
+ code: cloudflareStringRouteComponentModuleEntry({
2853
+ filename: route.filename,
2854
+ metadataImports,
2855
+ pageMetadataModuleName: "pageMetadataModule",
2856
+ shellFiles: route.shellFiles,
2857
+ shellMetadataModuleName: (index) => `shellMetadataModule${index}`,
2858
+ }),
2859
+ filename: `${route.filename}.mreact-cloudflare-string-route.js`,
2860
+ name: `${route.routeId}.string`,
2861
+ routeId: route.routeId,
2862
+ };
2863
+ }),
2864
+ );
2865
+ const output = await bundleRouterModules({
2866
+ cacheDir: options.cacheDir,
2867
+ chunkFileNames: "routes/chunks/[name].[hash].mjs",
2868
+ entries: bundleEntries,
2869
+ entryFileNames: "routes/[name].[hash].mjs",
2870
+ minify: true,
2871
+ platform: "node",
2872
+ plugins: [
2873
+ cloudflareVirtualModulesPlugin(virtualModules),
2874
+ cloudflareServerSourceTransformPlugin({
2875
+ projectRoot: options.projectRoot,
2876
+ serverOutput: "string",
2877
+ serverModules: options.serverModules,
2878
+ vitePlugins: options.vitePlugins,
2879
+ }),
2880
+ cloudflareWorkspaceRuntimePlugin(),
2881
+ ],
2882
+ root: options.projectRoot,
2883
+ target: "es2022",
2884
+ vitePlugins: options.vitePlugins,
2885
+ });
2886
+ const entriesByName = new Map(bundleEntries.map((entry) => [entry.name, entry]));
2887
+ const entries = new Map<string, CloudflareBatchedRouteModule>();
2888
+
2889
+ for (const chunk of output.chunks) {
2890
+ if (!chunk.isEntry) {
2891
+ continue;
2892
+ }
2893
+
2894
+ const entry = entriesByName.get(chunk.name);
2895
+
2896
+ if (entry === undefined) {
2897
+ continue;
2898
+ }
2899
+
2900
+ entries.set(entry.routeId, {
2901
+ code: chunk.code,
2902
+ fileName: chunk.fileName,
2903
+ });
2904
+ }
2905
+
2906
+ return { chunks: output.chunks, entries };
2907
+ }
2908
+
2909
+ function cloudflareStringRouteComponentModuleEntry(options: {
2910
+ filename: string;
2911
+ metadataImports: readonly string[];
2912
+ pageMetadataModuleName: string;
2913
+ shellFiles: readonly CloudflareShellFile[];
2914
+ shellMetadataModuleName: (index: number) => string;
2915
+ }): string {
2916
+ const shellImports = options.shellFiles.map(
2917
+ (shell, index) => `import * as shellRouteModule${index} from ${JSON.stringify(shell.file)};`,
2918
+ );
2919
+ const shellDefinitions = options.shellFiles.map(
2920
+ (shell, index) =>
2921
+ `{ component: selectComponent(shell${index}, ${JSON.stringify(shell.file)}), id: ${JSON.stringify(shell.id)}, kind: ${JSON.stringify(shell.kind)}, module: shell${index} }`,
2922
+ );
2923
+ const shellModules = options.shellFiles.map(
2924
+ (_, index) =>
2925
+ `const shell${index} = routeComponentModule(shellRouteModule${index}, ${options.shellMetadataModuleName(index)});`,
2926
+ );
2927
+
2928
+ return `import * as pageRouteModule from ${JSON.stringify(options.filename)};
2929
+ ${shellImports.join("\n")}
2930
+ ${options.metadataImports.join("\n")}
2931
+
2932
+ const pageModule = routeComponentModule(pageRouteModule, ${options.pageMetadataModuleName});
2933
+ ${shellModules.join("\n")}
2934
+ const pageComponent = selectComponent(pageModule, ${JSON.stringify(options.filename)});
2935
+ const shells = [${shellDefinitions.join(", ")}];
2936
+ export const slots = pageModule.slots;
2937
+ export const App = renderCloudflareStringRoute;
2938
+ export default renderCloudflareStringRoute;
2939
+
2940
+ async function renderCloudflareStringRoute(props) {
2941
+ const slotHtml = await renderRouteSlots(pageModule.slots, props);
2942
+ const layoutShells = await renderLayoutShells(shells, props, slotHtml);
2943
+ const metadata = await resolveRouteMetadata([...shells.map((shell) => shell.module), pageModule], props);
2944
+ let html = "<!DOCTYPE html>";
2945
+ for (const shell of layoutShells) {
2946
+ html += shell.prefix;
2947
+ }
2948
+ html += String(await pageComponent(props) ?? "");
2949
+ for (const shell of [...layoutShells].reverse()) {
2950
+ html += shell.suffix;
2951
+ }
2952
+ html = injectCloudflareHead(html, metadata, cloudflareRouteHeadTags(props.clientManifest, props.route.path));
2953
+ return new Response(html, {
2954
+ headers: {
2955
+ "content-type": "text/html; charset=utf-8"
2956
+ }
2957
+ });
2958
+ }
2959
+
2960
+ function routeComponentModule(routeModule, metadataModule) {
2961
+ const component = routeModule.default ?? routeModule.App ?? Object.values(routeModule).find((value) => typeof value === "function");
2962
+ return {
2963
+ ...routeModule,
2964
+ App: component,
2965
+ default: component,
2966
+ generateMetadata: metadataModule.generateMetadata,
2967
+ metadata: metadataModule.metadata,
2968
+ slots: routeModule.slots,
2969
+ };
2970
+ }
2971
+
2972
+ function selectComponent(module, label) {
2973
+ const component = module.default ?? module.App ?? Object.values(module).find((value) => typeof value === "function");
2974
+ if (typeof component !== "function") {
2975
+ throw new Error(\`No Cloudflare component export was found for \${label}.\`);
2976
+ }
2977
+ return component;
2978
+ }
2979
+
2980
+ async function renderRouteSlots(slots, props) {
2981
+ if (slots === undefined) {
2982
+ return {};
2983
+ }
2984
+ const rendered = {};
2985
+ for (const [name, value] of Object.entries(slots)) {
2986
+ rendered[name] = typeof value === "function" ? String(await value(props) ?? "") : String(value ?? "");
2987
+ }
2988
+ return rendered;
2989
+ }
2990
+
2991
+ ${cloudflareShellRuntimeSource()}`;
2992
+ }
2993
+
2558
2994
  async function buildCloudflareServerComponentModule(options: {
2559
2995
  cacheDir?: string | undefined;
2560
2996
  filename: string;
@@ -2571,15 +3007,10 @@ async function buildCloudflareServerComponentModule(options: {
2571
3007
  root: options.projectRoot,
2572
3008
  vitePlugins: options.vitePlugins,
2573
3009
  });
2574
- const entry = `import * as routeModule from ${JSON.stringify(options.filename)};
2575
- ${metadataModule === undefined ? "const metadataModule = {};" : 'import * as metadataModule from "mreact:metadata";'}
2576
-
2577
- const component = routeModule.default ?? routeModule.App ?? Object.values(routeModule).find((value) => typeof value === "function");
2578
- export const App = component;
2579
- export default component;
2580
- export const generateMetadata = metadataModule.generateMetadata;
2581
- export const metadata = metadataModule.metadata;
2582
- export const slots = routeModule.slots;`;
3010
+ const entry = cloudflareServerComponentModuleEntry(
3011
+ options.filename,
3012
+ metadataModule === undefined ? undefined : "mreact:metadata",
3013
+ );
2583
3014
 
2584
3015
  return bundleCloudflareVirtualModule({
2585
3016
  entry,
@@ -2603,6 +3034,21 @@ export const slots = routeModule.slots;`;
2603
3034
  });
2604
3035
  }
2605
3036
 
3037
+ function cloudflareServerComponentModuleEntry(
3038
+ filename: string,
3039
+ metadataModuleId: string | undefined,
3040
+ ): string {
3041
+ return `import * as routeModule from ${JSON.stringify(filename)};
3042
+ ${metadataModuleId === undefined ? "const metadataModule = {};" : `import * as metadataModule from ${JSON.stringify(metadataModuleId)};`}
3043
+
3044
+ const component = routeModule.default ?? routeModule.App ?? Object.values(routeModule).find((value) => typeof value === "function");
3045
+ export const App = component;
3046
+ export default component;
3047
+ export const generateMetadata = metadataModule.generateMetadata;
3048
+ export const metadata = metadataModule.metadata;
3049
+ export const slots = routeModule.slots;`;
3050
+ }
3051
+
2606
3052
  async function buildCloudflareStringRouteComponentModule(options: {
2607
3053
  cacheDir?: string | undefined;
2608
3054
  filename: string;
@@ -3341,28 +3787,14 @@ async function bundleCloudflareVirtualModule(options: {
3341
3787
  entry: options.entry,
3342
3788
  filename: options.filename,
3343
3789
  plugins: [
3344
- {
3345
- name: "mreact-cloudflare-virtual-modules",
3346
- setup(buildApi) {
3347
- buildApi.onResolve({ filter: /^mreact:/ }, (args) => ({
3348
- namespace: "mreact-cloudflare-virtual",
3349
- path: args.path,
3350
- }));
3351
- buildApi.onLoad({ filter: /.*/, namespace: "mreact-cloudflare-virtual" }, (args) => {
3352
- const contents = options.modules.get(args.path);
3353
-
3354
- if (contents === undefined) {
3355
- throw new Error(`Missing virtual Cloudflare module ${args.path}.`);
3356
- }
3357
-
3358
- return {
3359
- contents,
3360
- loader: "js",
3361
- resolveDir: options.resolveDir,
3362
- };
3363
- });
3364
- },
3365
- },
3790
+ cloudflareVirtualModulesPlugin(
3791
+ new Map(
3792
+ Array.from(options.modules, ([id, contents]) => [
3793
+ id,
3794
+ { contents, resolveDir: options.resolveDir },
3795
+ ]),
3796
+ ),
3797
+ ),
3366
3798
  ...options.plugins,
3367
3799
  ],
3368
3800
  resolveDir: options.resolveDir,
@@ -3371,6 +3803,38 @@ async function bundleCloudflareVirtualModule(options: {
3371
3803
  });
3372
3804
  }
3373
3805
 
3806
+ interface CloudflareVirtualModule {
3807
+ contents: string;
3808
+ resolveDir: string;
3809
+ }
3810
+
3811
+ function cloudflareVirtualModulesPlugin(
3812
+ modules: ReadonlyMap<string, CloudflareVirtualModule>,
3813
+ ): RouterCompatPlugin {
3814
+ return {
3815
+ name: "mreact-cloudflare-virtual-modules",
3816
+ setup(buildApi) {
3817
+ buildApi.onResolve({ filter: /^mreact:/ }, (args) => ({
3818
+ namespace: "mreact-cloudflare-virtual",
3819
+ path: args.path,
3820
+ }));
3821
+ buildApi.onLoad({ filter: /.*/, namespace: "mreact-cloudflare-virtual" }, (args) => {
3822
+ const module = modules.get(args.path);
3823
+
3824
+ if (module === undefined) {
3825
+ throw new Error(`Missing virtual Cloudflare module ${args.path}.`);
3826
+ }
3827
+
3828
+ return {
3829
+ contents: module.contents,
3830
+ loader: "js",
3831
+ resolveDir: module.resolveDir,
3832
+ };
3833
+ });
3834
+ },
3835
+ };
3836
+ }
3837
+
3374
3838
  async function cloudflareShellFilesForPage(
3375
3839
  routesDir: string,
3376
3840
  pageFile: string,