@reckona/mreact-router 0.0.202 → 0.0.204

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 (152) hide show
  1. package/README.md +58 -11
  2. package/dist/actions.d.ts +8 -3
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +125 -45
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/aws-lambda.d.ts +15 -4
  7. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  8. package/dist/adapters/aws-lambda.js +301 -63
  9. package/dist/adapters/aws-lambda.js.map +1 -1
  10. package/dist/adapters/cloudflare.d.ts +5 -2
  11. package/dist/adapters/cloudflare.d.ts.map +1 -1
  12. package/dist/adapters/cloudflare.js +35 -11
  13. package/dist/adapters/cloudflare.js.map +1 -1
  14. package/dist/adapters/edge.d.ts +1 -1
  15. package/dist/adapters/edge.d.ts.map +1 -1
  16. package/dist/adapters/edge.js.map +1 -1
  17. package/dist/adapters/node.d.ts +5 -3
  18. package/dist/adapters/node.d.ts.map +1 -1
  19. package/dist/adapters/node.js +2 -1
  20. package/dist/adapters/node.js.map +1 -1
  21. package/dist/adapters/static.d.ts.map +1 -1
  22. package/dist/adapters/static.js +24 -2
  23. package/dist/adapters/static.js.map +1 -1
  24. package/dist/build.d.ts +4 -1
  25. package/dist/build.d.ts.map +1 -1
  26. package/dist/build.js +185 -13
  27. package/dist/build.js.map +1 -1
  28. package/dist/built-runtime.d.ts +2 -0
  29. package/dist/built-runtime.d.ts.map +1 -1
  30. package/dist/built-runtime.js +16 -0
  31. package/dist/built-runtime.js.map +1 -1
  32. package/dist/cache.d.ts +4 -2
  33. package/dist/cache.d.ts.map +1 -1
  34. package/dist/cache.js +216 -17
  35. package/dist/cache.js.map +1 -1
  36. package/dist/cli-options.d.ts +3 -0
  37. package/dist/cli-options.d.ts.map +1 -1
  38. package/dist/cli-options.js +10 -2
  39. package/dist/cli-options.js.map +1 -1
  40. package/dist/cli.js +14 -3
  41. package/dist/cli.js.map +1 -1
  42. package/dist/client.d.ts +5 -0
  43. package/dist/client.d.ts.map +1 -1
  44. package/dist/client.js +43 -3
  45. package/dist/client.js.map +1 -1
  46. package/dist/config.d.ts +3 -0
  47. package/dist/config.d.ts.map +1 -1
  48. package/dist/config.js +21 -2
  49. package/dist/config.js.map +1 -1
  50. package/dist/csp.d.ts +1 -0
  51. package/dist/csp.d.ts.map +1 -1
  52. package/dist/csp.js +9 -0
  53. package/dist/csp.js.map +1 -1
  54. package/dist/dehydrate-policy.d.ts +3 -0
  55. package/dist/dehydrate-policy.d.ts.map +1 -0
  56. package/dist/dehydrate-policy.js +16 -0
  57. package/dist/dehydrate-policy.js.map +1 -0
  58. package/dist/dev-server.d.ts +7 -2
  59. package/dist/dev-server.d.ts.map +1 -1
  60. package/dist/dev-server.js +120 -11
  61. package/dist/dev-server.js.map +1 -1
  62. package/dist/http.d.ts +1 -1
  63. package/dist/http.d.ts.map +1 -1
  64. package/dist/http.js +147 -7
  65. package/dist/http.js.map +1 -1
  66. package/dist/index.d.ts +4 -3
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +1 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/logger.d.ts +22 -1
  71. package/dist/logger.d.ts.map +1 -1
  72. package/dist/logger.js +22 -8
  73. package/dist/logger.js.map +1 -1
  74. package/dist/metadata.d.ts +8 -0
  75. package/dist/metadata.d.ts.map +1 -1
  76. package/dist/metadata.js +60 -22
  77. package/dist/metadata.js.map +1 -1
  78. package/dist/navigation-marker.d.ts +2 -0
  79. package/dist/navigation-marker.d.ts.map +1 -0
  80. package/dist/navigation-marker.js +357 -0
  81. package/dist/navigation-marker.js.map +1 -0
  82. package/dist/navigation-runtime.d.ts +1 -1
  83. package/dist/navigation-runtime.d.ts.map +1 -1
  84. package/dist/navigation-runtime.js +1 -1
  85. package/dist/navigation-runtime.js.map +1 -1
  86. package/dist/navigation.d.ts +1 -0
  87. package/dist/navigation.d.ts.map +1 -1
  88. package/dist/navigation.js +1 -1
  89. package/dist/navigation.js.map +1 -1
  90. package/dist/node-server.d.ts +5 -2
  91. package/dist/node-server.d.ts.map +1 -1
  92. package/dist/node-server.js +75 -6
  93. package/dist/node-server.js.map +1 -1
  94. package/dist/prerender-entry.d.ts +17 -1
  95. package/dist/prerender-entry.d.ts.map +1 -1
  96. package/dist/prerender-entry.js +98 -3
  97. package/dist/prerender-entry.js.map +1 -1
  98. package/dist/render.d.ts +16 -2
  99. package/dist/render.d.ts.map +1 -1
  100. package/dist/render.js +142 -135
  101. package/dist/render.js.map +1 -1
  102. package/dist/serve.d.ts +11 -2
  103. package/dist/serve.d.ts.map +1 -1
  104. package/dist/serve.js +67 -41
  105. package/dist/serve.js.map +1 -1
  106. package/dist/session.d.ts.map +1 -1
  107. package/dist/session.js +52 -19
  108. package/dist/session.js.map +1 -1
  109. package/dist/upgrade.d.ts +59 -2
  110. package/dist/upgrade.d.ts.map +1 -1
  111. package/dist/upgrade.js +282 -1
  112. package/dist/upgrade.js.map +1 -1
  113. package/dist/vite-config.d.ts +2 -0
  114. package/dist/vite-config.d.ts.map +1 -1
  115. package/dist/vite-config.js +2 -0
  116. package/dist/vite-config.js.map +1 -1
  117. package/dist/vite.d.ts +4 -1
  118. package/dist/vite.d.ts.map +1 -1
  119. package/dist/vite.js +117 -6
  120. package/dist/vite.js.map +1 -1
  121. package/package.json +11 -11
  122. package/src/actions.ts +146 -60
  123. package/src/adapters/aws-lambda.ts +426 -115
  124. package/src/adapters/cloudflare.ts +58 -12
  125. package/src/adapters/edge.ts +2 -0
  126. package/src/adapters/node.ts +8 -10
  127. package/src/adapters/static.ts +31 -2
  128. package/src/build.ts +217 -13
  129. package/src/built-runtime.ts +28 -0
  130. package/src/cache.ts +272 -19
  131. package/src/cli-options.ts +15 -2
  132. package/src/cli.ts +16 -2
  133. package/src/client.ts +47 -3
  134. package/src/config.ts +43 -6
  135. package/src/csp.ts +11 -0
  136. package/src/dehydrate-policy.ts +23 -0
  137. package/src/dev-server.ts +188 -15
  138. package/src/http.ts +172 -6
  139. package/src/index.ts +14 -1
  140. package/src/logger.ts +47 -10
  141. package/src/metadata.ts +125 -60
  142. package/src/navigation-marker.ts +423 -0
  143. package/src/navigation-runtime.ts +1 -0
  144. package/src/navigation.ts +1 -1
  145. package/src/node-server.ts +128 -22
  146. package/src/prerender-entry.ts +127 -5
  147. package/src/render.ts +859 -749
  148. package/src/serve.ts +112 -82
  149. package/src/session.ts +56 -25
  150. package/src/upgrade.ts +402 -3
  151. package/src/vite-config.ts +6 -0
  152. package/src/vite.ts +157 -5
package/src/render.ts CHANGED
@@ -16,12 +16,13 @@ import {
16
16
  installQueryAsyncStorage,
17
17
  runWithQueryClient,
18
18
  type DehydratedQueryClient,
19
+ type DehydrateOptions,
19
20
  type QueryClient,
20
21
  } from "@reckona/mreact-query";
21
22
  import {
22
23
  createStringSink,
23
24
  type HtmlSink,
24
- renderAsyncBoundary,
25
+ renderOutOfOrderBoundary,
25
26
  renderOutOfOrderReorderScript,
26
27
  renderReactNodeToString,
27
28
  renderToReadableStream,
@@ -35,9 +36,9 @@ import {
35
36
  } from "./client-route-inference.js";
36
37
  import {
37
38
  hydrationMarkerParts,
39
+ routeMarkerParts,
38
40
  routeIdForPath,
39
41
  withHydrationMarkers,
40
- withRouteMarkers,
41
42
  } from "./navigation-runtime.js";
42
43
  import { assetPath } from "./assets.js";
43
44
  import { escapeHtmlAttribute } from "@reckona/mreact-shared/html-escape";
@@ -66,11 +67,9 @@ import {
66
67
  type TrackedHeaderRequest,
67
68
  withTrackedRequest,
68
69
  } from "./request-header-tracking.js";
69
- import {
70
- configuredHstsHeader,
71
- hasInvalidConfiguredHsts,
72
- } from "./security-headers.js";
70
+ import { configuredHstsHeader, hasInvalidConfiguredHsts } from "./security-headers.js";
73
71
  import { resolveRouterCacheLimit } from "./cache-config.js";
72
+ import { protectNonceBearingResponse } from "./prerender-entry.js";
74
73
  import {
75
74
  importAppRouterBuiltFileModule,
76
75
  importAppRouterFileModule,
@@ -129,13 +128,15 @@ import {
129
128
  type RouteMiddlewareControl,
130
129
  } from "./middleware.js";
131
130
  import {
132
- applyFileConventionMetadata,
131
+ applyFileConventionMetadataTemplate,
132
+ fileConventionMetadataTemplate,
133
133
  injectHeadMetadata,
134
134
  mergeRouteMetadata,
135
135
  responseHeadersForMetadata,
136
136
  serializeRobots,
137
137
  serializeSitemap,
138
138
  validateRouteMetadata,
139
+ type FileConventionMetadataTemplate,
139
140
  } from "./metadata.js";
140
141
  import {
141
142
  createSlotRenderContext,
@@ -202,6 +203,7 @@ export interface RenderAppRequestOptions {
202
203
  clientStylesByFile?: ReadonlyMap<string, readonly string[]>;
203
204
  clientStyles?: ReadonlyMap<string, readonly string[]>;
204
205
  define?: UserConfig["define"] | undefined;
206
+ dehydrateOptions?: DehydrateOptions | undefined;
205
207
  env?: unknown;
206
208
  importPolicy?: AppRouterImportPolicy | undefined;
207
209
  instrumentation?: RouterInstrumentation | undefined;
@@ -217,6 +219,7 @@ export interface RenderAppRequestOptions {
217
219
  routeMatcher?: RouteMatcher | undefined;
218
220
  routes?: readonly AppRoute[] | undefined;
219
221
  serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
222
+ serverRenderArtifactLoader?: AppRouterServerRenderArtifactLoader | undefined;
220
223
  serverModuleCacheVersion?: string | undefined;
221
224
  devServerModuleCacheVersion?: string | undefined;
222
225
  // True when serving through a dev server: request-time server transforms
@@ -233,6 +236,14 @@ export interface RenderAppRequestOptions {
233
236
  }
234
237
 
235
238
  export interface RenderAppRequestRuntimeOptions extends RenderAppRequestOptions {
239
+ /**
240
+ * Wraps a server-only page with build-time variant capture comments.
241
+ *
242
+ * This is an in-process render signal. It must not be derived from request
243
+ * headers because network clients are not allowed to change response shape
244
+ * outside the route-cache key.
245
+ */
246
+ prerenderVariantCapture?: boolean | undefined;
236
247
  /**
237
248
  * Receives whether the render depended on the incoming request headers.
238
249
  *
@@ -243,8 +254,12 @@ export interface RenderAppRequestRuntimeOptions extends RenderAppRequestOptions
243
254
  * dependent. Call it only after draining the response body, because a
244
255
  * streamed render can read headers while its deferred parts resolve.
245
256
  */
246
- renderSignals?: { headerDependent: () => boolean } | undefined;
247
- serverRenderArtifactLoader?: AppRouterServerRenderArtifactLoader | undefined;
257
+ renderSignals?:
258
+ | {
259
+ headerDependent: () => boolean;
260
+ strictTransportSecurity: () => string | undefined;
261
+ }
262
+ | undefined;
248
263
  }
249
264
 
250
265
  export interface AppRouterRenderPreload {
@@ -565,7 +580,12 @@ const middlewareModuleCache = new Map<string, Promise<MiddlewareModule>>();
565
580
  const middlewareModuleCacheCounters = createRouterRuntimeCacheCounters();
566
581
  const serverRouteModuleCache = new Map<string, Promise<Record<string, unknown>>>();
567
582
  const serverRouteModuleCacheCounters = createRouterRuntimeCacheCounters();
568
- const composedRouteMetadataCache = new Map<string, Promise<RouteMetadata | undefined>>();
583
+ interface ComposedRouteMetadataCacheEntry {
584
+ metadata: RouteMetadata | undefined;
585
+ template: FileConventionMetadataTemplate;
586
+ }
587
+
588
+ const composedRouteMetadataCache = new Map<string, Promise<ComposedRouteMetadataCacheEntry>>();
569
589
  const composedRouteMetadataCacheCounters = createRouterRuntimeCacheCounters();
570
590
  const maxServerTransformCacheEntries = resolveRouterCacheLimit("SERVER_TRANSFORM", 512);
571
591
  const maxServerSourceFileCacheEntries = resolveRouterCacheLimit("SERVER_SOURCE_FILE", 512);
@@ -688,7 +708,9 @@ export async function renderAppRequest(options: RenderAppRequestOptions): Promis
688
708
  };
689
709
  invokeRouterInstrumentation(options.instrumentation?.onRequestStart, requestEvent);
690
710
  const response = await renderAppRequestInternal({ ...options, requestUrl: url });
691
- const finalResponse = await applyAppRouterResponseHook(response, options);
711
+ const finalResponse = protectNonceBearingResponse(
712
+ await applyAppRouterResponseHook(response, options),
713
+ );
692
714
  invokeRouterInstrumentation(options.instrumentation?.onRequestEnd, {
693
715
  ...requestEvent,
694
716
  status: finalResponse.status,
@@ -717,43 +739,24 @@ function finishRenderTimingPhase(
717
739
  timing.phases[phaseName] = logDurationMs(startedAt);
718
740
  }
719
741
 
720
- async function readSettledPromiseAtNextTask<T>(
721
- promise: Promise<T>,
722
- ): Promise<{ settled: true; value: T } | { settled: false }> {
723
- return await Promise.race([
724
- promise.then((value) => ({ settled: true as const, value })),
725
- new Promise<{ settled: false }>((resolve) => {
726
- setTimeout(() => resolve({ settled: false }), 0);
727
- }),
728
- ]);
729
- }
742
+ type ObservedPromiseSettlement<T> =
743
+ | { status: "pending" }
744
+ | { status: "fulfilled"; value: T }
745
+ | { error: unknown; status: "rejected" };
730
746
 
731
- function createDeferredSignal(): {
732
- promise: Promise<void>;
733
- resolve(): void;
734
- } {
735
- let resolve: (() => void) | undefined;
736
- const promise = new Promise<void>((innerResolve) => {
737
- resolve = innerResolve;
738
- });
747
+ function observePromiseSettlement<T>(promise: Promise<T>): () => ObservedPromiseSettlement<T> {
748
+ let settlement: ObservedPromiseSettlement<T> = { status: "pending" };
739
749
 
740
- return {
741
- promise,
742
- resolve() {
743
- resolve?.();
750
+ void promise.then(
751
+ (value) => {
752
+ settlement = { status: "fulfilled", value };
744
753
  },
745
- };
746
- }
747
-
748
- async function waitForLoaderReadyOrSettled(
749
- dataPromise: Promise<unknown>,
750
- loaderReady: Promise<void> | undefined,
751
- ): Promise<void> {
752
- if (loaderReady === undefined) {
753
- return;
754
- }
754
+ (error: unknown) => {
755
+ settlement = { error, status: "rejected" };
756
+ },
757
+ );
755
758
 
756
- await Promise.race([loaderReady, dataPromise.then(() => undefined, () => undefined)]);
759
+ return () => settlement;
757
760
  }
758
761
 
759
762
  function addRenderTimingPhaseDuration(
@@ -768,14 +771,6 @@ function addRenderTimingPhaseDuration(
768
771
  timing.phases[phaseName] = (timing.phases[phaseName] ?? 0) + logDurationMs(startedAt);
769
772
  }
770
773
 
771
- function routeLoaderMayReturnControlResponse(code: string): boolean {
772
- return (
773
- /\b(?:redirect|redirectExternal|rewrite|notFound|throwNotFound)\s*\(/.test(code) ||
774
- /\bnew\s+Response\s*\(/.test(code) ||
775
- /\bResponse\.(?:redirect|json)\s*\(/.test(code)
776
- );
777
- }
778
-
779
774
  async function waitForRenderPreload(
780
775
  options: Pick<RenderAppRequestOptions, "preload">,
781
776
  timing: RenderTiming | undefined,
@@ -1000,211 +995,526 @@ async function renderAppRequestInternal(
1000
995
  let trackedRequest: TrackedHeaderRequest | undefined;
1001
996
  let sourceUsesRequestInput = true;
1002
997
  let invalidConfiguredHsts = false;
1003
- return (await withRouteCacheContext(options.routeCache, async () => {
1004
- try {
1005
- if (matched.route.kind === "asset") {
1006
- return await dispatchConventionAssetRoute({
1007
- file: matched.route.file,
1008
- request: options.request,
1009
- });
1010
- }
1011
-
1012
- if (matched.route.kind === "metadata") {
1013
- return await dispatchMetadataRoute({
1014
- appDir: options.appDir,
1015
- file: matched.route.file,
1016
- importPolicy: options.importPolicy,
1017
- params: matched.params,
1018
- request: options.request,
1019
- route: matched.route,
1020
- serverModules: options.serverModules,
1021
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1022
- serverSourceFiles: options.serverSourceFiles,
1023
- vitePlugins: options.vitePlugins,
1024
- });
1025
- }
1026
-
1027
- if (matched.route.kind === "server") {
1028
- return await dispatchServerRoute({
1029
- appDir: options.appDir,
1030
- env: options.env,
1031
- file: matched.route.file,
1032
- importPolicy: options.importPolicy,
1033
- params: matched.params,
1034
- request: options.request,
1035
- route: matched.route,
1036
- serverModules: options.serverModules,
1037
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1038
- serverSourceFiles: options.serverSourceFiles,
1039
- vitePlugins: options.vitePlugins,
1040
- });
1041
- }
1042
-
1043
- // Issue 080: page routes render HTML for GET / HEAD only. Other
1044
- // methods (PUT, PATCH, DELETE, PROPFIND, ...) get 405 with an
1045
- // Allow header so the response shape complies with RFC 9110 §9
1046
- // and so caching intermediaries do not cross-cache method results.
1047
- const methodResponse = pageRouteMethodResponse(options.request.method);
1048
- if (methodResponse !== undefined) {
1049
- return methodResponse;
1050
- }
1051
-
1052
- const clientScript = options.clientScripts?.get(matched.route.path);
1053
- const clientStyleSheets = options.clientStyles?.get(matched.route.path);
1054
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1055
- const originalCode = await readServerSourceFile(
1056
- matched.route.file,
1057
- options.serverModuleCacheVersion,
1058
- options.serverSourceFiles,
1059
- );
1060
- finishRenderTimingPhase(timing, phaseStartedAt, "readSourceMs");
1061
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1062
- const originalAnalysis = await analyzeRouteSource({
1063
- appDir: options.appDir,
1064
- artifact: options.serverModules?.get(matched.route.file)?.analysis,
1065
- code: originalCode,
1066
- filename: matched.route.file,
1067
- routePath: matched.route.path,
1068
- clientRouteInferenceCache,
1069
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1070
- serverSourceFiles: options.serverSourceFiles,
1071
- define: options.define,
1072
- timing,
1073
- vitePlugins: options.vitePlugins,
1074
- });
1075
- sourceUsesRequestInput = originalAnalysis.usesRequestInput;
1076
- finishRenderTimingPhase(timing, phaseStartedAt, "sourceAnalysisMs");
1077
- const cachePolicy = originalAnalysis.cachePolicy;
1078
- const navigationScript = options.navigationScripts?.get(matched.route.path);
1079
- const cacheKey = routeCacheKey(options.appDir, matched.route.path, url);
1080
- const mayUseRouteCache =
1081
- !sourceUsesRequestInput &&
1082
- (cachePolicy === undefined
1083
- ? originalAnalysis.usesRuntimeCacheControl
1084
- : cachePolicy.revalidateSeconds !== 0);
1085
- const reloadRouteCache = isNavigationRouteCacheReloadRequest(options.request);
1086
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1087
- const cachedResponse =
1088
- !mayUseRouteCache || reloadRouteCache
1089
- ? undefined
1090
- : await cachedRouteResponse({
1091
- cache: options.routeCache,
1092
- key: cacheKey,
998
+ let renderedStrictTransportSecurity: string | undefined;
999
+ return (
1000
+ await withRouteCacheContext(options.routeCache, async () => {
1001
+ try {
1002
+ if (matched.route.kind === "asset") {
1003
+ return await dispatchConventionAssetRoute({
1004
+ file: matched.route.file,
1093
1005
  request: options.request,
1094
1006
  });
1095
- finishRenderTimingPhase(timing, phaseStartedAt, "routeCacheMs");
1007
+ }
1096
1008
 
1097
- if (cachedResponse !== undefined) {
1098
- return cachedResponse;
1099
- }
1009
+ if (matched.route.kind === "metadata") {
1010
+ return await dispatchMetadataRoute({
1011
+ appDir: options.appDir,
1012
+ file: matched.route.file,
1013
+ importPolicy: options.importPolicy,
1014
+ params: matched.params,
1015
+ request: options.request,
1016
+ route: matched.route,
1017
+ serverModules: options.serverModules,
1018
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1019
+ serverSourceFiles: options.serverSourceFiles,
1020
+ vitePlugins: options.vitePlugins,
1021
+ });
1022
+ }
1100
1023
 
1101
- // Only a route whose result may be stored pays for header-read tracking;
1102
- // cache hits returned above never construct it. Application code reads
1103
- // headers through this request so that a render depending on one is never
1104
- // stored under a key that ignores it. Callers that store by path alone ask
1105
- // for tracking through `renderSignals` even when the route itself declares
1106
- // no cache policy.
1107
- trackedRequest =
1108
- mayUseRouteCache || options.renderSignals !== undefined
1109
- ? trackRequestHeaderReads(options.request)
1110
- : undefined;
1111
- const appRequest = trackedRequest?.request ?? options.request;
1112
-
1113
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1114
- const preparedActions = await prepareRouteServerActions({
1115
- appDir: options.appDir,
1116
- code: originalCode,
1117
- formActionReferences: options.serverActionReferencesByFile?.get(matched.route.file),
1118
- placeholders: true,
1119
- pageFile: matched.route.file,
1120
- request: options.request,
1121
- });
1122
- for (const diagnostic of preparedActions.diagnostics ?? []) {
1123
- console.warn(`${diagnostic.code}: ${diagnostic.message}`);
1124
- }
1125
- finishRenderTimingPhase(timing, phaseStartedAt, "serverActionsMs");
1126
- const code = preparedActions.code;
1127
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1128
- const routeAnalysis =
1129
- code === originalCode
1130
- ? originalAnalysis
1131
- : await analyzeRouteSource({
1024
+ if (matched.route.kind === "server") {
1025
+ return await dispatchServerRoute({
1132
1026
  appDir: options.appDir,
1133
- buildRenderCode: preparedActions.htmlReplacements !== undefined,
1134
- code,
1135
- filename: matched.route.file,
1136
- routePath: matched.route.path,
1137
- clientRouteInferenceCache,
1138
- serverModuleCacheVersion: undefined,
1027
+ env: options.env,
1028
+ file: matched.route.file,
1029
+ importPolicy: options.importPolicy,
1030
+ params: matched.params,
1031
+ request: options.request,
1032
+ route: matched.route,
1033
+ serverModules: options.serverModules,
1034
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1139
1035
  serverSourceFiles: options.serverSourceFiles,
1140
- define: options.define,
1141
1036
  vitePlugins: options.vitePlugins,
1142
1037
  });
1143
- finishRenderTimingPhase(timing, phaseStartedAt, "routeCodeAnalysisMs");
1144
- const routeCode = routeAnalysis.routeCode;
1145
- const streamRoute = routeAnalysis.streamRoute;
1146
- const clientInference = routeAnalysis.clientInference;
1147
- const clientRoute = clientInference.client;
1148
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1149
- const loaderReady = routeAnalysis.hasLoader ? createDeferredSignal() : undefined;
1150
- const dataPromise = routeAnalysis.hasLoader
1151
- ? loadRouteDataWithInstrumentation({
1038
+ }
1039
+
1040
+ // Issue 080: page routes render HTML for GET / HEAD only. Other
1041
+ // methods (PUT, PATCH, DELETE, PROPFIND, ...) get 405 with an
1042
+ // Allow header so the response shape complies with RFC 9110 §9
1043
+ // and so caching intermediaries do not cross-cache method results.
1044
+ const methodResponse = pageRouteMethodResponse(options.request.method);
1045
+ if (methodResponse !== undefined) {
1046
+ return methodResponse;
1047
+ }
1048
+
1049
+ const clientScript = options.clientScripts?.get(matched.route.path);
1050
+ const clientStyleSheets = options.clientStyles?.get(matched.route.path);
1051
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1052
+ const originalCode = await readServerSourceFile(
1053
+ matched.route.file,
1054
+ options.serverModuleCacheVersion,
1055
+ options.serverSourceFiles,
1056
+ );
1057
+ finishRenderTimingPhase(timing, phaseStartedAt, "readSourceMs");
1058
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1059
+ const originalAnalysis = await analyzeRouteSource({
1152
1060
  appDir: options.appDir,
1061
+ artifact: options.serverModules?.get(matched.route.file)?.analysis,
1153
1062
  code: originalCode,
1154
- context: withTrackedRequest({
1155
- env: options.env,
1156
- params: matched.params,
1157
- queryClient,
1158
- }, appRequest, trackedRequest),
1159
1063
  filename: matched.route.file,
1160
- importPolicy: options.importPolicy,
1161
- instrumentation: options.instrumentation,
1162
- devServerModuleCacheVersion: options.devServerModuleCacheVersion,
1163
- request: options.request,
1164
- routeId: routeIdForPath(matched.route.path),
1165
1064
  routePath: matched.route.path,
1166
- serverModules: options.serverModules,
1065
+ clientRouteInferenceCache,
1167
1066
  serverModuleCacheVersion: options.serverModuleCacheVersion,
1067
+ serverSourceFiles: options.serverSourceFiles,
1168
1068
  define: options.define,
1169
- vitePlugins: options.vitePlugins,
1170
1069
  timing,
1171
- onLoaderReady: loaderReady?.resolve,
1172
- })
1173
- : undefined;
1174
- finishRenderTimingPhase(timing, phaseStartedAt, "loaderStartMs");
1175
- recoveryRoute = {
1176
- clientRoute,
1177
- props: {
1178
- params: matched.params,
1179
- request: { url: options.request.url },
1180
- },
1181
- routePath: matched.route.path,
1182
- script: clientScript,
1183
- };
1184
- if (streamRoute) {
1185
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1186
- const loadingFile = await nearestExistingBoundaryFileForPage({
1187
- appDir: options.appDir,
1188
- filename: "loading.mreact.tsx",
1189
- pageFile: matched.route.file,
1190
- serverSourceFiles: options.serverSourceFiles,
1191
- });
1192
- finishRenderTimingPhase(timing, phaseStartedAt, "loadingBoundaryLookupMs");
1193
- const streamShellResponseHeaders = {
1194
- "content-type": "text/html; charset=utf-8",
1195
- "x-mreact-stream": "1",
1196
- };
1197
-
1198
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1199
- const mayRenderOutOfOrder = await mayRenderOutOfOrderBoundaryDeep({
1200
- code: routeCode,
1201
- filename: matched.route.file,
1202
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1203
- serverSourceFiles: options.serverSourceFiles,
1204
- });
1205
- finishRenderTimingPhase(timing, phaseStartedAt, "outOfOrderAnalysisMs");
1070
+ vitePlugins: options.vitePlugins,
1071
+ });
1072
+ sourceUsesRequestInput = originalAnalysis.usesRequestInput;
1073
+ finishRenderTimingPhase(timing, phaseStartedAt, "sourceAnalysisMs");
1074
+ const cachePolicy = originalAnalysis.cachePolicy;
1075
+ const navigationScript = options.navigationScripts?.get(matched.route.path);
1076
+ const navigationRequest = isNavigationRequest(options.request);
1077
+ const cacheKey = routeCacheKey(
1078
+ options.appDir,
1079
+ matched.route.path,
1080
+ url,
1081
+ navigationRequest ? "navigation" : "document",
1082
+ );
1083
+ const mayUseRouteCache =
1084
+ !sourceUsesRequestInput &&
1085
+ (cachePolicy === undefined
1086
+ ? originalAnalysis.usesRuntimeCacheControl
1087
+ : cachePolicy.revalidateSeconds !== 0);
1088
+ const reloadRouteCache = isNavigationRouteCacheReloadRequest(options.request);
1089
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1090
+ const cachedResponse =
1091
+ !mayUseRouteCache || reloadRouteCache
1092
+ ? undefined
1093
+ : await cachedRouteResponse({
1094
+ cache: options.routeCache,
1095
+ key: cacheKey,
1096
+ request: options.request,
1097
+ });
1098
+ finishRenderTimingPhase(timing, phaseStartedAt, "routeCacheMs");
1099
+
1100
+ if (cachedResponse !== undefined) {
1101
+ return cachedResponse;
1102
+ }
1103
+
1104
+ // Only a route whose result may be stored pays for header-read tracking;
1105
+ // cache hits returned above never construct it. Application code reads
1106
+ // headers through this request so that a render depending on one is never
1107
+ // stored under a key that ignores it. Callers that store by path alone ask
1108
+ // for tracking through `renderSignals` even when the route itself declares
1109
+ // no cache policy.
1110
+ trackedRequest =
1111
+ mayUseRouteCache || options.renderSignals !== undefined
1112
+ ? trackRequestHeaderReads(options.request)
1113
+ : undefined;
1114
+ const appRequest = trackedRequest?.request ?? options.request;
1115
+
1116
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1117
+ const preparedActions = await prepareRouteServerActions({
1118
+ appDir: options.appDir,
1119
+ code: originalCode,
1120
+ formActionReferences: options.serverActionReferencesByFile?.get(matched.route.file),
1121
+ placeholders: true,
1122
+ pageFile: matched.route.file,
1123
+ request: options.request,
1124
+ });
1125
+ for (const diagnostic of preparedActions.diagnostics ?? []) {
1126
+ console.warn(`${diagnostic.code}: ${diagnostic.message}`);
1127
+ }
1128
+ finishRenderTimingPhase(timing, phaseStartedAt, "serverActionsMs");
1129
+ const code = preparedActions.code;
1130
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1131
+ const routeAnalysis =
1132
+ code === originalCode
1133
+ ? originalAnalysis
1134
+ : await analyzeRouteSource({
1135
+ appDir: options.appDir,
1136
+ buildRenderCode: preparedActions.htmlReplacements !== undefined,
1137
+ code,
1138
+ filename: matched.route.file,
1139
+ routePath: matched.route.path,
1140
+ clientRouteInferenceCache,
1141
+ serverModuleCacheVersion: undefined,
1142
+ serverSourceFiles: options.serverSourceFiles,
1143
+ define: options.define,
1144
+ vitePlugins: options.vitePlugins,
1145
+ });
1146
+ finishRenderTimingPhase(timing, phaseStartedAt, "routeCodeAnalysisMs");
1147
+ const routeCode = routeAnalysis.routeCode;
1148
+ const streamRoute = routeAnalysis.streamRoute;
1149
+ const clientInference = routeAnalysis.clientInference;
1150
+ const clientRoute = clientInference.client;
1151
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1152
+ const dataPromise = routeAnalysis.hasLoader
1153
+ ? loadRouteDataWithInstrumentation({
1154
+ appDir: options.appDir,
1155
+ code: originalCode,
1156
+ context: withTrackedRequest(
1157
+ {
1158
+ env: options.env,
1159
+ params: matched.params,
1160
+ queryClient,
1161
+ },
1162
+ appRequest,
1163
+ trackedRequest,
1164
+ ),
1165
+ filename: matched.route.file,
1166
+ importPolicy: options.importPolicy,
1167
+ instrumentation: options.instrumentation,
1168
+ devServerModuleCacheVersion: options.devServerModuleCacheVersion,
1169
+ request: options.request,
1170
+ routeId: routeIdForPath(matched.route.path),
1171
+ routePath: matched.route.path,
1172
+ serverModules: options.serverModules,
1173
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1174
+ define: options.define,
1175
+ vitePlugins: options.vitePlugins,
1176
+ timing,
1177
+ })
1178
+ : undefined;
1179
+ // Route analysis and loading-boundary discovery can continue before the loader is awaited.
1180
+ // Observe an early rejection immediately so strict unhandled-rejection runtimes do not
1181
+ // terminate before the request-level control/error handler receives the same promise.
1182
+ void dataPromise?.catch(() => {});
1183
+ finishRenderTimingPhase(timing, phaseStartedAt, "loaderStartMs");
1184
+ recoveryRoute = {
1185
+ clientRoute,
1186
+ props: {
1187
+ params: matched.params,
1188
+ request: { url: url.pathname },
1189
+ },
1190
+ routePath: matched.route.path,
1191
+ script: clientScript,
1192
+ };
1193
+ if (streamRoute) {
1194
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1195
+ const loadingFile = await nearestExistingBoundaryFileForPage({
1196
+ appDir: options.appDir,
1197
+ filename: "loading.mreact.tsx",
1198
+ pageFile: matched.route.file,
1199
+ serverSourceFiles: options.serverSourceFiles,
1200
+ });
1201
+ finishRenderTimingPhase(timing, phaseStartedAt, "loadingBoundaryLookupMs");
1202
+ const streamShellResponseHeaders = {
1203
+ "content-type": "text/html; charset=utf-8",
1204
+ "x-mreact-stream": "1",
1205
+ };
1206
+
1207
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1208
+ const mayRenderOutOfOrder = await mayRenderOutOfOrderBoundaryDeep({
1209
+ code: routeCode,
1210
+ filename: matched.route.file,
1211
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1212
+ serverSourceFiles: options.serverSourceFiles,
1213
+ });
1214
+ finishRenderTimingPhase(timing, phaseStartedAt, "outOfOrderAnalysisMs");
1215
+
1216
+ if (loadingFile === undefined && !mayRenderOutOfOrder) {
1217
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1218
+ let data: unknown;
1219
+ try {
1220
+ data = dataPromise === undefined ? undefined : await dataPromise;
1221
+ } finally {
1222
+ finishRenderTimingPhase(timing, phaseStartedAt, "loaderWaitMs");
1223
+ }
1224
+ if (data instanceof Response) {
1225
+ emitRenderTiming(options, timing, data.status);
1226
+ return data;
1227
+ }
1228
+ await waitForRenderPreload(options, timing);
1229
+ await loadServerRenderArtifacts(options, matched.route.file, timing);
1230
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1231
+ const stringOutput = transformServerModule({
1232
+ code: routeCode,
1233
+ clientBoundaryImports: clientInference.clientBoundaryImports,
1234
+ clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
1235
+ filename: matched.route.file,
1236
+ serverModules: options.serverModules,
1237
+ serverOutput: "string",
1238
+ });
1239
+ finishRenderTimingPhase(timing, phaseStartedAt, "stringTransformMs");
1240
+ const stringFatalDiagnostics = fatalServerDiagnostics(stringOutput.diagnostics);
1241
+
1242
+ if (stringFatalDiagnostics.length > 0) {
1243
+ return new Response(
1244
+ formatServerDiagnostics(matched.route.file, stringFatalDiagnostics),
1245
+ {
1246
+ status: 500,
1247
+ headers: { "content-type": "text/plain; charset=utf-8" },
1248
+ },
1249
+ );
1250
+ }
1251
+
1252
+ const renderedPage = await runWithQueryClient(queryClient, () =>
1253
+ runServerModuleWithSlots(
1254
+ stringOutput.code,
1255
+ withTrackedRequest(
1256
+ {
1257
+ data,
1258
+ params: matched.params,
1259
+ queryClient,
1260
+ },
1261
+ appRequest,
1262
+ trackedRequest,
1263
+ ),
1264
+ matched.route.file,
1265
+ options.serverModules,
1266
+ options.serverModuleCacheVersion,
1267
+ options.define,
1268
+ options.vitePlugins,
1269
+ undefined,
1270
+ options.importPolicy,
1271
+ options.devServerModuleCacheVersion,
1272
+ ),
1273
+ );
1274
+ const pageHtml = renderedPage.html;
1275
+ const pageHtmlForLayout = clientRoute
1276
+ ? withHydrationMarkers({
1277
+ assetBaseUrl: options.assetBaseUrl,
1278
+ clientReferenceManifest: stringOutput.metadata.clientReferenceManifest,
1279
+ html: pageHtml,
1280
+ routePath: matched.route.path,
1281
+ script: clientScript,
1282
+ props: {
1283
+ params: matched.params,
1284
+ request: { url: url.pathname },
1285
+ data,
1286
+ },
1287
+ })
1288
+ : withServerOnlyRouteMarkers(
1289
+ pageHtml,
1290
+ matched.route.path,
1291
+ options.request,
1292
+ options.prerenderVariantCapture === true,
1293
+ );
1294
+ let html = await runWithQueryClient(queryClient, () =>
1295
+ applyLayouts({
1296
+ appDir: options.appDir,
1297
+ pageFile: matched.route.file,
1298
+ html: pageHtmlForLayout,
1299
+ props: withTrackedRequest(
1300
+ {
1301
+ data,
1302
+ params: matched.params,
1303
+ queryClient,
1304
+ },
1305
+ appRequest,
1306
+ trackedRequest,
1307
+ ),
1308
+ slots: renderedPage.slots,
1309
+ serverModules: options.serverModules,
1310
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1311
+ serverSourceFiles: options.serverSourceFiles,
1312
+ clientRouteInferenceCache,
1313
+ timing,
1314
+ vitePlugins: options.vitePlugins,
1315
+ importPolicy: options.importPolicy,
1316
+ }),
1317
+ );
1318
+ const metadata = await loadComposedRouteMetadata({
1319
+ appDir: options.appDir,
1320
+ code: originalCode,
1321
+ context: withTrackedRequest(
1322
+ {
1323
+ data,
1324
+ params: matched.params,
1325
+ },
1326
+ appRequest,
1327
+ trackedRequest,
1328
+ ),
1329
+ filename: matched.route.file,
1330
+ importPolicy: options.importPolicy,
1331
+ routes,
1332
+ serverModules: options.serverModules,
1333
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1334
+ devServerModuleCacheVersion: options.devServerModuleCacheVersion,
1335
+ serverSourceFiles: options.serverSourceFiles,
1336
+ define: options.define,
1337
+ vitePlugins: options.vitePlugins,
1338
+ });
1339
+ html = injectHeadMetadata(html, metadata);
1340
+ warnIfCspNonceWouldBlockInlineTags({
1341
+ html,
1342
+ logger: options.logger,
1343
+ metadata,
1344
+ request: options.request,
1345
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1346
+ });
1347
+ html = injectAuthSessionClaims(
1348
+ html,
1349
+ originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined,
1350
+ );
1351
+ html = injectQueryState(html, dehydrate(queryClient, options.dehydrateOptions));
1352
+ const response = withOptionalActionCookie(
1353
+ htmlResponse(
1354
+ applyActionHtmlReplacements(
1355
+ `<!DOCTYPE html>${clientNavigationHeadTags({
1356
+ assetBaseUrl: options.assetBaseUrl,
1357
+ currentStyleSheets: clientStyleSheets,
1358
+ currentScript: clientRoute ? clientScript : undefined,
1359
+ currentNavigationScript: navigationScript,
1360
+ routeScripts: options.clientScripts,
1361
+ })}${html}`,
1362
+ preparedActions.htmlReplacements,
1363
+ ),
1364
+ {
1365
+ headers: responseHeadersForMetadata(metadata, options.request, {
1366
+ "x-mreact-stream": "1",
1367
+ }),
1368
+ },
1369
+ ),
1370
+ preparedActions.csrfToken,
1371
+ preparedActions.csrfTokenIsNew === true,
1372
+ );
1373
+ appendNavigationVary(response.headers);
1374
+ emitRenderTiming(options, timing, response.status);
1375
+ return response;
1376
+ }
1377
+
1378
+ let streamData: unknown;
1379
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1380
+ try {
1381
+ streamData = dataPromise === undefined ? undefined : await dataPromise;
1382
+ } finally {
1383
+ finishRenderTimingPhase(timing, phaseStartedAt, "loaderWaitMs");
1384
+ }
1385
+ if (streamData instanceof Response) {
1386
+ emitRenderTiming(options, timing, streamData.status);
1387
+ return streamData;
1388
+ }
1389
+ const streamDataPromise = Promise.resolve(streamData);
1390
+
1391
+ const streamDataSettlement =
1392
+ loadingFile === undefined ? undefined : observePromiseSettlement(streamDataPromise);
1393
+
1394
+ await waitForRenderPreload(options, timing);
1395
+ await loadServerRenderArtifacts(options, matched.route.file, timing);
1396
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1397
+ const output = transformServerModule({
1398
+ code: routeCode,
1399
+ clientBoundaryImports: clientInference.clientBoundaryImports,
1400
+ clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
1401
+ filename: matched.route.file,
1402
+ serverModules: options.serverModules,
1403
+ serverOutput: "stream",
1404
+ serverAwaitHydration: clientRoute,
1405
+ });
1406
+ finishRenderTimingPhase(timing, phaseStartedAt, "streamTransformMs");
1407
+ const fatalDiagnostics = fatalServerDiagnostics(output.diagnostics);
1408
+
1409
+ if (fatalDiagnostics.length > 0) {
1410
+ return new Response(formatServerDiagnostics(matched.route.file, fatalDiagnostics), {
1411
+ status: 500,
1412
+ headers: { "content-type": "text/plain; charset=utf-8" },
1413
+ });
1414
+ }
1415
+
1416
+ if (loadingFile !== undefined && streamDataSettlement !== undefined) {
1417
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1418
+ const errorFile = await nearestExistingBoundaryFileForPage({
1419
+ appDir: options.appDir,
1420
+ filename: "error.mreact.tsx",
1421
+ pageFile: matched.route.file,
1422
+ serverSourceFiles: options.serverSourceFiles,
1423
+ });
1424
+ const streamResult = await runServerStreamModuleWithLoading(output.code, {
1425
+ appDir: options.appDir,
1426
+ assetBaseUrl: options.assetBaseUrl,
1427
+ clientRoute,
1428
+ data: streamDataPromise,
1429
+ dataSettlement: streamDataSettlement,
1430
+ define: options.define,
1431
+ errorFile,
1432
+ loadingFile,
1433
+ markerRequest: options.request,
1434
+ onRenderError: options.onRenderError,
1435
+ prerenderVariantCapture: options.prerenderVariantCapture,
1436
+ pageFile: matched.route.file,
1437
+ params: matched.params,
1438
+ queryClient,
1439
+ request: appRequest,
1440
+ requestUrl: url.pathname,
1441
+ trackedRequest,
1442
+ routePath: matched.route.path,
1443
+ routeScripts: options.clientScripts,
1444
+ serverModules: options.serverModules,
1445
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1446
+ serverSourceFiles: options.serverSourceFiles,
1447
+ vitePlugins: options.vitePlugins,
1448
+ clientRouteInferenceCache,
1449
+ importPolicy: options.importPolicy,
1450
+ script: clientScript,
1451
+ clientReferenceManifest: output.metadata.clientReferenceManifest,
1452
+ });
1453
+ finishRenderTimingPhase(timing, phaseStartedAt, "streamConstructionMs");
1454
+
1455
+ if (streamResult instanceof Response) {
1456
+ emitRenderTiming(options, timing, streamResult.status);
1457
+ return streamResult;
1458
+ }
1459
+
1460
+ const response = withOptionalActionCookie(
1461
+ new Response(streamResult, {
1462
+ headers: streamShellResponseHeaders,
1463
+ }),
1464
+ preparedActions.csrfToken,
1465
+ preparedActions.csrfTokenIsNew === true,
1466
+ );
1467
+ appendNavigationVary(response.headers);
1468
+ emitRenderTiming(options, timing, response.status);
1469
+ return response;
1470
+ }
1471
+
1472
+ const data = streamData;
1473
+ const props = withTrackedRequest(
1474
+ {
1475
+ data,
1476
+ params: matched.params,
1477
+ queryClient,
1478
+ },
1479
+ appRequest,
1480
+ trackedRequest,
1481
+ );
1482
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1483
+ const stream = runServerStreamModule(output.code, {
1484
+ appDir: options.appDir,
1485
+ assetBaseUrl: options.assetBaseUrl,
1486
+ pageFile: matched.route.file,
1487
+ markerRequest: options.request,
1488
+ prerenderVariantCapture: options.prerenderVariantCapture,
1489
+ props,
1490
+ requestUrl: url.pathname,
1491
+ routePath: matched.route.path,
1492
+ routeScripts: options.clientScripts,
1493
+ serverModules: options.serverModules,
1494
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1495
+ serverSourceFiles: options.serverSourceFiles,
1496
+ define: options.define,
1497
+ vitePlugins: options.vitePlugins,
1498
+ clientRouteInferenceCache,
1499
+ importPolicy: options.importPolicy,
1500
+ clientRoute,
1501
+ script: clientScript,
1502
+ clientReferenceManifest: output.metadata.clientReferenceManifest,
1503
+ });
1504
+ finishRenderTimingPhase(timing, phaseStartedAt, "streamConstructionMs");
1505
+
1506
+ const response = withOptionalActionCookie(
1507
+ new Response(stream, {
1508
+ headers: streamShellResponseHeaders,
1509
+ }),
1510
+ preparedActions.csrfToken,
1511
+ preparedActions.csrfTokenIsNew === true,
1512
+ );
1513
+ appendNavigationVary(response.headers);
1514
+ emitRenderTiming(options, timing, response.status);
1515
+ return response;
1516
+ }
1206
1517
 
1207
- if (loadingFile === undefined && !mayRenderOutOfOrder) {
1208
1518
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
1209
1519
  let data: unknown;
1210
1520
  try {
@@ -1216,10 +1526,16 @@ async function renderAppRequestInternal(
1216
1526
  emitRenderTiming(options, timing, data.status);
1217
1527
  return data;
1218
1528
  }
1529
+ sourceUsesRequestInput ||= await routeShellsMayUseRequestInput({
1530
+ appDir: options.appDir,
1531
+ pageFile: matched.route.file,
1532
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1533
+ serverSourceFiles: options.serverSourceFiles,
1534
+ });
1219
1535
  await waitForRenderPreload(options, timing);
1220
1536
  await loadServerRenderArtifacts(options, matched.route.file, timing);
1221
1537
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
1222
- const stringOutput = transformServerModule({
1538
+ const output = transformServerModule({
1223
1539
  code: routeCode,
1224
1540
  clientBoundaryImports: clientInference.clientBoundaryImports,
1225
1541
  clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
@@ -1228,63 +1544,79 @@ async function renderAppRequestInternal(
1228
1544
  serverOutput: "string",
1229
1545
  });
1230
1546
  finishRenderTimingPhase(timing, phaseStartedAt, "stringTransformMs");
1231
- const stringFatalDiagnostics = fatalServerDiagnostics(stringOutput.diagnostics);
1547
+ const fatalDiagnostics = fatalServerDiagnostics(output.diagnostics);
1232
1548
 
1233
- if (stringFatalDiagnostics.length > 0) {
1234
- return new Response(formatServerDiagnostics(matched.route.file, stringFatalDiagnostics), {
1549
+ if (fatalDiagnostics.length > 0) {
1550
+ return new Response(formatServerDiagnostics(matched.route.file, fatalDiagnostics), {
1235
1551
  status: 500,
1236
1552
  headers: { "content-type": "text/plain; charset=utf-8" },
1237
1553
  });
1238
1554
  }
1239
1555
 
1556
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1240
1557
  const renderedPage = await runWithQueryClient(queryClient, () =>
1241
1558
  runServerModuleWithSlots(
1242
- stringOutput.code,
1243
- withTrackedRequest({
1244
- data,
1245
- params: matched.params,
1246
- queryClient,
1247
- }, appRequest, trackedRequest),
1559
+ output.code,
1560
+ withTrackedRequest(
1561
+ {
1562
+ data,
1563
+ params: matched.params,
1564
+ queryClient,
1565
+ },
1566
+ appRequest,
1567
+ trackedRequest,
1568
+ ),
1248
1569
  matched.route.file,
1249
1570
  options.serverModules,
1250
1571
  options.serverModuleCacheVersion,
1251
1572
  options.define,
1252
1573
  options.vitePlugins,
1253
- undefined,
1574
+ timing,
1254
1575
  options.importPolicy,
1255
1576
  options.devServerModuleCacheVersion,
1256
1577
  ),
1257
1578
  );
1579
+ finishRenderTimingPhase(timing, phaseStartedAt, "pageRenderMs");
1258
1580
  const pageHtml = renderedPage.html;
1581
+ // Wrap the page (not the full document) with the hydration marker so
1582
+ // the marker sits inside <body>, not around <html>. Wrapping <html>
1583
+ // forces the browser HTML parser to strip the wrappers and promote
1584
+ // <head> / <body> children up to the marker, which flattens the
1585
+ // layout into the marker and breaks the hydration target lookup.
1259
1586
  const pageHtmlForLayout = clientRoute
1260
1587
  ? withHydrationMarkers({
1261
1588
  assetBaseUrl: options.assetBaseUrl,
1262
- clientReferenceManifest: stringOutput.metadata.clientReferenceManifest,
1589
+ clientReferenceManifest: output.metadata.clientReferenceManifest,
1263
1590
  html: pageHtml,
1264
1591
  routePath: matched.route.path,
1265
1592
  script: clientScript,
1266
1593
  props: {
1267
1594
  params: matched.params,
1268
- request: { url: options.request.url },
1595
+ request: { url: url.pathname },
1269
1596
  data,
1270
1597
  },
1271
1598
  })
1272
- : isNavigationRequest(options.request)
1273
- ? withRouteMarkers({
1274
- html: pageHtml,
1275
- routePath: matched.route.path,
1276
- })
1277
- : pageHtml;
1599
+ : withServerOnlyRouteMarkers(
1600
+ pageHtml,
1601
+ matched.route.path,
1602
+ options.request,
1603
+ options.prerenderVariantCapture === true,
1604
+ );
1605
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1278
1606
  let html = await runWithQueryClient(queryClient, () =>
1279
1607
  applyLayouts({
1280
1608
  appDir: options.appDir,
1281
1609
  pageFile: matched.route.file,
1282
1610
  html: pageHtmlForLayout,
1283
- props: withTrackedRequest({
1284
- data,
1285
- params: matched.params,
1286
- queryClient,
1287
- }, appRequest, trackedRequest),
1611
+ props: withTrackedRequest(
1612
+ {
1613
+ data,
1614
+ params: matched.params,
1615
+ queryClient,
1616
+ },
1617
+ appRequest,
1618
+ trackedRequest,
1619
+ ),
1288
1620
  slots: renderedPage.slots,
1289
1621
  serverModules: options.serverModules,
1290
1622
  serverModuleCacheVersion: options.serverModuleCacheVersion,
@@ -1292,16 +1624,21 @@ async function renderAppRequestInternal(
1292
1624
  clientRouteInferenceCache,
1293
1625
  timing,
1294
1626
  vitePlugins: options.vitePlugins,
1295
- importPolicy: options.importPolicy,
1296
1627
  }),
1297
1628
  );
1629
+ finishRenderTimingPhase(timing, phaseStartedAt, "layoutRenderMs");
1630
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1298
1631
  const metadata = await loadComposedRouteMetadata({
1299
1632
  appDir: options.appDir,
1300
1633
  code: originalCode,
1301
- context: withTrackedRequest({
1302
- data,
1303
- params: matched.params,
1304
- }, appRequest, trackedRequest),
1634
+ context: withTrackedRequest(
1635
+ {
1636
+ data,
1637
+ params: matched.params,
1638
+ },
1639
+ appRequest,
1640
+ trackedRequest,
1641
+ ),
1305
1642
  filename: matched.route.file,
1306
1643
  importPolicy: options.importPolicy,
1307
1644
  routes,
@@ -1312,6 +1649,8 @@ async function renderAppRequestInternal(
1312
1649
  define: options.define,
1313
1650
  vitePlugins: options.vitePlugins,
1314
1651
  });
1652
+ finishRenderTimingPhase(timing, phaseStartedAt, "metadataMs");
1653
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1315
1654
  html = injectHeadMetadata(html, metadata);
1316
1655
  warnIfCspNonceWouldBlockInlineTags({
1317
1656
  html,
@@ -1324,7 +1663,8 @@ async function renderAppRequestInternal(
1324
1663
  html,
1325
1664
  originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined,
1326
1665
  );
1327
- html = injectQueryState(html, dehydrate(queryClient));
1666
+ html = injectQueryState(html, dehydrate(queryClient, options.dehydrateOptions));
1667
+
1328
1668
  const response = withOptionalActionCookie(
1329
1669
  htmlResponse(
1330
1670
  applyActionHtmlReplacements(
@@ -1338,408 +1678,122 @@ async function renderAppRequestInternal(
1338
1678
  preparedActions.htmlReplacements,
1339
1679
  ),
1340
1680
  {
1341
- headers: responseHeadersForMetadata(metadata, options.request, {
1342
- "x-mreact-stream": "1",
1343
- }),
1681
+ headers: responseHeadersForMetadata(metadata, options.request),
1344
1682
  },
1345
1683
  ),
1346
1684
  preparedActions.csrfToken,
1347
1685
  preparedActions.csrfTokenIsNew === true,
1348
1686
  );
1349
- emitRenderTiming(options, timing, response.status);
1350
- return response;
1351
- }
1352
-
1353
- let streamData: unknown;
1354
- let streamDataPromise = dataPromise ?? Promise.resolve(undefined);
1355
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1356
- if (loadingFile === undefined || routeLoaderMayReturnControlResponse(originalCode)) {
1357
- try {
1358
- streamData = dataPromise === undefined ? undefined : await dataPromise;
1359
- } finally {
1360
- finishRenderTimingPhase(timing, phaseStartedAt, "loaderWaitMs");
1361
- }
1362
- if (streamData instanceof Response) {
1363
- emitRenderTiming(options, timing, streamData.status);
1364
- return streamData;
1365
- }
1366
- streamDataPromise = Promise.resolve(streamData);
1367
- } else {
1368
- try {
1369
- const settledData = await readSettledPromiseAtNextTask(streamDataPromise);
1370
- if (settledData.settled) {
1371
- if (settledData.value instanceof Response) {
1372
- emitRenderTiming(options, timing, settledData.value.status);
1373
- return settledData.value;
1374
- }
1375
- streamDataPromise = Promise.resolve(settledData.value);
1376
- } else {
1377
- await waitForLoaderReadyOrSettled(streamDataPromise, loaderReady?.promise);
1378
- }
1379
- } finally {
1380
- finishRenderTimingPhase(timing, phaseStartedAt, "loaderWaitMs");
1687
+ appendNavigationVary(response.headers);
1688
+
1689
+ const effectiveCachePolicy = cachePolicy ?? activeRouteCacheContext()?.cachePolicy;
1690
+ const configuredHsts = configuredHstsHeader(metadata?.security);
1691
+ renderedStrictTransportSecurity = configuredHsts;
1692
+ invalidConfiguredHsts = hasInvalidConfiguredHsts(metadata?.security);
1693
+
1694
+ const finalResponse = preparedActions.hasFormActions
1695
+ ? withRouteCacheHeader(response, effectiveCachePolicy)
1696
+ : await cacheRouteResponse({
1697
+ key: cacheKey,
1698
+ cache: options.routeCache,
1699
+ // A policy can still arrive from a runtime cacheControl() call made
1700
+ // outside the page source, which is what `mayUseRouteCache` scans.
1701
+ // Without a tracker there is no evidence the render ignored request
1702
+ // headers, so the entry is treated as header dependent.
1703
+ headerDependent:
1704
+ sourceUsesRequestInput ||
1705
+ invalidConfiguredHsts ||
1706
+ (trackedRequest?.requestDependent() ?? true),
1707
+ ...(configuredHsts === undefined ? {} : { strictTransportSecurity: configuredHsts }),
1708
+ path: matched.route.path,
1709
+ policy: effectiveCachePolicy,
1710
+ request: options.request,
1711
+ response,
1712
+ });
1713
+ finishRenderTimingPhase(timing, phaseStartedAt, "responseBuildMs");
1714
+ emitRenderTiming(options, timing, finalResponse.status);
1715
+ return finalResponse;
1716
+ } catch (error) {
1717
+ if (isRedirectError(error)) {
1718
+ const response = new Response(null, {
1719
+ headers: { location: error.location },
1720
+ status: error.status,
1721
+ });
1722
+ emitRenderTiming(options, timing, response.status);
1723
+ return response;
1381
1724
  }
1382
- }
1383
1725
 
1384
- await waitForRenderPreload(options, timing);
1385
- await loadServerRenderArtifacts(options, matched.route.file, timing);
1386
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1387
- const output = transformServerModule({
1388
- code: routeCode,
1389
- clientBoundaryImports: clientInference.clientBoundaryImports,
1390
- clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
1391
- filename: matched.route.file,
1392
- serverModules: options.serverModules,
1393
- serverOutput: "stream",
1394
- serverAwaitHydration: clientRoute,
1395
- });
1396
- finishRenderTimingPhase(timing, phaseStartedAt, "streamTransformMs");
1397
- const fatalDiagnostics = fatalServerDiagnostics(output.diagnostics);
1726
+ if (isNotFoundError(error)) {
1727
+ const notFoundFile = await nearestBoundaryFileForPage({
1728
+ appDir: options.appDir,
1729
+ filename: "not-found.mreact.tsx",
1730
+ pageFile: matched.route.file,
1731
+ });
1398
1732
 
1399
- if (fatalDiagnostics.length > 0) {
1400
- return new Response(formatServerDiagnostics(matched.route.file, fatalDiagnostics), {
1401
- status: 500,
1402
- headers: { "content-type": "text/plain; charset=utf-8" },
1733
+ const response = await renderSpecialRoute({
1734
+ appDir: options.appDir,
1735
+ assetBaseUrl: options.assetBaseUrl,
1736
+ currentStyleSheets: options.clientStylesByFile?.get(notFoundFile),
1737
+ error: undefined,
1738
+ request: options.request,
1739
+ routePath: matched.route.path,
1740
+ routeFile: notFoundFile,
1741
+ routeScripts: options.clientScripts,
1742
+ serverModules: options.serverModules,
1743
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1744
+ serverSourceFiles: options.serverSourceFiles,
1745
+ vitePlugins: options.vitePlugins,
1746
+ navigation: recoveryRoute,
1747
+ status: 404,
1748
+ textFallback: "Not Found",
1749
+ });
1750
+ emitRenderTiming(options, timing, response.status);
1751
+ return response;
1752
+ }
1753
+
1754
+ const errorFile = await nearestBoundaryFileForPage({
1755
+ appDir: options.appDir,
1756
+ filename: "error.mreact.tsx",
1757
+ pageFile: matched.route.file,
1403
1758
  });
1404
- }
1759
+ options.onRenderError?.(error);
1405
1760
 
1406
- if (loadingFile !== undefined) {
1407
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1408
- const stream = await runServerStreamModuleWithLoading(output.code, {
1761
+ const response = await renderSpecialRoute({
1409
1762
  appDir: options.appDir,
1410
1763
  assetBaseUrl: options.assetBaseUrl,
1411
- clientRoute,
1412
- data: streamDataPromise,
1413
- define: options.define,
1414
- loadingFile,
1415
- pageFile: matched.route.file,
1416
- params: matched.params,
1417
- queryClient,
1418
- request: appRequest,
1419
- trackedRequest,
1764
+ currentStyleSheets: options.clientStylesByFile?.get(errorFile),
1765
+ error,
1766
+ request: options.request,
1420
1767
  routePath: matched.route.path,
1768
+ routeFile: errorFile,
1421
1769
  routeScripts: options.clientScripts,
1422
1770
  serverModules: options.serverModules,
1423
1771
  serverModuleCacheVersion: options.serverModuleCacheVersion,
1424
1772
  serverSourceFiles: options.serverSourceFiles,
1425
1773
  vitePlugins: options.vitePlugins,
1426
- clientRouteInferenceCache,
1427
- importPolicy: options.importPolicy,
1428
- script: clientScript,
1429
- clientReferenceManifest: output.metadata.clientReferenceManifest,
1774
+ navigation: recoveryRoute,
1775
+ status: 500,
1776
+ textFallback: "Internal Server Error",
1430
1777
  });
1431
- finishRenderTimingPhase(timing, phaseStartedAt, "streamConstructionMs");
1432
-
1433
- const response = withOptionalActionCookie(
1434
- new Response(stream, {
1435
- headers: streamShellResponseHeaders,
1436
- }),
1437
- preparedActions.csrfToken,
1438
- preparedActions.csrfTokenIsNew === true,
1439
- );
1440
1778
  emitRenderTiming(options, timing, response.status);
1441
1779
  return response;
1442
- }
1443
-
1444
- const data = streamData;
1445
- const props = withTrackedRequest({
1446
- data,
1447
- params: matched.params,
1448
- queryClient,
1449
- }, appRequest, trackedRequest);
1450
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1451
- const stream = runServerStreamModule(output.code, {
1452
- appDir: options.appDir,
1453
- assetBaseUrl: options.assetBaseUrl,
1454
- pageFile: matched.route.file,
1455
- props,
1456
- routePath: matched.route.path,
1457
- routeScripts: options.clientScripts,
1458
- serverModules: options.serverModules,
1459
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1460
- serverSourceFiles: options.serverSourceFiles,
1461
- define: options.define,
1462
- vitePlugins: options.vitePlugins,
1463
- clientRouteInferenceCache,
1464
- importPolicy: options.importPolicy,
1465
- clientRoute,
1466
- script: clientScript,
1467
- clientReferenceManifest: output.metadata.clientReferenceManifest,
1468
- });
1469
- finishRenderTimingPhase(timing, phaseStartedAt, "streamConstructionMs");
1470
-
1471
- const response = withOptionalActionCookie(
1472
- new Response(stream, {
1473
- headers: streamShellResponseHeaders,
1474
- }),
1475
- preparedActions.csrfToken,
1476
- preparedActions.csrfTokenIsNew === true,
1477
- );
1478
- emitRenderTiming(options, timing, response.status);
1479
- return response;
1480
- }
1481
-
1482
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1483
- let data: unknown;
1484
- try {
1485
- data = dataPromise === undefined ? undefined : await dataPromise;
1486
- } finally {
1487
- finishRenderTimingPhase(timing, phaseStartedAt, "loaderWaitMs");
1488
- }
1489
- if (data instanceof Response) {
1490
- emitRenderTiming(options, timing, data.status);
1491
- return data;
1492
- }
1493
- sourceUsesRequestInput ||= await routeShellsMayUseRequestInput({
1494
- appDir: options.appDir,
1495
- pageFile: matched.route.file,
1496
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1497
- serverSourceFiles: options.serverSourceFiles,
1498
- });
1499
- await waitForRenderPreload(options, timing);
1500
- await loadServerRenderArtifacts(options, matched.route.file, timing);
1501
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1502
- const output = transformServerModule({
1503
- code: routeCode,
1504
- clientBoundaryImports: clientInference.clientBoundaryImports,
1505
- clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
1506
- filename: matched.route.file,
1507
- serverModules: options.serverModules,
1508
- serverOutput: "string",
1509
- });
1510
- finishRenderTimingPhase(timing, phaseStartedAt, "stringTransformMs");
1511
- const fatalDiagnostics = fatalServerDiagnostics(output.diagnostics);
1512
-
1513
- if (fatalDiagnostics.length > 0) {
1514
- return new Response(formatServerDiagnostics(matched.route.file, fatalDiagnostics), {
1515
- status: 500,
1516
- headers: { "content-type": "text/plain; charset=utf-8" },
1517
- });
1518
- }
1519
-
1520
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1521
- const renderedPage = await runWithQueryClient(queryClient, () =>
1522
- runServerModuleWithSlots(
1523
- output.code,
1524
- withTrackedRequest({
1525
- data,
1526
- params: matched.params,
1527
- queryClient,
1528
- }, appRequest, trackedRequest),
1529
- matched.route.file,
1530
- options.serverModules,
1531
- options.serverModuleCacheVersion,
1532
- options.define,
1533
- options.vitePlugins,
1534
- timing,
1535
- options.importPolicy,
1536
- options.devServerModuleCacheVersion,
1537
- ),
1538
- );
1539
- finishRenderTimingPhase(timing, phaseStartedAt, "pageRenderMs");
1540
- const pageHtml = renderedPage.html;
1541
- // Wrap the page (not the full document) with the hydration marker so
1542
- // the marker sits inside <body>, not around <html>. Wrapping <html>
1543
- // forces the browser HTML parser to strip the wrappers and promote
1544
- // <head> / <body> children up to the marker, which flattens the
1545
- // layout into the marker and breaks the hydration target lookup.
1546
- const pageHtmlForLayout = clientRoute
1547
- ? withHydrationMarkers({
1548
- assetBaseUrl: options.assetBaseUrl,
1549
- clientReferenceManifest: output.metadata.clientReferenceManifest,
1550
- html: pageHtml,
1551
- routePath: matched.route.path,
1552
- script: clientScript,
1553
- props: {
1554
- params: matched.params,
1555
- request: { url: options.request.url },
1556
- data,
1557
- },
1558
- })
1559
- : isNavigationRequest(options.request)
1560
- ? withRouteMarkers({
1561
- html: pageHtml,
1562
- routePath: matched.route.path,
1563
- })
1564
- : pageHtml;
1565
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1566
- let html = await runWithQueryClient(queryClient, () =>
1567
- applyLayouts({
1568
- appDir: options.appDir,
1569
- pageFile: matched.route.file,
1570
- html: pageHtmlForLayout,
1571
- props: withTrackedRequest({
1572
- data,
1573
- params: matched.params,
1574
- queryClient,
1575
- }, appRequest, trackedRequest),
1576
- slots: renderedPage.slots,
1577
- serverModules: options.serverModules,
1578
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1579
- serverSourceFiles: options.serverSourceFiles,
1580
- clientRouteInferenceCache,
1581
- timing,
1582
- vitePlugins: options.vitePlugins,
1583
- }),
1584
- );
1585
- finishRenderTimingPhase(timing, phaseStartedAt, "layoutRenderMs");
1586
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1587
- const metadata = await loadComposedRouteMetadata({
1588
- appDir: options.appDir,
1589
- code: originalCode,
1590
- context: withTrackedRequest({
1591
- data,
1592
- params: matched.params,
1593
- }, appRequest, trackedRequest),
1594
- filename: matched.route.file,
1595
- importPolicy: options.importPolicy,
1596
- routes,
1597
- serverModules: options.serverModules,
1598
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1599
- devServerModuleCacheVersion: options.devServerModuleCacheVersion,
1600
- serverSourceFiles: options.serverSourceFiles,
1601
- define: options.define,
1602
- vitePlugins: options.vitePlugins,
1603
- });
1604
- finishRenderTimingPhase(timing, phaseStartedAt, "metadataMs");
1605
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1606
- html = injectHeadMetadata(html, metadata);
1607
- warnIfCspNonceWouldBlockInlineTags({
1608
- html,
1609
- logger: options.logger,
1610
- metadata,
1611
- request: options.request,
1612
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1613
- });
1614
- html = injectAuthSessionClaims(
1615
- html,
1616
- originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined,
1617
- );
1618
- html = injectQueryState(html, dehydrate(queryClient));
1619
-
1620
- const response = withOptionalActionCookie(
1621
- htmlResponse(
1622
- applyActionHtmlReplacements(
1623
- `<!DOCTYPE html>${clientNavigationHeadTags({
1624
- assetBaseUrl: options.assetBaseUrl,
1625
- currentStyleSheets: clientStyleSheets,
1626
- currentScript: clientRoute ? clientScript : undefined,
1627
- currentNavigationScript: navigationScript,
1628
- routeScripts: options.clientScripts,
1629
- })}${html}`,
1630
- preparedActions.htmlReplacements,
1631
- ),
1632
- {
1633
- headers: responseHeadersForMetadata(metadata, options.request),
1634
- },
1635
- ),
1636
- preparedActions.csrfToken,
1637
- preparedActions.csrfTokenIsNew === true,
1638
- );
1639
-
1640
- const effectiveCachePolicy = cachePolicy ?? activeRouteCacheContext()?.cachePolicy;
1641
- const configuredHsts = configuredHstsHeader(metadata?.security);
1642
- invalidConfiguredHsts = hasInvalidConfiguredHsts(metadata?.security);
1643
-
1644
- const finalResponse = preparedActions.hasFormActions
1645
- ? withRouteCacheHeader(response, effectiveCachePolicy)
1646
- : await cacheRouteResponse({
1647
- key: cacheKey,
1648
- cache: options.routeCache,
1649
- // A policy can still arrive from a runtime cacheControl() call made
1650
- // outside the page source, which is what `mayUseRouteCache` scans.
1651
- // Without a tracker there is no evidence the render ignored request
1652
- // headers, so the entry is treated as header dependent.
1653
- headerDependent:
1780
+ } finally {
1781
+ // Reported from every return path, including the streaming ones, so that a
1782
+ // caller storing by path alone never mistakes an unreported render for a
1783
+ // shareable one. The value stays lazy because a streamed render can still
1784
+ // read request headers while its deferred parts resolve; callers read it
1785
+ // after draining the body.
1786
+ if (options.renderSignals !== undefined) {
1787
+ const tracked = trackedRequest;
1788
+ options.renderSignals.headerDependent = () =>
1654
1789
  sourceUsesRequestInput ||
1655
1790
  invalidConfiguredHsts ||
1656
- (trackedRequest?.requestDependent() ?? true),
1657
- ...(configuredHsts === undefined ? {} : { strictTransportSecurity: configuredHsts }),
1658
- path: matched.route.path,
1659
- policy: effectiveCachePolicy,
1660
- request: options.request,
1661
- response,
1662
- });
1663
- finishRenderTimingPhase(timing, phaseStartedAt, "responseBuildMs");
1664
- emitRenderTiming(options, timing, finalResponse.status);
1665
- return finalResponse;
1666
- } catch (error) {
1667
- if (isRedirectError(error)) {
1668
- const response = new Response(null, {
1669
- headers: { location: error.location },
1670
- status: error.status,
1671
- });
1672
- emitRenderTiming(options, timing, response.status);
1673
- return response;
1674
- }
1675
-
1676
- if (isNotFoundError(error)) {
1677
- const notFoundFile = await nearestBoundaryFileForPage({
1678
- appDir: options.appDir,
1679
- filename: "not-found.mreact.tsx",
1680
- pageFile: matched.route.file,
1681
- });
1682
-
1683
- const response = await renderSpecialRoute({
1684
- appDir: options.appDir,
1685
- assetBaseUrl: options.assetBaseUrl,
1686
- currentStyleSheets: options.clientStylesByFile?.get(notFoundFile),
1687
- error: undefined,
1688
- request: options.request,
1689
- routePath: matched.route.path,
1690
- routeFile: notFoundFile,
1691
- routeScripts: options.clientScripts,
1692
- serverModules: options.serverModules,
1693
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1694
- serverSourceFiles: options.serverSourceFiles,
1695
- vitePlugins: options.vitePlugins,
1696
- navigation: recoveryRoute,
1697
- status: 404,
1698
- textFallback: "Not Found",
1699
- });
1700
- emitRenderTiming(options, timing, response.status);
1701
- return response;
1702
- }
1703
-
1704
- const errorFile = await nearestBoundaryFileForPage({
1705
- appDir: options.appDir,
1706
- filename: "error.mreact.tsx",
1707
- pageFile: matched.route.file,
1708
- });
1709
- options.onRenderError?.(error);
1710
-
1711
- const response = await renderSpecialRoute({
1712
- appDir: options.appDir,
1713
- assetBaseUrl: options.assetBaseUrl,
1714
- currentStyleSheets: options.clientStylesByFile?.get(errorFile),
1715
- error,
1716
- request: options.request,
1717
- routePath: matched.route.path,
1718
- routeFile: errorFile,
1719
- routeScripts: options.clientScripts,
1720
- serverModules: options.serverModules,
1721
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1722
- serverSourceFiles: options.serverSourceFiles,
1723
- vitePlugins: options.vitePlugins,
1724
- navigation: recoveryRoute,
1725
- status: 500,
1726
- textFallback: "Internal Server Error",
1727
- });
1728
- emitRenderTiming(options, timing, response.status);
1729
- return response;
1730
- } finally {
1731
- // Reported from every return path, including the streaming ones, so that a
1732
- // caller storing by path alone never mistakes an unreported render for a
1733
- // shareable one. The value stays lazy because a streamed render can still
1734
- // read request headers while its deferred parts resolve; callers read it
1735
- // after draining the body.
1736
- if (options.renderSignals !== undefined) {
1737
- const tracked = trackedRequest;
1738
- options.renderSignals.headerDependent = () =>
1739
- sourceUsesRequestInput || invalidConfiguredHsts || (tracked?.requestDependent() ?? true);
1740
- }
1741
- }
1742
- })).value;
1791
+ (tracked?.requestDependent() ?? true);
1792
+ options.renderSignals.strictTransportSecurity = () => renderedStrictTransportSecurity;
1793
+ }
1794
+ }
1795
+ })
1796
+ ).value;
1743
1797
  }
1744
1798
 
1745
1799
  async function applyAppRouterResponseHook(
@@ -1966,6 +2020,48 @@ function isNavigationRequest(request: Request): boolean {
1966
2020
  return request.headers.get("x-mreact-navigation") === "1";
1967
2021
  }
1968
2022
 
2023
+ function withServerOnlyRouteMarkers(
2024
+ html: string,
2025
+ routePath: string,
2026
+ request: Request,
2027
+ prerenderVariantCapture: boolean,
2028
+ ): string {
2029
+ const marker = serverOnlyRouteMarkerParts(routePath, request, prerenderVariantCapture);
2030
+ return marker === undefined ? html : `${marker.prefix}${html}${marker.suffix}`;
2031
+ }
2032
+
2033
+ function serverOnlyRouteMarkerParts(
2034
+ routePath: string,
2035
+ request: Request,
2036
+ prerenderVariantCapture: boolean,
2037
+ ): { prefix: string; suffix: string } | undefined {
2038
+ if (prerenderVariantCapture) {
2039
+ return prerenderVariantMarkerParts(routePath);
2040
+ }
2041
+ return isNavigationRequest(request) ? routeMarkerParts(routePath) : undefined;
2042
+ }
2043
+
2044
+ export function prerenderVariantMarkerParts(routePath: string): {
2045
+ prefix: string;
2046
+ suffix: string;
2047
+ } {
2048
+ const routeId = routeIdForPath(routePath);
2049
+ return {
2050
+ prefix: `<!--mreact-route-variant-start:${routeId}-->`,
2051
+ suffix: `<!--mreact-route-variant-end:${routeId}-->`,
2052
+ };
2053
+ }
2054
+
2055
+ function appendNavigationVary(headers: Headers): void {
2056
+ const vary = headers.get("vary");
2057
+ if (
2058
+ vary?.split(",").some((value) => value.trim().toLowerCase() === "x-mreact-navigation") === true
2059
+ ) {
2060
+ return;
2061
+ }
2062
+ headers.append("vary", "x-mreact-navigation");
2063
+ }
2064
+
1969
2065
  function isNavigationRouteCacheReloadRequest(request: Request): boolean {
1970
2066
  return (
1971
2067
  isNavigationRequest(request) && request.headers.get("x-mreact-navigation-cache") === "reload"
@@ -2148,15 +2244,7 @@ async function renderSpecialRoute(options: {
2148
2244
  }
2149
2245
 
2150
2246
  const props = {
2151
- data: undefined,
2152
- debug: errorDebugContext(options.error, options.routePath),
2153
- error: normalizeErrorForProps(options.error),
2154
- params: {},
2155
- queryClient: createQueryClient(),
2156
- request: options.request,
2157
- requestId: requestIdForErrorContext(options.request),
2158
- routeId: routeIdForPath(options.routePath ?? new URL(options.request.url).pathname),
2159
- traceId: traceContextFromRequest(options.request)?.traceId,
2247
+ ...errorBoundaryProps(options.error, options.request, options.routePath),
2160
2248
  };
2161
2249
  const pageHtml = await renderServerFileToHtml(
2162
2250
  options.routeFile,
@@ -2205,6 +2293,20 @@ async function renderSpecialRoute(options: {
2205
2293
  );
2206
2294
  }
2207
2295
 
2296
+ function errorBoundaryProps(error: unknown, request: Request, routePath: string | undefined) {
2297
+ return {
2298
+ data: undefined,
2299
+ debug: errorDebugContext(error, routePath),
2300
+ error: normalizeErrorForProps(error),
2301
+ params: {},
2302
+ queryClient: createQueryClient(),
2303
+ request,
2304
+ requestId: requestIdForErrorContext(request),
2305
+ routeId: routeIdForPath(routePath ?? new URL(request.url).pathname),
2306
+ traceId: traceContextFromRequest(request)?.traceId,
2307
+ };
2308
+ }
2309
+
2208
2310
  async function renderServerFileToHtml(
2209
2311
  file: string,
2210
2312
  props: ServerComponentProps,
@@ -3399,8 +3501,11 @@ function runServerStreamModule(
3399
3501
  appDir: string;
3400
3502
  assetBaseUrl?: string | undefined;
3401
3503
  clientRouteInferenceCache: ClientRouteInferenceCache;
3504
+ markerRequest: Request;
3505
+ prerenderVariantCapture?: boolean | undefined;
3402
3506
  pageFile: string;
3403
3507
  props: ServerComponentProps;
3508
+ requestUrl: string;
3404
3509
  routePath: string;
3405
3510
  routeScripts?: ReadonlyMap<string, string> | undefined;
3406
3511
  clientRoute: boolean;
@@ -3445,11 +3550,15 @@ function runServerStreamModule(
3445
3550
  script: options.script,
3446
3551
  props: {
3447
3552
  params: options.props.params,
3448
- request: { url: options.props.request.url },
3553
+ request: { url: options.requestUrl },
3449
3554
  data: options.props.data,
3450
3555
  },
3451
3556
  })
3452
- : undefined;
3557
+ : serverOnlyRouteMarkerParts(
3558
+ options.routePath,
3559
+ options.markerRequest,
3560
+ options.prerenderVariantCapture === true,
3561
+ );
3453
3562
 
3454
3563
  sink.append("<!DOCTYPE html>");
3455
3564
  sink.append(
@@ -3665,12 +3774,18 @@ async function runServerStreamModuleWithLoading(
3665
3774
  clientRoute: boolean;
3666
3775
  clientReferenceManifest?: readonly ClientReferenceMetadata[] | undefined;
3667
3776
  data: Promise<unknown>;
3777
+ dataSettlement: () => ObservedPromiseSettlement<unknown>;
3668
3778
  define?: UserConfig["define"] | undefined;
3779
+ errorFile?: string | undefined;
3669
3780
  loadingFile: string;
3781
+ markerRequest: Request;
3782
+ onRenderError?: ((error: unknown) => void) | undefined;
3783
+ prerenderVariantCapture?: boolean | undefined;
3670
3784
  pageFile: string;
3671
3785
  params: RouteParams;
3672
3786
  queryClient: QueryClient;
3673
3787
  request: Request;
3788
+ requestUrl: string;
3674
3789
  trackedRequest?: TrackedHeaderRequest | undefined;
3675
3790
  routePath: string;
3676
3791
  routeScripts?: ReadonlyMap<string, string> | undefined;
@@ -3681,12 +3796,16 @@ async function runServerStreamModuleWithLoading(
3681
3796
  vitePlugins?: readonly PluginOption[] | undefined;
3682
3797
  importPolicy?: AppRouterImportPolicy | undefined;
3683
3798
  },
3684
- ): Promise<ReadableStream<Uint8Array>> {
3685
- const loadingProps = withTrackedRequest({
3686
- data: undefined,
3687
- params: options.params,
3688
- queryClient: options.queryClient,
3689
- }, options.request, options.trackedRequest);
3799
+ ): Promise<ReadableStream<Uint8Array> | Response> {
3800
+ const loadingProps = withTrackedRequest(
3801
+ {
3802
+ data: undefined,
3803
+ params: options.params,
3804
+ queryClient: options.queryClient,
3805
+ },
3806
+ options.request,
3807
+ options.trackedRequest,
3808
+ );
3690
3809
  const layoutShells = await layoutShellsForPage(
3691
3810
  options.appDir,
3692
3811
  options.pageFile,
@@ -3717,10 +3836,24 @@ async function runServerStreamModuleWithLoading(
3717
3836
  script: options.script,
3718
3837
  props: {
3719
3838
  params: options.params,
3720
- request: { url: options.request.url },
3839
+ request: { url: options.requestUrl },
3721
3840
  },
3722
3841
  })
3723
- : undefined;
3842
+ : serverOnlyRouteMarkerParts(
3843
+ options.routePath,
3844
+ options.markerRequest,
3845
+ options.prerenderVariantCapture === true,
3846
+ );
3847
+
3848
+ const dataSettlement = options.dataSettlement();
3849
+
3850
+ if (dataSettlement.status === "rejected") {
3851
+ throw dataSettlement.error;
3852
+ }
3853
+
3854
+ if (dataSettlement.status === "fulfilled" && dataSettlement.value instanceof Response) {
3855
+ return dataSettlement.value;
3856
+ }
3724
3857
 
3725
3858
  return renderToReadableStream((sink) => {
3726
3859
  sink.append("<!DOCTYPE html>");
@@ -3738,7 +3871,7 @@ async function runServerStreamModuleWithLoading(
3738
3871
 
3739
3872
  sink.append(marker?.prefix ?? "");
3740
3873
 
3741
- renderVisibleOutOfOrderBoundary(
3874
+ renderOutOfOrderBoundary(
3742
3875
  sink,
3743
3876
  "mreact-route",
3744
3877
  options.data,
@@ -3746,11 +3879,15 @@ async function runServerStreamModuleWithLoading(
3746
3879
  await appendServerStreamModule(
3747
3880
  code,
3748
3881
  boundarySink,
3749
- withTrackedRequest({
3750
- data,
3751
- params: options.params,
3752
- queryClient: options.queryClient,
3753
- }, options.request, options.trackedRequest),
3882
+ withTrackedRequest(
3883
+ {
3884
+ data,
3885
+ params: options.params,
3886
+ queryClient: options.queryClient,
3887
+ },
3888
+ options.request,
3889
+ options.trackedRequest,
3890
+ ),
3754
3891
  options.pageFile,
3755
3892
  options.serverModules,
3756
3893
  options.serverModuleCacheVersion,
@@ -3760,6 +3897,34 @@ async function runServerStreamModuleWithLoading(
3760
3897
  );
3761
3898
  },
3762
3899
  {
3900
+ async catch(boundarySink, error) {
3901
+ try {
3902
+ options.onRenderError?.(error);
3903
+ } catch {
3904
+ // Error reporting is best-effort and must not truncate a response that already flushed.
3905
+ }
3906
+
3907
+ if (options.errorFile === undefined) {
3908
+ boundarySink.append("Internal Server Error");
3909
+ return;
3910
+ }
3911
+
3912
+ try {
3913
+ boundarySink.append(
3914
+ await renderServerFileToHtml(
3915
+ options.errorFile,
3916
+ errorBoundaryProps(error, options.request, options.routePath),
3917
+ options.serverModules,
3918
+ options.serverModuleCacheVersion,
3919
+ options.serverSourceFiles,
3920
+ options.define,
3921
+ options.vitePlugins,
3922
+ ),
3923
+ );
3924
+ } catch {
3925
+ boundarySink.append("Internal Server Error");
3926
+ }
3927
+ },
3763
3928
  placeholder(boundarySink) {
3764
3929
  boundarySink.append(loadingHtml);
3765
3930
  },
@@ -3777,70 +3942,6 @@ async function runServerStreamModuleWithLoading(
3777
3942
  });
3778
3943
  }
3779
3944
 
3780
- function renderVisibleOutOfOrderBoundary<T>(
3781
- sink: HtmlSink,
3782
- id: string,
3783
- value: T,
3784
- render: (sink: HtmlSink, value: Awaited<T>) => void | PromiseLike<void>,
3785
- options: {
3786
- catch?: (sink: HtmlSink, error: unknown) => void | PromiseLike<void>;
3787
- placeholder?: (sink: HtmlSink) => void | PromiseLike<void>;
3788
- placeholderTag?: string;
3789
- } = {},
3790
- ): void {
3791
- const placeholderSink = createStringSink();
3792
- void options.placeholder?.(placeholderSink);
3793
- const placeholderTag = normalizeVisibleOutOfOrderPlaceholderTag(options.placeholderTag);
3794
- sink.append(
3795
- `<${placeholderTag} data-mreact-oob-placeholder="${escapeHtmlAttribute(id)}">${placeholderSink.toString()}</${placeholderTag}>`,
3796
- );
3797
-
3798
- const task = renderVisibleOutOfOrderFragment(sink, id, value, render, options);
3799
-
3800
- if (sink.defer === undefined) {
3801
- void task;
3802
- return;
3803
- }
3804
-
3805
- sink.defer(task);
3806
- }
3807
-
3808
- function normalizeVisibleOutOfOrderPlaceholderTag(tag: unknown): string {
3809
- if (typeof tag !== "string") {
3810
- return "span";
3811
- }
3812
-
3813
- const normalized = tag.trim().toLowerCase();
3814
- return /^[a-z][a-z0-9-]*$/.test(normalized) ? normalized : "span";
3815
- }
3816
-
3817
- async function renderVisibleOutOfOrderFragment<T>(
3818
- sink: HtmlSink,
3819
- id: string,
3820
- value: T,
3821
- render: (sink: HtmlSink, value: Awaited<T>) => void | PromiseLike<void>,
3822
- options: {
3823
- catch?: (sink: HtmlSink, error: unknown) => void | PromiseLike<void>;
3824
- },
3825
- ): Promise<void> {
3826
- const fragmentSink = createStringSink();
3827
-
3828
- await renderAsyncBoundary(
3829
- fragmentSink,
3830
- value,
3831
- render,
3832
- options.catch === undefined ? {} : { catch: options.catch },
3833
- );
3834
- await fragmentSink.drain();
3835
-
3836
- sink.append(renderVisibleOutOfOrderFragmentHtml(id, fragmentSink.toString()));
3837
- }
3838
-
3839
- function renderVisibleOutOfOrderFragmentHtml(id: string, html: string): string {
3840
- const escapedId = escapeHtmlAttribute(id);
3841
- return `<template data-mreact-oob-fragment="${escapedId}">${html}</template><mreact-oob-complete hidden data-mreact-oob-complete="${escapedId}"></mreact-oob-complete>`;
3842
- }
3843
-
3844
3945
  async function appendServerStreamModule(
3845
3946
  code: string,
3846
3947
  sink: HtmlSink,
@@ -5053,7 +5154,12 @@ async function loadComposedRouteMetadata(options: {
5053
5154
  composedRouteMetadataCacheCounters,
5054
5155
  );
5055
5156
  if (cached !== undefined) {
5056
- return cached;
5157
+ const entry = await cached;
5158
+ return applyFileConventionMetadataTemplate(
5159
+ entry.metadata,
5160
+ entry.template,
5161
+ options.context.params,
5162
+ );
5057
5163
  }
5058
5164
  }
5059
5165
 
@@ -5063,13 +5169,17 @@ async function loadComposedRouteMetadata(options: {
5063
5169
  setBoundedCacheEntry(
5064
5170
  composedRouteMetadataCache,
5065
5171
  cacheKey,
5066
- Promise.resolve(loaded.metadata),
5172
+ Promise.resolve({ metadata: loaded.metadata, template: loaded.template }),
5067
5173
  maxComposedRouteMetadataCacheEntries,
5068
5174
  composedRouteMetadataCacheCounters,
5069
5175
  );
5070
5176
  }
5071
5177
 
5072
- return loaded.metadata;
5178
+ return applyFileConventionMetadataTemplate(
5179
+ loaded.metadata,
5180
+ loaded.template,
5181
+ options.context.params,
5182
+ );
5073
5183
  } catch (error) {
5074
5184
  if (cacheKey !== undefined) {
5075
5185
  composedRouteMetadataCache.delete(cacheKey);
@@ -5111,7 +5221,11 @@ async function loadComposedRouteMetadataUncached(options: {
5111
5221
  serverModuleCacheVersion?: string | undefined;
5112
5222
  serverSourceFiles?: ReadonlyMap<string, string> | undefined;
5113
5223
  vitePlugins?: readonly PluginOption[] | undefined;
5114
- }): Promise<{ dynamic: boolean; metadata: RouteMetadata | undefined }> {
5224
+ }): Promise<{
5225
+ dynamic: boolean;
5226
+ metadata: RouteMetadata | undefined;
5227
+ template: FileConventionMetadataTemplate;
5228
+ }> {
5115
5229
  const layoutFiles = await shellFilesForPage(
5116
5230
  options.appDir,
5117
5231
  options.filename,
@@ -5174,12 +5288,8 @@ async function loadComposedRouteMetadataUncached(options: {
5174
5288
 
5175
5289
  return {
5176
5290
  dynamic,
5177
- metadata: applyFileConventionMetadata(
5178
- mergeRouteMetadata(metadata),
5179
- options.routes,
5180
- options.filename,
5181
- options.context.params,
5182
- ),
5291
+ metadata: mergeRouteMetadata(metadata),
5292
+ template: fileConventionMetadataTemplate(options.routes, options.filename),
5183
5293
  };
5184
5294
  }
5185
5295