@reckona/mreact-router 0.0.174 → 0.0.176

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/adapters/cloudflare.d.ts.map +1 -1
  2. package/dist/adapters/cloudflare.js +5 -49
  3. package/dist/adapters/cloudflare.js.map +1 -1
  4. package/dist/build.d.ts +1 -0
  5. package/dist/build.d.ts.map +1 -1
  6. package/dist/build.js +90 -0
  7. package/dist/build.js.map +1 -1
  8. package/dist/built-assets.d.ts +11 -0
  9. package/dist/built-assets.d.ts.map +1 -0
  10. package/dist/built-assets.js +130 -0
  11. package/dist/built-assets.js.map +1 -0
  12. package/dist/built-runtime.d.ts +53 -0
  13. package/dist/built-runtime.d.ts.map +1 -0
  14. package/dist/built-runtime.js +178 -0
  15. package/dist/built-runtime.js.map +1 -0
  16. package/dist/built-server-module-artifacts.d.ts +19 -0
  17. package/dist/built-server-module-artifacts.d.ts.map +1 -0
  18. package/dist/built-server-module-artifacts.js +217 -0
  19. package/dist/built-server-module-artifacts.js.map +1 -0
  20. package/dist/client-manifest-assets.d.ts +16 -0
  21. package/dist/client-manifest-assets.d.ts.map +1 -0
  22. package/dist/client-manifest-assets.js +60 -0
  23. package/dist/client-manifest-assets.js.map +1 -0
  24. package/dist/client.d.ts.map +1 -1
  25. package/dist/client.js +31 -3
  26. package/dist/client.js.map +1 -1
  27. package/dist/node-server.d.ts +29 -0
  28. package/dist/node-server.d.ts.map +1 -0
  29. package/dist/node-server.js +81 -0
  30. package/dist/node-server.js.map +1 -0
  31. package/dist/render.d.ts +9 -0
  32. package/dist/render.d.ts.map +1 -1
  33. package/dist/render.js +20 -41
  34. package/dist/render.js.map +1 -1
  35. package/dist/route-dispatch.d.ts +2 -0
  36. package/dist/route-dispatch.d.ts.map +1 -0
  37. package/dist/route-dispatch.js +16 -0
  38. package/dist/route-dispatch.js.map +1 -0
  39. package/dist/route-loader-runtime.d.ts +22 -0
  40. package/dist/route-loader-runtime.d.ts.map +1 -0
  41. package/dist/route-loader-runtime.js +30 -0
  42. package/dist/route-loader-runtime.js.map +1 -0
  43. package/dist/routes.d.ts +2 -0
  44. package/dist/routes.d.ts.map +1 -1
  45. package/dist/routes.js +48 -1
  46. package/dist/routes.js.map +1 -1
  47. package/dist/serve.d.ts +4 -3
  48. package/dist/serve.d.ts.map +1 -1
  49. package/dist/serve.js +84 -582
  50. package/dist/serve.js.map +1 -1
  51. package/package.json +11 -11
  52. package/src/adapters/cloudflare.ts +5 -60
  53. package/src/build.ts +143 -0
  54. package/src/built-assets.ts +164 -0
  55. package/src/built-runtime.ts +312 -0
  56. package/src/built-server-module-artifacts.ts +340 -0
  57. package/src/client-manifest-assets.ts +92 -0
  58. package/src/client.ts +31 -3
  59. package/src/node-server.ts +129 -0
  60. package/src/render.ts +38 -60
  61. package/src/route-dispatch.ts +17 -0
  62. package/src/route-loader-runtime.ts +54 -0
  63. package/src/routes.ts +70 -1
  64. package/src/serve.ts +105 -863
@@ -0,0 +1,340 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, normalize, sep } from "node:path";
3
+ import type { BuiltServerModuleArtifact } from "./build.js";
4
+ import { routeShellCandidates } from "./route-shells.js";
5
+
6
+ export interface BuiltServerModuleArtifactRuntime {
7
+ appDir: string;
8
+ serverModuleArtifactLoads: Map<string, Promise<void>>;
9
+ serverModuleClosureFiles: Map<string, readonly string[]>;
10
+ serverModuleFiles: ReadonlyMap<string, string>;
11
+ serverModuleRenderFiles: ReadonlyMap<string, string>;
12
+ serverModuleRequestFiles: ReadonlyMap<string, string>;
13
+ serverModules: Map<string, BuiltServerModuleArtifact>;
14
+ serverSourceFiles: ReadonlyMap<string, string>;
15
+ }
16
+
17
+ export type BuiltServerModuleArtifactKind = "all" | "render" | "request";
18
+
19
+ export async function loadBuiltServerModuleArtifacts(
20
+ runtime: BuiltServerModuleArtifactRuntime,
21
+ files: Iterable<string>,
22
+ kind: BuiltServerModuleArtifactKind = "all",
23
+ ): Promise<void> {
24
+ for (const file of files) {
25
+ await loadBuiltServerModuleArtifact(runtime, file, kind);
26
+ }
27
+ }
28
+
29
+ async function loadBuiltServerModuleArtifact(
30
+ runtime: BuiltServerModuleArtifactRuntime,
31
+ file: string,
32
+ kind: BuiltServerModuleArtifactKind = "all",
33
+ ): Promise<void> {
34
+ if (kind === "all") {
35
+ await loadBuiltServerModuleArtifact(runtime, file, "request");
36
+ await loadBuiltServerModuleArtifact(runtime, file, "render");
37
+ return;
38
+ }
39
+
40
+ const artifactPath =
41
+ kind === "request"
42
+ ? runtime.serverModuleRequestFiles.get(file) ?? runtime.serverModuleFiles.get(file)
43
+ : runtime.serverModuleRenderFiles.get(file) ?? runtime.serverModuleFiles.get(file);
44
+
45
+ if (artifactPath === undefined) {
46
+ return;
47
+ }
48
+
49
+ const cached = runtime.serverModuleArtifactLoads.get(`${kind}\0${file}`);
50
+
51
+ if (cached !== undefined) {
52
+ await cached;
53
+ return;
54
+ }
55
+
56
+ const loaded = readFile(artifactPath, "utf8")
57
+ .then((text) => {
58
+ const existing = runtime.serverModules.get(file) ?? {};
59
+ runtime.serverModules.set(
60
+ file,
61
+ mergeBuiltServerModuleArtifacts(
62
+ existing,
63
+ hydrateBuiltServerModuleArtifact(
64
+ parseBuiltJsonArtifact<BuiltServerModuleArtifact>(
65
+ text,
66
+ artifactPath,
67
+ `built server module artifact for ${file}`,
68
+ ),
69
+ builtServerDirForArtifactPath(artifactPath),
70
+ ),
71
+ ),
72
+ );
73
+ })
74
+ .catch((error) => {
75
+ runtime.serverModuleArtifactLoads.delete(`${kind}\0${file}`);
76
+ if (isMissingFileError(error)) {
77
+ throw builtArtifactReadError(`built server module artifact for ${file}`, artifactPath, error);
78
+ }
79
+ throw error;
80
+ });
81
+ runtime.serverModuleArtifactLoads.set(`${kind}\0${file}`, loaded);
82
+
83
+ await loaded;
84
+ }
85
+
86
+ export function allBuiltServerModuleFiles(
87
+ runtime: BuiltServerModuleArtifactRuntime,
88
+ ): Iterable<string> {
89
+ return new Set([
90
+ ...runtime.serverModuleFiles.keys(),
91
+ ...runtime.serverModuleRequestFiles.keys(),
92
+ ...runtime.serverModuleRenderFiles.keys(),
93
+ ]);
94
+ }
95
+
96
+ function builtServerDirForArtifactPath(artifactPath: string): string {
97
+ const marker = `${sep}server-modules${sep}`;
98
+ const index = artifactPath.lastIndexOf(marker);
99
+
100
+ return index === -1 ? dirname(dirname(artifactPath)) : artifactPath.slice(0, index);
101
+ }
102
+
103
+ function hydrateBuiltServerModuleArtifact(
104
+ artifact: BuiltServerModuleArtifact,
105
+ serverDir: string,
106
+ ): BuiltServerModuleArtifact {
107
+ return {
108
+ ...(artifact.analysis === undefined ? {} : { analysis: artifact.analysis }),
109
+ ...(artifact.loader === undefined
110
+ ? {}
111
+ : { loader: hydrateBuiltServerModuleOutput(artifact.loader, serverDir) }),
112
+ ...(artifact.routeMetadata === undefined
113
+ ? {}
114
+ : { routeMetadata: hydrateBuiltServerModuleOutput(artifact.routeMetadata, serverDir) }),
115
+ ...(artifact.request === undefined
116
+ ? {}
117
+ : { request: hydrateBuiltServerModuleOutput(artifact.request, serverDir) }),
118
+ ...(artifact.stream === undefined
119
+ ? {}
120
+ : { stream: hydrateBuiltServerModuleOutput(artifact.stream, serverDir) }),
121
+ ...(artifact.string === undefined
122
+ ? {}
123
+ : { string: hydrateBuiltServerModuleOutput(artifact.string, serverDir) }),
124
+ };
125
+ }
126
+
127
+ function hydrateBuiltServerModuleOutput<T extends { moduleFile?: string | undefined }>(
128
+ output: T,
129
+ serverDir: string,
130
+ ): T {
131
+ if (output.moduleFile === undefined || isAbsolute(output.moduleFile)) {
132
+ return output;
133
+ }
134
+
135
+ return {
136
+ ...output,
137
+ moduleFile: join(serverDir, safeBuiltServerManifestFilePath(output.moduleFile)),
138
+ };
139
+ }
140
+
141
+ function mergeBuiltServerModuleArtifacts(
142
+ existing: BuiltServerModuleArtifact,
143
+ loaded: BuiltServerModuleArtifact,
144
+ ): BuiltServerModuleArtifact {
145
+ return {
146
+ ...existing,
147
+ ...loaded,
148
+ };
149
+ }
150
+
151
+ export async function loadBuiltServerModuleArtifactsForRequest(
152
+ runtime: BuiltServerModuleArtifactRuntime,
153
+ routeFile: string | undefined,
154
+ options: {
155
+ includeRender?: boolean | undefined;
156
+ includeShells?: boolean | undefined;
157
+ } = {},
158
+ ): Promise<void> {
159
+ const roots = [
160
+ join(runtime.appDir, "middleware.ts"),
161
+ join(runtime.appDir, "middleware.mreact.ts"),
162
+ ...(routeFile === undefined
163
+ ? []
164
+ : [
165
+ routeFile,
166
+ ...(options.includeShells === false ? [] : shellFilesForRoute(runtime, routeFile)),
167
+ ]),
168
+ ];
169
+ const seen = new Set<string>();
170
+
171
+ for (const file of roots) {
172
+ await loadBuiltServerModuleArtifactClosure(runtime, file, seen, "request");
173
+ }
174
+
175
+ if (options.includeRender === true) {
176
+ seen.clear();
177
+ for (const file of roots) {
178
+ await loadBuiltServerModuleArtifactClosure(runtime, file, seen, "render");
179
+ }
180
+ }
181
+ }
182
+
183
+ async function loadBuiltServerModuleArtifactClosure(
184
+ runtime: BuiltServerModuleArtifactRuntime,
185
+ file: string,
186
+ seen: Set<string>,
187
+ kind: BuiltServerModuleArtifactKind,
188
+ ): Promise<void> {
189
+ for (const closureFile of builtServerModuleClosureFiles(runtime, file)) {
190
+ if (seen.has(closureFile)) {
191
+ continue;
192
+ }
193
+ seen.add(closureFile);
194
+ await loadBuiltServerModuleArtifact(runtime, closureFile, kind);
195
+ }
196
+ }
197
+
198
+ function builtServerModuleClosureFiles(
199
+ runtime: BuiltServerModuleArtifactRuntime,
200
+ file: string,
201
+ ): readonly string[] {
202
+ const cached = runtime.serverModuleClosureFiles.get(file);
203
+ if (cached !== undefined) {
204
+ return cached;
205
+ }
206
+
207
+ const closure: string[] = [];
208
+ collectBuiltServerModuleClosureFiles(runtime, file, new Set(), closure);
209
+ runtime.serverModuleClosureFiles.set(file, closure);
210
+ return closure;
211
+ }
212
+
213
+ function collectBuiltServerModuleClosureFiles(
214
+ runtime: BuiltServerModuleArtifactRuntime,
215
+ file: string,
216
+ seen: Set<string>,
217
+ closure: string[],
218
+ ): void {
219
+ if (seen.has(file)) {
220
+ return;
221
+ }
222
+ seen.add(file);
223
+ closure.push(file);
224
+
225
+ const source = runtime.serverSourceFiles.get(file);
226
+ if (source === undefined) {
227
+ return;
228
+ }
229
+
230
+ for (const specifier of localServerModuleSpecifiers(source)) {
231
+ const resolved = resolveBuiltLocalServerSourceImport(runtime, file, specifier);
232
+ if (resolved !== undefined) {
233
+ collectBuiltServerModuleClosureFiles(runtime, resolved, seen, closure);
234
+ }
235
+ }
236
+ }
237
+
238
+ const localServerModuleImportPattern =
239
+ /\b(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s*)?["'](?<source>\.{1,2}\/[^"']+)["']/g;
240
+
241
+ function localServerModuleSpecifiers(code: string): string[] {
242
+ const specifiers = new Set<string>();
243
+ localServerModuleImportPattern.lastIndex = 0;
244
+
245
+ for (const match of code.matchAll(localServerModuleImportPattern)) {
246
+ const source = match.groups?.source;
247
+
248
+ if (source !== undefined) {
249
+ specifiers.add(source);
250
+ }
251
+ }
252
+
253
+ return Array.from(specifiers);
254
+ }
255
+
256
+ function resolveBuiltLocalServerSourceImport(
257
+ runtime: BuiltServerModuleArtifactRuntime,
258
+ fromFile: string,
259
+ specifier: string,
260
+ ): string | undefined {
261
+ const base = join(dirname(fromFile), specifier);
262
+
263
+ for (const candidate of localServerSourceImportCandidates(base)) {
264
+ if (runtime.serverSourceFiles.has(candidate)) {
265
+ return candidate;
266
+ }
267
+ }
268
+
269
+ return undefined;
270
+ }
271
+
272
+ function localServerSourceImportCandidates(base: string): string[] {
273
+ const candidates = [base];
274
+
275
+ if (base.endsWith(".js")) {
276
+ const withoutJs = base.slice(0, -".js".length);
277
+ candidates.push(`${withoutJs}.ts`, `${withoutJs}.tsx`, `${withoutJs}.mreact.tsx`);
278
+ } else if (base.endsWith(".jsx")) {
279
+ const withoutJsx = base.slice(0, -".jsx".length);
280
+ candidates.push(`${withoutJsx}.tsx`, `${withoutJsx}.mreact.tsx`);
281
+ } else if (base.endsWith(".mreact")) {
282
+ candidates.push(`${base}.tsx`);
283
+ } else {
284
+ candidates.push(
285
+ `${base}.ts`,
286
+ `${base}.tsx`,
287
+ `${base}.mreact.tsx`,
288
+ join(base, "index.ts"),
289
+ join(base, "index.tsx"),
290
+ join(base, "index.mreact.tsx"),
291
+ );
292
+ }
293
+
294
+ return candidates;
295
+ }
296
+
297
+ function shellFilesForRoute(
298
+ runtime: BuiltServerModuleArtifactRuntime,
299
+ routeFile: string,
300
+ ): string[] {
301
+ return routeShellCandidates(runtime.appDir, routeFile)
302
+ .map((candidate) => candidate.file)
303
+ .filter((file) => runtime.serverSourceFiles.has(file));
304
+ }
305
+
306
+ function isMissingFileError(error: unknown): boolean {
307
+ return (
308
+ typeof error === "object" &&
309
+ error !== null &&
310
+ "code" in error &&
311
+ (error as { code?: unknown }).code === "ENOENT"
312
+ );
313
+ }
314
+
315
+ function builtArtifactReadError(label: string, artifactPath: string, error: unknown): Error {
316
+ const prefix = isMissingFileError(error) ? "Missing" : "Unable to read";
317
+ const detail = error instanceof Error && error.message !== "" ? `: ${error.message}` : "";
318
+
319
+ return new Error(`${prefix} ${label}: ${artifactPath}${detail}`, { cause: error });
320
+ }
321
+
322
+ function parseBuiltJsonArtifact<T>(text: string, artifactPath: string, label: string): T {
323
+ try {
324
+ return JSON.parse(text) as T;
325
+ } catch (error) {
326
+ const detail = error instanceof Error && error.message !== "" ? `: ${error.message}` : "";
327
+
328
+ throw new Error(`Invalid ${label}: ${artifactPath}${detail}`, { cause: error });
329
+ }
330
+ }
331
+
332
+ function safeBuiltServerManifestFilePath(pathname: string): string {
333
+ const normalized = normalize(pathname);
334
+
335
+ if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
336
+ throw new Error(`Invalid built app manifest file path: ${pathname}`);
337
+ }
338
+
339
+ return normalized;
340
+ }
@@ -0,0 +1,92 @@
1
+ export interface ClientManifestAssetRoute {
2
+ css?: readonly string[] | undefined;
3
+ imports?: readonly string[] | undefined;
4
+ navigationScript?: string | undefined;
5
+ script?: string | undefined;
6
+ sourceMap?: string | undefined;
7
+ }
8
+
9
+ export interface ClientManifestAssetSource {
10
+ assets?: readonly string[] | undefined;
11
+ routes: readonly ClientManifestAssetRoute[];
12
+ }
13
+
14
+ export function clientManifestAssetPaths(
15
+ manifest: ClientManifestAssetSource,
16
+ options: {
17
+ extraPaths?: readonly string[] | undefined;
18
+ prefix?: string | undefined;
19
+ } = {},
20
+ ): Set<string> {
21
+ const prefix = normalizeClientManifestAssetPrefix(options.prefix ?? "");
22
+ const paths = new Set<string>([`${prefix}manifest.json`]);
23
+
24
+ for (const route of manifest.routes) {
25
+ for (const asset of [
26
+ route.script,
27
+ route.sourceMap,
28
+ route.navigationScript,
29
+ ...(route.css ?? []),
30
+ ...(route.imports ?? []),
31
+ ]) {
32
+ const path = safeClientManifestAssetPath(asset);
33
+
34
+ if (path !== undefined) {
35
+ paths.add(`${prefix}${path}`);
36
+ }
37
+ }
38
+ }
39
+
40
+ for (const asset of manifest.assets ?? []) {
41
+ const path = safeClientManifestAssetPath(asset);
42
+
43
+ if (path !== undefined) {
44
+ paths.add(`${prefix}${path}`);
45
+ }
46
+ }
47
+
48
+ for (const asset of options.extraPaths ?? []) {
49
+ const path = safeClientManifestAssetPath(asset);
50
+
51
+ if (path !== undefined) {
52
+ paths.add(`${prefix}${path}`);
53
+ }
54
+ }
55
+
56
+ return paths;
57
+ }
58
+
59
+ function normalizeClientManifestAssetPrefix(prefix: string): string {
60
+ if (prefix === "") {
61
+ return "";
62
+ }
63
+
64
+ const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
65
+
66
+ return withLeadingSlash.endsWith("/") ? withLeadingSlash : `${withLeadingSlash}/`;
67
+ }
68
+
69
+ function safeClientManifestAssetPath(asset: string | undefined): string | undefined {
70
+ if (asset === undefined || asset === "" || asset.startsWith("/") || asset.includes("\\")) {
71
+ return undefined;
72
+ }
73
+
74
+ const segments = asset.split("/");
75
+
76
+ return segments.some((segment) => unsafeClientManifestAssetSegment(segment))
77
+ ? undefined
78
+ : segments.join("/");
79
+ }
80
+
81
+ function unsafeClientManifestAssetSegment(segment: string): boolean {
82
+ if (segment === "" || segment === "." || segment === "..") {
83
+ return true;
84
+ }
85
+
86
+ try {
87
+ const decoded = decodeURIComponent(segment);
88
+ return decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\");
89
+ } catch {
90
+ return true;
91
+ }
92
+ }
package/src/client.ts CHANGED
@@ -2657,7 +2657,7 @@ export async function __mreactPrefetch(url) {
2657
2657
  return true;
2658
2658
  }
2659
2659
 
2660
- __mreactNavigationState.prefetchedUrls.add(href);
2660
+ __mreactRememberPrefetchedUrl(href);
2661
2661
 
2662
2662
  const script = __mreactRouteScriptForNavigationUrl(href);
2663
2663
 
@@ -2684,6 +2684,33 @@ function __mreactPrefetchNavigationHtml(href) {
2684
2684
  }
2685
2685
 
2686
2686
  const __mreactNavigationHtmlCacheMaxEntries = 64;
2687
+ const __mreactNavigationPrefetchHistoryMaxEntries = 64;
2688
+
2689
+ function __mreactRememberPrefetchedUrl(href) {
2690
+ __mreactRememberPrefetchHistory(__mreactNavigationState.prefetchedUrls, href);
2691
+ }
2692
+
2693
+ function __mreactRememberPrefetchedScript(href) {
2694
+ __mreactRememberPrefetchHistory(__mreactNavigationState.prefetchedScripts, href);
2695
+ }
2696
+
2697
+ function __mreactRememberPrefetchHistory(history, href) {
2698
+ if (history.has(href)) {
2699
+ history.delete(href);
2700
+ }
2701
+
2702
+ history.add(href);
2703
+
2704
+ while (history.size > __mreactNavigationPrefetchHistoryMaxEntries) {
2705
+ const oldestHref = history.keys().next().value;
2706
+
2707
+ if (oldestHref === undefined) {
2708
+ return;
2709
+ }
2710
+
2711
+ history.delete(oldestHref);
2712
+ }
2713
+ }
2687
2714
 
2688
2715
  function __mreactCachedNavigationHtml(href) {
2689
2716
  const html = __mreactNavigationState.cache.get(href);
@@ -2712,6 +2739,7 @@ function __mreactRememberNavigationHtml(href, html) {
2712
2739
  }
2713
2740
 
2714
2741
  __mreactNavigationState.cache.delete(oldestHref);
2742
+ __mreactNavigationState.prefetchedUrls.delete(oldestHref);
2715
2743
  }
2716
2744
  }
2717
2745
 
@@ -2732,7 +2760,7 @@ function __mreactPrefetchRouteScript(script) {
2732
2760
 
2733
2761
  for (const link of Array.from(document.querySelectorAll('link[rel="modulepreload"][href]'))) {
2734
2762
  if (link.href === href) {
2735
- __mreactNavigationState.prefetchedScripts.add(href);
2763
+ __mreactRememberPrefetchedScript(href);
2736
2764
  return true;
2737
2765
  }
2738
2766
  }
@@ -2741,7 +2769,7 @@ function __mreactPrefetchRouteScript(script) {
2741
2769
  link.rel = "modulepreload";
2742
2770
  link.href = href;
2743
2771
  document.head.appendChild(link);
2744
- __mreactNavigationState.prefetchedScripts.add(href);
2772
+ __mreactRememberPrefetchedScript(href);
2745
2773
  return true;
2746
2774
  }
2747
2775
 
@@ -0,0 +1,129 @@
1
+ import { createServer, type Server } from "node:http";
2
+ import { nodeRequestToWebRequest, sendResponse } from "./http.js";
3
+ import {
4
+ emitRouterLog,
5
+ logDurationMs,
6
+ logError,
7
+ logNow,
8
+ nodeRequestPath,
9
+ requestLogFields,
10
+ type AppRouterLogger,
11
+ } from "./logger.js";
12
+ import type { HttpUpgradeHandler } from "./upgrade.js";
13
+
14
+ export interface StartNodeRequestServerOptions {
15
+ allowedHosts?: readonly string[] | undefined;
16
+ errorHandler?: ((error: unknown) => {
17
+ body: string;
18
+ status: number;
19
+ headers?: Record<string, string>;
20
+ }) | undefined;
21
+ hostname?: string | undefined;
22
+ hostPolicy?: "strict" | "trusted-proxy" | undefined;
23
+ logger?: AppRouterLogger | undefined;
24
+ onUpgrade?: HttpUpgradeHandler | undefined;
25
+ port: number;
26
+ render(request: Request): Promise<Response>;
27
+ resolveHost?: ((options: {
28
+ allowedHosts?: readonly string[] | undefined;
29
+ fallbackHost: string;
30
+ hostPolicy?: "strict" | "trusted-proxy" | undefined;
31
+ rawHost: string | undefined;
32
+ }) => string) | undefined;
33
+ }
34
+
35
+ export async function startNodeRequestServer(
36
+ options: StartNodeRequestServerOptions,
37
+ ): Promise<{ close(): Promise<void>; server: Server; url: string }> {
38
+ const server = createServer(async (incoming, outgoing) => {
39
+ const startedAt = logNow();
40
+ const fallbackRequestFields = {
41
+ method: incoming.method ?? "GET",
42
+ path: nodeRequestPath(incoming.url),
43
+ runtime: "node" as const,
44
+ };
45
+
46
+ try {
47
+ const fallbackHost = `${options.hostname ?? "127.0.0.1"}:${options.port}`;
48
+ const host = (options.resolveHost ?? defaultResolveRequestHost)({
49
+ allowedHosts: options.allowedHosts,
50
+ fallbackHost,
51
+ hostPolicy: options.hostPolicy,
52
+ rawHost: incoming.headers.host,
53
+ });
54
+ const request = nodeRequestToWebRequest(incoming, `http://${host}`);
55
+ const logFields = requestLogFields(request, "node");
56
+ emitRouterLog(options.logger, "info", {
57
+ ...logFields,
58
+ type: "router:request:start",
59
+ });
60
+ const response = await options.render(request);
61
+ emitRouterLog(options.logger, "info", {
62
+ ...logFields,
63
+ durationMs: logDurationMs(startedAt),
64
+ status: response.status,
65
+ type: "router:request:end",
66
+ });
67
+
68
+ await sendResponse(outgoing, response);
69
+ } catch (error) {
70
+ // Log the full stack to stderr for operator visibility; never put
71
+ // it in the response body where attackers can scrape it.
72
+ emitRouterLog(options.logger, "error", {
73
+ ...fallbackRequestFields,
74
+ durationMs: logDurationMs(startedAt),
75
+ error: logError(error),
76
+ type: "router:request:error",
77
+ });
78
+ if (options.logger === undefined) {
79
+ console.error("[mreact] startServer request failed:", error);
80
+ }
81
+ const payload = options.errorHandler
82
+ ? options.errorHandler(error)
83
+ : { body: "Internal Server Error", status: 500 };
84
+ outgoing.statusCode = payload.status;
85
+ outgoing.setHeader(
86
+ "content-type",
87
+ payload.headers?.["content-type"] ?? "text/plain; charset=utf-8",
88
+ );
89
+ for (const [name, value] of Object.entries(payload.headers ?? {})) {
90
+ if (name.toLowerCase() === "content-type") continue;
91
+ outgoing.setHeader(name, value);
92
+ }
93
+ outgoing.end(payload.body);
94
+ }
95
+ });
96
+
97
+ if (options.onUpgrade !== undefined) {
98
+ server.on("upgrade", options.onUpgrade);
99
+ }
100
+
101
+ await new Promise<void>((resolve) =>
102
+ server.listen(options.port, options.hostname ?? "127.0.0.1", resolve),
103
+ );
104
+ const address = server.address();
105
+ const port = typeof address === "object" && address !== null ? address.port : options.port;
106
+
107
+ return {
108
+ server,
109
+ url: `http://${options.hostname ?? "127.0.0.1"}:${port}`,
110
+ close: () =>
111
+ new Promise<void>((resolve, reject) =>
112
+ server.close((error) => (error ? reject(error) : resolve())),
113
+ ),
114
+ };
115
+ }
116
+
117
+ function defaultResolveRequestHost(options: {
118
+ allowedHosts?: readonly string[] | undefined;
119
+ fallbackHost: string;
120
+ hostPolicy?: "strict" | "trusted-proxy" | undefined;
121
+ rawHost: string | undefined;
122
+ }): string {
123
+ const raw = options.rawHost;
124
+ if (raw === undefined || raw === "") return options.fallbackHost;
125
+ if (options.allowedHosts === undefined) {
126
+ return options.hostPolicy === "strict" ? options.fallbackHost : raw;
127
+ }
128
+ return options.allowedHosts.includes(raw) ? raw : options.fallbackHost;
129
+ }