@reckona/mreact-router 0.0.101 → 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.101",
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.101",
109
- "@reckona/mreact-compiler": "0.0.101",
110
- "@reckona/mreact-devtools": "0.0.101",
111
- "@reckona/mreact-compat": "0.0.101",
112
- "@reckona/mreact-reactive-core": "0.0.101",
113
- "@reckona/mreact-query": "0.0.101",
114
- "@reckona/mreact-reactive-dom": "0.0.101",
115
- "@reckona/mreact-server": "0.0.101",
116
- "@reckona/mreact-shared": "0.0.101"
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.101"
122
+ "@reckona/mreact-router-native": "0.0.102"
123
123
  }
124
124
  }
package/src/build.ts CHANGED
@@ -2385,6 +2385,12 @@ interface CloudflareBatchedComponentRoute {
2385
2385
  routeId: string;
2386
2386
  }
2387
2387
 
2388
+ interface CloudflareBatchedStringRoute {
2389
+ filename: string;
2390
+ routeId: string;
2391
+ shellFiles: CloudflareShellFile[];
2392
+ }
2393
+
2388
2394
  interface RouteRequestModuleBatchEntry {
2389
2395
  code: string;
2390
2396
  filename: string;
@@ -2448,7 +2454,16 @@ async function writeCloudflareRouteModules(options: {
2448
2454
  serverModules: options.serverModules,
2449
2455
  sourceAnalysis: options.sourceAnalysis,
2450
2456
  });
2457
+ const stringShellComponentRoutes = await collectCloudflareStringShellComponentRoutes({
2458
+ requiredRoutes,
2459
+ routesDir: options.routesDir,
2460
+ serverModules: options.serverModules,
2461
+ sourceAnalysis: options.sourceAnalysis,
2462
+ });
2451
2463
  const directComponentRouteIds = new Set(directComponentRoutes.map((route) => route.routeId));
2464
+ const stringShellComponentRouteIds = new Set(
2465
+ stringShellComponentRoutes.map((route) => route.routeId),
2466
+ );
2452
2467
  const directComponentModules = await buildCloudflareServerComponentModuleBatch({
2453
2468
  cacheDir: options.cacheDir,
2454
2469
  projectRoot: options.projectRoot,
@@ -2457,11 +2472,20 @@ async function writeCloudflareRouteModules(options: {
2457
2472
  sourceAnalysis: options.sourceAnalysis,
2458
2473
  vitePlugins: options.vitePlugins,
2459
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
+ });
2460
2483
 
2461
2484
  await Promise.all([
2462
2485
  writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, serverRouteModules),
2463
2486
  writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, loaderRouteModules),
2464
2487
  writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, directComponentModules),
2488
+ writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, stringShellComponentModules),
2465
2489
  ]);
2466
2490
 
2467
2491
  const registryEntries = await mapWithBuildConcurrency(requiredRoutes, async ({ route, routeFile, routeId }) => {
@@ -2502,7 +2526,11 @@ async function writeCloudflareRouteModules(options: {
2502
2526
 
2503
2527
  try {
2504
2528
  const batchedComponent =
2505
- directComponentRouteIds.has(routeId) ? directComponentModules.entries.get(routeId) : undefined;
2529
+ stringShellComponentRouteIds.has(routeId)
2530
+ ? stringShellComponentModules.entries.get(routeId)
2531
+ : directComponentRouteIds.has(routeId)
2532
+ ? directComponentModules.entries.get(routeId)
2533
+ : undefined;
2506
2534
  let componentFile = batchedComponent?.fileName;
2507
2535
 
2508
2536
  if (batchedComponent === undefined) {
@@ -2618,6 +2646,45 @@ async function collectCloudflareDirectComponentRoutes(options: {
2618
2646
  return routes.filter((route): route is CloudflareBatchedComponentRoute => route !== undefined);
2619
2647
  }
2620
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
+
2621
2688
  interface CloudflareShellFile {
2622
2689
  file: string;
2623
2690
  id: string;
@@ -2714,6 +2781,216 @@ async function buildCloudflareServerComponentModuleBatch(options: {
2714
2781
  return { chunks: output.chunks, entries };
2715
2782
  }
2716
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
+
2717
2994
  async function buildCloudflareServerComponentModule(options: {
2718
2995
  cacheDir?: string | undefined;
2719
2996
  filename: string;