@reckona/mreact-router 0.0.201 → 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 (93) hide show
  1. package/README.md +6 -3
  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 +74 -11
  9. package/dist/adapters/cloudflare.js.map +1 -1
  10. package/dist/adapters/node.d.ts +7 -0
  11. package/dist/adapters/node.d.ts.map +1 -1
  12. package/dist/adapters/node.js +7 -1
  13. package/dist/adapters/node.js.map +1 -1
  14. package/dist/adapters/static.d.ts.map +1 -1
  15. package/dist/adapters/static.js +14 -3
  16. package/dist/adapters/static.js.map +1 -1
  17. package/dist/build.d.ts +5 -0
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +137 -14
  20. package/dist/build.js.map +1 -1
  21. package/dist/built-runtime.d.ts +4 -1
  22. package/dist/built-runtime.d.ts.map +1 -1
  23. package/dist/built-runtime.js.map +1 -1
  24. package/dist/cache.d.ts +27 -1
  25. package/dist/cache.d.ts.map +1 -1
  26. package/dist/cache.js +83 -16
  27. package/dist/cache.js.map +1 -1
  28. package/dist/cli-options.d.ts +7 -0
  29. package/dist/cli-options.d.ts.map +1 -1
  30. package/dist/cli-options.js +27 -2
  31. package/dist/cli-options.js.map +1 -1
  32. package/dist/cli.js +3 -2
  33. package/dist/cli.js.map +1 -1
  34. package/dist/client.d.ts +5 -0
  35. package/dist/client.d.ts.map +1 -1
  36. package/dist/client.js +11 -2
  37. package/dist/client.js.map +1 -1
  38. package/dist/navigation-marker.d.ts +2 -0
  39. package/dist/navigation-marker.d.ts.map +1 -0
  40. package/dist/navigation-marker.js +294 -0
  41. package/dist/navigation-marker.js.map +1 -0
  42. package/dist/navigation-runtime.d.ts +1 -1
  43. package/dist/navigation-runtime.d.ts.map +1 -1
  44. package/dist/navigation-runtime.js +1 -1
  45. package/dist/navigation-runtime.js.map +1 -1
  46. package/dist/node-server.d.ts +6 -0
  47. package/dist/node-server.d.ts.map +1 -1
  48. package/dist/node-server.js +19 -1
  49. package/dist/node-server.js.map +1 -1
  50. package/dist/prerender-entry.d.ts +7 -0
  51. package/dist/prerender-entry.d.ts.map +1 -0
  52. package/dist/prerender-entry.js +99 -0
  53. package/dist/prerender-entry.js.map +1 -0
  54. package/dist/render.d.ts +20 -1
  55. package/dist/render.d.ts.map +1 -1
  56. package/dist/render.js +190 -110
  57. package/dist/render.js.map +1 -1
  58. package/dist/request-header-tracking.d.ts +44 -0
  59. package/dist/request-header-tracking.d.ts.map +1 -0
  60. package/dist/request-header-tracking.js +116 -0
  61. package/dist/request-header-tracking.js.map +1 -0
  62. package/dist/route-source.d.ts +16 -0
  63. package/dist/route-source.d.ts.map +1 -1
  64. package/dist/route-source.js +55 -1
  65. package/dist/route-source.js.map +1 -1
  66. package/dist/security-headers.d.ts +16 -0
  67. package/dist/security-headers.d.ts.map +1 -1
  68. package/dist/security-headers.js +37 -0
  69. package/dist/security-headers.js.map +1 -1
  70. package/dist/serve.d.ts +10 -0
  71. package/dist/serve.d.ts.map +1 -1
  72. package/dist/serve.js +132 -42
  73. package/dist/serve.js.map +1 -1
  74. package/package.json +11 -11
  75. package/src/actions.ts +72 -56
  76. package/src/adapters/cloudflare.ts +118 -16
  77. package/src/adapters/node.ts +14 -1
  78. package/src/adapters/static.ts +18 -5
  79. package/src/build.ts +176 -12
  80. package/src/built-runtime.ts +2 -2
  81. package/src/cache.ts +127 -15
  82. package/src/cli-options.ts +39 -2
  83. package/src/cli.ts +7 -1
  84. package/src/client.ts +15 -2
  85. package/src/navigation-marker.ts +353 -0
  86. package/src/navigation-runtime.ts +1 -0
  87. package/src/node-server.ts +27 -1
  88. package/src/prerender-entry.ts +129 -0
  89. package/src/render.ts +267 -121
  90. package/src/request-header-tracking.ts +161 -0
  91. package/src/route-source.ts +71 -0
  92. package/src/security-headers.ts +41 -0
  93. package/src/serve.ts +188 -48
package/dist/render.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { readFileSync } from "node:fs";
3
4
  import { access, readFile, stat } from "node:fs/promises";
4
5
  import { dirname, join, relative, resolve, sep } from "node:path";
5
6
  import { formatDiagnostic, transform } from "@reckona/mreact-compiler";
6
7
  import { createQueryClient, dehydrate, __MREACT_QUERY_STATE_SCRIPT_ID, installQueryAsyncStorage, runWithQueryClient, } from "@reckona/mreact-query";
7
8
  import { createStringSink, renderAsyncBoundary, renderOutOfOrderReorderScript, renderReactNodeToString, renderToReadableStream, } from "@reckona/mreact-server";
8
9
  import { createClientRouteInferenceCache, formatClientRouteInferenceDiagnostic, inferClientRouteModule, } from "./client-route-inference.js";
9
- import { hydrationMarkerParts, routeIdForPath, withHydrationMarkers, withRouteMarkers, } from "./navigation-runtime.js";
10
+ import { hydrationMarkerParts, routeMarkerParts, routeIdForPath, withHydrationMarkers, } from "./navigation-runtime.js";
10
11
  import { assetPath } from "./assets.js";
11
12
  import { escapeHtmlAttribute } from "@reckona/mreact-shared/html-escape";
12
13
  import { matchRoute, scanAppRoutes } from "./routes.js";
@@ -14,13 +15,15 @@ import { appFileConventionContentType } from "./file-conventions.js";
14
15
  import { dispatchServerActionRequest, prepareRouteServerActionPlaceholders, prepareRouteServerActions, } from "./actions.js";
15
16
  import { serverActionCookie } from "./csrf.js";
16
17
  import { activeRouteCacheContext, withRouteCacheContext, cachedRouteResponse, cacheRouteResponse, routeCacheKey, routeCachePolicyFromSource, } from "./cache.js";
18
+ import { trackRequestHeaderReads, withTrackedRequest, } from "./request-header-tracking.js";
19
+ import { configuredHstsHeader, hasInvalidConfiguredHsts, } from "./security-headers.js";
17
20
  import { resolveRouterCacheLimit } from "./cache-config.js";
18
21
  import { importAppRouterBuiltFileModule, importAppRouterFileModule, importAppRouterSourceModule, fileImportMetaUrlPlugin, rewriteCompatVendorPlaceholderImportsForRunner, } from "./module-runner.js";
19
22
  import { bytesResponse, htmlResponse } from "./http.js";
20
23
  import { isNotFoundError, isRedirectError, rewriteLocation } from "./navigation.js";
21
24
  import { createAppRouterImportPolicyPlugin } from "./import-policy.js";
22
25
  import { existingRouteShellCandidates } from "./route-shells.js";
23
- import { hasLoaderExport, isStreamRouteSource, routeClosureMayUseAwaitBoundary, stripRouteBuildExports, stripRouteModuleExports, stripRouteClientOnlyExports, stripRouteClientSource, stripRouteLoaderOnlyExports, stripRouteMetadataOnlyExports, } from "./route-source.js";
26
+ import { hasLoaderExport, isStreamRouteSource, routeClosureMayUseAwaitBoundary, routeClosureMayUseRequestInput, stripRouteBuildExports, stripRouteModuleExports, stripRouteClientOnlyExports, stripRouteClientSource, stripRouteLoaderOnlyExports, stripRouteMetadataOnlyExports, } from "./route-source.js";
24
27
  import { emitRouterLog, logDurationMs, logNow } from "./logger.js";
25
28
  import { createRouterRuntimeCacheCounters, readRouterRuntimeCacheEntry, routerRuntimeCacheStat, } from "./cache-stats.js";
26
29
  import { bundleRouterModule } from "./bundle-pipeline.js";
@@ -240,23 +243,17 @@ export function routerRenderRuntimeCacheStats() {
240
243
  routerRuntimeCacheStat("middleware-module", middlewareModuleCache, maxMiddlewareModuleCacheEntries, middlewareModuleCacheCounters),
241
244
  routerRuntimeCacheStat("server-route-module", serverRouteModuleCache, maxServerRouteModuleCacheEntries, serverRouteModuleCacheCounters),
242
245
  routerRuntimeCacheStat("composed-route-metadata", composedRouteMetadataCache, maxComposedRouteMetadataCacheEntries, composedRouteMetadataCacheCounters),
243
- routerRuntimeCacheStat("rendered-shell", renderedShellCache, MAX_RENDERED_SHELL_CACHE_ENTRIES, renderedShellCacheCounters),
246
+ // Preserve the public diagnostics entry while making explicit that rendered HTML reuse is disabled.
247
+ {
248
+ evictions: 0,
249
+ hits: 0,
250
+ maxEntries: 0,
251
+ misses: 0,
252
+ name: "rendered-shell",
253
+ size: 0,
254
+ },
244
255
  ];
245
256
  }
246
- // Issue 086: per-shell prefix/suffix cache. Pure layouts (whose
247
- // exported component takes zero arguments and therefore cannot
248
- // depend on the request props) produce the same HTML for every
249
- // request, so we cache the already-split { prefix, suffix } strings
250
- // keyed by appDir + shellFile + serverModuleCacheVersion. Impure
251
- // layouts (function.length > 0) are tagged "impure" so we skip the
252
- // detection on subsequent requests but still render per-request.
253
- //
254
- // The cache is only active when a version is present (production
255
- // builds); dev mode keeps the previous behaviour so reloads pick up
256
- // edits without server restart.
257
- const renderedShellCache = new Map();
258
- const MAX_RENDERED_SHELL_CACHE_ENTRIES = 1024;
259
- const renderedShellCacheCounters = createRouterRuntimeCacheCounters();
260
257
  /**
261
258
  * Renders a source app-router request into a `Response`.
262
259
  */
@@ -449,6 +446,9 @@ async function renderAppRequestInternal(options) {
449
446
  renderSingleFlightNavigation: async (singleFlight) => renderAppRequestInternal({
450
447
  ...options,
451
448
  matchedRoute: undefined,
449
+ // This renders a different route than the caller asked about, so its
450
+ // header dependence says nothing about the caller's request.
451
+ renderSignals: undefined,
452
452
  request: singleFlightNavigationRequest({
453
453
  path: singleFlight.path,
454
454
  request: singleFlight.request,
@@ -490,6 +490,10 @@ async function renderAppRequestInternal(options) {
490
490
  }
491
491
  const queryClient = options.queryClient ?? createQueryClient();
492
492
  let recoveryRoute;
493
+ let trackedRequest;
494
+ let sourceUsesRequestInput = true;
495
+ let invalidConfiguredHsts = false;
496
+ let renderedStrictTransportSecurity;
493
497
  return (await withRouteCacheContext(options.routeCache, async () => {
494
498
  try {
495
499
  if (matched.route.kind === "asset") {
@@ -554,13 +558,16 @@ async function renderAppRequestInternal(options) {
554
558
  timing,
555
559
  vitePlugins: options.vitePlugins,
556
560
  });
561
+ sourceUsesRequestInput = originalAnalysis.usesRequestInput;
557
562
  finishRenderTimingPhase(timing, phaseStartedAt, "sourceAnalysisMs");
558
563
  const cachePolicy = originalAnalysis.cachePolicy;
559
564
  const navigationScript = options.navigationScripts?.get(matched.route.path);
560
- const cacheKey = routeCacheKey(options.appDir, matched.route.path, url);
561
- const mayUseRouteCache = cachePolicy === undefined
562
- ? originalAnalysis.usesRuntimeCacheControl
563
- : cachePolicy.revalidateSeconds !== 0;
565
+ const navigationRequest = isNavigationRequest(options.request);
566
+ const cacheKey = routeCacheKey(options.appDir, matched.route.path, url, navigationRequest ? "navigation" : "document");
567
+ const mayUseRouteCache = !sourceUsesRequestInput &&
568
+ (cachePolicy === undefined
569
+ ? originalAnalysis.usesRuntimeCacheControl
570
+ : cachePolicy.revalidateSeconds !== 0);
564
571
  const reloadRouteCache = isNavigationRouteCacheReloadRequest(options.request);
565
572
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
566
573
  const cachedResponse = !mayUseRouteCache || reloadRouteCache
@@ -574,6 +581,17 @@ async function renderAppRequestInternal(options) {
574
581
  if (cachedResponse !== undefined) {
575
582
  return cachedResponse;
576
583
  }
584
+ // Only a route whose result may be stored pays for header-read tracking;
585
+ // cache hits returned above never construct it. Application code reads
586
+ // headers through this request so that a render depending on one is never
587
+ // stored under a key that ignores it. Callers that store by path alone ask
588
+ // for tracking through `renderSignals` even when the route itself declares
589
+ // no cache policy.
590
+ trackedRequest =
591
+ mayUseRouteCache || options.renderSignals !== undefined
592
+ ? trackRequestHeaderReads(options.request)
593
+ : undefined;
594
+ const appRequest = trackedRequest?.request ?? options.request;
577
595
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
578
596
  const preparedActions = await prepareRouteServerActions({
579
597
  appDir: options.appDir,
@@ -614,12 +632,11 @@ async function renderAppRequestInternal(options) {
614
632
  ? loadRouteDataWithInstrumentation({
615
633
  appDir: options.appDir,
616
634
  code: originalCode,
617
- context: {
635
+ context: withTrackedRequest({
618
636
  env: options.env,
619
637
  params: matched.params,
620
638
  queryClient,
621
- request: options.request,
622
- },
639
+ }, appRequest, trackedRequest),
623
640
  filename: matched.route.file,
624
641
  importPolicy: options.importPolicy,
625
642
  instrumentation: options.instrumentation,
@@ -640,7 +657,7 @@ async function renderAppRequestInternal(options) {
640
657
  clientRoute,
641
658
  props: {
642
659
  params: matched.params,
643
- request: { url: options.request.url },
660
+ request: { url: url.pathname },
644
661
  },
645
662
  routePath: matched.route.path,
646
663
  script: clientScript,
@@ -698,12 +715,11 @@ async function renderAppRequestInternal(options) {
698
715
  headers: { "content-type": "text/plain; charset=utf-8" },
699
716
  });
700
717
  }
701
- const renderedPage = await runWithQueryClient(queryClient, () => runServerModuleWithSlots(stringOutput.code, {
718
+ const renderedPage = await runWithQueryClient(queryClient, () => runServerModuleWithSlots(stringOutput.code, withTrackedRequest({
702
719
  data,
703
720
  params: matched.params,
704
721
  queryClient,
705
- request: options.request,
706
- }, matched.route.file, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, undefined, options.importPolicy, options.devServerModuleCacheVersion));
722
+ }, appRequest, trackedRequest), matched.route.file, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, undefined, options.importPolicy, options.devServerModuleCacheVersion));
707
723
  const pageHtml = renderedPage.html;
708
724
  const pageHtmlForLayout = clientRoute
709
725
  ? withHydrationMarkers({
@@ -714,26 +730,20 @@ async function renderAppRequestInternal(options) {
714
730
  script: clientScript,
715
731
  props: {
716
732
  params: matched.params,
717
- request: { url: options.request.url },
733
+ request: { url: url.pathname },
718
734
  data,
719
735
  },
720
736
  })
721
- : isNavigationRequest(options.request)
722
- ? withRouteMarkers({
723
- html: pageHtml,
724
- routePath: matched.route.path,
725
- })
726
- : pageHtml;
737
+ : withServerOnlyRouteMarkers(pageHtml, matched.route.path, options.request);
727
738
  let html = await runWithQueryClient(queryClient, () => applyLayouts({
728
739
  appDir: options.appDir,
729
740
  pageFile: matched.route.file,
730
741
  html: pageHtmlForLayout,
731
- props: {
742
+ props: withTrackedRequest({
732
743
  data,
733
744
  params: matched.params,
734
745
  queryClient,
735
- request: options.request,
736
- },
746
+ }, appRequest, trackedRequest),
737
747
  slots: renderedPage.slots,
738
748
  serverModules: options.serverModules,
739
749
  serverModuleCacheVersion: options.serverModuleCacheVersion,
@@ -746,11 +756,10 @@ async function renderAppRequestInternal(options) {
746
756
  const metadata = await loadComposedRouteMetadata({
747
757
  appDir: options.appDir,
748
758
  code: originalCode,
749
- context: {
759
+ context: withTrackedRequest({
750
760
  data,
751
761
  params: matched.params,
752
- request: options.request,
753
- },
762
+ }, appRequest, trackedRequest),
754
763
  filename: matched.route.file,
755
764
  importPolicy: options.importPolicy,
756
765
  routes,
@@ -770,7 +779,7 @@ async function renderAppRequestInternal(options) {
770
779
  serverModuleCacheVersion: options.serverModuleCacheVersion,
771
780
  });
772
781
  html = injectAuthSessionClaims(html, originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined);
773
- html = injectQueryState(html, dehydrate(queryClient));
782
+ html = injectQueryState(html, dehydrate(queryClient, options.dehydrateOptions));
774
783
  const response = withOptionalActionCookie(htmlResponse(applyActionHtmlReplacements(`<!DOCTYPE html>${clientNavigationHeadTags({
775
784
  assetBaseUrl: options.assetBaseUrl,
776
785
  currentStyleSheets: clientStyleSheets,
@@ -782,6 +791,7 @@ async function renderAppRequestInternal(options) {
782
791
  "x-mreact-stream": "1",
783
792
  }),
784
793
  }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
794
+ appendNavigationVary(response.headers);
785
795
  emitRenderTiming(options, timing, response.status);
786
796
  return response;
787
797
  }
@@ -848,10 +858,13 @@ async function renderAppRequestInternal(options) {
848
858
  data: streamDataPromise,
849
859
  define: options.define,
850
860
  loadingFile,
861
+ markerRequest: options.request,
851
862
  pageFile: matched.route.file,
852
863
  params: matched.params,
853
864
  queryClient,
854
- request: options.request,
865
+ request: appRequest,
866
+ requestUrl: url.pathname,
867
+ trackedRequest,
855
868
  routePath: matched.route.path,
856
869
  routeScripts: options.clientScripts,
857
870
  serverModules: options.serverModules,
@@ -867,22 +880,24 @@ async function renderAppRequestInternal(options) {
867
880
  const response = withOptionalActionCookie(new Response(stream, {
868
881
  headers: streamShellResponseHeaders,
869
882
  }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
883
+ appendNavigationVary(response.headers);
870
884
  emitRenderTiming(options, timing, response.status);
871
885
  return response;
872
886
  }
873
887
  const data = streamData;
874
- const props = {
888
+ const props = withTrackedRequest({
875
889
  data,
876
890
  params: matched.params,
877
891
  queryClient,
878
- request: options.request,
879
- };
892
+ }, appRequest, trackedRequest);
880
893
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
881
894
  const stream = runServerStreamModule(output.code, {
882
895
  appDir: options.appDir,
883
896
  assetBaseUrl: options.assetBaseUrl,
884
897
  pageFile: matched.route.file,
898
+ markerRequest: options.request,
885
899
  props,
900
+ requestUrl: url.pathname,
886
901
  routePath: matched.route.path,
887
902
  routeScripts: options.clientScripts,
888
903
  serverModules: options.serverModules,
@@ -900,6 +915,7 @@ async function renderAppRequestInternal(options) {
900
915
  const response = withOptionalActionCookie(new Response(stream, {
901
916
  headers: streamShellResponseHeaders,
902
917
  }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
918
+ appendNavigationVary(response.headers);
903
919
  emitRenderTiming(options, timing, response.status);
904
920
  return response;
905
921
  }
@@ -915,6 +931,12 @@ async function renderAppRequestInternal(options) {
915
931
  emitRenderTiming(options, timing, data.status);
916
932
  return data;
917
933
  }
934
+ sourceUsesRequestInput ||= await routeShellsMayUseRequestInput({
935
+ appDir: options.appDir,
936
+ pageFile: matched.route.file,
937
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
938
+ serverSourceFiles: options.serverSourceFiles,
939
+ });
918
940
  await waitForRenderPreload(options, timing);
919
941
  await loadServerRenderArtifacts(options, matched.route.file, timing);
920
942
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
@@ -935,12 +957,11 @@ async function renderAppRequestInternal(options) {
935
957
  });
936
958
  }
937
959
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
938
- const renderedPage = await runWithQueryClient(queryClient, () => runServerModuleWithSlots(output.code, {
960
+ const renderedPage = await runWithQueryClient(queryClient, () => runServerModuleWithSlots(output.code, withTrackedRequest({
939
961
  data,
940
962
  params: matched.params,
941
963
  queryClient,
942
- request: options.request,
943
- }, matched.route.file, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, timing, options.importPolicy, options.devServerModuleCacheVersion));
964
+ }, appRequest, trackedRequest), matched.route.file, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, timing, options.importPolicy, options.devServerModuleCacheVersion));
944
965
  finishRenderTimingPhase(timing, phaseStartedAt, "pageRenderMs");
945
966
  const pageHtml = renderedPage.html;
946
967
  // Wrap the page (not the full document) with the hydration marker so
@@ -957,27 +978,21 @@ async function renderAppRequestInternal(options) {
957
978
  script: clientScript,
958
979
  props: {
959
980
  params: matched.params,
960
- request: { url: options.request.url },
981
+ request: { url: url.pathname },
961
982
  data,
962
983
  },
963
984
  })
964
- : isNavigationRequest(options.request)
965
- ? withRouteMarkers({
966
- html: pageHtml,
967
- routePath: matched.route.path,
968
- })
969
- : pageHtml;
985
+ : withServerOnlyRouteMarkers(pageHtml, matched.route.path, options.request);
970
986
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
971
987
  let html = await runWithQueryClient(queryClient, () => applyLayouts({
972
988
  appDir: options.appDir,
973
989
  pageFile: matched.route.file,
974
990
  html: pageHtmlForLayout,
975
- props: {
991
+ props: withTrackedRequest({
976
992
  data,
977
993
  params: matched.params,
978
994
  queryClient,
979
- request: options.request,
980
- },
995
+ }, appRequest, trackedRequest),
981
996
  slots: renderedPage.slots,
982
997
  serverModules: options.serverModules,
983
998
  serverModuleCacheVersion: options.serverModuleCacheVersion,
@@ -991,11 +1006,10 @@ async function renderAppRequestInternal(options) {
991
1006
  const metadata = await loadComposedRouteMetadata({
992
1007
  appDir: options.appDir,
993
1008
  code: originalCode,
994
- context: {
1009
+ context: withTrackedRequest({
995
1010
  data,
996
1011
  params: matched.params,
997
- request: options.request,
998
- },
1012
+ }, appRequest, trackedRequest),
999
1013
  filename: matched.route.file,
1000
1014
  importPolicy: options.importPolicy,
1001
1015
  routes,
@@ -1017,7 +1031,7 @@ async function renderAppRequestInternal(options) {
1017
1031
  serverModuleCacheVersion: options.serverModuleCacheVersion,
1018
1032
  });
1019
1033
  html = injectAuthSessionClaims(html, originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined);
1020
- html = injectQueryState(html, dehydrate(queryClient));
1034
+ html = injectQueryState(html, dehydrate(queryClient, options.dehydrateOptions));
1021
1035
  const response = withOptionalActionCookie(htmlResponse(applyActionHtmlReplacements(`<!DOCTYPE html>${clientNavigationHeadTags({
1022
1036
  assetBaseUrl: options.assetBaseUrl,
1023
1037
  currentStyleSheets: clientStyleSheets,
@@ -1027,12 +1041,24 @@ async function renderAppRequestInternal(options) {
1027
1041
  })}${html}`, preparedActions.htmlReplacements), {
1028
1042
  headers: responseHeadersForMetadata(metadata, options.request),
1029
1043
  }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
1044
+ appendNavigationVary(response.headers);
1030
1045
  const effectiveCachePolicy = cachePolicy ?? activeRouteCacheContext()?.cachePolicy;
1046
+ const configuredHsts = configuredHstsHeader(metadata?.security);
1047
+ renderedStrictTransportSecurity = configuredHsts;
1048
+ invalidConfiguredHsts = hasInvalidConfiguredHsts(metadata?.security);
1031
1049
  const finalResponse = preparedActions.hasFormActions
1032
1050
  ? withRouteCacheHeader(response, effectiveCachePolicy)
1033
1051
  : await cacheRouteResponse({
1034
1052
  key: cacheKey,
1035
1053
  cache: options.routeCache,
1054
+ // A policy can still arrive from a runtime cacheControl() call made
1055
+ // outside the page source, which is what `mayUseRouteCache` scans.
1056
+ // Without a tracker there is no evidence the render ignored request
1057
+ // headers, so the entry is treated as header dependent.
1058
+ headerDependent: sourceUsesRequestInput ||
1059
+ invalidConfiguredHsts ||
1060
+ (trackedRequest?.requestDependent() ?? true),
1061
+ ...(configuredHsts === undefined ? {} : { strictTransportSecurity: configuredHsts }),
1036
1062
  path: matched.route.path,
1037
1063
  policy: effectiveCachePolicy,
1038
1064
  request: options.request,
@@ -1103,13 +1129,32 @@ async function renderAppRequestInternal(options) {
1103
1129
  emitRenderTiming(options, timing, response.status);
1104
1130
  return response;
1105
1131
  }
1132
+ finally {
1133
+ // Reported from every return path, including the streaming ones, so that a
1134
+ // caller storing by path alone never mistakes an unreported render for a
1135
+ // shareable one. The value stays lazy because a streamed render can still
1136
+ // read request headers while its deferred parts resolve; callers read it
1137
+ // after draining the body.
1138
+ if (options.renderSignals !== undefined) {
1139
+ const tracked = trackedRequest;
1140
+ options.renderSignals.headerDependent = () => sourceUsesRequestInput || invalidConfiguredHsts || (tracked?.requestDependent() ?? true);
1141
+ options.renderSignals.strictTransportSecurity = () => renderedStrictTransportSecurity;
1142
+ }
1143
+ }
1106
1144
  })).value;
1107
1145
  }
1108
1146
  async function applyAppRouterResponseHook(response, options) {
1109
1147
  const onResponse = await resolveAppRouterResponseHook(options);
1110
- const hooked = await onResponse?.(response, {
1111
- request: options.request,
1112
- });
1148
+ if (onResponse === undefined) {
1149
+ return response;
1150
+ }
1151
+ const renderSignals = options.renderSignals;
1152
+ const trackedRequest = renderSignals === undefined ? undefined : trackRequestHeaderReads(options.request);
1153
+ const hooked = await onResponse(response, withTrackedRequest({}, trackedRequest?.request ?? options.request, trackedRequest));
1154
+ if (renderSignals !== undefined && trackedRequest !== undefined) {
1155
+ const renderHeaderDependent = renderSignals.headerDependent;
1156
+ renderSignals.headerDependent = () => renderHeaderDependent() || trackedRequest.requestDependent();
1157
+ }
1113
1158
  return hooked instanceof Response ? hooked : response;
1114
1159
  }
1115
1160
  export async function resolveAppRouterResponseHook(options) {
@@ -1231,6 +1276,32 @@ function routePrefetchManifestScript(routeScripts, assetBaseUrl) {
1231
1276
  function isNavigationRequest(request) {
1232
1277
  return request.headers.get("x-mreact-navigation") === "1";
1233
1278
  }
1279
+ function withServerOnlyRouteMarkers(html, routePath, request) {
1280
+ const marker = serverOnlyRouteMarkerParts(routePath, request);
1281
+ return marker === undefined ? html : `${marker.prefix}${html}${marker.suffix}`;
1282
+ }
1283
+ function serverOnlyRouteMarkerParts(routePath, request) {
1284
+ if (request.headers.get("x-mreact-prerender-variant-capture") === "1") {
1285
+ return prerenderVariantMarkerParts(routePath);
1286
+ }
1287
+ return isNavigationRequest(request) ? routeMarkerParts(routePath) : undefined;
1288
+ }
1289
+ export function prerenderVariantMarkerParts(routePath) {
1290
+ const routeId = routeIdForPath(routePath);
1291
+ return {
1292
+ prefix: `<!--mreact-route-variant-start:${routeId}-->`,
1293
+ suffix: `<!--mreact-route-variant-end:${routeId}-->`,
1294
+ };
1295
+ }
1296
+ function appendNavigationVary(headers) {
1297
+ const vary = headers.get("vary");
1298
+ if (vary
1299
+ ?.split(",")
1300
+ .some((value) => value.trim().toLowerCase() === "x-mreact-navigation") === true) {
1301
+ return;
1302
+ }
1303
+ headers.append("vary", "x-mreact-navigation");
1304
+ }
1234
1305
  function isNavigationRouteCacheReloadRequest(request) {
1235
1306
  return (isNavigationRequest(request) && request.headers.get("x-mreact-navigation-cache") === "reload");
1236
1307
  }
@@ -1934,6 +2005,9 @@ function routeSourceAnalysisFromArtifact(artifact) {
1934
2005
  hasLoader: artifact.hasLoader,
1935
2006
  routeCode: artifact.routeCode,
1936
2007
  streamRoute: artifact.streamRoute,
2008
+ // Older built artifacts predate request-input safety analysis. Treat them
2009
+ // as dependent until rebuilt rather than trusting an absent safety signal.
2010
+ usesRequestInput: artifact.usesRequestInput ?? true,
1937
2011
  usesRuntimeCacheControl: artifact.usesRuntimeCacheControl,
1938
2012
  };
1939
2013
  }
@@ -1962,11 +2036,52 @@ async function analyzeRouteSourceUncached(options) {
1962
2036
  projectRoot: options.appDir,
1963
2037
  source: options.code,
1964
2038
  }),
2039
+ usesRequestInput: routeClosureMayUseRequestInput({
2040
+ filename: options.filename,
2041
+ files: routeSourceFilesForAnalysis(options),
2042
+ projectRoot: options.appDir,
2043
+ source: options.code,
2044
+ }),
1965
2045
  usesRuntimeCacheControl: usesRuntimeCacheControl(options.code),
1966
2046
  };
1967
2047
  }
1968
2048
  function routeSourceFilesForAnalysis(options) {
1969
- return (file) => file === options.filename ? options.code : options.serverSourceFiles?.get(file);
2049
+ return (file) => {
2050
+ const known = file === options.filename ? options.code : options.serverSourceFiles?.get(file);
2051
+ if (known !== undefined) {
2052
+ return known;
2053
+ }
2054
+ const relativeFile = relative(resolve(options.appDir), resolve(file));
2055
+ if (relativeFile === "" || relativeFile === ".." || relativeFile.startsWith(`..${sep}`)) {
2056
+ return undefined;
2057
+ }
2058
+ try {
2059
+ return readFileSync(file, "utf8");
2060
+ }
2061
+ catch {
2062
+ return undefined;
2063
+ }
2064
+ };
2065
+ }
2066
+ async function routeShellsMayUseRequestInput(options) {
2067
+ const shells = await shellFilesForPage(options.appDir, options.pageFile, options.serverModuleCacheVersion);
2068
+ for (const shell of shells) {
2069
+ const source = await readServerSourceFile(shell.file, options.serverModuleCacheVersion, options.serverSourceFiles);
2070
+ if (routeClosureMayUseRequestInput({
2071
+ filename: shell.file,
2072
+ files: routeSourceFilesForAnalysis({
2073
+ appDir: options.appDir,
2074
+ code: source,
2075
+ filename: shell.file,
2076
+ serverSourceFiles: options.serverSourceFiles,
2077
+ }),
2078
+ projectRoot: options.appDir,
2079
+ source,
2080
+ })) {
2081
+ return true;
2082
+ }
2083
+ }
2084
+ return false;
1970
2085
  }
1971
2086
  async function runServerModule(code, props, sourcefile, serverModules, serverModuleCacheVersion, define, vitePlugins) {
1972
2087
  const component = await loadServerComponent(code, sourcefile, serverModules, serverModuleCacheVersion, define, vitePlugins);
@@ -2097,11 +2212,11 @@ function runServerStreamModule(code, options) {
2097
2212
  script: options.script,
2098
2213
  props: {
2099
2214
  params: options.props.params,
2100
- request: { url: options.props.request.url },
2215
+ request: { url: options.requestUrl },
2101
2216
  data: options.props.data,
2102
2217
  },
2103
2218
  })
2104
- : undefined;
2219
+ : serverOnlyRouteMarkerParts(options.routePath, options.markerRequest);
2105
2220
  sink.append("<!DOCTYPE html>");
2106
2221
  sink.append(clientNavigationHeadTags({
2107
2222
  assetBaseUrl: options.assetBaseUrl,
@@ -2215,12 +2330,11 @@ function localServerSourceImportCandidates(base) {
2215
2330
  return candidates;
2216
2331
  }
2217
2332
  async function runServerStreamModuleWithLoading(code, options) {
2218
- const loadingProps = {
2333
+ const loadingProps = withTrackedRequest({
2219
2334
  data: undefined,
2220
2335
  params: options.params,
2221
2336
  queryClient: options.queryClient,
2222
- request: options.request,
2223
- };
2337
+ }, options.request, options.trackedRequest);
2224
2338
  const layoutShells = await layoutShellsForPage(options.appDir, options.pageFile, loadingProps, {}, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles, options.clientRouteInferenceCache, options.define, options.vitePlugins, options.importPolicy);
2225
2339
  const loadingHtml = await renderServerFileToHtml(options.loadingFile, loadingProps, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles, options.define, options.vitePlugins);
2226
2340
  const marker = options.clientRoute
@@ -2231,10 +2345,10 @@ async function runServerStreamModuleWithLoading(code, options) {
2231
2345
  script: options.script,
2232
2346
  props: {
2233
2347
  params: options.params,
2234
- request: { url: options.request.url },
2348
+ request: { url: options.requestUrl },
2235
2349
  },
2236
2350
  })
2237
- : undefined;
2351
+ : serverOnlyRouteMarkerParts(options.routePath, options.markerRequest);
2238
2352
  return renderToReadableStream((sink) => {
2239
2353
  sink.append("<!DOCTYPE html>");
2240
2354
  sink.append(clientNavigationHeadTags({
@@ -2247,12 +2361,11 @@ async function runServerStreamModuleWithLoading(code, options) {
2247
2361
  }
2248
2362
  sink.append(marker?.prefix ?? "");
2249
2363
  renderVisibleOutOfOrderBoundary(sink, "mreact-route", options.data, async (boundarySink, data) => {
2250
- await appendServerStreamModule(code, boundarySink, {
2364
+ await appendServerStreamModule(code, boundarySink, withTrackedRequest({
2251
2365
  data,
2252
2366
  params: options.params,
2253
2367
  queryClient: options.queryClient,
2254
- request: options.request,
2255
- }, options.pageFile, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, options.importPolicy);
2368
+ }, options.request, options.trackedRequest), options.pageFile, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, options.importPolicy);
2256
2369
  }, {
2257
2370
  placeholder(boundarySink) {
2258
2371
  boundarySink.append(loadingHtml);
@@ -2441,12 +2554,6 @@ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, server
2441
2554
  const cacheKey = serverModuleCacheVersion === undefined || hasNamedSlots || shell.kind === "template"
2442
2555
  ? undefined
2443
2556
  : `${appDir}\0${shell.file}\0${serverModuleCacheVersion}\0${importPolicyCacheKey(importPolicy)}\0${viteDefineCacheKey(define)}\0${vitePluginsCacheKey(vitePlugins)}`;
2444
- if (cacheKey !== undefined) {
2445
- const cached = readRouterRuntimeCacheEntry(renderedShellCache, cacheKey, renderedShellCacheCounters);
2446
- if (cached !== undefined && cached !== "impure") {
2447
- return cached;
2448
- }
2449
- }
2450
2557
  const staticEntry = cacheKey === undefined ? undefined : shellStaticRenderCache.get(cacheKey);
2451
2558
  const loadedStaticEntry = staticEntry ??
2452
2559
  (await loadShellStaticRenderEntry({
@@ -2472,33 +2579,6 @@ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, server
2472
2579
  hasOutOfOrderBoundary: loadedStaticEntry.hasOutOfOrderBoundary,
2473
2580
  };
2474
2581
  addRenderTimingPhaseDuration(timing, phaseStartedAt, "layoutSlotSplitMs");
2475
- const shellCacheKey = loadedStaticEntry.shellUsesAwait ? undefined : cacheKey;
2476
- const cached = shellCacheKey !== undefined
2477
- ? readRouterRuntimeCacheEntry(renderedShellCache, shellCacheKey, renderedShellCacheCounters)
2478
- : undefined;
2479
- // Detect purity: a zero-arg component cannot depend on props. The
2480
- // markShellBoundary + splitLayoutSlot output is then constant for
2481
- // the (appDir, shellFile, version) tuple. We only set the cache
2482
- // entry on the first request that observes the function arity; on
2483
- // an "impure" tag we never overwrite it.
2484
- if (shellCacheKey !== undefined && cached !== "impure") {
2485
- if (loadedStaticEntry.component.length === 0) {
2486
- if (renderedShellCache.size >= MAX_RENDERED_SHELL_CACHE_ENTRIES) {
2487
- const oldestKey = renderedShellCache.keys().next().value;
2488
- if (oldestKey !== undefined) {
2489
- renderedShellCache.delete(oldestKey);
2490
- renderedShellCacheCounters.evictions += 1;
2491
- }
2492
- }
2493
- renderedShellCache.set(shellCacheKey, rendered);
2494
- }
2495
- else {
2496
- // Impure — stamp the cache so subsequent lookups short-circuit
2497
- // without re-checking arity. We still run the per-request
2498
- // render path above so the props are honoured.
2499
- renderedShellCache.set(shellCacheKey, "impure");
2500
- }
2501
- }
2502
2582
  return rendered;
2503
2583
  }
2504
2584
  async function loadShellStaticRenderEntry(options) {