@reckona/mreact-router 0.0.174 → 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/build.ts CHANGED
@@ -279,6 +279,7 @@ export interface BuiltServerManifest {
279
279
  routesDir?: string;
280
280
  routeServerActionReferences?: Record<string, BuiltServerActionExpressionReference[]>;
281
281
  serverActionManifest?: BuiltServerActionReference[];
282
+ serverModuleClosureFiles?: Record<string, string[]>;
282
283
  serverModuleFiles?: Record<string, string>;
283
284
  serverModuleRenderFiles?: Record<string, string>;
284
285
  serverModuleRequestFiles?: Record<string, string>;
@@ -625,6 +626,10 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
625
626
  ...(navigationRuntimeScript === undefined ? [] : [navigationRuntimeScript]),
626
627
  ]),
627
628
  ).sort();
629
+ const serverModuleClosureFiles = buildServerModuleClosureManifest(
630
+ serverModules,
631
+ sourceAnalysis,
632
+ );
628
633
  const prerenderedRoutes =
629
634
  shouldTrackBuildPhases === false
630
635
  ? await prerenderStaticRoutes({
@@ -708,6 +713,9 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
708
713
  ...(serverActionManifest.allowedActions.length === 0
709
714
  ? {}
710
715
  : { serverActionManifest: serverActionManifest.allowedActions }),
716
+ ...(Object.keys(serverModuleClosureFiles).length === 0
717
+ ? {}
718
+ : { serverModuleClosureFiles }),
711
719
  ...(Object.keys(serverModuleArtifacts.files).length === 0
712
720
  ? {}
713
721
  : { serverModuleFiles: serverModuleArtifacts.files }),
@@ -988,6 +996,141 @@ function buildSourceAnalysisForFile(
988
996
  return sourceAnalysis.byFile.get(relative(projectRoot, file).split(sep).join("/"));
989
997
  }
990
998
 
999
+ function buildServerModuleClosureManifest(
1000
+ serverModules: Record<string, BuiltServerModuleArtifact>,
1001
+ sourceAnalysis: BuildSourceAnalysisScope,
1002
+ ): Record<string, string[]> {
1003
+ const sourceFiles = new Set(sourceAnalysis.byFile.keys());
1004
+ const closureFiles: Record<string, string[]> = {};
1005
+
1006
+ for (const file of Object.keys(serverModules).sort()) {
1007
+ const closure: string[] = [];
1008
+ collectManifestServerModuleClosureFiles(file, sourceAnalysis, sourceFiles, new Set(), closure);
1009
+
1010
+ if (closure.length > 0) {
1011
+ closureFiles[file] = closure;
1012
+ }
1013
+ }
1014
+
1015
+ return closureFiles;
1016
+ }
1017
+
1018
+ function collectManifestServerModuleClosureFiles(
1019
+ file: string,
1020
+ sourceAnalysis: BuildSourceAnalysisScope,
1021
+ sourceFiles: ReadonlySet<string>,
1022
+ seen: Set<string>,
1023
+ closure: string[],
1024
+ ): void {
1025
+ if (seen.has(file)) {
1026
+ return;
1027
+ }
1028
+ seen.add(file);
1029
+ closure.push(file);
1030
+
1031
+ const source = sourceAnalysis.byFile.get(file)?.source;
1032
+ if (source === undefined) {
1033
+ return;
1034
+ }
1035
+
1036
+ for (const specifier of localManifestServerModuleSpecifiers(source)) {
1037
+ const resolved = resolveManifestLocalServerSourceImport(file, specifier, sourceFiles);
1038
+ if (resolved !== undefined) {
1039
+ collectManifestServerModuleClosureFiles(
1040
+ resolved,
1041
+ sourceAnalysis,
1042
+ sourceFiles,
1043
+ seen,
1044
+ closure,
1045
+ );
1046
+ }
1047
+ }
1048
+ }
1049
+
1050
+ const localManifestServerModuleImportPattern =
1051
+ /\b(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s*)?["'](?<source>\.{1,2}\/[^"']+)["']/g;
1052
+
1053
+ function localManifestServerModuleSpecifiers(code: string): string[] {
1054
+ const specifiers = new Set<string>();
1055
+ localManifestServerModuleImportPattern.lastIndex = 0;
1056
+
1057
+ for (const match of code.matchAll(localManifestServerModuleImportPattern)) {
1058
+ const source = match.groups?.source;
1059
+
1060
+ if (source !== undefined) {
1061
+ specifiers.add(source);
1062
+ }
1063
+ }
1064
+
1065
+ return Array.from(specifiers);
1066
+ }
1067
+
1068
+ function resolveManifestLocalServerSourceImport(
1069
+ fromFile: string,
1070
+ specifier: string,
1071
+ sourceFiles: ReadonlySet<string>,
1072
+ ): string | undefined {
1073
+ const base = resolveManifestRelativePath(manifestDirname(fromFile), specifier);
1074
+
1075
+ for (const candidate of manifestLocalServerSourceImportCandidates(base)) {
1076
+ if (sourceFiles.has(candidate)) {
1077
+ return candidate;
1078
+ }
1079
+ }
1080
+
1081
+ return undefined;
1082
+ }
1083
+
1084
+ function manifestLocalServerSourceImportCandidates(base: string): string[] {
1085
+ const candidates = [base];
1086
+
1087
+ if (base.endsWith(".js")) {
1088
+ const withoutJs = base.slice(0, -".js".length);
1089
+ candidates.push(`${withoutJs}.ts`, `${withoutJs}.tsx`, `${withoutJs}.mreact.tsx`);
1090
+ } else if (base.endsWith(".jsx")) {
1091
+ const withoutJsx = base.slice(0, -".jsx".length);
1092
+ candidates.push(`${withoutJsx}.tsx`, `${withoutJsx}.mreact.tsx`);
1093
+ } else if (base.endsWith(".mreact")) {
1094
+ candidates.push(`${base}.tsx`);
1095
+ } else {
1096
+ candidates.push(
1097
+ `${base}.ts`,
1098
+ `${base}.tsx`,
1099
+ `${base}.mreact.tsx`,
1100
+ `${base}/index.ts`,
1101
+ `${base}/index.tsx`,
1102
+ `${base}/index.mreact.tsx`,
1103
+ );
1104
+ }
1105
+
1106
+ return candidates;
1107
+ }
1108
+
1109
+ function manifestDirname(file: string): string {
1110
+ const index = file.lastIndexOf("/");
1111
+
1112
+ return index === -1 ? "" : file.slice(0, index);
1113
+ }
1114
+
1115
+ function resolveManifestRelativePath(fromDir: string, specifier: string): string {
1116
+ const segments = fromDir === "" ? [] : fromDir.split("/");
1117
+
1118
+ for (const segment of specifier.split("/")) {
1119
+ if (segment === "" || segment === ".") {
1120
+ continue;
1121
+ }
1122
+
1123
+ if (segment === "..") {
1124
+ segments.pop();
1125
+ continue;
1126
+ }
1127
+
1128
+ segments.push(segment);
1129
+ }
1130
+
1131
+ return segments.join("/");
1132
+ }
1133
+
991
1134
  function canUseBuildServerActionPlaceholders(
992
1135
  references: readonly BuiltServerActionExpressionReference[],
993
1136
  ): boolean {
@@ -0,0 +1,164 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { isAbsolute, join, normalize, sep } from "node:path";
3
+ import type { ClientRouteManifestEntry } from "./client-route-inference.js";
4
+ import { clientManifestAssetPaths } from "./client-manifest-assets.js";
5
+ import { bytesResponse, rawNodeRequestUrl } from "./http.js";
6
+
7
+ const builtPublicAssetCache = new Map<
8
+ string,
9
+ {
10
+ bytes: Uint8Array;
11
+ headers: HeadersInit;
12
+ }
13
+ >();
14
+
15
+ export async function readBuiltClientAsset(
16
+ outDir: string,
17
+ pathname: string,
18
+ allowedPaths: ReadonlySet<string>,
19
+ ): Promise<Response> {
20
+ const clientPrefix = "/_mreact/client/";
21
+ const normalized = safeBuiltClientAssetPath(pathname.slice(clientPrefix.length));
22
+
23
+ if (normalized === undefined || !allowedPaths.has(normalized)) {
24
+ return new Response("Not Found", { status: 404 });
25
+ }
26
+
27
+ try {
28
+ const code = await readFile(join(outDir, "client", normalized), "utf8");
29
+
30
+ return new Response(code, {
31
+ headers: clientAssetHeaders(normalized),
32
+ });
33
+ } catch {
34
+ return new Response("Not Found", { status: 404 });
35
+ }
36
+ }
37
+
38
+ export function builtClientAssetPathname(request: Request, url: URL): string | undefined {
39
+ const rawUrl = rawNodeRequestUrl(request);
40
+ const rawPathname = rawUrl?.split(/[?#]/, 1)[0];
41
+ const pathname = rawPathname === undefined || rawPathname === "" ? url.pathname : rawPathname;
42
+ const normalizedPathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
43
+
44
+ return normalizedPathname.startsWith("/_mreact/client/") ? normalizedPathname : undefined;
45
+ }
46
+
47
+ function safeBuiltClientAssetPath(relativePath: string): string | undefined {
48
+ let decoded: string;
49
+
50
+ try {
51
+ decoded = decodeURIComponent(relativePath);
52
+ } catch {
53
+ return undefined;
54
+ }
55
+
56
+ if (decoded.includes("\\") || decoded.includes("\0")) {
57
+ return undefined;
58
+ }
59
+
60
+ if (decoded.split("/").some((segment) => segment === "..")) {
61
+ return undefined;
62
+ }
63
+
64
+ const normalized = normalize(decoded);
65
+
66
+ if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${sep}`)) {
67
+ return undefined;
68
+ }
69
+
70
+ return normalized;
71
+ }
72
+
73
+ export function builtClientAssetPaths(manifest: {
74
+ assets?: readonly string[] | undefined;
75
+ routes: readonly ClientRouteManifestEntry[];
76
+ }): ReadonlySet<string> {
77
+ return clientManifestAssetPaths(manifest);
78
+ }
79
+
80
+ export async function readBuiltPublicAsset(
81
+ outDir: string,
82
+ pathname: string,
83
+ ): Promise<Response | undefined> {
84
+ const relativePath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
85
+
86
+ if (relativePath === "") {
87
+ return undefined;
88
+ }
89
+
90
+ const normalized = normalize(relativePath);
91
+
92
+ if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
93
+ return undefined;
94
+ }
95
+
96
+ try {
97
+ const cacheKey = `${outDir}\0${normalized}`;
98
+ const cached = builtPublicAssetCache.get(cacheKey);
99
+
100
+ if (cached !== undefined) {
101
+ return bytesResponse(cached.bytes, {
102
+ headers: cached.headers,
103
+ });
104
+ }
105
+
106
+ const bytes = await readFile(join(outDir, "client", "public", normalized));
107
+ const headers = publicAssetHeaders(normalized);
108
+
109
+ builtPublicAssetCache.set(cacheKey, {
110
+ bytes,
111
+ headers,
112
+ });
113
+
114
+ return bytesResponse(bytes, { headers });
115
+ } catch {
116
+ return undefined;
117
+ }
118
+ }
119
+
120
+ export function clearBuiltPublicAssetCacheForTest(): void {
121
+ builtPublicAssetCache.clear();
122
+ }
123
+
124
+ export function getBuiltPublicAssetCacheSizeForTest(): number {
125
+ return builtPublicAssetCache.size;
126
+ }
127
+
128
+ function clientAssetHeaders(pathname: string): HeadersInit {
129
+ if (pathname === "manifest.json") {
130
+ return {
131
+ "cache-control": "no-cache",
132
+ "content-type": "application/json; charset=utf-8",
133
+ };
134
+ }
135
+
136
+ return {
137
+ "cache-control": "public, max-age=31536000, immutable",
138
+ "content-type": pathname.endsWith(".css")
139
+ ? "text/css; charset=utf-8"
140
+ : "text/javascript; charset=utf-8",
141
+ };
142
+ }
143
+
144
+ function publicAssetHeaders(pathname: string): HeadersInit {
145
+ return {
146
+ "cache-control": "public, max-age=3600",
147
+ "content-type": publicAssetContentType(pathname),
148
+ };
149
+ }
150
+
151
+ function publicAssetContentType(pathname: string): string {
152
+ if (pathname.endsWith(".css")) return "text/css; charset=utf-8";
153
+ if (pathname.endsWith(".html")) return "text/html; charset=utf-8";
154
+ if (pathname.endsWith(".js")) return "text/javascript; charset=utf-8";
155
+ if (pathname.endsWith(".json")) return "application/json; charset=utf-8";
156
+ if (pathname.endsWith(".svg")) return "image/svg+xml";
157
+ if (pathname.endsWith(".png")) return "image/png";
158
+ if (pathname.endsWith(".jpg") || pathname.endsWith(".jpeg")) return "image/jpeg";
159
+ if (pathname.endsWith(".webp")) return "image/webp";
160
+ if (pathname.endsWith(".ico")) return "image/x-icon";
161
+ if (pathname.endsWith(".txt")) return "text/plain; charset=utf-8";
162
+
163
+ return "application/octet-stream";
164
+ }
@@ -0,0 +1,312 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, normalize } from "node:path";
4
+ import type {
5
+ BuiltPrerenderedRoute,
6
+ BuiltServerManifest,
7
+ BuiltServerModuleArtifact,
8
+ } from "./build.js";
9
+ import { builtClientAssetPaths } from "./built-assets.js";
10
+ import type { BuiltServerModuleArtifactRuntime } from "./built-server-module-artifacts.js";
11
+ import type { ClientRouteManifestEntry } from "./client-route-inference.js";
12
+ import type { AppRouterImportPolicy } from "./import-policy.js";
13
+ import { createRouteMatcher, type AppRoute, type RouteMatcher } from "./routes.js";
14
+
15
+ export interface BuiltRuntime extends BuiltServerModuleArtifactRuntime {
16
+ allowedSourceDirs: readonly string[];
17
+ assetBaseUrl?: string | undefined;
18
+ clientAssetPaths: ReadonlySet<string>;
19
+ clientScripts: ReadonlyMap<string, string>;
20
+ clientStylesByFile: ReadonlyMap<string, readonly string[]>;
21
+ clientStyles: ReadonlyMap<string, readonly string[]>;
22
+ generatedImportPolicy?: AppRouterImportPolicy | undefined;
23
+ hasMiddleware: boolean;
24
+ navigationScripts: ReadonlyMap<string, string>;
25
+ projectRoot: string;
26
+ publicAssetBaseUrl?: string | undefined;
27
+ prerenderableRoutes: ReadonlySet<string>;
28
+ prerenderLocks: Map<string, Promise<Response>>;
29
+ prerenderedRoutes: Map<string, BuiltPrerenderedRoute>;
30
+ routeMatcher: RouteMatcher;
31
+ routes: readonly AppRoute[];
32
+ serverActionReferencesByFile: ReadonlyMap<
33
+ string,
34
+ readonly {
35
+ end: number;
36
+ expression: string;
37
+ expressionEnd: number;
38
+ expressionStart: number;
39
+ moduleId: string;
40
+ exportName: string;
41
+ inferred: boolean;
42
+ sourceHash: string;
43
+ start: number;
44
+ }[]
45
+ >;
46
+ serverActionManifest?: readonly { moduleId: string; exportName: string; inferred?: boolean }[] | undefined;
47
+ serverModuleCacheVersion: string;
48
+ }
49
+
50
+ export async function materializeBuiltRuntime(options: {
51
+ clientManifestText: string;
52
+ clientManifestPath: string;
53
+ importPolicyPath: string;
54
+ importPolicyText: string | undefined;
55
+ onMaterialize?: (() => void) | undefined;
56
+ outDir: string;
57
+ runtimeDir: string;
58
+ serverManifestText: string;
59
+ serverManifestPath: string;
60
+ }): Promise<BuiltRuntime> {
61
+ options.onMaterialize?.();
62
+ const serverManifest = parseBuiltJsonArtifact<BuiltServerManifest>(
63
+ options.serverManifestText,
64
+ options.serverManifestPath,
65
+ "built app server manifest",
66
+ );
67
+ const clientManifest = parseBuiltJsonArtifact<{
68
+ assets?: readonly string[];
69
+ routes: ClientRouteManifestEntry[];
70
+ styles?: Array<{ css?: readonly string[]; file: string }>;
71
+ }>(options.clientManifestText, options.clientManifestPath, "built app client manifest");
72
+ const appDir = await materializeBuiltServerApp(options.runtimeDir, serverManifest);
73
+ const projectRoot = appDir;
74
+ const routesDir = join(projectRoot, serverManifest.routesDir ?? "");
75
+ const routes = serverManifest.routes.map((route) => ({
76
+ ...route,
77
+ file: join(projectRoot, route.file),
78
+ }));
79
+ const prerenderedRoutes = new Map(Object.entries(serverManifest.prerenderedRoutes ?? {}));
80
+ const prerenderableRoutes = new Set(prerenderedRoutes.keys());
81
+ const prerenderLocks = new Map<string, Promise<Response>>();
82
+ const serverModules = new Map<string, BuiltServerModuleArtifact>(
83
+ Object.entries(serverManifest.serverModules ?? {}).map(([file, artifact]) => [
84
+ join(appDir, file),
85
+ artifact,
86
+ ]),
87
+ );
88
+ const serverModuleClosureFiles = new Map<string, readonly string[]>(
89
+ Object.entries(serverManifest.serverModuleClosureFiles ?? {}).map(([file, closure]) => [
90
+ join(appDir, safeManifestFilePath(file)),
91
+ closure.map((closureFile) => join(appDir, safeManifestFilePath(closureFile))),
92
+ ]),
93
+ );
94
+ const serverModuleFiles = new Map(
95
+ Object.entries(serverManifest.serverModuleFiles ?? {}).map(([file, artifactFile]) => [
96
+ join(appDir, file),
97
+ join(options.outDir, "server", safeManifestFilePath(artifactFile)),
98
+ ]),
99
+ );
100
+ const serverModuleRequestFiles = new Map(
101
+ Object.entries(serverManifest.serverModuleRequestFiles ?? {}).map(([file, artifactFile]) => [
102
+ join(appDir, file),
103
+ join(options.outDir, "server", safeManifestFilePath(artifactFile)),
104
+ ]),
105
+ );
106
+ const serverModuleRenderFiles = new Map(
107
+ Object.entries(serverManifest.serverModuleRenderFiles ?? {}).map(([file, artifactFile]) => [
108
+ join(appDir, file),
109
+ join(options.outDir, "server", safeManifestFilePath(artifactFile)),
110
+ ]),
111
+ );
112
+ const serverSourceFiles = new Map(
113
+ Object.entries(serverManifest.files).map(([file, source]) => [join(appDir, file), source]),
114
+ );
115
+ const serverActionReferencesByFile = new Map(
116
+ Object.entries(serverManifest.routeServerActionReferences ?? {}).map(([file, references]) => [
117
+ join(appDir, file),
118
+ references,
119
+ ]),
120
+ );
121
+ const routeMatcher = createRouteMatcher(routes, serverManifest.routeMatcher);
122
+ const clientScripts = new Map(
123
+ clientManifest.routes.flatMap((route) =>
124
+ route.client && route.script !== undefined ? [[route.path, route.script]] : [],
125
+ ),
126
+ );
127
+ const clientStyles = new Map(
128
+ clientManifest.routes.flatMap((route) =>
129
+ route.css !== undefined && route.css.length > 0 ? [[route.path, route.css]] : [],
130
+ ),
131
+ );
132
+ const clientStylesByFile = new Map(
133
+ (clientManifest.styles ?? []).flatMap((style) =>
134
+ style.css !== undefined && style.css.length > 0
135
+ ? [[join(routesDir, style.file), style.css] as const]
136
+ : [],
137
+ ),
138
+ );
139
+ const navigationScripts = new Map(
140
+ clientManifest.routes.flatMap((route) =>
141
+ route.navigation === true && route.navigationScript !== undefined
142
+ ? [[route.path, route.navigationScript]]
143
+ : [],
144
+ ),
145
+ );
146
+ const hasMiddleware =
147
+ serverSourceFiles.has(join(routesDir, "middleware.ts")) ||
148
+ serverSourceFiles.has(join(routesDir, "middleware.mreact.ts"));
149
+ const serverModuleCacheVersion = createHash("sha256")
150
+ .update(options.serverManifestText)
151
+ .update("\0")
152
+ .update(options.clientManifestText)
153
+ .digest("hex")
154
+ .slice(0, 16);
155
+
156
+ const allowedSourceDirs = (serverManifest.allowedSourceDirs ?? [""]).map((directory) =>
157
+ join(projectRoot, directory),
158
+ );
159
+ const generatedImportPolicy = builtGeneratedImportPolicy(
160
+ options.importPolicyText,
161
+ options.importPolicyPath,
162
+ );
163
+
164
+ return {
165
+ appDir: routesDir,
166
+ allowedSourceDirs,
167
+ ...(serverManifest.assetBaseUrl === undefined
168
+ ? {}
169
+ : { assetBaseUrl: serverManifest.assetBaseUrl }),
170
+ clientAssetPaths: builtClientAssetPaths(clientManifest),
171
+ clientScripts,
172
+ clientStylesByFile,
173
+ clientStyles,
174
+ ...(generatedImportPolicy === undefined ? {} : { generatedImportPolicy }),
175
+ hasMiddleware,
176
+ navigationScripts,
177
+ projectRoot,
178
+ ...(serverManifest.publicAssetBaseUrl === undefined
179
+ ? {}
180
+ : { publicAssetBaseUrl: serverManifest.publicAssetBaseUrl }),
181
+ prerenderableRoutes,
182
+ prerenderLocks,
183
+ prerenderedRoutes,
184
+ routeMatcher,
185
+ routes,
186
+ serverActionReferencesByFile,
187
+ ...(serverManifest.serverActionManifest === undefined
188
+ ? {}
189
+ : { serverActionManifest: serverManifest.serverActionManifest }),
190
+ serverModuleArtifactLoads: new Map(),
191
+ serverModuleClosureFiles,
192
+ serverModuleFiles,
193
+ serverModuleRenderFiles,
194
+ serverModuleRequestFiles,
195
+ serverModules,
196
+ serverModuleCacheVersion,
197
+ serverSourceFiles,
198
+ };
199
+ }
200
+
201
+ export async function readBuiltImportPolicyText(outDir: string): Promise<string | undefined> {
202
+ const policyPath = join(outDir, "server", "import-policy.json");
203
+
204
+ try {
205
+ return await readFile(policyPath, "utf8");
206
+ } catch (error) {
207
+ if (isMissingFileError(error)) {
208
+ return undefined;
209
+ }
210
+
211
+ throw builtArtifactReadError("built app import policy", policyPath, error);
212
+ }
213
+ }
214
+
215
+ export function mergeBuiltRuntimeImportPolicy(
216
+ runtime: BuiltRuntime,
217
+ importPolicy: AppRouterImportPolicy | undefined,
218
+ ): AppRouterImportPolicy | undefined {
219
+ const generatedImportPolicy = runtime.generatedImportPolicy;
220
+
221
+ if (generatedImportPolicy === undefined) {
222
+ return importPolicy;
223
+ }
224
+
225
+ const allowedPackages = [
226
+ ...new Set([
227
+ ...(generatedImportPolicy.allowedPackages ?? []),
228
+ ...(importPolicy?.allowedPackages ?? []),
229
+ ]),
230
+ ];
231
+
232
+ return {
233
+ ...(allowedPackages.length === 0 ? {} : { allowedPackages }),
234
+ ...(importPolicy?.allowedSourceDirs === undefined
235
+ ? {}
236
+ : { allowedSourceDirs: importPolicy.allowedSourceDirs }),
237
+ ...(importPolicy?.projectRoot === undefined ? {} : { projectRoot: importPolicy.projectRoot }),
238
+ };
239
+ }
240
+
241
+ function builtGeneratedImportPolicy(
242
+ importPolicyText: string | undefined,
243
+ importPolicyPath: string,
244
+ ): AppRouterImportPolicy | undefined {
245
+ if (importPolicyText === undefined) {
246
+ return undefined;
247
+ }
248
+
249
+ const artifact = parseBuiltJsonArtifact<{
250
+ runtimePackages?: unknown;
251
+ }>(importPolicyText, importPolicyPath, "built app import policy");
252
+ const runtimePackages = Array.isArray(artifact.runtimePackages)
253
+ ? artifact.runtimePackages.filter((name): name is string => typeof name === "string")
254
+ : [];
255
+
256
+ return runtimePackages.length === 0 ? undefined : { allowedPackages: runtimePackages };
257
+ }
258
+
259
+ async function materializeBuiltServerApp(
260
+ runtimeDir: string,
261
+ manifest: BuiltServerManifest,
262
+ ): Promise<string> {
263
+ const appDir = join(runtimeDir, "app");
264
+
265
+ await rm(appDir, { force: true, recursive: true });
266
+ await Promise.all(
267
+ Object.entries(manifest.files).map(async ([file, code]) => {
268
+ const outputFile = join(appDir, safeManifestFilePath(file));
269
+
270
+ await mkdir(dirname(outputFile), { recursive: true });
271
+ await writeFile(outputFile, code);
272
+ }),
273
+ );
274
+
275
+ return appDir;
276
+ }
277
+
278
+ function isMissingFileError(error: unknown): boolean {
279
+ return (
280
+ typeof error === "object" &&
281
+ error !== null &&
282
+ "code" in error &&
283
+ (error as { code?: unknown }).code === "ENOENT"
284
+ );
285
+ }
286
+
287
+ function builtArtifactReadError(label: string, artifactPath: string, error: unknown): Error {
288
+ const prefix = isMissingFileError(error) ? "Missing" : "Unable to read";
289
+ const detail = error instanceof Error && error.message !== "" ? `: ${error.message}` : "";
290
+
291
+ return new Error(`${prefix} ${label}: ${artifactPath}${detail}`, { cause: error });
292
+ }
293
+
294
+ function parseBuiltJsonArtifact<T>(text: string, artifactPath: string, label: string): T {
295
+ try {
296
+ return JSON.parse(text) as T;
297
+ } catch (error) {
298
+ const detail = error instanceof Error && error.message !== "" ? `: ${error.message}` : "";
299
+
300
+ throw new Error(`Invalid ${label}: ${artifactPath}${detail}`, { cause: error });
301
+ }
302
+ }
303
+
304
+ function safeManifestFilePath(pathname: string): string {
305
+ const normalized = normalize(pathname);
306
+
307
+ if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
308
+ throw new Error(`Invalid built app manifest file path: ${pathname}`);
309
+ }
310
+
311
+ return normalized;
312
+ }