@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.
- package/README.md +6 -3
- package/dist/actions.d.ts +2 -3
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +61 -41
- package/dist/actions.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +3 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +74 -11
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/node.d.ts +7 -0
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +7 -1
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/static.d.ts.map +1 -1
- package/dist/adapters/static.js +14 -3
- package/dist/adapters/static.js.map +1 -1
- package/dist/build.d.ts +5 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +137 -14
- package/dist/build.js.map +1 -1
- package/dist/built-runtime.d.ts +4 -1
- package/dist/built-runtime.d.ts.map +1 -1
- package/dist/built-runtime.js.map +1 -1
- package/dist/cache.d.ts +27 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +83 -16
- package/dist/cache.js.map +1 -1
- package/dist/cli-options.d.ts +7 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +27 -2
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -2
- package/dist/client.js.map +1 -1
- package/dist/navigation-marker.d.ts +2 -0
- package/dist/navigation-marker.d.ts.map +1 -0
- package/dist/navigation-marker.js +294 -0
- package/dist/navigation-marker.js.map +1 -0
- package/dist/navigation-runtime.d.ts +1 -1
- package/dist/navigation-runtime.d.ts.map +1 -1
- package/dist/navigation-runtime.js +1 -1
- package/dist/navigation-runtime.js.map +1 -1
- package/dist/node-server.d.ts +6 -0
- package/dist/node-server.d.ts.map +1 -1
- package/dist/node-server.js +19 -1
- package/dist/node-server.js.map +1 -1
- package/dist/prerender-entry.d.ts +7 -0
- package/dist/prerender-entry.d.ts.map +1 -0
- package/dist/prerender-entry.js +99 -0
- package/dist/prerender-entry.js.map +1 -0
- package/dist/render.d.ts +20 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +190 -110
- package/dist/render.js.map +1 -1
- package/dist/request-header-tracking.d.ts +44 -0
- package/dist/request-header-tracking.d.ts.map +1 -0
- package/dist/request-header-tracking.js +116 -0
- package/dist/request-header-tracking.js.map +1 -0
- package/dist/route-source.d.ts +16 -0
- package/dist/route-source.d.ts.map +1 -1
- package/dist/route-source.js +55 -1
- package/dist/route-source.js.map +1 -1
- package/dist/security-headers.d.ts +16 -0
- package/dist/security-headers.d.ts.map +1 -1
- package/dist/security-headers.js +37 -0
- package/dist/security-headers.js.map +1 -1
- package/dist/serve.d.ts +10 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +132 -42
- package/dist/serve.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +72 -56
- package/src/adapters/cloudflare.ts +118 -16
- package/src/adapters/node.ts +14 -1
- package/src/adapters/static.ts +18 -5
- package/src/build.ts +176 -12
- package/src/built-runtime.ts +2 -2
- package/src/cache.ts +127 -15
- package/src/cli-options.ts +39 -2
- package/src/cli.ts +7 -1
- package/src/client.ts +15 -2
- package/src/navigation-marker.ts +353 -0
- package/src/navigation-runtime.ts +1 -0
- package/src/node-server.ts +27 -1
- package/src/prerender-entry.ts +129 -0
- package/src/render.ts +267 -121
- package/src/request-header-tracking.ts +161 -0
- package/src/route-source.ts +71 -0
- package/src/security-headers.ts +41 -0
- 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,
|
|
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
|
-
|
|
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
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
733
|
+
request: { url: url.pathname },
|
|
718
734
|
data,
|
|
719
735
|
},
|
|
720
736
|
})
|
|
721
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
981
|
+
request: { url: url.pathname },
|
|
961
982
|
data,
|
|
962
983
|
},
|
|
963
984
|
})
|
|
964
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
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) =>
|
|
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.
|
|
2215
|
+
request: { url: options.requestUrl },
|
|
2101
2216
|
data: options.props.data,
|
|
2102
2217
|
},
|
|
2103
2218
|
})
|
|
2104
|
-
:
|
|
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
|
-
|
|
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.
|
|
2348
|
+
request: { url: options.requestUrl },
|
|
2235
2349
|
},
|
|
2236
2350
|
})
|
|
2237
|
-
:
|
|
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
|
-
|
|
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) {
|