@reckona/mreact-router 0.0.73 → 0.0.75

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 (47) hide show
  1. package/README.md +3 -1
  2. package/dist/actions.d.ts +14 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +55 -44
  5. package/dist/actions.js.map +1 -1
  6. package/dist/build.d.ts +12 -0
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +151 -52
  9. package/dist/build.js.map +1 -1
  10. package/dist/cli-options.d.ts +13 -0
  11. package/dist/cli-options.d.ts.map +1 -1
  12. package/dist/cli-options.js +73 -0
  13. package/dist/cli-options.js.map +1 -1
  14. package/dist/cli.js +4 -1
  15. package/dist/cli.js.map +1 -1
  16. package/dist/client.d.ts.map +1 -1
  17. package/dist/client.js +17 -8
  18. package/dist/client.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/render.d.ts +2 -1
  23. package/dist/render.d.ts.map +1 -1
  24. package/dist/render.js +4 -0
  25. package/dist/render.js.map +1 -1
  26. package/dist/route-source.d.ts +8 -0
  27. package/dist/route-source.d.ts.map +1 -1
  28. package/dist/route-source.js +15 -1
  29. package/dist/route-source.js.map +1 -1
  30. package/dist/serve.d.ts.map +1 -1
  31. package/dist/serve.js +6 -0
  32. package/dist/serve.js.map +1 -1
  33. package/dist/server-action-inference.d.ts +50 -0
  34. package/dist/server-action-inference.d.ts.map +1 -0
  35. package/dist/server-action-inference.js +488 -0
  36. package/dist/server-action-inference.js.map +1 -0
  37. package/package.json +12 -11
  38. package/src/actions.ts +92 -62
  39. package/src/build.ts +231 -71
  40. package/src/cli-options.ts +103 -0
  41. package/src/cli.ts +6 -0
  42. package/src/client.ts +14 -11
  43. package/src/index.ts +1 -0
  44. package/src/render.ts +9 -0
  45. package/src/route-source.ts +25 -0
  46. package/src/serve.ts +22 -0
  47. package/src/server-action-inference.ts +804 -0
package/src/build.ts CHANGED
@@ -4,7 +4,6 @@ import { copyFile, cp, mkdir, readdir, readFile, rm, stat, writeFile } from "nod
4
4
  import { builtinModules } from "node:module";
5
5
  import { dirname, join, relative, sep } from "node:path";
6
6
  import {
7
- collectFormActionReferenceNames,
8
7
  collectStaticImportReferences,
9
8
  collectTopLevelValueExportNames,
10
9
  formatDiagnostic,
@@ -56,6 +55,7 @@ import {
56
55
  routeClosureMayUseAwaitBoundary,
57
56
  stripRouteBuildExports,
58
57
  stripRouteClientOnlyExports,
58
+ stripRouteClientSource,
59
59
  stripRouteLoaderOnlyExports,
60
60
  stripRouteMetadataOnlyExports,
61
61
  stripRouteRequestOnlyExports,
@@ -67,6 +67,7 @@ import {
67
67
  import { collectRouteCssFiles } from "./route-styles.js";
68
68
  import { existingRouteShellCandidates } from "./route-shells.js";
69
69
  import { sourceModuleCandidates } from "./source-modules.js";
70
+ import { collectBuildInferredServerActions } from "./server-action-inference.js";
70
71
  import { workspacePackageFile } from "./workspace-packages.js";
71
72
  import type { PluginOption, UserConfig } from "vite";
72
73
 
@@ -112,6 +113,7 @@ export interface BuiltServerManifest {
112
113
  prerenderedRoutes?: Record<string, BuiltPrerenderedRoute>;
113
114
  publicAssetBaseUrl?: string;
114
115
  routesDir?: string;
116
+ routeServerActionReferences?: Record<string, BuiltServerActionExpressionReference[]>;
115
117
  serverActionManifest?: BuiltServerActionReference[];
116
118
  serverModuleFiles?: Record<string, string>;
117
119
  serverModuleRenderFiles?: Record<string, string>;
@@ -126,6 +128,18 @@ export interface BuiltServerActionReference {
126
128
  moduleId: string;
127
129
  }
128
130
 
131
+ export interface BuiltServerActionExpressionReference {
132
+ end: number;
133
+ exportName: string;
134
+ expression: string;
135
+ expressionEnd: number;
136
+ expressionStart: number;
137
+ inferred: boolean;
138
+ moduleId: string;
139
+ sourceHash: string;
140
+ start: number;
141
+ }
142
+
129
143
  export interface BuiltServerModuleArtifact {
130
144
  analysis?: BuiltRouteSourceAnalysisSummary;
131
145
  loader?: BuiltServerModuleOutput;
@@ -200,7 +214,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
200
214
  ...new Set([...(await collectPublicAssetPaths(project.publicDir)), ...appConventionPublicAssets]),
201
215
  ].sort();
202
216
 
203
- const serverActionManifest = collectBuildServerActionManifest({
217
+ const serverActionManifest = await collectBuildServerActionManifest({
204
218
  files,
205
219
  projectRoot: project.projectRoot,
206
220
  routes,
@@ -216,7 +230,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
216
230
  routes,
217
231
  vitePlugins,
218
232
  });
219
- const generatedImportPolicy = buildGeneratedImportPolicy({
233
+ const generatedImportPolicy = await buildGeneratedImportPolicy({
220
234
  files,
221
235
  projectRoot: project.projectRoot,
222
236
  routes,
@@ -274,6 +288,11 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
274
288
  });
275
289
  }
276
290
 
291
+ const routeServerActionReferences = Object.fromEntries(
292
+ [...serverActionManifest.routeReferences.entries()].sort(([left], [right]) =>
293
+ left.localeCompare(right),
294
+ ),
295
+ );
277
296
  const serverManifest = {
278
297
  allowedSourceDirs: project.allowedSourceDirs.map((directory) =>
279
298
  relative(project.projectRoot, directory),
@@ -287,7 +306,12 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
287
306
  ...(project.publicAssetBaseUrl === undefined
288
307
  ? {}
289
308
  : { publicAssetBaseUrl: project.publicAssetBaseUrl }),
290
- ...(serverActionManifest.length === 0 ? {} : { serverActionManifest }),
309
+ ...(Object.keys(routeServerActionReferences).length === 0
310
+ ? {}
311
+ : { routeServerActionReferences }),
312
+ ...(serverActionManifest.allowedActions.length === 0
313
+ ? {}
314
+ : { serverActionManifest: serverActionManifest.allowedActions }),
291
315
  ...(Object.keys(serverModuleArtifacts.files).length === 0
292
316
  ? {}
293
317
  : { serverModuleFiles: serverModuleArtifacts.files }),
@@ -461,22 +485,28 @@ const frameworkRuntimePackages = new Set([
461
485
  "@reckona/mreact-router",
462
486
  "@reckona/mreact-server",
463
487
  ]);
488
+ const maxRuntimePackageManifestReads = 1000;
464
489
 
465
- function buildGeneratedImportPolicy(options: {
490
+ async function buildGeneratedImportPolicy(options: {
466
491
  files: Record<string, string>;
467
492
  projectRoot: string;
468
493
  routes: readonly AppRoute[];
469
494
  routesDir: string;
470
- }): BuiltImportPolicyArtifact {
495
+ }): Promise<BuiltImportPolicyArtifact> {
471
496
  const routePackages = new Map<string, string[]>();
472
497
  const allPackages = new Set<string>();
473
498
  const relativeRoutesDir = relative(options.projectRoot, options.routesDir);
499
+ const packageJsonLookupCache = new Map<
500
+ string,
501
+ Promise<(RuntimePackageManifest & { packageJsonPath: string }) | undefined>
502
+ >();
474
503
 
475
504
  for (const route of options.routes) {
476
505
  const file = relative(options.projectRoot, route.file);
477
- const packages = collectRuntimePackagesForFile({
506
+ const packages = await collectRuntimePackagesForFile({
478
507
  file,
479
508
  files: options.files,
509
+ packageJsonLookupCache,
480
510
  projectRoot: options.projectRoot,
481
511
  seen: new Set(),
482
512
  });
@@ -494,9 +524,10 @@ function buildGeneratedImportPolicy(options: {
494
524
  .find((file) => options.files[file] !== undefined);
495
525
 
496
526
  if (middlewareFile !== undefined) {
497
- const packages = collectRuntimePackagesForFile({
527
+ const packages = await collectRuntimePackagesForFile({
498
528
  file: middlewareFile,
499
529
  files: options.files,
530
+ packageJsonLookupCache,
500
531
  projectRoot: options.projectRoot,
501
532
  seen: new Set(),
502
533
  });
@@ -518,12 +549,13 @@ function buildGeneratedImportPolicy(options: {
518
549
  };
519
550
  }
520
551
 
521
- function collectRuntimePackagesForFile(options: {
552
+ async function collectRuntimePackagesForFile(options: {
522
553
  file: string;
523
554
  files: Record<string, string>;
555
+ packageJsonLookupCache: RuntimePackageManifestCache;
524
556
  projectRoot: string;
525
557
  seen: Set<string>;
526
- }): string[] {
558
+ }): Promise<string[]> {
527
559
  if (options.seen.has(options.file)) {
528
560
  return [];
529
561
  }
@@ -548,7 +580,15 @@ function collectRuntimePackagesForFile(options: {
548
580
  filename: join(options.projectRoot, options.file),
549
581
  })) {
550
582
  if (isRuntimePackageSpecifier(reference.source)) {
551
- packages.add(runtimePackageNameForSpecifier(reference.source));
583
+ const packageName = runtimePackageNameForSpecifier(reference.source);
584
+ packages.add(packageName);
585
+ for (const optionalPackageName of await collectRuntimeOptionalPackages({
586
+ packageJsonLookupCache: options.packageJsonLookupCache,
587
+ packageName,
588
+ projectRoot: options.projectRoot,
589
+ })) {
590
+ packages.add(optionalPackageName);
591
+ }
552
592
  continue;
553
593
  }
554
594
 
@@ -557,7 +597,7 @@ function collectRuntimePackagesForFile(options: {
557
597
  continue;
558
598
  }
559
599
 
560
- for (const packageName of collectRuntimePackagesForFile({
600
+ for (const packageName of await collectRuntimePackagesForFile({
561
601
  ...options,
562
602
  file: localFile,
563
603
  })) {
@@ -570,13 +610,140 @@ function collectRuntimePackagesForFile(options: {
570
610
  return [...packages].sort();
571
611
  }
572
612
 
613
+ interface RuntimePackageManifest {
614
+ dependencies?: Record<string, unknown> | undefined;
615
+ optionalDependencies?: Record<string, unknown> | undefined;
616
+ }
617
+
618
+ type RuntimePackageManifestCache = Map<
619
+ string,
620
+ Promise<(RuntimePackageManifest & { packageJsonPath: string }) | undefined>
621
+ >;
622
+
623
+ async function collectRuntimeOptionalPackages(options: {
624
+ packageJsonLookupCache: RuntimePackageManifestCache;
625
+ packageName: string;
626
+ projectRoot: string;
627
+ }): Promise<string[]> {
628
+ const optionalPackages = new Set<string>();
629
+ const seenPackageJson = new Set<string>();
630
+ const queue: Array<{ packageName: string; optional: boolean; startDir: string }> = [
631
+ { packageName: options.packageName, optional: false, startDir: options.projectRoot },
632
+ ];
633
+
634
+ for (let index = 0; index < queue.length && seenPackageJson.size < maxRuntimePackageManifestReads; index += 1) {
635
+ const item = queue[index];
636
+ if (item === undefined || !isValidRuntimePackageName(item.packageName)) {
637
+ continue;
638
+ }
639
+
640
+ const manifest = await readRuntimePackageManifest({
641
+ cache: options.packageJsonLookupCache,
642
+ packageName: item.packageName,
643
+ startDir: item.startDir,
644
+ });
645
+
646
+ if (manifest === undefined || seenPackageJson.has(manifest.packageJsonPath)) {
647
+ continue;
648
+ }
649
+
650
+ seenPackageJson.add(manifest.packageJsonPath);
651
+ if (item.optional && isRuntimePackageName(item.packageName)) {
652
+ optionalPackages.add(item.packageName);
653
+ }
654
+
655
+ const packageDir = dirname(manifest.packageJsonPath);
656
+ for (const dependencyName of Object.keys(manifest.dependencies ?? {})) {
657
+ if (isValidRuntimePackageName(dependencyName)) {
658
+ queue.push({ packageName: dependencyName, optional: false, startDir: packageDir });
659
+ }
660
+ }
661
+
662
+ for (const optionalDependencyName of Object.keys(manifest.optionalDependencies ?? {})) {
663
+ if (isValidRuntimePackageName(optionalDependencyName)) {
664
+ queue.push({ packageName: optionalDependencyName, optional: true, startDir: packageDir });
665
+ }
666
+ }
667
+ }
668
+
669
+ return [...optionalPackages].sort();
670
+ }
671
+
672
+ async function readRuntimePackageManifest(options: {
673
+ cache: RuntimePackageManifestCache;
674
+ packageName: string;
675
+ startDir: string;
676
+ }): Promise<(RuntimePackageManifest & { packageJsonPath: string }) | undefined> {
677
+ const key = `${options.startDir}\0${options.packageName}`;
678
+ const cached = options.cache.get(key);
679
+ const manifest =
680
+ cached ??
681
+ findRuntimePackageJson(options.packageName, options.startDir).then(async (packageJsonPath) => {
682
+ if (packageJsonPath === undefined) {
683
+ return undefined;
684
+ }
685
+
686
+ try {
687
+ const json = JSON.parse(await readFile(packageJsonPath, "utf8")) as RuntimePackageManifest;
688
+ return { ...json, packageJsonPath };
689
+ } catch {
690
+ return undefined;
691
+ }
692
+ });
693
+
694
+ if (cached === undefined) {
695
+ options.cache.set(key, manifest);
696
+ }
697
+
698
+ return (await manifest) as (RuntimePackageManifest & { packageJsonPath: string }) | undefined;
699
+ }
700
+
701
+ async function findRuntimePackageJson(
702
+ packageName: string,
703
+ startDir: string,
704
+ ): Promise<string | undefined> {
705
+ if (!isValidRuntimePackageName(packageName)) {
706
+ return undefined;
707
+ }
708
+
709
+ let current = startDir;
710
+
711
+ while (true) {
712
+ const candidate = join(current, "node_modules", ...packageName.split("/"), "package.json");
713
+ try {
714
+ if ((await stat(candidate)).isFile()) {
715
+ return candidate;
716
+ }
717
+ } catch {
718
+ // Keep walking toward the filesystem root.
719
+ }
720
+
721
+ const parent = dirname(current);
722
+ if (parent === current) {
723
+ return undefined;
724
+ }
725
+ current = parent;
726
+ }
727
+ }
728
+
573
729
  function isRuntimePackageSpecifier(specifier: string): boolean {
574
730
  if (specifier.startsWith(".") || specifier.startsWith("/") || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(specifier)) {
575
731
  return false;
576
732
  }
577
733
 
578
734
  const packageName = runtimePackageNameForSpecifier(specifier);
579
- return !nodeBuiltinPackages.has(specifier) && !frameworkRuntimePackages.has(packageName);
735
+ return !nodeBuiltinPackages.has(specifier) && isRuntimePackageName(packageName);
736
+ }
737
+
738
+ function isRuntimePackageName(packageName: string): boolean {
739
+ return !frameworkRuntimePackages.has(packageName);
740
+ }
741
+
742
+ function isValidRuntimePackageName(packageName: string): boolean {
743
+ return (
744
+ /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/u.test(packageName) &&
745
+ !packageName.includes("..")
746
+ );
580
747
  }
581
748
 
582
749
  function runtimePackageNameForSpecifier(specifier: string): string {
@@ -588,13 +755,17 @@ function runtimePackageNameForSpecifier(specifier: string): string {
588
755
  return scope !== undefined && name !== undefined ? `${scope}/${name}` : specifier;
589
756
  }
590
757
 
591
- function collectBuildServerActionManifest(options: {
758
+ async function collectBuildServerActionManifest(options: {
592
759
  files: Record<string, string>;
593
760
  projectRoot: string;
594
761
  routes: readonly AppRoute[];
595
762
  routesDir: string;
596
- }): BuiltServerActionReference[] {
763
+ }): Promise<{
764
+ allowedActions: BuiltServerActionReference[];
765
+ routeReferences: Map<string, BuiltServerActionExpressionReference[]>;
766
+ }> {
597
767
  const entries = new Map<string, BuiltServerActionReference>();
768
+ const routeReferences = new Map<string, BuiltServerActionExpressionReference[]>();
598
769
  const relativeRoutesDir = relative(options.projectRoot, options.routesDir);
599
770
  const routeSourceFiles = new Set(
600
771
  options.routes
@@ -616,26 +787,41 @@ function collectBuildServerActionManifest(options: {
616
787
  }
617
788
 
618
789
  if (routeSourceFiles.has(file)) {
619
- for (const reference of collectBuildInferredServerActionReferences({
790
+ const inference = await collectBuildInferredServerActionReferences({
620
791
  file,
621
792
  files: options.files,
622
793
  relativeRoutesDir,
623
794
  source: code,
624
- })) {
795
+ });
796
+
797
+ for (const diagnostic of inference.diagnostics) {
798
+ console.warn(formatServerActionInferenceDiagnostic(diagnostic));
799
+ }
800
+
801
+ routeReferences.set(file, inference.references);
802
+
803
+ for (const reference of inference.references) {
625
804
  const key = `${reference.moduleId}#${reference.exportName}`;
626
805
 
627
806
  if (!entries.has(key)) {
628
- entries.set(key, reference);
807
+ entries.set(key, {
808
+ exportName: reference.exportName,
809
+ inferred: reference.inferred,
810
+ moduleId: reference.moduleId,
811
+ });
629
812
  }
630
813
  }
631
814
  }
632
815
  }
633
816
 
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
- );
817
+ return {
818
+ allowedActions: [...entries.values()].sort((left, right) =>
819
+ left.moduleId === right.moduleId
820
+ ? left.exportName.localeCompare(right.exportName)
821
+ : left.moduleId.localeCompare(right.moduleId),
822
+ ),
823
+ routeReferences,
824
+ };
639
825
  }
640
826
 
641
827
  function collectBuildInferredServerActionReferences(options: {
@@ -643,54 +829,25 @@ function collectBuildInferredServerActionReferences(options: {
643
829
  files: Record<string, string>;
644
830
  relativeRoutesDir: string;
645
831
  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;
832
+ }): Promise<{
833
+ diagnostics: { code: string; message: string }[];
834
+ references: BuiltServerActionExpressionReference[];
835
+ }> {
836
+ return collectBuildInferredServerActions({
837
+ file: options.file,
838
+ files: options.files,
839
+ relativeRoutesDir: options.relativeRoutesDir,
840
+ resolveSourceImport: (importer, source) =>
841
+ resolveBuildLocalSourceImport(options.files, importer, source),
842
+ source: options.source,
843
+ });
690
844
  }
691
845
 
692
- function collectFormActionNames(code: string): Set<string> {
693
- return new Set(collectFormActionReferenceNames({ code }));
846
+ function formatServerActionInferenceDiagnostic(diagnostic: {
847
+ code: string;
848
+ message: string;
849
+ }): string {
850
+ return `${diagnostic.code}: ${diagnostic.message}`;
694
851
  }
695
852
 
696
853
  function isSourceModuleFile(file: string): boolean {
@@ -1038,7 +1195,10 @@ async function buildServerModuleArtifacts(options: {
1038
1195
  const clientInference = await inferClientRouteModule({
1039
1196
  ...(route === undefined ? {} : { appDir: options.project.routesDir }),
1040
1197
  cache: options.clientRouteInferenceCache,
1041
- code: stripRouteClientOnlyExports(source),
1198
+ code:
1199
+ route === undefined
1200
+ ? stripRouteClientOnlyExports(source)
1201
+ : stripRouteClientSource({ code: source, filename: route.file }),
1042
1202
  filename: join(options.projectRoot, file),
1043
1203
  ...(route === undefined ? {} : { routePath: route.path }),
1044
1204
  vitePlugins: options.vitePlugins,
@@ -2467,7 +2627,7 @@ async function writeClientRouteBundles(options: {
2467
2627
  vitePlugins: options.vitePlugins,
2468
2628
  });
2469
2629
  const source = await readFile(route.file, "utf8");
2470
- const clientSource = stripRouteClientOnlyExports(source);
2630
+ const clientSource = stripRouteClientSource({ code: source, filename: route.file });
2471
2631
  const navigation = detectNavigationRuntimeHint(source);
2472
2632
  const references = await collectClientRouteReferences({
2473
2633
  appDir: options.appDir,
@@ -3,6 +3,7 @@ import type {
3
3
  AppRouterBuildTarget,
4
4
  AppRouterClientSourceMapMode,
5
5
  } from "./config.js";
6
+ import type { RequestHostPolicy } from "./serve.js";
6
7
 
7
8
  export type CliRequestLogMode = "requests";
8
9
  export type CliBuildTarget = AppRouterBuildTarget | "all";
@@ -10,8 +11,11 @@ export type CliBuildTarget = AppRouterBuildTarget | "all";
10
11
  export interface ParsedCliArguments {
11
12
  clientSourceMaps?: AppRouterClientSourceMapMode | undefined;
12
13
  command: string;
14
+ allowedHosts?: readonly string[] | undefined;
13
15
  from?: string | undefined;
14
16
  help?: boolean | undefined;
17
+ host?: string | undefined;
18
+ hostPolicy?: RequestHostPolicy | undefined;
15
19
  log?: CliRequestLogMode | undefined;
16
20
  out?: string | undefined;
17
21
  routeArg?: string | undefined;
@@ -43,6 +47,39 @@ export function parseCliArguments(argv: readonly string[]): ParsedCliArguments {
43
47
  continue;
44
48
  }
45
49
 
50
+ if (value === "--host") {
51
+ parsed.host = readOptionValue(argv, index, "host");
52
+ index += 1;
53
+ continue;
54
+ }
55
+
56
+ if (value.startsWith("--host=")) {
57
+ parsed.host = value.slice("--host=".length);
58
+ continue;
59
+ }
60
+
61
+ if (value === "--host-policy") {
62
+ parsed.hostPolicy = parseCliHostPolicy(readOptionValue(argv, index, "host-policy"));
63
+ index += 1;
64
+ continue;
65
+ }
66
+
67
+ if (value.startsWith("--host-policy=")) {
68
+ parsed.hostPolicy = parseCliHostPolicy(value.slice("--host-policy=".length));
69
+ continue;
70
+ }
71
+
72
+ if (value === "--allowed-hosts") {
73
+ parsed.allowedHosts = parseCliAllowedHosts(readOptionValue(argv, index, "allowed-hosts"));
74
+ index += 1;
75
+ continue;
76
+ }
77
+
78
+ if (value.startsWith("--allowed-hosts=")) {
79
+ parsed.allowedHosts = parseCliAllowedHosts(value.slice("--allowed-hosts=".length));
80
+ continue;
81
+ }
82
+
46
83
  if (value === "--from") {
47
84
  parsed.from = readOptionValue(argv, index, "from");
48
85
  index += 1;
@@ -155,8 +192,21 @@ export function formatCliHelp(command?: string | undefined): string {
155
192
  "Serve built mreact app router output with the Node adapter.",
156
193
  "",
157
194
  "Options:",
195
+ " --host <host> Bind address. Default: 127.0.0.1. Use 0.0.0.0 inside containers behind explicit port publishing or a reverse proxy.",
196
+ " --host-policy=strict|trusted-proxy",
197
+ " Control Host header trust for request origin reconstruction.",
198
+ " --allowed-hosts <host[,host...]>",
199
+ " Exact Host header allow-list for public deployments.",
158
200
  " --log=requests Print request summaries.",
159
201
  " -h, --help Show this help message.",
202
+ "",
203
+ "Environment:",
204
+ " HOST Bind address when --host is not set.",
205
+ " MREACT_ROUTER_HOST_POLICY",
206
+ " Host header trust policy when --host-policy is not set.",
207
+ " MREACT_ROUTER_ALLOWED_HOSTS",
208
+ " Comma-separated Host header allow-list when --allowed-hosts is not set.",
209
+ " PORT TCP port. Default: 3001.",
160
210
  ].join("\n");
161
211
  }
162
212
 
@@ -222,6 +272,42 @@ export function resolveCliRequestLogMode(
222
272
  return parseCliRequestLogMode(envValue);
223
273
  }
224
274
 
275
+ export function resolveCliHost(
276
+ flagValue: string | undefined,
277
+ env: { HOST?: string | undefined },
278
+ ): string {
279
+ if (flagValue !== undefined && flagValue !== "") {
280
+ return flagValue;
281
+ }
282
+
283
+ const envValue = env.HOST;
284
+ return envValue === undefined || envValue === "" ? "127.0.0.1" : envValue;
285
+ }
286
+
287
+ export function resolveCliHostPolicy(
288
+ flagValue: RequestHostPolicy | undefined,
289
+ env: { MREACT_ROUTER_HOST_POLICY?: string | undefined },
290
+ ): RequestHostPolicy | undefined {
291
+ if (flagValue !== undefined) {
292
+ return flagValue;
293
+ }
294
+
295
+ const envValue = env.MREACT_ROUTER_HOST_POLICY;
296
+ return envValue === undefined || envValue === "" ? undefined : parseCliHostPolicy(envValue);
297
+ }
298
+
299
+ export function resolveCliAllowedHosts(
300
+ flagValue: readonly string[] | undefined,
301
+ env: { MREACT_ROUTER_ALLOWED_HOSTS?: string | undefined },
302
+ ): readonly string[] | undefined {
303
+ if (flagValue !== undefined) {
304
+ return flagValue;
305
+ }
306
+
307
+ const envValue = env.MREACT_ROUTER_ALLOWED_HOSTS;
308
+ return envValue === undefined || envValue === "" ? undefined : parseCliAllowedHosts(envValue);
309
+ }
310
+
225
311
  export function createCliRequestLogger(): AppRouterLogger {
226
312
  return {
227
313
  error(event) {
@@ -245,6 +331,23 @@ function parseCliRequestLogMode(value: string): CliRequestLogMode {
245
331
  throw new Error(`Unsupported log mode ${JSON.stringify(value)}. Expected "requests".`);
246
332
  }
247
333
 
334
+ function parseCliHostPolicy(value: string): RequestHostPolicy {
335
+ if (value === "strict" || value === "trusted-proxy") {
336
+ return value;
337
+ }
338
+
339
+ throw new Error(
340
+ `Unsupported host policy ${JSON.stringify(value)}. Expected "strict" or "trusted-proxy".`,
341
+ );
342
+ }
343
+
344
+ function parseCliAllowedHosts(value: string): readonly string[] {
345
+ return value
346
+ .split(",")
347
+ .map((entry) => entry.trim())
348
+ .filter((entry) => entry !== "");
349
+ }
350
+
248
351
  function parseCliBuildTarget(value: string): CliBuildTarget {
249
352
  if (value === "node" || value === "cloudflare" || value === "aws-lambda" || value === "all") {
250
353
  return value;
package/src/cli.ts CHANGED
@@ -7,6 +7,9 @@ import {
7
7
  createCliRequestLogger,
8
8
  formatCliHelp,
9
9
  parseCliArguments,
10
+ resolveCliAllowedHosts,
11
+ resolveCliHost,
12
+ resolveCliHostPolicy,
10
13
  resolveCliRequestLogMode,
11
14
  } from "./cli-options.js";
12
15
  import { startDevServer } from "./dev-server.js";
@@ -77,6 +80,9 @@ if (parsed !== undefined) {
77
80
  console.log(`mreact app router ready at ${server.url}`);
78
81
  } else if (command === "start") {
79
82
  const server = await startServer({
83
+ allowedHosts: resolveCliAllowedHosts(parsed.allowedHosts, process.env),
84
+ hostPolicy: resolveCliHostPolicy(parsed.hostPolicy, process.env),
85
+ hostname: resolveCliHost(parsed.host, process.env),
80
86
  logger,
81
87
  outDir: resolve(routeArg ?? ".mreact"),
82
88
  port: Number(process.env.PORT ?? 3001),