@reckona/mreact-router 0.0.202 → 0.0.203

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 (60) hide show
  1. package/README.md +5 -2
  2. package/dist/actions.d.ts +2 -3
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +61 -41
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/cloudflare.d.ts +3 -1
  7. package/dist/adapters/cloudflare.d.ts.map +1 -1
  8. package/dist/adapters/cloudflare.js +26 -10
  9. package/dist/adapters/cloudflare.js.map +1 -1
  10. package/dist/build.d.ts +3 -1
  11. package/dist/build.d.ts.map +1 -1
  12. package/dist/build.js +98 -12
  13. package/dist/build.js.map +1 -1
  14. package/dist/cache.d.ts +1 -1
  15. package/dist/cache.d.ts.map +1 -1
  16. package/dist/cache.js +5 -4
  17. package/dist/cache.js.map +1 -1
  18. package/dist/cli-options.d.ts +3 -0
  19. package/dist/cli-options.d.ts.map +1 -1
  20. package/dist/cli-options.js +10 -2
  21. package/dist/cli-options.js.map +1 -1
  22. package/dist/cli.js +2 -2
  23. package/dist/cli.js.map +1 -1
  24. package/dist/client.d.ts +5 -0
  25. package/dist/client.d.ts.map +1 -1
  26. package/dist/client.js +11 -2
  27. package/dist/client.js.map +1 -1
  28. package/dist/navigation-marker.d.ts +2 -0
  29. package/dist/navigation-marker.d.ts.map +1 -0
  30. package/dist/navigation-marker.js +294 -0
  31. package/dist/navigation-marker.js.map +1 -0
  32. package/dist/navigation-runtime.d.ts +1 -1
  33. package/dist/navigation-runtime.d.ts.map +1 -1
  34. package/dist/navigation-runtime.js +1 -1
  35. package/dist/navigation-runtime.js.map +1 -1
  36. package/dist/prerender-entry.d.ts +3 -1
  37. package/dist/prerender-entry.d.ts.map +1 -1
  38. package/dist/prerender-entry.js +61 -3
  39. package/dist/prerender-entry.js.map +1 -1
  40. package/dist/render.d.ts +7 -1
  41. package/dist/render.d.ts.map +1 -1
  42. package/dist/render.js +51 -23
  43. package/dist/render.js.map +1 -1
  44. package/dist/serve.d.ts +3 -0
  45. package/dist/serve.d.ts.map +1 -1
  46. package/dist/serve.js +54 -30
  47. package/dist/serve.js.map +1 -1
  48. package/package.json +11 -11
  49. package/src/actions.ts +72 -56
  50. package/src/adapters/cloudflare.ts +40 -10
  51. package/src/build.ts +107 -10
  52. package/src/cache.ts +10 -4
  53. package/src/cli-options.ts +15 -2
  54. package/src/cli.ts +2 -1
  55. package/src/client.ts +15 -2
  56. package/src/navigation-marker.ts +353 -0
  57. package/src/navigation-runtime.ts +1 -0
  58. package/src/prerender-entry.ts +78 -5
  59. package/src/render.ts +80 -24
  60. package/src/serve.ts +71 -34
package/src/render.ts CHANGED
@@ -16,6 +16,7 @@ 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 {
@@ -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";
@@ -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;
@@ -243,7 +245,12 @@ export interface RenderAppRequestRuntimeOptions extends RenderAppRequestOptions
243
245
  * dependent. Call it only after draining the response body, because a
244
246
  * streamed render can read headers while its deferred parts resolve.
245
247
  */
246
- renderSignals?: { headerDependent: () => boolean } | undefined;
248
+ renderSignals?:
249
+ | {
250
+ headerDependent: () => boolean;
251
+ strictTransportSecurity: () => string | undefined;
252
+ }
253
+ | undefined;
247
254
  serverRenderArtifactLoader?: AppRouterServerRenderArtifactLoader | undefined;
248
255
  }
249
256
 
@@ -1000,6 +1007,7 @@ async function renderAppRequestInternal(
1000
1007
  let trackedRequest: TrackedHeaderRequest | undefined;
1001
1008
  let sourceUsesRequestInput = true;
1002
1009
  let invalidConfiguredHsts = false;
1010
+ let renderedStrictTransportSecurity: string | undefined;
1003
1011
  return (await withRouteCacheContext(options.routeCache, async () => {
1004
1012
  try {
1005
1013
  if (matched.route.kind === "asset") {
@@ -1076,7 +1084,13 @@ async function renderAppRequestInternal(
1076
1084
  finishRenderTimingPhase(timing, phaseStartedAt, "sourceAnalysisMs");
1077
1085
  const cachePolicy = originalAnalysis.cachePolicy;
1078
1086
  const navigationScript = options.navigationScripts?.get(matched.route.path);
1079
- const cacheKey = routeCacheKey(options.appDir, matched.route.path, url);
1087
+ const navigationRequest = isNavigationRequest(options.request);
1088
+ const cacheKey = routeCacheKey(
1089
+ options.appDir,
1090
+ matched.route.path,
1091
+ url,
1092
+ navigationRequest ? "navigation" : "document",
1093
+ );
1080
1094
  const mayUseRouteCache =
1081
1095
  !sourceUsesRequestInput &&
1082
1096
  (cachePolicy === undefined
@@ -1176,7 +1190,7 @@ async function renderAppRequestInternal(
1176
1190
  clientRoute,
1177
1191
  props: {
1178
1192
  params: matched.params,
1179
- request: { url: options.request.url },
1193
+ request: { url: url.pathname },
1180
1194
  },
1181
1195
  routePath: matched.route.path,
1182
1196
  script: clientScript,
@@ -1265,16 +1279,11 @@ async function renderAppRequestInternal(
1265
1279
  script: clientScript,
1266
1280
  props: {
1267
1281
  params: matched.params,
1268
- request: { url: options.request.url },
1282
+ request: { url: url.pathname },
1269
1283
  data,
1270
1284
  },
1271
1285
  })
1272
- : isNavigationRequest(options.request)
1273
- ? withRouteMarkers({
1274
- html: pageHtml,
1275
- routePath: matched.route.path,
1276
- })
1277
- : pageHtml;
1286
+ : withServerOnlyRouteMarkers(pageHtml, matched.route.path, options.request);
1278
1287
  let html = await runWithQueryClient(queryClient, () =>
1279
1288
  applyLayouts({
1280
1289
  appDir: options.appDir,
@@ -1324,7 +1333,7 @@ async function renderAppRequestInternal(
1324
1333
  html,
1325
1334
  originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined,
1326
1335
  );
1327
- html = injectQueryState(html, dehydrate(queryClient));
1336
+ html = injectQueryState(html, dehydrate(queryClient, options.dehydrateOptions));
1328
1337
  const response = withOptionalActionCookie(
1329
1338
  htmlResponse(
1330
1339
  applyActionHtmlReplacements(
@@ -1346,6 +1355,7 @@ async function renderAppRequestInternal(
1346
1355
  preparedActions.csrfToken,
1347
1356
  preparedActions.csrfTokenIsNew === true,
1348
1357
  );
1358
+ appendNavigationVary(response.headers);
1349
1359
  emitRenderTiming(options, timing, response.status);
1350
1360
  return response;
1351
1361
  }
@@ -1412,10 +1422,12 @@ async function renderAppRequestInternal(
1412
1422
  data: streamDataPromise,
1413
1423
  define: options.define,
1414
1424
  loadingFile,
1425
+ markerRequest: options.request,
1415
1426
  pageFile: matched.route.file,
1416
1427
  params: matched.params,
1417
1428
  queryClient,
1418
1429
  request: appRequest,
1430
+ requestUrl: url.pathname,
1419
1431
  trackedRequest,
1420
1432
  routePath: matched.route.path,
1421
1433
  routeScripts: options.clientScripts,
@@ -1437,6 +1449,7 @@ async function renderAppRequestInternal(
1437
1449
  preparedActions.csrfToken,
1438
1450
  preparedActions.csrfTokenIsNew === true,
1439
1451
  );
1452
+ appendNavigationVary(response.headers);
1440
1453
  emitRenderTiming(options, timing, response.status);
1441
1454
  return response;
1442
1455
  }
@@ -1452,7 +1465,9 @@ async function renderAppRequestInternal(
1452
1465
  appDir: options.appDir,
1453
1466
  assetBaseUrl: options.assetBaseUrl,
1454
1467
  pageFile: matched.route.file,
1468
+ markerRequest: options.request,
1455
1469
  props,
1470
+ requestUrl: url.pathname,
1456
1471
  routePath: matched.route.path,
1457
1472
  routeScripts: options.clientScripts,
1458
1473
  serverModules: options.serverModules,
@@ -1475,6 +1490,7 @@ async function renderAppRequestInternal(
1475
1490
  preparedActions.csrfToken,
1476
1491
  preparedActions.csrfTokenIsNew === true,
1477
1492
  );
1493
+ appendNavigationVary(response.headers);
1478
1494
  emitRenderTiming(options, timing, response.status);
1479
1495
  return response;
1480
1496
  }
@@ -1552,16 +1568,11 @@ async function renderAppRequestInternal(
1552
1568
  script: clientScript,
1553
1569
  props: {
1554
1570
  params: matched.params,
1555
- request: { url: options.request.url },
1571
+ request: { url: url.pathname },
1556
1572
  data,
1557
1573
  },
1558
1574
  })
1559
- : isNavigationRequest(options.request)
1560
- ? withRouteMarkers({
1561
- html: pageHtml,
1562
- routePath: matched.route.path,
1563
- })
1564
- : pageHtml;
1575
+ : withServerOnlyRouteMarkers(pageHtml, matched.route.path, options.request);
1565
1576
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
1566
1577
  let html = await runWithQueryClient(queryClient, () =>
1567
1578
  applyLayouts({
@@ -1615,7 +1626,7 @@ async function renderAppRequestInternal(
1615
1626
  html,
1616
1627
  originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined,
1617
1628
  );
1618
- html = injectQueryState(html, dehydrate(queryClient));
1629
+ html = injectQueryState(html, dehydrate(queryClient, options.dehydrateOptions));
1619
1630
 
1620
1631
  const response = withOptionalActionCookie(
1621
1632
  htmlResponse(
@@ -1636,9 +1647,11 @@ async function renderAppRequestInternal(
1636
1647
  preparedActions.csrfToken,
1637
1648
  preparedActions.csrfTokenIsNew === true,
1638
1649
  );
1650
+ appendNavigationVary(response.headers);
1639
1651
 
1640
1652
  const effectiveCachePolicy = cachePolicy ?? activeRouteCacheContext()?.cachePolicy;
1641
1653
  const configuredHsts = configuredHstsHeader(metadata?.security);
1654
+ renderedStrictTransportSecurity = configuredHsts;
1642
1655
  invalidConfiguredHsts = hasInvalidConfiguredHsts(metadata?.security);
1643
1656
 
1644
1657
  const finalResponse = preparedActions.hasFormActions
@@ -1737,6 +1750,7 @@ async function renderAppRequestInternal(
1737
1750
  const tracked = trackedRequest;
1738
1751
  options.renderSignals.headerDependent = () =>
1739
1752
  sourceUsesRequestInput || invalidConfiguredHsts || (tracked?.requestDependent() ?? true);
1753
+ options.renderSignals.strictTransportSecurity = () => renderedStrictTransportSecurity;
1740
1754
  }
1741
1755
  }
1742
1756
  })).value;
@@ -1966,6 +1980,44 @@ function isNavigationRequest(request: Request): boolean {
1966
1980
  return request.headers.get("x-mreact-navigation") === "1";
1967
1981
  }
1968
1982
 
1983
+ function withServerOnlyRouteMarkers(html: string, routePath: string, request: Request): string {
1984
+ const marker = serverOnlyRouteMarkerParts(routePath, request);
1985
+ return marker === undefined ? html : `${marker.prefix}${html}${marker.suffix}`;
1986
+ }
1987
+
1988
+ function serverOnlyRouteMarkerParts(
1989
+ routePath: string,
1990
+ request: Request,
1991
+ ): { prefix: string; suffix: string } | undefined {
1992
+ if (request.headers.get("x-mreact-prerender-variant-capture") === "1") {
1993
+ return prerenderVariantMarkerParts(routePath);
1994
+ }
1995
+ return isNavigationRequest(request) ? routeMarkerParts(routePath) : undefined;
1996
+ }
1997
+
1998
+ export function prerenderVariantMarkerParts(routePath: string): {
1999
+ prefix: string;
2000
+ suffix: string;
2001
+ } {
2002
+ const routeId = routeIdForPath(routePath);
2003
+ return {
2004
+ prefix: `<!--mreact-route-variant-start:${routeId}-->`,
2005
+ suffix: `<!--mreact-route-variant-end:${routeId}-->`,
2006
+ };
2007
+ }
2008
+
2009
+ function appendNavigationVary(headers: Headers): void {
2010
+ const vary = headers.get("vary");
2011
+ if (
2012
+ vary
2013
+ ?.split(",")
2014
+ .some((value) => value.trim().toLowerCase() === "x-mreact-navigation") === true
2015
+ ) {
2016
+ return;
2017
+ }
2018
+ headers.append("vary", "x-mreact-navigation");
2019
+ }
2020
+
1969
2021
  function isNavigationRouteCacheReloadRequest(request: Request): boolean {
1970
2022
  return (
1971
2023
  isNavigationRequest(request) && request.headers.get("x-mreact-navigation-cache") === "reload"
@@ -3399,8 +3451,10 @@ function runServerStreamModule(
3399
3451
  appDir: string;
3400
3452
  assetBaseUrl?: string | undefined;
3401
3453
  clientRouteInferenceCache: ClientRouteInferenceCache;
3454
+ markerRequest: Request;
3402
3455
  pageFile: string;
3403
3456
  props: ServerComponentProps;
3457
+ requestUrl: string;
3404
3458
  routePath: string;
3405
3459
  routeScripts?: ReadonlyMap<string, string> | undefined;
3406
3460
  clientRoute: boolean;
@@ -3445,11 +3499,11 @@ function runServerStreamModule(
3445
3499
  script: options.script,
3446
3500
  props: {
3447
3501
  params: options.props.params,
3448
- request: { url: options.props.request.url },
3502
+ request: { url: options.requestUrl },
3449
3503
  data: options.props.data,
3450
3504
  },
3451
3505
  })
3452
- : undefined;
3506
+ : serverOnlyRouteMarkerParts(options.routePath, options.markerRequest);
3453
3507
 
3454
3508
  sink.append("<!DOCTYPE html>");
3455
3509
  sink.append(
@@ -3667,10 +3721,12 @@ async function runServerStreamModuleWithLoading(
3667
3721
  data: Promise<unknown>;
3668
3722
  define?: UserConfig["define"] | undefined;
3669
3723
  loadingFile: string;
3724
+ markerRequest: Request;
3670
3725
  pageFile: string;
3671
3726
  params: RouteParams;
3672
3727
  queryClient: QueryClient;
3673
3728
  request: Request;
3729
+ requestUrl: string;
3674
3730
  trackedRequest?: TrackedHeaderRequest | undefined;
3675
3731
  routePath: string;
3676
3732
  routeScripts?: ReadonlyMap<string, string> | undefined;
@@ -3717,10 +3773,10 @@ async function runServerStreamModuleWithLoading(
3717
3773
  script: options.script,
3718
3774
  props: {
3719
3775
  params: options.params,
3720
- request: { url: options.request.url },
3776
+ request: { url: options.requestUrl },
3721
3777
  },
3722
3778
  })
3723
- : undefined;
3779
+ : serverOnlyRouteMarkerParts(options.routePath, options.markerRequest);
3724
3780
 
3725
3781
  return renderToReadableStream((sink) => {
3726
3782
  sink.append("<!DOCTYPE html>");
package/src/serve.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Server } from "node:http";
2
+ import type { DehydrateOptions } from "@reckona/mreact-query";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import { join } from "node:path";
4
5
  import type { BuiltPrerenderedRoute } from "./build.js";
@@ -61,6 +62,8 @@ import {
61
62
  isCurrentPrerenderedRoute,
62
63
  isVisitorDependentResponse,
63
64
  PRERENDERED_ROUTE_SCHEMA_VERSION,
65
+ replayedPrerenderedRouteHeaders,
66
+ storedPrerenderedRouteHeaders,
64
67
  } from "./prerender-entry.js";
65
68
 
66
69
  interface BuiltRuntimeCacheEntry {
@@ -99,6 +102,7 @@ let warnedImplicitHostTrust = false;
99
102
  * Configures rendering a request against a built app-router output directory.
100
103
  */
101
104
  export interface RenderBuiltAppRequestOptions {
105
+ dehydrateOptions?: DehydrateOptions | undefined;
102
106
  outDir: string;
103
107
  importPolicy?: AppRouterImportPolicy | undefined;
104
108
  instrumentation?: RouterInstrumentation | undefined;
@@ -156,6 +160,7 @@ export interface BuiltAppRuntimePreloadStrategy {
156
160
  * Configures the Node HTTP server used to serve a built app-router output.
157
161
  */
158
162
  export interface StartServerOptions {
163
+ dehydrateOptions?: DehydrateOptions | undefined;
159
164
  outDir: string;
160
165
  port: number;
161
166
  hostname?: string;
@@ -480,7 +485,7 @@ async function renderBuiltAppRequestWithRuntime(
480
485
  normalizedPath,
481
486
  options.prerenderStore,
482
487
  );
483
- const prerenderedResponse = prerenderedRouteResponse(prerendered, request.method);
488
+ const prerenderedResponse = prerenderedRouteResponse(prerendered, request);
484
489
 
485
490
  if (prerenderedResponse !== undefined) {
486
491
  return prerenderedResponse;
@@ -515,7 +520,7 @@ async function renderBuiltAppRequestWithRuntime(
515
520
  normalizedPath,
516
521
  options.prerenderStore,
517
522
  );
518
- const prerenderedResponse = prerenderedRouteResponse(prerendered, request.method);
523
+ const prerenderedResponse = prerenderedRouteResponse(prerendered, request);
519
524
 
520
525
  if (prerenderedResponse !== undefined) {
521
526
  emitBuiltRenderTiming(options, request, timing, prerenderedResponse.status);
@@ -553,21 +558,27 @@ async function renderBuiltAppRequestWithRuntime(
553
558
 
554
559
  function prerenderedRouteResponse(
555
560
  prerendered: BuiltPrerenderedRoute | undefined,
556
- method: string,
561
+ request: Request,
557
562
  ): Response | undefined {
558
563
  if (!isCurrentPrerenderedRoute(prerendered)) {
559
564
  return undefined;
560
565
  }
561
566
 
562
- if (method === "HEAD") {
567
+ const navigation = request.headers.get("x-mreact-navigation") === "1";
568
+ const html = navigation ? prerendered.navigationHtml : prerendered.html;
569
+ if (html === undefined) {
570
+ return undefined;
571
+ }
572
+
573
+ if (request.method === "HEAD") {
563
574
  return new Response(null, {
564
- headers: prerendered.headers,
575
+ headers: replayedPrerenderedRouteHeaders(prerendered, request),
565
576
  status: prerendered.status,
566
577
  });
567
578
  }
568
579
 
569
- return htmlResponse(prerendered.html, {
570
- headers: prerendered.headers,
580
+ return htmlResponse(html, {
581
+ headers: replayedPrerenderedRouteHeaders(prerendered, request),
571
582
  status: prerendered.status,
572
583
  });
573
584
  }
@@ -709,6 +720,7 @@ export async function startServer(
709
720
  trustForwardedProto: options.trustForwardedProto,
710
721
  render: (request) =>
711
722
  runtime.render(request, {
723
+ dehydrateOptions: options.dehydrateOptions,
712
724
  instrumentation: options.instrumentation,
713
725
  logger: options.logger,
714
726
  onResponse: options.onResponse,
@@ -927,6 +939,7 @@ function builtRenderAppRequestOptions(
927
939
  clientScripts: options.runtime.clientScripts,
928
940
  clientStylesByFile: options.runtime.clientStylesByFile,
929
941
  clientStyles: options.runtime.clientStyles,
942
+ dehydrateOptions: options.dehydrateOptions,
930
943
  importPolicy: {
931
944
  ...options.importPolicy,
932
945
  allowedSourceDirs: options.runtime.allowedSourceDirs,
@@ -976,7 +989,11 @@ async function renderAndCachePrerenderWithLock(
976
989
  options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
977
990
  path: string,
978
991
  ): Promise<Response> {
979
- const existing = options.runtime.prerenderLocks.get(path);
992
+ const lockKey =
993
+ options.request.headers.get("x-mreact-navigation") === "1"
994
+ ? `${path}\0navigation`
995
+ : path;
996
+ const existing = options.runtime.prerenderLocks.get(lockKey);
980
997
 
981
998
  if (existing !== undefined) {
982
999
  const result = await existing;
@@ -988,12 +1005,12 @@ async function renderAndCachePrerenderWithLock(
988
1005
  }
989
1006
 
990
1007
  const task = runPrerenderRegeneration(options, path);
991
- options.runtime.prerenderLocks.set(path, task);
1008
+ options.runtime.prerenderLocks.set(lockKey, task);
992
1009
 
993
1010
  try {
994
1011
  return cloneResponse((await task).response);
995
1012
  } finally {
996
- options.runtime.prerenderLocks.delete(path);
1013
+ options.runtime.prerenderLocks.delete(lockKey);
997
1014
  }
998
1015
  }
999
1016
 
@@ -1001,15 +1018,17 @@ async function runPrerenderRegeneration(
1001
1018
  options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
1002
1019
  path: string,
1003
1020
  ): Promise<{ response: Response; shareable: boolean }> {
1021
+ const lockKey =
1022
+ options.request.headers.get("x-mreact-navigation") === "1"
1023
+ ? `${path}\0navigation`
1024
+ : path;
1004
1025
  const regenerate = async () => {
1005
1026
  const stored = await readPrerenderedRoute(options.runtime, path, options.prerenderStore);
1027
+ const storedResponse = prerenderedRouteResponse(stored, options.request);
1006
1028
 
1007
- if (stored !== undefined) {
1029
+ if (storedResponse !== undefined) {
1008
1030
  return {
1009
- response: htmlResponse(stored.html, {
1010
- headers: stored.headers,
1011
- status: stored.status,
1012
- }),
1031
+ response: storedResponse,
1013
1032
  shareable: true,
1014
1033
  };
1015
1034
  }
@@ -1018,7 +1037,10 @@ async function runPrerenderRegeneration(
1018
1037
  // render that depended on this visitor's request headers must not become
1019
1038
  // one; it is returned to the caller without being stored. The signal starts
1020
1039
  // closed so that a render which never reports back is not stored either.
1021
- const renderSignals = { headerDependent: () => true };
1040
+ const renderSignals = {
1041
+ headerDependent: () => true,
1042
+ strictTransportSecurity: () => undefined as string | undefined,
1043
+ };
1022
1044
  const response = await renderBuiltDynamicResponse({ ...options, renderSignals });
1023
1045
 
1024
1046
  if (!response.ok) {
@@ -1039,21 +1061,34 @@ async function runPrerenderRegeneration(
1039
1061
  };
1040
1062
  }
1041
1063
 
1064
+ const navigation = options.request.headers.get("x-mreact-navigation") === "1";
1065
+ if (navigation && stored === undefined) {
1066
+ return {
1067
+ response: htmlResponse(body, {
1068
+ headers: response.headers,
1069
+ status: response.status,
1070
+ }),
1071
+ shareable: false,
1072
+ };
1073
+ }
1074
+
1042
1075
  return {
1043
1076
  response: await cacheRegeneratedPrerenderedRoute(
1044
- options.runtime,
1045
- path,
1046
- body,
1047
- response,
1048
- options.prerenderStore,
1049
- ),
1077
+ options.runtime,
1078
+ path,
1079
+ body,
1080
+ response,
1081
+ renderSignals.strictTransportSecurity(),
1082
+ options.prerenderStore,
1083
+ navigation ? stored : undefined,
1084
+ ),
1050
1085
  shareable: true,
1051
1086
  };
1052
1087
  };
1053
1088
 
1054
1089
  return options.prerenderStore?.withLock === undefined
1055
1090
  ? await regenerate()
1056
- : await options.prerenderStore.withLock(path, regenerate);
1091
+ : await options.prerenderStore.withLock(lockKey, regenerate);
1057
1092
  }
1058
1093
 
1059
1094
  async function applyBuiltPrerenderInvalidations(
@@ -1079,19 +1114,21 @@ async function cacheRegeneratedPrerenderedRoute(
1079
1114
  path: string,
1080
1115
  body: string,
1081
1116
  response: Response,
1117
+ strictTransportSecurity: string | undefined,
1082
1118
  store: AppRouterPrerenderStore | undefined,
1119
+ existing?: BuiltPrerenderedRoute | undefined,
1083
1120
  ): Promise<Response> {
1084
- const headers: Record<string, string> = {};
1085
-
1086
- response.headers.forEach((value, key) => {
1087
- headers[key] = value;
1088
- });
1089
- const entry: BuiltPrerenderedRoute = {
1090
- headers,
1091
- html: body,
1092
- schemaVersion: PRERENDERED_ROUTE_SCHEMA_VERSION,
1093
- status: response.status,
1094
- };
1121
+ const headers = storedPrerenderedRouteHeaders(response.headers);
1122
+ const entry: BuiltPrerenderedRoute =
1123
+ existing === undefined
1124
+ ? {
1125
+ headers,
1126
+ html: body,
1127
+ schemaVersion: PRERENDERED_ROUTE_SCHEMA_VERSION,
1128
+ status: response.status,
1129
+ ...(strictTransportSecurity === undefined ? {} : { strictTransportSecurity }),
1130
+ }
1131
+ : { ...existing, navigationHtml: body };
1095
1132
  runtime.prerenderedRoutes.set(path, entry);
1096
1133
  await store?.set(path, entry);
1097
1134