@reckona/mreact-router 0.0.26 → 0.0.28

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 (56) hide show
  1. package/README.md +11 -15
  2. package/dist/actions.js +7 -10
  3. package/dist/actions.js.map +1 -1
  4. package/dist/adapters/aws-lambda.d.ts +5 -1
  5. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  6. package/dist/adapters/aws-lambda.js +50 -4
  7. package/dist/adapters/aws-lambda.js.map +1 -1
  8. package/dist/adapters/cloudflare.d.ts +20 -2
  9. package/dist/adapters/cloudflare.d.ts.map +1 -1
  10. package/dist/adapters/cloudflare.js +57 -13
  11. package/dist/adapters/cloudflare.js.map +1 -1
  12. package/dist/build.d.ts +20 -0
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.js +482 -49
  15. package/dist/build.js.map +1 -1
  16. package/dist/bundle-pipeline.d.ts +50 -0
  17. package/dist/bundle-pipeline.d.ts.map +1 -0
  18. package/dist/bundle-pipeline.js +211 -0
  19. package/dist/bundle-pipeline.js.map +1 -0
  20. package/dist/cli-options.d.ts +4 -0
  21. package/dist/cli-options.d.ts.map +1 -1
  22. package/dist/cli-options.js +110 -6
  23. package/dist/cli-options.js.map +1 -1
  24. package/dist/cli.js +54 -39
  25. package/dist/cli.js.map +1 -1
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +9 -29
  28. package/dist/client.js.map +1 -1
  29. package/dist/config.d.ts +1 -1
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +2 -2
  32. package/dist/config.js.map +1 -1
  33. package/dist/index.d.ts +5 -4
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/module-runner.d.ts.map +1 -1
  38. package/dist/module-runner.js +9 -31
  39. package/dist/module-runner.js.map +1 -1
  40. package/dist/native-route-matcher.d.ts.map +1 -1
  41. package/dist/native-route-matcher.js +13 -1
  42. package/dist/native-route-matcher.js.map +1 -1
  43. package/dist/navigation.d.ts +7 -0
  44. package/dist/navigation.d.ts.map +1 -1
  45. package/dist/navigation.js +27 -0
  46. package/dist/navigation.js.map +1 -1
  47. package/dist/render.d.ts.map +1 -1
  48. package/dist/render.js +28 -55
  49. package/dist/render.js.map +1 -1
  50. package/dist/route-source.d.ts +1 -0
  51. package/dist/route-source.d.ts.map +1 -1
  52. package/dist/route-source.js +4 -1
  53. package/dist/route-source.js.map +1 -1
  54. package/dist/types.d.ts +23 -0
  55. package/dist/types.d.ts.map +1 -1
  56. package/package.json +8 -9
package/dist/build.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
- import { access, cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
3
- import { dirname, join, relative, sep } from "node:path";
4
- import { build as bundle } from "esbuild";
5
- import { collectTopLevelValueExportNames, formatDiagnostic, hasModuleDirective, transform, } from "@reckona/mreact-compiler";
2
+ import { access, cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import { builtinModules } from "node:module";
4
+ import { dirname, extname, join, relative, sep } from "node:path";
5
+ import { collectJsxComponentRootNames, collectStaticImportReferences, collectTopLevelValueExportNames, formatDiagnostic, hasModuleDirective, transform, } from "@reckona/mreact-compiler";
6
6
  import { buildClientRouteOutput, buildNavigationRuntimeBundle, clientScriptForPath, createClientRouteInferenceCache, detectClientNavigationHint, detectNavigationRuntimeHint, formatClientRouteInferenceDiagnostic, inferClientRouteModule, isClientRouteModule, routeIdForPath, } from "./client.js";
7
7
  import { bundleAppRouterSourceModule, importAppRouterSourceModule } from "./module-runner.js";
8
8
  import { scanAppRoutes } from "./routes.js";
@@ -10,7 +10,8 @@ import { resolveAppRouterProjectOptions, resolveBuildTargets, } from "./config.j
10
10
  import { routeCachePolicyFromSource } from "./cache.js";
11
11
  import { bundleMiddlewareModuleCode, renderAppRequest } from "./render.js";
12
12
  import { createAppRouterImportPolicyPlugin } from "./import-policy.js";
13
- import { hasGenerateStaticParamsExport, hasLoaderExport, hasPrerenderExport, isStreamRouteSource, stripRouteBuildExports, stripRouteClientOnlyExports, stripRouteLoaderOnlyExports, stripRouteMetadataOnlyExports, stripRouteRequestOnlyExports, } from "./route-source.js";
13
+ import { hasGenerateStaticParamsExport, hasLoaderExport, hasPrerenderExport, isStreamRouteSource, mayUseAwaitBoundarySource, stripRouteBuildExports, stripRouteClientOnlyExports, stripRouteLoaderOnlyExports, stripRouteMetadataOnlyExports, stripRouteRequestOnlyExports, } from "./route-source.js";
14
+ import { bundleRouterModule, } from "./bundle-pipeline.js";
14
15
  import { workspacePackageFile } from "./workspace-packages.js";
15
16
  const nativeEscapeTransform = {
16
17
  batchImportName: "escapeHtmlBatch",
@@ -20,11 +21,13 @@ export async function buildApp(options) {
20
21
  const project = resolveAppRouterProjectOptions(options);
21
22
  const buildTargets = resolveBuildTargets(options.targets ?? project.buildTargets);
22
23
  const shouldBuildCloudflare = buildTargets.includes("cloudflare");
24
+ const shouldBuildAwsLambda = buildTargets.includes("aws-lambda");
23
25
  const routes = await scanAppRoutes({ appDir: project.routesDir });
26
+ const files = await collectBuildFiles(project.projectRoot, project.allowedSourceDirs);
24
27
  const serverDir = join(options.outDir, "server");
25
28
  const clientDir = join(options.outDir, "client");
26
29
  const cloudflareDir = join(options.outDir, "cloudflare");
27
- await validateProductionRoutes(routes);
30
+ await validateProductionRoutes({ files, projectRoot: project.projectRoot, routes });
28
31
  await rm(options.outDir, { force: true, recursive: true });
29
32
  await mkdir(serverDir, { recursive: true });
30
33
  await mkdir(clientDir, { recursive: true });
@@ -34,7 +37,8 @@ export async function buildApp(options) {
34
37
  await mkdir(join(clientDir, ".vite"), { recursive: true });
35
38
  await mkdir(join(clientDir, "assets", "routes"), { recursive: true });
36
39
  await copyPublicAssets(project.publicDir, join(clientDir, "public"));
37
- const files = await collectBuildFiles(project.projectRoot, project.allowedSourceDirs);
40
+ await copyPublicAssets(project.publicDir, clientDir);
41
+ const publicAssets = await collectPublicAssetPaths(project.publicDir);
38
42
  const serverActionManifest = collectBuildServerActionManifest({
39
43
  files,
40
44
  projectRoot: project.projectRoot,
@@ -48,6 +52,12 @@ export async function buildApp(options) {
48
52
  projectRoot: project.projectRoot,
49
53
  routes,
50
54
  });
55
+ const generatedImportPolicy = buildGeneratedImportPolicy({
56
+ files,
57
+ projectRoot: project.projectRoot,
58
+ routes,
59
+ routesDir: project.routesDir,
60
+ });
51
61
  const serverModuleFiles = await writeServerModuleArtifactFiles(serverDir, serverModules);
52
62
  const serverRoutes = routes.map((route) => ({
53
63
  ...route,
@@ -74,9 +84,11 @@ export async function buildApp(options) {
74
84
  clientRoutes: clientManifestRoutes,
75
85
  routes,
76
86
  });
87
+ let cloudflareRouteModules;
77
88
  if (shouldBuildCloudflare) {
78
- await writeCloudflareRouteModules({
89
+ cloudflareRouteModules = await writeCloudflareRouteModules({
79
90
  cloudflareDir,
91
+ files,
80
92
  prerenderedRoutes,
81
93
  projectRoot: project.projectRoot,
82
94
  routesDir: project.routesDir,
@@ -84,7 +96,7 @@ export async function buildApp(options) {
84
96
  serverModules,
85
97
  });
86
98
  }
87
- await writeFile(join(serverDir, "manifest.json"), JSON.stringify({
99
+ const serverManifest = {
88
100
  allowedSourceDirs: project.allowedSourceDirs.map((directory) => relative(project.projectRoot, directory)),
89
101
  ...(project.assetBaseUrl === undefined ? {} : { assetBaseUrl: project.assetBaseUrl }),
90
102
  version: 1,
@@ -97,9 +109,26 @@ export async function buildApp(options) {
97
109
  : { publicAssetBaseUrl: project.publicAssetBaseUrl }),
98
110
  ...(serverActionManifest.length === 0 ? {} : { serverActionManifest }),
99
111
  ...(Object.keys(serverModuleFiles).length === 0 ? {} : { serverModuleFiles }),
100
- }, null, 2));
101
- await writeFile(join(clientDir, "manifest.json"), JSON.stringify({ routes: clientManifestRoutes }, null, 2));
112
+ };
113
+ const clientManifest = {
114
+ ...(publicAssets.length === 0 ? {} : { publicAssets }),
115
+ routes: clientManifestRoutes,
116
+ };
117
+ await writeFile(join(serverDir, "manifest.json"), JSON.stringify(serverManifest, null, 2));
118
+ await writeFile(join(serverDir, "import-policy.json"), JSON.stringify(generatedImportPolicy, null, 2));
119
+ await writeFile(join(clientDir, "manifest.json"), JSON.stringify(clientManifest, null, 2));
102
120
  await writeFile(join(clientDir, ".vite", "manifest.json"), JSON.stringify(viteManifestFromClientRoutes(clientManifestRoutes), null, 2));
121
+ if (shouldBuildAwsLambda) {
122
+ await writeAwsLambdaHandlerArtifact(options.outDir);
123
+ }
124
+ if (shouldBuildCloudflare) {
125
+ await writeCloudflareWorkerArtifact({
126
+ cloudflareDir,
127
+ clientManifest,
128
+ modulesFile: cloudflareRouteModules?.registryFile ?? "route-modules.mjs",
129
+ serverManifest,
130
+ });
131
+ }
103
132
  return { routes };
104
133
  }
105
134
  async function writeServerModuleArtifactFiles(serverDir, serverModules) {
@@ -114,6 +143,104 @@ async function writeServerModuleArtifactFiles(serverDir, serverModules) {
114
143
  }
115
144
  return files;
116
145
  }
146
+ const nodeBuiltinPackages = new Set(builtinModules.flatMap((name) => [name, `node:${name}`]));
147
+ const frameworkRuntimePackages = new Set([
148
+ "@reckona/mreact",
149
+ "@reckona/mreact-auth",
150
+ "@reckona/mreact-compiler",
151
+ "@reckona/mreact-query",
152
+ "@reckona/mreact-reactive-core",
153
+ "@reckona/mreact-router",
154
+ "@reckona/mreact-server",
155
+ ]);
156
+ function buildGeneratedImportPolicy(options) {
157
+ const routePackages = new Map();
158
+ const allPackages = new Set();
159
+ const relativeRoutesDir = relative(options.projectRoot, options.routesDir);
160
+ for (const route of options.routes) {
161
+ const file = relative(options.projectRoot, route.file);
162
+ const packages = collectRuntimePackagesForFile({
163
+ file,
164
+ files: options.files,
165
+ projectRoot: options.projectRoot,
166
+ seen: new Set(),
167
+ });
168
+ if (packages.length > 0) {
169
+ routePackages.set(route.path, packages);
170
+ for (const packageName of packages) {
171
+ allPackages.add(packageName);
172
+ }
173
+ }
174
+ }
175
+ const middlewareFile = ["middleware.ts", "middleware.mreact.ts"]
176
+ .map((file) => (relativeRoutesDir === "" ? file : `${relativeRoutesDir}/${file}`))
177
+ .find((file) => options.files[file] !== undefined);
178
+ if (middlewareFile !== undefined) {
179
+ const packages = collectRuntimePackagesForFile({
180
+ file: middlewareFile,
181
+ files: options.files,
182
+ projectRoot: options.projectRoot,
183
+ seen: new Set(),
184
+ });
185
+ if (packages.length > 0) {
186
+ routePackages.set("middleware", packages);
187
+ for (const packageName of packages) {
188
+ allPackages.add(packageName);
189
+ }
190
+ }
191
+ }
192
+ return {
193
+ byRoute: Object.fromEntries([...routePackages.entries()].sort(([left], [right]) => left.localeCompare(right))),
194
+ runtimePackages: [...allPackages].sort(),
195
+ version: 1,
196
+ };
197
+ }
198
+ function collectRuntimePackagesForFile(options) {
199
+ if (options.seen.has(options.file)) {
200
+ return [];
201
+ }
202
+ const source = options.files[options.file];
203
+ if (source === undefined || hasModuleDirective({ code: source, directive: "use client" })) {
204
+ return [];
205
+ }
206
+ options.seen.add(options.file);
207
+ const packages = new Set();
208
+ for (const reference of collectStaticImportReferences({
209
+ code: source,
210
+ filename: join(options.projectRoot, options.file),
211
+ })) {
212
+ if (isRuntimePackageSpecifier(reference.source)) {
213
+ packages.add(runtimePackageNameForSpecifier(reference.source));
214
+ continue;
215
+ }
216
+ const localFile = resolveBuildLocalSourceImport(options.files, options.file, reference.source);
217
+ if (localFile === undefined) {
218
+ continue;
219
+ }
220
+ for (const packageName of collectRuntimePackagesForFile({
221
+ ...options,
222
+ file: localFile,
223
+ })) {
224
+ packages.add(packageName);
225
+ }
226
+ }
227
+ options.seen.delete(options.file);
228
+ return [...packages].sort();
229
+ }
230
+ function isRuntimePackageSpecifier(specifier) {
231
+ if (specifier.startsWith(".") || specifier.startsWith("/") || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(specifier)) {
232
+ return false;
233
+ }
234
+ const packageName = runtimePackageNameForSpecifier(specifier);
235
+ return !nodeBuiltinPackages.has(specifier) && !frameworkRuntimePackages.has(packageName);
236
+ }
237
+ function runtimePackageNameForSpecifier(specifier) {
238
+ if (!specifier.startsWith("@")) {
239
+ return specifier.split("/")[0] ?? specifier;
240
+ }
241
+ const [scope, name] = specifier.split("/");
242
+ return scope !== undefined && name !== undefined ? `${scope}/${name}` : specifier;
243
+ }
117
244
  function collectBuildServerActionManifest(options) {
118
245
  const entries = [];
119
246
  const relativeRoutesDir = relative(options.projectRoot, options.routesDir);
@@ -153,6 +280,36 @@ async function copyPublicAssets(publicDir, outDir) {
153
280
  throw error;
154
281
  }
155
282
  }
283
+ async function collectPublicAssetPaths(publicDir) {
284
+ try {
285
+ const info = await stat(publicDir);
286
+ if (!info.isDirectory()) {
287
+ return [];
288
+ }
289
+ }
290
+ catch (error) {
291
+ if (isNodeError(error) && error.code === "ENOENT") {
292
+ return [];
293
+ }
294
+ throw error;
295
+ }
296
+ const paths = [];
297
+ await collectPublicAssetPathsInner(publicDir, "", paths);
298
+ return paths.sort();
299
+ }
300
+ async function collectPublicAssetPathsInner(publicDir, relativeDir, paths) {
301
+ const entries = await readdir(join(publicDir, relativeDir), { withFileTypes: true });
302
+ for (const entry of entries) {
303
+ const relativePath = relativeDir === "" ? entry.name : `${relativeDir}/${entry.name}`;
304
+ if (entry.isDirectory()) {
305
+ await collectPublicAssetPathsInner(publicDir, relativePath, paths);
306
+ continue;
307
+ }
308
+ if (entry.isFile()) {
309
+ paths.push(`/${relativePath}`);
310
+ }
311
+ }
312
+ }
156
313
  function isNodeError(error) {
157
314
  return error instanceof Error && "code" in error;
158
315
  }
@@ -300,9 +457,14 @@ async function buildServerModuleArtifacts(options) {
300
457
  }
301
458
  continue;
302
459
  }
303
- const serverOutputs = route !== undefined && isStreamRouteSource(source)
304
- ? ["stream", "string"]
305
- : ["string"];
460
+ const streamRoute = route !== undefined &&
461
+ shouldBuildRouteAsStream({
462
+ filename: file,
463
+ files: options.files,
464
+ projectRoot: options.projectRoot,
465
+ source,
466
+ });
467
+ const serverOutputs = streamRoute ? ["stream", "string"] : ["string"];
306
468
  const code = route === undefined ? source : stripRouteBuildExports(source);
307
469
  const clientInference = route === undefined
308
470
  ? { client: false, clientBoundaryImports: [], diagnostics: [] }
@@ -323,6 +485,7 @@ async function buildServerModuleArtifacts(options) {
323
485
  route,
324
486
  routeCode: code,
325
487
  source,
488
+ streamRoute,
326
489
  });
327
490
  }
328
491
  for (const serverOutput of serverOutputs) {
@@ -337,6 +500,12 @@ async function buildServerModuleArtifacts(options) {
337
500
  });
338
501
  const fatalDiagnostics = output.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
339
502
  if (fatalDiagnostics.length > 0) {
503
+ if (serverOutput === "string" &&
504
+ streamRoute &&
505
+ route?.kind === "page" &&
506
+ fatalDiagnostics.every((diagnostic) => diagnostic.code === "MR_UNSUPPORTED_AWAIT_INNER_COMPONENT")) {
507
+ continue;
508
+ }
340
509
  throw new Error(fatalDiagnostics.map((diagnostic) => formatDiagnostic(file, diagnostic)).join("\n"));
341
510
  }
342
511
  artifact[serverOutput] = {
@@ -360,10 +529,124 @@ function builtRouteSourceAnalysisSummary(options) {
360
529
  routeCode: options.routeCode,
361
530
  routePath: options.route.path,
362
531
  sourceHash: hashText(options.source),
363
- streamRoute: isStreamRouteSource(options.source),
532
+ streamRoute: options.streamRoute,
364
533
  usesRuntimeCacheControl: usesRuntimeCacheControl(options.source),
365
534
  };
366
535
  }
536
+ function shouldBuildRouteAsStream(options) {
537
+ return (isStreamRouteSource(options.source) ||
538
+ routeClosureMayUseAwaitBoundary({
539
+ filename: options.filename,
540
+ files: options.files,
541
+ projectRoot: options.projectRoot,
542
+ source: options.source,
543
+ seen: new Set(),
544
+ }));
545
+ }
546
+ function routeClosureMayUseAwaitBoundary(options) {
547
+ if (options.seen.has(options.filename) ||
548
+ hasModuleDirective({ code: options.source, directive: "use client" })) {
549
+ return false;
550
+ }
551
+ options.seen.add(options.filename);
552
+ try {
553
+ if (mayUseAwaitBoundarySource(options.source)) {
554
+ return true;
555
+ }
556
+ const jsxComponentRoots = new Set(collectJsxComponentRootNames({
557
+ code: options.source,
558
+ filename: join(options.projectRoot, options.filename),
559
+ }));
560
+ for (const reference of collectStaticImportReferences({
561
+ code: options.source,
562
+ filename: join(options.projectRoot, options.filename),
563
+ })) {
564
+ if (!isRenderedStaticImportReference(reference, jsxComponentRoots)) {
565
+ continue;
566
+ }
567
+ const resolved = resolveBuildLocalSourceImport(options.files, options.filename, reference.source);
568
+ if (resolved === undefined) {
569
+ continue;
570
+ }
571
+ const importedSource = options.files[resolved];
572
+ if (importedSource !== undefined &&
573
+ routeClosureMayUseAwaitBoundary({
574
+ filename: resolved,
575
+ files: options.files,
576
+ projectRoot: options.projectRoot,
577
+ seen: options.seen,
578
+ source: importedSource,
579
+ })) {
580
+ return true;
581
+ }
582
+ }
583
+ return false;
584
+ }
585
+ finally {
586
+ options.seen.delete(options.filename);
587
+ }
588
+ }
589
+ function isRenderedStaticImportReference(reference, jsxComponentRoots) {
590
+ return reference.localNames.some((localName) => jsxComponentRoots.has(localName));
591
+ }
592
+ function resolveBuildLocalSourceImport(files, importer, specifier) {
593
+ if (!specifier.startsWith(".")) {
594
+ return undefined;
595
+ }
596
+ const base = join(dirname(importer), specifier);
597
+ for (const candidate of buildSourceModuleCandidates(base)) {
598
+ if (files[candidate] !== undefined) {
599
+ return candidate;
600
+ }
601
+ }
602
+ return undefined;
603
+ }
604
+ function buildSourceModuleCandidates(base) {
605
+ if (hasSourceModuleExtension(base)) {
606
+ return [base, ...typescriptSourceModuleCandidates(base)];
607
+ }
608
+ if (extname(base) !== "") {
609
+ return [];
610
+ }
611
+ return [
612
+ `${base}.ts`,
613
+ `${base}.tsx`,
614
+ `${base}.mreact.tsx`,
615
+ `${base}.js`,
616
+ `${base}.jsx`,
617
+ `${base}.mjs`,
618
+ `${base}.mts`,
619
+ `${base}.cjs`,
620
+ `${base}.cts`,
621
+ join(base, "index.ts"),
622
+ join(base, "index.tsx"),
623
+ join(base, "index.mreact.tsx"),
624
+ join(base, "index.js"),
625
+ join(base, "index.jsx"),
626
+ join(base, "index.mjs"),
627
+ join(base, "index.mts"),
628
+ join(base, "index.cjs"),
629
+ join(base, "index.cts"),
630
+ ];
631
+ }
632
+ function hasSourceModuleExtension(path) {
633
+ return /\.(?:mreact\.tsx|tsx?|jsx?|mjs|mts|cjs|cts)$/.test(path);
634
+ }
635
+ function typescriptSourceModuleCandidates(path) {
636
+ if (path.endsWith(".js")) {
637
+ return [`${path.slice(0, -3)}.ts`, `${path.slice(0, -3)}.tsx`];
638
+ }
639
+ if (path.endsWith(".jsx")) {
640
+ return [`${path.slice(0, -4)}.tsx`];
641
+ }
642
+ if (path.endsWith(".mjs")) {
643
+ return [`${path.slice(0, -4)}.mts`];
644
+ }
645
+ if (path.endsWith(".cjs")) {
646
+ return [`${path.slice(0, -4)}.cts`];
647
+ }
648
+ return [];
649
+ }
367
650
  function usesRuntimeCacheControl(code) {
368
651
  return /\bcacheControl\s*\(/.test(code);
369
652
  }
@@ -407,10 +690,9 @@ async function bundleRouteMetadataModuleCode(options) {
407
690
  });
408
691
  }
409
692
  async function bundleRouteRequestModuleCode(options) {
410
- const output = await bundle({
411
- bundle: true,
412
- format: "esm",
413
- logLevel: "silent",
693
+ const output = await bundleRouterModule({
694
+ code: options.code,
695
+ filename: options.filename,
414
696
  platform: "node",
415
697
  plugins: [
416
698
  createAppRouterImportPolicyPlugin({
@@ -419,18 +701,8 @@ async function bundleRouteRequestModuleCode(options) {
419
701
  label: options.label,
420
702
  }),
421
703
  ],
422
- write: false,
423
- jsx: "transform",
424
- jsxFactory: "__mreact_jsx",
425
- jsxFragment: "__mreact_fragment",
426
- stdin: {
427
- contents: options.code,
428
- loader: "tsx",
429
- resolveDir: dirname(options.filename),
430
- sourcefile: options.filename,
431
- },
432
704
  });
433
- const code = output.outputFiles[0]?.text;
705
+ const code = output.code;
434
706
  if (code === undefined) {
435
707
  throw new Error(`Failed to compile ${options.label.toLowerCase()} for ${options.filename}.`);
436
708
  }
@@ -467,7 +739,32 @@ async function writeCloudflareRouteModules(options) {
467
739
  const routeId = routeIdForPath(route.path);
468
740
  const routeModuleFile = `routes/${routeId}.mjs`;
469
741
  let routeModuleExports;
470
- const serverOutput = isStreamRouteSource(source) ? "stream" : "string";
742
+ if (route.kind === "server") {
743
+ try {
744
+ const routeOutput = await buildCloudflareServerRouteModule({
745
+ filename: route.file,
746
+ });
747
+ const serverRouteFile = `routes/${routeId}.${hashText(routeOutput).slice(0, 8)}.server.mjs`;
748
+ await writeFile(join(options.cloudflareDir, serverRouteFile), routeOutput);
749
+ const serverRouteImport = `./${serverRouteFile.split("/").pop() ?? serverRouteFile}`;
750
+ routeModuleExports = [`export * from ${JSON.stringify(serverRouteImport)};`];
751
+ }
752
+ catch (error) {
753
+ throw new Error(`Failed to build Cloudflare server route module for ${routeFile}: ${errorMessage(error)}`);
754
+ }
755
+ await writeFile(join(options.cloudflareDir, routeModuleFile), `${routeModuleExports.join("\n")}\n`);
756
+ registryEntries.push(`${JSON.stringify(routeFile)}: () => import(${JSON.stringify(`./${routeModuleFile}`)})`);
757
+ continue;
758
+ }
759
+ const serverOutput = options.serverModules[routeFile]?.analysis?.streamRoute === true ||
760
+ shouldBuildRouteAsStream({
761
+ filename: routeFile,
762
+ files: options.files,
763
+ projectRoot: options.projectRoot,
764
+ source,
765
+ })
766
+ ? "stream"
767
+ : "string";
471
768
  try {
472
769
  const componentOutput = serverOutput === "stream"
473
770
  ? await buildCloudflareStreamRouteComponentModule({
@@ -518,6 +815,7 @@ async function writeCloudflareRouteModules(options) {
518
815
  ``,
519
816
  ].join("\n");
520
817
  await writeFile(join(options.cloudflareDir, "route-modules.mjs"), registrySource);
818
+ return { registryFile: "route-modules.mjs" };
521
819
  }
522
820
  async function buildCloudflareServerComponentModule(options) {
523
821
  const entry = `import * as routeModule from ${JSON.stringify(options.filename)};
@@ -819,24 +1117,37 @@ async function buildCloudflareRouteLoaderModule(options) {
819
1117
  resolveDir: dirname(options.filename),
820
1118
  });
821
1119
  }
1120
+ async function buildCloudflareServerRouteModule(options) {
1121
+ const entry = `import * as routeModule from ${JSON.stringify(options.filename)};
1122
+
1123
+ export const GET = routeModule.GET;
1124
+ export const HEAD = routeModule.HEAD;
1125
+ export const POST = routeModule.POST;
1126
+ export const PUT = routeModule.PUT;
1127
+ export const PATCH = routeModule.PATCH;
1128
+ export const DELETE = routeModule.DELETE;
1129
+ export const OPTIONS = routeModule.OPTIONS;
1130
+ export const ALL = routeModule.ALL;
1131
+ const defaultHandler = routeModule.default;
1132
+ export default defaultHandler;`;
1133
+ return bundleCloudflareModule({
1134
+ entry,
1135
+ filename: `${options.filename}.mreact-cloudflare-server-route.js`,
1136
+ plugins: [cloudflareWorkspaceRuntimePlugin()],
1137
+ resolveDir: dirname(options.filename),
1138
+ });
1139
+ }
822
1140
  async function bundleCloudflareModule(options) {
823
- const output = await bundle({
824
- bundle: true,
825
- format: "esm",
826
- logLevel: "silent",
1141
+ const output = await bundleRouterModule({
1142
+ code: options.entry,
1143
+ filename: options.filename,
827
1144
  minify: true,
828
1145
  platform: "browser",
1146
+ preserveExports: true,
829
1147
  plugins: options.plugins,
830
1148
  target: "es2022",
831
- write: false,
832
- stdin: {
833
- contents: options.entry,
834
- loader: "js",
835
- resolveDir: options.resolveDir,
836
- sourcefile: options.filename,
837
- },
838
1149
  });
839
- const code = output.outputFiles[0]?.text;
1150
+ const code = output.code;
840
1151
  if (code === undefined) {
841
1152
  throw new Error(`Failed to build Cloudflare route module for ${options.filename}.`);
842
1153
  }
@@ -1039,9 +1350,10 @@ export { cookies, headers, html, json, next, notFound, redirect, redirectExterna
1039
1350
  };
1040
1351
  }
1041
1352
  function cloudflareRouteRequiresGeneratedModule(route, prerenderedRoutes) {
1042
- return (route.kind === "page" &&
1043
- (route.segments.some((segment) => segment.kind !== "static") ||
1044
- prerenderedRoutes[route.path] === undefined));
1353
+ return (route.kind === "server" ||
1354
+ (route.kind === "page" &&
1355
+ (route.segments.some((segment) => segment.kind !== "static") ||
1356
+ prerenderedRoutes[route.path] === undefined)));
1045
1357
  }
1046
1358
  function isServerComponentFile(file) {
1047
1359
  return /(?:^|\/)(?:page|layout|template|loading|error|not-found)(?:\.mreact)?\.tsx$/.test(file);
@@ -1153,18 +1465,139 @@ async function writeNavigationRuntimeBundle(clientDir) {
1153
1465
  await writeFile(join(clientDir, script), output.code);
1154
1466
  return script;
1155
1467
  }
1156
- async function validateProductionRoutes(routes) {
1157
- for (const route of routes) {
1468
+ async function writeAwsLambdaHandlerArtifact(outDir) {
1469
+ const awsLambdaDir = join(outDir, "aws-lambda");
1470
+ await mkdir(awsLambdaDir, { recursive: true });
1471
+ await writeFile(join(awsLambdaDir, "mreact-handler.mjs"), awsLambdaHandlerSource(".."));
1472
+ }
1473
+ async function writeCloudflareWorkerArtifact(options) {
1474
+ await writeFile(join(options.cloudflareDir, "worker.mjs"), [
1475
+ `import { createCloudflareBuiltRequestHandler, createCloudflareRouteModuleRenderer, createCloudflareStaticAssetLoader } from "@reckona/mreact-router/adapters/cloudflare";`,
1476
+ `import { routeModules } from ${JSON.stringify(`./${options.modulesFile}`)};`,
1477
+ ``,
1478
+ `const serverManifest = ${JSON.stringify(options.serverManifest, null, 2)};`,
1479
+ `const clientManifest = ${JSON.stringify(options.clientManifest, null, 2)};`,
1480
+ ``,
1481
+ `export default createCloudflareBuiltRequestHandler({`,
1482
+ ` assets: createCloudflareStaticAssetLoader({`,
1483
+ ` binding: (env) => env?.ASSETS,`,
1484
+ ` clientManifest,`,
1485
+ ` }),`,
1486
+ ` clientManifest,`,
1487
+ ` renderRoute: createCloudflareRouteModuleRenderer({ modules: routeModules }),`,
1488
+ ` serverManifest,`,
1489
+ `});`,
1490
+ ``,
1491
+ ].join("\n"));
1492
+ }
1493
+ export async function packageAwsLambdaArtifact(options) {
1494
+ await assertRequiredBuildFile(join(options.fromDir, "server", "manifest.json"));
1495
+ await assertRequiredBuildFile(join(options.fromDir, "server", "import-policy.json"));
1496
+ await assertRequiredBuildFile(join(options.fromDir, "client", "manifest.json"));
1497
+ await rm(options.outDir, { force: true, recursive: true });
1498
+ await mkdir(options.outDir, { recursive: true });
1499
+ await cp(options.fromDir, join(options.outDir, ".mreact"), {
1500
+ force: true,
1501
+ recursive: true,
1502
+ });
1503
+ await copyAwsLambdaProjectMetadata({
1504
+ fromDir: dirname(options.fromDir),
1505
+ outDir: options.outDir,
1506
+ });
1507
+ await writeFile(join(options.outDir, "mreact-handler.mjs"), awsLambdaHandlerSource(".mreact"));
1508
+ const files = await collectAwsLambdaArtifactFiles(options.outDir, "");
1509
+ const manifest = {
1510
+ files,
1511
+ handler: "mreact-handler.handler",
1512
+ runtime: "aws-lambda",
1513
+ totalBytes: files.reduce((total, file) => total + file.bytes, 0),
1514
+ version: 1,
1515
+ };
1516
+ await writeFile(join(options.outDir, "mreact-lambda-artifact.json"), JSON.stringify(manifest, null, 2));
1517
+ return manifest;
1518
+ }
1519
+ async function assertRequiredBuildFile(path) {
1520
+ try {
1521
+ const info = await stat(path);
1522
+ if (info.isFile()) {
1523
+ return;
1524
+ }
1525
+ }
1526
+ catch {
1527
+ // Throw the actionable error below.
1528
+ }
1529
+ throw new Error(`Missing required mreact build artifact: ${path}`);
1530
+ }
1531
+ async function copyAwsLambdaProjectMetadata(options) {
1532
+ for (const file of [
1533
+ "package.json",
1534
+ "package-lock.json",
1535
+ "npm-shrinkwrap.json",
1536
+ "pnpm-lock.yaml",
1537
+ "pnpm-workspace.yaml",
1538
+ "bun.lock",
1539
+ ]) {
1540
+ try {
1541
+ await cp(join(options.fromDir, file), join(options.outDir, file), { force: true });
1542
+ }
1543
+ catch (error) {
1544
+ if (!isNodeError(error) || error.code !== "ENOENT") {
1545
+ throw error;
1546
+ }
1547
+ }
1548
+ }
1549
+ }
1550
+ async function collectAwsLambdaArtifactFiles(rootDir, relativeDir) {
1551
+ const entries = await readdir(join(rootDir, relativeDir), { withFileTypes: true });
1552
+ const files = [];
1553
+ for (const entry of entries) {
1554
+ const relativePath = relativeDir === "" ? entry.name : `${relativeDir}/${entry.name}`;
1555
+ const absolutePath = join(rootDir, relativePath);
1556
+ if (entry.isDirectory()) {
1557
+ files.push(...(await collectAwsLambdaArtifactFiles(rootDir, relativePath)));
1558
+ continue;
1559
+ }
1560
+ if (entry.isFile()) {
1561
+ const info = await stat(absolutePath);
1562
+ files.push({ bytes: info.size, path: relativePath });
1563
+ }
1564
+ }
1565
+ return files.sort((left, right) => right.bytes - left.bytes || left.path.localeCompare(right.path));
1566
+ }
1567
+ function awsLambdaHandlerSource(outDirRelativeToHandler) {
1568
+ return `import { fileURLToPath } from "node:url";
1569
+ import { dirname, resolve } from "node:path";
1570
+ import { createPreloadedAwsLambdaRequestHandler } from "@reckona/mreact-router/adapters/aws-lambda";
1571
+
1572
+ const here = dirname(fileURLToPath(import.meta.url));
1573
+
1574
+ export const handler = await createPreloadedAwsLambdaRequestHandler({
1575
+ importPolicy: "generated",
1576
+ outDir: resolve(here, ${JSON.stringify(outDirRelativeToHandler)}),
1577
+ timings: process.env.MREACT_ROUTER_TIMINGS === "1",
1578
+ });
1579
+ `;
1580
+ }
1581
+ async function validateProductionRoutes(options) {
1582
+ for (const route of options.routes) {
1158
1583
  if (route.kind !== "page") {
1159
1584
  continue;
1160
1585
  }
1161
1586
  const source = await readFile(route.file, "utf8");
1587
+ const filename = relative(options.projectRoot, route.file);
1162
1588
  const output = transform({
1163
1589
  code: stripRouteBuildExports(source),
1164
1590
  dev: false,
1165
1591
  filename: route.file,
1166
1592
  serverEscape: nativeEscapeTransform,
1167
- serverOutput: isStreamRouteSource(source) ? "stream" : "string",
1593
+ serverOutput: shouldBuildRouteAsStream({
1594
+ filename,
1595
+ files: options.files,
1596
+ projectRoot: options.projectRoot,
1597
+ source,
1598
+ })
1599
+ ? "stream"
1600
+ : "string",
1168
1601
  target: "server",
1169
1602
  });
1170
1603
  const fatalDiagnostics = output.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");