@reckona/mreact-router 0.0.65 → 0.0.67
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.
- package/package.json +13 -12
- package/src/actions.ts +1130 -0
- package/src/adapters/aws-lambda.ts +993 -0
- package/src/adapters/cloudflare.ts +1286 -0
- package/src/adapters/devtools.ts +5 -0
- package/src/adapters/edge.ts +70 -0
- package/src/adapters/node.ts +126 -0
- package/src/adapters/static.ts +61 -0
- package/src/app-router-globals.ts +19 -0
- package/src/assets.ts +113 -0
- package/src/build.ts +2948 -0
- package/src/bundle-pipeline.ts +496 -0
- package/src/cache-config.ts +35 -0
- package/src/cache-stats.ts +54 -0
- package/src/cache.ts +418 -0
- package/src/cli-options.ts +296 -0
- package/src/cli.ts +94 -0
- package/src/client.ts +3398 -0
- package/src/config.ts +146 -0
- package/src/cookies.ts +113 -0
- package/src/csp.ts +103 -0
- package/src/csrf.ts +132 -0
- package/src/deferred.ts +52 -0
- package/src/dev-server.ts +262 -0
- package/src/file-conventions.ts +88 -0
- package/src/http.ts +128 -0
- package/src/i18n.ts +98 -0
- package/src/import-policy.ts +261 -0
- package/src/index.ts +221 -0
- package/src/link.ts +47 -0
- package/src/logger.ts +149 -0
- package/src/module-runner.ts +554 -0
- package/src/multipart.ts +577 -0
- package/src/native-escape.ts +53 -0
- package/src/native-route-matcher.ts +173 -0
- package/src/navigation-state.ts +98 -0
- package/src/navigation.ts +215 -0
- package/src/prerender-store.ts +233 -0
- package/src/render.ts +5187 -0
- package/src/route-path.ts +5 -0
- package/src/route-shells.ts +47 -0
- package/src/route-source.ts +224 -0
- package/src/route-styles.ts +91 -0
- package/src/routes.ts +362 -0
- package/src/runtime-cache.ts +8 -0
- package/src/runtime-state.ts +37 -0
- package/src/security-headers.ts +134 -0
- package/src/serve.ts +1265 -0
- package/src/session.ts +238 -0
- package/src/source-jsx.ts +30 -0
- package/src/source-modules.ts +69 -0
- package/src/stream-list.ts +45 -0
- package/src/trace.ts +114 -0
- package/src/types.ts +155 -0
- package/src/upgrade.ts +8 -0
- package/src/vite-config.ts +63 -0
- package/src/vite-plugin-cache-key.ts +53 -0
- package/src/vite.ts +690 -0
- 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
|
+
}
|