@reckona/mreact-router 0.0.96 → 0.0.98

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.
Files changed (100) hide show
  1. package/README.md +4 -0
  2. package/dist/actions.d.ts +7 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +26 -2
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  7. package/dist/adapters/aws-lambda.js +10 -7
  8. package/dist/adapters/aws-lambda.js.map +1 -1
  9. package/dist/adapters/static.d.ts.map +1 -1
  10. package/dist/adapters/static.js +13 -0
  11. package/dist/adapters/static.js.map +1 -1
  12. package/dist/build.d.ts +38 -1
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.js +1093 -307
  15. package/dist/build.js.map +1 -1
  16. package/dist/bundle-pipeline.d.ts +7 -0
  17. package/dist/bundle-pipeline.d.ts.map +1 -1
  18. package/dist/bundle-pipeline.js +121 -69
  19. package/dist/bundle-pipeline.js.map +1 -1
  20. package/dist/cache.d.ts.map +1 -1
  21. package/dist/cache.js +10 -8
  22. package/dist/cache.js.map +1 -1
  23. package/dist/cli-options.d.ts +1 -0
  24. package/dist/cli-options.d.ts.map +1 -1
  25. package/dist/cli-options.js +16 -0
  26. package/dist/cli-options.js.map +1 -1
  27. package/dist/cli.js +1 -0
  28. package/dist/cli.js.map +1 -1
  29. package/dist/client.d.ts +6 -1
  30. package/dist/client.d.ts.map +1 -1
  31. package/dist/client.js +120 -9
  32. package/dist/client.js.map +1 -1
  33. package/dist/csrf.d.ts.map +1 -1
  34. package/dist/csrf.js +7 -3
  35. package/dist/csrf.js.map +1 -1
  36. package/dist/http.d.ts.map +1 -1
  37. package/dist/http.js +18 -1
  38. package/dist/http.js.map +1 -1
  39. package/dist/import-policy.d.ts.map +1 -1
  40. package/dist/import-policy.js +7 -3
  41. package/dist/import-policy.js.map +1 -1
  42. package/dist/index.d.ts +4 -3
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/link.d.ts +5 -3
  46. package/dist/link.d.ts.map +1 -1
  47. package/dist/link.js +92 -3
  48. package/dist/link.js.map +1 -1
  49. package/dist/logger.d.ts +7 -1
  50. package/dist/logger.d.ts.map +1 -1
  51. package/dist/logger.js.map +1 -1
  52. package/dist/module-runner.d.ts +2 -0
  53. package/dist/module-runner.d.ts.map +1 -1
  54. package/dist/module-runner.js +1 -0
  55. package/dist/module-runner.js.map +1 -1
  56. package/dist/native-route-matcher.d.ts +2 -1
  57. package/dist/native-route-matcher.d.ts.map +1 -1
  58. package/dist/native-route-matcher.js +13 -2
  59. package/dist/native-route-matcher.js.map +1 -1
  60. package/dist/render.d.ts.map +1 -1
  61. package/dist/render.js +121 -6
  62. package/dist/render.js.map +1 -1
  63. package/dist/route-source.d.ts +11 -11
  64. package/dist/route-source.d.ts.map +1 -1
  65. package/dist/route-source.js +32 -25
  66. package/dist/route-source.js.map +1 -1
  67. package/dist/route-styles.d.ts +6 -0
  68. package/dist/route-styles.d.ts.map +1 -1
  69. package/dist/route-styles.js +10 -1
  70. package/dist/route-styles.js.map +1 -1
  71. package/dist/serve.d.ts +5 -0
  72. package/dist/serve.d.ts.map +1 -1
  73. package/dist/serve.js +7 -4
  74. package/dist/serve.js.map +1 -1
  75. package/dist/vite.d.ts.map +1 -1
  76. package/dist/vite.js +32 -5
  77. package/dist/vite.js.map +1 -1
  78. package/package.json +11 -11
  79. package/src/actions.ts +42 -2
  80. package/src/adapters/aws-lambda.ts +33 -16
  81. package/src/adapters/static.ts +25 -0
  82. package/src/build.ts +1605 -421
  83. package/src/bundle-pipeline.ts +136 -70
  84. package/src/cache.ts +13 -8
  85. package/src/cli-options.ts +20 -0
  86. package/src/cli.ts +1 -0
  87. package/src/client.ts +152 -15
  88. package/src/csrf.ts +8 -3
  89. package/src/http.ts +21 -1
  90. package/src/import-policy.ts +8 -3
  91. package/src/index.ts +5 -0
  92. package/src/link.ts +135 -8
  93. package/src/logger.ts +9 -1
  94. package/src/module-runner.ts +4 -0
  95. package/src/native-route-matcher.ts +13 -2
  96. package/src/render.ts +181 -6
  97. package/src/route-source.ts +33 -20
  98. package/src/route-styles.ts +17 -1
  99. package/src/serve.ts +11 -6
  100. package/src/vite.ts +41 -5
package/src/build.ts CHANGED
@@ -2,7 +2,9 @@ import { createHash } from "node:crypto";
2
2
  import type { Dirent } from "node:fs";
3
3
  import { copyFile, cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { builtinModules } from "node:module";
5
+ import { availableParallelism } from "node:os";
5
6
  import { dirname, join, relative, resolve, sep } from "node:path";
7
+ import { fileURLToPath } from "node:url";
6
8
  import {
7
9
  collectStaticImportReferences,
8
10
  collectTopLevelValueExportNames,
@@ -30,7 +32,11 @@ import {
30
32
  routeIdForPath,
31
33
  type BuildClientRouteOutputOptions,
32
34
  } from "./navigation-runtime.js";
33
- import { bundleAppRouterSourceModule, importAppRouterSourceModule } from "./module-runner.js";
35
+ import {
36
+ bundleAppRouterSourceModule,
37
+ fileImportMetaUrlPlugin,
38
+ importAppRouterSourceModule,
39
+ } from "./module-runner.js";
34
40
  import { scanAppRoutes } from "./routes.js";
35
41
  import type { AppRoute } from "./routes.js";
36
42
  import {
@@ -64,12 +70,16 @@ import {
64
70
  } from "./route-source.js";
65
71
  import {
66
72
  bundleRouterModule,
73
+ bundleRouterModules,
67
74
  type RouterCompatPlugin,
75
+ type RouterBundleChunkOutput,
76
+ type RouterBundleOutput,
68
77
  } from "./bundle-pipeline.js";
69
- import { collectRouteCssFiles } from "./route-styles.js";
78
+ import { collectRouteCssFilesFromSources } from "./route-styles.js";
70
79
  import { existingRouteShellCandidates } from "./route-shells.js";
71
80
  import { sourceModuleCandidates } from "./source-modules.js";
72
81
  import { collectBuildInferredServerActions } from "./server-action-inference.js";
82
+ import { vitePluginsCacheKey } from "./vite-plugin-cache-key.js";
73
83
  import { workspacePackageFile } from "./workspace-packages.js";
74
84
  import type { PluginOption, UserConfig } from "vite";
75
85
 
@@ -78,12 +88,42 @@ const nativeEscapeTransform = {
78
88
  batchImportSource: "@reckona/mreact-router/native-escape",
79
89
  } as const;
80
90
 
91
+ const defaultBuildConcurrency = Math.max(1, Math.min(2, availableParallelism()));
92
+ const serverArtifactFilesystemConcurrency = 2;
93
+
94
+ type ServerTransformOutput = ReturnType<typeof transform>;
95
+ type ServerTransformCache = Map<string, Promise<ServerTransformOutput>>;
96
+
81
97
  export interface BuildAppOptions extends AppRouterProjectOptions {
98
+ onBuildPhaseTiming?: ((timing: BuildAppPhaseTiming) => void) | undefined;
82
99
  outDir: string;
83
100
  targets?: readonly AppRouterBuildTarget[] | undefined;
84
101
  viteConfig?: Pick<UserConfig, "plugins"> | undefined;
85
102
  }
86
103
 
104
+ export type BuildAppPhase =
105
+ | "scan"
106
+ | "collectFiles"
107
+ | "analyzeSources"
108
+ | "validate"
109
+ | "prepareOutput"
110
+ | "publicAssets"
111
+ | "serverActionManifest"
112
+ | "serverModules"
113
+ | "importPolicy"
114
+ | "serverModuleArtifacts"
115
+ | "clientBundles"
116
+ | "navigationRuntime"
117
+ | "prerender"
118
+ | "cloudflare"
119
+ | "writeManifests"
120
+ | "adapterArtifacts";
121
+
122
+ export interface BuildAppPhaseTiming {
123
+ ms: number;
124
+ phase: BuildAppPhase;
125
+ }
126
+
87
127
  export interface BuildAppResult {
88
128
  routes: AppRoute[];
89
129
  }
@@ -112,6 +152,7 @@ export interface CloudflarePagesArtifactManifest {
112
152
 
113
153
  export interface PackageAwsLambdaArtifactOptions {
114
154
  fromDir: string;
155
+ handlerEntry?: string | undefined;
115
156
  outDir: string;
116
157
  skipRuntimeDependencyCheck?: boolean | undefined;
117
158
  }
@@ -178,6 +219,32 @@ export interface BuiltRouteSourceAnalysisSummary {
178
219
  usesRuntimeCacheControl: boolean;
179
220
  }
180
221
 
222
+ interface BuildSourceAnalysis {
223
+ authIncludesClaims: boolean;
224
+ cachePolicy?: RouteCachePolicy | undefined;
225
+ hasGenerateStaticParams: boolean;
226
+ hasLoader: boolean;
227
+ hasMetadata: boolean;
228
+ hasPrerender: boolean;
229
+ source: string;
230
+ sourceHash: string;
231
+ usesRuntimeCacheControl: boolean;
232
+ }
233
+
234
+ interface BuildRouteSourceAnalysis extends BuildSourceAnalysis {
235
+ clientBoundaryImports: readonly string[];
236
+ clientRoute: boolean;
237
+ file: string;
238
+ route: AppRoute & { kind: "page" };
239
+ routeCode: string;
240
+ streamRoute: boolean;
241
+ }
242
+
243
+ interface BuildSourceAnalysisScope {
244
+ byFile: ReadonlyMap<string, BuildSourceAnalysis>;
245
+ byRouteFile: ReadonlyMap<string, BuildRouteSourceAnalysis>;
246
+ }
247
+
181
248
  export interface BuiltServerModuleOutput {
182
249
  bundleCode?: string;
183
250
  code: string;
@@ -195,84 +262,217 @@ export interface BuiltPrerenderedRoute {
195
262
  type StaticParams = Record<string, string | number | boolean | readonly string[]>;
196
263
 
197
264
  export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult> {
265
+ const timingSink = options.onBuildPhaseTiming;
198
266
  const project = resolveAppRouterProjectOptions(options);
199
267
  const buildTargets = resolveBuildTargets(options.targets ?? project.buildTargets);
200
268
  const shouldBuildCloudflare = buildTargets.includes("cloudflare");
201
269
  const shouldBuildAwsLambda = buildTargets.includes("aws-lambda");
202
- const routes = await scanAppRoutes({ appDir: project.routesDir });
270
+ const routes =
271
+ timingSink === undefined
272
+ ? await scanAppRoutes({ appDir: project.routesDir })
273
+ : await timeBuildPhase(timingSink, "scan", () =>
274
+ scanAppRoutes({ appDir: project.routesDir }),
275
+ );
203
276
  const vitePlugins = options.viteConfig?.plugins;
204
- const files = await collectBuildFiles(
205
- project.projectRoot,
206
- project.allowedSourceDirs,
207
- project.routesDir,
208
- );
277
+ const files =
278
+ timingSink === undefined
279
+ ? await collectBuildFiles(project.projectRoot, project.allowedSourceDirs, project.routesDir)
280
+ : await timeBuildPhase(timingSink, "collectFiles", () =>
281
+ collectBuildFiles(project.projectRoot, project.allowedSourceDirs, project.routesDir),
282
+ );
283
+ const serverClientRouteInferenceCache = createClientRouteInferenceCache();
284
+ const serverTransformCache: ServerTransformCache = new Map();
209
285
  const serverDir = join(options.outDir, "server");
210
286
  const clientDir = join(options.outDir, "client");
211
287
  const cloudflareDir = join(options.outDir, "cloudflare");
288
+ const sourceAnalysis =
289
+ timingSink === undefined
290
+ ? await analyzeBuildRouteSources({
291
+ clientRouteInferenceCache: serverClientRouteInferenceCache,
292
+ files,
293
+ project,
294
+ projectRoot: project.projectRoot,
295
+ routes,
296
+ vitePlugins,
297
+ })
298
+ : await timeBuildPhase(timingSink, "analyzeSources", () =>
299
+ analyzeBuildRouteSources({
300
+ clientRouteInferenceCache: serverClientRouteInferenceCache,
301
+ files,
302
+ project,
303
+ projectRoot: project.projectRoot,
304
+ routes,
305
+ vitePlugins,
306
+ }),
307
+ );
212
308
 
213
- await validateProductionRoutes({ files, projectRoot: project.projectRoot, routes });
309
+ if (timingSink === undefined) {
310
+ await validateProductionRoutes({
311
+ clientRouteInferenceCache: serverClientRouteInferenceCache,
312
+ files,
313
+ project,
314
+ projectRoot: project.projectRoot,
315
+ routes,
316
+ sourceAnalysis,
317
+ serverTransformCache,
318
+ vitePlugins,
319
+ });
320
+ } else {
321
+ await timeBuildPhase(timingSink, "validate", () =>
322
+ validateProductionRoutes({
323
+ clientRouteInferenceCache: serverClientRouteInferenceCache,
324
+ files,
325
+ project,
326
+ projectRoot: project.projectRoot,
327
+ routes,
328
+ sourceAnalysis,
329
+ serverTransformCache,
330
+ vitePlugins,
331
+ }),
332
+ );
333
+ }
214
334
 
215
- await rm(options.outDir, { force: true, recursive: true });
216
- await mkdir(serverDir, { recursive: true });
217
- await mkdir(clientDir, { recursive: true });
218
- if (shouldBuildCloudflare) {
219
- await mkdir(cloudflareDir, { recursive: true });
220
- }
221
- await mkdir(join(clientDir, ".vite"), { recursive: true });
222
- await mkdir(join(clientDir, "assets", "routes"), { recursive: true });
223
- await copyPublicAssets(project.publicDir, join(clientDir, "public"));
224
- const appConventionPublicAssets = await copyAppFileConventionAssets(
225
- project.routesDir,
226
- clientDir,
227
- );
228
- await copyPublicAssets(project.publicDir, clientDir);
229
- const publicAssets = [
230
- ...new Set([...(await collectPublicAssetPaths(project.publicDir)), ...appConventionPublicAssets]),
231
- ].sort();
335
+ if (timingSink === undefined) {
336
+ await rm(options.outDir, { force: true, recursive: true });
337
+ await Promise.all([
338
+ mkdir(serverDir, { recursive: true }),
339
+ mkdir(clientDir, { recursive: true }),
340
+ ...(shouldBuildCloudflare ? [mkdir(cloudflareDir, { recursive: true })] : []),
341
+ mkdir(join(clientDir, ".vite"), { recursive: true }),
342
+ mkdir(join(clientDir, "assets", "routes"), { recursive: true }),
343
+ ]);
344
+ } else {
345
+ await timeBuildPhase(timingSink, "prepareOutput", async () => {
346
+ await rm(options.outDir, { force: true, recursive: true });
347
+ await Promise.all([
348
+ mkdir(serverDir, { recursive: true }),
349
+ mkdir(clientDir, { recursive: true }),
350
+ ...(shouldBuildCloudflare ? [mkdir(cloudflareDir, { recursive: true })] : []),
351
+ mkdir(join(clientDir, ".vite"), { recursive: true }),
352
+ mkdir(join(clientDir, "assets", "routes"), { recursive: true }),
353
+ ]);
354
+ });
355
+ }
356
+
357
+ const publicAssets =
358
+ timingSink === undefined
359
+ ? await buildPublicAssetManifest(project, clientDir)
360
+ : await timeBuildPhase(timingSink, "publicAssets", () =>
361
+ buildPublicAssetManifest(project, clientDir),
362
+ );
232
363
 
233
- const serverActionManifest = await collectBuildServerActionManifest({
234
- files,
235
- projectRoot: project.projectRoot,
236
- routes,
237
- routesDir: project.routesDir,
238
- });
239
364
  const clientRouteInferenceCache = createClientRouteInferenceCache();
240
- const serverModules = await buildServerModuleArtifacts({
241
- clientRouteInferenceCache,
242
- files,
243
- prebundleServerComponents: buildTargets.includes("node") || shouldBuildAwsLambda,
244
- project,
245
- projectRoot: project.projectRoot,
246
- routes,
247
- vitePlugins,
248
- });
249
- const generatedImportPolicy = await buildGeneratedImportPolicy({
250
- files,
251
- projectRoot: project.projectRoot,
252
- routes,
253
- routesDir: project.routesDir,
254
- });
255
- const serverModuleArtifacts = await writeServerModuleArtifactFiles(serverDir, serverModules);
365
+ const [serverActionManifest, serverModules, generatedImportPolicy] = await Promise.all([
366
+ timingSink === undefined
367
+ ? collectBuildServerActionManifest({
368
+ files,
369
+ projectRoot: project.projectRoot,
370
+ routes,
371
+ routesDir: project.routesDir,
372
+ })
373
+ : timeBuildPhase(timingSink, "serverActionManifest", () =>
374
+ collectBuildServerActionManifest({
375
+ files,
376
+ projectRoot: project.projectRoot,
377
+ routes,
378
+ routesDir: project.routesDir,
379
+ }),
380
+ ),
381
+ timingSink === undefined
382
+ ? buildServerModuleArtifacts({
383
+ bundleCache: new Map(),
384
+ clientRouteInferenceCache: serverClientRouteInferenceCache,
385
+ files,
386
+ prebundleServerComponents: buildTargets.includes("node") || shouldBuildAwsLambda,
387
+ project,
388
+ projectRoot: project.projectRoot,
389
+ routes,
390
+ sourceAnalysis,
391
+ serverTransformCache,
392
+ vitePlugins,
393
+ })
394
+ : timeBuildPhase(timingSink, "serverModules", () =>
395
+ buildServerModuleArtifacts({
396
+ bundleCache: new Map(),
397
+ clientRouteInferenceCache: serverClientRouteInferenceCache,
398
+ files,
399
+ prebundleServerComponents: buildTargets.includes("node") || shouldBuildAwsLambda,
400
+ project,
401
+ projectRoot: project.projectRoot,
402
+ routes,
403
+ sourceAnalysis,
404
+ serverTransformCache,
405
+ vitePlugins,
406
+ }),
407
+ ),
408
+ timingSink === undefined
409
+ ? buildGeneratedImportPolicy({
410
+ files,
411
+ projectRoot: project.projectRoot,
412
+ routes,
413
+ routesDir: project.routesDir,
414
+ })
415
+ : timeBuildPhase(timingSink, "importPolicy", () =>
416
+ buildGeneratedImportPolicy({
417
+ files,
418
+ projectRoot: project.projectRoot,
419
+ routes,
420
+ routesDir: project.routesDir,
421
+ }),
422
+ ),
423
+ ]);
256
424
  const serverRoutes = routes.map((route) => ({
257
425
  ...route,
258
426
  file: relative(project.projectRoot, route.file),
259
427
  }));
260
- const clientRoutes = await writeClientRouteBundles({
261
- appDir: project.routesDir,
262
- assetBaseUrl: project.assetBaseUrl,
263
- clientDir,
264
- clientConsolePureFunctions: project.clientConsolePureFunctions,
265
- clientRouteInferenceCache,
266
- projectRoot: project.projectRoot,
267
- routes,
268
- sourceMapDir: join(options.outDir, "source-maps", "client"),
269
- sourceMaps: project.clientSourceMaps,
270
- vitePlugins,
271
- });
428
+ const [serverModuleArtifacts, clientRoutes] = await Promise.all([
429
+ timingSink === undefined
430
+ ? writeServerModuleArtifactFiles(serverDir, serverModules, generatedImportPolicy.runtimePackages)
431
+ : timeBuildPhase(timingSink, "serverModuleArtifacts", () =>
432
+ writeServerModuleArtifactFiles(
433
+ serverDir,
434
+ serverModules,
435
+ generatedImportPolicy.runtimePackages,
436
+ ),
437
+ ),
438
+ timingSink === undefined
439
+ ? writeClientRouteBundles({
440
+ appDir: project.routesDir,
441
+ assetBaseUrl: project.assetBaseUrl,
442
+ clientDir,
443
+ clientConsolePureFunctions: project.clientConsolePureFunctions,
444
+ clientRouteInferenceCache,
445
+ projectRoot: project.projectRoot,
446
+ routes,
447
+ sourceAnalysis,
448
+ sourceMapDir: join(options.outDir, "source-maps", "client"),
449
+ sourceMaps: project.clientSourceMaps,
450
+ vitePlugins,
451
+ })
452
+ : timeBuildPhase(timingSink, "clientBundles", () =>
453
+ writeClientRouteBundles({
454
+ appDir: project.routesDir,
455
+ assetBaseUrl: project.assetBaseUrl,
456
+ clientDir,
457
+ clientConsolePureFunctions: project.clientConsolePureFunctions,
458
+ clientRouteInferenceCache,
459
+ projectRoot: project.projectRoot,
460
+ routes,
461
+ sourceAnalysis,
462
+ sourceMapDir: join(options.outDir, "source-maps", "client"),
463
+ sourceMaps: project.clientSourceMaps,
464
+ vitePlugins,
465
+ }),
466
+ ),
467
+ ]);
272
468
  const navigationRuntimeScript = clientRoutes.some(
273
469
  (route) => route.navigation === true && !route.client,
274
470
  )
275
- ? await writeNavigationRuntimeBundle(clientDir, project.clientConsolePureFunctions)
471
+ ? timingSink === undefined
472
+ ? await writeNavigationRuntimeBundle(clientDir, project.clientConsolePureFunctions)
473
+ : await timeBuildPhase(timingSink, "navigationRuntime", () =>
474
+ writeNavigationRuntimeBundle(clientDir, project.clientConsolePureFunctions),
475
+ )
276
476
  : undefined;
277
477
  const clientManifestRoutes =
278
478
  navigationRuntimeScript === undefined
@@ -282,27 +482,58 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
282
482
  ? { ...route, navigationScript: navigationRuntimeScript }
283
483
  : route,
284
484
  );
285
- const prerenderedRoutes = await prerenderStaticRoutes({
286
- appDir: project.routesDir,
287
- assetBaseUrl: project.assetBaseUrl,
288
- clientRoutes: clientManifestRoutes,
289
- project,
290
- routes,
291
- serverModules,
292
- vitePlugins,
293
- });
485
+ const prerenderedRoutes =
486
+ timingSink === undefined
487
+ ? await prerenderStaticRoutes({
488
+ appDir: project.routesDir,
489
+ assetBaseUrl: project.assetBaseUrl,
490
+ clientRoutes: clientManifestRoutes,
491
+ project,
492
+ routes,
493
+ serverModules,
494
+ sourceAnalysis,
495
+ vitePlugins,
496
+ })
497
+ : await timeBuildPhase(timingSink, "prerender", () =>
498
+ prerenderStaticRoutes({
499
+ appDir: project.routesDir,
500
+ assetBaseUrl: project.assetBaseUrl,
501
+ clientRoutes: clientManifestRoutes,
502
+ project,
503
+ routes,
504
+ serverModules,
505
+ sourceAnalysis,
506
+ vitePlugins,
507
+ }),
508
+ );
294
509
  let cloudflareRouteModules: CloudflareRouteModulesOutput | undefined;
295
510
  if (shouldBuildCloudflare) {
296
- cloudflareRouteModules = await writeCloudflareRouteModules({
297
- cloudflareDir,
298
- files,
299
- prerenderedRoutes,
300
- projectRoot: project.projectRoot,
301
- routesDir: project.routesDir,
302
- routes,
303
- serverModules,
304
- vitePlugins,
305
- });
511
+ cloudflareRouteModules =
512
+ timingSink === undefined
513
+ ? await writeCloudflareRouteModules({
514
+ cloudflareDir,
515
+ files,
516
+ prerenderedRoutes,
517
+ projectRoot: project.projectRoot,
518
+ routesDir: project.routesDir,
519
+ routes,
520
+ serverModules,
521
+ sourceAnalysis,
522
+ vitePlugins,
523
+ })
524
+ : await timeBuildPhase(timingSink, "cloudflare", () =>
525
+ writeCloudflareRouteModules({
526
+ cloudflareDir,
527
+ files,
528
+ prerenderedRoutes,
529
+ projectRoot: project.projectRoot,
530
+ routesDir: project.routesDir,
531
+ routes,
532
+ serverModules,
533
+ sourceAnalysis,
534
+ vitePlugins,
535
+ }),
536
+ );
306
537
  }
307
538
 
308
539
  const routeServerActionReferences = Object.fromEntries(
@@ -343,40 +574,246 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
343
574
  ...(publicAssets.length === 0 ? {} : { publicAssets }),
344
575
  routes: clientManifestRoutes,
345
576
  };
346
- await writeFile(
347
- join(serverDir, "manifest.json"),
348
- JSON.stringify(serverManifest, null, 2),
349
- );
350
- await writeFile(
351
- join(serverDir, "import-policy.json"),
352
- JSON.stringify(generatedImportPolicy, null, 2),
353
- );
354
- await writeFile(
355
- join(clientDir, "manifest.json"),
356
- JSON.stringify(clientManifest, null, 2),
357
- );
358
- await writeFile(
359
- join(clientDir, ".vite", "manifest.json"),
360
- JSON.stringify(viteManifestFromClientRoutes(clientManifestRoutes), null, 2),
361
- );
362
- if (shouldBuildAwsLambda) {
363
- await writeAwsLambdaHandlerArtifact(options.outDir);
577
+ const writeManifestFiles = async () => {
578
+ await Promise.all([
579
+ writeFile(join(serverDir, "manifest.json"), JSON.stringify(serverManifest, null, 2)),
580
+ writeFile(
581
+ join(serverDir, "import-policy.json"),
582
+ JSON.stringify(generatedImportPolicy, null, 2),
583
+ ),
584
+ writeFile(join(clientDir, "manifest.json"), JSON.stringify(clientManifest, null, 2)),
585
+ writeFile(
586
+ join(clientDir, ".vite", "manifest.json"),
587
+ JSON.stringify(viteManifestFromClientRoutes(clientManifestRoutes), null, 2),
588
+ ),
589
+ ]);
590
+ };
591
+ if (timingSink === undefined) {
592
+ await writeManifestFiles();
593
+ } else {
594
+ await timeBuildPhase(timingSink, "writeManifests", writeManifestFiles);
364
595
  }
365
- if (shouldBuildCloudflare) {
366
- await writeCloudflareWorkerArtifact({
367
- cloudflareDir,
368
- clientManifest,
369
- modulesFile: cloudflareRouteModules?.registryFile ?? "route-modules.mjs",
370
- serverManifest,
596
+
597
+ if (timingSink === undefined) {
598
+ await Promise.all([
599
+ ...(shouldBuildAwsLambda ? [writeAwsLambdaHandlerArtifact(options.outDir)] : []),
600
+ ...(shouldBuildCloudflare
601
+ ? [
602
+ writeCloudflareWorkerArtifact({
603
+ cloudflareDir,
604
+ clientManifest,
605
+ modulesFile: cloudflareRouteModules?.registryFile ?? "route-modules.mjs",
606
+ serverManifest,
607
+ }),
608
+ ]
609
+ : []),
610
+ ]);
611
+ } else {
612
+ await timeBuildPhase(timingSink, "adapterArtifacts", async () => {
613
+ await Promise.all([
614
+ ...(shouldBuildAwsLambda ? [writeAwsLambdaHandlerArtifact(options.outDir)] : []),
615
+ ...(shouldBuildCloudflare
616
+ ? [
617
+ writeCloudflareWorkerArtifact({
618
+ cloudflareDir,
619
+ clientManifest,
620
+ modulesFile: cloudflareRouteModules?.registryFile ?? "route-modules.mjs",
621
+ serverManifest,
622
+ }),
623
+ ]
624
+ : []),
625
+ ]);
371
626
  });
372
627
  }
373
628
 
374
629
  return { routes };
375
630
  }
376
631
 
632
+ async function timeBuildPhase<T>(
633
+ sink: (timing: BuildAppPhaseTiming) => void,
634
+ phase: BuildAppPhase,
635
+ run: () => Promise<T>,
636
+ ): Promise<T> {
637
+ const startedAt = performance.now();
638
+
639
+ try {
640
+ return await run();
641
+ } finally {
642
+ sink({
643
+ ms: roundBuildPhaseMs(performance.now() - startedAt),
644
+ phase,
645
+ });
646
+ }
647
+ }
648
+
649
+ function roundBuildPhaseMs(value: number): number {
650
+ return Math.round(value * 100) / 100;
651
+ }
652
+
653
+ async function mapWithBuildConcurrency<T, R>(
654
+ items: readonly T[],
655
+ map: (item: T, index: number) => Promise<R>,
656
+ concurrency = defaultBuildConcurrency,
657
+ ): Promise<R[]> {
658
+ if (items.length === 0) {
659
+ return [];
660
+ }
661
+
662
+ const workerCount = Math.max(1, Math.min(concurrency, items.length));
663
+ const results: R[] = [];
664
+ results.length = items.length;
665
+ let nextIndex = 0;
666
+
667
+ async function runWorker(): Promise<void> {
668
+ while (nextIndex < items.length) {
669
+ const index = nextIndex;
670
+ nextIndex += 1;
671
+ results[index] = await map(items[index] as T, index);
672
+ }
673
+ }
674
+
675
+ await Promise.all(
676
+ Array.from({ length: workerCount }, async () => {
677
+ await runWorker();
678
+ }),
679
+ );
680
+
681
+ return results;
682
+ }
683
+
684
+ export async function __mapWithBuildConcurrencyForTests<T, R>(
685
+ items: readonly T[],
686
+ map: (item: T, index: number) => Promise<R>,
687
+ concurrency?: number,
688
+ ): Promise<R[]> {
689
+ return await mapWithBuildConcurrency(items, map, concurrency);
690
+ }
691
+
692
+ async function analyzeBuildRouteSources(options: {
693
+ clientRouteInferenceCache: ClientRouteInferenceCache;
694
+ files: Record<string, string>;
695
+ project: ResolvedAppRouterProject;
696
+ projectRoot: string;
697
+ routes: readonly AppRoute[];
698
+ vitePlugins?: readonly PluginOption[] | undefined;
699
+ }): Promise<BuildSourceAnalysisScope> {
700
+ const byFile = new Map<string, BuildSourceAnalysis>();
701
+
702
+ for (const [file, source] of Object.entries(options.files)) {
703
+ byFile.set(file, analyzeBuildSource(source, join(options.projectRoot, file)));
704
+ }
705
+
706
+ const routeAnalyses = await mapWithBuildConcurrency(
707
+ options.routes.filter((route): route is AppRoute & { kind: "page" } => route.kind === "page"),
708
+ async (route) => {
709
+ const file = relative(options.projectRoot, route.file).split(sep).join("/");
710
+ const source = options.files[file];
711
+
712
+ if (source === undefined) {
713
+ return undefined;
714
+ }
715
+
716
+ const routeCode = stripRouteBuildExports(source, route.file);
717
+ const clientInference = await inferClientRouteModule({
718
+ appDir: options.project.routesDir,
719
+ cache: options.clientRouteInferenceCache,
720
+ code: stripRouteClientSource({ code: source, filename: route.file }),
721
+ filename: route.file,
722
+ routePath: route.path,
723
+ vitePlugins: options.vitePlugins,
724
+ });
725
+
726
+ for (const diagnostic of clientInference.diagnostics) {
727
+ console.warn(formatClientRouteInferenceDiagnostic(diagnostic));
728
+ }
729
+
730
+ return [
731
+ file,
732
+ {
733
+ ...analyzeBuildSource(source, route.file),
734
+ clientBoundaryImports: clientInference.clientBoundaryImports,
735
+ clientRoute: clientInference.client,
736
+ file,
737
+ route,
738
+ routeCode,
739
+ streamRoute: shouldBuildRouteAsStream({
740
+ filename: file,
741
+ files: options.files,
742
+ projectRoot: options.projectRoot,
743
+ source,
744
+ }),
745
+ },
746
+ ] as const;
747
+ },
748
+ );
749
+ const byRouteFile = new Map<string, BuildRouteSourceAnalysis>();
750
+
751
+ for (const entry of routeAnalyses) {
752
+ if (entry !== undefined) {
753
+ byRouteFile.set(entry[0], entry[1]);
754
+ }
755
+ }
756
+
757
+ return { byFile, byRouteFile };
758
+ }
759
+
760
+ function analyzeBuildSource(source: string, filename: string): BuildSourceAnalysis {
761
+ const cachePolicy = routeCachePolicyFromSource(source);
762
+ const sourceHash = hashText(source);
763
+
764
+ if (!isSourceModuleFile(filename)) {
765
+ return {
766
+ authIncludesClaims: false,
767
+ ...(cachePolicy === undefined ? {} : { cachePolicy }),
768
+ hasGenerateStaticParams: false,
769
+ hasLoader: false,
770
+ hasMetadata: false,
771
+ hasPrerender: false,
772
+ source,
773
+ sourceHash,
774
+ usesRuntimeCacheControl: usesRuntimeCacheControl(source),
775
+ };
776
+ }
777
+
778
+ return {
779
+ authIncludesClaims: authIncludesClaims(source),
780
+ ...(cachePolicy === undefined ? {} : { cachePolicy }),
781
+ hasGenerateStaticParams: hasGenerateStaticParamsExport(source, filename),
782
+ hasLoader: hasLoaderExport(source, filename),
783
+ hasMetadata: hasMetadataExport(source),
784
+ hasPrerender: hasPrerenderExport(source, filename),
785
+ source,
786
+ sourceHash,
787
+ usesRuntimeCacheControl: usesRuntimeCacheControl(source),
788
+ };
789
+ }
790
+
791
+ function buildSourceAnalysisForFile(
792
+ sourceAnalysis: BuildSourceAnalysisScope,
793
+ projectRoot: string,
794
+ file: string,
795
+ ): BuildSourceAnalysis | undefined {
796
+ return sourceAnalysis.byFile.get(relative(projectRoot, file).split(sep).join("/"));
797
+ }
798
+
799
+ async function buildPublicAssetManifest(
800
+ project: ResolvedAppRouterProject,
801
+ clientDir: string,
802
+ ): Promise<string[]> {
803
+ const [publicAssetPaths, appConventionPublicAssets] = await Promise.all([
804
+ collectPublicAssetPaths(project.publicDir),
805
+ copyAppFileConventionAssets(project.routesDir, clientDir),
806
+ copyPublicAssets(project.publicDir, join(clientDir, "public")),
807
+ copyPublicAssets(project.publicDir, clientDir),
808
+ ]);
809
+
810
+ return [...new Set([...publicAssetPaths, ...appConventionPublicAssets])].sort();
811
+ }
812
+
377
813
  async function writeServerModuleArtifactFiles(
378
814
  serverDir: string,
379
815
  serverModules: Record<string, BuiltServerModuleArtifact>,
816
+ portableRuntimePackages: readonly string[] = [],
380
817
  ): Promise<{
381
818
  files: Record<string, string>;
382
819
  renderFiles: Record<string, string>;
@@ -386,48 +823,94 @@ async function writeServerModuleArtifactFiles(
386
823
  const renderFiles: Record<string, string> = {};
387
824
  const requestFiles: Record<string, string> = {};
388
825
  const modulesDir = join(serverDir, "server-modules");
826
+ const artifactEntries = Object.entries(serverModules);
827
+ type WrittenServerModuleArtifact =
828
+ | { artifactFile: string; file: string }
829
+ | { file: string; renderArtifactFile: string; requestArtifactFile: string };
830
+
831
+ await Promise.all([
832
+ mkdir(modulesDir, { recursive: true }),
833
+ mkdir(join(modulesDir, "code"), { recursive: true }),
834
+ mkdir(join(modulesDir, "request"), { recursive: true }),
835
+ mkdir(join(modulesDir, "render"), { recursive: true }),
836
+ ]);
837
+
838
+ const writtenArtifacts = await mapWithBuildConcurrency<
839
+ [string, BuiltServerModuleArtifact],
840
+ WrittenServerModuleArtifact
841
+ >(
842
+ artifactEntries,
843
+ async ([file, artifact]) => {
844
+ const externalized = await externalizeServerModuleArtifactCode(
845
+ serverDir,
846
+ artifact,
847
+ portableRuntimePackages,
848
+ );
849
+ const requestArtifact = requestServerModuleArtifact(externalized);
850
+ const renderArtifact = renderServerModuleArtifact(externalized);
851
+
852
+ if (Object.keys(requestArtifact).length > 0 && Object.keys(renderArtifact).length > 0) {
853
+ const requestJson = JSON.stringify(requestArtifact);
854
+ const requestArtifactFile = `server-modules/request/${hashText(`${file}\0request\0${requestJson}`).slice(0, 16)}.json`;
855
+ await writeFile(join(serverDir, requestArtifactFile), requestJson);
856
+
857
+ const renderJson = JSON.stringify(renderArtifact);
858
+ const renderArtifactFile = `server-modules/render/${hashText(`${file}\0render\0${renderJson}`).slice(0, 16)}.json`;
859
+ await writeFile(join(serverDir, renderArtifactFile), renderJson);
860
+
861
+ return { file, renderArtifactFile, requestArtifactFile };
862
+ }
863
+
864
+ const json = JSON.stringify(externalized);
865
+ const artifactFile = `server-modules/${hashText(`${file}\0${json}`).slice(0, 16)}.json`;
389
866
 
390
- for (const [file, artifact] of Object.entries(serverModules)) {
391
- const externalized = await externalizeServerModuleArtifactCode(serverDir, artifact);
392
- const requestArtifact = requestServerModuleArtifact(externalized);
393
- const renderArtifact = renderServerModuleArtifact(externalized);
394
-
395
- await mkdir(modulesDir, { recursive: true });
396
-
397
- if (Object.keys(requestArtifact).length > 0 && Object.keys(renderArtifact).length > 0) {
398
- const requestJson = JSON.stringify(requestArtifact);
399
- const requestArtifactFile = `server-modules/request/${hashText(`${file}\0request\0${requestJson}`).slice(0, 16)}.json`;
400
- await mkdir(join(modulesDir, "request"), { recursive: true });
401
- await writeFile(join(serverDir, requestArtifactFile), requestJson);
402
- requestFiles[file] = requestArtifactFile;
403
-
404
- const renderJson = JSON.stringify(renderArtifact);
405
- const renderArtifactFile = `server-modules/render/${hashText(`${file}\0render\0${renderJson}`).slice(0, 16)}.json`;
406
- await mkdir(join(modulesDir, "render"), { recursive: true });
407
- await writeFile(join(serverDir, renderArtifactFile), renderJson);
408
- renderFiles[file] = renderArtifactFile;
867
+ await writeFile(join(serverDir, artifactFile), json);
868
+ return { artifactFile, file };
869
+ },
870
+ serverArtifactFilesystemConcurrency,
871
+ );
872
+
873
+ for (const artifact of writtenArtifacts) {
874
+ if ("requestArtifactFile" in artifact) {
875
+ requestFiles[artifact.file] = artifact.requestArtifactFile;
876
+ renderFiles[artifact.file] = artifact.renderArtifactFile;
409
877
  continue;
410
878
  }
411
879
 
412
- const json = JSON.stringify(externalized);
413
- const artifactFile = `server-modules/${hashText(`${file}\0${json}`).slice(0, 16)}.json`;
414
-
415
- await writeFile(join(serverDir, artifactFile), json);
416
- files[file] = artifactFile;
880
+ files[artifact.file] = artifact.artifactFile;
417
881
  }
418
882
 
419
883
  return { files, renderFiles, requestFiles };
420
884
  }
421
885
 
886
+ export async function __writeServerModuleArtifactFilesForTests(
887
+ serverDir: string,
888
+ serverModules: Record<string, BuiltServerModuleArtifact>,
889
+ ): Promise<{
890
+ files: Record<string, string>;
891
+ renderFiles: Record<string, string>;
892
+ requestFiles: Record<string, string>;
893
+ }> {
894
+ return await writeServerModuleArtifactFiles(serverDir, serverModules);
895
+ }
896
+
422
897
  async function externalizeServerModuleArtifactCode(
423
898
  serverDir: string,
424
899
  artifact: BuiltServerModuleArtifact,
900
+ portableRuntimePackages: readonly string[],
425
901
  ): Promise<BuiltServerModuleArtifact> {
426
902
  return {
427
903
  ...(artifact.analysis === undefined ? {} : { analysis: artifact.analysis }),
428
904
  ...(artifact.loader === undefined
429
905
  ? {}
430
- : { loader: await externalizeServerModuleOutputCode(serverDir, artifact.loader, "code") }),
906
+ : {
907
+ loader: await externalizeServerModuleOutputCode(
908
+ serverDir,
909
+ artifact.loader,
910
+ "code",
911
+ portableRuntimePackages,
912
+ ),
913
+ }),
431
914
  ...(artifact.routeMetadata === undefined
432
915
  ? {}
433
916
  : {
@@ -435,17 +918,39 @@ async function externalizeServerModuleArtifactCode(
435
918
  serverDir,
436
919
  artifact.routeMetadata,
437
920
  "code",
921
+ portableRuntimePackages,
438
922
  ),
439
923
  }),
440
924
  ...(artifact.request === undefined
441
925
  ? {}
442
- : { request: await externalizeServerModuleOutputCode(serverDir, artifact.request, "code") }),
926
+ : {
927
+ request: await externalizeServerModuleOutputCode(
928
+ serverDir,
929
+ artifact.request,
930
+ "code",
931
+ portableRuntimePackages,
932
+ ),
933
+ }),
443
934
  ...(artifact.stream === undefined
444
935
  ? {}
445
- : { stream: await externalizeServerModuleOutputCode(serverDir, artifact.stream, "bundle") }),
936
+ : {
937
+ stream: await externalizeServerModuleOutputCode(
938
+ serverDir,
939
+ artifact.stream,
940
+ "bundle",
941
+ portableRuntimePackages,
942
+ ),
943
+ }),
446
944
  ...(artifact.string === undefined
447
945
  ? {}
448
- : { string: await externalizeServerModuleOutputCode(serverDir, artifact.string, "bundle") }),
946
+ : {
947
+ string: await externalizeServerModuleOutputCode(
948
+ serverDir,
949
+ artifact.string,
950
+ "bundle",
951
+ portableRuntimePackages,
952
+ ),
953
+ }),
449
954
  };
450
955
  }
451
956
 
@@ -453,15 +958,19 @@ async function externalizeServerModuleOutputCode(
453
958
  serverDir: string,
454
959
  output: BuiltServerModuleOutput,
455
960
  kind: "bundle" | "code",
961
+ portableRuntimePackages: readonly string[],
456
962
  ): Promise<BuiltServerModuleOutput> {
457
- const moduleCode = kind === "bundle" ? output.bundleCode : output.code;
963
+ const sourceCode = kind === "bundle" ? output.bundleCode : output.code;
964
+ const moduleCode =
965
+ sourceCode === undefined
966
+ ? undefined
967
+ : rewritePortableRuntimePackageImports(sourceCode, portableRuntimePackages);
458
968
 
459
969
  if (moduleCode === undefined || moduleCode.length === 0) {
460
970
  return output;
461
971
  }
462
972
 
463
973
  const moduleFile = `server-modules/code/${hashText(moduleCode).slice(0, 16)}.mjs`;
464
- await mkdir(join(serverDir, "server-modules", "code"), { recursive: true });
465
974
  await writeFile(join(serverDir, moduleFile), moduleCode);
466
975
 
467
976
  return {
@@ -472,6 +981,42 @@ async function externalizeServerModuleOutputCode(
472
981
  };
473
982
  }
474
983
 
984
+ function rewritePortableRuntimePackageImports(
985
+ code: string,
986
+ runtimePackages: readonly string[],
987
+ ): string {
988
+ if (runtimePackages.length === 0 || !code.includes("file://")) {
989
+ return code;
990
+ }
991
+
992
+ return code.replace(
993
+ /\b(from\s+|import\s*)["'](?<specifier>file:\/\/[^"']+)["']/gu,
994
+ (match, prefix: string, specifier: string | undefined) => {
995
+ const packageName =
996
+ specifier === undefined ? undefined : runtimePackageForFileUrl(specifier, runtimePackages);
997
+
998
+ return packageName === undefined ? match : `${prefix}"${packageName}"`;
999
+ },
1000
+ );
1001
+ }
1002
+
1003
+ function runtimePackageForFileUrl(
1004
+ specifier: string,
1005
+ runtimePackages: readonly string[],
1006
+ ): string | undefined {
1007
+ let filePath: string;
1008
+
1009
+ try {
1010
+ filePath = fileURLToPath(specifier).split(sep).join("/");
1011
+ } catch {
1012
+ return undefined;
1013
+ }
1014
+
1015
+ return [...runtimePackages]
1016
+ .sort((left, right) => right.length - left.length)
1017
+ .find((packageName) => filePath.includes(`/node_modules/${packageName}/`));
1018
+ }
1019
+
475
1020
  function requestServerModuleArtifact(
476
1021
  artifact: BuiltServerModuleArtifact,
477
1022
  ): BuiltServerModuleArtifact {
@@ -982,6 +1527,7 @@ async function prerenderStaticRoutes(options: {
982
1527
  project: ResolvedAppRouterProject;
983
1528
  routes: readonly AppRoute[];
984
1529
  serverModules: Record<string, BuiltServerModuleArtifact>;
1530
+ sourceAnalysis: BuildSourceAnalysisScope;
985
1531
  vitePlugins?: readonly PluginOption[] | undefined;
986
1532
  }): Promise<Record<string, BuiltPrerenderedRoute>> {
987
1533
  const clientScripts = new Map(
@@ -1002,37 +1548,48 @@ async function prerenderStaticRoutes(options: {
1002
1548
  ]),
1003
1549
  );
1004
1550
 
1005
- for (const route of options.routes) {
1006
- if (route.kind !== "page") {
1007
- continue;
1008
- }
1551
+ const prerenderedEntries = await mapWithBuildConcurrency(
1552
+ options.routes.filter((route): route is AppRoute & { kind: "page" } => route.kind === "page"),
1553
+ async (route) => {
1554
+ const routeFile = relative(options.project.projectRoot, route.file).split(sep).join("/");
1555
+ const analysis = options.sourceAnalysis.byRouteFile.get(routeFile);
1009
1556
 
1010
- const source = await readFile(route.file, "utf8");
1557
+ if (analysis === undefined || !analysis.hasPrerender) {
1558
+ return [] as Array<[string, BuiltPrerenderedRoute]>;
1559
+ }
1011
1560
 
1012
- if (!hasPrerenderExport(source)) {
1013
- continue;
1014
- }
1561
+ const entries: Array<[string, BuiltPrerenderedRoute]> = [];
1562
+ for (const pathname of await prerenderPathsForRoute(route, analysis, options.vitePlugins)) {
1563
+ const response = await renderAppRequest({
1564
+ appDir: options.appDir,
1565
+ assetBaseUrl: options.assetBaseUrl,
1566
+ clientScripts,
1567
+ importPolicy,
1568
+ request: new Request(`http://mreact.local${pathname}`),
1569
+ serverModules: serverModuleMap,
1570
+ vitePlugins: options.vitePlugins,
1571
+ });
1572
+ const headers: Record<string, string> = {};
1015
1573
 
1016
- for (const pathname of await prerenderPathsForRoute(route, source, options.vitePlugins)) {
1017
- const response = await renderAppRequest({
1018
- appDir: options.appDir,
1019
- assetBaseUrl: options.assetBaseUrl,
1020
- clientScripts,
1021
- importPolicy,
1022
- request: new Request(`http://mreact.local${pathname}`),
1023
- serverModules: serverModuleMap,
1024
- vitePlugins: options.vitePlugins,
1025
- });
1026
- const headers: Record<string, string> = {};
1574
+ response.headers.forEach((value, key) => {
1575
+ headers[key] = value;
1576
+ });
1577
+ entries.push([
1578
+ pathname,
1579
+ {
1580
+ headers,
1581
+ html: await response.text(),
1582
+ status: response.status,
1583
+ },
1584
+ ]);
1585
+ }
1586
+ return entries;
1587
+ },
1588
+ );
1027
1589
 
1028
- response.headers.forEach((value, key) => {
1029
- headers[key] = value;
1030
- });
1031
- prerendered[pathname] = {
1032
- headers,
1033
- html: await response.text(),
1034
- status: response.status,
1035
- };
1590
+ for (const entries of prerenderedEntries) {
1591
+ for (const [pathname, route] of entries) {
1592
+ prerendered[pathname] = route;
1036
1593
  }
1037
1594
  }
1038
1595
 
@@ -1041,21 +1598,21 @@ async function prerenderStaticRoutes(options: {
1041
1598
 
1042
1599
  async function prerenderPathsForRoute(
1043
1600
  route: AppRoute,
1044
- source: string,
1601
+ analysis: BuildRouteSourceAnalysis,
1045
1602
  vitePlugins: readonly PluginOption[] | undefined,
1046
1603
  ): Promise<string[]> {
1047
1604
  if (route.segments.every((segment) => segment.kind === "static")) {
1048
1605
  return [route.path];
1049
1606
  }
1050
1607
 
1051
- if (!hasGenerateStaticParamsExport(source)) {
1608
+ if (!analysis.hasGenerateStaticParams) {
1052
1609
  return [];
1053
1610
  }
1054
1611
 
1055
1612
  const module = await importAppRouterSourceModule<{
1056
1613
  generateStaticParams?: () => Iterable<StaticParams> | PromiseLike<Iterable<StaticParams>>;
1057
1614
  }>({
1058
- code: source,
1615
+ code: analysis.source,
1059
1616
  label: `generate-static-params:${route.file}`,
1060
1617
  resolveDir: dirname(route.file),
1061
1618
  sourcefile: route.file,
@@ -1095,12 +1652,16 @@ function routePathFromParams(route: AppRoute, params: StaticParams): string {
1095
1652
  }
1096
1653
 
1097
1654
  async function buildServerModuleArtifacts(options: {
1655
+ bundleCache: Map<string, Promise<RouterBundleOutput>>;
1656
+ cacheDir?: string | undefined;
1098
1657
  clientRouteInferenceCache: ClientRouteInferenceCache;
1099
1658
  files: Record<string, string>;
1100
1659
  prebundleServerComponents: boolean;
1101
1660
  project: ResolvedAppRouterProject;
1102
1661
  projectRoot: string;
1103
1662
  routes: readonly AppRoute[];
1663
+ sourceAnalysis: BuildSourceAnalysisScope;
1664
+ serverTransformCache: ServerTransformCache;
1104
1665
  vitePlugins?: readonly PluginOption[] | undefined;
1105
1666
  }): Promise<Record<string, BuiltServerModuleArtifact>> {
1106
1667
  const routeByFile = new Map(
@@ -1116,9 +1677,10 @@ async function buildServerModuleArtifacts(options: {
1116
1677
  } satisfies AppRouterImportPolicy;
1117
1678
  const artifacts: Record<string, BuiltServerModuleArtifact> = {};
1118
1679
 
1119
- for (const [file, source] of Object.entries(options.files)) {
1680
+ for (const file of Object.keys(options.files)) {
1120
1681
  const absoluteFile = join(options.projectRoot, file);
1121
1682
  const route = routeByFile.get(file);
1683
+ const analysis = options.sourceAnalysis.byFile.get(file);
1122
1684
 
1123
1685
  if (isMiddlewareFile(options.project.routesDir, absoluteFile)) {
1124
1686
  requestArtifactFiles.add(file);
@@ -1128,18 +1690,68 @@ async function buildServerModuleArtifacts(options: {
1128
1690
  requestArtifactFiles.add(file);
1129
1691
  }
1130
1692
 
1131
- if (route?.kind === "page" && hasLoaderExport(source)) {
1693
+ if (route?.kind === "page" && analysis?.hasLoader === true) {
1132
1694
  loaderArtifactFiles.add(file);
1133
1695
  }
1134
1696
 
1135
- if (hasMetadataExport(source)) {
1697
+ if (isServerComponentFile(file) && analysis?.hasMetadata === true) {
1136
1698
  metadataArtifactFiles.add(file);
1137
1699
  }
1138
1700
  }
1139
1701
 
1702
+ const requestBatchEntries: RouteRequestModuleBatchEntry[] = [];
1140
1703
  for (const [file, source] of Object.entries(options.files)) {
1141
1704
  const absoluteFile = join(options.projectRoot, file);
1142
1705
  const route = routeByFile.get(file);
1706
+
1707
+ if (loaderArtifactFiles.has(file)) {
1708
+ requestBatchEntries.push({
1709
+ code: stripRouteLoaderOnlyExports(source, absoluteFile),
1710
+ filename: absoluteFile,
1711
+ key: routeRequestArtifactBatchKey(file, "loader"),
1712
+ label: "Loader",
1713
+ });
1714
+ }
1715
+
1716
+ if (metadataArtifactFiles.has(file)) {
1717
+ requestBatchEntries.push({
1718
+ code: stripRouteMetadataOnlyExports(source, absoluteFile),
1719
+ filename: absoluteFile,
1720
+ key: routeRequestArtifactBatchKey(file, "metadata"),
1721
+ label: "Metadata",
1722
+ });
1723
+ }
1724
+
1725
+ if (
1726
+ requestArtifactFiles.has(file) &&
1727
+ !isMiddlewareFile(options.project.routesDir, absoluteFile) &&
1728
+ route?.kind !== "server" &&
1729
+ route?.kind !== "metadata"
1730
+ ) {
1731
+ requestBatchEntries.push({
1732
+ code: stripRouteRequestOnlyExports(source, absoluteFile),
1733
+ filename: absoluteFile,
1734
+ key: routeRequestArtifactBatchKey(file, "request"),
1735
+ label: "Loader",
1736
+ });
1737
+ }
1738
+ }
1739
+
1740
+ const requestBatchOutputs = requestBatchEntries.length >= 3
1741
+ ? await bundleRouteRequestModuleBatchCode({
1742
+ appDir: options.project.routesDir,
1743
+ bundleCache: options.bundleCache,
1744
+ cacheDir: options.cacheDir,
1745
+ entries: requestBatchEntries,
1746
+ importPolicy: requestModuleImportPolicy,
1747
+ vitePlugins: options.vitePlugins,
1748
+ })
1749
+ : new Map<string, string>();
1750
+
1751
+ const artifactEntries = await mapWithBuildConcurrency(Object.entries(options.files), async ([file, source]) => {
1752
+ const absoluteFile = join(options.projectRoot, file);
1753
+ const route = routeByFile.get(file);
1754
+ const routeAnalysis = options.sourceAnalysis.byRouteFile.get(file);
1143
1755
  const artifact: BuiltServerModuleArtifact = {};
1144
1756
 
1145
1757
  if (
@@ -1148,13 +1760,16 @@ async function buildServerModuleArtifacts(options: {
1148
1760
  metadataArtifactFiles.has(file)
1149
1761
  ) {
1150
1762
  if (loaderArtifactFiles.has(file)) {
1151
- const code = await bundleRouteLoaderModuleCode({
1152
- appDir: options.project.routesDir,
1153
- code: stripRouteLoaderOnlyExports(source),
1154
- filename: absoluteFile,
1155
- importPolicy: requestModuleImportPolicy,
1156
- vitePlugins: options.vitePlugins,
1157
- });
1763
+ const code = requestBatchOutputs.get(routeRequestArtifactBatchKey(file, "loader")) ??
1764
+ await bundleRouteLoaderModuleCode({
1765
+ appDir: options.project.routesDir,
1766
+ bundleCache: options.bundleCache,
1767
+ cacheDir: options.cacheDir,
1768
+ code: stripRouteLoaderOnlyExports(source, absoluteFile),
1769
+ filename: absoluteFile,
1770
+ importPolicy: requestModuleImportPolicy,
1771
+ vitePlugins: options.vitePlugins,
1772
+ });
1158
1773
  artifact.loader = {
1159
1774
  code,
1160
1775
  sourceHash: hashText(source),
@@ -1162,13 +1777,17 @@ async function buildServerModuleArtifacts(options: {
1162
1777
  }
1163
1778
 
1164
1779
  if (metadataArtifactFiles.has(file)) {
1165
- const code = await bundleRouteMetadataModuleCode({
1166
- appDir: options.project.routesDir,
1167
- code: stripRouteMetadataOnlyExports(source),
1168
- filename: absoluteFile,
1169
- importPolicy: requestModuleImportPolicy,
1170
- vitePlugins: options.vitePlugins,
1171
- });
1780
+ const code = requestBatchOutputs.get(routeRequestArtifactBatchKey(file, "metadata")) ??
1781
+ await bundleRouteRequestModuleCode({
1782
+ appDir: options.project.routesDir,
1783
+ bundleCache: options.bundleCache,
1784
+ cacheDir: options.cacheDir,
1785
+ code: stripRouteMetadataOnlyExports(source, absoluteFile),
1786
+ filename: absoluteFile,
1787
+ importPolicy: requestModuleImportPolicy,
1788
+ label: "Metadata",
1789
+ vitePlugins: options.vitePlugins,
1790
+ });
1172
1791
  artifact.routeMetadata = {
1173
1792
  code,
1174
1793
  sourceHash: hashText(source),
@@ -1176,117 +1795,152 @@ async function buildServerModuleArtifacts(options: {
1176
1795
  }
1177
1796
 
1178
1797
  if (requestArtifactFiles.has(file)) {
1798
+ const batchedRequestCode = requestBatchOutputs.get(
1799
+ routeRequestArtifactBatchKey(file, "request"),
1800
+ );
1179
1801
  artifact.request = {
1180
- code: await buildRequestModuleArtifactCode({
1181
- appDir: options.project.routesDir,
1182
- filename: absoluteFile,
1183
- importPolicy: requestModuleImportPolicy,
1184
- routeKind: route?.kind,
1185
- source,
1186
- vitePlugins: options.vitePlugins,
1187
- }),
1802
+ code: batchedRequestCode ??
1803
+ await buildRequestModuleArtifactCode({
1804
+ appDir: options.project.routesDir,
1805
+ bundleCache: options.bundleCache,
1806
+ cacheDir: options.cacheDir,
1807
+ filename: absoluteFile,
1808
+ importPolicy: requestModuleImportPolicy,
1809
+ routeKind: route?.kind,
1810
+ source,
1811
+ vitePlugins: options.vitePlugins,
1812
+ }),
1188
1813
  sourceHash: hashText(source),
1189
1814
  };
1190
1815
  }
1191
1816
  }
1192
1817
 
1193
1818
  if (!isServerComponentFile(file)) {
1194
- if (Object.keys(artifact).length > 0) {
1195
- artifacts[file] = artifact;
1196
- }
1197
- continue;
1819
+ return Object.keys(artifact).length > 0 ? ([file, artifact] as const) : undefined;
1198
1820
  }
1199
1821
 
1200
- const closureUsesAwait = shouldBuildRouteAsStream({
1201
- filename: file,
1202
- files: options.files,
1203
- projectRoot: options.projectRoot,
1204
- source,
1205
- });
1822
+ const closureUsesAwait = routeAnalysis?.streamRoute ??
1823
+ shouldBuildRouteAsStream({
1824
+ filename: file,
1825
+ files: options.files,
1826
+ projectRoot: options.projectRoot,
1827
+ source,
1828
+ });
1206
1829
  const streamRoute = route !== undefined && closureUsesAwait;
1207
1830
  const serverOutputs =
1208
1831
  streamRoute || (route === undefined && closureUsesAwait)
1209
1832
  ? (["stream", "string"] as const)
1210
1833
  : (["string"] as const);
1211
- const code = route === undefined ? source : stripRouteBuildExports(source);
1212
- const clientInference = await inferClientRouteModule({
1213
- ...(route === undefined ? {} : { appDir: options.project.routesDir }),
1214
- cache: options.clientRouteInferenceCache,
1215
- code:
1216
- route === undefined
1217
- ? stripRouteClientOnlyExports(source)
1218
- : stripRouteClientSource({ code: source, filename: route.file }),
1219
- filename: join(options.projectRoot, file),
1220
- ...(route === undefined ? {} : { routePath: route.path }),
1221
- vitePlugins: options.vitePlugins,
1222
- });
1223
- const clientBoundaryImports = clientInference.clientBoundaryImports;
1834
+ const code = routeAnalysis?.routeCode ??
1835
+ (route === undefined ? source : stripRouteBuildExports(source, absoluteFile));
1836
+ const clientInference = routeAnalysis === undefined
1837
+ ? await inferClientRouteModule({
1838
+ ...(route === undefined ? {} : { appDir: options.project.routesDir }),
1839
+ cache: options.clientRouteInferenceCache,
1840
+ code:
1841
+ route === undefined
1842
+ ? stripRouteClientOnlyExports(source, absoluteFile)
1843
+ : stripRouteClientSource({ code: source, filename: route.file }),
1844
+ filename: join(options.projectRoot, file),
1845
+ ...(route === undefined ? {} : { routePath: route.path }),
1846
+ vitePlugins: options.vitePlugins,
1847
+ })
1848
+ : undefined;
1849
+ const clientBoundaryImports = routeAnalysis?.clientBoundaryImports ??
1850
+ clientInference?.clientBoundaryImports ??
1851
+ [];
1224
1852
 
1225
- for (const diagnostic of clientInference.diagnostics) {
1853
+ for (const diagnostic of clientInference?.diagnostics ?? []) {
1226
1854
  console.warn(formatClientRouteInferenceDiagnostic(diagnostic));
1227
1855
  }
1228
1856
 
1229
- if (route?.kind === "page") {
1857
+ if (routeAnalysis !== undefined) {
1230
1858
  artifact.analysis = builtRouteSourceAnalysisSummary({
1231
- clientBoundaryImports,
1232
- clientRoute: clientInference.client,
1233
- route,
1234
- routeCode: code,
1235
- source,
1236
- streamRoute,
1859
+ analysis: routeAnalysis,
1237
1860
  });
1238
1861
  }
1239
1862
 
1240
- for (const serverOutput of serverOutputs) {
1241
- const output = transform({
1242
- code,
1243
- clientBoundaryImports,
1244
- dev: false,
1245
- filename: join(options.projectRoot, file),
1246
- serverEscape: nativeEscapeTransform,
1247
- serverOutput,
1248
- target: "server",
1249
- });
1250
- const fatalDiagnostics = output.diagnostics.filter(
1251
- (diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER",
1252
- );
1863
+ const serverOutputArtifacts = await mapWithBuildConcurrency(
1864
+ serverOutputs,
1865
+ async (serverOutput) => {
1866
+ const output = await transformServerRouteSource({
1867
+ cache: options.serverTransformCache,
1868
+ code,
1869
+ clientBoundaryImports,
1870
+ filename: join(options.projectRoot, file),
1871
+ moduleContextCache: options.clientRouteInferenceCache,
1872
+ serverOutput,
1873
+ });
1874
+ const fatalDiagnostics = output.diagnostics.filter(
1875
+ (diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER",
1876
+ );
1877
+
1878
+ if (fatalDiagnostics.length > 0) {
1879
+ if (
1880
+ serverOutput === "string" &&
1881
+ streamRoute &&
1882
+ route?.kind === "page" &&
1883
+ fatalDiagnostics.every(
1884
+ (diagnostic) => diagnostic.code === "MR_UNSUPPORTED_AWAIT_INNER_COMPONENT",
1885
+ )
1886
+ ) {
1887
+ return undefined;
1888
+ }
1889
+
1890
+ throw new Error(
1891
+ fatalDiagnostics.map((diagnostic) => formatDiagnostic(file, diagnostic)).join("\n"),
1892
+ );
1893
+ }
1253
1894
 
1254
- if (fatalDiagnostics.length > 0) {
1255
1895
  if (
1256
1896
  serverOutput === "string" &&
1257
1897
  streamRoute &&
1258
1898
  route?.kind === "page" &&
1259
- fatalDiagnostics.every(
1260
- (diagnostic) => diagnostic.code === "MR_UNSUPPORTED_AWAIT_INNER_COMPONENT",
1261
- )
1899
+ source.includes("<Await") &&
1900
+ /\bLink\b/.test(source)
1262
1901
  ) {
1263
- continue;
1902
+ // Native Link renders pre-rendered children as HTML. Keep direct
1903
+ // Await renderers on the stream artifact so the string artifact
1904
+ // cannot drop deferred Link content before the boundary resolves.
1905
+ return undefined;
1264
1906
  }
1265
1907
 
1266
- throw new Error(
1267
- fatalDiagnostics.map((diagnostic) => formatDiagnostic(file, diagnostic)).join("\n"),
1268
- );
1269
- }
1908
+ return [
1909
+ serverOutput,
1910
+ {
1911
+ ...(options.prebundleServerComponents
1912
+ ? {
1913
+ bundleCode: await buildServerComponentBundleArtifactCode({
1914
+ clientRouteInferenceCache: options.clientRouteInferenceCache,
1915
+ code: output.code,
1916
+ filename: absoluteFile,
1917
+ root: options.projectRoot,
1918
+ serverOutput,
1919
+ vitePlugins: options.vitePlugins,
1920
+ }),
1921
+ }
1922
+ : {}),
1923
+ code: output.code,
1924
+ metadata: output.metadata,
1925
+ sourceHash: hashText(code),
1926
+ },
1927
+ ] as const;
1928
+ },
1929
+ );
1270
1930
 
1271
- artifact[serverOutput] = {
1272
- ...(options.prebundleServerComponents
1273
- ? {
1274
- bundleCode: await buildServerComponentBundleArtifactCode({
1275
- clientRouteInferenceCache: options.clientRouteInferenceCache,
1276
- code: output.code,
1277
- filename: absoluteFile,
1278
- serverOutput,
1279
- vitePlugins: options.vitePlugins,
1280
- }),
1281
- }
1282
- : {}),
1283
- code: output.code,
1284
- metadata: output.metadata,
1285
- sourceHash: hashText(code),
1286
- };
1931
+ for (const entry of serverOutputArtifacts) {
1932
+ if (entry !== undefined) {
1933
+ artifact[entry[0]] = entry[1];
1934
+ }
1287
1935
  }
1288
1936
 
1289
- artifacts[file] = artifact;
1937
+ return [file, artifact] as const;
1938
+ });
1939
+
1940
+ for (const entry of artifactEntries) {
1941
+ if (entry !== undefined) {
1942
+ artifacts[entry[0]] = entry[1];
1943
+ }
1290
1944
  }
1291
1945
 
1292
1946
  return artifacts;
@@ -1296,6 +1950,7 @@ async function buildServerComponentBundleArtifactCode(options: {
1296
1950
  clientRouteInferenceCache: ClientRouteInferenceCache;
1297
1951
  code: string;
1298
1952
  filename: string;
1953
+ root?: string | undefined;
1299
1954
  serverOutput: ServerOutputMode;
1300
1955
  vitePlugins?: readonly PluginOption[] | undefined;
1301
1956
  }): Promise<string> {
@@ -1303,6 +1958,7 @@ async function buildServerComponentBundleArtifactCode(options: {
1303
1958
  code: options.code,
1304
1959
  label: `server-component:${options.filename}`,
1305
1960
  resolveDir: dirname(options.filename),
1961
+ root: options.root,
1306
1962
  serverSourceTransform: {
1307
1963
  clientRouteInferenceCache: options.clientRouteInferenceCache,
1308
1964
  dev: false,
@@ -1314,27 +1970,63 @@ async function buildServerComponentBundleArtifactCode(options: {
1314
1970
  });
1315
1971
  }
1316
1972
 
1317
- function builtRouteSourceAnalysisSummary(options: {
1973
+ async function transformServerRouteSource(options: {
1974
+ cache: ServerTransformCache;
1318
1975
  clientBoundaryImports: readonly string[];
1319
- clientRoute: boolean;
1320
- route: AppRoute;
1321
- routeCode: string;
1322
- source: string;
1323
- streamRoute: boolean;
1324
- }): BuiltRouteSourceAnalysisSummary {
1325
- const cachePolicy = routeCachePolicyFromSource(options.source);
1976
+ code: string;
1977
+ filename: string;
1978
+ moduleContextCache: ClientRouteInferenceCache;
1979
+ serverOutput: ServerOutputMode;
1980
+ }): Promise<ServerTransformOutput> {
1981
+ const cacheKey = stableCacheKey({
1982
+ clientBoundaryImports: options.clientBoundaryImports,
1983
+ codeHash: hashText(options.code),
1984
+ filename: resolve(options.filename),
1985
+ serverOutput: options.serverOutput,
1986
+ target: "server",
1987
+ });
1988
+ const cached = options.cache.get(cacheKey);
1989
+
1990
+ if (cached !== undefined) {
1991
+ return await cached;
1992
+ }
1993
+
1994
+ const transformed = Promise.resolve().then(async () => {
1995
+ const moduleContext = await compilerModuleContextForSource({
1996
+ cache: options.moduleContextCache,
1997
+ code: options.code,
1998
+ filename: options.filename,
1999
+ });
1326
2000
 
2001
+ return transformCompilerModuleContext({
2002
+ code: options.code,
2003
+ clientBoundaryImports: options.clientBoundaryImports,
2004
+ dev: false,
2005
+ filename: options.filename,
2006
+ moduleContext,
2007
+ serverEscape: nativeEscapeTransform,
2008
+ serverOutput: options.serverOutput,
2009
+ target: "server",
2010
+ });
2011
+ });
2012
+ options.cache.set(cacheKey, transformed);
2013
+ return await transformed;
2014
+ }
2015
+
2016
+ function builtRouteSourceAnalysisSummary(options: {
2017
+ analysis: BuildRouteSourceAnalysis;
2018
+ }): BuiltRouteSourceAnalysisSummary {
1327
2019
  return {
1328
- authIncludesClaims: authIncludesClaims(options.source),
1329
- ...(cachePolicy === undefined ? {} : { cachePolicy }),
1330
- clientBoundaryImports: options.clientBoundaryImports,
1331
- clientRoute: options.clientRoute,
1332
- hasLoader: hasLoaderExport(options.source),
1333
- routeCode: options.routeCode,
1334
- routePath: options.route.path,
1335
- sourceHash: hashText(options.source),
1336
- streamRoute: options.streamRoute,
1337
- usesRuntimeCacheControl: usesRuntimeCacheControl(options.source),
2020
+ authIncludesClaims: options.analysis.authIncludesClaims,
2021
+ ...(options.analysis.cachePolicy === undefined ? {} : { cachePolicy: options.analysis.cachePolicy }),
2022
+ clientBoundaryImports: options.analysis.clientBoundaryImports,
2023
+ clientRoute: options.analysis.clientRoute,
2024
+ hasLoader: options.analysis.hasLoader,
2025
+ routeCode: options.analysis.routeCode,
2026
+ routePath: options.analysis.route.path,
2027
+ sourceHash: options.analysis.sourceHash,
2028
+ streamRoute: options.analysis.streamRoute,
2029
+ usesRuntimeCacheControl: options.analysis.usesRuntimeCacheControl,
1338
2030
  };
1339
2031
  }
1340
2032
 
@@ -1345,7 +2037,10 @@ function shouldBuildRouteAsStream(options: {
1345
2037
  source: string;
1346
2038
  }): boolean {
1347
2039
  return (
1348
- isStreamRouteSource(options.source) ||
2040
+ isStreamRouteSource(
2041
+ options.source,
2042
+ sourceFilenameForBuildAnalysis(options.projectRoot, options.filename),
2043
+ ) ||
1349
2044
  routeClosureMayUseAwaitBoundary({
1350
2045
  filename: options.filename,
1351
2046
  files: options.files,
@@ -1355,6 +2050,10 @@ function shouldBuildRouteAsStream(options: {
1355
2050
  );
1356
2051
  }
1357
2052
 
2053
+ function sourceFilenameForBuildAnalysis(projectRoot: string, filename: string): string {
2054
+ return filename.startsWith("/") ? filename : join(projectRoot, filename);
2055
+ }
2056
+
1358
2057
  function resolveBuildLocalSourceImport(
1359
2058
  files: Record<string, string>,
1360
2059
  importer: string,
@@ -1379,12 +2078,18 @@ function usesRuntimeCacheControl(code: string): boolean {
1379
2078
  return /\bcacheControl\s*\(/.test(code);
1380
2079
  }
1381
2080
 
2081
+ function routeRequestArtifactBatchKey(file: string, kind: "loader" | "metadata" | "request"): string {
2082
+ return `${kind}:${file}`;
2083
+ }
2084
+
1382
2085
  function authIncludesClaims(code: string): boolean {
1383
2086
  return /\bexport\s+const\s+auth\s*=\s*["']include-claims["']\s*;?/.test(code);
1384
2087
  }
1385
2088
 
1386
2089
  async function buildRequestModuleArtifactCode(options: {
1387
2090
  appDir: string;
2091
+ bundleCache: Map<string, Promise<RouterBundleOutput>>;
2092
+ cacheDir?: string | undefined;
1388
2093
  filename: string;
1389
2094
  importPolicy: AppRouterImportPolicy;
1390
2095
  routeKind?: AppRoute["kind"] | undefined;
@@ -1401,53 +2106,161 @@ async function buildRequestModuleArtifactCode(options: {
1401
2106
  });
1402
2107
  }
1403
2108
 
1404
- if (options.routeKind === "server" || options.routeKind === "metadata") {
1405
- return await bundleAppRouterSourceModule({
1406
- code: options.source,
1407
- label: `server-route:${options.filename}`,
1408
- resolveDir: dirname(options.filename),
1409
- sourcefile: options.filename,
1410
- vitePlugins: options.vitePlugins,
1411
- });
1412
- }
2109
+ if (options.routeKind === "server" || options.routeKind === "metadata") {
2110
+ return await bundleAppRouterSourceModule({
2111
+ code: options.source,
2112
+ label: `server-route:${options.filename}`,
2113
+ plugins: [fileImportMetaUrlPlugin()],
2114
+ resolveDir: dirname(options.filename),
2115
+ root: options.importPolicy.projectRoot,
2116
+ sourcefile: options.filename,
2117
+ vitePlugins: options.vitePlugins,
2118
+ });
2119
+ }
2120
+
2121
+ return await bundleRouteLoaderModuleCode({
2122
+ appDir: options.appDir,
2123
+ bundleCache: options.bundleCache,
2124
+ cacheDir: options.cacheDir,
2125
+ code: stripRouteRequestOnlyExports(options.source, options.filename),
2126
+ filename: options.filename,
2127
+ importPolicy: options.importPolicy,
2128
+ vitePlugins: options.vitePlugins,
2129
+ });
2130
+ }
2131
+
2132
+ async function bundleRouteLoaderModuleCode(options: {
2133
+ appDir: string;
2134
+ bundleCache?: Map<string, Promise<RouterBundleOutput>> | undefined;
2135
+ cacheDir?: string | undefined;
2136
+ code: string;
2137
+ filename: string;
2138
+ importPolicy?: AppRouterImportPolicy | undefined;
2139
+ vitePlugins?: readonly PluginOption[] | undefined;
2140
+ }): Promise<string> {
2141
+ return await bundleRouteRequestModuleCode({
2142
+ ...options,
2143
+ label: "Loader",
2144
+ });
2145
+ }
2146
+
2147
+ async function bundleRouteRequestModuleBatchCode(options: {
2148
+ appDir: string;
2149
+ bundleCache?: Map<string, Promise<RouterBundleOutput>> | undefined;
2150
+ cacheDir?: string | undefined;
2151
+ entries: readonly RouteRequestModuleBatchEntry[];
2152
+ importPolicy?: AppRouterImportPolicy | undefined;
2153
+ vitePlugins?: readonly PluginOption[] | undefined;
2154
+ }): Promise<Map<string, string>> {
2155
+ if (options.entries.length === 0) {
2156
+ return new Map();
2157
+ }
2158
+
2159
+ if (options.entries.length === 1) {
2160
+ const entry = options.entries[0];
2161
+ if (entry === undefined) {
2162
+ return new Map();
2163
+ }
2164
+
2165
+ return new Map([
2166
+ [
2167
+ entry.key,
2168
+ await bundleRouteRequestModuleCode({
2169
+ appDir: options.appDir,
2170
+ bundleCache: options.bundleCache,
2171
+ cacheDir: options.cacheDir,
2172
+ code: entry.code,
2173
+ filename: entry.filename,
2174
+ importPolicy: options.importPolicy,
2175
+ label: entry.label,
2176
+ vitePlugins: options.vitePlugins,
2177
+ }),
2178
+ ],
2179
+ ]);
2180
+ }
2181
+
2182
+ const namesByKey = new Map(
2183
+ options.entries.map((entry, index) => [
2184
+ entry.key,
2185
+ `request-${index}-${hashText(entry.key).slice(0, 8)}`,
2186
+ ]),
2187
+ );
2188
+ const output = await bundleRouterModules({
2189
+ cacheDir: options.cacheDir,
2190
+ chunkFileNames: "request/chunks/[name].[hash].js",
2191
+ entries: options.entries.map((entry) => ({
2192
+ code: entry.code,
2193
+ filename: entry.filename,
2194
+ name: namesByKey.get(entry.key) ?? hashText(entry.key).slice(0, 8),
2195
+ })),
2196
+ entryFileNames: "request/[name].js",
2197
+ platform: "node",
2198
+ plugins: [
2199
+ fileImportMetaUrlPlugin(),
2200
+ createAppRouterImportPolicyPlugin({
2201
+ appDir: options.appDir,
2202
+ importPolicy: options.importPolicy,
2203
+ label: "Request artifact",
2204
+ }),
2205
+ ],
2206
+ root: options.importPolicy?.projectRoot ?? dirname(options.entries[0]?.filename ?? options.appDir),
2207
+ vitePlugins: options.vitePlugins,
2208
+ });
2209
+
2210
+ if (output.chunks.some((chunk) => !chunk.isEntry)) {
2211
+ const fallbackEntries = await mapWithBuildConcurrency(options.entries, async (entry) => [
2212
+ entry.key,
2213
+ await bundleRouteRequestModuleCode({
2214
+ appDir: options.appDir,
2215
+ bundleCache: options.bundleCache,
2216
+ cacheDir: options.cacheDir,
2217
+ code: entry.code,
2218
+ filename: entry.filename,
2219
+ importPolicy: options.importPolicy,
2220
+ label: entry.label,
2221
+ vitePlugins: options.vitePlugins,
2222
+ }),
2223
+ ] as const);
2224
+ return new Map(fallbackEntries);
2225
+ }
2226
+
2227
+ const chunksByName = new Map(output.chunks.map((chunk) => [chunk.name, chunk]));
2228
+ return new Map(
2229
+ options.entries.map((entry) => {
2230
+ const name = namesByKey.get(entry.key);
2231
+ const chunk = name === undefined ? undefined : chunksByName.get(name);
1413
2232
 
1414
- return await bundleRouteLoaderModuleCode({
1415
- appDir: options.appDir,
1416
- code: stripRouteRequestOnlyExports(options.source),
1417
- filename: options.filename,
1418
- importPolicy: options.importPolicy,
1419
- vitePlugins: options.vitePlugins,
1420
- });
1421
- }
2233
+ if (chunk === undefined) {
2234
+ throw new Error(`Failed to compile request artifact for ${entry.filename}.`);
2235
+ }
1422
2236
 
1423
- async function bundleRouteLoaderModuleCode(options: {
1424
- appDir: string;
1425
- code: string;
1426
- filename: string;
1427
- importPolicy?: AppRouterImportPolicy | undefined;
1428
- vitePlugins?: readonly PluginOption[] | undefined;
1429
- }): Promise<string> {
1430
- return await bundleRouteRequestModuleCode({
1431
- ...options,
1432
- label: "Loader",
1433
- });
2237
+ return [entry.key, chunk.code] as const;
2238
+ }),
2239
+ );
1434
2240
  }
1435
2241
 
1436
- async function bundleRouteMetadataModuleCode(options: {
2242
+ export async function __bundleRouteRequestModuleBatchForTests(options: {
1437
2243
  appDir: string;
1438
- code: string;
1439
- filename: string;
2244
+ cacheDir?: string | undefined;
2245
+ entries: readonly RouteRequestModuleBatchEntry[];
1440
2246
  importPolicy?: AppRouterImportPolicy | undefined;
1441
2247
  vitePlugins?: readonly PluginOption[] | undefined;
1442
- }): Promise<string> {
1443
- return await bundleRouteRequestModuleCode({
1444
- ...options,
1445
- label: "Metadata",
1446
- });
2248
+ }): Promise<Record<string, string>> {
2249
+ return Object.fromEntries(
2250
+ await bundleRouteRequestModuleBatchCode({
2251
+ appDir: options.appDir,
2252
+ cacheDir: options.cacheDir,
2253
+ entries: options.entries,
2254
+ importPolicy: options.importPolicy,
2255
+ vitePlugins: options.vitePlugins,
2256
+ }),
2257
+ );
1447
2258
  }
1448
2259
 
1449
2260
  async function bundleRouteRequestModuleCode(options: {
1450
2261
  appDir: string;
2262
+ bundleCache?: Map<string, Promise<RouterBundleOutput>> | undefined;
2263
+ cacheDir?: string | undefined;
1451
2264
  code: string;
1452
2265
  filename: string;
1453
2266
  importPolicy?: AppRouterImportPolicy | undefined;
@@ -1455,11 +2268,16 @@ async function bundleRouteRequestModuleCode(options: {
1455
2268
  vitePlugins?: readonly PluginOption[] | undefined;
1456
2269
  }): Promise<string> {
1457
2270
  const output = await bundleRouterModule({
2271
+ cache: options.bundleCache,
2272
+ cacheDir: options.cacheDir,
2273
+ cacheKey: routeRequestBundleCacheKey(options),
1458
2274
  code: options.code,
1459
2275
  filename: options.filename,
1460
2276
  platform: "node",
2277
+ root: options.importPolicy?.projectRoot,
1461
2278
  vitePlugins: options.vitePlugins,
1462
2279
  plugins: [
2280
+ fileImportMetaUrlPlugin(),
1463
2281
  createAppRouterImportPolicyPlugin({
1464
2282
  appDir: options.appDir,
1465
2283
  importPolicy: options.importPolicy,
@@ -1476,6 +2294,42 @@ async function bundleRouteRequestModuleCode(options: {
1476
2294
  return code;
1477
2295
  }
1478
2296
 
2297
+ function routeRequestBundleCacheKey(options: {
2298
+ appDir: string;
2299
+ code: string;
2300
+ filename: string;
2301
+ importPolicy?: AppRouterImportPolicy | undefined;
2302
+ label: "Loader" | "Metadata";
2303
+ vitePlugins?: readonly PluginOption[] | undefined;
2304
+ }): string {
2305
+ return stableCacheKey({
2306
+ appDir: resolve(options.appDir),
2307
+ codeHash: hashText(options.code),
2308
+ filename: resolve(options.filename),
2309
+ importPolicy:
2310
+ options.importPolicy === undefined
2311
+ ? undefined
2312
+ : {
2313
+ allowedPackages: [...(options.importPolicy.allowedPackages ?? [])].sort(),
2314
+ allowedSourceDirs: (options.importPolicy.allowedSourceDirs ?? [])
2315
+ .map((dir) => resolve(dir))
2316
+ .sort(),
2317
+ projectRoot:
2318
+ options.importPolicy.projectRoot === undefined
2319
+ ? undefined
2320
+ : resolve(options.importPolicy.projectRoot),
2321
+ },
2322
+ label: options.label,
2323
+ platform: "node",
2324
+ target: "es2022",
2325
+ vitePlugins: vitePluginsCacheKey(options.vitePlugins),
2326
+ });
2327
+ }
2328
+
2329
+ function stableCacheKey(value: unknown): string {
2330
+ return JSON.stringify(value);
2331
+ }
2332
+
1479
2333
  function isMiddlewareFile(appDir: string, file: string): boolean {
1480
2334
  return file === join(appDir, "middleware.ts") || file === join(appDir, "middleware.mreact.ts");
1481
2335
  }
@@ -1510,7 +2364,31 @@ interface CloudflareRouteModulesOutput {
1510
2364
  registryFile: string;
1511
2365
  }
1512
2366
 
2367
+ interface CloudflareRequiredRoute {
2368
+ route: AppRoute;
2369
+ routeFile: string;
2370
+ routeId: string;
2371
+ }
2372
+
2373
+ interface CloudflareBatchedRouteModule {
2374
+ code: string;
2375
+ fileName: string;
2376
+ }
2377
+
2378
+ interface CloudflareBatchedRouteModules {
2379
+ chunks: readonly RouterBundleChunkOutput[];
2380
+ entries: ReadonlyMap<string, CloudflareBatchedRouteModule>;
2381
+ }
2382
+
2383
+ interface RouteRequestModuleBatchEntry {
2384
+ code: string;
2385
+ filename: string;
2386
+ key: string;
2387
+ label: "Loader" | "Metadata";
2388
+ }
2389
+
1513
2390
  async function writeCloudflareRouteModules(options: {
2391
+ cacheDir?: string | undefined;
1514
2392
  cloudflareDir: string;
1515
2393
  files: Record<string, string>;
1516
2394
  prerenderedRoutes: Record<string, BuiltPrerenderedRoute>;
@@ -1518,32 +2396,66 @@ async function writeCloudflareRouteModules(options: {
1518
2396
  routesDir: string;
1519
2397
  routes: readonly AppRoute[];
1520
2398
  serverModules: Record<string, BuiltServerModuleArtifact>;
2399
+ sourceAnalysis: BuildSourceAnalysisScope;
1521
2400
  vitePlugins?: readonly PluginOption[] | undefined;
1522
2401
  }): Promise<CloudflareRouteModulesOutput> {
1523
2402
  const routesDir = join(options.cloudflareDir, "routes");
1524
- const requiredRoutes = options.routes.filter((route) =>
1525
- cloudflareRouteRequiresGeneratedModule(route, options.prerenderedRoutes),
2403
+ const requiredRoutes = await Promise.all(
2404
+ options.routes
2405
+ .filter((route) => cloudflareRouteRequiresGeneratedModule(route, options.prerenderedRoutes))
2406
+ .flatMap((route): CloudflareRequiredRoute[] => {
2407
+ const routeFile = relative(options.projectRoot, route.file).replaceAll(sep, "/");
2408
+ const analysis = options.sourceAnalysis.byFile.get(routeFile);
2409
+
2410
+ return analysis === undefined
2411
+ ? []
2412
+ : [{
2413
+ route,
2414
+ routeFile,
2415
+ routeId: routeIdForPath(route.path),
2416
+ }];
2417
+ }),
1526
2418
  );
1527
- const registryEntries: string[] = [];
1528
2419
 
1529
2420
  await mkdir(routesDir, { recursive: true });
1530
2421
 
1531
- for (const route of requiredRoutes) {
1532
- const routeFile = relative(options.projectRoot, route.file).replaceAll(sep, "/");
1533
- const source = await readFile(route.file, "utf8");
1534
- const routeId = routeIdForPath(route.path);
2422
+ const serverRouteModules = await buildCloudflareServerRouteModuleBatch({
2423
+ cacheDir: options.cacheDir,
2424
+ routes: requiredRoutes
2425
+ .filter(({ route }) => route.kind === "server" || route.kind === "metadata")
2426
+ .map(({ route, routeId }) => ({ filename: route.file, routeId })),
2427
+ root: options.projectRoot,
2428
+ vitePlugins: options.vitePlugins,
2429
+ });
2430
+ const loaderRouteModules = await buildCloudflareRouteLoaderModuleBatch({
2431
+ cacheDir: options.cacheDir,
2432
+ routes: requiredRoutes
2433
+ .filter(({ route, routeFile }) =>
2434
+ route.kind === "page" && options.sourceAnalysis.byRouteFile.get(routeFile)?.hasLoader === true
2435
+ )
2436
+ .map(({ route, routeId }) => ({ filename: route.file, routeId })),
2437
+ root: options.projectRoot,
2438
+ vitePlugins: options.vitePlugins,
2439
+ });
2440
+
2441
+ await Promise.all([
2442
+ writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, serverRouteModules),
2443
+ writeCloudflareBatchedRouteModuleChunks(options.cloudflareDir, loaderRouteModules),
2444
+ ]);
2445
+
2446
+ const registryEntries = await mapWithBuildConcurrency(requiredRoutes, async ({ route, routeFile, routeId }) => {
1535
2447
  const routeModuleFile = `routes/${routeId}.mjs`;
1536
2448
  let routeModuleExports: string[];
1537
2449
 
1538
2450
  if (route.kind === "server" || route.kind === "metadata") {
1539
2451
  try {
1540
- const routeOutput = await buildCloudflareServerRouteModule({
1541
- filename: route.file,
1542
- vitePlugins: options.vitePlugins,
1543
- });
1544
- const serverRouteFile = `routes/${routeId}.${hashText(routeOutput).slice(0, 8)}.server.mjs`;
2452
+ const serverRouteModule = serverRouteModules.entries.get(routeId);
2453
+
2454
+ if (serverRouteModule === undefined) {
2455
+ throw new Error(`Missing bundled Cloudflare ${route.kind} route module.`);
2456
+ }
1545
2457
 
1546
- await writeFile(join(options.cloudflareDir, serverRouteFile), routeOutput);
2458
+ const serverRouteFile = serverRouteModule.fileName;
1547
2459
  const serverRouteImport = `./${serverRouteFile.split("/").pop() ?? serverRouteFile}`;
1548
2460
  routeModuleExports = [
1549
2461
  `export * from ${JSON.stringify(serverRouteImport)};`,
@@ -1558,20 +2470,12 @@ async function writeCloudflareRouteModules(options: {
1558
2470
  }
1559
2471
 
1560
2472
  await writeFile(join(options.cloudflareDir, routeModuleFile), `${routeModuleExports.join("\n")}\n`);
1561
- registryEntries.push(
1562
- `${JSON.stringify(routeFile)}: () => import(${JSON.stringify(`./${routeModuleFile}`)})`,
1563
- );
1564
- continue;
2473
+ return `${JSON.stringify(routeFile)}: () => import(${JSON.stringify(`./${routeModuleFile}`)})`;
1565
2474
  }
1566
2475
 
1567
2476
  const serverOutput =
1568
2477
  options.serverModules[routeFile]?.analysis?.streamRoute === true ||
1569
- shouldBuildRouteAsStream({
1570
- filename: routeFile,
1571
- files: options.files,
1572
- projectRoot: options.projectRoot,
1573
- source,
1574
- })
2478
+ options.sourceAnalysis.byRouteFile.get(routeFile)?.streamRoute === true
1575
2479
  ? "stream"
1576
2480
  : "string";
1577
2481
 
@@ -1579,17 +2483,21 @@ async function writeCloudflareRouteModules(options: {
1579
2483
  const componentOutput =
1580
2484
  serverOutput === "stream"
1581
2485
  ? await buildCloudflareStreamRouteComponentModule({
2486
+ cacheDir: options.cacheDir,
1582
2487
  filename: route.file,
1583
2488
  projectRoot: options.projectRoot,
1584
2489
  routesDir: options.routesDir,
1585
2490
  serverModules: options.serverModules,
2491
+ sourceAnalysis: options.sourceAnalysis,
1586
2492
  vitePlugins: options.vitePlugins,
1587
2493
  })
1588
2494
  : await buildCloudflareStringRouteComponentModule({
2495
+ cacheDir: options.cacheDir,
1589
2496
  filename: route.file,
1590
2497
  projectRoot: options.projectRoot,
1591
2498
  routesDir: options.routesDir,
1592
2499
  serverModules: options.serverModules,
2500
+ sourceAnalysis: options.sourceAnalysis,
1593
2501
  vitePlugins: options.vitePlugins,
1594
2502
  });
1595
2503
  const componentFile = `routes/${routeId}.${hashText(componentOutput).slice(0, 8)}.component.mjs`;
@@ -1606,16 +2514,15 @@ async function writeCloudflareRouteModules(options: {
1606
2514
  );
1607
2515
  }
1608
2516
 
1609
- if (hasLoaderExport(source)) {
2517
+ if (options.sourceAnalysis.byRouteFile.get(routeFile)?.hasLoader === true) {
1610
2518
  try {
1611
- const loaderOutput = await buildCloudflareRouteLoaderModule({
1612
- filename: route.file,
1613
- projectRoot: options.projectRoot,
1614
- vitePlugins: options.vitePlugins,
1615
- });
1616
- const loaderFile = `routes/${routeId}.${hashText(loaderOutput).slice(0, 8)}.loader.mjs`;
2519
+ const loaderModule = loaderRouteModules.entries.get(routeId);
2520
+
2521
+ if (loaderModule === undefined) {
2522
+ throw new Error("Missing bundled Cloudflare loader module.");
2523
+ }
1617
2524
 
1618
- await writeFile(join(options.cloudflareDir, loaderFile), loaderOutput);
2525
+ const loaderFile = loaderModule.fileName;
1619
2526
  const loaderImport = `./${loaderFile.split("/").pop() ?? loaderFile}`;
1620
2527
  routeModuleExports.push(`export { loader } from ${JSON.stringify(loaderImport)};`);
1621
2528
  } catch (error) {
@@ -1626,10 +2533,8 @@ async function writeCloudflareRouteModules(options: {
1626
2533
  }
1627
2534
 
1628
2535
  await writeFile(join(options.cloudflareDir, routeModuleFile), `${routeModuleExports.join("\n")}\n`);
1629
- registryEntries.push(
1630
- `${JSON.stringify(routeFile)}: () => import(${JSON.stringify(`./${routeModuleFile}`)})`,
1631
- );
1632
- }
2536
+ return `${JSON.stringify(routeFile)}: () => import(${JSON.stringify(`./${routeModuleFile}`)})`;
2537
+ });
1633
2538
 
1634
2539
  const registrySource = [
1635
2540
  `export const routeModules = {`,
@@ -1651,14 +2556,19 @@ interface CloudflareShellFile {
1651
2556
  }
1652
2557
 
1653
2558
  async function buildCloudflareServerComponentModule(options: {
2559
+ cacheDir?: string | undefined;
1654
2560
  filename: string;
1655
2561
  projectRoot: string;
1656
2562
  serverOutput: ServerOutputMode;
1657
2563
  serverModules: Record<string, BuiltServerModuleArtifact>;
2564
+ sourceAnalysis: BuildSourceAnalysisScope;
1658
2565
  vitePlugins?: readonly PluginOption[] | undefined;
1659
2566
  }): Promise<string> {
1660
2567
  const metadataModule = await buildCloudflareRouteMetadataExportModule({
2568
+ cacheDir: options.cacheDir,
1661
2569
  filename: options.filename,
2570
+ hasMetadata: buildSourceAnalysisForFile(options.sourceAnalysis, options.projectRoot, options.filename)?.hasMetadata,
2571
+ root: options.projectRoot,
1662
2572
  vitePlugins: options.vitePlugins,
1663
2573
  });
1664
2574
  const entry = `import * as routeModule from ${JSON.stringify(options.filename)};
@@ -1674,6 +2584,7 @@ export const slots = routeModule.slots;`;
1674
2584
  return bundleCloudflareVirtualModule({
1675
2585
  entry,
1676
2586
  filename: `${options.filename}.mreact-cloudflare-component.js`,
2587
+ cacheDir: options.cacheDir,
1677
2588
  modules: metadataModule === undefined
1678
2589
  ? new Map()
1679
2590
  : new Map([["mreact:metadata", metadataModule]]),
@@ -1687,43 +2598,52 @@ export const slots = routeModule.slots;`;
1687
2598
  cloudflareWorkspaceRuntimePlugin(),
1688
2599
  ],
1689
2600
  resolveDir: dirname(options.filename),
2601
+ root: options.projectRoot,
1690
2602
  vitePlugins: options.vitePlugins,
1691
2603
  });
1692
2604
  }
1693
2605
 
1694
2606
  async function buildCloudflareStringRouteComponentModule(options: {
2607
+ cacheDir?: string | undefined;
1695
2608
  filename: string;
1696
2609
  projectRoot: string;
1697
2610
  routesDir: string;
1698
2611
  serverModules: Record<string, BuiltServerModuleArtifact>;
2612
+ sourceAnalysis: BuildSourceAnalysisScope;
1699
2613
  vitePlugins?: readonly PluginOption[] | undefined;
1700
2614
  }): Promise<string> {
1701
2615
  const shellFiles = await cloudflareShellFilesForPage(options.routesDir, options.filename);
1702
2616
 
1703
2617
  if (shellFiles.length === 0) {
1704
2618
  return buildCloudflareServerComponentModule({
2619
+ cacheDir: options.cacheDir,
1705
2620
  filename: options.filename,
1706
2621
  projectRoot: options.projectRoot,
1707
2622
  serverModules: options.serverModules,
1708
2623
  serverOutput: "string",
2624
+ sourceAnalysis: options.sourceAnalysis,
1709
2625
  vitePlugins: options.vitePlugins,
1710
2626
  });
1711
2627
  }
1712
2628
 
1713
2629
  const pageModule = await buildCloudflareComponentExportModule({
2630
+ cacheDir: options.cacheDir,
1714
2631
  filename: options.filename,
1715
2632
  projectRoot: options.projectRoot,
1716
2633
  serverModules: options.serverModules,
1717
2634
  serverOutput: "string",
2635
+ sourceAnalysis: options.sourceAnalysis,
1718
2636
  vitePlugins: options.vitePlugins,
1719
2637
  });
1720
2638
  const shellModules = await Promise.all(
1721
2639
  shellFiles.map((shell) =>
1722
2640
  buildCloudflareComponentExportModule({
2641
+ cacheDir: options.cacheDir,
1723
2642
  filename: shell.file,
1724
2643
  projectRoot: options.projectRoot,
1725
2644
  serverModules: options.serverModules,
1726
2645
  serverOutput: "string",
2646
+ sourceAnalysis: options.sourceAnalysis,
1727
2647
  vitePlugins: options.vitePlugins,
1728
2648
  }),
1729
2649
  ),
@@ -1785,6 +2705,7 @@ ${cloudflareShellRuntimeSource()}`;
1785
2705
 
1786
2706
  return bundleCloudflareVirtualModule({
1787
2707
  entry,
2708
+ cacheDir: options.cacheDir,
1788
2709
  filename: `${options.filename}.mreact-cloudflare-string-route.js`,
1789
2710
  modules: new Map([
1790
2711
  ["mreact:page", pageModule],
@@ -1792,32 +2713,39 @@ ${cloudflareShellRuntimeSource()}`;
1792
2713
  ]),
1793
2714
  plugins: [cloudflareWorkspaceRuntimePlugin()],
1794
2715
  resolveDir: dirname(options.filename),
2716
+ root: options.projectRoot,
1795
2717
  vitePlugins: options.vitePlugins,
1796
2718
  });
1797
2719
  }
1798
2720
 
1799
2721
  async function buildCloudflareStreamRouteComponentModule(options: {
2722
+ cacheDir?: string | undefined;
1800
2723
  filename: string;
1801
2724
  projectRoot: string;
1802
2725
  routesDir: string;
1803
2726
  serverModules: Record<string, BuiltServerModuleArtifact>;
2727
+ sourceAnalysis: BuildSourceAnalysisScope;
1804
2728
  vitePlugins?: readonly PluginOption[] | undefined;
1805
2729
  }): Promise<string> {
1806
2730
  const pageModule = await buildCloudflareComponentExportModule({
2731
+ cacheDir: options.cacheDir,
1807
2732
  filename: options.filename,
1808
2733
  projectRoot: options.projectRoot,
1809
2734
  serverModules: options.serverModules,
1810
2735
  serverOutput: "stream",
2736
+ sourceAnalysis: options.sourceAnalysis,
1811
2737
  vitePlugins: options.vitePlugins,
1812
2738
  });
1813
2739
  const shellFiles = await cloudflareShellFilesForPage(options.routesDir, options.filename);
1814
2740
  const shellModules = await Promise.all(
1815
2741
  shellFiles.map((shell) =>
1816
2742
  buildCloudflareComponentExportModule({
2743
+ cacheDir: options.cacheDir,
1817
2744
  filename: shell.file,
1818
2745
  projectRoot: options.projectRoot,
1819
2746
  serverModules: options.serverModules,
1820
2747
  serverOutput: "string",
2748
+ sourceAnalysis: options.sourceAnalysis,
1821
2749
  vitePlugins: options.vitePlugins,
1822
2750
  }),
1823
2751
  ),
@@ -1895,6 +2823,7 @@ ${cloudflareShellRuntimeSource()}`;
1895
2823
 
1896
2824
  return bundleCloudflareVirtualModule({
1897
2825
  entry,
2826
+ cacheDir: options.cacheDir,
1898
2827
  filename: `${options.filename}.mreact-cloudflare-stream-route.js`,
1899
2828
  modules: new Map([
1900
2829
  ["mreact:page", pageModule],
@@ -1902,6 +2831,7 @@ ${cloudflareShellRuntimeSource()}`;
1902
2831
  ]),
1903
2832
  plugins: [cloudflareWorkspaceRuntimePlugin()],
1904
2833
  resolveDir: dirname(options.filename),
2834
+ root: options.projectRoot,
1905
2835
  vitePlugins: options.vitePlugins,
1906
2836
  });
1907
2837
  }
@@ -2155,14 +3085,19 @@ function escapeHtml(value) {
2155
3085
  }
2156
3086
 
2157
3087
  async function buildCloudflareComponentExportModule(options: {
3088
+ cacheDir?: string | undefined;
2158
3089
  filename: string;
2159
3090
  projectRoot: string;
2160
3091
  serverModules: Record<string, BuiltServerModuleArtifact>;
2161
3092
  serverOutput: ServerOutputMode;
3093
+ sourceAnalysis: BuildSourceAnalysisScope;
2162
3094
  vitePlugins?: readonly PluginOption[] | undefined;
2163
3095
  }): Promise<string> {
2164
3096
  const metadataModule = await buildCloudflareRouteMetadataExportModule({
3097
+ cacheDir: options.cacheDir,
2165
3098
  filename: options.filename,
3099
+ hasMetadata: buildSourceAnalysisForFile(options.sourceAnalysis, options.projectRoot, options.filename)?.hasMetadata,
3100
+ root: options.projectRoot,
2166
3101
  vitePlugins: options.vitePlugins,
2167
3102
  });
2168
3103
  const entry = `import * as routeModule from ${JSON.stringify(options.filename)};
@@ -2177,6 +3112,7 @@ export const slots = routeModule.slots;`;
2177
3112
 
2178
3113
  return bundleCloudflareVirtualModule({
2179
3114
  entry,
3115
+ cacheDir: options.cacheDir,
2180
3116
  filename: `${options.filename}.mreact-cloudflare-${options.serverOutput}-component.js`,
2181
3117
  modules: metadataModule === undefined
2182
3118
  ? new Map()
@@ -2191,33 +3127,142 @@ export const slots = routeModule.slots;`;
2191
3127
  cloudflareWorkspaceRuntimePlugin(),
2192
3128
  ],
2193
3129
  resolveDir: dirname(options.filename),
3130
+ root: options.projectRoot,
2194
3131
  vitePlugins: options.vitePlugins,
2195
3132
  });
2196
3133
  }
2197
3134
 
2198
- async function buildCloudflareRouteLoaderModule(options: {
2199
- filename: string;
3135
+ async function writeCloudflareBatchedRouteModuleChunks(
3136
+ cloudflareDir: string,
3137
+ modules: CloudflareBatchedRouteModules,
3138
+ ): Promise<void> {
3139
+ if (modules.chunks.some((chunk) => chunk.fileName.startsWith("routes/chunks/"))) {
3140
+ await mkdir(join(cloudflareDir, "routes", "chunks"), { recursive: true });
3141
+ }
3142
+
3143
+ await Promise.all(
3144
+ modules.chunks.map((chunk) => writeFile(join(cloudflareDir, chunk.fileName), chunk.code)),
3145
+ );
3146
+ }
3147
+
3148
+ async function buildCloudflareRouteLoaderModuleBatch(options: {
3149
+ cacheDir?: string | undefined;
3150
+ routes: readonly { filename: string; routeId: string }[];
3151
+ root: string;
3152
+ vitePlugins?: readonly PluginOption[] | undefined;
3153
+ }): Promise<CloudflareBatchedRouteModules> {
3154
+ return await bundleCloudflareModuleBatch({
3155
+ cacheDir: options.cacheDir,
3156
+ entries: options.routes.map((route) => ({
3157
+ code: cloudflareRouteLoaderModuleEntry(route.filename),
3158
+ filename: `${route.filename}.mreact-cloudflare-loader.js`,
3159
+ name: `${route.routeId}.loader`,
3160
+ routeId: route.routeId,
3161
+ })),
3162
+ root: options.root,
3163
+ vitePlugins: options.vitePlugins,
3164
+ });
3165
+ }
3166
+
3167
+ export async function __buildCloudflareRouteLoaderModuleBatchForTests(options: {
3168
+ cacheDir?: string | undefined;
2200
3169
  projectRoot: string;
3170
+ routes: readonly { filename: string; routeId: string }[];
2201
3171
  vitePlugins?: readonly PluginOption[] | undefined;
2202
- }): Promise<string> {
2203
- const entry = `export { loader } from ${JSON.stringify(options.filename)};`;
3172
+ }): Promise<Record<string, string>> {
3173
+ const output = await buildCloudflareRouteLoaderModuleBatch({
3174
+ cacheDir: options.cacheDir,
3175
+ root: options.projectRoot,
3176
+ routes: options.routes,
3177
+ vitePlugins: options.vitePlugins,
3178
+ });
2204
3179
 
2205
- return bundleCloudflareModule({
2206
- entry,
2207
- filename: `${options.filename}.mreact-cloudflare-loader.js`,
3180
+ return Object.fromEntries(
3181
+ options.routes.map((route) => [route.routeId, output.entries.get(route.routeId)?.code ?? ""]),
3182
+ );
3183
+ }
3184
+
3185
+ async function buildCloudflareServerRouteModuleBatch(options: {
3186
+ cacheDir?: string | undefined;
3187
+ routes: readonly { filename: string; routeId: string }[];
3188
+ root: string;
3189
+ vitePlugins?: readonly PluginOption[] | undefined;
3190
+ }): Promise<CloudflareBatchedRouteModules> {
3191
+ return await bundleCloudflareModuleBatch({
3192
+ cacheDir: options.cacheDir,
3193
+ entries: options.routes.map((route) => ({
3194
+ code: cloudflareServerRouteModuleEntry(route.filename),
3195
+ filename: `${route.filename}.mreact-cloudflare-server-route.js`,
3196
+ name: `${route.routeId}.server`,
3197
+ routeId: route.routeId,
3198
+ })),
3199
+ root: options.root,
3200
+ vitePlugins: options.vitePlugins,
3201
+ });
3202
+ }
3203
+
3204
+ async function bundleCloudflareModuleBatch(options: {
3205
+ cacheDir?: string | undefined;
3206
+ entries: readonly {
3207
+ code: string;
3208
+ filename: string;
3209
+ name: string;
3210
+ routeId: string;
3211
+ }[];
3212
+ root: string;
3213
+ vitePlugins?: readonly PluginOption[] | undefined;
3214
+ }): Promise<CloudflareBatchedRouteModules> {
3215
+ if (options.entries.length === 0) {
3216
+ return { chunks: [], entries: new Map() };
3217
+ }
3218
+
3219
+ const output = await bundleRouterModules({
3220
+ cacheDir: options.cacheDir,
3221
+ chunkFileNames: "routes/chunks/[name].[hash].mjs",
3222
+ entries: options.entries,
3223
+ entryFileNames: "routes/[name].[hash].mjs",
3224
+ minify: true,
3225
+ platform: "node",
2208
3226
  plugins: [cloudflareWorkspaceRuntimePlugin()],
2209
- resolveDir: dirname(options.filename),
3227
+ root: options.root,
3228
+ target: "es2022",
2210
3229
  vitePlugins: options.vitePlugins,
2211
3230
  });
3231
+ const entriesByName = new Map(options.entries.map((entry) => [entry.name, entry]));
3232
+ const entries = new Map<string, CloudflareBatchedRouteModule>();
3233
+
3234
+ for (const chunk of output.chunks) {
3235
+ if (!chunk.isEntry) {
3236
+ continue;
3237
+ }
3238
+
3239
+ const entry = entriesByName.get(chunk.name);
3240
+
3241
+ if (entry === undefined) {
3242
+ continue;
3243
+ }
3244
+
3245
+ entries.set(entry.routeId, {
3246
+ code: chunk.code,
3247
+ fileName: chunk.fileName,
3248
+ });
3249
+ }
3250
+
3251
+ return { chunks: output.chunks, entries };
3252
+ }
3253
+
3254
+ function cloudflareRouteLoaderModuleEntry(filename: string): string {
3255
+ return `export { loader } from ${JSON.stringify(filename)};`;
2212
3256
  }
2213
3257
 
2214
3258
  async function buildCloudflareRouteMetadataExportModule(options: {
3259
+ cacheDir?: string | undefined;
2215
3260
  filename: string;
3261
+ hasMetadata?: boolean | undefined;
3262
+ root?: string | undefined;
2216
3263
  vitePlugins?: readonly PluginOption[] | undefined;
2217
3264
  }): Promise<string | undefined> {
2218
- const source = await readFile(options.filename, "utf8");
2219
-
2220
- if (!hasMetadataExport(source)) {
3265
+ if (options.hasMetadata !== true) {
2221
3266
  return undefined;
2222
3267
  }
2223
3268
 
@@ -2227,18 +3272,17 @@ export const metadata = routeMetadataModule.metadata;`;
2227
3272
 
2228
3273
  return bundleCloudflareModule({
2229
3274
  entry,
3275
+ cacheDir: options.cacheDir,
2230
3276
  filename: `${options.filename}.mreact-cloudflare-metadata.js`,
2231
3277
  plugins: [cloudflareWorkspaceRuntimePlugin()],
2232
3278
  resolveDir: dirname(options.filename),
3279
+ root: options.root,
2233
3280
  vitePlugins: options.vitePlugins,
2234
3281
  });
2235
3282
  }
2236
3283
 
2237
- async function buildCloudflareServerRouteModule(options: {
2238
- filename: string;
2239
- vitePlugins?: readonly PluginOption[] | undefined;
2240
- }): Promise<string> {
2241
- const entry = `import * as routeModule from ${JSON.stringify(options.filename)};
3284
+ function cloudflareServerRouteModuleEntry(filename: string): string {
3285
+ return `import * as routeModule from ${JSON.stringify(filename)};
2242
3286
 
2243
3287
  export const GET = routeModule.GET;
2244
3288
  export const HEAD = routeModule.HEAD;
@@ -2250,30 +3294,26 @@ export const OPTIONS = routeModule.OPTIONS;
2250
3294
  export const ALL = routeModule.ALL;
2251
3295
  const defaultHandler = routeModule.default;
2252
3296
  export default defaultHandler;`;
2253
-
2254
- return bundleCloudflareModule({
2255
- entry,
2256
- filename: `${options.filename}.mreact-cloudflare-server-route.js`,
2257
- plugins: [cloudflareWorkspaceRuntimePlugin()],
2258
- resolveDir: dirname(options.filename),
2259
- vitePlugins: options.vitePlugins,
2260
- });
2261
3297
  }
2262
3298
 
2263
3299
  async function bundleCloudflareModule(options: {
3300
+ cacheDir?: string | undefined;
2264
3301
  entry: string;
2265
3302
  filename: string;
2266
3303
  plugins: RouterCompatPlugin[];
2267
3304
  resolveDir: string;
3305
+ root?: string | undefined;
2268
3306
  vitePlugins?: readonly PluginOption[] | undefined;
2269
3307
  }): Promise<string> {
2270
3308
  const output = await bundleRouterModule({
3309
+ cacheDir: options.cacheDir,
2271
3310
  code: options.entry,
2272
3311
  filename: options.filename,
2273
3312
  minify: true,
2274
- platform: "browser",
3313
+ platform: "node",
2275
3314
  preserveExports: true,
2276
3315
  plugins: options.plugins,
3316
+ root: options.root,
2277
3317
  target: "es2022",
2278
3318
  vitePlugins: options.vitePlugins,
2279
3319
  });
@@ -2287,14 +3327,17 @@ async function bundleCloudflareModule(options: {
2287
3327
  }
2288
3328
 
2289
3329
  async function bundleCloudflareVirtualModule(options: {
3330
+ cacheDir?: string | undefined;
2290
3331
  entry: string;
2291
3332
  filename: string;
2292
3333
  modules: ReadonlyMap<string, string>;
2293
3334
  plugins: RouterCompatPlugin[];
2294
3335
  resolveDir: string;
3336
+ root?: string | undefined;
2295
3337
  vitePlugins?: readonly PluginOption[] | undefined;
2296
3338
  }): Promise<string> {
2297
3339
  return bundleCloudflareModule({
3340
+ cacheDir: options.cacheDir,
2298
3341
  entry: options.entry,
2299
3342
  filename: options.filename,
2300
3343
  plugins: [
@@ -2323,6 +3366,7 @@ async function bundleCloudflareVirtualModule(options: {
2323
3366
  ...options.plugins,
2324
3367
  ],
2325
3368
  resolveDir: options.resolveDir,
3369
+ root: options.root,
2326
3370
  vitePlugins: options.vitePlugins,
2327
3371
  });
2328
3372
  }
@@ -2371,7 +3415,9 @@ function cloudflareServerSourceTransformPlugin(options: {
2371
3415
  }
2372
3416
 
2373
3417
  const source = await readFile(args.path, "utf8");
2374
- const serverSource = isServerComponentFile(args.path) ? stripRouteBuildExports(source) : source;
3418
+ const serverSource = isServerComponentFile(args.path)
3419
+ ? stripRouteBuildExports(source, args.path)
3420
+ : source;
2375
3421
  const sourceHash = hashText(serverSource);
2376
3422
  const routeFile = relative(options.projectRoot, args.path).replaceAll(sep, "/");
2377
3423
  const artifact = options.serverModules[routeFile]?.[options.serverOutput];
@@ -2616,11 +3662,13 @@ function viteManifestFromClientRoutes(routes: ClientRouteManifestEntry[]): Recor
2616
3662
  async function writeClientRouteBundles(options: {
2617
3663
  appDir: string;
2618
3664
  assetBaseUrl?: string | undefined;
3665
+ cacheDir?: string | undefined;
2619
3666
  clientDir: string;
2620
3667
  clientConsolePureFunctions?: readonly string[] | undefined;
2621
3668
  clientRouteInferenceCache: ClientRouteInferenceCache;
2622
3669
  projectRoot: string;
2623
3670
  routes: readonly AppRoute[];
3671
+ sourceAnalysis: BuildSourceAnalysisScope;
2624
3672
  sourceMapDir: string;
2625
3673
  sourceMaps: AppRouterClientSourceMapMode;
2626
3674
  vitePlugins?: readonly PluginOption[] | undefined;
@@ -2633,6 +3681,18 @@ async function writeClientRouteBundles(options: {
2633
3681
  };
2634
3682
  type PreparedClientManifestEntry = { manifest: ClientRouteManifestEntry };
2635
3683
  type PreparedRouteEntry = PreparedClientRouteEntry | PreparedClientManifestEntry;
3684
+ const pageRoutes = options.routes.filter(
3685
+ (route): route is AppRoute & { kind: "page" } => route.kind === "page",
3686
+ );
3687
+ const routeCssAssets = await writeRouteCssAssetBatches({
3688
+ appDir: options.appDir,
3689
+ cacheDir: options.cacheDir,
3690
+ clientDir: options.clientDir,
3691
+ pageRoutes,
3692
+ projectRoot: options.projectRoot,
3693
+ sourceAnalysis: options.sourceAnalysis,
3694
+ vitePlugins: options.vitePlugins,
3695
+ });
2636
3696
  const entries: PreparedRouteEntry[] = await Promise.all(
2637
3697
  options.routes.map(async (route) => {
2638
3698
  if (route.kind !== "page") {
@@ -2641,21 +3701,26 @@ async function writeClientRouteBundles(options: {
2641
3701
  };
2642
3702
  }
2643
3703
 
2644
- const css = await writeRouteCssAssets({
2645
- appDir: options.appDir,
2646
- clientDir: options.clientDir,
2647
- pageFile: route.file,
2648
- projectRoot: options.projectRoot,
2649
- routeId: routeIdForPath(route.path),
2650
- vitePlugins: options.vitePlugins,
2651
- });
2652
- const source = await readFile(route.file, "utf8");
3704
+ const css = routeCssAssets.get(route.file) ?? [];
3705
+ const source = buildSourceAnalysisForFile(
3706
+ options.sourceAnalysis,
3707
+ options.projectRoot,
3708
+ route.file,
3709
+ )?.source;
3710
+
3711
+ if (source === undefined) {
3712
+ return {
3713
+ manifest: { path: route.path, kind: route.kind, client: false },
3714
+ };
3715
+ }
3716
+
2653
3717
  const clientSource = stripRouteClientSource({ code: source, filename: route.file });
2654
3718
  const references = await collectClientRouteReferences({
2655
3719
  appDir: options.appDir,
2656
3720
  cache: options.clientRouteInferenceCache,
2657
3721
  code: clientSource,
2658
3722
  filename: route.file,
3723
+ routePath: route.path,
2659
3724
  vitePlugins: options.vitePlugins,
2660
3725
  });
2661
3726
  const navigation = await resolveNavigationRuntime({
@@ -2663,6 +3728,7 @@ async function writeClientRouteBundles(options: {
2663
3728
  code: source,
2664
3729
  filename: route.file,
2665
3730
  references,
3731
+ vitePlugins: options.vitePlugins,
2666
3732
  });
2667
3733
 
2668
3734
  for (const diagnostic of references.diagnostics) {
@@ -2691,6 +3757,7 @@ async function writeClientRouteBundles(options: {
2691
3757
  clientReferenceImports: references.clientReferenceImports,
2692
3758
  clientReferenceManifest: references.clientReferenceManifest,
2693
3759
  clientNavigation: detectClientNavigationHint(source),
3760
+ cacheDir: options.cacheDir,
2694
3761
  dropConsoleFunctions: options.clientConsolePureFunctions,
2695
3762
  filename: route.file,
2696
3763
  minify: true,
@@ -2714,6 +3781,7 @@ async function writeClientRouteBundles(options: {
2714
3781
  try {
2715
3782
  output = await buildClientRouteBatchOutput({
2716
3783
  assetBaseUrl: options.assetBaseUrl,
3784
+ cacheDir: options.cacheDir,
2717
3785
  dropConsoleFunctions: options.clientConsolePureFunctions,
2718
3786
  minify: true,
2719
3787
  projectRoot: options.projectRoot,
@@ -2802,20 +3870,75 @@ async function writeClientRouteBundles(options: {
2802
3870
  });
2803
3871
  }
2804
3872
 
2805
- async function writeRouteCssAssets(options: {
3873
+ async function writeRouteCssAssetBatches(options: {
2806
3874
  appDir: string;
3875
+ cacheDir?: string | undefined;
2807
3876
  clientDir: string;
2808
- pageFile: string;
3877
+ pageRoutes: readonly (AppRoute & { kind: "page" })[];
2809
3878
  projectRoot: string;
2810
- routeId: string;
3879
+ sourceAnalysis: BuildSourceAnalysisScope;
2811
3880
  vitePlugins?: readonly PluginOption[] | undefined;
2812
- }): Promise<string[]> {
2813
- const cssFiles = await collectRouteCssFiles({
2814
- appDir: options.appDir,
2815
- pageFile: options.pageFile,
2816
- projectRoot: options.projectRoot,
3881
+ }): Promise<Map<string, string[]>> {
3882
+ const cssInputs = await mapWithBuildConcurrency(options.pageRoutes, async (route) => {
3883
+ const cssFiles = await collectRouteCssFilesFromSources({
3884
+ appDir: options.appDir,
3885
+ pageFile: route.file,
3886
+ projectRoot: options.projectRoot,
3887
+ readSource: (file) => buildSourceAnalysisForFile(options.sourceAnalysis, options.projectRoot, file)?.source,
3888
+ });
3889
+
3890
+ return { cssFiles, route };
2817
3891
  });
3892
+ const groups = new Map<string, { cssFiles: string[]; routeIds: string[]; routeFiles: string[] }>();
3893
+
3894
+ for (const { cssFiles, route } of cssInputs) {
3895
+ if (cssFiles.length === 0) {
3896
+ continue;
3897
+ }
3898
+
3899
+ const key = cssFiles.join("\0");
3900
+ const group = groups.get(key) ?? { cssFiles, routeFiles: [], routeIds: [] };
3901
+ group.routeFiles.push(route.file);
3902
+ group.routeIds.push(routeIdForPath(route.path));
3903
+ groups.set(key, group);
3904
+ }
2818
3905
 
3906
+ const writtenGroups = await mapWithBuildConcurrency([...groups.entries()], async ([key, group]) => [
3907
+ key,
3908
+ await writeRouteCssAssetsForFiles({
3909
+ cacheDir: options.cacheDir,
3910
+ clientDir: options.clientDir,
3911
+ cssFiles: group.cssFiles,
3912
+ pageFile: group.routeFiles[0] ?? options.appDir,
3913
+ projectRoot: options.projectRoot,
3914
+ routeIds: group.routeIds,
3915
+ vitePlugins: options.vitePlugins,
3916
+ }),
3917
+ ] as const);
3918
+ const cssByGroup = new Map(writtenGroups);
3919
+ const cssByRoute = new Map<string, string[]>();
3920
+
3921
+ for (const [key, group] of groups) {
3922
+ const css = cssByGroup.get(key) ?? [];
3923
+
3924
+ for (const routeFile of group.routeFiles) {
3925
+ cssByRoute.set(routeFile, css);
3926
+ }
3927
+ }
3928
+
3929
+ return cssByRoute;
3930
+ }
3931
+
3932
+ async function writeRouteCssAssetsForFiles(options: {
3933
+ cacheDir?: string | undefined;
3934
+ clientDir: string;
3935
+ cssFiles: readonly string[];
3936
+ pageFile: string;
3937
+ projectRoot: string;
3938
+ routeIds: readonly string[];
3939
+ vitePlugins?: readonly PluginOption[] | undefined;
3940
+ }): Promise<string[]> {
3941
+ const cssFiles = [...options.cssFiles];
2819
3942
  if (cssFiles.length === 0) {
2820
3943
  return [];
2821
3944
  }
@@ -2825,22 +3948,27 @@ async function writeRouteCssAssets(options: {
2825
3948
  "export default undefined;",
2826
3949
  ].join("\n");
2827
3950
  const output = await bundleRouterModule({
3951
+ cacheDir: options.cacheDir,
2828
3952
  code,
2829
3953
  filename: options.pageFile,
2830
3954
  minify: true,
2831
3955
  platform: "browser",
3956
+ root: options.projectRoot,
2832
3957
  vitePlugins: options.vitePlugins,
2833
3958
  });
2834
3959
  const cssAssets = (output.assets ?? []).filter((asset) => asset.fileName.endsWith(".css"));
2835
3960
  const written: string[] = [];
3961
+ const routeStem = options.routeIds.length === 1
3962
+ ? (options.routeIds[0] ?? "index")
3963
+ : `shared.${hashText(cssFiles.join("\0")).slice(0, 8)}`;
2836
3964
 
2837
- for (const asset of cssAssets) {
3965
+ for (const [index, asset] of cssAssets.entries()) {
2838
3966
  const source =
2839
3967
  typeof asset.source === "string"
2840
3968
  ? asset.source
2841
3969
  : Buffer.from(asset.source).toString("utf8");
2842
3970
  const hash = createHash("sha256").update(source).digest("hex").slice(0, 8);
2843
- const cssFile = `assets/routes/${options.routeId}.${hash}.css`;
3971
+ const cssFile = `assets/routes/${routeStem}${cssAssets.length === 1 ? "" : `.${index}`}.${hash}.css`;
2844
3972
 
2845
3973
  await mkdir(dirname(join(options.clientDir, cssFile)), { recursive: true });
2846
3974
  await writeFile(join(options.clientDir, cssFile), source);
@@ -2945,7 +4073,15 @@ export async function packageAwsLambdaArtifact(
2945
4073
  fromDir: dirname(options.fromDir),
2946
4074
  outDir: options.outDir,
2947
4075
  });
2948
- await writeFile(join(options.outDir, "mreact-handler.mjs"), awsLambdaHandlerSource(".mreact"));
4076
+ if (options.handlerEntry === undefined) {
4077
+ await writeFile(join(options.outDir, "mreact-handler.mjs"), awsLambdaHandlerSource(".mreact"));
4078
+ } else {
4079
+ await writeAwsLambdaCustomHandlerArtifact({
4080
+ entry: options.handlerEntry,
4081
+ projectRoot: dirname(options.fromDir),
4082
+ outDir: options.outDir,
4083
+ });
4084
+ }
2949
4085
 
2950
4086
  if (options.skipRuntimeDependencyCheck !== true) {
2951
4087
  await assertAwsLambdaRuntimeDependencies(options.outDir);
@@ -2968,6 +4104,44 @@ export async function packageAwsLambdaArtifact(
2968
4104
  return manifest;
2969
4105
  }
2970
4106
 
4107
+ async function writeAwsLambdaCustomHandlerArtifact(options: {
4108
+ entry: string;
4109
+ outDir: string;
4110
+ projectRoot: string;
4111
+ }): Promise<void> {
4112
+ const entry = resolve(options.entry);
4113
+ const output = await bundleRouterModule({
4114
+ code: await readFile(entry, "utf8"),
4115
+ filename: entry,
4116
+ outfile: "mreact-handler.mjs",
4117
+ platform: "node",
4118
+ plugins: [externalizePackageImportsPlugin()],
4119
+ preserveExports: true,
4120
+ root: options.projectRoot,
4121
+ target: "node24",
4122
+ });
4123
+
4124
+ await writeFile(join(options.outDir, "mreact-handler.mjs"), output.code);
4125
+ }
4126
+
4127
+ function externalizePackageImportsPlugin(): RouterCompatPlugin {
4128
+ return {
4129
+ name: "mreact-router-externalize-package-imports",
4130
+ setup(buildApi) {
4131
+ buildApi.onResolve({ filter: /^(?:node:|[A-Za-z@])/ }, (args) =>
4132
+ isBarePackageImport(args.path) ? { external: true, path: args.path } : undefined,
4133
+ );
4134
+ },
4135
+ };
4136
+ }
4137
+
4138
+ function isBarePackageImport(specifier: string): boolean {
4139
+ return (
4140
+ specifier.startsWith("node:") ||
4141
+ (!specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.includes("\\"))
4142
+ );
4143
+ }
4144
+
2971
4145
  async function assertAwsLambdaRuntimeDependencies(outDir: string): Promise<void> {
2972
4146
  try {
2973
4147
  const info = await stat(join(outDir, "node_modules", "@reckona", "mreact-router", "package.json"));
@@ -3146,31 +4320,34 @@ export const handler = await createPreloadedAwsLambdaRequestHandler({
3146
4320
  }
3147
4321
 
3148
4322
  async function validateProductionRoutes(options: {
4323
+ clientRouteInferenceCache: ClientRouteInferenceCache;
3149
4324
  files: Record<string, string>;
4325
+ project: ResolvedAppRouterProject;
3150
4326
  projectRoot: string;
3151
4327
  routes: readonly AppRoute[];
4328
+ sourceAnalysis: BuildSourceAnalysisScope;
4329
+ serverTransformCache: ServerTransformCache;
4330
+ vitePlugins?: readonly PluginOption[] | undefined;
3152
4331
  }): Promise<void> {
3153
4332
  for (const route of options.routes) {
3154
4333
  if (route.kind !== "page") {
3155
4334
  continue;
3156
4335
  }
3157
4336
 
3158
- const source = await readFile(route.file, "utf8");
3159
4337
  const filename = relative(options.projectRoot, route.file);
3160
- const output = transform({
3161
- code: stripRouteBuildExports(source),
3162
- dev: false,
4338
+ const analysis = options.sourceAnalysis.byRouteFile.get(filename.split(sep).join("/"));
4339
+
4340
+ if (analysis === undefined) {
4341
+ continue;
4342
+ }
4343
+
4344
+ const output = await transformServerRouteSource({
4345
+ cache: options.serverTransformCache,
4346
+ code: analysis.routeCode,
4347
+ clientBoundaryImports: analysis.clientBoundaryImports,
3163
4348
  filename: route.file,
3164
- serverEscape: nativeEscapeTransform,
3165
- serverOutput: shouldBuildRouteAsStream({
3166
- filename,
3167
- files: options.files,
3168
- projectRoot: options.projectRoot,
3169
- source,
3170
- })
3171
- ? "stream"
3172
- : "string",
3173
- target: "server",
4349
+ moduleContextCache: options.clientRouteInferenceCache,
4350
+ serverOutput: analysis.streamRoute ? "stream" : "string",
3174
4351
  });
3175
4352
  const fatalDiagnostics = output.diagnostics.filter(
3176
4353
  (diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER",
@@ -3192,18 +4369,26 @@ async function collectBuildFiles(
3192
4369
  const files: Record<string, string> = {};
3193
4370
 
3194
4371
  for (const directory of allowedSourceDirs) {
3195
- for (const file of await collectFiles(directory)) {
4372
+ const sourceFiles = (await collectFiles(directory)).flatMap((file) => {
3196
4373
  if (isAppFileConventionAsset(file, appDir)) {
3197
- continue;
4374
+ return [];
3198
4375
  }
3199
4376
 
3200
4377
  const relativeFile = relative(projectRoot, file);
3201
4378
 
3202
4379
  if (relativeFile === "" || relativeFile.startsWith("..") || relativeFile.startsWith(sep)) {
3203
- continue;
4380
+ return [];
3204
4381
  }
3205
4382
 
3206
- files[relativeFile] = await readFile(file, "utf8");
4383
+ return [{ file, relativeFile }];
4384
+ });
4385
+ const fileContents = await mapWithBuildConcurrency(sourceFiles, async ({ file, relativeFile }) => [
4386
+ relativeFile,
4387
+ await readFile(file, "utf8"),
4388
+ ] as const);
4389
+
4390
+ for (const [relativeFile, source] of fileContents) {
4391
+ files[relativeFile] = source;
3207
4392
  }
3208
4393
  }
3209
4394
 
@@ -3221,22 +4406,21 @@ function isAppFileConventionAsset(file: string, appDir: string): boolean {
3221
4406
 
3222
4407
  async function collectFiles(directory: string): Promise<string[]> {
3223
4408
  const entries = await readdir(directory, { withFileTypes: true });
3224
- const files: string[] = [];
3225
-
3226
- for (const entry of entries) {
4409
+ const nestedFiles = await mapWithBuildConcurrency(entries, async (entry) => {
3227
4410
  const path = join(directory, entry.name);
3228
4411
 
3229
4412
  if (entry.isDirectory()) {
3230
- files.push(...(await collectFiles(path)));
3231
- continue;
4413
+ return await collectFiles(path);
3232
4414
  }
3233
4415
 
3234
4416
  if (entry.isFile()) {
3235
- files.push(path);
4417
+ return [path];
3236
4418
  }
3237
- }
3238
4419
 
3239
- return files;
4420
+ return [];
4421
+ });
4422
+
4423
+ return nestedFiles.flat();
3240
4424
  }
3241
4425
 
3242
4426
  function hashText(text: string): string {