@reckona/mreact-router 0.0.66 → 0.0.68

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 (117) hide show
  1. package/dist/actions.js.map +1 -1
  2. package/dist/adapters/aws-lambda.js.map +1 -1
  3. package/dist/adapters/cloudflare.js.map +1 -1
  4. package/dist/adapters/devtools.js.map +1 -1
  5. package/dist/adapters/edge.js.map +1 -1
  6. package/dist/adapters/node.js.map +1 -1
  7. package/dist/adapters/static.js.map +1 -1
  8. package/dist/app-router-globals.js.map +1 -1
  9. package/dist/assets.js.map +1 -1
  10. package/dist/build.js.map +1 -1
  11. package/dist/bundle-pipeline.js.map +1 -1
  12. package/dist/cache-config.js.map +1 -1
  13. package/dist/cache-stats.js.map +1 -1
  14. package/dist/cache.js.map +1 -1
  15. package/dist/cli-options.js.map +1 -1
  16. package/dist/cli.js.map +1 -1
  17. package/dist/client.js.map +1 -1
  18. package/dist/config.js.map +1 -1
  19. package/dist/cookies.js.map +1 -1
  20. package/dist/csp.js.map +1 -1
  21. package/dist/csrf.js.map +1 -1
  22. package/dist/deferred.js.map +1 -1
  23. package/dist/dev-server.js.map +1 -1
  24. package/dist/file-conventions.js.map +1 -1
  25. package/dist/http.js.map +1 -1
  26. package/dist/i18n.js.map +1 -1
  27. package/dist/import-policy.js.map +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/link.js.map +1 -1
  30. package/dist/logger.js.map +1 -1
  31. package/dist/module-runner.js.map +1 -1
  32. package/dist/multipart.js.map +1 -1
  33. package/dist/native-escape.js.map +1 -1
  34. package/dist/native-route-matcher.js.map +1 -1
  35. package/dist/navigation-state.js.map +1 -1
  36. package/dist/navigation.js.map +1 -1
  37. package/dist/prerender-store.js.map +1 -1
  38. package/dist/render.js.map +1 -1
  39. package/dist/route-path.js.map +1 -1
  40. package/dist/route-shells.js.map +1 -1
  41. package/dist/route-source.js.map +1 -1
  42. package/dist/route-styles.js.map +1 -1
  43. package/dist/routes.js.map +1 -1
  44. package/dist/runtime-cache.js.map +1 -1
  45. package/dist/runtime-state.js.map +1 -1
  46. package/dist/security-headers.js.map +1 -1
  47. package/dist/serve.js.map +1 -1
  48. package/dist/session.js.map +1 -1
  49. package/dist/source-jsx.js.map +1 -1
  50. package/dist/source-modules.js.map +1 -1
  51. package/dist/stream-list.js.map +1 -1
  52. package/dist/trace.js.map +1 -1
  53. package/dist/types.js.map +1 -1
  54. package/dist/upgrade.js.map +1 -1
  55. package/dist/vite-config.js.map +1 -1
  56. package/dist/vite-plugin-cache-key.js.map +1 -1
  57. package/dist/vite.js.map +1 -1
  58. package/dist/workspace-packages.js.map +1 -1
  59. package/package.json +13 -12
  60. package/src/actions.ts +1130 -0
  61. package/src/adapters/aws-lambda.ts +993 -0
  62. package/src/adapters/cloudflare.ts +1286 -0
  63. package/src/adapters/devtools.ts +5 -0
  64. package/src/adapters/edge.ts +70 -0
  65. package/src/adapters/node.ts +126 -0
  66. package/src/adapters/static.ts +61 -0
  67. package/src/app-router-globals.ts +19 -0
  68. package/src/assets.ts +113 -0
  69. package/src/build.ts +2948 -0
  70. package/src/bundle-pipeline.ts +496 -0
  71. package/src/cache-config.ts +35 -0
  72. package/src/cache-stats.ts +54 -0
  73. package/src/cache.ts +418 -0
  74. package/src/cli-options.ts +296 -0
  75. package/src/cli.ts +94 -0
  76. package/src/client.ts +3398 -0
  77. package/src/config.ts +146 -0
  78. package/src/cookies.ts +113 -0
  79. package/src/csp.ts +103 -0
  80. package/src/csrf.ts +132 -0
  81. package/src/deferred.ts +52 -0
  82. package/src/dev-server.ts +262 -0
  83. package/src/file-conventions.ts +88 -0
  84. package/src/http.ts +128 -0
  85. package/src/i18n.ts +98 -0
  86. package/src/import-policy.ts +261 -0
  87. package/src/index.ts +221 -0
  88. package/src/link.ts +47 -0
  89. package/src/logger.ts +149 -0
  90. package/src/module-runner.ts +554 -0
  91. package/src/multipart.ts +577 -0
  92. package/src/native-escape.ts +53 -0
  93. package/src/native-route-matcher.ts +173 -0
  94. package/src/navigation-state.ts +98 -0
  95. package/src/navigation.ts +215 -0
  96. package/src/prerender-store.ts +233 -0
  97. package/src/render.ts +5187 -0
  98. package/src/route-path.ts +5 -0
  99. package/src/route-shells.ts +47 -0
  100. package/src/route-source.ts +224 -0
  101. package/src/route-styles.ts +91 -0
  102. package/src/routes.ts +362 -0
  103. package/src/runtime-cache.ts +8 -0
  104. package/src/runtime-state.ts +37 -0
  105. package/src/security-headers.ts +134 -0
  106. package/src/serve.ts +1265 -0
  107. package/src/session.ts +238 -0
  108. package/src/source-jsx.ts +30 -0
  109. package/src/source-modules.ts +69 -0
  110. package/src/stream-list.ts +45 -0
  111. package/src/trace.ts +114 -0
  112. package/src/types.ts +155 -0
  113. package/src/upgrade.ts +8 -0
  114. package/src/vite-config.ts +63 -0
  115. package/src/vite-plugin-cache-key.ts +53 -0
  116. package/src/vite.ts +690 -0
  117. package/src/workspace-packages.ts +67 -0
package/src/serve.ts ADDED
@@ -0,0 +1,1265 @@
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";
10
+ import type { AppRouterCache } from "./cache.js";
11
+ import type { ClientRouteManifestEntry } from "./client.js";
12
+ import { createRouteMatcher, type AppRoute, type RouteMatcher } from "./routes.js";
13
+ import type { AppRouterServerActionOptions } from "./actions.js";
14
+ import type { AppRouterImportPolicy } from "./import-policy.js";
15
+ import {
16
+ preloadBuiltRequestModules,
17
+ renderAppRequest,
18
+ resolveAppRouterMiddleware,
19
+ type AppRouterRenderPreload,
20
+ type AppRouterResponseHook,
21
+ type RenderAppRequestOptions,
22
+ } from "./render.js";
23
+ import type { RouterInstrumentation } from "./trace.js";
24
+ import { bytesResponse, htmlResponse, nodeRequestToWebRequest, sendResponse } from "./http.js";
25
+ import {
26
+ emitRouterLog,
27
+ logDurationMs,
28
+ logError,
29
+ logNow,
30
+ nodeRequestPath,
31
+ requestLogFields,
32
+ type AppRouterLogger,
33
+ } from "./logger.js";
34
+ import { routeShellCandidates } from "./route-shells.js";
35
+ import { normalizeRoutePath } from "./route-path.js";
36
+ import type { HttpUpgradeHandler } from "./upgrade.js";
37
+
38
+ interface BuiltRuntime {
39
+ appDir: string;
40
+ allowedSourceDirs: readonly string[];
41
+ assetBaseUrl?: string | undefined;
42
+ clientScripts: ReadonlyMap<string, string>;
43
+ clientStyles: ReadonlyMap<string, readonly string[]>;
44
+ hasMiddleware: boolean;
45
+ navigationScripts: ReadonlyMap<string, string>;
46
+ projectRoot: string;
47
+ publicAssetBaseUrl?: string | undefined;
48
+ prerenderableRoutes: ReadonlySet<string>;
49
+ prerenderLocks: Map<string, Promise<Response>>;
50
+ prerenderedRoutes: Map<string, BuiltPrerenderedRoute>;
51
+ routeMatcher: RouteMatcher;
52
+ routes: readonly AppRoute[];
53
+ serverActionManifest?: readonly { moduleId: string; exportName: string; inferred?: boolean }[] | undefined;
54
+ serverModuleArtifactLoads: Map<string, Promise<void>>;
55
+ serverModuleFiles: ReadonlyMap<string, string>;
56
+ serverModuleRenderFiles: ReadonlyMap<string, string>;
57
+ serverModuleRequestFiles: ReadonlyMap<string, string>;
58
+ serverModules: Map<string, BuiltServerModuleArtifact>;
59
+ serverModuleCacheVersion: string;
60
+ serverSourceFiles: ReadonlyMap<string, string>;
61
+ }
62
+
63
+ interface BuiltRuntimeCacheEntry {
64
+ clientManifestText: string;
65
+ runtime: Promise<BuiltRuntime>;
66
+ serverManifestText: string;
67
+ }
68
+
69
+ const builtRuntimeCache = new Map<string, BuiltRuntimeCacheEntry>();
70
+ const builtPublicAssetCache = new Map<string, BuiltPublicAsset | null>();
71
+
72
+ interface BuiltPublicAsset {
73
+ bytes: Uint8Array;
74
+ headers: HeadersInit;
75
+ }
76
+
77
+ /**
78
+ * Strategy for the final response body shape sent to the HTTP layer.
79
+ *
80
+ * - `"string"` (default, cross-runtime): body is the raw HTML string. The
81
+ * underlying runtime encodes to UTF-8 bytes when writing to the socket.
82
+ * - `"buffer"` (Node only): materialize the response body into a Buffer
83
+ * before returning. Skips the implicit encode at the Response → socket
84
+ * boundary and can let Node's HTTP layer write bytes directly.
85
+ *
86
+ * The buffer path forces full materialization of streaming responses
87
+ * (loses TTFB streaming) — only opt in if the throughput gain outweighs
88
+ * that on your workload.
89
+ */
90
+ export type ResponseSinkStrategy = "string" | "buffer";
91
+ export type RequestHostPolicy = "strict" | "trusted-proxy";
92
+
93
+ let warnedImplicitHostTrust = false;
94
+
95
+ export interface RenderBuiltAppRequestOptions {
96
+ outDir: string;
97
+ importPolicy?: AppRouterImportPolicy | undefined;
98
+ instrumentation?: RouterInstrumentation | undefined;
99
+ logger?: AppRouterLogger | undefined;
100
+ onResponse?: AppRouterResponseHook | undefined;
101
+ prerenderStore?: AppRouterPrerenderStore | undefined;
102
+ request: Request;
103
+ routeCache?: AppRouterCache | undefined;
104
+ runtimeDir?: string | undefined;
105
+ serverActions?: AppRouterServerActionOptions | undefined;
106
+ sinkStrategy?: ResponseSinkStrategy;
107
+ preload?: AppRouterRenderPreload | undefined;
108
+ }
109
+
110
+ export type BuiltAppRuntimePreloadMode =
111
+ | "all"
112
+ | "hot-route-requests"
113
+ | "hot-routes"
114
+ | "middleware"
115
+ | "none";
116
+
117
+ export interface BuiltAppRuntimePreloadStrategy {
118
+ mode: BuiltAppRuntimePreloadMode;
119
+ routes?: readonly string[] | undefined;
120
+ }
121
+
122
+ export interface StartServerOptions {
123
+ outDir: string;
124
+ port: number;
125
+ hostname?: string;
126
+ // Optional hook for customizing the 500 response. The default returns
127
+ // a generic "Internal Server Error" body and logs the stack to stderr
128
+ // via console.error. Issue 071: stack traces must never end up in
129
+ // production responses.
130
+ errorHandler?: (error: unknown) => { body: string; status: number; headers?: Record<string, string> };
131
+ // When set, an incoming Host header that does not exactly match one of
132
+ // the listed values is replaced with the configured hostname/port for
133
+ // origin reconstruction. Use this in front of public deployments to
134
+ // block Host header injection (Issue 068). Undefined preserves the
135
+ // legacy "trust Host" behavior for backward compatibility when
136
+ // hostPolicy is not configured.
137
+ allowedHosts?: readonly string[] | undefined;
138
+ hostPolicy?: RequestHostPolicy | undefined;
139
+ importPolicy?: AppRouterImportPolicy | undefined;
140
+ instrumentation?: RouterInstrumentation | undefined;
141
+ logger?: AppRouterLogger | undefined;
142
+ onResponse?: AppRouterResponseHook | undefined;
143
+ prerenderStore?: AppRouterPrerenderStore | undefined;
144
+ routeCache?: AppRouterCache | undefined;
145
+ serverActions?: AppRouterServerActionOptions | undefined;
146
+ sinkStrategy?: ResponseSinkStrategy;
147
+ onUpgrade?: HttpUpgradeHandler | undefined;
148
+ }
149
+
150
+ export function resolveRequestHost(options: {
151
+ allowedHosts?: readonly string[] | undefined;
152
+ fallbackHost: string;
153
+ hostPolicy?: RequestHostPolicy | undefined;
154
+ rawHost: string | undefined;
155
+ }): string {
156
+ const raw = options.rawHost;
157
+ if (raw === undefined || raw === "") return options.fallbackHost;
158
+ if (options.allowedHosts === undefined) {
159
+ return options.hostPolicy === "strict" ? options.fallbackHost : raw;
160
+ }
161
+ return options.allowedHosts.includes(raw) ? raw : options.fallbackHost;
162
+ }
163
+
164
+ export function warnIfImplicitHostTrust(options: {
165
+ allowedHosts?: readonly string[] | undefined;
166
+ hostPolicy?: RequestHostPolicy | undefined;
167
+ }): void {
168
+ if (
169
+ process.env.NODE_ENV !== "production" ||
170
+ options.allowedHosts !== undefined ||
171
+ options.hostPolicy !== undefined ||
172
+ warnedImplicitHostTrust
173
+ ) {
174
+ return;
175
+ }
176
+
177
+ warnedImplicitHostTrust = true;
178
+ console.error(
179
+ "[mreact] Host header trust is implicit because neither allowedHosts nor hostPolicy is configured. Set allowedHosts for public deployments, hostPolicy: \"strict\" to reject unlisted Host headers, or hostPolicy: \"trusted-proxy\" when a trusted reverse proxy normalizes Host.",
180
+ );
181
+ }
182
+
183
+ export interface AppRouterPrerenderStore {
184
+ delete(path: string): void | Promise<void>;
185
+ get(path: string): BuiltPrerenderedRoute | undefined | Promise<BuiltPrerenderedRoute | undefined>;
186
+ set(path: string, entry: BuiltPrerenderedRoute): void | Promise<void>;
187
+ withLock?<T>(path: string, task: () => Promise<T>): Promise<T>;
188
+ }
189
+
190
+ export async function preloadBuiltAppRuntime(options: {
191
+ importPolicy?: AppRouterImportPolicy | undefined;
192
+ outDir: string;
193
+ preload?: BuiltAppRuntimePreloadStrategy | undefined;
194
+ runtimeDir?: string | undefined;
195
+ }): Promise<void> {
196
+ const runtime = await readBuiltRuntime({
197
+ outDir: options.outDir,
198
+ runtimeDir: options.runtimeDir,
199
+ });
200
+ const strategy = options.preload ?? { mode: "all" };
201
+
202
+ if (strategy.mode === "none") {
203
+ return;
204
+ }
205
+
206
+ const routes = builtRuntimePreloadRoutes(runtime, strategy);
207
+ if (strategy.mode === "all") {
208
+ await loadBuiltServerModuleArtifacts(runtime, allBuiltServerModuleFiles(runtime), "all");
209
+ } else {
210
+ await loadBuiltServerModuleArtifactsForRequest(runtime, undefined, {
211
+ includeRender: strategy.mode !== "hot-route-requests",
212
+ });
213
+ for (const route of routes) {
214
+ await loadBuiltServerModuleArtifactsForRequest(runtime, route.file, {
215
+ includeShells: strategy.mode !== "hot-route-requests",
216
+ includeRender: strategy.mode !== "hot-route-requests",
217
+ });
218
+ }
219
+ }
220
+ await preloadBuiltRequestModules({
221
+ appDir: runtime.appDir,
222
+ importPolicy: {
223
+ ...options.importPolicy,
224
+ allowedSourceDirs: runtime.allowedSourceDirs,
225
+ projectRoot: runtime.projectRoot,
226
+ },
227
+ routes,
228
+ serverModules: runtime.serverModules,
229
+ serverModuleCacheVersion: runtime.serverModuleCacheVersion,
230
+ serverSourceFiles: runtime.serverSourceFiles,
231
+ includeRenderModules: strategy.mode !== "hot-route-requests",
232
+ });
233
+ }
234
+
235
+ function builtRuntimePreloadRoutes(
236
+ runtime: BuiltRuntime,
237
+ strategy: BuiltAppRuntimePreloadStrategy,
238
+ ): readonly AppRoute[] {
239
+ if (strategy.mode === "all") {
240
+ return runtime.routes;
241
+ }
242
+
243
+ if (strategy.mode === "middleware" || strategy.mode === "none") {
244
+ return [];
245
+ }
246
+
247
+ const routes = strategy.routes ?? [];
248
+ return routes.map((path) => {
249
+ const route = runtime.routeMatcher.match(normalizeRoutePath(path))?.route;
250
+ if (route === undefined) {
251
+ throw new Error(`Unknown hot route preload path: ${path}`);
252
+ }
253
+ return route;
254
+ });
255
+ }
256
+
257
+ export async function renderBuiltAppRequest(
258
+ options: RenderBuiltAppRequestOptions,
259
+ ): Promise<Response> {
260
+ const response = await renderBuiltAppRequestWithRuntime({
261
+ ...options,
262
+ runtime: await readBuiltRuntime({
263
+ outDir: options.outDir,
264
+ runtimeDir: options.runtimeDir,
265
+ }),
266
+ });
267
+
268
+ return applyBuiltAppResponseHook(response, options);
269
+ }
270
+
271
+ async function renderBuiltAppRequestWithRuntime(
272
+ options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
273
+ ): Promise<Response> {
274
+ const url = new URL(options.request.url);
275
+ const timing = createBuiltRenderTiming(options.logger);
276
+
277
+ if (url.pathname.startsWith("/_mreact/client/")) {
278
+ return readBuiltClientAsset(options.outDir, url.pathname);
279
+ }
280
+
281
+ if (options.request.method === "GET" || options.request.method === "HEAD") {
282
+ const publicAsset = await readBuiltPublicAsset(options.outDir, url.pathname);
283
+
284
+ if (publicAsset !== undefined) {
285
+ return publicAsset;
286
+ }
287
+ }
288
+
289
+ let request = options.request;
290
+ let normalizedPath = normalizeRoutePath(url.pathname);
291
+ let matched = options.runtime.routeMatcher.match(normalizedPath);
292
+
293
+ if (options.request.method === "GET" || options.request.method === "HEAD") {
294
+ // Sync fast path when no external prerender store is configured (the
295
+ // common case): skip the Promise wrap that `readPrerenderedRoute`
296
+ // would otherwise introduce just to satisfy the async signature.
297
+ const prerendered =
298
+ options.prerenderStore === undefined
299
+ ? options.runtime.prerenderedRoutes.get(normalizedPath)
300
+ : await readPrerenderedRoute(
301
+ options.runtime,
302
+ normalizedPath,
303
+ options.prerenderStore,
304
+ );
305
+
306
+ if (prerendered !== undefined) {
307
+ if (options.request.method === "HEAD") {
308
+ return new Response(null, {
309
+ headers: prerendered.headers,
310
+ status: prerendered.status,
311
+ });
312
+ }
313
+ return htmlResponse(prerendered.html, {
314
+ headers: prerendered.headers,
315
+ status: prerendered.status,
316
+ });
317
+ }
318
+ }
319
+
320
+ const middlewareStartedAt = builtRenderTimingPhaseStartedAt(timing);
321
+ const middlewareResult = await resolveBuiltMiddleware({ ...options, timing }, request);
322
+ finishBuiltRenderTimingPhase(timing, middlewareStartedAt, "middlewareMs");
323
+ if (middlewareResult.type === "response") {
324
+ emitBuiltRenderTiming(options, request, timing, middlewareResult.response.status);
325
+ return middlewareResult.response;
326
+ }
327
+ request = middlewareResult.request;
328
+ normalizedPath = normalizeRoutePath(new URL(request.url).pathname);
329
+ matched = options.runtime.routeMatcher.match(normalizedPath);
330
+
331
+ if (request.method === "GET" && options.runtime.prerenderableRoutes.has(normalizedPath)) {
332
+ await loadBuiltServerModuleArtifactsForRequest(options.runtime, matched?.route.file, {
333
+ includeRender: false,
334
+ });
335
+ return renderAndCachePrerenderWithLock({ ...options, request }, normalizedPath);
336
+ }
337
+
338
+ await loadBuiltServerModuleArtifactsForRequest(options.runtime, matched?.route.file, {
339
+ includeRender: false,
340
+ });
341
+ const response = await renderBuiltDynamicResponse({ ...options, request });
342
+
343
+ await applyBuiltPrerenderInvalidations(
344
+ options.runtime,
345
+ response,
346
+ options.prerenderStore,
347
+ );
348
+
349
+ return options.sinkStrategy === "buffer"
350
+ ? await materializeResponseAsBuffer(response)
351
+ : response;
352
+ }
353
+
354
+ async function resolveBuiltMiddleware(
355
+ options: RenderBuiltAppRequestOptions & {
356
+ runtime: BuiltRuntime;
357
+ timing?: { phases: Record<string, number> } | undefined;
358
+ },
359
+ request: Request,
360
+ ): Promise<{ request: Request; type: "continue" } | { response: Response; type: "response" }> {
361
+ if (!options.runtime.hasMiddleware) {
362
+ return { request, type: "continue" };
363
+ }
364
+
365
+ await loadBuiltServerModuleArtifactsForRequest(options.runtime, undefined);
366
+
367
+ return resolveAppRouterMiddleware({
368
+ appDir: options.runtime.appDir,
369
+ importPolicy: {
370
+ ...options.importPolicy,
371
+ allowedSourceDirs: options.runtime.allowedSourceDirs,
372
+ projectRoot: options.runtime.projectRoot,
373
+ },
374
+ instrumentation: options.instrumentation,
375
+ request,
376
+ serverModules: options.runtime.serverModules,
377
+ serverModuleCacheVersion: options.runtime.serverModuleCacheVersion,
378
+ serverSourceFiles: options.runtime.serverSourceFiles,
379
+ timing: options.timing,
380
+ });
381
+ }
382
+
383
+ function createBuiltRenderTiming(
384
+ logger: AppRouterLogger | undefined,
385
+ ): { phases: Record<string, number> } | undefined {
386
+ return logger?.debug === undefined ? undefined : { phases: {} };
387
+ }
388
+
389
+ function builtRenderTimingPhaseStartedAt(
390
+ timing: { phases: Record<string, number> } | undefined,
391
+ ): number | undefined {
392
+ return timing === undefined ? undefined : logNow();
393
+ }
394
+
395
+ function finishBuiltRenderTimingPhase(
396
+ timing: { phases: Record<string, number> } | undefined,
397
+ startedAt: number | undefined,
398
+ phaseName: string,
399
+ ): void {
400
+ if (timing === undefined || startedAt === undefined) {
401
+ return;
402
+ }
403
+
404
+ timing.phases[phaseName] = logDurationMs(startedAt);
405
+ }
406
+
407
+ function emitBuiltRenderTiming(
408
+ options: RenderBuiltAppRequestOptions,
409
+ request: Request,
410
+ timing: { phases: Record<string, number> } | undefined,
411
+ status: number,
412
+ ): void {
413
+ if (timing === undefined) {
414
+ return;
415
+ }
416
+
417
+ emitRouterLog(options.logger, "debug", {
418
+ method: request.method,
419
+ path: new URL(request.url).pathname,
420
+ phases: timing.phases,
421
+ status,
422
+ type: "router:render:timing",
423
+ });
424
+ }
425
+
426
+ async function applyBuiltAppResponseHook(
427
+ response: Response,
428
+ options: Pick<RenderBuiltAppRequestOptions, "onResponse" | "request">,
429
+ ): Promise<Response> {
430
+ const hooked = await options.onResponse?.(response, {
431
+ request: options.request,
432
+ });
433
+
434
+ return hooked instanceof Response ? hooked : response;
435
+ }
436
+
437
+ async function materializeResponseAsBuffer(response: Response): Promise<Response> {
438
+ if (response.body === null) {
439
+ return response;
440
+ }
441
+
442
+ // Drains streaming responses into a single Buffer (loses TTFB streaming
443
+ // by design — opt-in via sinkStrategy === "buffer"). Avoids the
444
+ // string → UTF-8 encode the Response stream would otherwise do lazily
445
+ // during the socket write. Tagged via `bytesResponse` so `sendResponse`
446
+ // can take the raw-bytes fast path.
447
+ const bytes = new Uint8Array(await response.arrayBuffer());
448
+ return bytesResponse(bytes, {
449
+ headers: response.headers,
450
+ status: response.status,
451
+ statusText: response.statusText,
452
+ });
453
+ }
454
+
455
+ export async function startServer(
456
+ options: StartServerOptions,
457
+ ): Promise<{ close(): Promise<void>; server: Server; url: string }> {
458
+ warnIfImplicitHostTrust(options);
459
+ const runtime = await readBuiltRuntime({ outDir: options.outDir });
460
+ const server = createServer(async (incoming, outgoing) => {
461
+ const startedAt = logNow();
462
+ const fallbackRequestFields = {
463
+ method: incoming.method ?? "GET",
464
+ path: nodeRequestPath(incoming.url),
465
+ runtime: "node" as const,
466
+ };
467
+
468
+ try {
469
+ const fallbackHost = `${options.hostname ?? "127.0.0.1"}:${options.port}`;
470
+ const host = resolveRequestHost({
471
+ allowedHosts: options.allowedHosts,
472
+ fallbackHost,
473
+ hostPolicy: options.hostPolicy,
474
+ rawHost: incoming.headers.host,
475
+ });
476
+ const origin = `http://${host}`;
477
+ const request = nodeRequestToWebRequest(incoming, origin);
478
+ const logFields = requestLogFields(request, "node");
479
+ emitRouterLog(options.logger, "info", {
480
+ ...logFields,
481
+ type: "router:request:start",
482
+ });
483
+ const response = await applyBuiltAppResponseHook(await renderBuiltAppRequestWithRuntime({
484
+ outDir: options.outDir,
485
+ importPolicy: options.importPolicy,
486
+ instrumentation: options.instrumentation,
487
+ logger: options.logger,
488
+ onResponse: options.onResponse,
489
+ prerenderStore: options.prerenderStore,
490
+ request,
491
+ routeCache: options.routeCache,
492
+ runtime,
493
+ serverActions: options.serverActions,
494
+ ...(options.sinkStrategy === undefined ? {} : { sinkStrategy: options.sinkStrategy }),
495
+ }), { onResponse: options.onResponse, request });
496
+ emitRouterLog(options.logger, "info", {
497
+ ...logFields,
498
+ durationMs: logDurationMs(startedAt),
499
+ status: response.status,
500
+ type: "router:request:end",
501
+ });
502
+
503
+ await sendResponse(outgoing, response);
504
+ } catch (error) {
505
+ // Log the full stack to stderr for operator visibility; never
506
+ // place it in the response body where attackers can scrape it
507
+ // (Issue 071). The errorHandler hook lets embedders customize
508
+ // the public response shape while still benefiting from the
509
+ // server-side log.
510
+ emitRouterLog(options.logger, "error", {
511
+ ...fallbackRequestFields,
512
+ durationMs: logDurationMs(startedAt),
513
+ error: logError(error),
514
+ type: "router:request:error",
515
+ });
516
+ if (options.logger === undefined) {
517
+ console.error("[mreact] startServer request failed:", error);
518
+ }
519
+ const payload = options.errorHandler
520
+ ? options.errorHandler(error)
521
+ : { body: "Internal Server Error", status: 500 };
522
+ outgoing.statusCode = payload.status;
523
+ outgoing.setHeader(
524
+ "content-type",
525
+ payload.headers?.["content-type"] ?? "text/plain; charset=utf-8",
526
+ );
527
+ for (const [name, value] of Object.entries(payload.headers ?? {})) {
528
+ if (name.toLowerCase() === "content-type") continue;
529
+ outgoing.setHeader(name, value);
530
+ }
531
+ outgoing.end(payload.body);
532
+ }
533
+ });
534
+
535
+ if (options.onUpgrade !== undefined) {
536
+ server.on("upgrade", options.onUpgrade);
537
+ }
538
+
539
+ await new Promise<void>((resolve) =>
540
+ server.listen(options.port, options.hostname ?? "127.0.0.1", resolve),
541
+ );
542
+ const address = server.address();
543
+ const port = typeof address === "object" && address !== null ? address.port : options.port;
544
+
545
+ return {
546
+ server,
547
+ url: `http://${options.hostname ?? "127.0.0.1"}:${port}`,
548
+ close: () =>
549
+ new Promise<void>((resolve, reject) =>
550
+ server.close((error) => (error ? reject(error) : resolve())),
551
+ ),
552
+ };
553
+ }
554
+
555
+ async function readBuiltClientAsset(outDir: string, pathname: string): Promise<Response> {
556
+ const clientPrefix = "/_mreact/client/";
557
+ const relativePath = pathname.slice(clientPrefix.length);
558
+ const normalized = normalize(relativePath);
559
+
560
+ if (normalized.startsWith("..")) {
561
+ return new Response("Not Found", { status: 404 });
562
+ }
563
+
564
+ try {
565
+ const code = await readFile(join(outDir, "client", normalized), "utf8");
566
+
567
+ return new Response(code, {
568
+ headers: clientAssetHeaders(normalized),
569
+ });
570
+ } catch {
571
+ return new Response("Not Found", { status: 404 });
572
+ }
573
+ }
574
+
575
+ async function readBuiltPublicAsset(
576
+ outDir: string,
577
+ pathname: string,
578
+ ): Promise<Response | undefined> {
579
+ const relativePath = pathname.startsWith("/") ? pathname.slice(1) : pathname;
580
+
581
+ if (relativePath === "") {
582
+ return undefined;
583
+ }
584
+
585
+ const normalized = normalize(relativePath);
586
+
587
+ if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
588
+ return undefined;
589
+ }
590
+
591
+ try {
592
+ const cacheKey = `${outDir}\0${normalized}`;
593
+ const cached = builtPublicAssetCache.get(cacheKey);
594
+
595
+ if (cached === null) {
596
+ return undefined;
597
+ }
598
+
599
+ if (cached !== undefined) {
600
+ return bytesResponse(cached.bytes, {
601
+ headers: cached.headers,
602
+ });
603
+ }
604
+
605
+ const bytes = await readFile(join(outDir, "client", "public", normalized));
606
+ const headers = publicAssetHeaders(normalized);
607
+
608
+ builtPublicAssetCache.set(cacheKey, {
609
+ bytes,
610
+ headers,
611
+ });
612
+
613
+ return bytesResponse(bytes, { headers });
614
+ } catch {
615
+ builtPublicAssetCache.set(`${outDir}\0${normalized}`, null);
616
+ return undefined;
617
+ }
618
+ }
619
+
620
+ async function readBuiltRuntime(options: {
621
+ outDir: string;
622
+ runtimeDir?: string | undefined;
623
+ }): Promise<BuiltRuntime> {
624
+ const outDir = options.outDir;
625
+ const runtimeDir = options.runtimeDir ?? join(outDir, "server", "runtime");
626
+ const [serverManifestText, clientManifestText] = await Promise.all([
627
+ readFile(join(outDir, "server", "manifest.json"), "utf8"),
628
+ readFile(join(outDir, "client", "manifest.json"), "utf8"),
629
+ ]);
630
+ const cacheKey = `${outDir}\0${runtimeDir}`;
631
+ const cached = builtRuntimeCache.get(cacheKey);
632
+
633
+ if (
634
+ cached !== undefined &&
635
+ cached.serverManifestText === serverManifestText &&
636
+ cached.clientManifestText === clientManifestText
637
+ ) {
638
+ return cached.runtime;
639
+ }
640
+
641
+ const runtime = materializeBuiltRuntime({
642
+ clientManifestText,
643
+ outDir,
644
+ runtimeDir,
645
+ serverManifestText,
646
+ });
647
+
648
+ builtRuntimeCache.set(cacheKey, {
649
+ clientManifestText,
650
+ runtime,
651
+ serverManifestText,
652
+ });
653
+
654
+ return runtime;
655
+ }
656
+
657
+ async function materializeBuiltRuntime(options: {
658
+ clientManifestText: string;
659
+ outDir: string;
660
+ runtimeDir: string;
661
+ serverManifestText: string;
662
+ }): Promise<BuiltRuntime> {
663
+ const serverManifest = JSON.parse(options.serverManifestText) as BuiltServerManifest;
664
+ const clientManifest = JSON.parse(options.clientManifestText) as {
665
+ routes: ClientRouteManifestEntry[];
666
+ };
667
+ const appDir = await materializeBuiltServerApp(options.runtimeDir, serverManifest);
668
+ const projectRoot = appDir;
669
+ const routesDir = join(projectRoot, serverManifest.routesDir ?? "");
670
+ const routes = serverManifest.routes.map((route) => ({
671
+ ...route,
672
+ file: join(projectRoot, route.file),
673
+ }));
674
+ const prerenderedRoutes = new Map(Object.entries(serverManifest.prerenderedRoutes ?? {}));
675
+ const prerenderableRoutes = new Set(prerenderedRoutes.keys());
676
+ const prerenderLocks = new Map<string, Promise<Response>>();
677
+ const serverModules = new Map<string, BuiltServerModuleArtifact>(
678
+ Object.entries(serverManifest.serverModules ?? {}).map(([file, artifact]) => [
679
+ join(appDir, file),
680
+ artifact,
681
+ ]),
682
+ );
683
+ const serverModuleFiles = new Map(
684
+ Object.entries(serverManifest.serverModuleFiles ?? {}).map(([file, artifactFile]) => [
685
+ join(appDir, file),
686
+ join(options.outDir, "server", safeManifestFilePath(artifactFile)),
687
+ ]),
688
+ );
689
+ const serverModuleRequestFiles = new Map(
690
+ Object.entries(serverManifest.serverModuleRequestFiles ?? {}).map(([file, artifactFile]) => [
691
+ join(appDir, file),
692
+ join(options.outDir, "server", safeManifestFilePath(artifactFile)),
693
+ ]),
694
+ );
695
+ const serverModuleRenderFiles = new Map(
696
+ Object.entries(serverManifest.serverModuleRenderFiles ?? {}).map(([file, artifactFile]) => [
697
+ join(appDir, file),
698
+ join(options.outDir, "server", safeManifestFilePath(artifactFile)),
699
+ ]),
700
+ );
701
+ const serverSourceFiles = new Map(
702
+ Object.entries(serverManifest.files).map(([file, source]) => [join(appDir, file), source]),
703
+ );
704
+ const routeMatcher = createRouteMatcher(routes);
705
+ const clientScripts = new Map(
706
+ clientManifest.routes.flatMap((route) =>
707
+ route.client && route.script !== undefined ? [[route.path, route.script]] : [],
708
+ ),
709
+ );
710
+ const clientStyles = new Map(
711
+ clientManifest.routes.flatMap((route) =>
712
+ route.css !== undefined && route.css.length > 0 ? [[route.path, route.css]] : [],
713
+ ),
714
+ );
715
+ const navigationScripts = new Map(
716
+ clientManifest.routes.flatMap((route) =>
717
+ route.navigation === true && route.navigationScript !== undefined
718
+ ? [[route.path, route.navigationScript]]
719
+ : [],
720
+ ),
721
+ );
722
+ const hasMiddleware =
723
+ serverSourceFiles.has(join(routesDir, "middleware.ts")) ||
724
+ serverSourceFiles.has(join(routesDir, "middleware.mreact.ts"));
725
+ const serverModuleCacheVersion = createHash("sha256")
726
+ .update(options.serverManifestText)
727
+ .update("\0")
728
+ .update(options.clientManifestText)
729
+ .digest("hex")
730
+ .slice(0, 16);
731
+
732
+ const allowedSourceDirs = (serverManifest.allowedSourceDirs ?? [""]).map((directory) =>
733
+ join(projectRoot, directory),
734
+ );
735
+
736
+ return {
737
+ appDir: routesDir,
738
+ allowedSourceDirs,
739
+ ...(serverManifest.assetBaseUrl === undefined
740
+ ? {}
741
+ : { assetBaseUrl: serverManifest.assetBaseUrl }),
742
+ clientScripts,
743
+ clientStyles,
744
+ hasMiddleware,
745
+ navigationScripts,
746
+ projectRoot,
747
+ ...(serverManifest.publicAssetBaseUrl === undefined
748
+ ? {}
749
+ : { publicAssetBaseUrl: serverManifest.publicAssetBaseUrl }),
750
+ prerenderableRoutes,
751
+ prerenderLocks,
752
+ prerenderedRoutes,
753
+ routeMatcher,
754
+ routes,
755
+ ...(serverManifest.serverActionManifest === undefined
756
+ ? {}
757
+ : { serverActionManifest: serverManifest.serverActionManifest }),
758
+ serverModuleArtifactLoads: new Map(),
759
+ serverModuleFiles,
760
+ serverModuleRenderFiles,
761
+ serverModuleRequestFiles,
762
+ serverModules,
763
+ serverModuleCacheVersion,
764
+ serverSourceFiles,
765
+ };
766
+ }
767
+
768
+ async function loadBuiltServerModuleArtifacts(
769
+ runtime: BuiltRuntime,
770
+ files: Iterable<string>,
771
+ kind: BuiltServerModuleArtifactKind = "all",
772
+ ): Promise<void> {
773
+ for (const file of files) {
774
+ await loadBuiltServerModuleArtifact(runtime, file, kind);
775
+ }
776
+ }
777
+
778
+ type BuiltServerModuleArtifactKind = "all" | "render" | "request";
779
+
780
+ async function loadBuiltServerModuleArtifact(
781
+ runtime: BuiltRuntime,
782
+ file: string,
783
+ kind: BuiltServerModuleArtifactKind = "all",
784
+ ): Promise<void> {
785
+ if (kind === "all") {
786
+ await loadBuiltServerModuleArtifact(runtime, file, "request");
787
+ await loadBuiltServerModuleArtifact(runtime, file, "render");
788
+ return;
789
+ }
790
+
791
+ const artifactPath =
792
+ kind === "request"
793
+ ? runtime.serverModuleRequestFiles.get(file) ?? runtime.serverModuleFiles.get(file)
794
+ : runtime.serverModuleRenderFiles.get(file) ?? runtime.serverModuleFiles.get(file);
795
+
796
+ if (artifactPath === undefined) {
797
+ return;
798
+ }
799
+
800
+ const cached = runtime.serverModuleArtifactLoads.get(`${kind}\0${file}`);
801
+
802
+ if (cached !== undefined) {
803
+ await cached;
804
+ return;
805
+ }
806
+
807
+ const loaded = readFile(artifactPath, "utf8")
808
+ .then((text) => {
809
+ const existing = runtime.serverModules.get(file) ?? {};
810
+ runtime.serverModules.set(
811
+ file,
812
+ mergeBuiltServerModuleArtifacts(
813
+ existing,
814
+ hydrateBuiltServerModuleArtifact(
815
+ JSON.parse(text) as BuiltServerModuleArtifact,
816
+ builtServerDirForArtifactPath(artifactPath),
817
+ ),
818
+ ),
819
+ );
820
+ })
821
+ .catch((error) => {
822
+ runtime.serverModuleArtifactLoads.delete(`${kind}\0${file}`);
823
+ throw error;
824
+ });
825
+ runtime.serverModuleArtifactLoads.set(`${kind}\0${file}`, loaded);
826
+
827
+ await loaded;
828
+ }
829
+
830
+ function allBuiltServerModuleFiles(runtime: BuiltRuntime): Iterable<string> {
831
+ return new Set([
832
+ ...runtime.serverModuleFiles.keys(),
833
+ ...runtime.serverModuleRequestFiles.keys(),
834
+ ...runtime.serverModuleRenderFiles.keys(),
835
+ ]);
836
+ }
837
+
838
+ function builtServerDirForArtifactPath(artifactPath: string): string {
839
+ const marker = `${sep}server-modules${sep}`;
840
+ const index = artifactPath.lastIndexOf(marker);
841
+
842
+ return index === -1 ? dirname(dirname(artifactPath)) : artifactPath.slice(0, index);
843
+ }
844
+
845
+ function hydrateBuiltServerModuleArtifact(
846
+ artifact: BuiltServerModuleArtifact,
847
+ serverDir: string,
848
+ ): BuiltServerModuleArtifact {
849
+ return {
850
+ ...(artifact.analysis === undefined ? {} : { analysis: artifact.analysis }),
851
+ ...(artifact.loader === undefined
852
+ ? {}
853
+ : { loader: hydrateBuiltServerModuleOutput(artifact.loader, serverDir) }),
854
+ ...(artifact.routeMetadata === undefined
855
+ ? {}
856
+ : { routeMetadata: hydrateBuiltServerModuleOutput(artifact.routeMetadata, serverDir) }),
857
+ ...(artifact.request === undefined
858
+ ? {}
859
+ : { request: hydrateBuiltServerModuleOutput(artifact.request, serverDir) }),
860
+ ...(artifact.stream === undefined
861
+ ? {}
862
+ : { stream: hydrateBuiltServerModuleOutput(artifact.stream, serverDir) }),
863
+ ...(artifact.string === undefined
864
+ ? {}
865
+ : { string: hydrateBuiltServerModuleOutput(artifact.string, serverDir) }),
866
+ };
867
+ }
868
+
869
+ function hydrateBuiltServerModuleOutput<T extends { moduleFile?: string | undefined }>(
870
+ output: T,
871
+ serverDir: string,
872
+ ): T {
873
+ if (output.moduleFile === undefined || isAbsolute(output.moduleFile)) {
874
+ return output;
875
+ }
876
+
877
+ return {
878
+ ...output,
879
+ moduleFile: join(serverDir, safeManifestFilePath(output.moduleFile)),
880
+ };
881
+ }
882
+
883
+ function mergeBuiltServerModuleArtifacts(
884
+ existing: BuiltServerModuleArtifact,
885
+ loaded: BuiltServerModuleArtifact,
886
+ ): BuiltServerModuleArtifact {
887
+ return {
888
+ ...existing,
889
+ ...loaded,
890
+ };
891
+ }
892
+
893
+ async function loadBuiltServerModuleArtifactsForRequest(
894
+ runtime: BuiltRuntime,
895
+ routeFile: string | undefined,
896
+ options: {
897
+ includeRender?: boolean | undefined;
898
+ includeShells?: boolean | undefined;
899
+ } = {},
900
+ ): Promise<void> {
901
+ const roots = [
902
+ join(runtime.appDir, "middleware.ts"),
903
+ join(runtime.appDir, "middleware.mreact.ts"),
904
+ ...(routeFile === undefined
905
+ ? []
906
+ : [
907
+ routeFile,
908
+ ...(options.includeShells === false ? [] : shellFilesForRoute(runtime, routeFile)),
909
+ ]),
910
+ ];
911
+ const seen = new Set<string>();
912
+
913
+ for (const file of roots) {
914
+ await loadBuiltServerModuleArtifactClosure(runtime, file, seen, "request");
915
+ }
916
+
917
+ if (options.includeRender === true) {
918
+ seen.clear();
919
+ for (const file of roots) {
920
+ await loadBuiltServerModuleArtifactClosure(runtime, file, seen, "render");
921
+ }
922
+ }
923
+ }
924
+
925
+ async function loadBuiltServerModuleArtifactClosure(
926
+ runtime: BuiltRuntime,
927
+ file: string,
928
+ seen: Set<string>,
929
+ kind: BuiltServerModuleArtifactKind,
930
+ ): Promise<void> {
931
+ if (seen.has(file)) {
932
+ return;
933
+ }
934
+ seen.add(file);
935
+
936
+ await loadBuiltServerModuleArtifact(runtime, file, kind);
937
+
938
+ const source = runtime.serverSourceFiles.get(file);
939
+
940
+ if (source === undefined) {
941
+ return;
942
+ }
943
+
944
+ for (const specifier of localServerModuleSpecifiers(source)) {
945
+ const resolved = resolveBuiltLocalServerSourceImport(runtime, file, specifier);
946
+
947
+ if (resolved !== undefined) {
948
+ await loadBuiltServerModuleArtifactClosure(runtime, resolved, seen, kind);
949
+ }
950
+ }
951
+ }
952
+
953
+ function localServerModuleSpecifiers(code: string): string[] {
954
+ const specifiers = new Set<string>();
955
+ const importPattern =
956
+ /\b(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s*)?["'](?<source>\.{1,2}\/[^"']+)["']/g;
957
+
958
+ for (const match of code.matchAll(importPattern)) {
959
+ const source = match.groups?.source;
960
+
961
+ if (source !== undefined) {
962
+ specifiers.add(source);
963
+ }
964
+ }
965
+
966
+ return Array.from(specifiers);
967
+ }
968
+
969
+ function resolveBuiltLocalServerSourceImport(
970
+ runtime: BuiltRuntime,
971
+ fromFile: string,
972
+ specifier: string,
973
+ ): string | undefined {
974
+ const base = join(dirname(fromFile), specifier);
975
+
976
+ for (const candidate of localServerSourceImportCandidates(base)) {
977
+ if (runtime.serverSourceFiles.has(candidate)) {
978
+ return candidate;
979
+ }
980
+ }
981
+
982
+ return undefined;
983
+ }
984
+
985
+ function localServerSourceImportCandidates(base: string): string[] {
986
+ const candidates = [base];
987
+
988
+ if (base.endsWith(".js")) {
989
+ const withoutJs = base.slice(0, -".js".length);
990
+ candidates.push(`${withoutJs}.ts`, `${withoutJs}.tsx`, `${withoutJs}.mreact.tsx`);
991
+ } else if (base.endsWith(".jsx")) {
992
+ const withoutJsx = base.slice(0, -".jsx".length);
993
+ candidates.push(`${withoutJsx}.tsx`, `${withoutJsx}.mreact.tsx`);
994
+ } else if (base.endsWith(".mreact")) {
995
+ candidates.push(`${base}.tsx`);
996
+ } else {
997
+ candidates.push(
998
+ `${base}.ts`,
999
+ `${base}.tsx`,
1000
+ `${base}.mreact.tsx`,
1001
+ join(base, "index.ts"),
1002
+ join(base, "index.tsx"),
1003
+ join(base, "index.mreact.tsx"),
1004
+ );
1005
+ }
1006
+
1007
+ return candidates;
1008
+ }
1009
+
1010
+ function shellFilesForRoute(runtime: BuiltRuntime, routeFile: string): string[] {
1011
+ return routeShellCandidates(runtime.appDir, routeFile)
1012
+ .map((candidate) => candidate.file)
1013
+ .filter((file) => runtime.serverSourceFiles.has(file));
1014
+ }
1015
+
1016
+ async function readPrerenderedRoute(
1017
+ runtime: BuiltRuntime,
1018
+ path: string,
1019
+ store: AppRouterPrerenderStore | undefined,
1020
+ ): Promise<BuiltPrerenderedRoute | undefined> {
1021
+ const stored = await store?.get(path);
1022
+
1023
+ if (stored !== undefined) {
1024
+ runtime.prerenderedRoutes.set(path, stored);
1025
+ return stored;
1026
+ }
1027
+
1028
+ const manifestEntry = runtime.prerenderedRoutes.get(path);
1029
+
1030
+ if (manifestEntry !== undefined && store !== undefined) {
1031
+ await store.set(path, manifestEntry);
1032
+ }
1033
+
1034
+ return manifestEntry;
1035
+ }
1036
+
1037
+ function renderBuiltDynamicResponse(
1038
+ options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
1039
+ ): Promise<Response> {
1040
+ return renderAppRequest(builtRenderAppRequestOptions(options));
1041
+ }
1042
+
1043
+ function builtRenderAppRequestOptions(
1044
+ options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
1045
+ ): RenderAppRequestOptions {
1046
+ const renderOptions: RenderAppRequestOptions & {
1047
+ __mreactLoadServerRenderArtifacts?: ((routeFile: string) => Promise<void>) | undefined;
1048
+ } = {
1049
+ appDir: options.runtime.appDir,
1050
+ assetBaseUrl: options.runtime.assetBaseUrl,
1051
+ clientScripts: options.runtime.clientScripts,
1052
+ clientStyles: options.runtime.clientStyles,
1053
+ importPolicy: {
1054
+ ...options.importPolicy,
1055
+ allowedSourceDirs: options.runtime.allowedSourceDirs,
1056
+ projectRoot: options.runtime.projectRoot,
1057
+ },
1058
+ request: options.request,
1059
+ instrumentation: options.instrumentation,
1060
+ logger: options.logger,
1061
+ navigationScripts: options.runtime.navigationScripts,
1062
+ routeCache: options.routeCache,
1063
+ routeMatcher: options.runtime.routeMatcher,
1064
+ routes: options.runtime.routes,
1065
+ __mreactLoadServerRenderArtifacts: async (routeFile: string) => {
1066
+ await loadBuiltServerModuleArtifactsForRequest(options.runtime, routeFile, {
1067
+ includeRender: true,
1068
+ });
1069
+ },
1070
+ serverModules: options.runtime.serverModules,
1071
+ serverModuleCacheVersion: options.runtime.serverModuleCacheVersion,
1072
+ serverSourceFiles: options.runtime.serverSourceFiles,
1073
+ serverActions: mergeBuiltServerActionOptions(
1074
+ options.serverActions,
1075
+ options.runtime.serverActionManifest,
1076
+ ),
1077
+ skipMiddleware: true,
1078
+ ...(options.preload === undefined ? {} : { preload: options.preload }),
1079
+ };
1080
+
1081
+ return renderOptions;
1082
+ }
1083
+
1084
+ function mergeBuiltServerActionOptions(
1085
+ options: AppRouterServerActionOptions | undefined,
1086
+ allowedActions: readonly { moduleId: string; exportName: string; inferred?: boolean }[] | undefined,
1087
+ ): AppRouterServerActionOptions | undefined {
1088
+ if (allowedActions === undefined) {
1089
+ return options;
1090
+ }
1091
+
1092
+ return {
1093
+ ...options,
1094
+ allowedActions,
1095
+ };
1096
+ }
1097
+
1098
+ async function renderAndCachePrerenderWithLock(
1099
+ options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
1100
+ path: string,
1101
+ ): Promise<Response> {
1102
+ const existing = options.runtime.prerenderLocks.get(path);
1103
+
1104
+ if (existing !== undefined) {
1105
+ return cloneResponse(await existing);
1106
+ }
1107
+
1108
+ const task = runPrerenderRegeneration(options, path);
1109
+ options.runtime.prerenderLocks.set(path, task);
1110
+
1111
+ try {
1112
+ return cloneResponse(await task);
1113
+ } finally {
1114
+ options.runtime.prerenderLocks.delete(path);
1115
+ }
1116
+ }
1117
+
1118
+ async function runPrerenderRegeneration(
1119
+ options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
1120
+ path: string,
1121
+ ): Promise<Response> {
1122
+ const regenerate = async () => {
1123
+ const stored = await readPrerenderedRoute(options.runtime, path, options.prerenderStore);
1124
+
1125
+ if (stored !== undefined) {
1126
+ return htmlResponse(stored.html, {
1127
+ headers: stored.headers,
1128
+ status: stored.status,
1129
+ });
1130
+ }
1131
+
1132
+ const response = await renderBuiltDynamicResponse(options);
1133
+
1134
+ return response.ok
1135
+ ? await cacheRegeneratedPrerenderedRoute(
1136
+ options.runtime,
1137
+ path,
1138
+ response,
1139
+ options.prerenderStore,
1140
+ )
1141
+ : response;
1142
+ };
1143
+
1144
+ return options.prerenderStore?.withLock === undefined
1145
+ ? await regenerate()
1146
+ : await options.prerenderStore.withLock(path, regenerate);
1147
+ }
1148
+
1149
+ async function applyBuiltPrerenderInvalidations(
1150
+ runtime: BuiltRuntime,
1151
+ response: Response,
1152
+ store: AppRouterPrerenderStore | undefined,
1153
+ ): Promise<void> {
1154
+ const revalidated = response.headers.get("x-mreact-revalidate");
1155
+
1156
+ if (revalidated === null) {
1157
+ return;
1158
+ }
1159
+
1160
+ for (const path of revalidated.split(",")) {
1161
+ const normalized = normalizeRoutePath(path.trim());
1162
+ runtime.prerenderedRoutes.delete(normalized);
1163
+ await store?.delete(normalized);
1164
+ }
1165
+ }
1166
+
1167
+ async function cacheRegeneratedPrerenderedRoute(
1168
+ runtime: BuiltRuntime,
1169
+ path: string,
1170
+ response: Response,
1171
+ store: AppRouterPrerenderStore | undefined,
1172
+ ): Promise<Response> {
1173
+ const body = await response.text();
1174
+ const headers: Record<string, string> = {};
1175
+
1176
+ response.headers.forEach((value, key) => {
1177
+ headers[key] = value;
1178
+ });
1179
+ const entry = {
1180
+ headers,
1181
+ html: body,
1182
+ status: response.status,
1183
+ };
1184
+ runtime.prerenderedRoutes.set(path, entry);
1185
+ await store?.set(path, entry);
1186
+
1187
+ return htmlResponse(body, {
1188
+ headers: response.headers,
1189
+ status: response.status,
1190
+ });
1191
+ }
1192
+
1193
+ async function cloneResponse(response: Response): Promise<Response> {
1194
+ return htmlResponse(await response.clone().text(), {
1195
+ headers: response.headers,
1196
+ status: response.status,
1197
+ });
1198
+ }
1199
+
1200
+ async function materializeBuiltServerApp(
1201
+ runtimeDir: string,
1202
+ manifest: BuiltServerManifest,
1203
+ ): Promise<string> {
1204
+ const appDir = join(runtimeDir, "app");
1205
+
1206
+ await rm(appDir, { force: true, recursive: true });
1207
+ await Promise.all(
1208
+ Object.entries(manifest.files).map(async ([file, code]) => {
1209
+ const outputFile = join(appDir, safeManifestFilePath(file));
1210
+
1211
+ await mkdir(dirname(outputFile), { recursive: true });
1212
+ await writeFile(outputFile, code);
1213
+ }),
1214
+ );
1215
+
1216
+ return appDir;
1217
+ }
1218
+
1219
+ function safeManifestFilePath(pathname: string): string {
1220
+ const normalized = normalize(pathname);
1221
+
1222
+ if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
1223
+ throw new Error(`Invalid built app manifest file path: ${pathname}`);
1224
+ }
1225
+
1226
+ return normalized;
1227
+ }
1228
+
1229
+ function clientAssetHeaders(pathname: string): HeadersInit {
1230
+ if (pathname === "manifest.json") {
1231
+ return {
1232
+ "cache-control": "no-cache",
1233
+ "content-type": "application/json; charset=utf-8",
1234
+ };
1235
+ }
1236
+
1237
+ return {
1238
+ "cache-control": "public, max-age=31536000, immutable",
1239
+ "content-type": pathname.endsWith(".css")
1240
+ ? "text/css; charset=utf-8"
1241
+ : "text/javascript; charset=utf-8",
1242
+ };
1243
+ }
1244
+
1245
+ function publicAssetHeaders(pathname: string): HeadersInit {
1246
+ return {
1247
+ "cache-control": "public, max-age=3600",
1248
+ "content-type": publicAssetContentType(pathname),
1249
+ };
1250
+ }
1251
+
1252
+ function publicAssetContentType(pathname: string): string {
1253
+ if (pathname.endsWith(".css")) return "text/css; charset=utf-8";
1254
+ if (pathname.endsWith(".html")) return "text/html; charset=utf-8";
1255
+ if (pathname.endsWith(".js")) return "text/javascript; charset=utf-8";
1256
+ if (pathname.endsWith(".json")) return "application/json; charset=utf-8";
1257
+ if (pathname.endsWith(".svg")) return "image/svg+xml";
1258
+ if (pathname.endsWith(".png")) return "image/png";
1259
+ if (pathname.endsWith(".jpg") || pathname.endsWith(".jpeg")) return "image/jpeg";
1260
+ if (pathname.endsWith(".webp")) return "image/webp";
1261
+ if (pathname.endsWith(".ico")) return "image/x-icon";
1262
+ if (pathname.endsWith(".txt")) return "text/plain; charset=utf-8";
1263
+
1264
+ return "application/octet-stream";
1265
+ }