@reckona/mreact-router 0.0.1

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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +101 -0
  3. package/dist/actions.d.ts +43 -0
  4. package/dist/actions.d.ts.map +1 -0
  5. package/dist/actions.js +577 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/adapters/aws-lambda.d.ts +45 -0
  8. package/dist/adapters/aws-lambda.d.ts.map +1 -0
  9. package/dist/adapters/aws-lambda.js +168 -0
  10. package/dist/adapters/aws-lambda.js.map +1 -0
  11. package/dist/adapters/cloudflare.d.ts +94 -0
  12. package/dist/adapters/cloudflare.d.ts.map +1 -0
  13. package/dist/adapters/cloudflare.js +390 -0
  14. package/dist/adapters/cloudflare.js.map +1 -0
  15. package/dist/adapters/devtools.d.ts +4 -0
  16. package/dist/adapters/devtools.d.ts.map +1 -0
  17. package/dist/adapters/devtools.js +5 -0
  18. package/dist/adapters/devtools.js.map +1 -0
  19. package/dist/adapters/edge.d.ts +9 -0
  20. package/dist/adapters/edge.d.ts.map +1 -0
  21. package/dist/adapters/edge.js +53 -0
  22. package/dist/adapters/edge.js.map +1 -0
  23. package/dist/adapters/node.d.ts +26 -0
  24. package/dist/adapters/node.d.ts.map +1 -0
  25. package/dist/adapters/node.js +64 -0
  26. package/dist/adapters/node.js.map +1 -0
  27. package/dist/adapters/static.d.ts +10 -0
  28. package/dist/adapters/static.d.ts.map +1 -0
  29. package/dist/adapters/static.js +34 -0
  30. package/dist/adapters/static.js.map +1 -0
  31. package/dist/assets.d.ts +18 -0
  32. package/dist/assets.d.ts.map +1 -0
  33. package/dist/assets.js +67 -0
  34. package/dist/assets.js.map +1 -0
  35. package/dist/build.d.ts +36 -0
  36. package/dist/build.d.ts.map +1 -0
  37. package/dist/build.js +322 -0
  38. package/dist/build.js.map +1 -0
  39. package/dist/cache.d.ts +54 -0
  40. package/dist/cache.d.ts.map +1 -0
  41. package/dist/cache.js +221 -0
  42. package/dist/cache.js.map +1 -0
  43. package/dist/cli.d.ts +3 -0
  44. package/dist/cli.d.ts.map +1 -0
  45. package/dist/cli.js +37 -0
  46. package/dist/cli.js.map +1 -0
  47. package/dist/client.d.ts +105 -0
  48. package/dist/client.d.ts.map +1 -0
  49. package/dist/client.js +1268 -0
  50. package/dist/client.js.map +1 -0
  51. package/dist/config.d.ts +27 -0
  52. package/dist/config.d.ts.map +1 -0
  53. package/dist/config.js +44 -0
  54. package/dist/config.js.map +1 -0
  55. package/dist/cookies.d.ts +14 -0
  56. package/dist/cookies.d.ts.map +1 -0
  57. package/dist/cookies.js +69 -0
  58. package/dist/cookies.js.map +1 -0
  59. package/dist/csp.d.ts +6 -0
  60. package/dist/csp.d.ts.map +1 -0
  61. package/dist/csp.js +70 -0
  62. package/dist/csp.js.map +1 -0
  63. package/dist/dev-server.d.ts +16 -0
  64. package/dist/dev-server.d.ts.map +1 -0
  65. package/dist/dev-server.js +103 -0
  66. package/dist/dev-server.js.map +1 -0
  67. package/dist/http.d.ts +23 -0
  68. package/dist/http.d.ts.map +1 -0
  69. package/dist/http.js +106 -0
  70. package/dist/http.js.map +1 -0
  71. package/dist/i18n.d.ts +15 -0
  72. package/dist/i18n.d.ts.map +1 -0
  73. package/dist/i18n.js +61 -0
  74. package/dist/i18n.js.map +1 -0
  75. package/dist/import-policy.d.ts +30 -0
  76. package/dist/import-policy.d.ts.map +1 -0
  77. package/dist/import-policy.js +105 -0
  78. package/dist/import-policy.js.map +1 -0
  79. package/dist/index.d.ts +60 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +34 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/logger.d.ts +47 -0
  84. package/dist/logger.d.ts.map +1 -0
  85. package/dist/logger.js +60 -0
  86. package/dist/logger.js.map +1 -0
  87. package/dist/module-runner.d.ts +9 -0
  88. package/dist/module-runner.d.ts.map +1 -0
  89. package/dist/module-runner.js +112 -0
  90. package/dist/module-runner.js.map +1 -0
  91. package/dist/native-escape.d.ts +2 -0
  92. package/dist/native-escape.d.ts.map +1 -0
  93. package/dist/native-escape.js +43 -0
  94. package/dist/native-escape.js.map +1 -0
  95. package/dist/native-route-matcher.d.ts +5 -0
  96. package/dist/native-route-matcher.d.ts.map +1 -0
  97. package/dist/native-route-matcher.js +91 -0
  98. package/dist/native-route-matcher.js.map +1 -0
  99. package/dist/navigation.d.ts +25 -0
  100. package/dist/navigation.d.ts.map +1 -0
  101. package/dist/navigation.js +125 -0
  102. package/dist/navigation.js.map +1 -0
  103. package/dist/prerender-store.d.ts +37 -0
  104. package/dist/prerender-store.d.ts.map +1 -0
  105. package/dist/prerender-store.js +158 -0
  106. package/dist/prerender-store.js.map +1 -0
  107. package/dist/render.d.ts +26 -0
  108. package/dist/render.d.ts.map +1 -0
  109. package/dist/render.js +1688 -0
  110. package/dist/render.js.map +1 -0
  111. package/dist/route-path.d.ts +2 -0
  112. package/dist/route-path.d.ts.map +1 -0
  113. package/dist/route-path.js +5 -0
  114. package/dist/route-path.js.map +1 -0
  115. package/dist/route-source.d.ts +9 -0
  116. package/dist/route-source.d.ts.map +1 -0
  117. package/dist/route-source.js +44 -0
  118. package/dist/route-source.js.map +1 -0
  119. package/dist/routes.d.ts +38 -0
  120. package/dist/routes.d.ts.map +1 -0
  121. package/dist/routes.js +168 -0
  122. package/dist/routes.js.map +1 -0
  123. package/dist/serve.d.ts +63 -0
  124. package/dist/serve.d.ts.map +1 -0
  125. package/dist/serve.js +445 -0
  126. package/dist/serve.js.map +1 -0
  127. package/dist/session.d.ts +25 -0
  128. package/dist/session.d.ts.map +1 -0
  129. package/dist/session.js +104 -0
  130. package/dist/session.js.map +1 -0
  131. package/dist/vite-config.d.ts +8 -0
  132. package/dist/vite-config.d.ts.map +1 -0
  133. package/dist/vite-config.js +17 -0
  134. package/dist/vite-config.js.map +1 -0
  135. package/dist/vite.d.ts +25 -0
  136. package/dist/vite.d.ts.map +1 -0
  137. package/dist/vite.js +150 -0
  138. package/dist/vite.js.map +1 -0
  139. package/package.json +91 -0
package/dist/render.js ADDED
@@ -0,0 +1,1688 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { access, readFile } from "node:fs/promises";
4
+ import { dirname, join, relative, sep } from "node:path";
5
+ import { transform, } from "@reckona/mreact-compiler";
6
+ import { createQueryClient, dehydrate, __MREACT_QUERY_STATE_SCRIPT_ID, runWithQueryClient, } from "@reckona/mreact-query";
7
+ import { build as bundle } from "esbuild";
8
+ import { createStringSink, renderAsyncBoundary, renderOutOfOrderReorderScript, renderToReadableStream, } from "@reckona/mreact-server";
9
+ import { hydrationMarkerParts, inferClientRouteModule, withHydrationMarkers, withRouteMarkers, } from "./client.js";
10
+ import { assetPath } from "./assets.js";
11
+ import { escapeHtmlAttribute, escapeHtmlText as escapeHtml, } from "@reckona/mreact-shared/html-escape";
12
+ import { matchRoute, scanAppRoutes } from "./routes.js";
13
+ import { dispatchServerActionRequest, prepareRouteServerActions, serverActionCookie, } from "./actions.js";
14
+ import { beginRouteCacheContext, cachedRouteResponse, cacheRouteResponse, routeCacheKey, routeCachePolicyFromSource, } from "./cache.js";
15
+ import { importAppRouterFileModule, importAppRouterSourceModule } from "./module-runner.js";
16
+ import { contentSecurityPolicy } from "./csp.js";
17
+ import { htmlResponse } from "./http.js";
18
+ import { isNotFoundError, isRedirectError, rewriteLocation } from "./navigation.js";
19
+ import { createAppRouterImportPolicyPlugin } from "./import-policy.js";
20
+ import { hasLoaderExport, isStreamRouteSource, stripRouteModuleExports } from "./route-source.js";
21
+ const nativeEscapeTransform = {
22
+ batchImportName: "escapeHtmlBatch",
23
+ batchImportSource: "@reckona/mreact-router/internal/native-escape",
24
+ };
25
+ const authRuntimeStateKey = "__mreactAuthRuntimeState";
26
+ const authSessionScriptId = "__mreact_auth_session";
27
+ const serverTransformCache = new Map();
28
+ const serverSourceFileCache = new Map();
29
+ const routeSourceAnalysisCache = new Map();
30
+ const composedRouteMetadataCache = new Map();
31
+ const maxServerTransformCacheEntries = 512;
32
+ const maxServerSourceFileCacheEntries = 512;
33
+ const maxRouteSourceAnalysisCacheEntries = 512;
34
+ const maxComposedRouteMetadataCacheEntries = 512;
35
+ // Issue 086: per-shell prefix/suffix cache. Pure layouts (whose
36
+ // exported component takes zero arguments and therefore cannot
37
+ // depend on the request props) produce the same HTML for every
38
+ // request, so we cache the already-split { prefix, suffix } strings
39
+ // keyed by appDir + shellFile + serverModuleCacheVersion. Impure
40
+ // layouts (function.length > 0) are tagged "impure" so we skip the
41
+ // detection on subsequent requests but still render per-request.
42
+ //
43
+ // The cache is only active when a version is present (production
44
+ // builds); dev mode keeps the previous behaviour so reloads pick up
45
+ // edits without server restart.
46
+ const renderedShellCache = new Map();
47
+ const MAX_RENDERED_SHELL_CACHE_ENTRIES = 1024;
48
+ export async function renderAppRequest(options) {
49
+ const authStorage = authRequestStorage();
50
+ if (authStorage.getStore() === undefined) {
51
+ return authStorage.run({}, () => renderAppRequest(options));
52
+ }
53
+ const routes = options.routes ?? (await scanAppRoutes({ appDir: options.appDir }));
54
+ const url = new URL(options.request.url);
55
+ const middlewareResponse = options.skipMiddleware === true
56
+ ? undefined
57
+ : await runMiddleware({
58
+ appDir: options.appDir,
59
+ importPolicy: options.importPolicy,
60
+ request: options.request,
61
+ });
62
+ if (middlewareResponse !== undefined) {
63
+ const location = rewriteLocation(middlewareResponse);
64
+ if (location !== undefined) {
65
+ const rewriteUrl = new URL(location, options.request.url);
66
+ return renderAppRequest({
67
+ ...options,
68
+ request: new Request(rewriteUrl, options.request),
69
+ skipMiddleware: true,
70
+ });
71
+ }
72
+ return middlewareResponse;
73
+ }
74
+ if (url.pathname === "/_mreact/actions") {
75
+ return dispatchServerActionRequest({
76
+ appDir: options.appDir,
77
+ importPolicy: options.importPolicy,
78
+ request: options.request,
79
+ routeCache: options.routeCache,
80
+ ...(options.serverModuleCacheVersion === undefined
81
+ ? {}
82
+ : { serverActionCacheVersion: options.serverModuleCacheVersion }),
83
+ serverActions: options.serverActions,
84
+ });
85
+ }
86
+ const matched = options.routeMatcher?.match(url.pathname) ?? matchRoute(routes, url.pathname);
87
+ if (matched === undefined) {
88
+ const notFoundFile = await nearestBoundaryFileForPath({
89
+ appDir: options.appDir,
90
+ filename: "not-found.mreact.tsx",
91
+ pathname: url.pathname,
92
+ });
93
+ return renderSpecialRoute({
94
+ appDir: options.appDir,
95
+ assetBaseUrl: options.assetBaseUrl,
96
+ error: undefined,
97
+ request: options.request,
98
+ routeFile: notFoundFile,
99
+ serverModules: options.serverModules,
100
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
101
+ serverSourceFiles: options.serverSourceFiles,
102
+ status: 404,
103
+ textFallback: "Not Found",
104
+ });
105
+ }
106
+ const queryClient = options.queryClient ?? createQueryClient();
107
+ let recoveryRoute;
108
+ let routeCacheContext;
109
+ try {
110
+ if (matched.route.kind === "server") {
111
+ return await dispatchServerRoute(matched.route.file, options.request);
112
+ }
113
+ // Issue 080: page routes render HTML for GET / HEAD only. Other
114
+ // methods (PUT, PATCH, DELETE, PROPFIND, ...) get 405 with an
115
+ // Allow header so the response shape complies with RFC 9110 §9
116
+ // and so caching intermediaries do not cross-cache method results.
117
+ const method = options.request.method;
118
+ if (method === "OPTIONS") {
119
+ return new Response(null, {
120
+ status: 204,
121
+ headers: { allow: "GET, HEAD, OPTIONS" },
122
+ });
123
+ }
124
+ if (method !== "GET" && method !== "HEAD") {
125
+ return new Response("Method Not Allowed", {
126
+ status: 405,
127
+ headers: { allow: "GET, HEAD, OPTIONS" },
128
+ });
129
+ }
130
+ routeCacheContext = beginRouteCacheContext(options.routeCache);
131
+ const clientScript = options.clientScripts?.get(matched.route.path);
132
+ const originalCode = await readServerSourceFile(matched.route.file, options.serverModuleCacheVersion, options.serverSourceFiles);
133
+ const originalAnalysis = await analyzeRouteSource({
134
+ code: originalCode,
135
+ filename: matched.route.file,
136
+ routePath: matched.route.path,
137
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
138
+ });
139
+ const cachePolicy = originalAnalysis.cachePolicy;
140
+ const cacheKey = routeCacheKey(options.appDir, matched.route.path, url);
141
+ const mayUseRouteCache = cachePolicy === undefined
142
+ ? originalAnalysis.usesRuntimeCacheControl
143
+ : cachePolicy.revalidateSeconds !== 0;
144
+ const cachedResponse = !mayUseRouteCache
145
+ ? undefined
146
+ : await cachedRouteResponse({
147
+ cache: options.routeCache,
148
+ key: cacheKey,
149
+ });
150
+ if (cachedResponse !== undefined) {
151
+ return cachedResponse;
152
+ }
153
+ const preparedActions = await prepareRouteServerActions({
154
+ appDir: options.appDir,
155
+ code: originalCode,
156
+ pageFile: matched.route.file,
157
+ request: options.request,
158
+ });
159
+ const code = preparedActions.code;
160
+ const routeAnalysis = code === originalCode
161
+ ? originalAnalysis
162
+ : await analyzeRouteSource({
163
+ code,
164
+ filename: matched.route.file,
165
+ routePath: matched.route.path,
166
+ serverModuleCacheVersion: undefined,
167
+ });
168
+ const routeCode = routeAnalysis.routeCode;
169
+ const streamRoute = routeAnalysis.streamRoute;
170
+ const clientInference = routeAnalysis.clientInference;
171
+ const clientRoute = clientInference.client;
172
+ const dataPromise = routeAnalysis.hasLoader
173
+ ? loadRouteData({
174
+ code,
175
+ context: {
176
+ params: matched.params,
177
+ queryClient,
178
+ request: options.request,
179
+ },
180
+ appDir: options.appDir,
181
+ filename: matched.route.file,
182
+ importPolicy: options.importPolicy,
183
+ })
184
+ : undefined;
185
+ recoveryRoute = {
186
+ clientRoute,
187
+ props: {
188
+ params: matched.params,
189
+ request: { url: options.request.url },
190
+ },
191
+ routePath: matched.route.path,
192
+ script: clientScript,
193
+ };
194
+ if (streamRoute) {
195
+ const loadingFile = await nearestExistingBoundaryFileForPage({
196
+ appDir: options.appDir,
197
+ filename: "loading.mreact.tsx",
198
+ pageFile: matched.route.file,
199
+ });
200
+ const streamShellResponseHeaders = {
201
+ "content-type": "text/html; charset=utf-8",
202
+ "x-mreact-stream": "1",
203
+ };
204
+ if (loadingFile === undefined && !mayRenderOutOfOrderBoundary(routeCode)) {
205
+ const stringOutput = transformServerModule({
206
+ code: routeCode,
207
+ clientBoundaryImports: clientInference.clientBoundaryImports,
208
+ filename: matched.route.file,
209
+ serverModules: options.serverModules,
210
+ serverOutput: "string",
211
+ });
212
+ const stringFatalDiagnostics = stringOutput.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
213
+ if (stringFatalDiagnostics.length > 0) {
214
+ return new Response(stringFatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"), {
215
+ status: 500,
216
+ headers: { "content-type": "text/plain; charset=utf-8" },
217
+ });
218
+ }
219
+ const data = dataPromise === undefined ? undefined : await dataPromise;
220
+ const renderedPage = await runWithQueryClient(queryClient, () => runServerModuleWithSlots(stringOutput.code, {
221
+ data,
222
+ params: matched.params,
223
+ queryClient,
224
+ request: options.request,
225
+ }, matched.route.file, options.serverModules, options.serverModuleCacheVersion));
226
+ const pageHtml = renderedPage.html;
227
+ const pageHtmlForLayout = clientRoute
228
+ ? withHydrationMarkers({
229
+ assetBaseUrl: options.assetBaseUrl,
230
+ clientReferenceManifest: stringOutput.metadata.clientReferenceManifest,
231
+ html: pageHtml,
232
+ routePath: matched.route.path,
233
+ script: clientScript,
234
+ props: {
235
+ params: matched.params,
236
+ request: { url: options.request.url },
237
+ data,
238
+ },
239
+ })
240
+ : isNavigationRequest(options.request)
241
+ ? withRouteMarkers({
242
+ html: pageHtml,
243
+ routePath: matched.route.path,
244
+ })
245
+ : pageHtml;
246
+ let html = await runWithQueryClient(queryClient, () => applyLayouts({
247
+ appDir: options.appDir,
248
+ pageFile: matched.route.file,
249
+ html: pageHtmlForLayout,
250
+ props: {
251
+ data,
252
+ params: matched.params,
253
+ queryClient,
254
+ request: options.request,
255
+ },
256
+ slots: renderedPage.slots,
257
+ serverModules: options.serverModules,
258
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
259
+ serverSourceFiles: options.serverSourceFiles,
260
+ }));
261
+ const metadata = await loadComposedRouteMetadata({
262
+ appDir: options.appDir,
263
+ code: originalCode,
264
+ filename: matched.route.file,
265
+ importPolicy: options.importPolicy,
266
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
267
+ serverSourceFiles: options.serverSourceFiles,
268
+ });
269
+ html = injectHeadMetadata(html, metadata);
270
+ html = injectAuthSessionClaims(html, originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined);
271
+ html = injectQueryState(html, dehydrate(queryClient));
272
+ const headers = new Headers(responseHeadersForMetadata(metadata));
273
+ headers.set("x-mreact-stream", "1");
274
+ return withOptionalActionCookie(htmlResponse(`<!DOCTYPE html>${modulePreloadTags(clientRoute ? clientScript : undefined, options.assetBaseUrl)}${html}`, { headers }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
275
+ }
276
+ const output = transformServerModule({
277
+ code: routeCode,
278
+ clientBoundaryImports: clientInference.clientBoundaryImports,
279
+ filename: matched.route.file,
280
+ serverModules: options.serverModules,
281
+ serverOutput: "stream",
282
+ serverAwaitHydration: clientRoute,
283
+ });
284
+ const fatalDiagnostics = output.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
285
+ if (fatalDiagnostics.length > 0) {
286
+ return new Response(fatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"), {
287
+ status: 500,
288
+ headers: { "content-type": "text/plain; charset=utf-8" },
289
+ });
290
+ }
291
+ if (loadingFile !== undefined) {
292
+ const stream = await runServerStreamModuleWithLoading(output.code, {
293
+ appDir: options.appDir,
294
+ assetBaseUrl: options.assetBaseUrl,
295
+ clientRoute,
296
+ data: dataPromise ?? Promise.resolve(undefined),
297
+ loadingFile,
298
+ pageFile: matched.route.file,
299
+ params: matched.params,
300
+ queryClient,
301
+ request: options.request,
302
+ routePath: matched.route.path,
303
+ serverModules: options.serverModules,
304
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
305
+ serverSourceFiles: options.serverSourceFiles,
306
+ script: clientScript,
307
+ clientReferenceManifest: output.metadata.clientReferenceManifest,
308
+ });
309
+ return withOptionalActionCookie(new Response(stream, {
310
+ headers: streamShellResponseHeaders,
311
+ }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
312
+ }
313
+ const data = dataPromise === undefined ? undefined : await dataPromise;
314
+ const props = {
315
+ data,
316
+ params: matched.params,
317
+ queryClient,
318
+ request: options.request,
319
+ };
320
+ const stream = runServerStreamModule(output.code, {
321
+ appDir: options.appDir,
322
+ assetBaseUrl: options.assetBaseUrl,
323
+ pageFile: matched.route.file,
324
+ props,
325
+ routePath: matched.route.path,
326
+ serverModules: options.serverModules,
327
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
328
+ serverSourceFiles: options.serverSourceFiles,
329
+ clientRoute,
330
+ script: clientScript,
331
+ clientReferenceManifest: output.metadata.clientReferenceManifest,
332
+ });
333
+ return withOptionalActionCookie(new Response(stream, {
334
+ headers: streamShellResponseHeaders,
335
+ }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
336
+ }
337
+ const output = transformServerModule({
338
+ code: routeCode,
339
+ clientBoundaryImports: clientInference.clientBoundaryImports,
340
+ filename: matched.route.file,
341
+ serverModules: options.serverModules,
342
+ serverOutput: "string",
343
+ });
344
+ const fatalDiagnostics = output.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
345
+ if (fatalDiagnostics.length > 0) {
346
+ return new Response(fatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"), {
347
+ status: 500,
348
+ headers: { "content-type": "text/plain; charset=utf-8" },
349
+ });
350
+ }
351
+ const data = dataPromise === undefined ? undefined : await dataPromise;
352
+ const renderedPage = await runWithQueryClient(queryClient, () => runServerModuleWithSlots(output.code, {
353
+ data,
354
+ params: matched.params,
355
+ queryClient,
356
+ request: options.request,
357
+ }, matched.route.file, options.serverModules, options.serverModuleCacheVersion));
358
+ const pageHtml = renderedPage.html;
359
+ // Wrap the page (not the full document) with the hydration marker so
360
+ // the marker sits inside <body>, not around <html>. Wrapping <html>
361
+ // forces the browser HTML parser to strip the wrappers and promote
362
+ // <head> / <body> children up to the marker, which flattens the
363
+ // layout into the marker and breaks the hydration target lookup.
364
+ const pageHtmlForLayout = clientRoute
365
+ ? withHydrationMarkers({
366
+ assetBaseUrl: options.assetBaseUrl,
367
+ clientReferenceManifest: output.metadata.clientReferenceManifest,
368
+ html: pageHtml,
369
+ routePath: matched.route.path,
370
+ script: clientScript,
371
+ props: {
372
+ params: matched.params,
373
+ request: { url: options.request.url },
374
+ data,
375
+ },
376
+ })
377
+ : isNavigationRequest(options.request)
378
+ ? withRouteMarkers({
379
+ html: pageHtml,
380
+ routePath: matched.route.path,
381
+ })
382
+ : pageHtml;
383
+ let html = await runWithQueryClient(queryClient, () => applyLayouts({
384
+ appDir: options.appDir,
385
+ pageFile: matched.route.file,
386
+ html: pageHtmlForLayout,
387
+ props: {
388
+ data,
389
+ params: matched.params,
390
+ queryClient,
391
+ request: options.request,
392
+ },
393
+ slots: renderedPage.slots,
394
+ serverModules: options.serverModules,
395
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
396
+ serverSourceFiles: options.serverSourceFiles,
397
+ }));
398
+ const metadata = await loadComposedRouteMetadata({
399
+ appDir: options.appDir,
400
+ code: originalCode,
401
+ filename: matched.route.file,
402
+ importPolicy: options.importPolicy,
403
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
404
+ serverSourceFiles: options.serverSourceFiles,
405
+ });
406
+ html = injectHeadMetadata(html, metadata);
407
+ html = injectAuthSessionClaims(html, originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined);
408
+ html = injectQueryState(html, dehydrate(queryClient));
409
+ const response = withOptionalActionCookie(htmlResponse(`<!DOCTYPE html>${modulePreloadTags(clientRoute ? clientScript : undefined, options.assetBaseUrl)}${html}`, {
410
+ headers: responseHeadersForMetadata(metadata),
411
+ }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
412
+ const effectiveCachePolicy = cachePolicy ?? routeCacheContext.cachePolicy;
413
+ return preparedActions.hasFormActions
414
+ ? withRouteCacheHeader(response, effectiveCachePolicy)
415
+ : await cacheRouteResponse({
416
+ key: cacheKey,
417
+ cache: options.routeCache,
418
+ path: matched.route.path,
419
+ policy: effectiveCachePolicy,
420
+ response,
421
+ });
422
+ }
423
+ catch (error) {
424
+ if (isRedirectError(error)) {
425
+ return new Response(null, {
426
+ headers: { location: error.location },
427
+ status: error.status,
428
+ });
429
+ }
430
+ if (isNotFoundError(error)) {
431
+ const notFoundFile = await nearestBoundaryFileForPage({
432
+ appDir: options.appDir,
433
+ filename: "not-found.mreact.tsx",
434
+ pageFile: matched.route.file,
435
+ });
436
+ return renderSpecialRoute({
437
+ appDir: options.appDir,
438
+ assetBaseUrl: options.assetBaseUrl,
439
+ error: undefined,
440
+ request: options.request,
441
+ routeFile: notFoundFile,
442
+ serverModules: options.serverModules,
443
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
444
+ serverSourceFiles: options.serverSourceFiles,
445
+ navigation: recoveryRoute,
446
+ status: 404,
447
+ textFallback: "Not Found",
448
+ });
449
+ }
450
+ const errorFile = await nearestBoundaryFileForPage({
451
+ appDir: options.appDir,
452
+ filename: "error.mreact.tsx",
453
+ pageFile: matched.route.file,
454
+ });
455
+ return renderSpecialRoute({
456
+ appDir: options.appDir,
457
+ assetBaseUrl: options.assetBaseUrl,
458
+ error,
459
+ request: options.request,
460
+ routeFile: errorFile,
461
+ serverModules: options.serverModules,
462
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
463
+ serverSourceFiles: options.serverSourceFiles,
464
+ navigation: recoveryRoute,
465
+ status: 500,
466
+ textFallback: error instanceof Error ? error.message : String(error),
467
+ });
468
+ }
469
+ finally {
470
+ await routeCacheContext?.dispose();
471
+ }
472
+ }
473
+ function withOptionalActionCookie(response, csrfToken, csrfTokenIsNew) {
474
+ // Only re-issue Set-Cookie when this render minted the token. Reusing
475
+ // an incoming cookie value (Issue 070) means no Set-Cookie is needed
476
+ // and avoids stomping on a concurrent tab's hidden form input.
477
+ if (csrfToken !== undefined && csrfTokenIsNew) {
478
+ response.headers.append("set-cookie", serverActionCookie(csrfToken));
479
+ }
480
+ return response;
481
+ }
482
+ function modulePreloadTags(script, assetBaseUrl) {
483
+ return script === undefined
484
+ ? ""
485
+ : `<link rel="modulepreload" href="${escapeHtmlAttribute(assetPath(script, assetBaseUrl ?? "/_mreact/client/"))}">`;
486
+ }
487
+ function isNavigationRequest(request) {
488
+ return request.headers.get("x-mreact-navigation") === "1";
489
+ }
490
+ async function nearestBoundaryFileForPage(options) {
491
+ const relativeDir = relative(options.appDir, dirname(options.pageFile));
492
+ const parts = relativeDir === "" ? [] : relativeDir.split(sep);
493
+ return nearestBoundaryFileFromParts({
494
+ appDir: options.appDir,
495
+ filename: options.filename,
496
+ parts,
497
+ });
498
+ }
499
+ async function nearestExistingBoundaryFileForPage(options) {
500
+ const relativeDir = relative(options.appDir, dirname(options.pageFile));
501
+ const parts = relativeDir === "" ? [] : relativeDir.split(sep);
502
+ return nearestExistingBoundaryFileFromParts({
503
+ appDir: options.appDir,
504
+ filename: options.filename,
505
+ parts,
506
+ });
507
+ }
508
+ async function nearestBoundaryFileForPath(options) {
509
+ const parts = options.pathname
510
+ .replace(/^\/+|\/+$/g, "")
511
+ .split("/")
512
+ .filter((part) => part.length > 0);
513
+ return nearestBoundaryFileFromParts({
514
+ appDir: options.appDir,
515
+ filename: options.filename,
516
+ parts,
517
+ });
518
+ }
519
+ async function nearestBoundaryFileFromParts(options) {
520
+ for (let count = options.parts.length; count >= 0; count -= 1) {
521
+ for (const filename of boundaryFilenameCandidates(options.filename)) {
522
+ const candidate = join(options.appDir, ...options.parts.slice(0, count), filename);
523
+ try {
524
+ await access(candidate);
525
+ return candidate;
526
+ }
527
+ catch {
528
+ // Keep walking toward the root boundary.
529
+ }
530
+ }
531
+ }
532
+ return join(options.appDir, boundaryFilenameCandidates(options.filename)[0] ?? options.filename);
533
+ }
534
+ async function nearestExistingBoundaryFileFromParts(options) {
535
+ for (let count = options.parts.length; count >= 0; count -= 1) {
536
+ for (const filename of boundaryFilenameCandidates(options.filename)) {
537
+ const candidate = join(options.appDir, ...options.parts.slice(0, count), filename);
538
+ try {
539
+ await access(candidate);
540
+ return candidate;
541
+ }
542
+ catch {
543
+ // Keep walking toward the root boundary.
544
+ }
545
+ }
546
+ }
547
+ return undefined;
548
+ }
549
+ function boundaryFilenameCandidates(filename) {
550
+ if (!filename.endsWith(".mreact.tsx")) {
551
+ return [filename];
552
+ }
553
+ const standardFilename = filename.replace(".mreact.tsx", ".tsx");
554
+ return [standardFilename, filename];
555
+ }
556
+ async function renderSpecialRoute(options) {
557
+ try {
558
+ await access(options.routeFile);
559
+ }
560
+ catch {
561
+ return new Response(options.textFallback, { status: options.status });
562
+ }
563
+ const props = {
564
+ data: undefined,
565
+ error: normalizeErrorForProps(options.error),
566
+ params: {},
567
+ queryClient: createQueryClient(),
568
+ request: options.request,
569
+ };
570
+ const pageHtml = await renderServerFileToHtml(options.routeFile, props, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles);
571
+ const pageHtmlForLayout = options.navigation?.clientRoute === true
572
+ ? withHydrationMarkers({
573
+ assetBaseUrl: options.assetBaseUrl,
574
+ clientReferenceManifest: undefined,
575
+ html: pageHtml,
576
+ props: options.navigation.props,
577
+ routePath: options.navigation.routePath,
578
+ script: options.navigation.script,
579
+ })
580
+ : pageHtml;
581
+ const html = await applyLayouts({
582
+ appDir: options.appDir,
583
+ pageFile: options.routeFile,
584
+ html: pageHtmlForLayout,
585
+ props,
586
+ serverModules: options.serverModules,
587
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
588
+ serverSourceFiles: options.serverSourceFiles,
589
+ });
590
+ return new Response(`<!DOCTYPE html>${modulePreloadTags(options.navigation?.clientRoute === true ? options.navigation.script : undefined, options.assetBaseUrl)}${html}`, {
591
+ headers: { "content-type": "text/html; charset=utf-8" },
592
+ status: options.status,
593
+ });
594
+ }
595
+ async function renderServerFileToHtml(file, props, serverModules, serverModuleCacheVersion, serverSourceFiles) {
596
+ const code = await readServerSourceFile(file, serverModuleCacheVersion, serverSourceFiles);
597
+ const output = transformServerModule({
598
+ code,
599
+ filename: file,
600
+ serverModules,
601
+ serverOutput: "string",
602
+ });
603
+ const fatalDiagnostics = output.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
604
+ if (fatalDiagnostics.length > 0) {
605
+ throw new Error(fatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"));
606
+ }
607
+ return runServerModule(output.code, props, file, serverModules, serverModuleCacheVersion);
608
+ }
609
+ function normalizeErrorForProps(error) {
610
+ if (error instanceof Error) {
611
+ return { message: error.message };
612
+ }
613
+ return { message: String(error) };
614
+ }
615
+ async function dispatchServerRoute(file, request) {
616
+ const module = await importAppRouterFileModule(file);
617
+ const handler = module[request.method] ?? module.ALL ?? module.default;
618
+ if (typeof handler !== "function") {
619
+ return new Response("Method Not Allowed", { status: 405 });
620
+ }
621
+ const response = await handler(request);
622
+ return response instanceof Response
623
+ ? response
624
+ : new Response("Invalid route response", { status: 500 });
625
+ }
626
+ async function runMiddleware(options) {
627
+ const candidates = [
628
+ join(options.appDir, "middleware.ts"),
629
+ join(options.appDir, "middleware.mreact.ts"),
630
+ ];
631
+ for (const file of candidates) {
632
+ try {
633
+ await access(file);
634
+ }
635
+ catch {
636
+ continue;
637
+ }
638
+ const module = await loadMiddlewareModule({
639
+ appDir: options.appDir,
640
+ file,
641
+ importPolicy: options.importPolicy,
642
+ });
643
+ if (!middlewareMatches(module.config, new URL(options.request.url).pathname)) {
644
+ return undefined;
645
+ }
646
+ const middleware = module.middleware ?? module.default;
647
+ if (typeof middleware !== "function") {
648
+ return undefined;
649
+ }
650
+ try {
651
+ const response = await middleware(options.request);
652
+ return response instanceof Response ? response : undefined;
653
+ }
654
+ catch (error) {
655
+ if (isRedirectError(error)) {
656
+ return new Response(null, {
657
+ headers: { location: error.location },
658
+ status: error.status,
659
+ });
660
+ }
661
+ if (isNotFoundError(error)) {
662
+ return new Response("Not Found", { status: 404 });
663
+ }
664
+ throw error;
665
+ }
666
+ }
667
+ return undefined;
668
+ }
669
+ async function loadMiddlewareModule(options) {
670
+ const code = await readFile(options.file, "utf8");
671
+ const output = await bundle({
672
+ bundle: true,
673
+ format: "esm",
674
+ logLevel: "silent",
675
+ platform: "node",
676
+ plugins: [
677
+ createAppRouterImportPolicyPlugin({
678
+ appDir: options.appDir,
679
+ importPolicy: options.importPolicy,
680
+ label: "Middleware",
681
+ }),
682
+ ],
683
+ write: false,
684
+ jsx: "transform",
685
+ jsxFactory: "__mreact_jsx",
686
+ jsxFragment: "__mreact_fragment",
687
+ stdin: {
688
+ contents: code,
689
+ loader: "ts",
690
+ resolveDir: dirname(options.file),
691
+ sourcefile: options.file,
692
+ },
693
+ });
694
+ const compiled = output.outputFiles[0]?.text;
695
+ if (compiled === undefined) {
696
+ throw new Error(`Failed to compile middleware for ${options.file}.`);
697
+ }
698
+ return importAppRouterSourceModule({
699
+ code: compiled,
700
+ label: `middleware:${options.file}`,
701
+ });
702
+ }
703
+ function middlewareMatches(config, pathname) {
704
+ const matcher = config?.matcher;
705
+ if (matcher === undefined) {
706
+ return true;
707
+ }
708
+ if (matcher instanceof RegExp) {
709
+ return matcher.test(pathname);
710
+ }
711
+ if (Array.isArray(matcher)) {
712
+ return matcher.some((item) => middlewarePatternMatches(item, pathname));
713
+ }
714
+ return typeof matcher === "string" && middlewarePatternMatches(matcher, pathname);
715
+ }
716
+ function middlewarePatternMatches(pattern, pathname) {
717
+ if (pattern === pathname) {
718
+ return true;
719
+ }
720
+ if (pattern.endsWith("/:path*")) {
721
+ const prefix = pattern.slice(0, -"/:path*".length);
722
+ return pathname === prefix || pathname.startsWith(`${prefix}/`);
723
+ }
724
+ if (pattern.endsWith("*")) {
725
+ const prefix = pattern.slice(0, -1);
726
+ return pathname.startsWith(prefix);
727
+ }
728
+ return false;
729
+ }
730
+ function transformServerModule(options) {
731
+ const sourceHash = memoizedHashText(options.code);
732
+ const artifact = options.serverModules?.get(options.filename)?.[options.serverOutput];
733
+ if (artifact !== undefined &&
734
+ artifact.sourceHash === sourceHash &&
735
+ options.serverAwaitHydration !== true) {
736
+ return {
737
+ code: artifact.code,
738
+ diagnostics: [],
739
+ map: null,
740
+ metadata: artifact.metadata ?? {
741
+ compiler: {
742
+ frontend: "oxc",
743
+ typescriptFallback: false,
744
+ },
745
+ components: [],
746
+ filename: options.filename,
747
+ imports: [],
748
+ serverOutput: options.serverOutput,
749
+ target: "server",
750
+ },
751
+ };
752
+ }
753
+ const awaitHydrationKey = options.serverAwaitHydration === true ? "1" : "0";
754
+ const key = `${options.filename}\0${options.serverOutput}\0${sourceHash}\0${awaitHydrationKey}`;
755
+ const cached = serverTransformCache.get(key);
756
+ if (cached !== undefined) {
757
+ return cached;
758
+ }
759
+ const output = transform({
760
+ code: options.code,
761
+ ...(options.clientBoundaryImports === undefined
762
+ ? {}
763
+ : { clientBoundaryImports: options.clientBoundaryImports }),
764
+ dev: true,
765
+ filename: options.filename,
766
+ serverEscape: nativeEscapeTransform,
767
+ serverOutput: options.serverOutput,
768
+ target: "server",
769
+ ...(options.serverAwaitHydration === true ? { serverAwaitHydration: true } : {}),
770
+ });
771
+ setBoundedCacheEntry(serverTransformCache, key, output, maxServerTransformCacheEntries);
772
+ return output;
773
+ }
774
+ async function analyzeRouteSource(options) {
775
+ const sourceHash = memoizedHashText(options.code);
776
+ const cacheKey = `${options.serverModuleCacheVersion ?? "dev"}\0${options.filename}\0${sourceHash}`;
777
+ const cached = routeSourceAnalysisCache.get(cacheKey);
778
+ if (cached !== undefined) {
779
+ return cached;
780
+ }
781
+ const pending = analyzeRouteSourceUncached(options).catch((error) => {
782
+ routeSourceAnalysisCache.delete(cacheKey);
783
+ throw error;
784
+ });
785
+ setBoundedCacheEntry(routeSourceAnalysisCache, cacheKey, pending, maxRouteSourceAnalysisCacheEntries);
786
+ return pending;
787
+ }
788
+ async function analyzeRouteSourceUncached(options) {
789
+ const routeCode = stripRouteModuleExports(options.code);
790
+ const clientInference = await inferClientRouteModule({
791
+ code: routeCode,
792
+ filename: options.filename,
793
+ routePath: options.routePath,
794
+ });
795
+ return {
796
+ authIncludesClaims: authIncludesClaims(options.code),
797
+ cachePolicy: routeCachePolicyFromSource(options.code),
798
+ clientInference,
799
+ hasLoader: hasLoaderExport(options.code),
800
+ routeCode,
801
+ streamRoute: isStreamRouteSource(options.code),
802
+ usesRuntimeCacheControl: usesRuntimeCacheControl(options.code),
803
+ };
804
+ }
805
+ // Per-request hashText (SHA-256) is one of the hot path's dominant
806
+ // costs. Cache hashes for `code` strings we have already seen this
807
+ // process (common case: the prepared code is identical across requests
808
+ // when the source file is unchanged).
809
+ const codeHashCache = new Map();
810
+ const MAX_CODE_HASH_ENTRIES = 256;
811
+ function memoizedHashText(code) {
812
+ const cached = codeHashCache.get(code);
813
+ if (cached !== undefined) {
814
+ return cached;
815
+ }
816
+ const hash = hashText(code);
817
+ if (codeHashCache.size >= MAX_CODE_HASH_ENTRIES) {
818
+ // Simple LRU eviction: drop the oldest entry (Map keeps insertion order).
819
+ const oldestKey = codeHashCache.keys().next().value;
820
+ if (oldestKey !== undefined) {
821
+ codeHashCache.delete(oldestKey);
822
+ }
823
+ }
824
+ codeHashCache.set(code, hash);
825
+ return hash;
826
+ }
827
+ async function runServerModule(code, props, sourcefile, serverModules, serverModuleCacheVersion) {
828
+ const component = await loadServerComponent(code, sourcefile, serverModules, serverModuleCacheVersion);
829
+ return component(props);
830
+ }
831
+ async function runServerModuleWithSlots(code, props, sourcefile, serverModules, serverModuleCacheVersion) {
832
+ const module = await loadServerModule(code, sourcefile, serverModules, serverModuleCacheVersion);
833
+ const component = selectServerComponent(module);
834
+ return {
835
+ html: await component(props),
836
+ slots: await renderRouteSlots(module.slots, props),
837
+ };
838
+ }
839
+ async function loadServerModule(code, sourcefile, serverModules, serverModuleCacheVersion) {
840
+ const artifact = serverModules?.get(sourcefile)?.string;
841
+ const codeHash = memoizedHashText(code);
842
+ const moduleCode = artifact !== undefined && artifact.sourceHash === codeHash ? artifact.code : code;
843
+ const cacheKey = serverModuleCacheVersion === undefined
844
+ ? undefined
845
+ : `server-component:${serverModuleCacheVersion}:${sourcefile}:${moduleCode === code ? codeHash : memoizedHashText(moduleCode)}`;
846
+ return await importAppRouterSourceModule({
847
+ cacheKey,
848
+ code: moduleCode,
849
+ label: `server-component:${sourcefile}`,
850
+ resolveDir: dirname(sourcefile),
851
+ sourcefile,
852
+ });
853
+ }
854
+ async function loadServerComponent(code, sourcefile, serverModules, serverModuleCacheVersion) {
855
+ const module = await loadServerModule(code, sourcefile, serverModules, serverModuleCacheVersion);
856
+ return selectServerComponent(module);
857
+ }
858
+ function selectServerComponent(module) {
859
+ const component = module.default ?? module.App ?? Object.values(module)[0];
860
+ if (typeof component !== "function") {
861
+ throw new Error("No page component export was found.");
862
+ }
863
+ return component;
864
+ }
865
+ async function renderRouteSlots(slots, props) {
866
+ if (slots === undefined) {
867
+ return {};
868
+ }
869
+ const rendered = {};
870
+ for (const [name, value] of Object.entries(slots)) {
871
+ rendered[name] = typeof value === "function" ? await value(props) : value;
872
+ }
873
+ return rendered;
874
+ }
875
+ function runServerStreamModule(code, options) {
876
+ return renderToReadableStream(async (sink) => {
877
+ const slots = await renderServerStreamSlots(code, {
878
+ pageFile: options.pageFile,
879
+ props: options.props,
880
+ serverModules: options.serverModules,
881
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
882
+ });
883
+ const layoutShells = await layoutShellsForPage(options.appDir, options.pageFile, options.props, slots, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles);
884
+ const marker = options.clientRoute
885
+ ? hydrationMarkerParts({
886
+ assetBaseUrl: options.assetBaseUrl,
887
+ clientReferenceManifest: options.clientReferenceManifest,
888
+ routePath: options.routePath,
889
+ script: options.script,
890
+ props: {
891
+ params: options.props.params,
892
+ request: { url: options.props.request.url },
893
+ data: options.props.data,
894
+ },
895
+ })
896
+ : undefined;
897
+ sink.append("<!DOCTYPE html>");
898
+ sink.append(modulePreloadTags(options.clientRoute ? options.script : undefined, options.assetBaseUrl));
899
+ for (const shell of layoutShells) {
900
+ sink.append(shell.prefix);
901
+ }
902
+ sink.append(marker?.prefix ?? "");
903
+ await appendServerStreamModule(code, sink, options.props, options.pageFile, options.serverModules, options.serverModuleCacheVersion);
904
+ sink.append(marker?.suffix ?? "");
905
+ for (const shell of [...layoutShells].reverse()) {
906
+ sink.append(shell.suffix);
907
+ }
908
+ if (hasOutOfOrderBoundary(code)) {
909
+ renderOutOfOrderReorderScript(sink);
910
+ }
911
+ });
912
+ }
913
+ function hasOutOfOrderBoundary(code) {
914
+ return code.includes("renderOutOfOrderBoundary");
915
+ }
916
+ function mayRenderOutOfOrderBoundary(code) {
917
+ return (code.includes("<Await") || code.includes("Await(") || code.includes("renderOutOfOrderBoundary"));
918
+ }
919
+ async function runServerStreamModuleWithLoading(code, options) {
920
+ const loadingProps = {
921
+ data: undefined,
922
+ params: options.params,
923
+ queryClient: options.queryClient,
924
+ request: options.request,
925
+ };
926
+ const layoutShells = await layoutShellsForPage(options.appDir, options.pageFile, loadingProps, {}, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles);
927
+ const loadingHtml = await renderServerFileToHtml(options.loadingFile, loadingProps, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles);
928
+ const marker = options.clientRoute
929
+ ? hydrationMarkerParts({
930
+ assetBaseUrl: options.assetBaseUrl,
931
+ clientReferenceManifest: options.clientReferenceManifest,
932
+ routePath: options.routePath,
933
+ script: options.script,
934
+ props: {
935
+ params: options.params,
936
+ request: { url: options.request.url },
937
+ },
938
+ })
939
+ : undefined;
940
+ return renderToReadableStream((sink) => {
941
+ sink.append("<!DOCTYPE html>");
942
+ sink.append(modulePreloadTags(options.clientRoute ? options.script : undefined, options.assetBaseUrl));
943
+ for (const shell of layoutShells) {
944
+ sink.append(shell.prefix);
945
+ }
946
+ sink.append(marker?.prefix ?? "");
947
+ renderVisibleOutOfOrderBoundary(sink, "mreact-route", options.data, async (boundarySink, data) => {
948
+ await appendServerStreamModule(code, boundarySink, {
949
+ data,
950
+ params: options.params,
951
+ queryClient: options.queryClient,
952
+ request: options.request,
953
+ }, options.pageFile, options.serverModules, options.serverModuleCacheVersion);
954
+ }, {
955
+ placeholder(boundarySink) {
956
+ boundarySink.append(loadingHtml);
957
+ },
958
+ });
959
+ sink.append(marker?.suffix ?? "");
960
+ for (const shell of [...layoutShells].reverse()) {
961
+ sink.append(shell.suffix);
962
+ }
963
+ renderOutOfOrderReorderScript(sink);
964
+ });
965
+ }
966
+ function renderVisibleOutOfOrderBoundary(sink, id, value, render, options = {}) {
967
+ const placeholderSink = createStringSink();
968
+ void options.placeholder?.(placeholderSink);
969
+ sink.append(`<span data-mreact-oob-placeholder="${escapeHtmlAttribute(id)}">${placeholderSink.toString()}</span>`);
970
+ const task = renderVisibleOutOfOrderFragment(sink, id, value, render, options);
971
+ if (sink.defer === undefined) {
972
+ void task;
973
+ return;
974
+ }
975
+ sink.defer(task);
976
+ }
977
+ async function renderVisibleOutOfOrderFragment(sink, id, value, render, options) {
978
+ const fragmentSink = createStringSink();
979
+ await renderAsyncBoundary(fragmentSink, value, render, options.catch === undefined ? {} : { catch: options.catch });
980
+ sink.append(`<template data-mreact-oob-fragment="${escapeHtmlAttribute(id)}">${fragmentSink.toString()}</template>`);
981
+ }
982
+ async function appendServerStreamModule(code, sink, props, sourcefile, serverModules, serverModuleCacheVersion) {
983
+ const module = await loadServerStreamModule(code, sourcefile, serverModules, serverModuleCacheVersion);
984
+ const component = selectStreamComponent(module);
985
+ await component(sink, props);
986
+ }
987
+ async function renderServerStreamSlots(code, options) {
988
+ if (!hasRouteSlotsExport(code)) {
989
+ return {};
990
+ }
991
+ const module = await loadServerStreamModule(code, options.pageFile, options.serverModules, options.serverModuleCacheVersion);
992
+ if (module.slots === undefined) {
993
+ return {};
994
+ }
995
+ const rendered = {};
996
+ for (const [name, value] of Object.entries(module.slots)) {
997
+ if (typeof value !== "function") {
998
+ rendered[name] = value;
999
+ continue;
1000
+ }
1001
+ const sink = createStringSink();
1002
+ await value(sink, options.props);
1003
+ await sink.drain();
1004
+ rendered[name] = sink.toString();
1005
+ }
1006
+ return rendered;
1007
+ }
1008
+ function hasRouteSlotsExport(code) {
1009
+ return /^\s*export\s+const\s+slots\s*=/m.test(code);
1010
+ }
1011
+ async function loadServerStreamModule(code, sourcefile, serverModules, serverModuleCacheVersion) {
1012
+ const artifactCode = serverModules?.get(sourcefile)?.stream;
1013
+ const codeHash = memoizedHashText(code);
1014
+ const moduleCode = artifactCode !== undefined && artifactCode.sourceHash === codeHash ? artifactCode.code : code;
1015
+ const cacheKey = serverModuleCacheVersion === undefined
1016
+ ? undefined
1017
+ : `server-stream-component:${serverModuleCacheVersion}:${sourcefile}:${moduleCode === code ? codeHash : memoizedHashText(moduleCode)}`;
1018
+ return await importAppRouterSourceModule({
1019
+ cacheKey,
1020
+ code: moduleCode,
1021
+ label: `server-stream-component:${sourcefile}`,
1022
+ resolveDir: dirname(sourcefile),
1023
+ sourcefile,
1024
+ });
1025
+ }
1026
+ function selectStreamComponent(module) {
1027
+ const component = module.default ?? module.App ?? Object.values(module)[0];
1028
+ if (typeof component !== "function") {
1029
+ throw new Error("No page component export was found.");
1030
+ }
1031
+ return component;
1032
+ }
1033
+ async function applyLayouts(options) {
1034
+ const layoutFiles = await shellFilesForPage(options.appDir, options.pageFile, options.serverModuleCacheVersion);
1035
+ let html = options.html;
1036
+ const slotContext = createSlotRenderContext(options.slots);
1037
+ for (const shell of layoutFiles.reverse()) {
1038
+ const rendered = await renderShellPrefixSuffix(options.appDir, shell, options.props, slotContext, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles);
1039
+ html = `${rendered.prefix}${html}${rendered.suffix}`;
1040
+ }
1041
+ warnUnconsumedRouteSlots({
1042
+ appDir: options.appDir,
1043
+ pageFile: options.pageFile,
1044
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1045
+ slotContext,
1046
+ });
1047
+ return html;
1048
+ }
1049
+ async function layoutShellsForPage(appDir, pageFile, props, slots, serverModules, serverModuleCacheVersion, serverSourceFiles) {
1050
+ const layoutFiles = await shellFilesForPage(appDir, pageFile, serverModuleCacheVersion);
1051
+ const shells = [];
1052
+ const slotContext = createSlotRenderContext(slots);
1053
+ for (const shell of layoutFiles) {
1054
+ shells.push(await renderShellPrefixSuffix(appDir, shell, props, slotContext, serverModules, serverModuleCacheVersion, serverSourceFiles));
1055
+ }
1056
+ warnUnconsumedRouteSlots({
1057
+ appDir,
1058
+ pageFile,
1059
+ serverModuleCacheVersion,
1060
+ slotContext,
1061
+ });
1062
+ return shells;
1063
+ }
1064
+ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, serverModules, serverModuleCacheVersion, serverSourceFiles) {
1065
+ const hasNamedSlots = Object.keys(slotContext.namedSlots).length > 0;
1066
+ const cacheKey = serverModuleCacheVersion === undefined || hasNamedSlots || shell.kind === "template"
1067
+ ? undefined
1068
+ : `${appDir}\0${shell.file}\0${serverModuleCacheVersion}`;
1069
+ if (cacheKey !== undefined) {
1070
+ const cached = renderedShellCache.get(cacheKey);
1071
+ if (cached !== undefined && cached !== "impure") {
1072
+ return cached;
1073
+ }
1074
+ }
1075
+ const code = await readServerSourceFile(shell.file, serverModuleCacheVersion, serverSourceFiles);
1076
+ const output = transformServerModule({
1077
+ code,
1078
+ filename: shell.file,
1079
+ serverModules,
1080
+ serverOutput: "string",
1081
+ });
1082
+ const fatalDiagnostics = output.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
1083
+ if (fatalDiagnostics.length > 0) {
1084
+ throw new Error(fatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"));
1085
+ }
1086
+ const component = await loadServerComponent(output.code, shell.file, serverModules, serverModuleCacheVersion);
1087
+ const rendered = splitLayoutSlot(markShellBoundary(await component(props), shell), slotContext);
1088
+ const cached = cacheKey !== undefined ? renderedShellCache.get(cacheKey) : undefined;
1089
+ // Detect purity: a zero-arg component cannot depend on props. The
1090
+ // markShellBoundary + splitLayoutSlot output is then constant for
1091
+ // the (appDir, shellFile, version) tuple. We only set the cache
1092
+ // entry on the first request that observes the function arity; on
1093
+ // an "impure" tag we never overwrite it.
1094
+ if (cacheKey !== undefined && cached !== "impure") {
1095
+ if (component.length === 0) {
1096
+ if (renderedShellCache.size >= MAX_RENDERED_SHELL_CACHE_ENTRIES) {
1097
+ const oldestKey = renderedShellCache.keys().next().value;
1098
+ if (oldestKey !== undefined) {
1099
+ renderedShellCache.delete(oldestKey);
1100
+ }
1101
+ }
1102
+ renderedShellCache.set(cacheKey, rendered);
1103
+ }
1104
+ else {
1105
+ // Impure — stamp the cache so subsequent lookups short-circuit
1106
+ // without re-checking arity. We still run the per-request
1107
+ // render path above so the props are honoured.
1108
+ renderedShellCache.set(cacheKey, "impure");
1109
+ }
1110
+ }
1111
+ return rendered;
1112
+ }
1113
+ function splitLayoutSlot(layoutHtml, slotContext = createSlotRenderContext()) {
1114
+ const html = replaceNamedLayoutSlots(layoutHtml, slotContext);
1115
+ const match = findDefaultLayoutSlot(html);
1116
+ if (match === null) {
1117
+ return { prefix: html, suffix: "" };
1118
+ }
1119
+ return {
1120
+ prefix: html.slice(0, match.index),
1121
+ suffix: html.slice(match.index + match[0].length),
1122
+ };
1123
+ }
1124
+ // Layout/template files for a given page do not change during a server's
1125
+ // lifetime in production. Each cache miss costs up to N×4 filesystem
1126
+ // `access()` syscalls (~5-10μs each on a fast SSD), making this one of
1127
+ // the largest fixed costs in `renderBuiltAppRequest` for a minimal page.
1128
+ //
1129
+ // We cache by `appDir + pageFile + serverModuleCacheVersion` so the cache
1130
+ // is only active when a server-module manifest version is available
1131
+ // (= production builds). In dev mode the version is `undefined`, so we
1132
+ // skip the cache and pick up newly added layout / template files on the
1133
+ // next request.
1134
+ const shellFilesCache = new Map();
1135
+ const MAX_SHELL_FILES_CACHE_ENTRIES = 1024;
1136
+ async function shellFilesForPage(appDir, pageFile, serverModuleCacheVersion) {
1137
+ const cacheKey = serverModuleCacheVersion === undefined
1138
+ ? undefined
1139
+ : `${appDir}\0${pageFile}\0${serverModuleCacheVersion}`;
1140
+ if (cacheKey !== undefined) {
1141
+ const cached = shellFilesCache.get(cacheKey);
1142
+ if (cached !== undefined) {
1143
+ return cached;
1144
+ }
1145
+ }
1146
+ const relativeDir = relative(appDir, dirname(pageFile));
1147
+ const parts = relativeDir === "" ? [] : relativeDir.split("/");
1148
+ const directories = [appDir];
1149
+ for (let index = 0; index < parts.length; index += 1) {
1150
+ directories.push(join(appDir, ...parts.slice(0, index + 1)));
1151
+ }
1152
+ const files = [];
1153
+ for (const directory of directories) {
1154
+ const shellId = shellBoundaryId(appDir, directory);
1155
+ for (const [filename, kind] of [
1156
+ ["layout.tsx", "layout"],
1157
+ ["layout.mreact.tsx", "layout"],
1158
+ ["template.tsx", "template"],
1159
+ ["template.mreact.tsx", "template"],
1160
+ ]) {
1161
+ const candidate = join(directory, filename);
1162
+ try {
1163
+ await access(candidate);
1164
+ files.push({ file: candidate, id: shellId, kind });
1165
+ }
1166
+ catch {
1167
+ // Missing shell files are allowed.
1168
+ }
1169
+ }
1170
+ }
1171
+ if (cacheKey !== undefined) {
1172
+ if (shellFilesCache.size >= MAX_SHELL_FILES_CACHE_ENTRIES) {
1173
+ const oldestKey = shellFilesCache.keys().next().value;
1174
+ if (oldestKey !== undefined) {
1175
+ shellFilesCache.delete(oldestKey);
1176
+ }
1177
+ }
1178
+ shellFilesCache.set(cacheKey, files);
1179
+ }
1180
+ return files;
1181
+ }
1182
+ function withRouteCacheHeader(response, policy) {
1183
+ if (policy !== undefined) {
1184
+ response.headers.set("cache-control", policy.cacheControl);
1185
+ }
1186
+ return response;
1187
+ }
1188
+ function shellBoundaryId(appDir, directory) {
1189
+ const relativeDirectory = relative(appDir, directory);
1190
+ return relativeDirectory === ""
1191
+ ? "root"
1192
+ : relativeDirectory.replaceAll(sep, "/").replace(/[^A-Za-z0-9_$/-]/g, "_");
1193
+ }
1194
+ function markShellBoundary(html, shell) {
1195
+ const attributeName = shell.kind === "layout" ? "data-mreact-layout-boundary" : "data-mreact-template-boundary";
1196
+ if (html.includes(`${attributeName}=`)) {
1197
+ return html;
1198
+ }
1199
+ return html.replace(/<([A-Za-z][^\s/>]*)([^>]*)>/, `<$1$2 ${attributeName}="${escapeHtmlAttribute(shell.id)}">`);
1200
+ }
1201
+ function replaceLayoutSlot(layoutHtml, childHtml, slotContext = createSlotRenderContext()) {
1202
+ const html = replaceNamedLayoutSlots(layoutHtml, slotContext);
1203
+ const match = findDefaultLayoutSlot(html);
1204
+ return match === null
1205
+ ? `${html}${childHtml}`
1206
+ : `${html.slice(0, match.index)}${childHtml}${html.slice(match.index + match[0].length)}`;
1207
+ }
1208
+ function replaceNamedLayoutSlots(layoutHtml, slotContext) {
1209
+ return layoutHtml.replace(SLOT_TAG_PATTERN, (source, openAttributes) => {
1210
+ const name = readSlotName(openAttributes);
1211
+ if (name === undefined || name === "default") {
1212
+ return source;
1213
+ }
1214
+ if (Object.hasOwn(slotContext.namedSlots, name)) {
1215
+ slotContext.consumedSlots.add(name);
1216
+ return slotContext.namedSlots[name] ?? "";
1217
+ }
1218
+ return "";
1219
+ });
1220
+ }
1221
+ const SLOT_TAG_PATTERN = /<slot\b([^>]*)>(?:<\/slot\s*>)?/g;
1222
+ function findDefaultLayoutSlot(html) {
1223
+ SLOT_TAG_PATTERN.lastIndex = 0;
1224
+ for (;;) {
1225
+ const match = SLOT_TAG_PATTERN.exec(html);
1226
+ if (match === null) {
1227
+ return null;
1228
+ }
1229
+ const name = readSlotName(match[1] ?? "");
1230
+ if (name === undefined || name === "default") {
1231
+ return match;
1232
+ }
1233
+ }
1234
+ }
1235
+ function readSlotName(attributes) {
1236
+ const match = /\bname\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(attributes);
1237
+ return match?.[1] ?? match?.[2];
1238
+ }
1239
+ function createSlotRenderContext(namedSlots = {}) {
1240
+ return {
1241
+ consumedSlots: new Set(),
1242
+ namedSlots,
1243
+ };
1244
+ }
1245
+ function warnUnconsumedRouteSlots(options) {
1246
+ if (options.serverModuleCacheVersion !== undefined) {
1247
+ return;
1248
+ }
1249
+ const slotNames = Object.keys(options.slotContext.namedSlots);
1250
+ if (slotNames.length === 0) {
1251
+ return;
1252
+ }
1253
+ const routeLabel = relative(options.appDir, options.pageFile).replaceAll(sep, "/");
1254
+ for (const name of slotNames) {
1255
+ if (name === "default") {
1256
+ console.warn(`[mreact] ${routeLabel}: slots.default does not target <Slot />; use the page body for default slot content.`);
1257
+ continue;
1258
+ }
1259
+ if (!options.slotContext.consumedSlots.has(name)) {
1260
+ console.warn(`[mreact] ${routeLabel}: slots.{${name}} is not consumed by any ancestor layout or template.`);
1261
+ }
1262
+ }
1263
+ }
1264
+ async function loadRouteData(options) {
1265
+ if (!hasLoaderExport(options.code)) {
1266
+ return undefined;
1267
+ }
1268
+ const output = await bundle({
1269
+ bundle: true,
1270
+ format: "esm",
1271
+ logLevel: "silent",
1272
+ platform: "node",
1273
+ plugins: [
1274
+ createAppRouterImportPolicyPlugin({
1275
+ appDir: options.appDir,
1276
+ importPolicy: options.importPolicy,
1277
+ label: "Loader",
1278
+ }),
1279
+ ],
1280
+ write: false,
1281
+ jsx: "transform",
1282
+ jsxFactory: "__mreact_jsx",
1283
+ jsxFragment: "__mreact_fragment",
1284
+ stdin: {
1285
+ contents: options.code,
1286
+ loader: "tsx",
1287
+ resolveDir: dirname(options.filename),
1288
+ sourcefile: options.filename,
1289
+ },
1290
+ });
1291
+ const code = output.outputFiles[0]?.text;
1292
+ if (code === undefined) {
1293
+ throw new Error(`Failed to compile loader for ${options.filename}.`);
1294
+ }
1295
+ const module = await importAppRouterSourceModule({
1296
+ code,
1297
+ label: `loader:${options.filename}`,
1298
+ });
1299
+ return module.loader === undefined ? undefined : await module.loader(options.context);
1300
+ }
1301
+ async function loadRouteMetadata(options) {
1302
+ if (!hasMetadataExport(options.code)) {
1303
+ return undefined;
1304
+ }
1305
+ const output = await bundle({
1306
+ bundle: true,
1307
+ format: "esm",
1308
+ logLevel: "silent",
1309
+ platform: "node",
1310
+ plugins: [
1311
+ createAppRouterImportPolicyPlugin({
1312
+ appDir: options.appDir,
1313
+ importPolicy: options.importPolicy,
1314
+ label: "Metadata",
1315
+ }),
1316
+ ],
1317
+ write: false,
1318
+ jsx: "transform",
1319
+ jsxFactory: "__mreact_jsx",
1320
+ jsxFragment: "__mreact_fragment",
1321
+ stdin: {
1322
+ contents: options.code,
1323
+ loader: "tsx",
1324
+ resolveDir: dirname(options.filename),
1325
+ sourcefile: options.filename,
1326
+ },
1327
+ });
1328
+ const code = output.outputFiles[0]?.text;
1329
+ if (code === undefined) {
1330
+ throw new Error(`Failed to compile metadata for ${options.filename}.`);
1331
+ }
1332
+ const module = await importAppRouterSourceModule({
1333
+ code,
1334
+ label: `metadata:${options.filename}`,
1335
+ });
1336
+ return module.metadata;
1337
+ }
1338
+ async function loadComposedRouteMetadata(options) {
1339
+ const cacheKey = options.serverModuleCacheVersion === undefined
1340
+ ? undefined
1341
+ : `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}`;
1342
+ if (cacheKey !== undefined) {
1343
+ const cached = composedRouteMetadataCache.get(cacheKey);
1344
+ if (cached !== undefined) {
1345
+ return cached;
1346
+ }
1347
+ }
1348
+ const loaded = loadComposedRouteMetadataUncached(options).catch((error) => {
1349
+ if (cacheKey !== undefined) {
1350
+ composedRouteMetadataCache.delete(cacheKey);
1351
+ }
1352
+ throw error;
1353
+ });
1354
+ if (cacheKey !== undefined) {
1355
+ setBoundedCacheEntry(composedRouteMetadataCache, cacheKey, loaded, maxComposedRouteMetadataCacheEntries);
1356
+ }
1357
+ return loaded;
1358
+ }
1359
+ async function loadComposedRouteMetadataUncached(options) {
1360
+ const layoutFiles = await shellFilesForPage(options.appDir, options.filename, options.serverModuleCacheVersion);
1361
+ const metadata = [];
1362
+ for (const shell of layoutFiles) {
1363
+ if (shell.kind !== "layout") {
1364
+ continue;
1365
+ }
1366
+ const code = await readServerSourceFile(shell.file, options.serverModuleCacheVersion, options.serverSourceFiles);
1367
+ const shellMetadata = await loadRouteMetadata({
1368
+ appDir: options.appDir,
1369
+ code,
1370
+ filename: shell.file,
1371
+ importPolicy: options.importPolicy,
1372
+ });
1373
+ if (shellMetadata !== undefined) {
1374
+ metadata.push(shellMetadata);
1375
+ }
1376
+ }
1377
+ const pageMetadata = await loadRouteMetadata({
1378
+ appDir: options.appDir,
1379
+ code: options.code,
1380
+ filename: options.filename,
1381
+ importPolicy: options.importPolicy,
1382
+ });
1383
+ if (pageMetadata !== undefined) {
1384
+ metadata.push(pageMetadata);
1385
+ }
1386
+ return mergeRouteMetadata(metadata);
1387
+ }
1388
+ function mergeRouteMetadata(metadata) {
1389
+ if (metadata.length === 0) {
1390
+ return undefined;
1391
+ }
1392
+ return metadata.reduce((merged, next) => {
1393
+ const mergedMetadata = { ...merged, ...next };
1394
+ const alternates = mergeObject(merged.alternates, next.alternates);
1395
+ const csp = mergeCspMetadata(merged.csp, next.csp);
1396
+ const head = mergeReadonlyArrays(merged.head, next.head);
1397
+ const icons = mergeObject(merged.icons, next.icons);
1398
+ const openGraph = mergeOpenGraphMetadata(merged.openGraph, next.openGraph);
1399
+ if (alternates !== undefined) {
1400
+ mergedMetadata.alternates = alternates;
1401
+ }
1402
+ if (csp !== undefined) {
1403
+ mergedMetadata.csp = csp;
1404
+ }
1405
+ if (head !== undefined) {
1406
+ mergedMetadata.head = head;
1407
+ }
1408
+ if (icons !== undefined) {
1409
+ mergedMetadata.icons = icons;
1410
+ }
1411
+ if (openGraph !== undefined) {
1412
+ mergedMetadata.openGraph = openGraph;
1413
+ }
1414
+ return mergedMetadata;
1415
+ }, {});
1416
+ }
1417
+ function mergeObject(left, right) {
1418
+ if (left === undefined) {
1419
+ return right;
1420
+ }
1421
+ if (right === undefined) {
1422
+ return left;
1423
+ }
1424
+ return { ...left, ...right };
1425
+ }
1426
+ function mergeReadonlyArrays(left, right) {
1427
+ if (left === undefined || left.length === 0) {
1428
+ return right;
1429
+ }
1430
+ if (right === undefined || right.length === 0) {
1431
+ return left;
1432
+ }
1433
+ return [...left, ...right];
1434
+ }
1435
+ function mergeCspMetadata(left, right) {
1436
+ if (left === undefined) {
1437
+ return right;
1438
+ }
1439
+ if (right === undefined) {
1440
+ return left;
1441
+ }
1442
+ const merged = {
1443
+ ...left,
1444
+ ...right,
1445
+ };
1446
+ const directives = mergeObject(left.directives, right.directives);
1447
+ if (directives !== undefined) {
1448
+ merged.directives = directives;
1449
+ }
1450
+ return merged;
1451
+ }
1452
+ function mergeOpenGraphMetadata(left, right) {
1453
+ if (left === undefined) {
1454
+ return right;
1455
+ }
1456
+ if (right === undefined) {
1457
+ return left;
1458
+ }
1459
+ const merged = {
1460
+ ...left,
1461
+ ...right,
1462
+ };
1463
+ const images = mergeReadonlyArrays(openGraphImages(left), openGraphImages(right));
1464
+ if (images !== undefined && images.length > 0) {
1465
+ merged.images = images;
1466
+ }
1467
+ return merged;
1468
+ }
1469
+ function hasMetadataExport(code) {
1470
+ return /\bexport\s+const\s+metadata\s*=/.test(code);
1471
+ }
1472
+ function usesRuntimeCacheControl(code) {
1473
+ return /\bcacheControl\s*\(/.test(code);
1474
+ }
1475
+ function injectHeadMetadata(html, metadata) {
1476
+ if (metadata === undefined) {
1477
+ return html;
1478
+ }
1479
+ const tags = [
1480
+ metadata.title === undefined
1481
+ ? undefined
1482
+ : `<title>${escapeHtml(metadataString(metadata.title, "title"))}</title>`,
1483
+ metadata.description === undefined
1484
+ ? undefined
1485
+ : `<meta name="description" content="${escapeHtmlAttribute(metadataString(metadata.description, "description"))}">`,
1486
+ metadata.alternates?.canonical === undefined
1487
+ ? undefined
1488
+ : `<link rel="canonical" href="${escapeHtmlAttribute(metadataString(metadata.alternates.canonical, "alternates.canonical"))}">`,
1489
+ metadata.openGraph?.title === undefined
1490
+ ? undefined
1491
+ : `<meta property="og:title" content="${escapeHtmlAttribute(metadataString(metadata.openGraph.title, "openGraph.title"))}">`,
1492
+ metadata.openGraph?.description === undefined
1493
+ ? undefined
1494
+ : `<meta property="og:description" content="${escapeHtmlAttribute(metadataString(metadata.openGraph.description, "openGraph.description"))}">`,
1495
+ ...openGraphImages(metadata.openGraph).map((image) => `<meta property="og:image" content="${escapeHtmlAttribute(image)}">`),
1496
+ metadata.icons?.icon === undefined
1497
+ ? undefined
1498
+ : `<link rel="icon" href="${escapeHtmlAttribute(metadataString(metadata.icons.icon, "icons.icon"))}">`,
1499
+ metadata.icons?.apple === undefined
1500
+ ? undefined
1501
+ : `<link rel="apple-touch-icon" href="${escapeHtmlAttribute(metadataString(metadata.icons.apple, "icons.apple"))}">`,
1502
+ metadata.robots === undefined
1503
+ ? undefined
1504
+ : `<meta name="robots" content="${escapeHtmlAttribute(robotsContent(metadata.robots))}">`,
1505
+ metadata.themeColor === undefined ? undefined : themeColorTag(metadata.themeColor),
1506
+ metadata.viewport === undefined
1507
+ ? undefined
1508
+ : `<meta name="viewport" content="${escapeHtmlAttribute(viewportContent(metadata.viewport))}">`,
1509
+ ...headDescriptorTags(metadata.head, metadata.csp?.nonce),
1510
+ ]
1511
+ .filter((tag) => tag !== undefined)
1512
+ .join("");
1513
+ if (tags === "") {
1514
+ return html;
1515
+ }
1516
+ if (/<head(?:\s[^>]*)?>/i.test(html)) {
1517
+ return html.replace(/<head(\s[^>]*)?>/i, (match) => `${match}${tags}`);
1518
+ }
1519
+ if (/<html(?:\s[^>]*)?>/i.test(html)) {
1520
+ return html.replace(/<html(\s[^>]*)?>/i, (match) => `${match}<head>${tags}</head>`);
1521
+ }
1522
+ return `<head>${tags}</head>${html}`;
1523
+ }
1524
+ function responseHeadersForMetadata(metadata) {
1525
+ const headers = new Headers({ "content-type": "text/html; charset=utf-8" });
1526
+ const csp = contentSecurityPolicy(metadata?.csp);
1527
+ if (csp !== undefined) {
1528
+ headers.set("content-security-policy", csp);
1529
+ }
1530
+ return headers;
1531
+ }
1532
+ function injectQueryState(html, state) {
1533
+ if (state.queries.length === 0) {
1534
+ return html;
1535
+ }
1536
+ const script = `<script type="application/json" id="${__MREACT_QUERY_STATE_SCRIPT_ID}">${escapeJsonForHtml(JSON.stringify(state))}</script>`;
1537
+ return /<\/body>/i.test(html)
1538
+ ? html.replace(/<\/body>/i, `${script}</body>`)
1539
+ : `${html}${script}`;
1540
+ }
1541
+ function injectAuthSessionClaims(html, claims) {
1542
+ if (claims === undefined) {
1543
+ return html;
1544
+ }
1545
+ const script = `<script type="application/json" id="${authSessionScriptId}">${escapeJsonForHtml(JSON.stringify(claims))}</script>`;
1546
+ return /<\/body>/i.test(html)
1547
+ ? html.replace(/<\/body>/i, `${script}</body>`)
1548
+ : `${html}${script}`;
1549
+ }
1550
+ function authIncludesClaims(code) {
1551
+ return /\bexport\s+const\s+auth\s*=\s*["']include-claims["']\s*;?/.test(code);
1552
+ }
1553
+ function currentAuthClaims() {
1554
+ return authRequestStorage().getStore()?.claims;
1555
+ }
1556
+ function authRequestStorage() {
1557
+ const global = globalThis;
1558
+ global[authRuntimeStateKey] ??= {};
1559
+ global[authRuntimeStateKey].storage ??= new AsyncLocalStorage();
1560
+ return global[authRuntimeStateKey].storage;
1561
+ }
1562
+ function escapeJsonForHtml(value) {
1563
+ return value
1564
+ .replaceAll("&", "\\u0026")
1565
+ .replaceAll("<", "\\u003c")
1566
+ .replaceAll(">", "\\u003e")
1567
+ .replaceAll("\u2028", "\\u2028")
1568
+ .replaceAll("\u2029", "\\u2029");
1569
+ }
1570
+ function headDescriptorTags(descriptors, nonce) {
1571
+ return (descriptors ?? []).flatMap((descriptor) => {
1572
+ const descriptorNonce = descriptor.nonce === true ? nonce : descriptor.nonce || undefined;
1573
+ const attrs = {
1574
+ ...descriptor.attrs,
1575
+ ...(descriptorNonce === undefined ? {} : { nonce: descriptorNonce }),
1576
+ };
1577
+ const attrText = Object.entries(attrs)
1578
+ .flatMap(([name, value]) => {
1579
+ if (value === undefined || value === false) {
1580
+ return [];
1581
+ }
1582
+ return value === true
1583
+ ? [escapeHtmlAttribute(name)]
1584
+ : [`${escapeHtmlAttribute(name)}="${escapeHtmlAttribute(String(value))}"`];
1585
+ })
1586
+ .join(" ");
1587
+ const open = attrText === "" ? `<${descriptor.tag}>` : `<${descriptor.tag} ${attrText}>`;
1588
+ if (descriptor.tag === "meta" || descriptor.tag === "link" || descriptor.tag === "base") {
1589
+ return [open.slice(0, -1) + ">"];
1590
+ }
1591
+ return [`${open}${escapeHeadTextContent(descriptor.content ?? "")}</${descriptor.tag}>`];
1592
+ });
1593
+ }
1594
+ function escapeHeadTextContent(value) {
1595
+ return value.replaceAll("<", "\\u003c");
1596
+ }
1597
+ function metadataString(value, path) {
1598
+ if (isMetadataScalar(value)) {
1599
+ return String(value);
1600
+ }
1601
+ throw new Error(`Invalid metadata field ${path}: expected string, number, or boolean.`);
1602
+ }
1603
+ function metadataKebabName(name) {
1604
+ return name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
1605
+ }
1606
+ function viewportContent(viewport) {
1607
+ if (isMetadataScalar(viewport)) {
1608
+ return metadataString(viewport, "viewport");
1609
+ }
1610
+ return Object.entries(viewport)
1611
+ .flatMap(([key, value]) => {
1612
+ if (value === undefined || value === null || value === false) {
1613
+ return [];
1614
+ }
1615
+ return [`${metadataKebabName(key)}=${metadataString(value, `viewport.${key}`)}`];
1616
+ })
1617
+ .join(", ");
1618
+ }
1619
+ function themeColorTag(themeColor) {
1620
+ if (isMetadataScalar(themeColor)) {
1621
+ return `<meta name="theme-color" content="${escapeHtmlAttribute(metadataString(themeColor, "themeColor"))}">`;
1622
+ }
1623
+ const content = themeColor.color;
1624
+ if (!isMetadataScalar(content)) {
1625
+ throw new Error("Invalid metadata field themeColor.color: expected string, number, or boolean.");
1626
+ }
1627
+ const media = themeColor.media === undefined
1628
+ ? ""
1629
+ : ` media="${escapeHtmlAttribute(metadataString(metadataScalarField(themeColor.media, "themeColor.media"), "themeColor.media"))}"`;
1630
+ return `<meta name="theme-color"${media} content="${escapeHtmlAttribute(metadataString(content, "themeColor.color"))}">`;
1631
+ }
1632
+ function metadataScalarField(value, path) {
1633
+ if (isMetadataScalar(value)) {
1634
+ return value;
1635
+ }
1636
+ throw new Error(`Invalid metadata field ${path}: expected string, number, or boolean.`);
1637
+ }
1638
+ function isMetadataScalar(value) {
1639
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1640
+ }
1641
+ function openGraphImages(openGraph) {
1642
+ if (openGraph?.images !== undefined) {
1643
+ return openGraph.images.map((image, index) => metadataString(image, `openGraph.images.${index}`));
1644
+ }
1645
+ return openGraph?.image === undefined ? [] : [metadataString(openGraph.image, "openGraph.image")];
1646
+ }
1647
+ function robotsContent(robots) {
1648
+ if (typeof robots === "string") {
1649
+ return robots;
1650
+ }
1651
+ return [
1652
+ robots.index === false ? "noindex" : "index",
1653
+ robots.follow === false ? "nofollow" : "follow",
1654
+ ].join(",");
1655
+ }
1656
+ function readServerSourceFile(file, serverModuleCacheVersion, serverSourceFiles) {
1657
+ const manifestSource = serverSourceFiles?.get(file);
1658
+ if (manifestSource !== undefined) {
1659
+ return Promise.resolve(manifestSource);
1660
+ }
1661
+ if (serverModuleCacheVersion === undefined) {
1662
+ return readFile(file, "utf8");
1663
+ }
1664
+ const key = `${serverModuleCacheVersion}:${file}`;
1665
+ const cached = serverSourceFileCache.get(key);
1666
+ if (cached !== undefined) {
1667
+ return cached;
1668
+ }
1669
+ const loaded = readFile(file, "utf8").catch((error) => {
1670
+ serverSourceFileCache.delete(key);
1671
+ throw error;
1672
+ });
1673
+ setBoundedCacheEntry(serverSourceFileCache, key, loaded, maxServerSourceFileCacheEntries);
1674
+ return loaded;
1675
+ }
1676
+ function hashText(text) {
1677
+ return createHash("sha256").update(text).digest("hex").slice(0, 16);
1678
+ }
1679
+ function setBoundedCacheEntry(cache, key, value, maxEntries) {
1680
+ if (cache.size >= maxEntries) {
1681
+ const oldestKey = cache.keys().next().value;
1682
+ if (oldestKey !== undefined) {
1683
+ cache.delete(oldestKey);
1684
+ }
1685
+ }
1686
+ cache.set(key, value);
1687
+ }
1688
+ //# sourceMappingURL=render.js.map