@reckona/mreact-router 0.0.175 → 0.0.176

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 (64) hide show
  1. package/dist/adapters/cloudflare.d.ts.map +1 -1
  2. package/dist/adapters/cloudflare.js +5 -49
  3. package/dist/adapters/cloudflare.js.map +1 -1
  4. package/dist/build.d.ts +1 -0
  5. package/dist/build.d.ts.map +1 -1
  6. package/dist/build.js +90 -0
  7. package/dist/build.js.map +1 -1
  8. package/dist/built-assets.d.ts +11 -0
  9. package/dist/built-assets.d.ts.map +1 -0
  10. package/dist/built-assets.js +130 -0
  11. package/dist/built-assets.js.map +1 -0
  12. package/dist/built-runtime.d.ts +53 -0
  13. package/dist/built-runtime.d.ts.map +1 -0
  14. package/dist/built-runtime.js +178 -0
  15. package/dist/built-runtime.js.map +1 -0
  16. package/dist/built-server-module-artifacts.d.ts +19 -0
  17. package/dist/built-server-module-artifacts.d.ts.map +1 -0
  18. package/dist/built-server-module-artifacts.js +217 -0
  19. package/dist/built-server-module-artifacts.js.map +1 -0
  20. package/dist/client-manifest-assets.d.ts +16 -0
  21. package/dist/client-manifest-assets.d.ts.map +1 -0
  22. package/dist/client-manifest-assets.js +60 -0
  23. package/dist/client-manifest-assets.js.map +1 -0
  24. package/dist/client.d.ts.map +1 -1
  25. package/dist/client.js +31 -3
  26. package/dist/client.js.map +1 -1
  27. package/dist/node-server.d.ts +29 -0
  28. package/dist/node-server.d.ts.map +1 -0
  29. package/dist/node-server.js +81 -0
  30. package/dist/node-server.js.map +1 -0
  31. package/dist/render.d.ts +9 -0
  32. package/dist/render.d.ts.map +1 -1
  33. package/dist/render.js +20 -41
  34. package/dist/render.js.map +1 -1
  35. package/dist/route-dispatch.d.ts +2 -0
  36. package/dist/route-dispatch.d.ts.map +1 -0
  37. package/dist/route-dispatch.js +16 -0
  38. package/dist/route-dispatch.js.map +1 -0
  39. package/dist/route-loader-runtime.d.ts +22 -0
  40. package/dist/route-loader-runtime.d.ts.map +1 -0
  41. package/dist/route-loader-runtime.js +30 -0
  42. package/dist/route-loader-runtime.js.map +1 -0
  43. package/dist/routes.d.ts +2 -0
  44. package/dist/routes.d.ts.map +1 -1
  45. package/dist/routes.js +48 -1
  46. package/dist/routes.js.map +1 -1
  47. package/dist/serve.d.ts +4 -3
  48. package/dist/serve.d.ts.map +1 -1
  49. package/dist/serve.js +84 -582
  50. package/dist/serve.js.map +1 -1
  51. package/package.json +11 -11
  52. package/src/adapters/cloudflare.ts +5 -60
  53. package/src/build.ts +143 -0
  54. package/src/built-assets.ts +164 -0
  55. package/src/built-runtime.ts +312 -0
  56. package/src/built-server-module-artifacts.ts +340 -0
  57. package/src/client-manifest-assets.ts +92 -0
  58. package/src/client.ts +31 -3
  59. package/src/node-server.ts +129 -0
  60. package/src/render.ts +38 -60
  61. package/src/route-dispatch.ts +17 -0
  62. package/src/route-loader-runtime.ts +54 -0
  63. package/src/routes.ts +70 -1
  64. package/src/serve.ts +105 -863
package/src/serve.ts CHANGED
@@ -1,19 +1,29 @@
1
- import { createServer, type Server } from "node:http";
2
- import { createHash } from "node:crypto";
3
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
- import { dirname, isAbsolute, join, normalize, sep } from "node:path";
5
- import type {
6
- BuiltPrerenderedRoute,
7
- BuiltServerManifest,
8
- BuiltServerModuleArtifact,
9
- } from "./build.js";
1
+ import type { Server } from "node:http";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import type { BuiltPrerenderedRoute } from "./build.js";
10
5
  import type { AppRouterCache } from "./cache.js";
11
- import type { ClientRouteManifestEntry } from "./client-route-inference.js";
12
6
  import {
13
- createRouteMatcher,
7
+ builtClientAssetPathname,
8
+ clearBuiltPublicAssetCacheForTest,
9
+ getBuiltPublicAssetCacheSizeForTest,
10
+ readBuiltClientAsset,
11
+ readBuiltPublicAsset,
12
+ } from "./built-assets.js";
13
+ import {
14
+ materializeBuiltRuntime,
15
+ mergeBuiltRuntimeImportPolicy,
16
+ readBuiltImportPolicyText,
17
+ type BuiltRuntime,
18
+ } from "./built-runtime.js";
19
+ import {
20
+ allBuiltServerModuleFiles,
21
+ loadBuiltServerModuleArtifacts,
22
+ loadBuiltServerModuleArtifactsForRequest,
23
+ } from "./built-server-module-artifacts.js";
24
+ import {
14
25
  type AppRoute,
15
26
  type MatchedRoute,
16
- type RouteMatcher,
17
27
  } from "./routes.js";
18
28
  import type { AppRouterServerActionOptions } from "./actions.js";
19
29
  import type { AppRouterImportPolicy } from "./import-policy.js";
@@ -21,75 +31,27 @@ import {
21
31
  preloadBuiltRequestModules,
22
32
  renderAppRequest,
23
33
  resolveAppRouterMiddleware,
34
+ type AppRouterServerRenderArtifactLoader,
24
35
  type AppRouterRenderPreload,
25
36
  type AppRouterResponseHook,
26
- type RenderAppRequestOptions,
37
+ type RenderAppRequestRuntimeOptions,
27
38
  } from "./render.js";
28
39
  import type { RouterInstrumentation } from "./trace.js";
29
40
  import {
30
41
  bytesResponse,
31
42
  htmlResponse,
32
- nodeRequestToWebRequest,
33
- rawNodeRequestUrl,
34
- sendResponse,
35
43
  } from "./http.js";
36
44
  import {
37
45
  emitRouterLog,
38
46
  logDurationMs,
39
- logError,
40
47
  logNow,
41
- nodeRequestPath,
42
- requestLogFields,
43
48
  type AppRouterLogger,
44
49
  } from "./logger.js";
50
+ import { startNodeRequestServer } from "./node-server.js";
45
51
  import { builtAppRuntimePreloadPlan } from "./preload-policy.js";
46
- import { routeShellCandidates } from "./route-shells.js";
47
52
  import { normalizeRoutePath } from "./route-path.js";
48
53
  import type { HttpUpgradeHandler } from "./upgrade.js";
49
54
 
50
- interface BuiltRuntime {
51
- appDir: string;
52
- allowedSourceDirs: readonly string[];
53
- assetBaseUrl?: string | undefined;
54
- clientAssetPaths: ReadonlySet<string>;
55
- clientScripts: ReadonlyMap<string, string>;
56
- clientStylesByFile: ReadonlyMap<string, readonly string[]>;
57
- clientStyles: ReadonlyMap<string, readonly string[]>;
58
- generatedImportPolicy?: AppRouterImportPolicy | undefined;
59
- hasMiddleware: boolean;
60
- navigationScripts: ReadonlyMap<string, string>;
61
- projectRoot: string;
62
- publicAssetBaseUrl?: string | undefined;
63
- prerenderableRoutes: ReadonlySet<string>;
64
- prerenderLocks: Map<string, Promise<Response>>;
65
- prerenderedRoutes: Map<string, BuiltPrerenderedRoute>;
66
- routeMatcher: RouteMatcher;
67
- routes: readonly AppRoute[];
68
- serverActionReferencesByFile: ReadonlyMap<
69
- string,
70
- readonly {
71
- end: number;
72
- expression: string;
73
- expressionEnd: number;
74
- expressionStart: number;
75
- moduleId: string;
76
- exportName: string;
77
- inferred: boolean;
78
- sourceHash: string;
79
- start: number;
80
- }[]
81
- >;
82
- serverActionManifest?: readonly { moduleId: string; exportName: string; inferred?: boolean }[] | undefined;
83
- serverModuleArtifactLoads: Map<string, Promise<void>>;
84
- serverModuleClosureFiles: Map<string, readonly string[]>;
85
- serverModuleFiles: ReadonlyMap<string, string>;
86
- serverModuleRenderFiles: ReadonlyMap<string, string>;
87
- serverModuleRequestFiles: ReadonlyMap<string, string>;
88
- serverModules: Map<string, BuiltServerModuleArtifact>;
89
- serverModuleCacheVersion: string;
90
- serverSourceFiles: ReadonlyMap<string, string>;
91
- }
92
-
93
55
  interface BuiltRuntimeCacheEntry {
94
56
  clientManifestText: string;
95
57
  importPolicyText: string | undefined;
@@ -98,12 +60,8 @@ interface BuiltRuntimeCacheEntry {
98
60
  }
99
61
 
100
62
  const builtRuntimeCache = new Map<string, BuiltRuntimeCacheEntry>();
101
- const builtPublicAssetCache = new Map<string, BuiltPublicAsset>();
102
-
103
- interface BuiltPublicAsset {
104
- bytes: Uint8Array;
105
- headers: HeadersInit;
106
- }
63
+ const builtRuntimeReadInflight = new Map<string, Promise<BuiltRuntime>>();
64
+ let builtRuntimeMaterializeCountForTest = 0;
107
65
 
108
66
  /**
109
67
  * Strategy for the final response body shape sent to the HTTP layer.
@@ -615,30 +573,18 @@ export async function startServer(
615
573
  importPolicy: options.importPolicy,
616
574
  outDir: options.outDir,
617
575
  });
618
- const server = createServer(async (incoming, outgoing) => {
619
- const startedAt = logNow();
620
- const fallbackRequestFields = {
621
- method: incoming.method ?? "GET",
622
- path: nodeRequestPath(incoming.url),
623
- runtime: "node" as const,
624
- };
625
-
626
- try {
627
- const fallbackHost = `${options.hostname ?? "127.0.0.1"}:${options.port}`;
628
- const host = resolveRequestHost({
629
- allowedHosts: options.allowedHosts,
630
- fallbackHost,
631
- hostPolicy: options.hostPolicy,
632
- rawHost: incoming.headers.host,
633
- });
634
- const origin = `http://${host}`;
635
- const request = nodeRequestToWebRequest(incoming, origin);
636
- const logFields = requestLogFields(request, "node");
637
- emitRouterLog(options.logger, "info", {
638
- ...logFields,
639
- type: "router:request:start",
640
- });
641
- const response = await runtime.render(request, {
576
+
577
+ return await startNodeRequestServer({
578
+ allowedHosts: options.allowedHosts,
579
+ errorHandler: options.errorHandler,
580
+ hostname: options.hostname,
581
+ hostPolicy: options.hostPolicy,
582
+ logger: options.logger,
583
+ onUpgrade: options.onUpgrade,
584
+ port: options.port,
585
+ resolveHost: resolveRequestHost,
586
+ render: (request) =>
587
+ runtime.render(request, {
642
588
  instrumentation: options.instrumentation,
643
589
  logger: options.logger,
644
590
  onResponse: options.onResponse,
@@ -646,217 +592,28 @@ export async function startServer(
646
592
  routeCache: options.routeCache,
647
593
  serverActions: options.serverActions,
648
594
  ...(options.sinkStrategy === undefined ? {} : { sinkStrategy: options.sinkStrategy }),
649
- });
650
- emitRouterLog(options.logger, "info", {
651
- ...logFields,
652
- durationMs: logDurationMs(startedAt),
653
- status: response.status,
654
- type: "router:request:end",
655
- });
656
-
657
- await sendResponse(outgoing, response);
658
- } catch (error) {
659
- // Log the full stack to stderr for operator visibility; never
660
- // place it in the response body where attackers can scrape it
661
- // (Issue 071). The errorHandler hook lets embedders customize
662
- // the public response shape while still benefiting from the
663
- // server-side log.
664
- emitRouterLog(options.logger, "error", {
665
- ...fallbackRequestFields,
666
- durationMs: logDurationMs(startedAt),
667
- error: logError(error),
668
- type: "router:request:error",
669
- });
670
- if (options.logger === undefined) {
671
- console.error("[mreact] startServer request failed:", error);
672
- }
673
- const payload = options.errorHandler
674
- ? options.errorHandler(error)
675
- : { body: "Internal Server Error", status: 500 };
676
- outgoing.statusCode = payload.status;
677
- outgoing.setHeader(
678
- "content-type",
679
- payload.headers?.["content-type"] ?? "text/plain; charset=utf-8",
680
- );
681
- for (const [name, value] of Object.entries(payload.headers ?? {})) {
682
- if (name.toLowerCase() === "content-type") continue;
683
- outgoing.setHeader(name, value);
684
- }
685
- outgoing.end(payload.body);
686
- }
595
+ }),
687
596
  });
688
-
689
- if (options.onUpgrade !== undefined) {
690
- server.on("upgrade", options.onUpgrade);
691
- }
692
-
693
- await new Promise<void>((resolve) =>
694
- server.listen(options.port, options.hostname ?? "127.0.0.1", resolve),
695
- );
696
- const address = server.address();
697
- const port = typeof address === "object" && address !== null ? address.port : options.port;
698
-
699
- return {
700
- server,
701
- url: `http://${options.hostname ?? "127.0.0.1"}:${port}`,
702
- close: () =>
703
- new Promise<void>((resolve, reject) =>
704
- server.close((error) => (error ? reject(error) : resolve())),
705
- ),
706
- };
707
- }
708
-
709
- async function readBuiltClientAsset(
710
- outDir: string,
711
- pathname: string,
712
- allowedPaths: ReadonlySet<string>,
713
- ): Promise<Response> {
714
- const clientPrefix = "/_mreact/client/";
715
- const normalized = safeBuiltClientAssetPath(pathname.slice(clientPrefix.length));
716
-
717
- if (normalized === undefined || !allowedPaths.has(normalized)) {
718
- return new Response("Not Found", { status: 404 });
719
- }
720
-
721
- try {
722
- const code = await readFile(join(outDir, "client", normalized), "utf8");
723
-
724
- return new Response(code, {
725
- headers: clientAssetHeaders(normalized),
726
- });
727
- } catch {
728
- return new Response("Not Found", { status: 404 });
729
- }
730
597
  }
731
598
 
732
- function builtClientAssetPathname(request: Request, url: URL): string | undefined {
733
- const rawUrl = rawNodeRequestUrl(request);
734
- const rawPathname = rawUrl?.split(/[?#]/, 1)[0];
735
- const pathname = rawPathname === undefined || rawPathname === "" ? url.pathname : rawPathname;
736
- const normalizedPathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
737
-
738
- return normalizedPathname.startsWith("/_mreact/client/") ? normalizedPathname : undefined;
739
- }
740
-
741
- function safeBuiltClientAssetPath(relativePath: string): string | undefined {
742
- let decoded: string;
743
-
744
- try {
745
- decoded = decodeURIComponent(relativePath);
746
- } catch {
747
- return undefined;
748
- }
749
-
750
- if (decoded.includes("\\") || decoded.includes("\0")) {
751
- return undefined;
752
- }
753
-
754
- if (decoded.split("/").some((segment) => segment === "..")) {
755
- return undefined;
756
- }
757
-
758
- const normalized = normalize(decoded);
759
-
760
- if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${sep}`)) {
761
- return undefined;
762
- }
763
-
764
- return normalized;
765
- }
766
-
767
- function builtClientAssetPaths(manifest: {
768
- assets?: readonly string[] | undefined;
769
- routes: readonly ClientRouteManifestEntry[];
770
- }): ReadonlySet<string> {
771
- const paths = new Set<string>(["manifest.json"]);
772
-
773
- for (const route of manifest.routes) {
774
- for (const asset of [
775
- route.script,
776
- route.sourceMap,
777
- route.navigationScript,
778
- ...(route.css ?? []),
779
- ...(route.imports ?? []),
780
- ]) {
781
- const path = safeClientManifestAssetPath(asset);
782
-
783
- if (path !== undefined) {
784
- paths.add(path);
785
- }
786
- }
787
- }
788
-
789
- for (const asset of manifest.assets ?? []) {
790
- const path = safeClientManifestAssetPath(asset);
791
-
792
- if (path !== undefined) {
793
- paths.add(path);
794
- }
795
- }
796
-
797
- return paths;
798
- }
799
-
800
- function safeClientManifestAssetPath(asset: string | undefined): string | undefined {
801
- if (asset === undefined || asset === "" || asset.startsWith("/") || asset.includes("\\")) {
802
- return undefined;
803
- }
804
-
805
- const segments = asset.split("/");
599
+ export const __readBuiltPublicAssetForTest = readBuiltPublicAsset;
806
600
 
807
- return segments.some((segment) => segment === "" || segment === "." || segment === "..")
808
- ? undefined
809
- : segments.join("/");
601
+ export function __clearBuiltRuntimeCacheForTest(): void {
602
+ builtRuntimeCache.clear();
603
+ builtRuntimeReadInflight.clear();
604
+ builtRuntimeMaterializeCountForTest = 0;
810
605
  }
811
606
 
812
- async function readBuiltPublicAsset(
813
- outDir: string,
814
- pathname: string,
815
- ): Promise<Response | undefined> {
816
- const relativePath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
817
-
818
- if (relativePath === "") {
819
- return undefined;
820
- }
821
-
822
- const normalized = normalize(relativePath);
823
-
824
- if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
825
- return undefined;
826
- }
827
-
828
- try {
829
- const cacheKey = `${outDir}\0${normalized}`;
830
- const cached = builtPublicAssetCache.get(cacheKey);
831
-
832
- if (cached !== undefined) {
833
- return bytesResponse(cached.bytes, {
834
- headers: cached.headers,
835
- });
836
- }
837
-
838
- const bytes = await readFile(join(outDir, "client", "public", normalized));
839
- const headers = publicAssetHeaders(normalized);
840
-
841
- builtPublicAssetCache.set(cacheKey, {
842
- bytes,
843
- headers,
844
- });
845
-
846
- return bytesResponse(bytes, { headers });
847
- } catch {
848
- return undefined;
849
- }
607
+ export function __getBuiltRuntimeMaterializeCountForTest(): number {
608
+ return builtRuntimeMaterializeCountForTest;
850
609
  }
851
610
 
852
- export const __readBuiltPublicAssetForTest = readBuiltPublicAsset;
853
-
854
611
  export function __clearBuiltPublicAssetCacheForTest(): void {
855
- builtPublicAssetCache.clear();
612
+ clearBuiltPublicAssetCacheForTest();
856
613
  }
857
614
 
858
615
  export function __getBuiltPublicAssetCacheSizeForTest(): number {
859
- return builtPublicAssetCache.size;
616
+ return getBuiltPublicAssetCacheSizeForTest();
860
617
  }
861
618
 
862
619
  async function readBuiltRuntime(options: {
@@ -873,6 +630,44 @@ async function readBuiltRuntime(options: {
873
630
  return cached.runtime;
874
631
  }
875
632
 
633
+ if (cached === undefined) {
634
+ const inflight = builtRuntimeReadInflight.get(cacheKey);
635
+ if (inflight !== undefined) {
636
+ return inflight;
637
+ }
638
+
639
+ const runtime = readBuiltRuntimeUncached({
640
+ cacheKey,
641
+ cached,
642
+ outDir,
643
+ runtimeDir,
644
+ });
645
+ builtRuntimeReadInflight.set(cacheKey, runtime);
646
+ void runtime
647
+ .finally(() => {
648
+ if (builtRuntimeReadInflight.get(cacheKey) === runtime) {
649
+ builtRuntimeReadInflight.delete(cacheKey);
650
+ }
651
+ })
652
+ .catch(() => {});
653
+ return runtime;
654
+ }
655
+
656
+ return readBuiltRuntimeUncached({
657
+ cacheKey,
658
+ cached,
659
+ outDir,
660
+ runtimeDir,
661
+ });
662
+ }
663
+
664
+ async function readBuiltRuntimeUncached(options: {
665
+ cached: BuiltRuntimeCacheEntry | undefined;
666
+ cacheKey: string;
667
+ outDir: string;
668
+ runtimeDir: string;
669
+ }): Promise<BuiltRuntime> {
670
+ const { cacheKey, cached, outDir, runtimeDir } = options;
876
671
  const serverManifestPath = join(outDir, "server", "manifest.json");
877
672
  const clientManifestPath = join(outDir, "client", "manifest.json");
878
673
  const importPolicyPath = join(outDir, "server", "import-policy.json");
@@ -896,6 +691,9 @@ async function readBuiltRuntime(options: {
896
691
  clientManifestPath,
897
692
  importPolicyText,
898
693
  importPolicyPath,
694
+ onMaterialize() {
695
+ builtRuntimeMaterializeCountForTest += 1;
696
+ },
899
697
  outDir,
900
698
  runtimeDir,
901
699
  serverManifestText,
@@ -908,209 +706,13 @@ async function readBuiltRuntime(options: {
908
706
  runtime,
909
707
  serverManifestText,
910
708
  });
911
-
912
- return runtime;
913
- }
914
-
915
- async function materializeBuiltRuntime(options: {
916
- clientManifestText: string;
917
- clientManifestPath: string;
918
- importPolicyPath: string;
919
- importPolicyText: string | undefined;
920
- outDir: string;
921
- runtimeDir: string;
922
- serverManifestText: string;
923
- serverManifestPath: string;
924
- }): Promise<BuiltRuntime> {
925
- const serverManifest = parseBuiltJsonArtifact<BuiltServerManifest>(
926
- options.serverManifestText,
927
- options.serverManifestPath,
928
- "built app server manifest",
929
- );
930
- const clientManifest = parseBuiltJsonArtifact<{
931
- assets?: readonly string[];
932
- routes: ClientRouteManifestEntry[];
933
- styles?: Array<{ css?: readonly string[]; file: string }>;
934
- }>(options.clientManifestText, options.clientManifestPath, "built app client manifest");
935
- const appDir = await materializeBuiltServerApp(options.runtimeDir, serverManifest);
936
- const projectRoot = appDir;
937
- const routesDir = join(projectRoot, serverManifest.routesDir ?? "");
938
- const routes = serverManifest.routes.map((route) => ({
939
- ...route,
940
- file: join(projectRoot, route.file),
941
- }));
942
- const prerenderedRoutes = new Map(Object.entries(serverManifest.prerenderedRoutes ?? {}));
943
- const prerenderableRoutes = new Set(prerenderedRoutes.keys());
944
- const prerenderLocks = new Map<string, Promise<Response>>();
945
- const serverModules = new Map<string, BuiltServerModuleArtifact>(
946
- Object.entries(serverManifest.serverModules ?? {}).map(([file, artifact]) => [
947
- join(appDir, file),
948
- artifact,
949
- ]),
950
- );
951
- const serverModuleFiles = new Map(
952
- Object.entries(serverManifest.serverModuleFiles ?? {}).map(([file, artifactFile]) => [
953
- join(appDir, file),
954
- join(options.outDir, "server", safeManifestFilePath(artifactFile)),
955
- ]),
956
- );
957
- const serverModuleRequestFiles = new Map(
958
- Object.entries(serverManifest.serverModuleRequestFiles ?? {}).map(([file, artifactFile]) => [
959
- join(appDir, file),
960
- join(options.outDir, "server", safeManifestFilePath(artifactFile)),
961
- ]),
962
- );
963
- const serverModuleRenderFiles = new Map(
964
- Object.entries(serverManifest.serverModuleRenderFiles ?? {}).map(([file, artifactFile]) => [
965
- join(appDir, file),
966
- join(options.outDir, "server", safeManifestFilePath(artifactFile)),
967
- ]),
968
- );
969
- const serverSourceFiles = new Map(
970
- Object.entries(serverManifest.files).map(([file, source]) => [join(appDir, file), source]),
971
- );
972
- const serverActionReferencesByFile = new Map(
973
- Object.entries(serverManifest.routeServerActionReferences ?? {}).map(([file, references]) => [
974
- join(appDir, file),
975
- references,
976
- ]),
977
- );
978
- const routeMatcher = createRouteMatcher(routes, serverManifest.routeMatcher);
979
- const clientScripts = new Map(
980
- clientManifest.routes.flatMap((route) =>
981
- route.client && route.script !== undefined ? [[route.path, route.script]] : [],
982
- ),
983
- );
984
- const clientStyles = new Map(
985
- clientManifest.routes.flatMap((route) =>
986
- route.css !== undefined && route.css.length > 0 ? [[route.path, route.css]] : [],
987
- ),
988
- );
989
- const clientStylesByFile = new Map(
990
- (clientManifest.styles ?? []).flatMap((style) =>
991
- style.css !== undefined && style.css.length > 0
992
- ? [[join(routesDir, style.file), style.css] as const]
993
- : [],
994
- ),
995
- );
996
- const navigationScripts = new Map(
997
- clientManifest.routes.flatMap((route) =>
998
- route.navigation === true && route.navigationScript !== undefined
999
- ? [[route.path, route.navigationScript]]
1000
- : [],
1001
- ),
1002
- );
1003
- const hasMiddleware =
1004
- serverSourceFiles.has(join(routesDir, "middleware.ts")) ||
1005
- serverSourceFiles.has(join(routesDir, "middleware.mreact.ts"));
1006
- const serverModuleCacheVersion = createHash("sha256")
1007
- .update(options.serverManifestText)
1008
- .update("\0")
1009
- .update(options.clientManifestText)
1010
- .digest("hex")
1011
- .slice(0, 16);
1012
-
1013
- const allowedSourceDirs = (serverManifest.allowedSourceDirs ?? [""]).map((directory) =>
1014
- join(projectRoot, directory),
1015
- );
1016
- const generatedImportPolicy = builtGeneratedImportPolicy(
1017
- options.importPolicyText,
1018
- options.importPolicyPath,
1019
- );
1020
-
1021
- return {
1022
- appDir: routesDir,
1023
- allowedSourceDirs,
1024
- ...(serverManifest.assetBaseUrl === undefined
1025
- ? {}
1026
- : { assetBaseUrl: serverManifest.assetBaseUrl }),
1027
- clientAssetPaths: builtClientAssetPaths(clientManifest),
1028
- clientScripts,
1029
- clientStylesByFile,
1030
- clientStyles,
1031
- ...(generatedImportPolicy === undefined ? {} : { generatedImportPolicy }),
1032
- hasMiddleware,
1033
- navigationScripts,
1034
- projectRoot,
1035
- ...(serverManifest.publicAssetBaseUrl === undefined
1036
- ? {}
1037
- : { publicAssetBaseUrl: serverManifest.publicAssetBaseUrl }),
1038
- prerenderableRoutes,
1039
- prerenderLocks,
1040
- prerenderedRoutes,
1041
- routeMatcher,
1042
- routes,
1043
- serverActionReferencesByFile,
1044
- ...(serverManifest.serverActionManifest === undefined
1045
- ? {}
1046
- : { serverActionManifest: serverManifest.serverActionManifest }),
1047
- serverModuleArtifactLoads: new Map(),
1048
- serverModuleClosureFiles: new Map(),
1049
- serverModuleFiles,
1050
- serverModuleRenderFiles,
1051
- serverModuleRequestFiles,
1052
- serverModules,
1053
- serverModuleCacheVersion,
1054
- serverSourceFiles,
1055
- };
1056
- }
1057
-
1058
- async function readBuiltImportPolicyText(outDir: string): Promise<string | undefined> {
1059
- const policyPath = join(outDir, "server", "import-policy.json");
1060
-
1061
- try {
1062
- return await readFile(policyPath, "utf8");
1063
- } catch (error) {
1064
- if (isMissingFileError(error)) {
1065
- return undefined;
709
+ runtime.catch(() => {
710
+ if (builtRuntimeCache.get(cacheKey)?.runtime === runtime) {
711
+ builtRuntimeCache.delete(cacheKey);
1066
712
  }
713
+ });
1067
714
 
1068
- throw builtArtifactReadError("built app import policy", policyPath, error);
1069
- }
1070
- }
1071
-
1072
- function builtGeneratedImportPolicy(
1073
- importPolicyText: string | undefined,
1074
- importPolicyPath: string,
1075
- ): AppRouterImportPolicy | undefined {
1076
- if (importPolicyText === undefined) {
1077
- return undefined;
1078
- }
1079
-
1080
- const artifact = parseBuiltJsonArtifact<{
1081
- runtimePackages?: unknown;
1082
- }>(importPolicyText, importPolicyPath, "built app import policy");
1083
- const runtimePackages = Array.isArray(artifact.runtimePackages)
1084
- ? artifact.runtimePackages.filter((name): name is string => typeof name === "string")
1085
- : [];
1086
-
1087
- return runtimePackages.length === 0 ? undefined : { allowedPackages: runtimePackages };
1088
- }
1089
-
1090
- function mergeBuiltRuntimeImportPolicy(
1091
- runtime: BuiltRuntime,
1092
- importPolicy: AppRouterImportPolicy | undefined,
1093
- ): AppRouterImportPolicy | undefined {
1094
- const generatedImportPolicy = runtime.generatedImportPolicy;
1095
-
1096
- if (generatedImportPolicy === undefined) {
1097
- return importPolicy;
1098
- }
1099
-
1100
- const allowedPackages = [
1101
- ...new Set([
1102
- ...(generatedImportPolicy.allowedPackages ?? []),
1103
- ...(importPolicy?.allowedPackages ?? []),
1104
- ]),
1105
- ];
1106
-
1107
- return {
1108
- ...(allowedPackages.length === 0 ? {} : { allowedPackages }),
1109
- ...(importPolicy?.allowedSourceDirs === undefined
1110
- ? {}
1111
- : { allowedSourceDirs: importPolicy.allowedSourceDirs }),
1112
- ...(importPolicy?.projectRoot === undefined ? {} : { projectRoot: importPolicy.projectRoot }),
1113
- };
715
+ return runtime;
1114
716
  }
1115
717
 
1116
718
  function isMissingFileError(error: unknown): boolean {
@@ -1137,300 +739,6 @@ function builtArtifactReadError(label: string, artifactPath: string, error: unkn
1137
739
  return new Error(`${prefix} ${label}: ${artifactPath}${detail}`, { cause: error });
1138
740
  }
1139
741
 
1140
- function parseBuiltJsonArtifact<T>(text: string, artifactPath: string, label: string): T {
1141
- try {
1142
- return JSON.parse(text) as T;
1143
- } catch (error) {
1144
- const detail = error instanceof Error && error.message !== "" ? `: ${error.message}` : "";
1145
-
1146
- throw new Error(`Invalid ${label}: ${artifactPath}${detail}`, { cause: error });
1147
- }
1148
- }
1149
-
1150
- async function loadBuiltServerModuleArtifacts(
1151
- runtime: BuiltRuntime,
1152
- files: Iterable<string>,
1153
- kind: BuiltServerModuleArtifactKind = "all",
1154
- ): Promise<void> {
1155
- for (const file of files) {
1156
- await loadBuiltServerModuleArtifact(runtime, file, kind);
1157
- }
1158
- }
1159
-
1160
- type BuiltServerModuleArtifactKind = "all" | "render" | "request";
1161
-
1162
- async function loadBuiltServerModuleArtifact(
1163
- runtime: BuiltRuntime,
1164
- file: string,
1165
- kind: BuiltServerModuleArtifactKind = "all",
1166
- ): Promise<void> {
1167
- if (kind === "all") {
1168
- await loadBuiltServerModuleArtifact(runtime, file, "request");
1169
- await loadBuiltServerModuleArtifact(runtime, file, "render");
1170
- return;
1171
- }
1172
-
1173
- const artifactPath =
1174
- kind === "request"
1175
- ? runtime.serverModuleRequestFiles.get(file) ?? runtime.serverModuleFiles.get(file)
1176
- : runtime.serverModuleRenderFiles.get(file) ?? runtime.serverModuleFiles.get(file);
1177
-
1178
- if (artifactPath === undefined) {
1179
- return;
1180
- }
1181
-
1182
- const cached = runtime.serverModuleArtifactLoads.get(`${kind}\0${file}`);
1183
-
1184
- if (cached !== undefined) {
1185
- await cached;
1186
- return;
1187
- }
1188
-
1189
- const loaded = readFile(artifactPath, "utf8")
1190
- .then((text) => {
1191
- const existing = runtime.serverModules.get(file) ?? {};
1192
- runtime.serverModules.set(
1193
- file,
1194
- mergeBuiltServerModuleArtifacts(
1195
- existing,
1196
- hydrateBuiltServerModuleArtifact(
1197
- parseBuiltJsonArtifact<BuiltServerModuleArtifact>(
1198
- text,
1199
- artifactPath,
1200
- `built server module artifact for ${file}`,
1201
- ),
1202
- builtServerDirForArtifactPath(artifactPath),
1203
- ),
1204
- ),
1205
- );
1206
- })
1207
- .catch((error) => {
1208
- runtime.serverModuleArtifactLoads.delete(`${kind}\0${file}`);
1209
- if (isMissingFileError(error)) {
1210
- throw builtArtifactReadError(`built server module artifact for ${file}`, artifactPath, error);
1211
- }
1212
- throw error;
1213
- });
1214
- runtime.serverModuleArtifactLoads.set(`${kind}\0${file}`, loaded);
1215
-
1216
- await loaded;
1217
- }
1218
-
1219
- function allBuiltServerModuleFiles(runtime: BuiltRuntime): Iterable<string> {
1220
- return new Set([
1221
- ...runtime.serverModuleFiles.keys(),
1222
- ...runtime.serverModuleRequestFiles.keys(),
1223
- ...runtime.serverModuleRenderFiles.keys(),
1224
- ]);
1225
- }
1226
-
1227
- function builtServerDirForArtifactPath(artifactPath: string): string {
1228
- const marker = `${sep}server-modules${sep}`;
1229
- const index = artifactPath.lastIndexOf(marker);
1230
-
1231
- return index === -1 ? dirname(dirname(artifactPath)) : artifactPath.slice(0, index);
1232
- }
1233
-
1234
- function hydrateBuiltServerModuleArtifact(
1235
- artifact: BuiltServerModuleArtifact,
1236
- serverDir: string,
1237
- ): BuiltServerModuleArtifact {
1238
- return {
1239
- ...(artifact.analysis === undefined ? {} : { analysis: artifact.analysis }),
1240
- ...(artifact.loader === undefined
1241
- ? {}
1242
- : { loader: hydrateBuiltServerModuleOutput(artifact.loader, serverDir) }),
1243
- ...(artifact.routeMetadata === undefined
1244
- ? {}
1245
- : { routeMetadata: hydrateBuiltServerModuleOutput(artifact.routeMetadata, serverDir) }),
1246
- ...(artifact.request === undefined
1247
- ? {}
1248
- : { request: hydrateBuiltServerModuleOutput(artifact.request, serverDir) }),
1249
- ...(artifact.stream === undefined
1250
- ? {}
1251
- : { stream: hydrateBuiltServerModuleOutput(artifact.stream, serverDir) }),
1252
- ...(artifact.string === undefined
1253
- ? {}
1254
- : { string: hydrateBuiltServerModuleOutput(artifact.string, serverDir) }),
1255
- };
1256
- }
1257
-
1258
- function hydrateBuiltServerModuleOutput<T extends { moduleFile?: string | undefined }>(
1259
- output: T,
1260
- serverDir: string,
1261
- ): T {
1262
- if (output.moduleFile === undefined || isAbsolute(output.moduleFile)) {
1263
- return output;
1264
- }
1265
-
1266
- return {
1267
- ...output,
1268
- moduleFile: join(serverDir, safeManifestFilePath(output.moduleFile)),
1269
- };
1270
- }
1271
-
1272
- function mergeBuiltServerModuleArtifacts(
1273
- existing: BuiltServerModuleArtifact,
1274
- loaded: BuiltServerModuleArtifact,
1275
- ): BuiltServerModuleArtifact {
1276
- return {
1277
- ...existing,
1278
- ...loaded,
1279
- };
1280
- }
1281
-
1282
- async function loadBuiltServerModuleArtifactsForRequest(
1283
- runtime: BuiltRuntime,
1284
- routeFile: string | undefined,
1285
- options: {
1286
- includeRender?: boolean | undefined;
1287
- includeShells?: boolean | undefined;
1288
- } = {},
1289
- ): Promise<void> {
1290
- const roots = [
1291
- join(runtime.appDir, "middleware.ts"),
1292
- join(runtime.appDir, "middleware.mreact.ts"),
1293
- ...(routeFile === undefined
1294
- ? []
1295
- : [
1296
- routeFile,
1297
- ...(options.includeShells === false ? [] : shellFilesForRoute(runtime, routeFile)),
1298
- ]),
1299
- ];
1300
- const seen = new Set<string>();
1301
-
1302
- for (const file of roots) {
1303
- await loadBuiltServerModuleArtifactClosure(runtime, file, seen, "request");
1304
- }
1305
-
1306
- if (options.includeRender === true) {
1307
- seen.clear();
1308
- for (const file of roots) {
1309
- await loadBuiltServerModuleArtifactClosure(runtime, file, seen, "render");
1310
- }
1311
- }
1312
- }
1313
-
1314
- async function loadBuiltServerModuleArtifactClosure(
1315
- runtime: BuiltRuntime,
1316
- file: string,
1317
- seen: Set<string>,
1318
- kind: BuiltServerModuleArtifactKind,
1319
- ): Promise<void> {
1320
- for (const closureFile of builtServerModuleClosureFiles(runtime, file)) {
1321
- if (seen.has(closureFile)) {
1322
- continue;
1323
- }
1324
- seen.add(closureFile);
1325
- await loadBuiltServerModuleArtifact(runtime, closureFile, kind);
1326
- }
1327
- }
1328
-
1329
- function builtServerModuleClosureFiles(
1330
- runtime: BuiltRuntime,
1331
- file: string,
1332
- ): readonly string[] {
1333
- const cached = runtime.serverModuleClosureFiles.get(file);
1334
- if (cached !== undefined) {
1335
- return cached;
1336
- }
1337
-
1338
- const closure: string[] = [];
1339
- collectBuiltServerModuleClosureFiles(runtime, file, new Set(), closure);
1340
- runtime.serverModuleClosureFiles.set(file, closure);
1341
- return closure;
1342
- }
1343
-
1344
- function collectBuiltServerModuleClosureFiles(
1345
- runtime: BuiltRuntime,
1346
- file: string,
1347
- seen: Set<string>,
1348
- closure: string[],
1349
- ): void {
1350
- if (seen.has(file)) {
1351
- return;
1352
- }
1353
- seen.add(file);
1354
- closure.push(file);
1355
-
1356
- const source = runtime.serverSourceFiles.get(file);
1357
- if (source === undefined) {
1358
- return;
1359
- }
1360
-
1361
- for (const specifier of localServerModuleSpecifiers(source)) {
1362
- const resolved = resolveBuiltLocalServerSourceImport(runtime, file, specifier);
1363
- if (resolved !== undefined) {
1364
- collectBuiltServerModuleClosureFiles(runtime, resolved, seen, closure);
1365
- }
1366
- }
1367
- }
1368
-
1369
- const localServerModuleImportPattern =
1370
- /\b(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s*)?["'](?<source>\.{1,2}\/[^"']+)["']/g;
1371
-
1372
- function localServerModuleSpecifiers(code: string): string[] {
1373
- const specifiers = new Set<string>();
1374
- localServerModuleImportPattern.lastIndex = 0;
1375
-
1376
- for (const match of code.matchAll(localServerModuleImportPattern)) {
1377
- const source = match.groups?.source;
1378
-
1379
- if (source !== undefined) {
1380
- specifiers.add(source);
1381
- }
1382
- }
1383
-
1384
- return Array.from(specifiers);
1385
- }
1386
-
1387
- function resolveBuiltLocalServerSourceImport(
1388
- runtime: BuiltRuntime,
1389
- fromFile: string,
1390
- specifier: string,
1391
- ): string | undefined {
1392
- const base = join(dirname(fromFile), specifier);
1393
-
1394
- for (const candidate of localServerSourceImportCandidates(base)) {
1395
- if (runtime.serverSourceFiles.has(candidate)) {
1396
- return candidate;
1397
- }
1398
- }
1399
-
1400
- return undefined;
1401
- }
1402
-
1403
- function localServerSourceImportCandidates(base: string): string[] {
1404
- const candidates = [base];
1405
-
1406
- if (base.endsWith(".js")) {
1407
- const withoutJs = base.slice(0, -".js".length);
1408
- candidates.push(`${withoutJs}.ts`, `${withoutJs}.tsx`, `${withoutJs}.mreact.tsx`);
1409
- } else if (base.endsWith(".jsx")) {
1410
- const withoutJsx = base.slice(0, -".jsx".length);
1411
- candidates.push(`${withoutJsx}.tsx`, `${withoutJsx}.mreact.tsx`);
1412
- } else if (base.endsWith(".mreact")) {
1413
- candidates.push(`${base}.tsx`);
1414
- } else {
1415
- candidates.push(
1416
- `${base}.ts`,
1417
- `${base}.tsx`,
1418
- `${base}.mreact.tsx`,
1419
- join(base, "index.ts"),
1420
- join(base, "index.tsx"),
1421
- join(base, "index.mreact.tsx"),
1422
- );
1423
- }
1424
-
1425
- return candidates;
1426
- }
1427
-
1428
- function shellFilesForRoute(runtime: BuiltRuntime, routeFile: string): string[] {
1429
- return routeShellCandidates(runtime.appDir, routeFile)
1430
- .map((candidate) => candidate.file)
1431
- .filter((file) => runtime.serverSourceFiles.has(file));
1432
- }
1433
-
1434
742
  async function readPrerenderedRoute(
1435
743
  runtime: BuiltRuntime,
1436
744
  path: string,
@@ -1468,10 +776,15 @@ function builtRenderAppRequestOptions(
1468
776
  requestUrl?: URL | undefined;
1469
777
  runtime: BuiltRuntime;
1470
778
  },
1471
- ): RenderAppRequestOptions {
1472
- const renderOptions: RenderAppRequestOptions & {
1473
- __mreactLoadServerRenderArtifacts?: ((routeFile: string) => Promise<void>) | undefined;
1474
- } = {
779
+ ): RenderAppRequestRuntimeOptions {
780
+ const serverRenderArtifactLoader: AppRouterServerRenderArtifactLoader = {
781
+ async load(routeFile: string) {
782
+ await loadBuiltServerModuleArtifactsForRequest(options.runtime, routeFile, {
783
+ includeRender: true,
784
+ });
785
+ },
786
+ };
787
+ const renderOptions: RenderAppRequestRuntimeOptions = {
1475
788
  appDir: options.runtime.appDir,
1476
789
  assetBaseUrl: options.runtime.assetBaseUrl,
1477
790
  clientScripts: options.runtime.clientScripts,
@@ -1491,11 +804,7 @@ function builtRenderAppRequestOptions(
1491
804
  routeMatcher: options.runtime.routeMatcher,
1492
805
  requestUrl: options.requestUrl,
1493
806
  routes: options.runtime.routes,
1494
- __mreactLoadServerRenderArtifacts: async (routeFile: string) => {
1495
- await loadBuiltServerModuleArtifactsForRequest(options.runtime, routeFile, {
1496
- includeRender: true,
1497
- });
1498
- },
807
+ serverRenderArtifactLoader,
1499
808
  serverModules: options.runtime.serverModules,
1500
809
  serverModuleCacheVersion: options.runtime.serverModuleCacheVersion,
1501
810
  serverSourceFiles: options.runtime.serverSourceFiles,
@@ -1626,70 +935,3 @@ async function cloneResponse(response: Response): Promise<Response> {
1626
935
  status: response.status,
1627
936
  });
1628
937
  }
1629
-
1630
- async function materializeBuiltServerApp(
1631
- runtimeDir: string,
1632
- manifest: BuiltServerManifest,
1633
- ): Promise<string> {
1634
- const appDir = join(runtimeDir, "app");
1635
-
1636
- await rm(appDir, { force: true, recursive: true });
1637
- await Promise.all(
1638
- Object.entries(manifest.files).map(async ([file, code]) => {
1639
- const outputFile = join(appDir, safeManifestFilePath(file));
1640
-
1641
- await mkdir(dirname(outputFile), { recursive: true });
1642
- await writeFile(outputFile, code);
1643
- }),
1644
- );
1645
-
1646
- return appDir;
1647
- }
1648
-
1649
- function safeManifestFilePath(pathname: string): string {
1650
- const normalized = normalize(pathname);
1651
-
1652
- if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
1653
- throw new Error(`Invalid built app manifest file path: ${pathname}`);
1654
- }
1655
-
1656
- return normalized;
1657
- }
1658
-
1659
- function clientAssetHeaders(pathname: string): HeadersInit {
1660
- if (pathname === "manifest.json") {
1661
- return {
1662
- "cache-control": "no-cache",
1663
- "content-type": "application/json; charset=utf-8",
1664
- };
1665
- }
1666
-
1667
- return {
1668
- "cache-control": "public, max-age=31536000, immutable",
1669
- "content-type": pathname.endsWith(".css")
1670
- ? "text/css; charset=utf-8"
1671
- : "text/javascript; charset=utf-8",
1672
- };
1673
- }
1674
-
1675
- function publicAssetHeaders(pathname: string): HeadersInit {
1676
- return {
1677
- "cache-control": "public, max-age=3600",
1678
- "content-type": publicAssetContentType(pathname),
1679
- };
1680
- }
1681
-
1682
- function publicAssetContentType(pathname: string): string {
1683
- if (pathname.endsWith(".css")) return "text/css; charset=utf-8";
1684
- if (pathname.endsWith(".html")) return "text/html; charset=utf-8";
1685
- if (pathname.endsWith(".js")) return "text/javascript; charset=utf-8";
1686
- if (pathname.endsWith(".json")) return "application/json; charset=utf-8";
1687
- if (pathname.endsWith(".svg")) return "image/svg+xml";
1688
- if (pathname.endsWith(".png")) return "image/png";
1689
- if (pathname.endsWith(".jpg") || pathname.endsWith(".jpeg")) return "image/jpeg";
1690
- if (pathname.endsWith(".webp")) return "image/webp";
1691
- if (pathname.endsWith(".ico")) return "image/x-icon";
1692
- if (pathname.endsWith(".txt")) return "text/plain; charset=utf-8";
1693
-
1694
- return "application/octet-stream";
1695
- }