@reckona/mreact-router 0.0.203 → 0.0.205

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 (140) hide show
  1. package/README.md +53 -9
  2. package/dist/actions.d.ts +6 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +70 -10
  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 +2 -1
  11. package/dist/adapters/cloudflare.d.ts.map +1 -1
  12. package/dist/adapters/cloudflare.js +18 -10
  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 +1 -0
  25. package/dist/build.d.ts.map +1 -1
  26. package/dist/build.js +91 -5
  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 +3 -1
  33. package/dist/cache.d.ts.map +1 -1
  34. package/dist/cache.js +211 -13
  35. package/dist/cache.js.map +1 -1
  36. package/dist/cli.js +12 -1
  37. package/dist/cli.js.map +1 -1
  38. package/dist/client.d.ts.map +1 -1
  39. package/dist/client.js +32 -1
  40. package/dist/client.js.map +1 -1
  41. package/dist/config.d.ts +3 -0
  42. package/dist/config.d.ts.map +1 -1
  43. package/dist/config.js +21 -2
  44. package/dist/config.js.map +1 -1
  45. package/dist/csp.d.ts +1 -0
  46. package/dist/csp.d.ts.map +1 -1
  47. package/dist/csp.js +11 -6
  48. package/dist/csp.js.map +1 -1
  49. package/dist/dehydrate-policy.d.ts +3 -0
  50. package/dist/dehydrate-policy.d.ts.map +1 -0
  51. package/dist/dehydrate-policy.js +16 -0
  52. package/dist/dehydrate-policy.js.map +1 -0
  53. package/dist/dev-server.d.ts +7 -2
  54. package/dist/dev-server.d.ts.map +1 -1
  55. package/dist/dev-server.js +120 -11
  56. package/dist/dev-server.js.map +1 -1
  57. package/dist/http.d.ts +1 -1
  58. package/dist/http.d.ts.map +1 -1
  59. package/dist/http.js +147 -7
  60. package/dist/http.js.map +1 -1
  61. package/dist/index.d.ts +4 -3
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/logger.d.ts +22 -1
  66. package/dist/logger.d.ts.map +1 -1
  67. package/dist/logger.js +22 -8
  68. package/dist/logger.js.map +1 -1
  69. package/dist/metadata.d.ts +8 -0
  70. package/dist/metadata.d.ts.map +1 -1
  71. package/dist/metadata.js +60 -22
  72. package/dist/metadata.js.map +1 -1
  73. package/dist/navigation-marker.d.ts.map +1 -1
  74. package/dist/navigation-marker.js +72 -9
  75. package/dist/navigation-marker.js.map +1 -1
  76. package/dist/navigation.d.ts +1 -0
  77. package/dist/navigation.d.ts.map +1 -1
  78. package/dist/navigation.js +1 -1
  79. package/dist/navigation.js.map +1 -1
  80. package/dist/node-server.d.ts +5 -2
  81. package/dist/node-server.d.ts.map +1 -1
  82. package/dist/node-server.js +75 -6
  83. package/dist/node-server.js.map +1 -1
  84. package/dist/prerender-entry.d.ts +14 -0
  85. package/dist/prerender-entry.d.ts.map +1 -1
  86. package/dist/prerender-entry.js +40 -3
  87. package/dist/prerender-entry.js.map +1 -1
  88. package/dist/render.d.ts +9 -1
  89. package/dist/render.d.ts.map +1 -1
  90. package/dist/render.js +102 -123
  91. package/dist/render.js.map +1 -1
  92. package/dist/serve.d.ts +8 -2
  93. package/dist/serve.d.ts.map +1 -1
  94. package/dist/serve.js +22 -20
  95. package/dist/serve.js.map +1 -1
  96. package/dist/session.d.ts.map +1 -1
  97. package/dist/session.js +52 -19
  98. package/dist/session.js.map +1 -1
  99. package/dist/upgrade.d.ts +59 -2
  100. package/dist/upgrade.d.ts.map +1 -1
  101. package/dist/upgrade.js +282 -1
  102. package/dist/upgrade.js.map +1 -1
  103. package/dist/vite-config.d.ts +2 -0
  104. package/dist/vite-config.d.ts.map +1 -1
  105. package/dist/vite-config.js +2 -0
  106. package/dist/vite-config.js.map +1 -1
  107. package/dist/vite.d.ts +4 -1
  108. package/dist/vite.d.ts.map +1 -1
  109. package/dist/vite.js +117 -6
  110. package/dist/vite.js.map +1 -1
  111. package/package.json +11 -11
  112. package/src/actions.ts +81 -11
  113. package/src/adapters/aws-lambda.ts +426 -115
  114. package/src/adapters/cloudflare.ts +29 -13
  115. package/src/adapters/edge.ts +2 -0
  116. package/src/adapters/node.ts +8 -10
  117. package/src/adapters/static.ts +31 -2
  118. package/src/build.ts +117 -10
  119. package/src/built-runtime.ts +28 -0
  120. package/src/cache.ts +262 -15
  121. package/src/cli.ts +14 -1
  122. package/src/client.ts +32 -1
  123. package/src/config.ts +43 -6
  124. package/src/csp.ts +14 -11
  125. package/src/dehydrate-policy.ts +23 -0
  126. package/src/dev-server.ts +188 -15
  127. package/src/http.ts +172 -6
  128. package/src/index.ts +14 -1
  129. package/src/logger.ts +47 -10
  130. package/src/metadata.ts +125 -60
  131. package/src/navigation-marker.ts +79 -9
  132. package/src/navigation.ts +1 -1
  133. package/src/node-server.ts +128 -22
  134. package/src/prerender-entry.ts +52 -3
  135. package/src/render.ts +807 -753
  136. package/src/serve.ts +49 -56
  137. package/src/session.ts +56 -25
  138. package/src/upgrade.ts +402 -3
  139. package/src/vite-config.ts +6 -0
  140. package/src/vite.ts +157 -5
package/src/render.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  import {
23
23
  createStringSink,
24
24
  type HtmlSink,
25
- renderAsyncBoundary,
25
+ renderOutOfOrderBoundary,
26
26
  renderOutOfOrderReorderScript,
27
27
  renderReactNodeToString,
28
28
  renderToReadableStream,
@@ -67,11 +67,9 @@ import {
67
67
  type TrackedHeaderRequest,
68
68
  withTrackedRequest,
69
69
  } from "./request-header-tracking.js";
70
- import {
71
- configuredHstsHeader,
72
- hasInvalidConfiguredHsts,
73
- } from "./security-headers.js";
70
+ import { configuredHstsHeader, hasInvalidConfiguredHsts } from "./security-headers.js";
74
71
  import { resolveRouterCacheLimit } from "./cache-config.js";
72
+ import { protectNonceBearingResponse } from "./prerender-entry.js";
75
73
  import {
76
74
  importAppRouterBuiltFileModule,
77
75
  importAppRouterFileModule,
@@ -130,13 +128,15 @@ import {
130
128
  type RouteMiddlewareControl,
131
129
  } from "./middleware.js";
132
130
  import {
133
- applyFileConventionMetadata,
131
+ applyFileConventionMetadataTemplate,
132
+ fileConventionMetadataTemplate,
134
133
  injectHeadMetadata,
135
134
  mergeRouteMetadata,
136
135
  responseHeadersForMetadata,
137
136
  serializeRobots,
138
137
  serializeSitemap,
139
138
  validateRouteMetadata,
139
+ type FileConventionMetadataTemplate,
140
140
  } from "./metadata.js";
141
141
  import {
142
142
  createSlotRenderContext,
@@ -219,6 +219,7 @@ export interface RenderAppRequestOptions {
219
219
  routeMatcher?: RouteMatcher | undefined;
220
220
  routes?: readonly AppRoute[] | undefined;
221
221
  serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
222
+ serverRenderArtifactLoader?: AppRouterServerRenderArtifactLoader | undefined;
222
223
  serverModuleCacheVersion?: string | undefined;
223
224
  devServerModuleCacheVersion?: string | undefined;
224
225
  // True when serving through a dev server: request-time server transforms
@@ -235,6 +236,14 @@ export interface RenderAppRequestOptions {
235
236
  }
236
237
 
237
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;
238
247
  /**
239
248
  * Receives whether the render depended on the incoming request headers.
240
249
  *
@@ -251,7 +260,6 @@ export interface RenderAppRequestRuntimeOptions extends RenderAppRequestOptions
251
260
  strictTransportSecurity: () => string | undefined;
252
261
  }
253
262
  | undefined;
254
- serverRenderArtifactLoader?: AppRouterServerRenderArtifactLoader | undefined;
255
263
  }
256
264
 
257
265
  export interface AppRouterRenderPreload {
@@ -572,7 +580,12 @@ const middlewareModuleCache = new Map<string, Promise<MiddlewareModule>>();
572
580
  const middlewareModuleCacheCounters = createRouterRuntimeCacheCounters();
573
581
  const serverRouteModuleCache = new Map<string, Promise<Record<string, unknown>>>();
574
582
  const serverRouteModuleCacheCounters = createRouterRuntimeCacheCounters();
575
- 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>>();
576
589
  const composedRouteMetadataCacheCounters = createRouterRuntimeCacheCounters();
577
590
  const maxServerTransformCacheEntries = resolveRouterCacheLimit("SERVER_TRANSFORM", 512);
578
591
  const maxServerSourceFileCacheEntries = resolveRouterCacheLimit("SERVER_SOURCE_FILE", 512);
@@ -695,7 +708,9 @@ export async function renderAppRequest(options: RenderAppRequestOptions): Promis
695
708
  };
696
709
  invokeRouterInstrumentation(options.instrumentation?.onRequestStart, requestEvent);
697
710
  const response = await renderAppRequestInternal({ ...options, requestUrl: url });
698
- const finalResponse = await applyAppRouterResponseHook(response, options);
711
+ const finalResponse = protectNonceBearingResponse(
712
+ await applyAppRouterResponseHook(response, options),
713
+ );
699
714
  invokeRouterInstrumentation(options.instrumentation?.onRequestEnd, {
700
715
  ...requestEvent,
701
716
  status: finalResponse.status,
@@ -724,43 +739,24 @@ function finishRenderTimingPhase(
724
739
  timing.phases[phaseName] = logDurationMs(startedAt);
725
740
  }
726
741
 
727
- async function readSettledPromiseAtNextTask<T>(
728
- promise: Promise<T>,
729
- ): Promise<{ settled: true; value: T } | { settled: false }> {
730
- return await Promise.race([
731
- promise.then((value) => ({ settled: true as const, value })),
732
- new Promise<{ settled: false }>((resolve) => {
733
- setTimeout(() => resolve({ settled: false }), 0);
734
- }),
735
- ]);
736
- }
742
+ type ObservedPromiseSettlement<T> =
743
+ | { status: "pending" }
744
+ | { status: "fulfilled"; value: T }
745
+ | { error: unknown; status: "rejected" };
737
746
 
738
- function createDeferredSignal(): {
739
- promise: Promise<void>;
740
- resolve(): void;
741
- } {
742
- let resolve: (() => void) | undefined;
743
- const promise = new Promise<void>((innerResolve) => {
744
- resolve = innerResolve;
745
- });
747
+ function observePromiseSettlement<T>(promise: Promise<T>): () => ObservedPromiseSettlement<T> {
748
+ let settlement: ObservedPromiseSettlement<T> = { status: "pending" };
746
749
 
747
- return {
748
- promise,
749
- resolve() {
750
- resolve?.();
750
+ void promise.then(
751
+ (value) => {
752
+ settlement = { status: "fulfilled", value };
751
753
  },
752
- };
753
- }
754
-
755
- async function waitForLoaderReadyOrSettled(
756
- dataPromise: Promise<unknown>,
757
- loaderReady: Promise<void> | undefined,
758
- ): Promise<void> {
759
- if (loaderReady === undefined) {
760
- return;
761
- }
754
+ (error: unknown) => {
755
+ settlement = { error, status: "rejected" };
756
+ },
757
+ );
762
758
 
763
- await Promise.race([loaderReady, dataPromise.then(() => undefined, () => undefined)]);
759
+ return () => settlement;
764
760
  }
765
761
 
766
762
  function addRenderTimingPhaseDuration(
@@ -775,14 +771,6 @@ function addRenderTimingPhaseDuration(
775
771
  timing.phases[phaseName] = (timing.phases[phaseName] ?? 0) + logDurationMs(startedAt);
776
772
  }
777
773
 
778
- function routeLoaderMayReturnControlResponse(code: string): boolean {
779
- return (
780
- /\b(?:redirect|redirectExternal|rewrite|notFound|throwNotFound)\s*\(/.test(code) ||
781
- /\bnew\s+Response\s*\(/.test(code) ||
782
- /\bResponse\.(?:redirect|json)\s*\(/.test(code)
783
- );
784
- }
785
-
786
774
  async function waitForRenderPreload(
787
775
  options: Pick<RenderAppRequestOptions, "preload">,
788
776
  timing: RenderTiming | undefined,
@@ -1008,217 +996,525 @@ async function renderAppRequestInternal(
1008
996
  let sourceUsesRequestInput = true;
1009
997
  let invalidConfiguredHsts = false;
1010
998
  let renderedStrictTransportSecurity: string | undefined;
1011
- return (await withRouteCacheContext(options.routeCache, async () => {
1012
- try {
1013
- if (matched.route.kind === "asset") {
1014
- return await dispatchConventionAssetRoute({
1015
- file: matched.route.file,
1016
- request: options.request,
1017
- });
1018
- }
1019
-
1020
- if (matched.route.kind === "metadata") {
1021
- return await dispatchMetadataRoute({
1022
- appDir: options.appDir,
1023
- file: matched.route.file,
1024
- importPolicy: options.importPolicy,
1025
- params: matched.params,
1026
- request: options.request,
1027
- route: matched.route,
1028
- serverModules: options.serverModules,
1029
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1030
- serverSourceFiles: options.serverSourceFiles,
1031
- vitePlugins: options.vitePlugins,
1032
- });
1033
- }
1034
-
1035
- if (matched.route.kind === "server") {
1036
- return await dispatchServerRoute({
1037
- appDir: options.appDir,
1038
- env: options.env,
1039
- file: matched.route.file,
1040
- importPolicy: options.importPolicy,
1041
- params: matched.params,
1042
- request: options.request,
1043
- route: matched.route,
1044
- serverModules: options.serverModules,
1045
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1046
- serverSourceFiles: options.serverSourceFiles,
1047
- vitePlugins: options.vitePlugins,
1048
- });
1049
- }
1050
-
1051
- // Issue 080: page routes render HTML for GET / HEAD only. Other
1052
- // methods (PUT, PATCH, DELETE, PROPFIND, ...) get 405 with an
1053
- // Allow header so the response shape complies with RFC 9110 §9
1054
- // and so caching intermediaries do not cross-cache method results.
1055
- const methodResponse = pageRouteMethodResponse(options.request.method);
1056
- if (methodResponse !== undefined) {
1057
- return methodResponse;
1058
- }
1059
-
1060
- const clientScript = options.clientScripts?.get(matched.route.path);
1061
- const clientStyleSheets = options.clientStyles?.get(matched.route.path);
1062
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1063
- const originalCode = await readServerSourceFile(
1064
- matched.route.file,
1065
- options.serverModuleCacheVersion,
1066
- options.serverSourceFiles,
1067
- );
1068
- finishRenderTimingPhase(timing, phaseStartedAt, "readSourceMs");
1069
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1070
- const originalAnalysis = await analyzeRouteSource({
1071
- appDir: options.appDir,
1072
- artifact: options.serverModules?.get(matched.route.file)?.analysis,
1073
- code: originalCode,
1074
- filename: matched.route.file,
1075
- routePath: matched.route.path,
1076
- clientRouteInferenceCache,
1077
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1078
- serverSourceFiles: options.serverSourceFiles,
1079
- define: options.define,
1080
- timing,
1081
- vitePlugins: options.vitePlugins,
1082
- });
1083
- sourceUsesRequestInput = originalAnalysis.usesRequestInput;
1084
- finishRenderTimingPhase(timing, phaseStartedAt, "sourceAnalysisMs");
1085
- const cachePolicy = originalAnalysis.cachePolicy;
1086
- const navigationScript = options.navigationScripts?.get(matched.route.path);
1087
- const navigationRequest = isNavigationRequest(options.request);
1088
- const cacheKey = routeCacheKey(
1089
- options.appDir,
1090
- matched.route.path,
1091
- url,
1092
- navigationRequest ? "navigation" : "document",
1093
- );
1094
- const mayUseRouteCache =
1095
- !sourceUsesRequestInput &&
1096
- (cachePolicy === undefined
1097
- ? originalAnalysis.usesRuntimeCacheControl
1098
- : cachePolicy.revalidateSeconds !== 0);
1099
- const reloadRouteCache = isNavigationRouteCacheReloadRequest(options.request);
1100
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1101
- const cachedResponse =
1102
- !mayUseRouteCache || reloadRouteCache
1103
- ? undefined
1104
- : await cachedRouteResponse({
1105
- cache: options.routeCache,
1106
- key: cacheKey,
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,
1107
1005
  request: options.request,
1108
1006
  });
1109
- finishRenderTimingPhase(timing, phaseStartedAt, "routeCacheMs");
1007
+ }
1110
1008
 
1111
- if (cachedResponse !== undefined) {
1112
- return cachedResponse;
1113
- }
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
+ }
1114
1023
 
1115
- // Only a route whose result may be stored pays for header-read tracking;
1116
- // cache hits returned above never construct it. Application code reads
1117
- // headers through this request so that a render depending on one is never
1118
- // stored under a key that ignores it. Callers that store by path alone ask
1119
- // for tracking through `renderSignals` even when the route itself declares
1120
- // no cache policy.
1121
- trackedRequest =
1122
- mayUseRouteCache || options.renderSignals !== undefined
1123
- ? trackRequestHeaderReads(options.request)
1124
- : undefined;
1125
- const appRequest = trackedRequest?.request ?? options.request;
1126
-
1127
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1128
- const preparedActions = await prepareRouteServerActions({
1129
- appDir: options.appDir,
1130
- code: originalCode,
1131
- formActionReferences: options.serverActionReferencesByFile?.get(matched.route.file),
1132
- placeholders: true,
1133
- pageFile: matched.route.file,
1134
- request: options.request,
1135
- });
1136
- for (const diagnostic of preparedActions.diagnostics ?? []) {
1137
- console.warn(`${diagnostic.code}: ${diagnostic.message}`);
1138
- }
1139
- finishRenderTimingPhase(timing, phaseStartedAt, "serverActionsMs");
1140
- const code = preparedActions.code;
1141
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1142
- const routeAnalysis =
1143
- code === originalCode
1144
- ? originalAnalysis
1145
- : await analyzeRouteSource({
1024
+ if (matched.route.kind === "server") {
1025
+ return await dispatchServerRoute({
1146
1026
  appDir: options.appDir,
1147
- buildRenderCode: preparedActions.htmlReplacements !== undefined,
1148
- code,
1149
- filename: matched.route.file,
1150
- routePath: matched.route.path,
1151
- clientRouteInferenceCache,
1152
- 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,
1153
1035
  serverSourceFiles: options.serverSourceFiles,
1154
- define: options.define,
1155
1036
  vitePlugins: options.vitePlugins,
1156
1037
  });
1157
- finishRenderTimingPhase(timing, phaseStartedAt, "routeCodeAnalysisMs");
1158
- const routeCode = routeAnalysis.routeCode;
1159
- const streamRoute = routeAnalysis.streamRoute;
1160
- const clientInference = routeAnalysis.clientInference;
1161
- const clientRoute = clientInference.client;
1162
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1163
- const loaderReady = routeAnalysis.hasLoader ? createDeferredSignal() : undefined;
1164
- const dataPromise = routeAnalysis.hasLoader
1165
- ? 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({
1166
1060
  appDir: options.appDir,
1061
+ artifact: options.serverModules?.get(matched.route.file)?.analysis,
1167
1062
  code: originalCode,
1168
- context: withTrackedRequest({
1169
- env: options.env,
1170
- params: matched.params,
1171
- queryClient,
1172
- }, appRequest, trackedRequest),
1173
1063
  filename: matched.route.file,
1174
- importPolicy: options.importPolicy,
1175
- instrumentation: options.instrumentation,
1176
- devServerModuleCacheVersion: options.devServerModuleCacheVersion,
1177
- request: options.request,
1178
- routeId: routeIdForPath(matched.route.path),
1179
1064
  routePath: matched.route.path,
1180
- serverModules: options.serverModules,
1065
+ clientRouteInferenceCache,
1181
1066
  serverModuleCacheVersion: options.serverModuleCacheVersion,
1067
+ serverSourceFiles: options.serverSourceFiles,
1182
1068
  define: options.define,
1183
- vitePlugins: options.vitePlugins,
1184
1069
  timing,
1185
- onLoaderReady: loaderReady?.resolve,
1186
- })
1187
- : undefined;
1188
- finishRenderTimingPhase(timing, phaseStartedAt, "loaderStartMs");
1189
- recoveryRoute = {
1190
- clientRoute,
1191
- props: {
1192
- params: matched.params,
1193
- request: { url: url.pathname },
1194
- },
1195
- routePath: matched.route.path,
1196
- script: clientScript,
1197
- };
1198
- if (streamRoute) {
1199
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1200
- const loadingFile = await nearestExistingBoundaryFileForPage({
1201
- appDir: options.appDir,
1202
- filename: "loading.mreact.tsx",
1203
- pageFile: matched.route.file,
1204
- serverSourceFiles: options.serverSourceFiles,
1205
- });
1206
- finishRenderTimingPhase(timing, phaseStartedAt, "loadingBoundaryLookupMs");
1207
- const streamShellResponseHeaders = {
1208
- "content-type": "text/html; charset=utf-8",
1209
- "x-mreact-stream": "1",
1210
- };
1211
-
1212
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1213
- const mayRenderOutOfOrder = await mayRenderOutOfOrderBoundaryDeep({
1214
- code: routeCode,
1215
- filename: matched.route.file,
1216
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1217
- serverSourceFiles: options.serverSourceFiles,
1218
- });
1219
- 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
+ }
1220
1517
 
1221
- if (loadingFile === undefined && !mayRenderOutOfOrder) {
1222
1518
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
1223
1519
  let data: unknown;
1224
1520
  try {
@@ -1230,10 +1526,16 @@ async function renderAppRequestInternal(
1230
1526
  emitRenderTiming(options, timing, data.status);
1231
1527
  return data;
1232
1528
  }
1529
+ sourceUsesRequestInput ||= await routeShellsMayUseRequestInput({
1530
+ appDir: options.appDir,
1531
+ pageFile: matched.route.file,
1532
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1533
+ serverSourceFiles: options.serverSourceFiles,
1534
+ });
1233
1535
  await waitForRenderPreload(options, timing);
1234
1536
  await loadServerRenderArtifacts(options, matched.route.file, timing);
1235
1537
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
1236
- const stringOutput = transformServerModule({
1538
+ const output = transformServerModule({
1237
1539
  code: routeCode,
1238
1540
  clientBoundaryImports: clientInference.clientBoundaryImports,
1239
1541
  clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
@@ -1242,38 +1544,49 @@ async function renderAppRequestInternal(
1242
1544
  serverOutput: "string",
1243
1545
  });
1244
1546
  finishRenderTimingPhase(timing, phaseStartedAt, "stringTransformMs");
1245
- const stringFatalDiagnostics = fatalServerDiagnostics(stringOutput.diagnostics);
1547
+ const fatalDiagnostics = fatalServerDiagnostics(output.diagnostics);
1246
1548
 
1247
- if (stringFatalDiagnostics.length > 0) {
1248
- return new Response(formatServerDiagnostics(matched.route.file, stringFatalDiagnostics), {
1549
+ if (fatalDiagnostics.length > 0) {
1550
+ return new Response(formatServerDiagnostics(matched.route.file, fatalDiagnostics), {
1249
1551
  status: 500,
1250
1552
  headers: { "content-type": "text/plain; charset=utf-8" },
1251
1553
  });
1252
1554
  }
1253
1555
 
1556
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1254
1557
  const renderedPage = await runWithQueryClient(queryClient, () =>
1255
1558
  runServerModuleWithSlots(
1256
- stringOutput.code,
1257
- withTrackedRequest({
1258
- data,
1259
- params: matched.params,
1260
- queryClient,
1261
- }, appRequest, trackedRequest),
1559
+ output.code,
1560
+ withTrackedRequest(
1561
+ {
1562
+ data,
1563
+ params: matched.params,
1564
+ queryClient,
1565
+ },
1566
+ appRequest,
1567
+ trackedRequest,
1568
+ ),
1262
1569
  matched.route.file,
1263
1570
  options.serverModules,
1264
1571
  options.serverModuleCacheVersion,
1265
1572
  options.define,
1266
1573
  options.vitePlugins,
1267
- undefined,
1574
+ timing,
1268
1575
  options.importPolicy,
1269
1576
  options.devServerModuleCacheVersion,
1270
1577
  ),
1271
1578
  );
1579
+ finishRenderTimingPhase(timing, phaseStartedAt, "pageRenderMs");
1272
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.
1273
1586
  const pageHtmlForLayout = clientRoute
1274
1587
  ? withHydrationMarkers({
1275
1588
  assetBaseUrl: options.assetBaseUrl,
1276
- clientReferenceManifest: stringOutput.metadata.clientReferenceManifest,
1589
+ clientReferenceManifest: output.metadata.clientReferenceManifest,
1277
1590
  html: pageHtml,
1278
1591
  routePath: matched.route.path,
1279
1592
  script: clientScript,
@@ -1283,17 +1596,27 @@ async function renderAppRequestInternal(
1283
1596
  data,
1284
1597
  },
1285
1598
  })
1286
- : withServerOnlyRouteMarkers(pageHtml, matched.route.path, options.request);
1599
+ : withServerOnlyRouteMarkers(
1600
+ pageHtml,
1601
+ matched.route.path,
1602
+ options.request,
1603
+ options.prerenderVariantCapture === true,
1604
+ );
1605
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1287
1606
  let html = await runWithQueryClient(queryClient, () =>
1288
1607
  applyLayouts({
1289
1608
  appDir: options.appDir,
1290
1609
  pageFile: matched.route.file,
1291
1610
  html: pageHtmlForLayout,
1292
- props: withTrackedRequest({
1293
- data,
1294
- params: matched.params,
1295
- queryClient,
1296
- }, appRequest, trackedRequest),
1611
+ props: withTrackedRequest(
1612
+ {
1613
+ data,
1614
+ params: matched.params,
1615
+ queryClient,
1616
+ },
1617
+ appRequest,
1618
+ trackedRequest,
1619
+ ),
1297
1620
  slots: renderedPage.slots,
1298
1621
  serverModules: options.serverModules,
1299
1622
  serverModuleCacheVersion: options.serverModuleCacheVersion,
@@ -1301,16 +1624,21 @@ async function renderAppRequestInternal(
1301
1624
  clientRouteInferenceCache,
1302
1625
  timing,
1303
1626
  vitePlugins: options.vitePlugins,
1304
- importPolicy: options.importPolicy,
1305
1627
  }),
1306
1628
  );
1629
+ finishRenderTimingPhase(timing, phaseStartedAt, "layoutRenderMs");
1630
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1307
1631
  const metadata = await loadComposedRouteMetadata({
1308
1632
  appDir: options.appDir,
1309
1633
  code: originalCode,
1310
- context: withTrackedRequest({
1311
- data,
1312
- params: matched.params,
1313
- }, appRequest, trackedRequest),
1634
+ context: withTrackedRequest(
1635
+ {
1636
+ data,
1637
+ params: matched.params,
1638
+ },
1639
+ appRequest,
1640
+ trackedRequest,
1641
+ ),
1314
1642
  filename: matched.route.file,
1315
1643
  importPolicy: options.importPolicy,
1316
1644
  routes,
@@ -1321,6 +1649,8 @@ async function renderAppRequestInternal(
1321
1649
  define: options.define,
1322
1650
  vitePlugins: options.vitePlugins,
1323
1651
  });
1652
+ finishRenderTimingPhase(timing, phaseStartedAt, "metadataMs");
1653
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
1324
1654
  html = injectHeadMetadata(html, metadata);
1325
1655
  warnIfCspNonceWouldBlockInlineTags({
1326
1656
  html,
@@ -1334,6 +1664,7 @@ async function renderAppRequestInternal(
1334
1664
  originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined,
1335
1665
  );
1336
1666
  html = injectQueryState(html, dehydrate(queryClient, options.dehydrateOptions));
1667
+
1337
1668
  const response = withOptionalActionCookie(
1338
1669
  htmlResponse(
1339
1670
  applyActionHtmlReplacements(
@@ -1347,413 +1678,122 @@ async function renderAppRequestInternal(
1347
1678
  preparedActions.htmlReplacements,
1348
1679
  ),
1349
1680
  {
1350
- headers: responseHeadersForMetadata(metadata, options.request, {
1351
- "x-mreact-stream": "1",
1352
- }),
1681
+ headers: responseHeadersForMetadata(metadata, options.request),
1353
1682
  },
1354
1683
  ),
1355
1684
  preparedActions.csrfToken,
1356
1685
  preparedActions.csrfTokenIsNew === true,
1357
1686
  );
1358
1687
  appendNavigationVary(response.headers);
1359
- emitRenderTiming(options, timing, response.status);
1360
- return response;
1361
- }
1362
1688
 
1363
- let streamData: unknown;
1364
- let streamDataPromise = dataPromise ?? Promise.resolve(undefined);
1365
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1366
- if (loadingFile === undefined || routeLoaderMayReturnControlResponse(originalCode)) {
1367
- try {
1368
- streamData = dataPromise === undefined ? undefined : await dataPromise;
1369
- } finally {
1370
- finishRenderTimingPhase(timing, phaseStartedAt, "loaderWaitMs");
1371
- }
1372
- if (streamData instanceof Response) {
1373
- emitRenderTiming(options, timing, streamData.status);
1374
- return streamData;
1375
- }
1376
- streamDataPromise = Promise.resolve(streamData);
1377
- } else {
1378
- try {
1379
- const settledData = await readSettledPromiseAtNextTask(streamDataPromise);
1380
- if (settledData.settled) {
1381
- if (settledData.value instanceof Response) {
1382
- emitRenderTiming(options, timing, settledData.value.status);
1383
- return settledData.value;
1384
- }
1385
- streamDataPromise = Promise.resolve(settledData.value);
1386
- } else {
1387
- await waitForLoaderReadyOrSettled(streamDataPromise, loaderReady?.promise);
1388
- }
1389
- } finally {
1390
- finishRenderTimingPhase(timing, phaseStartedAt, "loaderWaitMs");
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;
1391
1724
  }
1392
- }
1393
1725
 
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);
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
+ });
1408
1732
 
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" },
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,
1413
1758
  });
1414
- }
1759
+ options.onRenderError?.(error);
1415
1760
 
1416
- if (loadingFile !== undefined) {
1417
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1418
- const stream = await runServerStreamModuleWithLoading(output.code, {
1761
+ const response = await renderSpecialRoute({
1419
1762
  appDir: options.appDir,
1420
1763
  assetBaseUrl: options.assetBaseUrl,
1421
- clientRoute,
1422
- data: streamDataPromise,
1423
- define: options.define,
1424
- loadingFile,
1425
- markerRequest: options.request,
1426
- pageFile: matched.route.file,
1427
- params: matched.params,
1428
- queryClient,
1429
- request: appRequest,
1430
- requestUrl: url.pathname,
1431
- trackedRequest,
1764
+ currentStyleSheets: options.clientStylesByFile?.get(errorFile),
1765
+ error,
1766
+ request: options.request,
1432
1767
  routePath: matched.route.path,
1768
+ routeFile: errorFile,
1433
1769
  routeScripts: options.clientScripts,
1434
1770
  serverModules: options.serverModules,
1435
1771
  serverModuleCacheVersion: options.serverModuleCacheVersion,
1436
1772
  serverSourceFiles: options.serverSourceFiles,
1437
1773
  vitePlugins: options.vitePlugins,
1438
- clientRouteInferenceCache,
1439
- importPolicy: options.importPolicy,
1440
- script: clientScript,
1441
- clientReferenceManifest: output.metadata.clientReferenceManifest,
1774
+ navigation: recoveryRoute,
1775
+ status: 500,
1776
+ textFallback: "Internal Server Error",
1442
1777
  });
1443
- finishRenderTimingPhase(timing, phaseStartedAt, "streamConstructionMs");
1444
-
1445
- const response = withOptionalActionCookie(
1446
- new Response(stream, {
1447
- headers: streamShellResponseHeaders,
1448
- }),
1449
- preparedActions.csrfToken,
1450
- preparedActions.csrfTokenIsNew === true,
1451
- );
1452
- appendNavigationVary(response.headers);
1453
1778
  emitRenderTiming(options, timing, response.status);
1454
1779
  return response;
1455
- }
1456
-
1457
- const data = streamData;
1458
- const props = withTrackedRequest({
1459
- data,
1460
- params: matched.params,
1461
- queryClient,
1462
- }, appRequest, trackedRequest);
1463
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1464
- const stream = runServerStreamModule(output.code, {
1465
- appDir: options.appDir,
1466
- assetBaseUrl: options.assetBaseUrl,
1467
- pageFile: matched.route.file,
1468
- markerRequest: options.request,
1469
- props,
1470
- requestUrl: url.pathname,
1471
- routePath: matched.route.path,
1472
- routeScripts: options.clientScripts,
1473
- serverModules: options.serverModules,
1474
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1475
- serverSourceFiles: options.serverSourceFiles,
1476
- define: options.define,
1477
- vitePlugins: options.vitePlugins,
1478
- clientRouteInferenceCache,
1479
- importPolicy: options.importPolicy,
1480
- clientRoute,
1481
- script: clientScript,
1482
- clientReferenceManifest: output.metadata.clientReferenceManifest,
1483
- });
1484
- finishRenderTimingPhase(timing, phaseStartedAt, "streamConstructionMs");
1485
-
1486
- const response = withOptionalActionCookie(
1487
- new Response(stream, {
1488
- headers: streamShellResponseHeaders,
1489
- }),
1490
- preparedActions.csrfToken,
1491
- preparedActions.csrfTokenIsNew === true,
1492
- );
1493
- appendNavigationVary(response.headers);
1494
- emitRenderTiming(options, timing, response.status);
1495
- return response;
1496
- }
1497
-
1498
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1499
- let data: unknown;
1500
- try {
1501
- data = dataPromise === undefined ? undefined : await dataPromise;
1502
- } finally {
1503
- finishRenderTimingPhase(timing, phaseStartedAt, "loaderWaitMs");
1504
- }
1505
- if (data instanceof Response) {
1506
- emitRenderTiming(options, timing, data.status);
1507
- return data;
1508
- }
1509
- sourceUsesRequestInput ||= await routeShellsMayUseRequestInput({
1510
- appDir: options.appDir,
1511
- pageFile: matched.route.file,
1512
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1513
- serverSourceFiles: options.serverSourceFiles,
1514
- });
1515
- await waitForRenderPreload(options, timing);
1516
- await loadServerRenderArtifacts(options, matched.route.file, timing);
1517
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1518
- const output = transformServerModule({
1519
- code: routeCode,
1520
- clientBoundaryImports: clientInference.clientBoundaryImports,
1521
- clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
1522
- filename: matched.route.file,
1523
- serverModules: options.serverModules,
1524
- serverOutput: "string",
1525
- });
1526
- finishRenderTimingPhase(timing, phaseStartedAt, "stringTransformMs");
1527
- const fatalDiagnostics = fatalServerDiagnostics(output.diagnostics);
1528
-
1529
- if (fatalDiagnostics.length > 0) {
1530
- return new Response(formatServerDiagnostics(matched.route.file, fatalDiagnostics), {
1531
- status: 500,
1532
- headers: { "content-type": "text/plain; charset=utf-8" },
1533
- });
1534
- }
1535
-
1536
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1537
- const renderedPage = await runWithQueryClient(queryClient, () =>
1538
- runServerModuleWithSlots(
1539
- output.code,
1540
- withTrackedRequest({
1541
- data,
1542
- params: matched.params,
1543
- queryClient,
1544
- }, appRequest, trackedRequest),
1545
- matched.route.file,
1546
- options.serverModules,
1547
- options.serverModuleCacheVersion,
1548
- options.define,
1549
- options.vitePlugins,
1550
- timing,
1551
- options.importPolicy,
1552
- options.devServerModuleCacheVersion,
1553
- ),
1554
- );
1555
- finishRenderTimingPhase(timing, phaseStartedAt, "pageRenderMs");
1556
- const pageHtml = renderedPage.html;
1557
- // Wrap the page (not the full document) with the hydration marker so
1558
- // the marker sits inside <body>, not around <html>. Wrapping <html>
1559
- // forces the browser HTML parser to strip the wrappers and promote
1560
- // <head> / <body> children up to the marker, which flattens the
1561
- // layout into the marker and breaks the hydration target lookup.
1562
- const pageHtmlForLayout = clientRoute
1563
- ? withHydrationMarkers({
1564
- assetBaseUrl: options.assetBaseUrl,
1565
- clientReferenceManifest: output.metadata.clientReferenceManifest,
1566
- html: pageHtml,
1567
- routePath: matched.route.path,
1568
- script: clientScript,
1569
- props: {
1570
- params: matched.params,
1571
- request: { url: url.pathname },
1572
- data,
1573
- },
1574
- })
1575
- : withServerOnlyRouteMarkers(pageHtml, matched.route.path, options.request);
1576
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1577
- let html = await runWithQueryClient(queryClient, () =>
1578
- applyLayouts({
1579
- appDir: options.appDir,
1580
- pageFile: matched.route.file,
1581
- html: pageHtmlForLayout,
1582
- props: withTrackedRequest({
1583
- data,
1584
- params: matched.params,
1585
- queryClient,
1586
- }, appRequest, trackedRequest),
1587
- slots: renderedPage.slots,
1588
- serverModules: options.serverModules,
1589
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1590
- serverSourceFiles: options.serverSourceFiles,
1591
- clientRouteInferenceCache,
1592
- timing,
1593
- vitePlugins: options.vitePlugins,
1594
- }),
1595
- );
1596
- finishRenderTimingPhase(timing, phaseStartedAt, "layoutRenderMs");
1597
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1598
- const metadata = await loadComposedRouteMetadata({
1599
- appDir: options.appDir,
1600
- code: originalCode,
1601
- context: withTrackedRequest({
1602
- data,
1603
- params: matched.params,
1604
- }, appRequest, trackedRequest),
1605
- filename: matched.route.file,
1606
- importPolicy: options.importPolicy,
1607
- routes,
1608
- serverModules: options.serverModules,
1609
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1610
- devServerModuleCacheVersion: options.devServerModuleCacheVersion,
1611
- serverSourceFiles: options.serverSourceFiles,
1612
- define: options.define,
1613
- vitePlugins: options.vitePlugins,
1614
- });
1615
- finishRenderTimingPhase(timing, phaseStartedAt, "metadataMs");
1616
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
1617
- html = injectHeadMetadata(html, metadata);
1618
- warnIfCspNonceWouldBlockInlineTags({
1619
- html,
1620
- logger: options.logger,
1621
- metadata,
1622
- request: options.request,
1623
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1624
- });
1625
- html = injectAuthSessionClaims(
1626
- html,
1627
- originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined,
1628
- );
1629
- html = injectQueryState(html, dehydrate(queryClient, options.dehydrateOptions));
1630
-
1631
- const response = withOptionalActionCookie(
1632
- htmlResponse(
1633
- applyActionHtmlReplacements(
1634
- `<!DOCTYPE html>${clientNavigationHeadTags({
1635
- assetBaseUrl: options.assetBaseUrl,
1636
- currentStyleSheets: clientStyleSheets,
1637
- currentScript: clientRoute ? clientScript : undefined,
1638
- currentNavigationScript: navigationScript,
1639
- routeScripts: options.clientScripts,
1640
- })}${html}`,
1641
- preparedActions.htmlReplacements,
1642
- ),
1643
- {
1644
- headers: responseHeadersForMetadata(metadata, options.request),
1645
- },
1646
- ),
1647
- preparedActions.csrfToken,
1648
- preparedActions.csrfTokenIsNew === true,
1649
- );
1650
- appendNavigationVary(response.headers);
1651
-
1652
- const effectiveCachePolicy = cachePolicy ?? activeRouteCacheContext()?.cachePolicy;
1653
- const configuredHsts = configuredHstsHeader(metadata?.security);
1654
- renderedStrictTransportSecurity = configuredHsts;
1655
- invalidConfiguredHsts = hasInvalidConfiguredHsts(metadata?.security);
1656
-
1657
- const finalResponse = preparedActions.hasFormActions
1658
- ? withRouteCacheHeader(response, effectiveCachePolicy)
1659
- : await cacheRouteResponse({
1660
- key: cacheKey,
1661
- cache: options.routeCache,
1662
- // A policy can still arrive from a runtime cacheControl() call made
1663
- // outside the page source, which is what `mayUseRouteCache` scans.
1664
- // Without a tracker there is no evidence the render ignored request
1665
- // headers, so the entry is treated as header dependent.
1666
- 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 = () =>
1667
1789
  sourceUsesRequestInput ||
1668
1790
  invalidConfiguredHsts ||
1669
- (trackedRequest?.requestDependent() ?? true),
1670
- ...(configuredHsts === undefined ? {} : { strictTransportSecurity: configuredHsts }),
1671
- path: matched.route.path,
1672
- policy: effectiveCachePolicy,
1673
- request: options.request,
1674
- response,
1675
- });
1676
- finishRenderTimingPhase(timing, phaseStartedAt, "responseBuildMs");
1677
- emitRenderTiming(options, timing, finalResponse.status);
1678
- return finalResponse;
1679
- } catch (error) {
1680
- if (isRedirectError(error)) {
1681
- const response = new Response(null, {
1682
- headers: { location: error.location },
1683
- status: error.status,
1684
- });
1685
- emitRenderTiming(options, timing, response.status);
1686
- return response;
1687
- }
1688
-
1689
- if (isNotFoundError(error)) {
1690
- const notFoundFile = await nearestBoundaryFileForPage({
1691
- appDir: options.appDir,
1692
- filename: "not-found.mreact.tsx",
1693
- pageFile: matched.route.file,
1694
- });
1695
-
1696
- const response = await renderSpecialRoute({
1697
- appDir: options.appDir,
1698
- assetBaseUrl: options.assetBaseUrl,
1699
- currentStyleSheets: options.clientStylesByFile?.get(notFoundFile),
1700
- error: undefined,
1701
- request: options.request,
1702
- routePath: matched.route.path,
1703
- routeFile: notFoundFile,
1704
- routeScripts: options.clientScripts,
1705
- serverModules: options.serverModules,
1706
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1707
- serverSourceFiles: options.serverSourceFiles,
1708
- vitePlugins: options.vitePlugins,
1709
- navigation: recoveryRoute,
1710
- status: 404,
1711
- textFallback: "Not Found",
1712
- });
1713
- emitRenderTiming(options, timing, response.status);
1714
- return response;
1715
- }
1716
-
1717
- const errorFile = await nearestBoundaryFileForPage({
1718
- appDir: options.appDir,
1719
- filename: "error.mreact.tsx",
1720
- pageFile: matched.route.file,
1721
- });
1722
- options.onRenderError?.(error);
1723
-
1724
- const response = await renderSpecialRoute({
1725
- appDir: options.appDir,
1726
- assetBaseUrl: options.assetBaseUrl,
1727
- currentStyleSheets: options.clientStylesByFile?.get(errorFile),
1728
- error,
1729
- request: options.request,
1730
- routePath: matched.route.path,
1731
- routeFile: errorFile,
1732
- routeScripts: options.clientScripts,
1733
- serverModules: options.serverModules,
1734
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1735
- serverSourceFiles: options.serverSourceFiles,
1736
- vitePlugins: options.vitePlugins,
1737
- navigation: recoveryRoute,
1738
- status: 500,
1739
- textFallback: "Internal Server Error",
1740
- });
1741
- emitRenderTiming(options, timing, response.status);
1742
- return response;
1743
- } finally {
1744
- // Reported from every return path, including the streaming ones, so that a
1745
- // caller storing by path alone never mistakes an unreported render for a
1746
- // shareable one. The value stays lazy because a streamed render can still
1747
- // read request headers while its deferred parts resolve; callers read it
1748
- // after draining the body.
1749
- if (options.renderSignals !== undefined) {
1750
- const tracked = trackedRequest;
1751
- options.renderSignals.headerDependent = () =>
1752
- sourceUsesRequestInput || invalidConfiguredHsts || (tracked?.requestDependent() ?? true);
1753
- options.renderSignals.strictTransportSecurity = () => renderedStrictTransportSecurity;
1754
- }
1755
- }
1756
- })).value;
1791
+ (tracked?.requestDependent() ?? true);
1792
+ options.renderSignals.strictTransportSecurity = () => renderedStrictTransportSecurity;
1793
+ }
1794
+ }
1795
+ })
1796
+ ).value;
1757
1797
  }
1758
1798
 
1759
1799
  async function applyAppRouterResponseHook(
@@ -1980,16 +2020,22 @@ function isNavigationRequest(request: Request): boolean {
1980
2020
  return request.headers.get("x-mreact-navigation") === "1";
1981
2021
  }
1982
2022
 
1983
- function withServerOnlyRouteMarkers(html: string, routePath: string, request: Request): string {
1984
- const marker = serverOnlyRouteMarkerParts(routePath, request);
2023
+ function withServerOnlyRouteMarkers(
2024
+ html: string,
2025
+ routePath: string,
2026
+ request: Request,
2027
+ prerenderVariantCapture: boolean,
2028
+ ): string {
2029
+ const marker = serverOnlyRouteMarkerParts(routePath, request, prerenderVariantCapture);
1985
2030
  return marker === undefined ? html : `${marker.prefix}${html}${marker.suffix}`;
1986
2031
  }
1987
2032
 
1988
2033
  function serverOnlyRouteMarkerParts(
1989
2034
  routePath: string,
1990
2035
  request: Request,
2036
+ prerenderVariantCapture: boolean,
1991
2037
  ): { prefix: string; suffix: string } | undefined {
1992
- if (request.headers.get("x-mreact-prerender-variant-capture") === "1") {
2038
+ if (prerenderVariantCapture) {
1993
2039
  return prerenderVariantMarkerParts(routePath);
1994
2040
  }
1995
2041
  return isNavigationRequest(request) ? routeMarkerParts(routePath) : undefined;
@@ -2009,9 +2055,7 @@ export function prerenderVariantMarkerParts(routePath: string): {
2009
2055
  function appendNavigationVary(headers: Headers): void {
2010
2056
  const vary = headers.get("vary");
2011
2057
  if (
2012
- vary
2013
- ?.split(",")
2014
- .some((value) => value.trim().toLowerCase() === "x-mreact-navigation") === true
2058
+ vary?.split(",").some((value) => value.trim().toLowerCase() === "x-mreact-navigation") === true
2015
2059
  ) {
2016
2060
  return;
2017
2061
  }
@@ -2200,15 +2244,7 @@ async function renderSpecialRoute(options: {
2200
2244
  }
2201
2245
 
2202
2246
  const props = {
2203
- data: undefined,
2204
- debug: errorDebugContext(options.error, options.routePath),
2205
- error: normalizeErrorForProps(options.error),
2206
- params: {},
2207
- queryClient: createQueryClient(),
2208
- request: options.request,
2209
- requestId: requestIdForErrorContext(options.request),
2210
- routeId: routeIdForPath(options.routePath ?? new URL(options.request.url).pathname),
2211
- traceId: traceContextFromRequest(options.request)?.traceId,
2247
+ ...errorBoundaryProps(options.error, options.request, options.routePath),
2212
2248
  };
2213
2249
  const pageHtml = await renderServerFileToHtml(
2214
2250
  options.routeFile,
@@ -2257,6 +2293,20 @@ async function renderSpecialRoute(options: {
2257
2293
  );
2258
2294
  }
2259
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
+
2260
2310
  async function renderServerFileToHtml(
2261
2311
  file: string,
2262
2312
  props: ServerComponentProps,
@@ -3452,6 +3502,7 @@ function runServerStreamModule(
3452
3502
  assetBaseUrl?: string | undefined;
3453
3503
  clientRouteInferenceCache: ClientRouteInferenceCache;
3454
3504
  markerRequest: Request;
3505
+ prerenderVariantCapture?: boolean | undefined;
3455
3506
  pageFile: string;
3456
3507
  props: ServerComponentProps;
3457
3508
  requestUrl: string;
@@ -3503,7 +3554,11 @@ function runServerStreamModule(
3503
3554
  data: options.props.data,
3504
3555
  },
3505
3556
  })
3506
- : serverOnlyRouteMarkerParts(options.routePath, options.markerRequest);
3557
+ : serverOnlyRouteMarkerParts(
3558
+ options.routePath,
3559
+ options.markerRequest,
3560
+ options.prerenderVariantCapture === true,
3561
+ );
3507
3562
 
3508
3563
  sink.append("<!DOCTYPE html>");
3509
3564
  sink.append(
@@ -3719,9 +3774,13 @@ async function runServerStreamModuleWithLoading(
3719
3774
  clientRoute: boolean;
3720
3775
  clientReferenceManifest?: readonly ClientReferenceMetadata[] | undefined;
3721
3776
  data: Promise<unknown>;
3777
+ dataSettlement: () => ObservedPromiseSettlement<unknown>;
3722
3778
  define?: UserConfig["define"] | undefined;
3779
+ errorFile?: string | undefined;
3723
3780
  loadingFile: string;
3724
3781
  markerRequest: Request;
3782
+ onRenderError?: ((error: unknown) => void) | undefined;
3783
+ prerenderVariantCapture?: boolean | undefined;
3725
3784
  pageFile: string;
3726
3785
  params: RouteParams;
3727
3786
  queryClient: QueryClient;
@@ -3737,12 +3796,16 @@ async function runServerStreamModuleWithLoading(
3737
3796
  vitePlugins?: readonly PluginOption[] | undefined;
3738
3797
  importPolicy?: AppRouterImportPolicy | undefined;
3739
3798
  },
3740
- ): Promise<ReadableStream<Uint8Array>> {
3741
- const loadingProps = withTrackedRequest({
3742
- data: undefined,
3743
- params: options.params,
3744
- queryClient: options.queryClient,
3745
- }, 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
+ );
3746
3809
  const layoutShells = await layoutShellsForPage(
3747
3810
  options.appDir,
3748
3811
  options.pageFile,
@@ -3776,7 +3839,21 @@ async function runServerStreamModuleWithLoading(
3776
3839
  request: { url: options.requestUrl },
3777
3840
  },
3778
3841
  })
3779
- : serverOnlyRouteMarkerParts(options.routePath, options.markerRequest);
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
+ }
3780
3857
 
3781
3858
  return renderToReadableStream((sink) => {
3782
3859
  sink.append("<!DOCTYPE html>");
@@ -3794,7 +3871,7 @@ async function runServerStreamModuleWithLoading(
3794
3871
 
3795
3872
  sink.append(marker?.prefix ?? "");
3796
3873
 
3797
- renderVisibleOutOfOrderBoundary(
3874
+ renderOutOfOrderBoundary(
3798
3875
  sink,
3799
3876
  "mreact-route",
3800
3877
  options.data,
@@ -3802,11 +3879,15 @@ async function runServerStreamModuleWithLoading(
3802
3879
  await appendServerStreamModule(
3803
3880
  code,
3804
3881
  boundarySink,
3805
- withTrackedRequest({
3806
- data,
3807
- params: options.params,
3808
- queryClient: options.queryClient,
3809
- }, 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
+ ),
3810
3891
  options.pageFile,
3811
3892
  options.serverModules,
3812
3893
  options.serverModuleCacheVersion,
@@ -3816,6 +3897,34 @@ async function runServerStreamModuleWithLoading(
3816
3897
  );
3817
3898
  },
3818
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
+ },
3819
3928
  placeholder(boundarySink) {
3820
3929
  boundarySink.append(loadingHtml);
3821
3930
  },
@@ -3833,70 +3942,6 @@ async function runServerStreamModuleWithLoading(
3833
3942
  });
3834
3943
  }
3835
3944
 
3836
- function renderVisibleOutOfOrderBoundary<T>(
3837
- sink: HtmlSink,
3838
- id: string,
3839
- value: T,
3840
- render: (sink: HtmlSink, value: Awaited<T>) => void | PromiseLike<void>,
3841
- options: {
3842
- catch?: (sink: HtmlSink, error: unknown) => void | PromiseLike<void>;
3843
- placeholder?: (sink: HtmlSink) => void | PromiseLike<void>;
3844
- placeholderTag?: string;
3845
- } = {},
3846
- ): void {
3847
- const placeholderSink = createStringSink();
3848
- void options.placeholder?.(placeholderSink);
3849
- const placeholderTag = normalizeVisibleOutOfOrderPlaceholderTag(options.placeholderTag);
3850
- sink.append(
3851
- `<${placeholderTag} data-mreact-oob-placeholder="${escapeHtmlAttribute(id)}">${placeholderSink.toString()}</${placeholderTag}>`,
3852
- );
3853
-
3854
- const task = renderVisibleOutOfOrderFragment(sink, id, value, render, options);
3855
-
3856
- if (sink.defer === undefined) {
3857
- void task;
3858
- return;
3859
- }
3860
-
3861
- sink.defer(task);
3862
- }
3863
-
3864
- function normalizeVisibleOutOfOrderPlaceholderTag(tag: unknown): string {
3865
- if (typeof tag !== "string") {
3866
- return "span";
3867
- }
3868
-
3869
- const normalized = tag.trim().toLowerCase();
3870
- return /^[a-z][a-z0-9-]*$/.test(normalized) ? normalized : "span";
3871
- }
3872
-
3873
- async function renderVisibleOutOfOrderFragment<T>(
3874
- sink: HtmlSink,
3875
- id: string,
3876
- value: T,
3877
- render: (sink: HtmlSink, value: Awaited<T>) => void | PromiseLike<void>,
3878
- options: {
3879
- catch?: (sink: HtmlSink, error: unknown) => void | PromiseLike<void>;
3880
- },
3881
- ): Promise<void> {
3882
- const fragmentSink = createStringSink();
3883
-
3884
- await renderAsyncBoundary(
3885
- fragmentSink,
3886
- value,
3887
- render,
3888
- options.catch === undefined ? {} : { catch: options.catch },
3889
- );
3890
- await fragmentSink.drain();
3891
-
3892
- sink.append(renderVisibleOutOfOrderFragmentHtml(id, fragmentSink.toString()));
3893
- }
3894
-
3895
- function renderVisibleOutOfOrderFragmentHtml(id: string, html: string): string {
3896
- const escapedId = escapeHtmlAttribute(id);
3897
- return `<template data-mreact-oob-fragment="${escapedId}">${html}</template><mreact-oob-complete hidden data-mreact-oob-complete="${escapedId}"></mreact-oob-complete>`;
3898
- }
3899
-
3900
3945
  async function appendServerStreamModule(
3901
3946
  code: string,
3902
3947
  sink: HtmlSink,
@@ -5109,7 +5154,12 @@ async function loadComposedRouteMetadata(options: {
5109
5154
  composedRouteMetadataCacheCounters,
5110
5155
  );
5111
5156
  if (cached !== undefined) {
5112
- return cached;
5157
+ const entry = await cached;
5158
+ return applyFileConventionMetadataTemplate(
5159
+ entry.metadata,
5160
+ entry.template,
5161
+ options.context.params,
5162
+ );
5113
5163
  }
5114
5164
  }
5115
5165
 
@@ -5119,13 +5169,17 @@ async function loadComposedRouteMetadata(options: {
5119
5169
  setBoundedCacheEntry(
5120
5170
  composedRouteMetadataCache,
5121
5171
  cacheKey,
5122
- Promise.resolve(loaded.metadata),
5172
+ Promise.resolve({ metadata: loaded.metadata, template: loaded.template }),
5123
5173
  maxComposedRouteMetadataCacheEntries,
5124
5174
  composedRouteMetadataCacheCounters,
5125
5175
  );
5126
5176
  }
5127
5177
 
5128
- return loaded.metadata;
5178
+ return applyFileConventionMetadataTemplate(
5179
+ loaded.metadata,
5180
+ loaded.template,
5181
+ options.context.params,
5182
+ );
5129
5183
  } catch (error) {
5130
5184
  if (cacheKey !== undefined) {
5131
5185
  composedRouteMetadataCache.delete(cacheKey);
@@ -5167,7 +5221,11 @@ async function loadComposedRouteMetadataUncached(options: {
5167
5221
  serverModuleCacheVersion?: string | undefined;
5168
5222
  serverSourceFiles?: ReadonlyMap<string, string> | undefined;
5169
5223
  vitePlugins?: readonly PluginOption[] | undefined;
5170
- }): Promise<{ dynamic: boolean; metadata: RouteMetadata | undefined }> {
5224
+ }): Promise<{
5225
+ dynamic: boolean;
5226
+ metadata: RouteMetadata | undefined;
5227
+ template: FileConventionMetadataTemplate;
5228
+ }> {
5171
5229
  const layoutFiles = await shellFilesForPage(
5172
5230
  options.appDir,
5173
5231
  options.filename,
@@ -5230,12 +5288,8 @@ async function loadComposedRouteMetadataUncached(options: {
5230
5288
 
5231
5289
  return {
5232
5290
  dynamic,
5233
- metadata: applyFileConventionMetadata(
5234
- mergeRouteMetadata(metadata),
5235
- options.routes,
5236
- options.filename,
5237
- options.context.params,
5238
- ),
5291
+ metadata: mergeRouteMetadata(metadata),
5292
+ template: fileConventionMetadataTemplate(options.routes, options.filename),
5239
5293
  };
5240
5294
  }
5241
5295