@reckona/mreact-router 0.0.65 → 0.0.67

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 (59) hide show
  1. package/package.json +13 -12
  2. package/src/actions.ts +1130 -0
  3. package/src/adapters/aws-lambda.ts +993 -0
  4. package/src/adapters/cloudflare.ts +1286 -0
  5. package/src/adapters/devtools.ts +5 -0
  6. package/src/adapters/edge.ts +70 -0
  7. package/src/adapters/node.ts +126 -0
  8. package/src/adapters/static.ts +61 -0
  9. package/src/app-router-globals.ts +19 -0
  10. package/src/assets.ts +113 -0
  11. package/src/build.ts +2948 -0
  12. package/src/bundle-pipeline.ts +496 -0
  13. package/src/cache-config.ts +35 -0
  14. package/src/cache-stats.ts +54 -0
  15. package/src/cache.ts +418 -0
  16. package/src/cli-options.ts +296 -0
  17. package/src/cli.ts +94 -0
  18. package/src/client.ts +3398 -0
  19. package/src/config.ts +146 -0
  20. package/src/cookies.ts +113 -0
  21. package/src/csp.ts +103 -0
  22. package/src/csrf.ts +132 -0
  23. package/src/deferred.ts +52 -0
  24. package/src/dev-server.ts +262 -0
  25. package/src/file-conventions.ts +88 -0
  26. package/src/http.ts +128 -0
  27. package/src/i18n.ts +98 -0
  28. package/src/import-policy.ts +261 -0
  29. package/src/index.ts +221 -0
  30. package/src/link.ts +47 -0
  31. package/src/logger.ts +149 -0
  32. package/src/module-runner.ts +554 -0
  33. package/src/multipart.ts +577 -0
  34. package/src/native-escape.ts +53 -0
  35. package/src/native-route-matcher.ts +173 -0
  36. package/src/navigation-state.ts +98 -0
  37. package/src/navigation.ts +215 -0
  38. package/src/prerender-store.ts +233 -0
  39. package/src/render.ts +5187 -0
  40. package/src/route-path.ts +5 -0
  41. package/src/route-shells.ts +47 -0
  42. package/src/route-source.ts +224 -0
  43. package/src/route-styles.ts +91 -0
  44. package/src/routes.ts +362 -0
  45. package/src/runtime-cache.ts +8 -0
  46. package/src/runtime-state.ts +37 -0
  47. package/src/security-headers.ts +134 -0
  48. package/src/serve.ts +1265 -0
  49. package/src/session.ts +238 -0
  50. package/src/source-jsx.ts +30 -0
  51. package/src/source-modules.ts +69 -0
  52. package/src/stream-list.ts +45 -0
  53. package/src/trace.ts +114 -0
  54. package/src/types.ts +155 -0
  55. package/src/upgrade.ts +8 -0
  56. package/src/vite-config.ts +63 -0
  57. package/src/vite-plugin-cache-key.ts +53 -0
  58. package/src/vite.ts +690 -0
  59. package/src/workspace-packages.ts +67 -0
package/src/build.ts ADDED
@@ -0,0 +1,2948 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { Dirent } from "node:fs";
3
+ import { copyFile, cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
+ import { builtinModules } from "node:module";
5
+ import { dirname, join, relative, sep } from "node:path";
6
+ import {
7
+ collectFormActionReferenceNames,
8
+ collectStaticImportReferences,
9
+ collectTopLevelValueExportNames,
10
+ formatDiagnostic,
11
+ hasModuleDirective,
12
+ transform,
13
+ } from "@reckona/mreact-compiler";
14
+ import { transformCompilerModuleContext } from "@reckona/mreact-compiler/internal";
15
+ import type { ServerOutputMode } from "@reckona/mreact-compiler";
16
+ import {
17
+ buildClientRouteBatchOutput,
18
+ buildNavigationRuntimeBundle,
19
+ clientScriptForPath,
20
+ compilerModuleContextForSource,
21
+ collectClientRouteReferences,
22
+ createClientRouteInferenceCache,
23
+ detectClientNavigationHint,
24
+ detectNavigationRuntimeHint,
25
+ formatClientRouteInferenceDiagnostic,
26
+ inferClientRouteModule,
27
+ routeIdForPath,
28
+ type BuildClientRouteOutputOptions,
29
+ type ClientRouteManifestEntry,
30
+ type ClientRouteInferenceCache,
31
+ } from "./client.js";
32
+ import { bundleAppRouterSourceModule, importAppRouterSourceModule } from "./module-runner.js";
33
+ import { scanAppRoutes } from "./routes.js";
34
+ import type { AppRoute } from "./routes.js";
35
+ import {
36
+ appFileConventionForRootFilename,
37
+ } from "./file-conventions.js";
38
+ import {
39
+ resolveAppRouterProjectOptions,
40
+ resolveBuildTargets,
41
+ type AppRouterProjectOptions,
42
+ type AppRouterBuildTarget,
43
+ type AppRouterClientSourceMapMode,
44
+ type ResolvedAppRouterProject,
45
+ } from "./config.js";
46
+ import type { ModuleMetadata } from "@reckona/mreact-compiler";
47
+ import type { RouteCachePolicy } from "./cache.js";
48
+ import { routeCachePolicyFromSource } from "./cache.js";
49
+ import { bundleMiddlewareModuleCode, renderAppRequest } from "./render.js";
50
+ import { createAppRouterImportPolicyPlugin, type AppRouterImportPolicy } from "./import-policy.js";
51
+ import {
52
+ hasGenerateStaticParamsExport,
53
+ hasLoaderExport,
54
+ hasPrerenderExport,
55
+ isStreamRouteSource,
56
+ routeClosureMayUseAwaitBoundary,
57
+ stripRouteBuildExports,
58
+ stripRouteClientOnlyExports,
59
+ stripRouteLoaderOnlyExports,
60
+ stripRouteMetadataOnlyExports,
61
+ stripRouteRequestOnlyExports,
62
+ } from "./route-source.js";
63
+ import {
64
+ bundleRouterModule,
65
+ type RouterCompatPlugin,
66
+ } from "./bundle-pipeline.js";
67
+ import { collectRouteCssFiles } from "./route-styles.js";
68
+ import { existingRouteShellCandidates } from "./route-shells.js";
69
+ import { sourceModuleCandidates } from "./source-modules.js";
70
+ import { workspacePackageFile } from "./workspace-packages.js";
71
+ import type { PluginOption, UserConfig } from "vite";
72
+
73
+ const nativeEscapeTransform = {
74
+ batchImportName: "escapeHtmlBatch",
75
+ batchImportSource: "@reckona/mreact-router/native-escape",
76
+ } as const;
77
+
78
+ export interface BuildAppOptions extends AppRouterProjectOptions {
79
+ outDir: string;
80
+ targets?: readonly AppRouterBuildTarget[] | undefined;
81
+ viteConfig?: Pick<UserConfig, "plugins"> | undefined;
82
+ }
83
+
84
+ export interface BuildAppResult {
85
+ routes: AppRoute[];
86
+ }
87
+
88
+ export interface BuiltImportPolicyArtifact {
89
+ byRoute: Record<string, string[]>;
90
+ runtimePackages: string[];
91
+ version: 1;
92
+ }
93
+
94
+ export interface AwsLambdaArtifactManifest {
95
+ files: Array<{ bytes: number; path: string }>;
96
+ handler: string;
97
+ runtime: "aws-lambda";
98
+ totalBytes: number;
99
+ version: 1;
100
+ }
101
+
102
+ export interface PackageAwsLambdaArtifactOptions {
103
+ fromDir: string;
104
+ outDir: string;
105
+ }
106
+
107
+ export interface BuiltServerManifest {
108
+ allowedSourceDirs?: readonly string[];
109
+ assetBaseUrl?: string;
110
+ version: 1;
111
+ files: Record<string, string>;
112
+ prerenderedRoutes?: Record<string, BuiltPrerenderedRoute>;
113
+ publicAssetBaseUrl?: string;
114
+ routesDir?: string;
115
+ serverActionManifest?: BuiltServerActionReference[];
116
+ serverModuleFiles?: Record<string, string>;
117
+ serverModuleRenderFiles?: Record<string, string>;
118
+ serverModuleRequestFiles?: Record<string, string>;
119
+ routes: AppRoute[];
120
+ serverModules?: Record<string, BuiltServerModuleArtifact>;
121
+ }
122
+
123
+ export interface BuiltServerActionReference {
124
+ exportName: string;
125
+ inferred?: boolean;
126
+ moduleId: string;
127
+ }
128
+
129
+ export interface BuiltServerModuleArtifact {
130
+ analysis?: BuiltRouteSourceAnalysisSummary;
131
+ loader?: BuiltServerModuleOutput;
132
+ routeMetadata?: BuiltServerModuleOutput;
133
+ request?: BuiltServerModuleOutput;
134
+ stream?: BuiltServerModuleOutput;
135
+ string?: BuiltServerModuleOutput;
136
+ }
137
+
138
+ export interface BuiltRouteSourceAnalysisSummary {
139
+ authIncludesClaims: boolean;
140
+ cachePolicy?: RouteCachePolicy | undefined;
141
+ clientBoundaryImports: readonly string[];
142
+ clientRoute: boolean;
143
+ hasLoader: boolean;
144
+ routeCode: string;
145
+ routePath: string;
146
+ sourceHash: string;
147
+ streamRoute: boolean;
148
+ usesRuntimeCacheControl: boolean;
149
+ }
150
+
151
+ export interface BuiltServerModuleOutput {
152
+ bundleCode?: string;
153
+ code: string;
154
+ metadata?: ModuleMetadata;
155
+ moduleFile?: string;
156
+ sourceHash: string;
157
+ }
158
+
159
+ export interface BuiltPrerenderedRoute {
160
+ headers: Record<string, string>;
161
+ html: string;
162
+ status: number;
163
+ }
164
+
165
+ type StaticParams = Record<string, string | number | boolean | readonly string[]>;
166
+
167
+ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult> {
168
+ const project = resolveAppRouterProjectOptions(options);
169
+ const buildTargets = resolveBuildTargets(options.targets ?? project.buildTargets);
170
+ const shouldBuildCloudflare = buildTargets.includes("cloudflare");
171
+ const shouldBuildAwsLambda = buildTargets.includes("aws-lambda");
172
+ const routes = await scanAppRoutes({ appDir: project.routesDir });
173
+ const vitePlugins = options.viteConfig?.plugins;
174
+ const files = await collectBuildFiles(
175
+ project.projectRoot,
176
+ project.allowedSourceDirs,
177
+ project.routesDir,
178
+ );
179
+ const serverDir = join(options.outDir, "server");
180
+ const clientDir = join(options.outDir, "client");
181
+ const cloudflareDir = join(options.outDir, "cloudflare");
182
+
183
+ await validateProductionRoutes({ files, projectRoot: project.projectRoot, routes });
184
+
185
+ await rm(options.outDir, { force: true, recursive: true });
186
+ await mkdir(serverDir, { recursive: true });
187
+ await mkdir(clientDir, { recursive: true });
188
+ if (shouldBuildCloudflare) {
189
+ await mkdir(cloudflareDir, { recursive: true });
190
+ }
191
+ await mkdir(join(clientDir, ".vite"), { recursive: true });
192
+ await mkdir(join(clientDir, "assets", "routes"), { recursive: true });
193
+ await copyPublicAssets(project.publicDir, join(clientDir, "public"));
194
+ const appConventionPublicAssets = await copyAppFileConventionAssets(
195
+ project.routesDir,
196
+ clientDir,
197
+ );
198
+ await copyPublicAssets(project.publicDir, clientDir);
199
+ const publicAssets = [
200
+ ...new Set([...(await collectPublicAssetPaths(project.publicDir)), ...appConventionPublicAssets]),
201
+ ].sort();
202
+
203
+ const serverActionManifest = collectBuildServerActionManifest({
204
+ files,
205
+ projectRoot: project.projectRoot,
206
+ routes,
207
+ routesDir: project.routesDir,
208
+ });
209
+ const clientRouteInferenceCache = createClientRouteInferenceCache();
210
+ const serverModules = await buildServerModuleArtifacts({
211
+ clientRouteInferenceCache,
212
+ files,
213
+ prebundleServerComponents: buildTargets.includes("node") || shouldBuildAwsLambda,
214
+ project,
215
+ projectRoot: project.projectRoot,
216
+ routes,
217
+ vitePlugins,
218
+ });
219
+ const generatedImportPolicy = buildGeneratedImportPolicy({
220
+ files,
221
+ projectRoot: project.projectRoot,
222
+ routes,
223
+ routesDir: project.routesDir,
224
+ });
225
+ const serverModuleArtifacts = await writeServerModuleArtifactFiles(serverDir, serverModules);
226
+ const serverRoutes = routes.map((route) => ({
227
+ ...route,
228
+ file: relative(project.projectRoot, route.file),
229
+ }));
230
+ const clientRoutes = await writeClientRouteBundles({
231
+ appDir: project.routesDir,
232
+ assetBaseUrl: project.assetBaseUrl,
233
+ clientDir,
234
+ clientRouteInferenceCache,
235
+ projectRoot: project.projectRoot,
236
+ routes,
237
+ sourceMapDir: join(options.outDir, "source-maps", "client"),
238
+ sourceMaps: project.clientSourceMaps,
239
+ vitePlugins,
240
+ });
241
+ const navigationRuntimeScript = clientRoutes.some(
242
+ (route) => route.navigation === true && !route.client,
243
+ )
244
+ ? await writeNavigationRuntimeBundle(clientDir)
245
+ : undefined;
246
+ const clientManifestRoutes =
247
+ navigationRuntimeScript === undefined
248
+ ? clientRoutes
249
+ : clientRoutes.map((route) =>
250
+ route.navigation === true && !route.client
251
+ ? { ...route, navigationScript: navigationRuntimeScript }
252
+ : route,
253
+ );
254
+ const prerenderedRoutes = await prerenderStaticRoutes({
255
+ appDir: project.routesDir,
256
+ assetBaseUrl: project.assetBaseUrl,
257
+ clientRoutes: clientManifestRoutes,
258
+ project,
259
+ routes,
260
+ serverModules,
261
+ vitePlugins,
262
+ });
263
+ let cloudflareRouteModules: CloudflareRouteModulesOutput | undefined;
264
+ if (shouldBuildCloudflare) {
265
+ cloudflareRouteModules = await writeCloudflareRouteModules({
266
+ cloudflareDir,
267
+ files,
268
+ prerenderedRoutes,
269
+ projectRoot: project.projectRoot,
270
+ routesDir: project.routesDir,
271
+ routes,
272
+ serverModules,
273
+ vitePlugins,
274
+ });
275
+ }
276
+
277
+ const serverManifest = {
278
+ allowedSourceDirs: project.allowedSourceDirs.map((directory) =>
279
+ relative(project.projectRoot, directory),
280
+ ),
281
+ ...(project.assetBaseUrl === undefined ? {} : { assetBaseUrl: project.assetBaseUrl }),
282
+ version: 1,
283
+ routes: serverRoutes,
284
+ routesDir: relative(project.projectRoot, project.routesDir),
285
+ files,
286
+ prerenderedRoutes,
287
+ ...(project.publicAssetBaseUrl === undefined
288
+ ? {}
289
+ : { publicAssetBaseUrl: project.publicAssetBaseUrl }),
290
+ ...(serverActionManifest.length === 0 ? {} : { serverActionManifest }),
291
+ ...(Object.keys(serverModuleArtifacts.files).length === 0
292
+ ? {}
293
+ : { serverModuleFiles: serverModuleArtifacts.files }),
294
+ ...(Object.keys(serverModuleArtifacts.requestFiles).length === 0
295
+ ? {}
296
+ : { serverModuleRequestFiles: serverModuleArtifacts.requestFiles }),
297
+ ...(Object.keys(serverModuleArtifacts.renderFiles).length === 0
298
+ ? {}
299
+ : { serverModuleRenderFiles: serverModuleArtifacts.renderFiles }),
300
+ } satisfies BuiltServerManifest;
301
+ const clientManifest = {
302
+ ...(publicAssets.length === 0 ? {} : { publicAssets }),
303
+ routes: clientManifestRoutes,
304
+ };
305
+ await writeFile(
306
+ join(serverDir, "manifest.json"),
307
+ JSON.stringify(serverManifest, null, 2),
308
+ );
309
+ await writeFile(
310
+ join(serverDir, "import-policy.json"),
311
+ JSON.stringify(generatedImportPolicy, null, 2),
312
+ );
313
+ await writeFile(
314
+ join(clientDir, "manifest.json"),
315
+ JSON.stringify(clientManifest, null, 2),
316
+ );
317
+ await writeFile(
318
+ join(clientDir, ".vite", "manifest.json"),
319
+ JSON.stringify(viteManifestFromClientRoutes(clientManifestRoutes), null, 2),
320
+ );
321
+ if (shouldBuildAwsLambda) {
322
+ await writeAwsLambdaHandlerArtifact(options.outDir);
323
+ }
324
+ if (shouldBuildCloudflare) {
325
+ await writeCloudflareWorkerArtifact({
326
+ cloudflareDir,
327
+ clientManifest,
328
+ modulesFile: cloudflareRouteModules?.registryFile ?? "route-modules.mjs",
329
+ serverManifest,
330
+ });
331
+ }
332
+
333
+ return { routes };
334
+ }
335
+
336
+ async function writeServerModuleArtifactFiles(
337
+ serverDir: string,
338
+ serverModules: Record<string, BuiltServerModuleArtifact>,
339
+ ): Promise<{
340
+ files: Record<string, string>;
341
+ renderFiles: Record<string, string>;
342
+ requestFiles: Record<string, string>;
343
+ }> {
344
+ const files: Record<string, string> = {};
345
+ const renderFiles: Record<string, string> = {};
346
+ const requestFiles: Record<string, string> = {};
347
+ const modulesDir = join(serverDir, "server-modules");
348
+
349
+ for (const [file, artifact] of Object.entries(serverModules)) {
350
+ const externalized = await externalizeServerModuleArtifactCode(serverDir, artifact);
351
+ const requestArtifact = requestServerModuleArtifact(externalized);
352
+ const renderArtifact = renderServerModuleArtifact(externalized);
353
+
354
+ await mkdir(modulesDir, { recursive: true });
355
+
356
+ if (Object.keys(requestArtifact).length > 0 && Object.keys(renderArtifact).length > 0) {
357
+ const requestJson = JSON.stringify(requestArtifact);
358
+ const requestArtifactFile = `server-modules/request/${hashText(`${file}\0request\0${requestJson}`).slice(0, 16)}.json`;
359
+ await mkdir(join(modulesDir, "request"), { recursive: true });
360
+ await writeFile(join(serverDir, requestArtifactFile), requestJson);
361
+ requestFiles[file] = requestArtifactFile;
362
+
363
+ const renderJson = JSON.stringify(renderArtifact);
364
+ const renderArtifactFile = `server-modules/render/${hashText(`${file}\0render\0${renderJson}`).slice(0, 16)}.json`;
365
+ await mkdir(join(modulesDir, "render"), { recursive: true });
366
+ await writeFile(join(serverDir, renderArtifactFile), renderJson);
367
+ renderFiles[file] = renderArtifactFile;
368
+ continue;
369
+ }
370
+
371
+ const json = JSON.stringify(externalized);
372
+ const artifactFile = `server-modules/${hashText(`${file}\0${json}`).slice(0, 16)}.json`;
373
+
374
+ await writeFile(join(serverDir, artifactFile), json);
375
+ files[file] = artifactFile;
376
+ }
377
+
378
+ return { files, renderFiles, requestFiles };
379
+ }
380
+
381
+ async function externalizeServerModuleArtifactCode(
382
+ serverDir: string,
383
+ artifact: BuiltServerModuleArtifact,
384
+ ): Promise<BuiltServerModuleArtifact> {
385
+ return {
386
+ ...(artifact.analysis === undefined ? {} : { analysis: artifact.analysis }),
387
+ ...(artifact.loader === undefined
388
+ ? {}
389
+ : { loader: await externalizeServerModuleOutputCode(serverDir, artifact.loader, "code") }),
390
+ ...(artifact.routeMetadata === undefined
391
+ ? {}
392
+ : {
393
+ routeMetadata: await externalizeServerModuleOutputCode(
394
+ serverDir,
395
+ artifact.routeMetadata,
396
+ "code",
397
+ ),
398
+ }),
399
+ ...(artifact.request === undefined
400
+ ? {}
401
+ : { request: await externalizeServerModuleOutputCode(serverDir, artifact.request, "code") }),
402
+ ...(artifact.stream === undefined
403
+ ? {}
404
+ : { stream: await externalizeServerModuleOutputCode(serverDir, artifact.stream, "bundle") }),
405
+ ...(artifact.string === undefined
406
+ ? {}
407
+ : { string: await externalizeServerModuleOutputCode(serverDir, artifact.string, "bundle") }),
408
+ };
409
+ }
410
+
411
+ async function externalizeServerModuleOutputCode(
412
+ serverDir: string,
413
+ output: BuiltServerModuleOutput,
414
+ kind: "bundle" | "code",
415
+ ): Promise<BuiltServerModuleOutput> {
416
+ const moduleCode = kind === "bundle" ? output.bundleCode : output.code;
417
+
418
+ if (moduleCode === undefined || moduleCode.length === 0) {
419
+ return output;
420
+ }
421
+
422
+ const moduleFile = `server-modules/code/${hashText(moduleCode).slice(0, 16)}.mjs`;
423
+ await mkdir(join(serverDir, "server-modules", "code"), { recursive: true });
424
+ await writeFile(join(serverDir, moduleFile), moduleCode);
425
+
426
+ return {
427
+ code: kind === "code" ? "" : output.code,
428
+ ...(output.metadata === undefined ? {} : { metadata: output.metadata }),
429
+ moduleFile,
430
+ sourceHash: output.sourceHash,
431
+ };
432
+ }
433
+
434
+ function requestServerModuleArtifact(
435
+ artifact: BuiltServerModuleArtifact,
436
+ ): BuiltServerModuleArtifact {
437
+ return {
438
+ ...(artifact.analysis === undefined ? {} : { analysis: artifact.analysis }),
439
+ ...(artifact.loader === undefined ? {} : { loader: artifact.loader }),
440
+ ...(artifact.routeMetadata === undefined ? {} : { routeMetadata: artifact.routeMetadata }),
441
+ ...(artifact.request === undefined ? {} : { request: artifact.request }),
442
+ };
443
+ }
444
+
445
+ function renderServerModuleArtifact(
446
+ artifact: BuiltServerModuleArtifact,
447
+ ): BuiltServerModuleArtifact {
448
+ return {
449
+ ...(artifact.stream === undefined ? {} : { stream: artifact.stream }),
450
+ ...(artifact.string === undefined ? {} : { string: artifact.string }),
451
+ };
452
+ }
453
+
454
+ const nodeBuiltinPackages = new Set(builtinModules.flatMap((name) => [name, `node:${name}`]));
455
+ const frameworkRuntimePackages = new Set([
456
+ "@reckona/mreact",
457
+ "@reckona/mreact-auth",
458
+ "@reckona/mreact-compiler",
459
+ "@reckona/mreact-query",
460
+ "@reckona/mreact-reactive-core",
461
+ "@reckona/mreact-router",
462
+ "@reckona/mreact-server",
463
+ ]);
464
+
465
+ function buildGeneratedImportPolicy(options: {
466
+ files: Record<string, string>;
467
+ projectRoot: string;
468
+ routes: readonly AppRoute[];
469
+ routesDir: string;
470
+ }): BuiltImportPolicyArtifact {
471
+ const routePackages = new Map<string, string[]>();
472
+ const allPackages = new Set<string>();
473
+ const relativeRoutesDir = relative(options.projectRoot, options.routesDir);
474
+
475
+ for (const route of options.routes) {
476
+ const file = relative(options.projectRoot, route.file);
477
+ const packages = collectRuntimePackagesForFile({
478
+ file,
479
+ files: options.files,
480
+ projectRoot: options.projectRoot,
481
+ seen: new Set(),
482
+ });
483
+
484
+ if (packages.length > 0) {
485
+ routePackages.set(route.path, packages);
486
+ for (const packageName of packages) {
487
+ allPackages.add(packageName);
488
+ }
489
+ }
490
+ }
491
+
492
+ const middlewareFile = ["middleware.ts", "middleware.mreact.ts"]
493
+ .map((file) => (relativeRoutesDir === "" ? file : `${relativeRoutesDir}/${file}`))
494
+ .find((file) => options.files[file] !== undefined);
495
+
496
+ if (middlewareFile !== undefined) {
497
+ const packages = collectRuntimePackagesForFile({
498
+ file: middlewareFile,
499
+ files: options.files,
500
+ projectRoot: options.projectRoot,
501
+ seen: new Set(),
502
+ });
503
+
504
+ if (packages.length > 0) {
505
+ routePackages.set("middleware", packages);
506
+ for (const packageName of packages) {
507
+ allPackages.add(packageName);
508
+ }
509
+ }
510
+ }
511
+
512
+ return {
513
+ byRoute: Object.fromEntries(
514
+ [...routePackages.entries()].sort(([left], [right]) => left.localeCompare(right)),
515
+ ),
516
+ runtimePackages: [...allPackages].sort(),
517
+ version: 1,
518
+ };
519
+ }
520
+
521
+ function collectRuntimePackagesForFile(options: {
522
+ file: string;
523
+ files: Record<string, string>;
524
+ projectRoot: string;
525
+ seen: Set<string>;
526
+ }): string[] {
527
+ if (options.seen.has(options.file)) {
528
+ return [];
529
+ }
530
+
531
+ const source = options.files[options.file];
532
+ if (
533
+ source === undefined ||
534
+ hasModuleDirective({
535
+ code: source,
536
+ directive: "use client",
537
+ filename: join(options.projectRoot, options.file),
538
+ })
539
+ ) {
540
+ return [];
541
+ }
542
+
543
+ options.seen.add(options.file);
544
+ const packages = new Set<string>();
545
+
546
+ for (const reference of collectStaticImportReferences({
547
+ code: source,
548
+ filename: join(options.projectRoot, options.file),
549
+ })) {
550
+ if (isRuntimePackageSpecifier(reference.source)) {
551
+ packages.add(runtimePackageNameForSpecifier(reference.source));
552
+ continue;
553
+ }
554
+
555
+ const localFile = resolveBuildLocalSourceImport(options.files, options.file, reference.source);
556
+ if (localFile === undefined) {
557
+ continue;
558
+ }
559
+
560
+ for (const packageName of collectRuntimePackagesForFile({
561
+ ...options,
562
+ file: localFile,
563
+ })) {
564
+ packages.add(packageName);
565
+ }
566
+ }
567
+
568
+ options.seen.delete(options.file);
569
+
570
+ return [...packages].sort();
571
+ }
572
+
573
+ function isRuntimePackageSpecifier(specifier: string): boolean {
574
+ if (specifier.startsWith(".") || specifier.startsWith("/") || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(specifier)) {
575
+ return false;
576
+ }
577
+
578
+ const packageName = runtimePackageNameForSpecifier(specifier);
579
+ return !nodeBuiltinPackages.has(specifier) && !frameworkRuntimePackages.has(packageName);
580
+ }
581
+
582
+ function runtimePackageNameForSpecifier(specifier: string): string {
583
+ if (!specifier.startsWith("@")) {
584
+ return specifier.split("/")[0] ?? specifier;
585
+ }
586
+
587
+ const [scope, name] = specifier.split("/");
588
+ return scope !== undefined && name !== undefined ? `${scope}/${name}` : specifier;
589
+ }
590
+
591
+ function collectBuildServerActionManifest(options: {
592
+ files: Record<string, string>;
593
+ projectRoot: string;
594
+ routes: readonly AppRoute[];
595
+ routesDir: string;
596
+ }): BuiltServerActionReference[] {
597
+ const entries = new Map<string, BuiltServerActionReference>();
598
+ const relativeRoutesDir = relative(options.projectRoot, options.routesDir);
599
+ const routeSourceFiles = new Set(
600
+ options.routes
601
+ .filter((route) => route.kind === "page")
602
+ .map((route) => relative(options.projectRoot, route.file).split(sep).join("/")),
603
+ );
604
+
605
+ for (const [file, code] of Object.entries(options.files)) {
606
+ if (!isAppRelativeFile(file, relativeRoutesDir) || !isSourceModuleFile(file)) {
607
+ continue;
608
+ }
609
+
610
+ if (hasModuleDirective({ code, directive: "use server", filename: file })) {
611
+ const moduleId = moduleIdForBuildFile(file, relativeRoutesDir);
612
+
613
+ for (const exportName of collectTopLevelValueExportNames({ code, filename: file })) {
614
+ entries.set(`${moduleId}#${exportName}`, { moduleId, exportName });
615
+ }
616
+ }
617
+
618
+ if (routeSourceFiles.has(file)) {
619
+ for (const reference of collectBuildInferredServerActionReferences({
620
+ file,
621
+ files: options.files,
622
+ relativeRoutesDir,
623
+ source: code,
624
+ })) {
625
+ const key = `${reference.moduleId}#${reference.exportName}`;
626
+
627
+ if (!entries.has(key)) {
628
+ entries.set(key, reference);
629
+ }
630
+ }
631
+ }
632
+ }
633
+
634
+ return [...entries.values()].sort((left, right) =>
635
+ left.moduleId === right.moduleId
636
+ ? left.exportName.localeCompare(right.exportName)
637
+ : left.moduleId.localeCompare(right.moduleId),
638
+ );
639
+ }
640
+
641
+ function collectBuildInferredServerActionReferences(options: {
642
+ file: string;
643
+ files: Record<string, string>;
644
+ relativeRoutesDir: string;
645
+ source: string;
646
+ }): BuiltServerActionReference[] {
647
+ const actionNames = collectFormActionNames(options.source);
648
+
649
+ if (actionNames.size === 0) {
650
+ return [];
651
+ }
652
+
653
+ const references: BuiltServerActionReference[] = [];
654
+
655
+ for (const match of options.source.matchAll(
656
+ /^import\s+\{\s*(?<specifiers>[^}]+)\s*\}\s+from\s+["'](?<source>[^"']+)["'];?/gm,
657
+ )) {
658
+ const source = match.groups?.source;
659
+ const specifiers = match.groups?.specifiers;
660
+
661
+ if (source === undefined || specifiers === undefined || !source.startsWith(".")) {
662
+ continue;
663
+ }
664
+
665
+ const localFile = resolveBuildLocalSourceImport(options.files, options.file, source);
666
+
667
+ if (localFile === undefined) {
668
+ continue;
669
+ }
670
+
671
+ const moduleId = moduleIdForBuildFile(localFile, options.relativeRoutesDir);
672
+
673
+ for (const specifier of specifiers.split(",")) {
674
+ const [exportName, localName] = specifier.trim().split(/\s+as\s+/);
675
+ const imported = exportName?.trim();
676
+ const local = localName?.trim() ?? imported;
677
+
678
+ if (
679
+ imported !== undefined &&
680
+ imported.length > 0 &&
681
+ local !== undefined &&
682
+ actionNames.has(local)
683
+ ) {
684
+ references.push({ moduleId, exportName: imported, inferred: true });
685
+ }
686
+ }
687
+ }
688
+
689
+ return references;
690
+ }
691
+
692
+ function collectFormActionNames(code: string): Set<string> {
693
+ return new Set(collectFormActionReferenceNames({ code }));
694
+ }
695
+
696
+ function isSourceModuleFile(file: string): boolean {
697
+ return /\.(?:mreact\.tsx|tsx?|jsx?|mjs|mts|cjs|cts)$/.test(file);
698
+ }
699
+
700
+ function isAppRelativeFile(file: string, relativeRoutesDir: string): boolean {
701
+ return relativeRoutesDir === "" || file === relativeRoutesDir || file.startsWith(`${relativeRoutesDir}/`);
702
+ }
703
+
704
+ function moduleIdForBuildFile(file: string, relativeRoutesDir: string): string {
705
+ return relativeRoutesDir === "" ? file : file.slice(relativeRoutesDir.length + 1);
706
+ }
707
+
708
+ async function copyPublicAssets(publicDir: string, outDir: string): Promise<void> {
709
+ try {
710
+ await cp(publicDir, outDir, { force: true, recursive: true });
711
+ } catch (error) {
712
+ if (isNodeError(error) && error.code === "ENOENT") {
713
+ return;
714
+ }
715
+
716
+ throw error;
717
+ }
718
+ }
719
+
720
+ async function collectPublicAssetPaths(publicDir: string): Promise<string[]> {
721
+ try {
722
+ const info = await stat(publicDir);
723
+
724
+ if (!info.isDirectory()) {
725
+ return [];
726
+ }
727
+ } catch (error) {
728
+ if (isNodeError(error) && error.code === "ENOENT") {
729
+ return [];
730
+ }
731
+
732
+ throw error;
733
+ }
734
+
735
+ const paths: string[] = [];
736
+ await collectPublicAssetPathsInner(publicDir, "", paths);
737
+
738
+ return paths.sort();
739
+ }
740
+
741
+ async function copyAppFileConventionAssets(
742
+ appDir: string,
743
+ outDir: string,
744
+ ): Promise<string[]> {
745
+ let entries: Dirent[];
746
+ try {
747
+ entries = await readdir(appDir, { withFileTypes: true });
748
+ } catch (error) {
749
+ if (isNodeError(error) && error.code === "ENOENT") {
750
+ return [];
751
+ }
752
+
753
+ throw error;
754
+ }
755
+
756
+ const paths: string[] = [];
757
+ await mkdir(outDir, { recursive: true });
758
+
759
+ for (const entry of entries) {
760
+ if (!entry.isFile()) {
761
+ continue;
762
+ }
763
+
764
+ const convention = appFileConventionForRootFilename(entry.name);
765
+ if (convention === undefined || convention.kind !== "asset") {
766
+ continue;
767
+ }
768
+
769
+ const outputPath = convention.path.startsWith("/")
770
+ ? convention.path.slice(1)
771
+ : convention.path;
772
+ await copyFile(join(appDir, entry.name), join(outDir, outputPath));
773
+ paths.push(convention.path);
774
+ }
775
+
776
+ return paths.sort();
777
+ }
778
+
779
+ async function collectPublicAssetPathsInner(
780
+ publicDir: string,
781
+ relativeDir: string,
782
+ paths: string[],
783
+ ): Promise<void> {
784
+ const entries = await readdir(join(publicDir, relativeDir), { withFileTypes: true });
785
+
786
+ for (const entry of entries) {
787
+ const relativePath = relativeDir === "" ? entry.name : `${relativeDir}/${entry.name}`;
788
+
789
+ if (entry.isDirectory()) {
790
+ await collectPublicAssetPathsInner(publicDir, relativePath, paths);
791
+ continue;
792
+ }
793
+
794
+ if (entry.isFile()) {
795
+ paths.push(`/${relativePath}`);
796
+ }
797
+ }
798
+ }
799
+
800
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
801
+ return error instanceof Error && "code" in error;
802
+ }
803
+
804
+ async function prerenderStaticRoutes(options: {
805
+ appDir: string;
806
+ assetBaseUrl?: string | undefined;
807
+ clientRoutes: readonly ClientRouteManifestEntry[];
808
+ project: ResolvedAppRouterProject;
809
+ routes: readonly AppRoute[];
810
+ serverModules: Record<string, BuiltServerModuleArtifact>;
811
+ vitePlugins?: readonly PluginOption[] | undefined;
812
+ }): Promise<Record<string, BuiltPrerenderedRoute>> {
813
+ const clientScripts = new Map(
814
+ options.clientRoutes.flatMap((route) =>
815
+ route.client && route.script !== undefined ? [[route.path, route.script]] : [],
816
+ ),
817
+ );
818
+ const prerendered: Record<string, BuiltPrerenderedRoute> = {};
819
+ const importPolicy = {
820
+ allowedPackages: await readDeclaredProjectPackages(options.project.projectRoot),
821
+ allowedSourceDirs: options.project.allowedSourceDirs,
822
+ projectRoot: options.project.projectRoot,
823
+ } satisfies AppRouterImportPolicy;
824
+ const serverModuleMap = new Map(
825
+ Object.entries(options.serverModules).map(([file, artifact]) => [
826
+ join(options.project.projectRoot, file),
827
+ artifact,
828
+ ]),
829
+ );
830
+
831
+ for (const route of options.routes) {
832
+ if (route.kind !== "page") {
833
+ continue;
834
+ }
835
+
836
+ const source = await readFile(route.file, "utf8");
837
+
838
+ if (!hasPrerenderExport(source)) {
839
+ continue;
840
+ }
841
+
842
+ for (const pathname of await prerenderPathsForRoute(route, source, options.vitePlugins)) {
843
+ const response = await renderAppRequest({
844
+ appDir: options.appDir,
845
+ assetBaseUrl: options.assetBaseUrl,
846
+ clientScripts,
847
+ importPolicy,
848
+ request: new Request(`http://mreact.local${pathname}`),
849
+ serverModules: serverModuleMap,
850
+ vitePlugins: options.vitePlugins,
851
+ });
852
+ const headers: Record<string, string> = {};
853
+
854
+ response.headers.forEach((value, key) => {
855
+ headers[key] = value;
856
+ });
857
+ prerendered[pathname] = {
858
+ headers,
859
+ html: await response.text(),
860
+ status: response.status,
861
+ };
862
+ }
863
+ }
864
+
865
+ return prerendered;
866
+ }
867
+
868
+ async function prerenderPathsForRoute(
869
+ route: AppRoute,
870
+ source: string,
871
+ vitePlugins: readonly PluginOption[] | undefined,
872
+ ): Promise<string[]> {
873
+ if (route.segments.every((segment) => segment.kind === "static")) {
874
+ return [route.path];
875
+ }
876
+
877
+ if (!hasGenerateStaticParamsExport(source)) {
878
+ return [];
879
+ }
880
+
881
+ const module = await importAppRouterSourceModule<{
882
+ generateStaticParams?: () => Iterable<StaticParams> | PromiseLike<Iterable<StaticParams>>;
883
+ }>({
884
+ code: source,
885
+ label: `generate-static-params:${route.file}`,
886
+ resolveDir: dirname(route.file),
887
+ sourcefile: route.file,
888
+ vitePlugins,
889
+ });
890
+ const params = await module.generateStaticParams?.();
891
+
892
+ if (params === undefined) {
893
+ return [];
894
+ }
895
+
896
+ return Array.from(params, (entry) => routePathFromParams(route, entry));
897
+ }
898
+
899
+ function routePathFromParams(route: AppRoute, params: StaticParams): string {
900
+ const parts = route.segments.flatMap((segment) => {
901
+ if (segment.kind === "static") {
902
+ return [segment.value];
903
+ }
904
+
905
+ const value = params[segment.name];
906
+
907
+ if (value === undefined) {
908
+ throw new Error(`${route.file}: generateStaticParams() is missing "${segment.name}".`);
909
+ }
910
+
911
+ if (segment.kind === "catch-all") {
912
+ const values = Array.isArray(value) ? value : String(value).split("/");
913
+
914
+ return values.map((part) => encodeURIComponent(String(part)));
915
+ }
916
+
917
+ return [encodeURIComponent(String(value))];
918
+ });
919
+
920
+ return `/${parts.join("/")}`;
921
+ }
922
+
923
+ async function buildServerModuleArtifacts(options: {
924
+ clientRouteInferenceCache: ClientRouteInferenceCache;
925
+ files: Record<string, string>;
926
+ prebundleServerComponents: boolean;
927
+ project: ResolvedAppRouterProject;
928
+ projectRoot: string;
929
+ routes: readonly AppRoute[];
930
+ vitePlugins?: readonly PluginOption[] | undefined;
931
+ }): Promise<Record<string, BuiltServerModuleArtifact>> {
932
+ const routeByFile = new Map(
933
+ options.routes.map((route) => [relative(options.projectRoot, route.file), route]),
934
+ );
935
+ const loaderArtifactFiles = new Set<string>();
936
+ const metadataArtifactFiles = new Set<string>();
937
+ const requestArtifactFiles = new Set<string>();
938
+ const requestModuleImportPolicy = {
939
+ allowedPackages: await readDeclaredProjectPackages(options.project.projectRoot),
940
+ allowedSourceDirs: options.project.allowedSourceDirs,
941
+ projectRoot: options.project.projectRoot,
942
+ } satisfies AppRouterImportPolicy;
943
+ const artifacts: Record<string, BuiltServerModuleArtifact> = {};
944
+
945
+ for (const [file, source] of Object.entries(options.files)) {
946
+ const absoluteFile = join(options.projectRoot, file);
947
+ const route = routeByFile.get(file);
948
+
949
+ if (isMiddlewareFile(options.project.routesDir, absoluteFile)) {
950
+ requestArtifactFiles.add(file);
951
+ }
952
+
953
+ if (route?.kind === "server" || route?.kind === "metadata") {
954
+ requestArtifactFiles.add(file);
955
+ }
956
+
957
+ if (route?.kind === "page" && hasLoaderExport(source)) {
958
+ loaderArtifactFiles.add(file);
959
+ }
960
+
961
+ if (hasMetadataExport(source)) {
962
+ metadataArtifactFiles.add(file);
963
+ }
964
+ }
965
+
966
+ for (const [file, source] of Object.entries(options.files)) {
967
+ const absoluteFile = join(options.projectRoot, file);
968
+ const route = routeByFile.get(file);
969
+ const artifact: BuiltServerModuleArtifact = {};
970
+
971
+ if (
972
+ requestArtifactFiles.has(file) ||
973
+ loaderArtifactFiles.has(file) ||
974
+ metadataArtifactFiles.has(file)
975
+ ) {
976
+ if (loaderArtifactFiles.has(file)) {
977
+ const code = await bundleRouteLoaderModuleCode({
978
+ appDir: options.project.routesDir,
979
+ code: stripRouteLoaderOnlyExports(source),
980
+ filename: absoluteFile,
981
+ importPolicy: requestModuleImportPolicy,
982
+ vitePlugins: options.vitePlugins,
983
+ });
984
+ artifact.loader = {
985
+ code,
986
+ sourceHash: hashText(source),
987
+ };
988
+ }
989
+
990
+ if (metadataArtifactFiles.has(file)) {
991
+ const code = await bundleRouteMetadataModuleCode({
992
+ appDir: options.project.routesDir,
993
+ code: stripRouteMetadataOnlyExports(source),
994
+ filename: absoluteFile,
995
+ importPolicy: requestModuleImportPolicy,
996
+ vitePlugins: options.vitePlugins,
997
+ });
998
+ artifact.routeMetadata = {
999
+ code,
1000
+ sourceHash: hashText(source),
1001
+ };
1002
+ }
1003
+
1004
+ if (requestArtifactFiles.has(file)) {
1005
+ artifact.request = {
1006
+ code: await buildRequestModuleArtifactCode({
1007
+ appDir: options.project.routesDir,
1008
+ filename: absoluteFile,
1009
+ importPolicy: requestModuleImportPolicy,
1010
+ routeKind: route?.kind,
1011
+ source,
1012
+ vitePlugins: options.vitePlugins,
1013
+ }),
1014
+ sourceHash: hashText(source),
1015
+ };
1016
+ }
1017
+ }
1018
+
1019
+ if (!isServerComponentFile(file)) {
1020
+ if (Object.keys(artifact).length > 0) {
1021
+ artifacts[file] = artifact;
1022
+ }
1023
+ continue;
1024
+ }
1025
+
1026
+ const closureUsesAwait = shouldBuildRouteAsStream({
1027
+ filename: file,
1028
+ files: options.files,
1029
+ projectRoot: options.projectRoot,
1030
+ source,
1031
+ });
1032
+ const streamRoute = route !== undefined && closureUsesAwait;
1033
+ const serverOutputs =
1034
+ streamRoute || (route === undefined && closureUsesAwait)
1035
+ ? (["stream", "string"] as const)
1036
+ : (["string"] as const);
1037
+ const code = route === undefined ? source : stripRouteBuildExports(source);
1038
+ const clientInference = await inferClientRouteModule({
1039
+ ...(route === undefined ? {} : { appDir: options.project.routesDir }),
1040
+ cache: options.clientRouteInferenceCache,
1041
+ code: stripRouteClientOnlyExports(source),
1042
+ filename: join(options.projectRoot, file),
1043
+ ...(route === undefined ? {} : { routePath: route.path }),
1044
+ vitePlugins: options.vitePlugins,
1045
+ });
1046
+ const clientBoundaryImports = clientInference.clientBoundaryImports;
1047
+
1048
+ for (const diagnostic of clientInference.diagnostics) {
1049
+ console.warn(formatClientRouteInferenceDiagnostic(diagnostic));
1050
+ }
1051
+
1052
+ if (route?.kind === "page") {
1053
+ artifact.analysis = builtRouteSourceAnalysisSummary({
1054
+ clientBoundaryImports,
1055
+ clientRoute: clientInference.client,
1056
+ route,
1057
+ routeCode: code,
1058
+ source,
1059
+ streamRoute,
1060
+ });
1061
+ }
1062
+
1063
+ for (const serverOutput of serverOutputs) {
1064
+ const output = transform({
1065
+ code,
1066
+ clientBoundaryImports,
1067
+ dev: false,
1068
+ filename: join(options.projectRoot, file),
1069
+ serverEscape: nativeEscapeTransform,
1070
+ serverOutput,
1071
+ target: "server",
1072
+ });
1073
+ const fatalDiagnostics = output.diagnostics.filter(
1074
+ (diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER",
1075
+ );
1076
+
1077
+ if (fatalDiagnostics.length > 0) {
1078
+ if (
1079
+ serverOutput === "string" &&
1080
+ streamRoute &&
1081
+ route?.kind === "page" &&
1082
+ fatalDiagnostics.every(
1083
+ (diagnostic) => diagnostic.code === "MR_UNSUPPORTED_AWAIT_INNER_COMPONENT",
1084
+ )
1085
+ ) {
1086
+ continue;
1087
+ }
1088
+
1089
+ throw new Error(
1090
+ fatalDiagnostics.map((diagnostic) => formatDiagnostic(file, diagnostic)).join("\n"),
1091
+ );
1092
+ }
1093
+
1094
+ artifact[serverOutput] = {
1095
+ ...(options.prebundleServerComponents
1096
+ ? {
1097
+ bundleCode: await buildServerComponentBundleArtifactCode({
1098
+ clientRouteInferenceCache: options.clientRouteInferenceCache,
1099
+ code: output.code,
1100
+ filename: absoluteFile,
1101
+ serverOutput,
1102
+ vitePlugins: options.vitePlugins,
1103
+ }),
1104
+ }
1105
+ : {}),
1106
+ code: output.code,
1107
+ metadata: output.metadata,
1108
+ sourceHash: hashText(code),
1109
+ };
1110
+ }
1111
+
1112
+ artifacts[file] = artifact;
1113
+ }
1114
+
1115
+ return artifacts;
1116
+ }
1117
+
1118
+ async function buildServerComponentBundleArtifactCode(options: {
1119
+ clientRouteInferenceCache: ClientRouteInferenceCache;
1120
+ code: string;
1121
+ filename: string;
1122
+ serverOutput: ServerOutputMode;
1123
+ vitePlugins?: readonly PluginOption[] | undefined;
1124
+ }): Promise<string> {
1125
+ return await bundleAppRouterSourceModule({
1126
+ code: options.code,
1127
+ label: `server-component:${options.filename}`,
1128
+ resolveDir: dirname(options.filename),
1129
+ serverSourceTransform: {
1130
+ clientRouteInferenceCache: options.clientRouteInferenceCache,
1131
+ dev: false,
1132
+ serverOutput: options.serverOutput,
1133
+ vitePlugins: options.vitePlugins,
1134
+ },
1135
+ sourcefile: options.filename,
1136
+ vitePlugins: options.vitePlugins,
1137
+ });
1138
+ }
1139
+
1140
+ function builtRouteSourceAnalysisSummary(options: {
1141
+ clientBoundaryImports: readonly string[];
1142
+ clientRoute: boolean;
1143
+ route: AppRoute;
1144
+ routeCode: string;
1145
+ source: string;
1146
+ streamRoute: boolean;
1147
+ }): BuiltRouteSourceAnalysisSummary {
1148
+ const cachePolicy = routeCachePolicyFromSource(options.source);
1149
+
1150
+ return {
1151
+ authIncludesClaims: authIncludesClaims(options.source),
1152
+ ...(cachePolicy === undefined ? {} : { cachePolicy }),
1153
+ clientBoundaryImports: options.clientBoundaryImports,
1154
+ clientRoute: options.clientRoute,
1155
+ hasLoader: hasLoaderExport(options.source),
1156
+ routeCode: options.routeCode,
1157
+ routePath: options.route.path,
1158
+ sourceHash: hashText(options.source),
1159
+ streamRoute: options.streamRoute,
1160
+ usesRuntimeCacheControl: usesRuntimeCacheControl(options.source),
1161
+ };
1162
+ }
1163
+
1164
+ function shouldBuildRouteAsStream(options: {
1165
+ filename: string;
1166
+ files: Record<string, string>;
1167
+ projectRoot: string;
1168
+ source: string;
1169
+ }): boolean {
1170
+ return (
1171
+ isStreamRouteSource(options.source) ||
1172
+ routeClosureMayUseAwaitBoundary({
1173
+ filename: options.filename,
1174
+ files: options.files,
1175
+ projectRoot: options.projectRoot,
1176
+ source: options.source,
1177
+ })
1178
+ );
1179
+ }
1180
+
1181
+ function resolveBuildLocalSourceImport(
1182
+ files: Record<string, string>,
1183
+ importer: string,
1184
+ specifier: string,
1185
+ ): string | undefined {
1186
+ if (!specifier.startsWith(".")) {
1187
+ return undefined;
1188
+ }
1189
+
1190
+ const base = join(dirname(importer), specifier);
1191
+
1192
+ for (const candidate of sourceModuleCandidates(base)) {
1193
+ if (files[candidate] !== undefined) {
1194
+ return candidate;
1195
+ }
1196
+ }
1197
+
1198
+ return undefined;
1199
+ }
1200
+
1201
+ function usesRuntimeCacheControl(code: string): boolean {
1202
+ return /\bcacheControl\s*\(/.test(code);
1203
+ }
1204
+
1205
+ function authIncludesClaims(code: string): boolean {
1206
+ return /\bexport\s+const\s+auth\s*=\s*["']include-claims["']\s*;?/.test(code);
1207
+ }
1208
+
1209
+ async function buildRequestModuleArtifactCode(options: {
1210
+ appDir: string;
1211
+ filename: string;
1212
+ importPolicy: AppRouterImportPolicy;
1213
+ routeKind?: AppRoute["kind"] | undefined;
1214
+ source: string;
1215
+ vitePlugins?: readonly PluginOption[] | undefined;
1216
+ }): Promise<string> {
1217
+ if (isMiddlewareFile(options.appDir, options.filename)) {
1218
+ return await bundleMiddlewareModuleCode({
1219
+ appDir: options.appDir,
1220
+ code: options.source,
1221
+ file: options.filename,
1222
+ importPolicy: options.importPolicy,
1223
+ vitePlugins: options.vitePlugins,
1224
+ });
1225
+ }
1226
+
1227
+ if (options.routeKind === "server" || options.routeKind === "metadata") {
1228
+ return await bundleAppRouterSourceModule({
1229
+ code: options.source,
1230
+ label: `server-route:${options.filename}`,
1231
+ resolveDir: dirname(options.filename),
1232
+ sourcefile: options.filename,
1233
+ vitePlugins: options.vitePlugins,
1234
+ });
1235
+ }
1236
+
1237
+ return await bundleRouteLoaderModuleCode({
1238
+ appDir: options.appDir,
1239
+ code: stripRouteRequestOnlyExports(options.source),
1240
+ filename: options.filename,
1241
+ importPolicy: options.importPolicy,
1242
+ vitePlugins: options.vitePlugins,
1243
+ });
1244
+ }
1245
+
1246
+ async function bundleRouteLoaderModuleCode(options: {
1247
+ appDir: string;
1248
+ code: string;
1249
+ filename: string;
1250
+ importPolicy?: AppRouterImportPolicy | undefined;
1251
+ vitePlugins?: readonly PluginOption[] | undefined;
1252
+ }): Promise<string> {
1253
+ return await bundleRouteRequestModuleCode({
1254
+ ...options,
1255
+ label: "Loader",
1256
+ });
1257
+ }
1258
+
1259
+ async function bundleRouteMetadataModuleCode(options: {
1260
+ appDir: string;
1261
+ code: string;
1262
+ filename: string;
1263
+ importPolicy?: AppRouterImportPolicy | undefined;
1264
+ vitePlugins?: readonly PluginOption[] | undefined;
1265
+ }): Promise<string> {
1266
+ return await bundleRouteRequestModuleCode({
1267
+ ...options,
1268
+ label: "Metadata",
1269
+ });
1270
+ }
1271
+
1272
+ async function bundleRouteRequestModuleCode(options: {
1273
+ appDir: string;
1274
+ code: string;
1275
+ filename: string;
1276
+ importPolicy?: AppRouterImportPolicy | undefined;
1277
+ label: "Loader" | "Metadata";
1278
+ vitePlugins?: readonly PluginOption[] | undefined;
1279
+ }): Promise<string> {
1280
+ const output = await bundleRouterModule({
1281
+ code: options.code,
1282
+ filename: options.filename,
1283
+ platform: "node",
1284
+ vitePlugins: options.vitePlugins,
1285
+ plugins: [
1286
+ createAppRouterImportPolicyPlugin({
1287
+ appDir: options.appDir,
1288
+ importPolicy: options.importPolicy,
1289
+ label: options.label,
1290
+ }),
1291
+ ],
1292
+ });
1293
+ const code = output.code;
1294
+
1295
+ if (code === undefined) {
1296
+ throw new Error(`Failed to compile ${options.label.toLowerCase()} for ${options.filename}.`);
1297
+ }
1298
+
1299
+ return code;
1300
+ }
1301
+
1302
+ function isMiddlewareFile(appDir: string, file: string): boolean {
1303
+ return file === join(appDir, "middleware.ts") || file === join(appDir, "middleware.mreact.ts");
1304
+ }
1305
+
1306
+ function hasMetadataExport(code: string): boolean {
1307
+ return /\bexport\s+const\s+metadata\s*=/.test(code) ||
1308
+ /\bexport\s+(?:async\s+)?function\s+generateMetadata\b/.test(code) ||
1309
+ /\bexport\s*\{[^}]*\bgenerateMetadata\b[^}]*\}/.test(code);
1310
+ }
1311
+
1312
+ async function readDeclaredProjectPackages(projectRoot: string): Promise<string[]> {
1313
+ try {
1314
+ const json = JSON.parse(await readFile(join(projectRoot, "package.json"), "utf8")) as {
1315
+ dependencies?: Record<string, unknown> | undefined;
1316
+ devDependencies?: Record<string, unknown> | undefined;
1317
+ optionalDependencies?: Record<string, unknown> | undefined;
1318
+ peerDependencies?: Record<string, unknown> | undefined;
1319
+ };
1320
+
1321
+ return [
1322
+ ...Object.keys(json.dependencies ?? {}),
1323
+ ...Object.keys(json.devDependencies ?? {}),
1324
+ ...Object.keys(json.optionalDependencies ?? {}),
1325
+ ...Object.keys(json.peerDependencies ?? {}),
1326
+ ];
1327
+ } catch {
1328
+ return [];
1329
+ }
1330
+ }
1331
+
1332
+ interface CloudflareRouteModulesOutput {
1333
+ registryFile: string;
1334
+ }
1335
+
1336
+ async function writeCloudflareRouteModules(options: {
1337
+ cloudflareDir: string;
1338
+ files: Record<string, string>;
1339
+ prerenderedRoutes: Record<string, BuiltPrerenderedRoute>;
1340
+ projectRoot: string;
1341
+ routesDir: string;
1342
+ routes: readonly AppRoute[];
1343
+ serverModules: Record<string, BuiltServerModuleArtifact>;
1344
+ vitePlugins?: readonly PluginOption[] | undefined;
1345
+ }): Promise<CloudflareRouteModulesOutput> {
1346
+ const routesDir = join(options.cloudflareDir, "routes");
1347
+ const requiredRoutes = options.routes.filter((route) =>
1348
+ cloudflareRouteRequiresGeneratedModule(route, options.prerenderedRoutes),
1349
+ );
1350
+ const registryEntries: string[] = [];
1351
+
1352
+ await mkdir(routesDir, { recursive: true });
1353
+
1354
+ for (const route of requiredRoutes) {
1355
+ const routeFile = relative(options.projectRoot, route.file).replaceAll(sep, "/");
1356
+ const source = await readFile(route.file, "utf8");
1357
+ const routeId = routeIdForPath(route.path);
1358
+ const routeModuleFile = `routes/${routeId}.mjs`;
1359
+ let routeModuleExports: string[];
1360
+
1361
+ if (route.kind === "server" || route.kind === "metadata") {
1362
+ try {
1363
+ const routeOutput = await buildCloudflareServerRouteModule({
1364
+ filename: route.file,
1365
+ vitePlugins: options.vitePlugins,
1366
+ });
1367
+ const serverRouteFile = `routes/${routeId}.${hashText(routeOutput).slice(0, 8)}.server.mjs`;
1368
+
1369
+ await writeFile(join(options.cloudflareDir, serverRouteFile), routeOutput);
1370
+ const serverRouteImport = `./${serverRouteFile.split("/").pop() ?? serverRouteFile}`;
1371
+ routeModuleExports = [
1372
+ `export * from ${JSON.stringify(serverRouteImport)};`,
1373
+ ...(route.kind === "metadata"
1374
+ ? [`export { default } from ${JSON.stringify(serverRouteImport)};`]
1375
+ : []),
1376
+ ];
1377
+ } catch (error) {
1378
+ throw new Error(
1379
+ `Failed to build Cloudflare ${route.kind} route module for ${routeFile}: ${errorMessage(error)}`,
1380
+ );
1381
+ }
1382
+
1383
+ await writeFile(join(options.cloudflareDir, routeModuleFile), `${routeModuleExports.join("\n")}\n`);
1384
+ registryEntries.push(
1385
+ `${JSON.stringify(routeFile)}: () => import(${JSON.stringify(`./${routeModuleFile}`)})`,
1386
+ );
1387
+ continue;
1388
+ }
1389
+
1390
+ const serverOutput =
1391
+ options.serverModules[routeFile]?.analysis?.streamRoute === true ||
1392
+ shouldBuildRouteAsStream({
1393
+ filename: routeFile,
1394
+ files: options.files,
1395
+ projectRoot: options.projectRoot,
1396
+ source,
1397
+ })
1398
+ ? "stream"
1399
+ : "string";
1400
+
1401
+ try {
1402
+ const componentOutput =
1403
+ serverOutput === "stream"
1404
+ ? await buildCloudflareStreamRouteComponentModule({
1405
+ filename: route.file,
1406
+ projectRoot: options.projectRoot,
1407
+ routesDir: options.routesDir,
1408
+ serverModules: options.serverModules,
1409
+ vitePlugins: options.vitePlugins,
1410
+ })
1411
+ : await buildCloudflareStringRouteComponentModule({
1412
+ filename: route.file,
1413
+ projectRoot: options.projectRoot,
1414
+ routesDir: options.routesDir,
1415
+ serverModules: options.serverModules,
1416
+ vitePlugins: options.vitePlugins,
1417
+ });
1418
+ const componentFile = `routes/${routeId}.${hashText(componentOutput).slice(0, 8)}.component.mjs`;
1419
+
1420
+ await writeFile(join(options.cloudflareDir, componentFile), componentOutput);
1421
+
1422
+ const componentImport = `./${componentFile.split("/").pop() ?? componentFile}`;
1423
+ routeModuleExports = [
1424
+ `export { default, App, slots } from ${JSON.stringify(componentImport)};`,
1425
+ ];
1426
+ } catch (error) {
1427
+ throw new Error(
1428
+ `Failed to build Cloudflare route module for ${routeFile}: ${errorMessage(error)}`,
1429
+ );
1430
+ }
1431
+
1432
+ if (hasLoaderExport(source)) {
1433
+ try {
1434
+ const loaderOutput = await buildCloudflareRouteLoaderModule({
1435
+ filename: route.file,
1436
+ projectRoot: options.projectRoot,
1437
+ vitePlugins: options.vitePlugins,
1438
+ });
1439
+ const loaderFile = `routes/${routeId}.${hashText(loaderOutput).slice(0, 8)}.loader.mjs`;
1440
+
1441
+ await writeFile(join(options.cloudflareDir, loaderFile), loaderOutput);
1442
+ const loaderImport = `./${loaderFile.split("/").pop() ?? loaderFile}`;
1443
+ routeModuleExports.push(`export { loader } from ${JSON.stringify(loaderImport)};`);
1444
+ } catch (error) {
1445
+ throw new Error(
1446
+ `Failed to build Cloudflare loader module for ${routeFile}: ${errorMessage(error)}`,
1447
+ );
1448
+ }
1449
+ }
1450
+
1451
+ await writeFile(join(options.cloudflareDir, routeModuleFile), `${routeModuleExports.join("\n")}\n`);
1452
+ registryEntries.push(
1453
+ `${JSON.stringify(routeFile)}: () => import(${JSON.stringify(`./${routeModuleFile}`)})`,
1454
+ );
1455
+ }
1456
+
1457
+ const registrySource = [
1458
+ `export const routeModules = {`,
1459
+ ...registryEntries.map((entry) => ` ${entry},`),
1460
+ `};`,
1461
+ `export default routeModules;`,
1462
+ ``,
1463
+ ].join("\n");
1464
+
1465
+ await writeFile(join(options.cloudflareDir, "route-modules.mjs"), registrySource);
1466
+
1467
+ return { registryFile: "route-modules.mjs" };
1468
+ }
1469
+
1470
+ interface CloudflareShellFile {
1471
+ file: string;
1472
+ id: string;
1473
+ kind: "layout" | "template";
1474
+ }
1475
+
1476
+ async function buildCloudflareServerComponentModule(options: {
1477
+ filename: string;
1478
+ projectRoot: string;
1479
+ serverOutput: ServerOutputMode;
1480
+ serverModules: Record<string, BuiltServerModuleArtifact>;
1481
+ vitePlugins?: readonly PluginOption[] | undefined;
1482
+ }): Promise<string> {
1483
+ const metadataModule = await buildCloudflareRouteMetadataExportModule({
1484
+ filename: options.filename,
1485
+ vitePlugins: options.vitePlugins,
1486
+ });
1487
+ const entry = `import * as routeModule from ${JSON.stringify(options.filename)};
1488
+ ${metadataModule === undefined ? "const metadataModule = {};" : 'import * as metadataModule from "mreact:metadata";'}
1489
+
1490
+ const component = routeModule.default ?? routeModule.App ?? Object.values(routeModule).find((value) => typeof value === "function");
1491
+ export const App = component;
1492
+ export default component;
1493
+ export const generateMetadata = metadataModule.generateMetadata;
1494
+ export const metadata = metadataModule.metadata;
1495
+ export const slots = routeModule.slots;`;
1496
+
1497
+ return bundleCloudflareVirtualModule({
1498
+ entry,
1499
+ filename: `${options.filename}.mreact-cloudflare-component.js`,
1500
+ modules: metadataModule === undefined
1501
+ ? new Map()
1502
+ : new Map([["mreact:metadata", metadataModule]]),
1503
+ plugins: [
1504
+ cloudflareServerSourceTransformPlugin({
1505
+ projectRoot: options.projectRoot,
1506
+ serverOutput: options.serverOutput,
1507
+ serverModules: options.serverModules,
1508
+ vitePlugins: options.vitePlugins,
1509
+ }),
1510
+ cloudflareWorkspaceRuntimePlugin(),
1511
+ ],
1512
+ resolveDir: dirname(options.filename),
1513
+ vitePlugins: options.vitePlugins,
1514
+ });
1515
+ }
1516
+
1517
+ async function buildCloudflareStringRouteComponentModule(options: {
1518
+ filename: string;
1519
+ projectRoot: string;
1520
+ routesDir: string;
1521
+ serverModules: Record<string, BuiltServerModuleArtifact>;
1522
+ vitePlugins?: readonly PluginOption[] | undefined;
1523
+ }): Promise<string> {
1524
+ const shellFiles = await cloudflareShellFilesForPage(options.routesDir, options.filename);
1525
+
1526
+ if (shellFiles.length === 0) {
1527
+ return buildCloudflareServerComponentModule({
1528
+ filename: options.filename,
1529
+ projectRoot: options.projectRoot,
1530
+ serverModules: options.serverModules,
1531
+ serverOutput: "string",
1532
+ vitePlugins: options.vitePlugins,
1533
+ });
1534
+ }
1535
+
1536
+ const pageModule = await buildCloudflareComponentExportModule({
1537
+ filename: options.filename,
1538
+ projectRoot: options.projectRoot,
1539
+ serverModules: options.serverModules,
1540
+ serverOutput: "string",
1541
+ vitePlugins: options.vitePlugins,
1542
+ });
1543
+ const shellModules = await Promise.all(
1544
+ shellFiles.map((shell) =>
1545
+ buildCloudflareComponentExportModule({
1546
+ filename: shell.file,
1547
+ projectRoot: options.projectRoot,
1548
+ serverModules: options.serverModules,
1549
+ serverOutput: "string",
1550
+ vitePlugins: options.vitePlugins,
1551
+ }),
1552
+ ),
1553
+ );
1554
+ const shellImports = shellFiles.map((_, index) => `import * as shell${index} from "mreact:shell-${index}";`);
1555
+ const shellDefinitions = shellFiles.map(
1556
+ (shell, index) =>
1557
+ `{ component: selectComponent(shell${index}, ${JSON.stringify(shell.file)}), id: ${JSON.stringify(shell.id)}, kind: ${JSON.stringify(shell.kind)}, module: shell${index} }`,
1558
+ );
1559
+ const entry = `import * as pageModule from "mreact:page";
1560
+ ${shellImports.join("\n")}
1561
+
1562
+ const pageComponent = selectComponent(pageModule, ${JSON.stringify(options.filename)});
1563
+ const shells = [${shellDefinitions.join(", ")}];
1564
+ export const slots = pageModule.slots;
1565
+ export const App = renderCloudflareStringRoute;
1566
+ export default renderCloudflareStringRoute;
1567
+
1568
+ async function renderCloudflareStringRoute(props) {
1569
+ const slotHtml = await renderRouteSlots(pageModule.slots, props);
1570
+ const layoutShells = await renderLayoutShells(shells, props, slotHtml);
1571
+ const metadata = await resolveRouteMetadata([...shells.map((shell) => shell.module), pageModule], props);
1572
+ let html = "<!DOCTYPE html>";
1573
+ for (const shell of layoutShells) {
1574
+ html += shell.prefix;
1575
+ }
1576
+ html += String(await pageComponent(props) ?? "");
1577
+ for (const shell of [...layoutShells].reverse()) {
1578
+ html += shell.suffix;
1579
+ }
1580
+ html = injectCloudflareHead(html, metadata, cloudflareRouteHeadTags(props.clientManifest, props.route.path));
1581
+ return new Response(html, {
1582
+ headers: {
1583
+ "content-type": "text/html; charset=utf-8"
1584
+ }
1585
+ });
1586
+ }
1587
+
1588
+ function selectComponent(module, label) {
1589
+ const component = module.default ?? module.App ?? Object.values(module).find((value) => typeof value === "function");
1590
+ if (typeof component !== "function") {
1591
+ throw new Error(\`No Cloudflare component export was found for \${label}.\`);
1592
+ }
1593
+ return component;
1594
+ }
1595
+
1596
+ async function renderRouteSlots(slots, props) {
1597
+ if (slots === undefined) {
1598
+ return {};
1599
+ }
1600
+ const rendered = {};
1601
+ for (const [name, value] of Object.entries(slots)) {
1602
+ rendered[name] = typeof value === "function" ? String(await value(props) ?? "") : String(value ?? "");
1603
+ }
1604
+ return rendered;
1605
+ }
1606
+
1607
+ ${cloudflareShellRuntimeSource()}`;
1608
+
1609
+ return bundleCloudflareVirtualModule({
1610
+ entry,
1611
+ filename: `${options.filename}.mreact-cloudflare-string-route.js`,
1612
+ modules: new Map([
1613
+ ["mreact:page", pageModule],
1614
+ ...shellModules.map((source, index) => [`mreact:shell-${index}`, source] as const),
1615
+ ]),
1616
+ plugins: [cloudflareWorkspaceRuntimePlugin()],
1617
+ resolveDir: dirname(options.filename),
1618
+ vitePlugins: options.vitePlugins,
1619
+ });
1620
+ }
1621
+
1622
+ async function buildCloudflareStreamRouteComponentModule(options: {
1623
+ filename: string;
1624
+ projectRoot: string;
1625
+ routesDir: string;
1626
+ serverModules: Record<string, BuiltServerModuleArtifact>;
1627
+ vitePlugins?: readonly PluginOption[] | undefined;
1628
+ }): Promise<string> {
1629
+ const pageModule = await buildCloudflareComponentExportModule({
1630
+ filename: options.filename,
1631
+ projectRoot: options.projectRoot,
1632
+ serverModules: options.serverModules,
1633
+ serverOutput: "stream",
1634
+ vitePlugins: options.vitePlugins,
1635
+ });
1636
+ const shellFiles = await cloudflareShellFilesForPage(options.routesDir, options.filename);
1637
+ const shellModules = await Promise.all(
1638
+ shellFiles.map((shell) =>
1639
+ buildCloudflareComponentExportModule({
1640
+ filename: shell.file,
1641
+ projectRoot: options.projectRoot,
1642
+ serverModules: options.serverModules,
1643
+ serverOutput: "string",
1644
+ vitePlugins: options.vitePlugins,
1645
+ }),
1646
+ ),
1647
+ );
1648
+ const shellImports = shellFiles.map((_, index) => `import * as shell${index} from "mreact:shell-${index}";`);
1649
+ const shellDefinitions = shellFiles.map(
1650
+ (shell, index) =>
1651
+ `{ component: selectComponent(shell${index}, ${JSON.stringify(shell.file)}), id: ${JSON.stringify(shell.id)}, kind: ${JSON.stringify(shell.kind)}, module: shell${index} }`,
1652
+ );
1653
+ const entry = `import { createStringSink, renderOutOfOrderReorderScript, renderToReadableStream } from "@reckona/mreact-server";
1654
+ import * as pageModule from "mreact:page";
1655
+ ${shellImports.join("\n")}
1656
+
1657
+ const pageComponent = selectComponent(pageModule, ${JSON.stringify(options.filename)});
1658
+ const shells = [${shellDefinitions.join(", ")}];
1659
+ export const slots = pageModule.slots;
1660
+ export const App = renderCloudflareStreamRoute;
1661
+ export default renderCloudflareStreamRoute;
1662
+
1663
+ function renderCloudflareStreamRoute(props) {
1664
+ const body = renderToReadableStream(async ($sink) => {
1665
+ const slotHtml = await renderRouteSlots(pageModule.slots, props);
1666
+ const layoutShells = await renderLayoutShells(shells, props, slotHtml);
1667
+ const metadata = await resolveRouteMetadata([...shells.map((shell) => shell.module), pageModule], props);
1668
+ const routeHeadTags = cloudflareRouteHeadTags(props.clientManifest, props.route.path);
1669
+ $sink.append("<!DOCTYPE html>");
1670
+ if (layoutShells.length === 0) {
1671
+ $sink.append(injectCloudflareHead("", metadata, routeHeadTags));
1672
+ }
1673
+ for (let index = 0; index < layoutShells.length; index += 1) {
1674
+ const shell = layoutShells[index];
1675
+ $sink.append(index === 0 ? injectCloudflareHead(shell.prefix, metadata, routeHeadTags) : shell.prefix);
1676
+ }
1677
+ await pageComponent($sink, props);
1678
+ renderOutOfOrderReorderScript($sink);
1679
+ for (const shell of [...layoutShells].reverse()) {
1680
+ $sink.append(shell.suffix);
1681
+ }
1682
+ });
1683
+ return new Response(body, {
1684
+ headers: {
1685
+ "content-type": "text/html; charset=utf-8",
1686
+ "x-mreact-stream": "1"
1687
+ }
1688
+ });
1689
+ }
1690
+
1691
+ function selectComponent(module, label) {
1692
+ const component = module.default ?? module.App ?? Object.values(module).find((value) => typeof value === "function");
1693
+ if (typeof component !== "function") {
1694
+ throw new Error(\`No Cloudflare component export was found for \${label}.\`);
1695
+ }
1696
+ return component;
1697
+ }
1698
+
1699
+ async function renderRouteSlots(slots, props) {
1700
+ if (slots === undefined) {
1701
+ return {};
1702
+ }
1703
+ const rendered = {};
1704
+ for (const [name, value] of Object.entries(slots)) {
1705
+ if (typeof value !== "function") {
1706
+ rendered[name] = String(value ?? "");
1707
+ continue;
1708
+ }
1709
+ const sink = createStringSink();
1710
+ await value(sink, props);
1711
+ await sink.drain();
1712
+ rendered[name] = sink.toString();
1713
+ }
1714
+ return rendered;
1715
+ }
1716
+
1717
+ ${cloudflareShellRuntimeSource()}`;
1718
+
1719
+ return bundleCloudflareVirtualModule({
1720
+ entry,
1721
+ filename: `${options.filename}.mreact-cloudflare-stream-route.js`,
1722
+ modules: new Map([
1723
+ ["mreact:page", pageModule],
1724
+ ...shellModules.map((source, index) => [`mreact:shell-${index}`, source] as const),
1725
+ ]),
1726
+ plugins: [cloudflareWorkspaceRuntimePlugin()],
1727
+ resolveDir: dirname(options.filename),
1728
+ vitePlugins: options.vitePlugins,
1729
+ });
1730
+ }
1731
+
1732
+ function cloudflareShellRuntimeSource(): string {
1733
+ return `async function renderLayoutShells(shells, props, namedSlots) {
1734
+ const slotContext = { consumedSlots: new Set(), namedSlots };
1735
+ const rendered = [];
1736
+ for (const shell of shells) {
1737
+ const html = await shell.component(props);
1738
+ rendered.push(splitLayoutSlot(markShellBoundary(String(html ?? ""), shell), slotContext));
1739
+ }
1740
+ return rendered;
1741
+ }
1742
+
1743
+ function splitLayoutSlot(layoutHtml, slotContext) {
1744
+ const html = replaceNamedLayoutSlots(layoutHtml, slotContext);
1745
+ const match = findDefaultLayoutSlot(html);
1746
+ if (match === null) {
1747
+ return { prefix: html, suffix: "" };
1748
+ }
1749
+ return {
1750
+ prefix: html.slice(0, match.index),
1751
+ suffix: html.slice(match.index + match[0].length),
1752
+ };
1753
+ }
1754
+
1755
+ function markShellBoundary(html, shell) {
1756
+ const attributeName = shell.kind === "layout" ? "data-mreact-layout-boundary" : "data-mreact-template-boundary";
1757
+ if (html.includes(\`\${attributeName}=\`)) {
1758
+ return html;
1759
+ }
1760
+ return html.replace(/<([A-Za-z][^\\s/>]*)([^>]*)>/, \`<$1$2 \${attributeName}="\${escapeHtmlAttribute(shell.id)}">\`);
1761
+ }
1762
+
1763
+ const SLOT_TAG_PATTERN = /<slot\\b([^>]*)>(?:<\\/slot\\s*>)?/g;
1764
+
1765
+ function replaceNamedLayoutSlots(layoutHtml, slotContext) {
1766
+ return layoutHtml.replace(SLOT_TAG_PATTERN, (source, openAttributes) => {
1767
+ const name = readSlotName(openAttributes);
1768
+ if (name === undefined || name === "default") {
1769
+ return source;
1770
+ }
1771
+ if (Object.hasOwn(slotContext.namedSlots, name)) {
1772
+ slotContext.consumedSlots.add(name);
1773
+ return slotContext.namedSlots[name] ?? "";
1774
+ }
1775
+ return "";
1776
+ });
1777
+ }
1778
+
1779
+ function findDefaultLayoutSlot(html) {
1780
+ SLOT_TAG_PATTERN.lastIndex = 0;
1781
+ for (;;) {
1782
+ const match = SLOT_TAG_PATTERN.exec(html);
1783
+ if (match === null) {
1784
+ return null;
1785
+ }
1786
+ const name = readSlotName(match[1] ?? "");
1787
+ if (name === undefined || name === "default") {
1788
+ return match;
1789
+ }
1790
+ }
1791
+ }
1792
+
1793
+ function readSlotName(attributes) {
1794
+ const match = /\\bname\\s*=\\s*(?:"([^"]*)"|'([^']*)')/.exec(attributes);
1795
+ return match?.[1] ?? match?.[2];
1796
+ }
1797
+
1798
+ async function resolveRouteMetadata(modules, props) {
1799
+ const metadata = [];
1800
+ const context = {
1801
+ data: props.data,
1802
+ params: props.params,
1803
+ request: props.request
1804
+ };
1805
+ for (const module of modules) {
1806
+ let next = module.metadata;
1807
+ if (typeof module.generateMetadata === "function") {
1808
+ const generated = await module.generateMetadata(context);
1809
+ next = mergeRouteMetadata([next, generated].filter(Boolean));
1810
+ }
1811
+ if (next !== undefined) {
1812
+ metadata.push(next);
1813
+ }
1814
+ }
1815
+ return mergeRouteMetadata(metadata);
1816
+ }
1817
+
1818
+ function mergeRouteMetadata(metadata) {
1819
+ if (metadata.length === 0) {
1820
+ return undefined;
1821
+ }
1822
+ return metadata.reduce((merged, next) => ({
1823
+ ...merged,
1824
+ ...next,
1825
+ alternates: mergeObject(merged.alternates, next.alternates),
1826
+ csp: mergeObject(merged.csp, next.csp),
1827
+ head: mergeArrays(merged.head, next.head),
1828
+ icons: mergeObject(merged.icons, next.icons),
1829
+ openGraph: {
1830
+ ...mergeObject(merged.openGraph, next.openGraph),
1831
+ images: mergeArrays(merged.openGraph?.images, next.openGraph?.images),
1832
+ },
1833
+ }), {});
1834
+ }
1835
+
1836
+ function mergeObject(left, right) {
1837
+ if (left === undefined) {
1838
+ return right;
1839
+ }
1840
+ if (right === undefined) {
1841
+ return left;
1842
+ }
1843
+ return { ...left, ...right };
1844
+ }
1845
+
1846
+ function mergeArrays(left, right) {
1847
+ if (left === undefined || left.length === 0) {
1848
+ return right;
1849
+ }
1850
+ if (right === undefined || right.length === 0) {
1851
+ return left;
1852
+ }
1853
+ return [...left, ...right];
1854
+ }
1855
+
1856
+ function injectCloudflareHead(html, metadata, routeHeadTags) {
1857
+ const metadataTags = routeMetadataHeadTags(metadata);
1858
+ const tags = routeHeadTags + metadataTags;
1859
+ let nextHtml = metadata?.lang === undefined ? html : injectHtmlLangAttribute(html, metadataString(metadata.lang));
1860
+
1861
+ if (metadata?.title !== undefined) {
1862
+ nextHtml = nextHtml.replace(/<title\\b[^>]*>[\\s\\S]*?<\\/title\\s*>/i, "");
1863
+ }
1864
+
1865
+ if (tags === "") {
1866
+ return nextHtml;
1867
+ }
1868
+ if (/<head(?:\\s[^>]*)?>/i.test(nextHtml)) {
1869
+ return nextHtml.replace(/<head(\\s[^>]*)?>/i, (match) => \`\${match}\${tags}\`);
1870
+ }
1871
+ if (/<html(?:\\s[^>]*)?>/i.test(nextHtml)) {
1872
+ return nextHtml.replace(/<html(\\s[^>]*)?>/i, (match) => \`\${match}<head>\${tags}</head>\`);
1873
+ }
1874
+ return \`<head>\${tags}</head>\${nextHtml}\`;
1875
+ }
1876
+
1877
+ function routeMetadataHeadTags(metadata) {
1878
+ if (metadata === undefined) {
1879
+ return "";
1880
+ }
1881
+ const tags = [
1882
+ metadata.title === undefined ? undefined : \`<title>\${escapeHtml(metadataString(metadata.title))}</title>\`,
1883
+ metadata.description === undefined ? undefined : \`<meta name="description" content="\${escapeHtmlAttribute(metadataString(metadata.description))}">\`,
1884
+ metadata.alternates?.canonical === undefined ? undefined : \`<link rel="canonical" href="\${escapeHtmlAttribute(metadataString(metadata.alternates.canonical))}">\`,
1885
+ metadata.openGraph?.title === undefined ? undefined : \`<meta property="og:title" content="\${escapeHtmlAttribute(metadataString(metadata.openGraph.title))}">\`,
1886
+ metadata.openGraph?.description === undefined ? undefined : \`<meta property="og:description" content="\${escapeHtmlAttribute(metadataString(metadata.openGraph.description))}">\`,
1887
+ ...openGraphImages(metadata.openGraph).map((image) => \`<meta property="og:image" content="\${escapeHtmlAttribute(image)}">\`),
1888
+ metadata.icons?.icon === undefined ? undefined : \`<link rel="icon" href="\${escapeHtmlAttribute(metadataString(metadata.icons.icon))}">\`,
1889
+ metadata.icons?.apple === undefined ? undefined : \`<link rel="apple-touch-icon" href="\${escapeHtmlAttribute(metadataString(metadata.icons.apple))}">\`,
1890
+ ...headDescriptorTags(metadata.head, metadata.csp?.nonce),
1891
+ ];
1892
+ return tags.filter((tag) => tag !== undefined).join("");
1893
+ }
1894
+
1895
+ function openGraphImages(openGraph) {
1896
+ if (openGraph?.images !== undefined && openGraph.images.length > 0) {
1897
+ return openGraph.images.map(metadataImageUrl);
1898
+ }
1899
+ return openGraph?.image === undefined ? [] : [metadataImageUrl(openGraph.image)];
1900
+ }
1901
+
1902
+ function headDescriptorTags(descriptors, nonce) {
1903
+ return (descriptors ?? []).flatMap((descriptor) => {
1904
+ const descriptorNonce = descriptor.nonce === true ? nonce : descriptor.nonce || undefined;
1905
+ const attrs = {
1906
+ ...descriptor.attrs,
1907
+ ...(descriptorNonce === undefined ? {} : { nonce: descriptorNonce }),
1908
+ };
1909
+ const attrText = Object.entries(attrs)
1910
+ .flatMap(([name, value]) => {
1911
+ if (value === undefined || value === false) {
1912
+ return [];
1913
+ }
1914
+ return value === true
1915
+ ? [escapeHtmlAttribute(name)]
1916
+ : [\`\${escapeHtmlAttribute(name)}="\${escapeHtmlAttribute(String(value))}"\`];
1917
+ })
1918
+ .join(" ");
1919
+ const open = attrText === "" ? \`<\${descriptor.tag}>\` : \`<\${descriptor.tag} \${attrText}>\`;
1920
+ if (descriptor.tag === "meta" || descriptor.tag === "link" || descriptor.tag === "base") {
1921
+ return [open.slice(0, -1) + ">"];
1922
+ }
1923
+ return [\`\${open}\${String(descriptor.content ?? "").replaceAll("<", "\\\\u003c")}</\${descriptor.tag}>\`];
1924
+ });
1925
+ }
1926
+
1927
+ function injectHtmlLangAttribute(html, lang) {
1928
+ const escapedLang = escapeHtmlAttribute(lang);
1929
+ if (!/<html(?:\\s[^>]*)?>/i.test(html)) {
1930
+ return html;
1931
+ }
1932
+ return html.replace(/<html(\\s[^>]*)?>/i, (_match, attrs = "") => {
1933
+ const strippedAttrs = String(attrs).replace(/\\s+lang=(?:"[^"]*"|'[^']*'|[^\\s>]+)/i, "");
1934
+ return \`<html lang="\${escapedLang}"\${strippedAttrs}>\`;
1935
+ });
1936
+ }
1937
+
1938
+ function metadataString(value) {
1939
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1940
+ return String(value);
1941
+ }
1942
+ throw new Error("Invalid metadata field: expected string, number, or boolean.");
1943
+ }
1944
+
1945
+ function metadataImageUrl(value) {
1946
+ if (typeof value === "object" && value !== null && "url" in value) {
1947
+ return metadataString(value.url);
1948
+ }
1949
+ return metadataString(value);
1950
+ }
1951
+
1952
+ function cloudflareRouteHeadTags(manifest, routePath) {
1953
+ const route = manifest.routes.find((route) => route.path === routePath);
1954
+ const css = route?.css ?? [];
1955
+ const styles = css
1956
+ .map((styleSheet) => \`<link rel="stylesheet" href="/_mreact/client/\${escapeHtmlAttribute(styleSheet)}">\`)
1957
+ .join("");
1958
+ const script = route?.script;
1959
+ const preload = script === undefined
1960
+ ? ""
1961
+ : \`<link rel="modulepreload" href="/_mreact/client/\${escapeHtmlAttribute(script)}">\`;
1962
+ return styles + preload;
1963
+ }
1964
+
1965
+ function escapeHtmlAttribute(value) {
1966
+ return String(value ?? "")
1967
+ .replaceAll("&", "&amp;")
1968
+ .replaceAll('"', "&quot;")
1969
+ .replaceAll("<", "&lt;");
1970
+ }
1971
+
1972
+ function escapeHtml(value) {
1973
+ return String(value ?? "")
1974
+ .replaceAll("&", "&amp;")
1975
+ .replaceAll("<", "&lt;")
1976
+ .replaceAll(">", "&gt;");
1977
+ }`;
1978
+ }
1979
+
1980
+ async function buildCloudflareComponentExportModule(options: {
1981
+ filename: string;
1982
+ projectRoot: string;
1983
+ serverModules: Record<string, BuiltServerModuleArtifact>;
1984
+ serverOutput: ServerOutputMode;
1985
+ vitePlugins?: readonly PluginOption[] | undefined;
1986
+ }): Promise<string> {
1987
+ const metadataModule = await buildCloudflareRouteMetadataExportModule({
1988
+ filename: options.filename,
1989
+ vitePlugins: options.vitePlugins,
1990
+ });
1991
+ const entry = `import * as routeModule from ${JSON.stringify(options.filename)};
1992
+ ${metadataModule === undefined ? "const metadataModule = {};" : 'import * as metadataModule from "mreact:metadata";'}
1993
+
1994
+ const component = routeModule.default ?? routeModule.App ?? Object.values(routeModule).find((value) => typeof value === "function");
1995
+ export const App = component;
1996
+ export default component;
1997
+ export const generateMetadata = metadataModule.generateMetadata;
1998
+ export const metadata = metadataModule.metadata;
1999
+ export const slots = routeModule.slots;`;
2000
+
2001
+ return bundleCloudflareVirtualModule({
2002
+ entry,
2003
+ filename: `${options.filename}.mreact-cloudflare-${options.serverOutput}-component.js`,
2004
+ modules: metadataModule === undefined
2005
+ ? new Map()
2006
+ : new Map([["mreact:metadata", metadataModule]]),
2007
+ plugins: [
2008
+ cloudflareServerSourceTransformPlugin({
2009
+ projectRoot: options.projectRoot,
2010
+ serverOutput: options.serverOutput,
2011
+ serverModules: options.serverModules,
2012
+ vitePlugins: options.vitePlugins,
2013
+ }),
2014
+ cloudflareWorkspaceRuntimePlugin(),
2015
+ ],
2016
+ resolveDir: dirname(options.filename),
2017
+ vitePlugins: options.vitePlugins,
2018
+ });
2019
+ }
2020
+
2021
+ async function buildCloudflareRouteLoaderModule(options: {
2022
+ filename: string;
2023
+ projectRoot: string;
2024
+ vitePlugins?: readonly PluginOption[] | undefined;
2025
+ }): Promise<string> {
2026
+ const entry = `export { loader } from ${JSON.stringify(options.filename)};`;
2027
+
2028
+ return bundleCloudflareModule({
2029
+ entry,
2030
+ filename: `${options.filename}.mreact-cloudflare-loader.js`,
2031
+ plugins: [cloudflareWorkspaceRuntimePlugin()],
2032
+ resolveDir: dirname(options.filename),
2033
+ vitePlugins: options.vitePlugins,
2034
+ });
2035
+ }
2036
+
2037
+ async function buildCloudflareRouteMetadataExportModule(options: {
2038
+ filename: string;
2039
+ vitePlugins?: readonly PluginOption[] | undefined;
2040
+ }): Promise<string | undefined> {
2041
+ const source = await readFile(options.filename, "utf8");
2042
+
2043
+ if (!hasMetadataExport(source)) {
2044
+ return undefined;
2045
+ }
2046
+
2047
+ const entry = `import * as routeMetadataModule from ${JSON.stringify(options.filename)};
2048
+ export const generateMetadata = routeMetadataModule.generateMetadata;
2049
+ export const metadata = routeMetadataModule.metadata;`;
2050
+
2051
+ return bundleCloudflareModule({
2052
+ entry,
2053
+ filename: `${options.filename}.mreact-cloudflare-metadata.js`,
2054
+ plugins: [cloudflareWorkspaceRuntimePlugin()],
2055
+ resolveDir: dirname(options.filename),
2056
+ vitePlugins: options.vitePlugins,
2057
+ });
2058
+ }
2059
+
2060
+ async function buildCloudflareServerRouteModule(options: {
2061
+ filename: string;
2062
+ vitePlugins?: readonly PluginOption[] | undefined;
2063
+ }): Promise<string> {
2064
+ const entry = `import * as routeModule from ${JSON.stringify(options.filename)};
2065
+
2066
+ export const GET = routeModule.GET;
2067
+ export const HEAD = routeModule.HEAD;
2068
+ export const POST = routeModule.POST;
2069
+ export const PUT = routeModule.PUT;
2070
+ export const PATCH = routeModule.PATCH;
2071
+ export const DELETE = routeModule.DELETE;
2072
+ export const OPTIONS = routeModule.OPTIONS;
2073
+ export const ALL = routeModule.ALL;
2074
+ const defaultHandler = routeModule.default;
2075
+ export default defaultHandler;`;
2076
+
2077
+ return bundleCloudflareModule({
2078
+ entry,
2079
+ filename: `${options.filename}.mreact-cloudflare-server-route.js`,
2080
+ plugins: [cloudflareWorkspaceRuntimePlugin()],
2081
+ resolveDir: dirname(options.filename),
2082
+ vitePlugins: options.vitePlugins,
2083
+ });
2084
+ }
2085
+
2086
+ async function bundleCloudflareModule(options: {
2087
+ entry: string;
2088
+ filename: string;
2089
+ plugins: RouterCompatPlugin[];
2090
+ resolveDir: string;
2091
+ vitePlugins?: readonly PluginOption[] | undefined;
2092
+ }): Promise<string> {
2093
+ const output = await bundleRouterModule({
2094
+ code: options.entry,
2095
+ filename: options.filename,
2096
+ minify: true,
2097
+ platform: "browser",
2098
+ preserveExports: true,
2099
+ plugins: options.plugins,
2100
+ target: "es2022",
2101
+ vitePlugins: options.vitePlugins,
2102
+ });
2103
+ const code = output.code;
2104
+
2105
+ if (code === undefined) {
2106
+ throw new Error(`Failed to build Cloudflare route module for ${options.filename}.`);
2107
+ }
2108
+
2109
+ return code;
2110
+ }
2111
+
2112
+ async function bundleCloudflareVirtualModule(options: {
2113
+ entry: string;
2114
+ filename: string;
2115
+ modules: ReadonlyMap<string, string>;
2116
+ plugins: RouterCompatPlugin[];
2117
+ resolveDir: string;
2118
+ vitePlugins?: readonly PluginOption[] | undefined;
2119
+ }): Promise<string> {
2120
+ return bundleCloudflareModule({
2121
+ entry: options.entry,
2122
+ filename: options.filename,
2123
+ plugins: [
2124
+ {
2125
+ name: "mreact-cloudflare-virtual-modules",
2126
+ setup(buildApi) {
2127
+ buildApi.onResolve({ filter: /^mreact:/ }, (args) => ({
2128
+ namespace: "mreact-cloudflare-virtual",
2129
+ path: args.path,
2130
+ }));
2131
+ buildApi.onLoad({ filter: /.*/, namespace: "mreact-cloudflare-virtual" }, (args) => {
2132
+ const contents = options.modules.get(args.path);
2133
+
2134
+ if (contents === undefined) {
2135
+ throw new Error(`Missing virtual Cloudflare module ${args.path}.`);
2136
+ }
2137
+
2138
+ return {
2139
+ contents,
2140
+ loader: "js",
2141
+ resolveDir: options.resolveDir,
2142
+ };
2143
+ });
2144
+ },
2145
+ },
2146
+ ...options.plugins,
2147
+ ],
2148
+ resolveDir: options.resolveDir,
2149
+ vitePlugins: options.vitePlugins,
2150
+ });
2151
+ }
2152
+
2153
+ async function cloudflareShellFilesForPage(
2154
+ routesDir: string,
2155
+ pageFile: string,
2156
+ ): Promise<CloudflareShellFile[]> {
2157
+ const shells = await existingRouteShellCandidates(routesDir, pageFile, async (file) => {
2158
+ try {
2159
+ return (await stat(file)).isFile();
2160
+ } catch {
2161
+ return false;
2162
+ }
2163
+ });
2164
+
2165
+ return shells.map((shell) => ({
2166
+ file: shell.file,
2167
+ id: cloudflareShellBoundaryId(routesDir, shell.directory),
2168
+ kind: shell.kind,
2169
+ }));
2170
+ }
2171
+
2172
+ function cloudflareShellBoundaryId(routesDir: string, directory: string): string {
2173
+ const relativeDirectory = relative(routesDir, directory);
2174
+
2175
+ return relativeDirectory === ""
2176
+ ? "root"
2177
+ : relativeDirectory.replaceAll(sep, "/").replace(/[^A-Za-z0-9_$/-]/g, "_");
2178
+ }
2179
+
2180
+ function cloudflareServerSourceTransformPlugin(options: {
2181
+ projectRoot: string;
2182
+ serverOutput: ServerOutputMode;
2183
+ serverModules: Record<string, BuiltServerModuleArtifact>;
2184
+ vitePlugins?: readonly PluginOption[] | undefined;
2185
+ }): RouterCompatPlugin {
2186
+ const clientRouteInferenceCache = createClientRouteInferenceCache();
2187
+
2188
+ return {
2189
+ name: "mreact-cloudflare-server-source-transform",
2190
+ setup(buildApi) {
2191
+ buildApi.onLoad({ filter: /(?:\.mreact)?\.[cm]?[jt]sx$/ }, async (args) => {
2192
+ if (args.path.includes(`${sep}node_modules${sep}`)) {
2193
+ return undefined;
2194
+ }
2195
+
2196
+ const source = await readFile(args.path, "utf8");
2197
+ const serverSource = isServerComponentFile(args.path) ? stripRouteBuildExports(source) : source;
2198
+ const sourceHash = hashText(serverSource);
2199
+ const routeFile = relative(options.projectRoot, args.path).replaceAll(sep, "/");
2200
+ const artifact = options.serverModules[routeFile]?.[options.serverOutput];
2201
+ const contents =
2202
+ artifact !== undefined && artifact.sourceHash === sourceHash
2203
+ ? artifact.code
2204
+ : await transformCloudflareServerSource({
2205
+ cache: clientRouteInferenceCache,
2206
+ filename: args.path,
2207
+ serverOutput: options.serverOutput,
2208
+ source: serverSource,
2209
+ vitePlugins: options.vitePlugins,
2210
+ });
2211
+
2212
+ return {
2213
+ contents,
2214
+ loader: "js",
2215
+ resolveDir: dirname(args.path),
2216
+ };
2217
+ });
2218
+ },
2219
+ };
2220
+ }
2221
+
2222
+ async function transformCloudflareServerSource(options: {
2223
+ cache: ClientRouteInferenceCache;
2224
+ filename: string;
2225
+ serverOutput: ServerOutputMode;
2226
+ source: string;
2227
+ vitePlugins?: readonly PluginOption[] | undefined;
2228
+ }): Promise<string> {
2229
+ const moduleContext = await compilerModuleContextForSource({
2230
+ cache: options.cache,
2231
+ code: options.source,
2232
+ filename: options.filename,
2233
+ });
2234
+ const clientInference = await inferClientRouteModule({
2235
+ cache: options.cache,
2236
+ code: options.source,
2237
+ filename: options.filename,
2238
+ moduleContext,
2239
+ vitePlugins: options.vitePlugins,
2240
+ });
2241
+
2242
+ for (const diagnostic of clientInference.diagnostics) {
2243
+ console.warn(formatClientRouteInferenceDiagnostic(diagnostic));
2244
+ }
2245
+
2246
+ const output = transformCompilerModuleContext({
2247
+ code: options.source,
2248
+ clientBoundaryImports: clientInference.clientBoundaryImports,
2249
+ dev: false,
2250
+ filename: options.filename,
2251
+ moduleContext,
2252
+ serverEscape: nativeEscapeTransform,
2253
+ serverOutput: options.serverOutput,
2254
+ target: "server",
2255
+ });
2256
+ const fatalDiagnostics = output.diagnostics.filter(
2257
+ (diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER",
2258
+ );
2259
+
2260
+ if (fatalDiagnostics.length > 0) {
2261
+ throw new Error(
2262
+ fatalDiagnostics.map((diagnostic) => formatDiagnostic(options.filename, diagnostic)).join("\n"),
2263
+ );
2264
+ }
2265
+
2266
+ return output.code;
2267
+ }
2268
+
2269
+ function cloudflareWorkspaceRuntimePlugin(): RouterCompatPlugin {
2270
+ const packageFile = (
2271
+ monorepoDir: string,
2272
+ packageName: string,
2273
+ entry: string,
2274
+ sourceExtension?: "ts" | "tsx" | undefined,
2275
+ ): string =>
2276
+ workspacePackageFile({
2277
+ currentFileUrl: import.meta.url,
2278
+ entry,
2279
+ monorepoDir,
2280
+ packageName,
2281
+ ...(sourceExtension === undefined ? {} : { sourceExtension }),
2282
+ });
2283
+ const routerCachePath = packageFile("router", "@reckona/mreact-router", "cache");
2284
+ const routerCookiesPath = packageFile("router", "@reckona/mreact-router", "cookies");
2285
+ const routerCsrfPath = packageFile("router", "@reckona/mreact-router", "csrf");
2286
+ const routerDeferredPath = packageFile("router", "@reckona/mreact-router", "deferred");
2287
+ const routerI18nPath = packageFile("router", "@reckona/mreact-router", "i18n");
2288
+ const routerLinkPath = packageFile("router", "@reckona/mreact-router", "link");
2289
+ const routerMultipartPath = packageFile("router", "@reckona/mreact-router", "multipart");
2290
+ const routerNavigationPath = packageFile("router", "@reckona/mreact-router", "navigation");
2291
+ const routerRuntimeStatePath = packageFile("router", "@reckona/mreact-router", "runtime-state");
2292
+ const routerStreamListPath = packageFile("router", "@reckona/mreact-router", "stream-list");
2293
+ const runtimePaths = new Map([
2294
+ ["@reckona/mreact", packageFile("react", "@reckona/mreact", "index")],
2295
+ ["@reckona/mreact/jsx-dev-runtime", packageFile("react", "@reckona/mreact", "jsx-dev-runtime")],
2296
+ ["@reckona/mreact/jsx-runtime", packageFile("react", "@reckona/mreact", "jsx-runtime")],
2297
+ ["@reckona/mreact-auth", packageFile("auth", "@reckona/mreact-auth", "index")],
2298
+ ["@reckona/mreact-compat", packageFile("react-compat", "@reckona/mreact-compat", "index")],
2299
+ [
2300
+ "@reckona/mreact-compat/event-priority",
2301
+ packageFile("react-compat", "@reckona/mreact-compat", "event-priority"),
2302
+ ],
2303
+ ["@reckona/mreact-compat/flight", packageFile("react-compat", "@reckona/mreact-compat", "flight")],
2304
+ ["@reckona/mreact-compat/internal", packageFile("react-compat", "@reckona/mreact-compat", "internal")],
2305
+ [
2306
+ "@reckona/mreact-compat/jsx-dev-runtime",
2307
+ packageFile("react-compat", "@reckona/mreact-compat", "jsx-dev-runtime"),
2308
+ ],
2309
+ [
2310
+ "@reckona/mreact-compat/jsx-runtime",
2311
+ packageFile("react-compat", "@reckona/mreact-compat", "jsx-runtime"),
2312
+ ],
2313
+ ["@reckona/mreact-compat/scheduler", packageFile("react-compat", "@reckona/mreact-compat", "scheduler")],
2314
+ ["@reckona/mreact-query", packageFile("query", "@reckona/mreact-query", "index")],
2315
+ ["@reckona/mreact-reactive-core", packageFile("reactive-core", "@reckona/mreact-reactive-core", "index")],
2316
+ ["@reckona/mreact-router/link", routerLinkPath],
2317
+ ["@reckona/mreact-router/runtime-state", routerRuntimeStatePath],
2318
+ ["@reckona/mreact-router/session", packageFile("router", "@reckona/mreact-router", "session")],
2319
+ ["@reckona/mreact-router/stream-list", routerStreamListPath],
2320
+ ["@reckona/mreact-server", packageFile("server", "@reckona/mreact-server", "index")],
2321
+ [
2322
+ "@reckona/mreact-shared/html-escape",
2323
+ packageFile("shared", "@reckona/mreact-shared", "html-escape"),
2324
+ ],
2325
+ ]);
2326
+
2327
+ return {
2328
+ name: "mreact-cloudflare-workspace-runtime",
2329
+ setup(buildApi) {
2330
+ buildApi.onResolve({ filter: /^@reckona\/mreact-router\/(?:internal\/)?native-escape$/ }, () => ({
2331
+ namespace: "mreact-cloudflare-native-escape",
2332
+ path: "native-escape",
2333
+ }));
2334
+ buildApi.onResolve({ filter: /^@reckona\/mreact-router$/ }, () => ({
2335
+ namespace: "mreact-cloudflare-router-index",
2336
+ path: "index",
2337
+ }));
2338
+ buildApi.onResolve({ filter: /^@reckona\/mreact(?:-[\w-]+)?(?:\/[\w/-]+)?$/ }, (args) => {
2339
+ const path = runtimePaths.get(args.path);
2340
+
2341
+ return path === undefined ? undefined : { path };
2342
+ });
2343
+ buildApi.onLoad({ filter: /^native-escape$/, namespace: "mreact-cloudflare-native-escape" }, () => ({
2344
+ contents: `function escapeHtml(value) {
2345
+ return String(value ?? "").replace(/[&<>"']/g, (char) =>
2346
+ char === "&" ? "&amp;" : char === "<" ? "&lt;" : char === ">" ? "&gt;" : char === '"' ? "&quot;" : "&#39;"
2347
+ );
2348
+ }
2349
+ export function escapeHtmlBatch(values) {
2350
+ return values.map(escapeHtml);
2351
+ }`,
2352
+ loader: "js",
2353
+ }));
2354
+ buildApi.onLoad({ filter: /^index$/, namespace: "mreact-cloudflare-router-index" }, () => ({
2355
+ contents: `export { cacheControl, revalidatePath } from ${JSON.stringify(routerCachePath)};
2356
+ export { deleteCookie, parseCookieHeader, serializeCookie, setCookie } from ${JSON.stringify(routerCookiesPath)};
2357
+ export { createFormCsrfToken, formCsrfCookie, formCsrfFieldName, serverActionCookie, validateFormCsrf } from ${JSON.stringify(routerCsrfPath)};
2358
+ export { defer, isDeferredLoaderData } from ${JSON.stringify(routerDeferredPath)};
2359
+ export { defineMessages, detectLocale } from ${JSON.stringify(routerI18nPath)};
2360
+ export { Link, linkProps } from ${JSON.stringify(routerLinkPath)};
2361
+ export { parseMultipartStream } from ${JSON.stringify(routerMultipartPath)};
2362
+ export { cookies, headers, html, json, next, notFound, redirect, redirectExternal, rewrite } from ${JSON.stringify(routerNavigationPath)};
2363
+ export { getServerRuntimeState } from ${JSON.stringify(routerRuntimeStatePath)};`,
2364
+ loader: "js",
2365
+ resolveDir: dirname(routerNavigationPath),
2366
+ }));
2367
+ buildApi.onLoad({ filter: /(?:^|[/\\])packages[/\\]server[/\\](?:src|dist)[/\\]native-flight\.[jt]s$/ }, () => ({
2368
+ contents: `export function getNativeFlight() {
2369
+ return undefined;
2370
+ }`,
2371
+ loader: "js",
2372
+ }));
2373
+ },
2374
+ };
2375
+ }
2376
+
2377
+ function cloudflareRouteRequiresGeneratedModule(
2378
+ route: AppRoute,
2379
+ prerenderedRoutes: Record<string, BuiltPrerenderedRoute>,
2380
+ ): boolean {
2381
+ return (
2382
+ route.kind === "metadata" ||
2383
+ route.kind === "server" ||
2384
+ (route.kind === "page" &&
2385
+ (route.segments.some((segment) => segment.kind !== "static") ||
2386
+ prerenderedRoutes[route.path] === undefined))
2387
+ );
2388
+ }
2389
+
2390
+ function isServerComponentFile(file: string): boolean {
2391
+ return /(?:^|\/)(?:page|layout|template|loading|error|not-found)(?:\.mreact)?\.tsx$/.test(file);
2392
+ }
2393
+
2394
+ function viteManifestFromClientRoutes(routes: ClientRouteManifestEntry[]): Record<
2395
+ string,
2396
+ {
2397
+ css?: readonly string[];
2398
+ file: string;
2399
+ imports?: readonly string[];
2400
+ isEntry: true;
2401
+ name: string;
2402
+ src: string;
2403
+ }
2404
+ > {
2405
+ const manifest: Record<
2406
+ string,
2407
+ {
2408
+ file: string;
2409
+ imports?: readonly string[];
2410
+ isEntry: true;
2411
+ name: string;
2412
+ src: string;
2413
+ }
2414
+ > = {};
2415
+
2416
+ for (const route of routes) {
2417
+ if (!route.client || route.script === undefined || route.devScript === undefined) {
2418
+ continue;
2419
+ }
2420
+
2421
+ manifest[route.devScript] = {
2422
+ ...(route.css === undefined ? {} : { css: route.css }),
2423
+ file: route.script,
2424
+ ...(route.imports === undefined ? {} : { imports: route.imports }),
2425
+ isEntry: true,
2426
+ name: route.routeId ?? routeIdForPath(route.path),
2427
+ src: route.devScript,
2428
+ };
2429
+ }
2430
+
2431
+ return manifest;
2432
+ }
2433
+
2434
+ async function writeClientRouteBundles(options: {
2435
+ appDir: string;
2436
+ assetBaseUrl?: string | undefined;
2437
+ clientDir: string;
2438
+ clientRouteInferenceCache: ClientRouteInferenceCache;
2439
+ projectRoot: string;
2440
+ routes: readonly AppRoute[];
2441
+ sourceMapDir: string;
2442
+ sourceMaps: AppRouterClientSourceMapMode;
2443
+ vitePlugins?: readonly PluginOption[] | undefined;
2444
+ }): Promise<ClientRouteManifestEntry[]> {
2445
+ type PreparedClientRouteEntry = {
2446
+ build: BuildClientRouteOutputOptions;
2447
+ css: string[];
2448
+ navigation: boolean;
2449
+ route: AppRoute & { kind: "page" };
2450
+ };
2451
+ type PreparedClientManifestEntry = { manifest: ClientRouteManifestEntry };
2452
+ type PreparedRouteEntry = PreparedClientRouteEntry | PreparedClientManifestEntry;
2453
+ const entries: PreparedRouteEntry[] = await Promise.all(
2454
+ options.routes.map(async (route) => {
2455
+ if (route.kind !== "page") {
2456
+ return {
2457
+ manifest: { path: route.path, kind: route.kind, client: false },
2458
+ };
2459
+ }
2460
+
2461
+ const css = await writeRouteCssAssets({
2462
+ appDir: options.appDir,
2463
+ clientDir: options.clientDir,
2464
+ pageFile: route.file,
2465
+ projectRoot: options.projectRoot,
2466
+ routeId: routeIdForPath(route.path),
2467
+ vitePlugins: options.vitePlugins,
2468
+ });
2469
+ const source = await readFile(route.file, "utf8");
2470
+ const clientSource = stripRouteClientOnlyExports(source);
2471
+ const navigation = detectNavigationRuntimeHint(source);
2472
+ const references = await collectClientRouteReferences({
2473
+ appDir: options.appDir,
2474
+ cache: options.clientRouteInferenceCache,
2475
+ code: clientSource,
2476
+ filename: route.file,
2477
+ vitePlugins: options.vitePlugins,
2478
+ });
2479
+
2480
+ for (const diagnostic of references.diagnostics) {
2481
+ console.warn(formatClientRouteInferenceDiagnostic(diagnostic));
2482
+ }
2483
+
2484
+ if (!references.client) {
2485
+ return {
2486
+ manifest: {
2487
+ path: route.path,
2488
+ kind: route.kind,
2489
+ client: false,
2490
+ ...(css.length === 0 ? {} : { css }),
2491
+ ...(navigation ? { navigation } : {}),
2492
+ },
2493
+ };
2494
+ }
2495
+
2496
+ return {
2497
+ css,
2498
+ navigation,
2499
+ route: route as AppRoute & { kind: "page" },
2500
+ build: {
2501
+ code: clientSource,
2502
+ clientBoundaryImports: references.clientBoundaryImports,
2503
+ clientReferenceImports: references.clientReferenceImports,
2504
+ clientReferenceManifest: references.clientReferenceManifest,
2505
+ clientNavigation: detectClientNavigationHint(source),
2506
+ filename: route.file,
2507
+ minify: true,
2508
+ routePath: route.path,
2509
+ sourceMap: options.sourceMaps !== "none",
2510
+ vitePlugins: options.vitePlugins,
2511
+ },
2512
+ };
2513
+ }),
2514
+ );
2515
+ const clientEntries = entries.filter(
2516
+ (entry): entry is PreparedClientRouteEntry => "build" in entry,
2517
+ );
2518
+
2519
+ if (clientEntries.length === 0) {
2520
+ return entries.flatMap((entry) => ("manifest" in entry ? [entry.manifest] : []));
2521
+ }
2522
+
2523
+ let output: Awaited<ReturnType<typeof buildClientRouteBatchOutput>>;
2524
+
2525
+ try {
2526
+ output = await buildClientRouteBatchOutput({
2527
+ assetBaseUrl: options.assetBaseUrl,
2528
+ minify: true,
2529
+ projectRoot: options.projectRoot,
2530
+ routes: clientEntries.map((entry) => entry.build),
2531
+ sourceMap: options.sourceMaps !== "none",
2532
+ vitePlugins: options.vitePlugins,
2533
+ });
2534
+ } catch (error) {
2535
+ const routeContexts = clientEntries
2536
+ .map((entry) => `Failed to build client bundle for ${entry.route.path} (${entry.route.file}).`)
2537
+ .join("\n");
2538
+
2539
+ throw new Error(`${routeContexts}\nFailed to build client route bundles.\n${errorMessage(error)}`, {
2540
+ cause: error,
2541
+ });
2542
+ }
2543
+
2544
+ const routeOutputs = new Map(output.routes.map((route) => [route.routePath, route]));
2545
+ const mapAssets = new Map(
2546
+ output.chunks.flatMap((chunk) =>
2547
+ chunk.map === undefined ? [] : [[`${chunk.fileName}.map`, chunk.map] as const]
2548
+ ),
2549
+ );
2550
+
2551
+ for (const chunk of output.chunks) {
2552
+ const scriptBasename = chunk.fileName.split("/").pop() ?? "chunk.js";
2553
+ const code = applyClientSourceMapReference({
2554
+ code: chunk.code,
2555
+ scriptBasename,
2556
+ sourceMaps: options.sourceMaps,
2557
+ });
2558
+
2559
+ await mkdir(dirname(join(options.clientDir, chunk.fileName)), { recursive: true });
2560
+ await writeFile(join(options.clientDir, chunk.fileName), code);
2561
+ }
2562
+
2563
+ for (const [sourceMap, map] of mapAssets) {
2564
+ const mapBaseDir = options.sourceMaps === "hidden" ? options.sourceMapDir : options.clientDir;
2565
+
2566
+ await mkdir(dirname(join(mapBaseDir, sourceMap)), { recursive: true });
2567
+ await writeFile(join(mapBaseDir, sourceMap), map);
2568
+ }
2569
+
2570
+ for (const asset of output.assets ?? []) {
2571
+ const source =
2572
+ typeof asset.source === "string"
2573
+ ? asset.source
2574
+ : Buffer.from(asset.source).toString("utf8");
2575
+
2576
+ await mkdir(dirname(join(options.clientDir, asset.fileName)), { recursive: true });
2577
+ await writeFile(join(options.clientDir, asset.fileName), source);
2578
+ }
2579
+
2580
+ return entries.map((entry) => {
2581
+ if (!("build" in entry)) {
2582
+ return entry.manifest;
2583
+ }
2584
+
2585
+ const routeOutput = routeOutputs.get(entry.route.path);
2586
+
2587
+ if (routeOutput === undefined) {
2588
+ throw new Error(`Failed to build client bundle for ${entry.route.path}: missing output.`);
2589
+ }
2590
+
2591
+ const routeId = routeIdForPath(entry.route.path);
2592
+ const sourceMap = `${routeOutput.chunk.fileName}.map`;
2593
+ const code = applyClientSourceMapReference({
2594
+ code: routeOutput.chunk.code,
2595
+ scriptBasename: routeOutput.chunk.fileName.split("/").pop() ?? "route.js",
2596
+ sourceMaps: options.sourceMaps,
2597
+ });
2598
+
2599
+ return {
2600
+ bytes: Buffer.byteLength(code),
2601
+ path: entry.route.path,
2602
+ kind: entry.route.kind,
2603
+ client: true,
2604
+ ...(entry.css.length === 0 ? {} : { css: entry.css }),
2605
+ ...(routeOutput.chunk.imports.length === 0 ? {} : { imports: routeOutput.chunk.imports }),
2606
+ ...(entry.navigation ? { navigation: entry.navigation } : {}),
2607
+ routeId,
2608
+ script: routeOutput.chunk.fileName,
2609
+ ...(options.sourceMaps === "linked" ? { sourceMap } : {}),
2610
+ devScript: clientScriptForPath(entry.route.path),
2611
+ };
2612
+ });
2613
+ }
2614
+
2615
+ async function writeRouteCssAssets(options: {
2616
+ appDir: string;
2617
+ clientDir: string;
2618
+ pageFile: string;
2619
+ projectRoot: string;
2620
+ routeId: string;
2621
+ vitePlugins?: readonly PluginOption[] | undefined;
2622
+ }): Promise<string[]> {
2623
+ const cssFiles = await collectRouteCssFiles({
2624
+ appDir: options.appDir,
2625
+ pageFile: options.pageFile,
2626
+ projectRoot: options.projectRoot,
2627
+ });
2628
+
2629
+ if (cssFiles.length === 0) {
2630
+ return [];
2631
+ }
2632
+
2633
+ const code = [
2634
+ ...cssFiles.map((file) => `import ${JSON.stringify(file)};`),
2635
+ "export default undefined;",
2636
+ ].join("\n");
2637
+ const output = await bundleRouterModule({
2638
+ code,
2639
+ filename: options.pageFile,
2640
+ minify: true,
2641
+ platform: "browser",
2642
+ vitePlugins: options.vitePlugins,
2643
+ });
2644
+ const cssAssets = (output.assets ?? []).filter((asset) => asset.fileName.endsWith(".css"));
2645
+ const written: string[] = [];
2646
+
2647
+ for (const asset of cssAssets) {
2648
+ const source =
2649
+ typeof asset.source === "string"
2650
+ ? asset.source
2651
+ : Buffer.from(asset.source).toString("utf8");
2652
+ const hash = createHash("sha256").update(source).digest("hex").slice(0, 8);
2653
+ const cssFile = `assets/routes/${options.routeId}.${hash}.css`;
2654
+
2655
+ await mkdir(dirname(join(options.clientDir, cssFile)), { recursive: true });
2656
+ await writeFile(join(options.clientDir, cssFile), source);
2657
+ written.push(cssFile);
2658
+ }
2659
+
2660
+ return written;
2661
+ }
2662
+
2663
+ function applyClientSourceMapReference(options: {
2664
+ code: string;
2665
+ scriptBasename: string;
2666
+ sourceMaps: AppRouterClientSourceMapMode;
2667
+ }): string {
2668
+ const sourceMappingUrlPattern = /\n?\/\/# sourceMappingURL=route\.js\.map\s*$/;
2669
+
2670
+ if (options.sourceMaps === "none") {
2671
+ return options.code;
2672
+ }
2673
+
2674
+ if (options.sourceMaps === "hidden") {
2675
+ return options.code.replace(sourceMappingUrlPattern, "");
2676
+ }
2677
+
2678
+ const code = options.code.replace(
2679
+ sourceMappingUrlPattern,
2680
+ `\n//# sourceMappingURL=${options.scriptBasename}.map`,
2681
+ );
2682
+
2683
+ return code.includes("sourceMappingURL=")
2684
+ ? code
2685
+ : `${code}\n//# sourceMappingURL=${options.scriptBasename}.map`;
2686
+ }
2687
+
2688
+ async function writeNavigationRuntimeBundle(clientDir: string): Promise<string> {
2689
+ const output = await buildNavigationRuntimeBundle({
2690
+ minify: true,
2691
+ sourceMap: false,
2692
+ });
2693
+ const hash = createHash("sha256").update(output.code).digest("hex").slice(0, 8);
2694
+ const script = `assets/navigation.${hash}.js`;
2695
+
2696
+ await mkdir(dirname(join(clientDir, script)), { recursive: true });
2697
+ await writeFile(join(clientDir, script), output.code);
2698
+
2699
+ return script;
2700
+ }
2701
+
2702
+ async function writeAwsLambdaHandlerArtifact(outDir: string): Promise<void> {
2703
+ const awsLambdaDir = join(outDir, "aws-lambda");
2704
+ await mkdir(awsLambdaDir, { recursive: true });
2705
+ await writeFile(join(awsLambdaDir, "mreact-handler.mjs"), awsLambdaHandlerSource(".."));
2706
+ }
2707
+
2708
+ async function writeCloudflareWorkerArtifact(options: {
2709
+ cloudflareDir: string;
2710
+ clientManifest: { publicAssets?: readonly string[]; routes: readonly ClientRouteManifestEntry[] };
2711
+ modulesFile: string;
2712
+ serverManifest: BuiltServerManifest;
2713
+ }): Promise<void> {
2714
+ await writeFile(
2715
+ join(options.cloudflareDir, "worker.mjs"),
2716
+ [
2717
+ `import { createCloudflareBuiltRequestHandler, createCloudflareRouteModuleRenderer, createCloudflareStaticAssetLoader } from "@reckona/mreact-router/adapters/cloudflare";`,
2718
+ `import { routeModules } from ${JSON.stringify(`./${options.modulesFile}`)};`,
2719
+ ``,
2720
+ `const serverManifest = ${JSON.stringify(options.serverManifest, null, 2)};`,
2721
+ `const clientManifest = ${JSON.stringify(options.clientManifest, null, 2)};`,
2722
+ ``,
2723
+ `export default createCloudflareBuiltRequestHandler({`,
2724
+ ` assets: createCloudflareStaticAssetLoader({`,
2725
+ ` binding: (env) => env?.ASSETS,`,
2726
+ ` clientManifest,`,
2727
+ ` }),`,
2728
+ ` clientManifest,`,
2729
+ ` renderRoute: createCloudflareRouteModuleRenderer({ modules: routeModules }),`,
2730
+ ` serverManifest,`,
2731
+ `});`,
2732
+ ``,
2733
+ ].join("\n"),
2734
+ );
2735
+ }
2736
+
2737
+ export async function packageAwsLambdaArtifact(
2738
+ options: PackageAwsLambdaArtifactOptions,
2739
+ ): Promise<AwsLambdaArtifactManifest> {
2740
+ await assertRequiredBuildFile(join(options.fromDir, "server", "manifest.json"));
2741
+ await assertRequiredBuildFile(join(options.fromDir, "server", "import-policy.json"));
2742
+ await assertRequiredBuildFile(join(options.fromDir, "client", "manifest.json"));
2743
+
2744
+ await rm(options.outDir, { force: true, recursive: true });
2745
+ await mkdir(options.outDir, { recursive: true });
2746
+ await cp(options.fromDir, join(options.outDir, ".mreact"), {
2747
+ force: true,
2748
+ recursive: true,
2749
+ });
2750
+ await copyAwsLambdaProjectMetadata({
2751
+ fromDir: dirname(options.fromDir),
2752
+ outDir: options.outDir,
2753
+ });
2754
+ await writeFile(join(options.outDir, "mreact-handler.mjs"), awsLambdaHandlerSource(".mreact"));
2755
+
2756
+ const files = await collectAwsLambdaArtifactFiles(options.outDir, "");
2757
+ const manifest = {
2758
+ files,
2759
+ handler: "mreact-handler.handler",
2760
+ runtime: "aws-lambda",
2761
+ totalBytes: files.reduce((total, file) => total + file.bytes, 0),
2762
+ version: 1,
2763
+ } satisfies AwsLambdaArtifactManifest;
2764
+
2765
+ await writeFile(
2766
+ join(options.outDir, "mreact-lambda-artifact.json"),
2767
+ JSON.stringify(manifest, null, 2),
2768
+ );
2769
+
2770
+ return manifest;
2771
+ }
2772
+
2773
+ async function assertRequiredBuildFile(path: string): Promise<void> {
2774
+ try {
2775
+ const info = await stat(path);
2776
+ if (info.isFile()) {
2777
+ return;
2778
+ }
2779
+ } catch {
2780
+ // Throw the actionable error below.
2781
+ }
2782
+
2783
+ throw new Error(`Missing required mreact build artifact: ${path}`);
2784
+ }
2785
+
2786
+ async function copyAwsLambdaProjectMetadata(options: {
2787
+ fromDir: string;
2788
+ outDir: string;
2789
+ }): Promise<void> {
2790
+ for (const file of [
2791
+ "package.json",
2792
+ "package-lock.json",
2793
+ "npm-shrinkwrap.json",
2794
+ "pnpm-lock.yaml",
2795
+ "pnpm-workspace.yaml",
2796
+ "bun.lock",
2797
+ ]) {
2798
+ try {
2799
+ await cp(join(options.fromDir, file), join(options.outDir, file), { force: true });
2800
+ } catch (error) {
2801
+ if (!isNodeError(error) || error.code !== "ENOENT") {
2802
+ throw error;
2803
+ }
2804
+ }
2805
+ }
2806
+ }
2807
+
2808
+ async function collectAwsLambdaArtifactFiles(
2809
+ rootDir: string,
2810
+ relativeDir: string,
2811
+ ): Promise<Array<{ bytes: number; path: string }>> {
2812
+ const entries = await readdir(join(rootDir, relativeDir), { withFileTypes: true });
2813
+ const files: Array<{ bytes: number; path: string }> = [];
2814
+
2815
+ for (const entry of entries) {
2816
+ const relativePath = relativeDir === "" ? entry.name : `${relativeDir}/${entry.name}`;
2817
+ const absolutePath = join(rootDir, relativePath);
2818
+
2819
+ if (entry.isDirectory()) {
2820
+ files.push(...(await collectAwsLambdaArtifactFiles(rootDir, relativePath)));
2821
+ continue;
2822
+ }
2823
+
2824
+ if (entry.isFile()) {
2825
+ const info = await stat(absolutePath);
2826
+ files.push({ bytes: info.size, path: relativePath });
2827
+ }
2828
+ }
2829
+
2830
+ return files.sort((left, right) => right.bytes - left.bytes || left.path.localeCompare(right.path));
2831
+ }
2832
+
2833
+ function awsLambdaHandlerSource(outDirRelativeToHandler: string): string {
2834
+ return `import { fileURLToPath } from "node:url";
2835
+ import { dirname, resolve } from "node:path";
2836
+ import { createPreloadedAwsLambdaRequestHandler } from "@reckona/mreact-router/adapters/aws-lambda";
2837
+
2838
+ const here = dirname(fileURLToPath(import.meta.url));
2839
+
2840
+ export const handler = await createPreloadedAwsLambdaRequestHandler({
2841
+ importPolicy: "generated",
2842
+ outDir: resolve(here, ${JSON.stringify(outDirRelativeToHandler)}),
2843
+ timings: process.env.MREACT_ROUTER_TIMINGS === "1",
2844
+ });
2845
+ `;
2846
+ }
2847
+
2848
+ async function validateProductionRoutes(options: {
2849
+ files: Record<string, string>;
2850
+ projectRoot: string;
2851
+ routes: readonly AppRoute[];
2852
+ }): Promise<void> {
2853
+ for (const route of options.routes) {
2854
+ if (route.kind !== "page") {
2855
+ continue;
2856
+ }
2857
+
2858
+ const source = await readFile(route.file, "utf8");
2859
+ const filename = relative(options.projectRoot, route.file);
2860
+ const output = transform({
2861
+ code: stripRouteBuildExports(source),
2862
+ dev: false,
2863
+ filename: route.file,
2864
+ serverEscape: nativeEscapeTransform,
2865
+ serverOutput: shouldBuildRouteAsStream({
2866
+ filename,
2867
+ files: options.files,
2868
+ projectRoot: options.projectRoot,
2869
+ source,
2870
+ })
2871
+ ? "stream"
2872
+ : "string",
2873
+ target: "server",
2874
+ });
2875
+ const fatalDiagnostics = output.diagnostics.filter(
2876
+ (diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER",
2877
+ );
2878
+
2879
+ if (fatalDiagnostics.length > 0) {
2880
+ throw new Error(
2881
+ fatalDiagnostics.map((diagnostic) => formatDiagnostic(route.file, diagnostic)).join("\n"),
2882
+ );
2883
+ }
2884
+ }
2885
+ }
2886
+
2887
+ async function collectBuildFiles(
2888
+ projectRoot: string,
2889
+ allowedSourceDirs: readonly string[],
2890
+ appDir: string,
2891
+ ): Promise<Record<string, string>> {
2892
+ const files: Record<string, string> = {};
2893
+
2894
+ for (const directory of allowedSourceDirs) {
2895
+ for (const file of await collectFiles(directory)) {
2896
+ if (isAppFileConventionAsset(file, appDir)) {
2897
+ continue;
2898
+ }
2899
+
2900
+ const relativeFile = relative(projectRoot, file);
2901
+
2902
+ if (relativeFile === "" || relativeFile.startsWith("..") || relativeFile.startsWith(sep)) {
2903
+ continue;
2904
+ }
2905
+
2906
+ files[relativeFile] = await readFile(file, "utf8");
2907
+ }
2908
+ }
2909
+
2910
+ return files;
2911
+ }
2912
+
2913
+ function isAppFileConventionAsset(file: string, appDir: string): boolean {
2914
+ const relativeFile = relative(appDir, file);
2915
+ if (relativeFile === "" || relativeFile.startsWith("..") || relativeFile.includes(sep)) {
2916
+ return false;
2917
+ }
2918
+
2919
+ return appFileConventionForRootFilename(relativeFile)?.kind === "asset";
2920
+ }
2921
+
2922
+ async function collectFiles(directory: string): Promise<string[]> {
2923
+ const entries = await readdir(directory, { withFileTypes: true });
2924
+ const files: string[] = [];
2925
+
2926
+ for (const entry of entries) {
2927
+ const path = join(directory, entry.name);
2928
+
2929
+ if (entry.isDirectory()) {
2930
+ files.push(...(await collectFiles(path)));
2931
+ continue;
2932
+ }
2933
+
2934
+ if (entry.isFile()) {
2935
+ files.push(path);
2936
+ }
2937
+ }
2938
+
2939
+ return files;
2940
+ }
2941
+
2942
+ function hashText(text: string): string {
2943
+ return createHash("sha256").update(text).digest("hex").slice(0, 16);
2944
+ }
2945
+
2946
+ function errorMessage(error: unknown): string {
2947
+ return error instanceof Error ? error.message : String(error);
2948
+ }