@reckona/mreact-router 0.0.99 → 0.0.101

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/src/build.ts CHANGED
@@ -2380,6 +2380,11 @@ interface CloudflareBatchedRouteModules {
2380
2380
  entries: ReadonlyMap<string, CloudflareBatchedRouteModule>;
2381
2381
  }
2382
2382
 
2383
+ interface CloudflareBatchedComponentRoute {
2384
+ filename: string;
2385
+ routeId: string;
2386
+ }
2387
+
2383
2388
  interface RouteRequestModuleBatchEntry {
2384
2389
  code: string;
2385
2390
  filename: string;
@@ -2437,10 +2442,26 @@ async function writeCloudflareRouteModules(options: {
2437
2442
  root: options.projectRoot,
2438
2443
  vitePlugins: options.vitePlugins,
2439
2444
  });
2445
+ const directComponentRoutes = await collectCloudflareDirectComponentRoutes({
2446
+ requiredRoutes,
2447
+ routesDir: options.routesDir,
2448
+ serverModules: options.serverModules,
2449
+ sourceAnalysis: options.sourceAnalysis,
2450
+ });
2451
+ const directComponentRouteIds = new Set(directComponentRoutes.map((route) => route.routeId));
2452
+ const directComponentModules = await buildCloudflareServerComponentModuleBatch({
2453
+ cacheDir: options.cacheDir,
2454
+ projectRoot: options.projectRoot,
2455
+ routes: directComponentRoutes,
2456
+ serverModules: options.serverModules,
2457
+ sourceAnalysis: options.sourceAnalysis,
2458
+ vitePlugins: options.vitePlugins,
2459
+ });
2440
2460
 
2441
2461
  await Promise.all([
2442
2462
  writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, serverRouteModules),
2443
2463
  writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, loaderRouteModules),
2464
+ writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, directComponentModules),
2444
2465
  ]);
2445
2466
 
2446
2467
  const registryEntries = await mapWithBuildConcurrency(requiredRoutes, async ({ route, routeFile, routeId }) => {
@@ -2480,29 +2501,39 @@ async function writeCloudflareRouteModules(options: {
2480
2501
  : "string";
2481
2502
 
2482
2503
  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`;
2504
+ const batchedComponent =
2505
+ directComponentRouteIds.has(routeId) ? directComponentModules.entries.get(routeId) : undefined;
2506
+ let componentFile = batchedComponent?.fileName;
2507
+
2508
+ if (batchedComponent === undefined) {
2509
+ const componentOutput =
2510
+ serverOutput === "stream"
2511
+ ? await buildCloudflareStreamRouteComponentModule({
2512
+ cacheDir: options.cacheDir,
2513
+ filename: route.file,
2514
+ projectRoot: options.projectRoot,
2515
+ routesDir: options.routesDir,
2516
+ serverModules: options.serverModules,
2517
+ sourceAnalysis: options.sourceAnalysis,
2518
+ vitePlugins: options.vitePlugins,
2519
+ })
2520
+ : await buildCloudflareStringRouteComponentModule({
2521
+ cacheDir: options.cacheDir,
2522
+ filename: route.file,
2523
+ projectRoot: options.projectRoot,
2524
+ routesDir: options.routesDir,
2525
+ serverModules: options.serverModules,
2526
+ sourceAnalysis: options.sourceAnalysis,
2527
+ vitePlugins: options.vitePlugins,
2528
+ });
2504
2529
 
2505
- await writeFile(join(options.cloudflareDir, componentFile), componentOutput);
2530
+ componentFile = `routes/${routeId}.${hashText(componentOutput).slice(0, 8)}.component.mjs`;
2531
+ await writeFile(join(options.cloudflareDir, componentFile), componentOutput);
2532
+ }
2533
+
2534
+ if (componentFile === undefined) {
2535
+ throw new Error("Missing bundled Cloudflare component module.");
2536
+ }
2506
2537
 
2507
2538
  const componentImport = `./${componentFile.split("/").pop() ?? componentFile}`;
2508
2539
  routeModuleExports = [
@@ -2549,12 +2580,140 @@ async function writeCloudflareRouteModules(options: {
2549
2580
  return { registryFile: "route-modules.mjs" };
2550
2581
  }
2551
2582
 
2583
+ async function collectCloudflareDirectComponentRoutes(options: {
2584
+ requiredRoutes: readonly CloudflareRequiredRoute[];
2585
+ routesDir: string;
2586
+ serverModules: Record<string, BuiltServerModuleArtifact>;
2587
+ sourceAnalysis: BuildSourceAnalysisScope;
2588
+ }): Promise<CloudflareBatchedComponentRoute[]> {
2589
+ const routes = await Promise.all(
2590
+ options.requiredRoutes.map(async ({ route, routeFile, routeId }) => {
2591
+ if (route.kind !== "page") {
2592
+ return undefined;
2593
+ }
2594
+
2595
+ const serverOutput =
2596
+ options.serverModules[routeFile]?.analysis?.streamRoute === true ||
2597
+ options.sourceAnalysis.byRouteFile.get(routeFile)?.streamRoute === true
2598
+ ? "stream"
2599
+ : "string";
2600
+
2601
+ if (serverOutput !== "string") {
2602
+ return undefined;
2603
+ }
2604
+
2605
+ const shellFiles = await cloudflareShellFilesForPage(options.routesDir, route.file);
2606
+
2607
+ if (shellFiles.length > 0) {
2608
+ return undefined;
2609
+ }
2610
+
2611
+ return {
2612
+ filename: route.file,
2613
+ routeId,
2614
+ };
2615
+ }),
2616
+ );
2617
+
2618
+ return routes.filter((route): route is CloudflareBatchedComponentRoute => route !== undefined);
2619
+ }
2620
+
2552
2621
  interface CloudflareShellFile {
2553
2622
  file: string;
2554
2623
  id: string;
2555
2624
  kind: "layout" | "template";
2556
2625
  }
2557
2626
 
2627
+ async function buildCloudflareServerComponentModuleBatch(options: {
2628
+ cacheDir?: string | undefined;
2629
+ projectRoot: string;
2630
+ routes: readonly CloudflareBatchedComponentRoute[];
2631
+ serverModules: Record<string, BuiltServerModuleArtifact>;
2632
+ sourceAnalysis: BuildSourceAnalysisScope;
2633
+ vitePlugins?: readonly PluginOption[] | undefined;
2634
+ }): Promise<CloudflareBatchedRouteModules> {
2635
+ if (options.routes.length === 0) {
2636
+ return { chunks: [], entries: new Map() };
2637
+ }
2638
+
2639
+ const virtualModules = new Map<string, CloudflareVirtualModule>();
2640
+ const bundleEntries = await Promise.all(
2641
+ options.routes.map(async (route) => {
2642
+ const metadataModule = await buildCloudflareRouteMetadataExportModule({
2643
+ cacheDir: options.cacheDir,
2644
+ filename: route.filename,
2645
+ hasMetadata: buildSourceAnalysisForFile(
2646
+ options.sourceAnalysis,
2647
+ options.projectRoot,
2648
+ route.filename,
2649
+ )?.hasMetadata,
2650
+ root: options.projectRoot,
2651
+ vitePlugins: options.vitePlugins,
2652
+ });
2653
+ const metadataId = `mreact:metadata:${route.routeId}`;
2654
+
2655
+ if (metadataModule !== undefined) {
2656
+ virtualModules.set(metadataId, {
2657
+ contents: metadataModule,
2658
+ resolveDir: dirname(route.filename),
2659
+ });
2660
+ }
2661
+
2662
+ return {
2663
+ code: cloudflareServerComponentModuleEntry(
2664
+ route.filename,
2665
+ metadataModule === undefined ? undefined : metadataId,
2666
+ ),
2667
+ filename: `${route.filename}.mreact-cloudflare-component.js`,
2668
+ name: `${route.routeId}.component`,
2669
+ routeId: route.routeId,
2670
+ };
2671
+ }),
2672
+ );
2673
+ const output = await bundleRouterModules({
2674
+ cacheDir: options.cacheDir,
2675
+ chunkFileNames: "routes/chunks/[name].[hash].mjs",
2676
+ entries: bundleEntries,
2677
+ entryFileNames: "routes/[name].[hash].mjs",
2678
+ minify: true,
2679
+ platform: "node",
2680
+ plugins: [
2681
+ cloudflareVirtualModulesPlugin(virtualModules),
2682
+ cloudflareServerSourceTransformPlugin({
2683
+ projectRoot: options.projectRoot,
2684
+ serverOutput: "string",
2685
+ serverModules: options.serverModules,
2686
+ vitePlugins: options.vitePlugins,
2687
+ }),
2688
+ cloudflareWorkspaceRuntimePlugin(),
2689
+ ],
2690
+ root: options.projectRoot,
2691
+ target: "es2022",
2692
+ vitePlugins: options.vitePlugins,
2693
+ });
2694
+ const entriesByName = new Map(bundleEntries.map((entry) => [entry.name, entry]));
2695
+ const entries = new Map<string, CloudflareBatchedRouteModule>();
2696
+
2697
+ for (const chunk of output.chunks) {
2698
+ if (!chunk.isEntry) {
2699
+ continue;
2700
+ }
2701
+
2702
+ const entry = entriesByName.get(chunk.name);
2703
+
2704
+ if (entry === undefined) {
2705
+ continue;
2706
+ }
2707
+
2708
+ entries.set(entry.routeId, {
2709
+ code: chunk.code,
2710
+ fileName: chunk.fileName,
2711
+ });
2712
+ }
2713
+
2714
+ return { chunks: output.chunks, entries };
2715
+ }
2716
+
2558
2717
  async function buildCloudflareServerComponentModule(options: {
2559
2718
  cacheDir?: string | undefined;
2560
2719
  filename: string;
@@ -2571,15 +2730,10 @@ async function buildCloudflareServerComponentModule(options: {
2571
2730
  root: options.projectRoot,
2572
2731
  vitePlugins: options.vitePlugins,
2573
2732
  });
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;`;
2733
+ const entry = cloudflareServerComponentModuleEntry(
2734
+ options.filename,
2735
+ metadataModule === undefined ? undefined : "mreact:metadata",
2736
+ );
2583
2737
 
2584
2738
  return bundleCloudflareVirtualModule({
2585
2739
  entry,
@@ -2603,6 +2757,21 @@ export const slots = routeModule.slots;`;
2603
2757
  });
2604
2758
  }
2605
2759
 
2760
+ function cloudflareServerComponentModuleEntry(
2761
+ filename: string,
2762
+ metadataModuleId: string | undefined,
2763
+ ): string {
2764
+ return `import * as routeModule from ${JSON.stringify(filename)};
2765
+ ${metadataModuleId === undefined ? "const metadataModule = {};" : `import * as metadataModule from ${JSON.stringify(metadataModuleId)};`}
2766
+
2767
+ const component = routeModule.default ?? routeModule.App ?? Object.values(routeModule).find((value) => typeof value === "function");
2768
+ export const App = component;
2769
+ export default component;
2770
+ export const generateMetadata = metadataModule.generateMetadata;
2771
+ export const metadata = metadataModule.metadata;
2772
+ export const slots = routeModule.slots;`;
2773
+ }
2774
+
2606
2775
  async function buildCloudflareStringRouteComponentModule(options: {
2607
2776
  cacheDir?: string | undefined;
2608
2777
  filename: string;
@@ -3341,28 +3510,14 @@ async function bundleCloudflareVirtualModule(options: {
3341
3510
  entry: options.entry,
3342
3511
  filename: options.filename,
3343
3512
  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
- },
3513
+ cloudflareVirtualModulesPlugin(
3514
+ new Map(
3515
+ Array.from(options.modules, ([id, contents]) => [
3516
+ id,
3517
+ { contents, resolveDir: options.resolveDir },
3518
+ ]),
3519
+ ),
3520
+ ),
3366
3521
  ...options.plugins,
3367
3522
  ],
3368
3523
  resolveDir: options.resolveDir,
@@ -3371,6 +3526,38 @@ async function bundleCloudflareVirtualModule(options: {
3371
3526
  });
3372
3527
  }
3373
3528
 
3529
+ interface CloudflareVirtualModule {
3530
+ contents: string;
3531
+ resolveDir: string;
3532
+ }
3533
+
3534
+ function cloudflareVirtualModulesPlugin(
3535
+ modules: ReadonlyMap<string, CloudflareVirtualModule>,
3536
+ ): RouterCompatPlugin {
3537
+ return {
3538
+ name: "mreact-cloudflare-virtual-modules",
3539
+ setup(buildApi) {
3540
+ buildApi.onResolve({ filter: /^mreact:/ }, (args) => ({
3541
+ namespace: "mreact-cloudflare-virtual",
3542
+ path: args.path,
3543
+ }));
3544
+ buildApi.onLoad({ filter: /.*/, namespace: "mreact-cloudflare-virtual" }, (args) => {
3545
+ const module = modules.get(args.path);
3546
+
3547
+ if (module === undefined) {
3548
+ throw new Error(`Missing virtual Cloudflare module ${args.path}.`);
3549
+ }
3550
+
3551
+ return {
3552
+ contents: module.contents,
3553
+ loader: "js",
3554
+ resolveDir: module.resolveDir,
3555
+ };
3556
+ });
3557
+ },
3558
+ };
3559
+ }
3560
+
3374
3561
  async function cloudflareShellFilesForPage(
3375
3562
  routesDir: string,
3376
3563
  pageFile: string,
package/src/vite.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import type { ServerResponse } from "node:http";
3
- import { dirname } from "node:path";
3
+ import { dirname, relative, sep } from "node:path";
4
4
  import { formatDiagnostic } from "@reckona/mreact-compiler";
5
5
  import {
6
6
  createCompilerModuleContext,
@@ -69,6 +69,7 @@ export interface AppRouterVitePluginOptions extends AppRouterProjectOptions {
69
69
 
70
70
  const clientPrefix = "/_mreact/client/";
71
71
  const devCssPrefix = "/_mreact/dev-css/";
72
+ const devCssSourceQuery = "mreact-router-dev-css-source";
72
73
  const clientRouteModuleQuery = "mreact-router-client-route";
73
74
  const virtualClientPrefix = "\0mreact-router-client:";
74
75
  const virtualReactiveCoreId = "\0mreact-router-reactive-core";
@@ -233,6 +234,13 @@ export function createAppRouterVitePlugin(options: AppRouterVitePluginOptions):
233
234
  return [];
234
235
  },
235
236
  load(id) {
237
+ if (isDevCssSourceModuleId(id)) {
238
+ return loadDevCssSourceModule({
239
+ cssFile: clientRequestPath(id),
240
+ sourceDirs: project.allowedSourceDirs,
241
+ });
242
+ }
243
+
236
244
  if (id === virtualReactiveCoreId) {
237
245
  return `import { cell as nativeCell } from ${JSON.stringify(reactiveCorePath)};
238
246
  export * from ${JSON.stringify(reactiveCorePath)};
@@ -675,6 +683,59 @@ function isMreactClientDevModuleId(id: string | null | undefined): boolean {
675
683
  );
676
684
  }
677
685
 
686
+ function isDevCssSourceModuleId(id: string): boolean {
687
+ return new URLSearchParams(id.slice(id.indexOf("?") + 1)).has(devCssSourceQuery);
688
+ }
689
+
690
+ async function loadDevCssSourceModule(options: {
691
+ cssFile: string;
692
+ sourceDirs: readonly string[];
693
+ }): Promise<string | undefined> {
694
+ const code = await readFile(options.cssFile, "utf8");
695
+
696
+ return prependDevTailwindSourceDirectives({
697
+ code,
698
+ cssFile: options.cssFile,
699
+ sourceDirs: options.sourceDirs,
700
+ });
701
+ }
702
+
703
+ function prependDevTailwindSourceDirectives(options: {
704
+ code: string;
705
+ cssFile: string;
706
+ sourceDirs: readonly string[];
707
+ }): string {
708
+ if (!isTailwindCssEntry(options.code)) {
709
+ return options.code;
710
+ }
711
+
712
+ const cssDir = dirname(options.cssFile);
713
+ const directives = [...new Set(options.sourceDirs)]
714
+ .map((sourceDir) => `${devTailwindSourceDirective(cssDir, sourceDir)}\n`)
715
+ .join("");
716
+
717
+ return directives.length === 0 ? options.code : `${directives}${options.code}`;
718
+ }
719
+
720
+ function isTailwindCssEntry(code: string): boolean {
721
+ return (
722
+ /@import\s+(?:url\()?["']tailwindcss(?:\/[^"']*)?["']\)?/u.test(code) ||
723
+ /@tailwind\s+(?:base|components|utilities)\b/u.test(code)
724
+ );
725
+ }
726
+
727
+ function devTailwindSourceDirective(cssDir: string, sourceDir: string): string {
728
+ const relativeSourceDir = relative(cssDir, sourceDir).split(sep).join("/");
729
+ const normalizedSourceDir =
730
+ relativeSourceDir === ""
731
+ ? "."
732
+ : relativeSourceDir.startsWith(".")
733
+ ? relativeSourceDir
734
+ : `./${relativeSourceDir}`;
735
+
736
+ return `@source ${JSON.stringify(`${normalizedSourceDir}/**/*.{js,jsx,ts,tsx,mdx}`)};`;
737
+ }
738
+
678
739
  function importerInRuntimePackage(
679
740
  importer: string | undefined,
680
741
  directories: readonly string[],
@@ -793,7 +854,9 @@ function createDevCssProxyMiddleware(): Connect.NextHandleFunction {
793
854
  }
794
855
  };
795
856
 
796
- incoming.url = `${sourcePath}${url.search}`;
857
+ const sourceSearch = new URLSearchParams(url.search);
858
+ sourceSearch.set(devCssSourceQuery, "");
859
+ incoming.url = `${sourcePath}?${sourceSearch.toString()}`;
797
860
  incoming.headers.accept = "text/css,*/*;q=0.1";
798
861
  outgoing.once("finish", restore);
799
862
  outgoing.once("close", restore);