@reckona/mreact-router 0.0.201 → 0.0.202
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 +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +51 -4
- 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 +3 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +42 -5
- 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 +26 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +78 -12
- package/dist/cache.js.map +1 -1
- package/dist/cli-options.d.ts +4 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +17 -0
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.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 +5 -0
- package/dist/prerender-entry.d.ts.map +1 -0
- package/dist/prerender-entry.js +41 -0
- package/dist/prerender-entry.js.map +1 -0
- package/dist/render.d.ts +13 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +139 -87
- 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 +7 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +94 -28
- package/dist/serve.js.map +1 -1
- package/package.json +11 -11
- package/src/adapters/cloudflare.ts +81 -9
- package/src/adapters/node.ts +14 -1
- package/src/adapters/static.ts +18 -5
- package/src/build.ts +71 -4
- package/src/built-runtime.ts +2 -2
- package/src/cache.ts +117 -11
- package/src/cli-options.ts +24 -0
- package/src/cli.ts +5 -0
- package/src/node-server.ts +27 -1
- package/src/prerender-entry.ts +56 -0
- package/src/render.ts +188 -98
- 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 +133 -30
package/dist/render.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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";
|
|
@@ -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,9 @@ 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;
|
|
493
496
|
return (await withRouteCacheContext(options.routeCache, async () => {
|
|
494
497
|
try {
|
|
495
498
|
if (matched.route.kind === "asset") {
|
|
@@ -554,13 +557,15 @@ async function renderAppRequestInternal(options) {
|
|
|
554
557
|
timing,
|
|
555
558
|
vitePlugins: options.vitePlugins,
|
|
556
559
|
});
|
|
560
|
+
sourceUsesRequestInput = originalAnalysis.usesRequestInput;
|
|
557
561
|
finishRenderTimingPhase(timing, phaseStartedAt, "sourceAnalysisMs");
|
|
558
562
|
const cachePolicy = originalAnalysis.cachePolicy;
|
|
559
563
|
const navigationScript = options.navigationScripts?.get(matched.route.path);
|
|
560
564
|
const cacheKey = routeCacheKey(options.appDir, matched.route.path, url);
|
|
561
|
-
const mayUseRouteCache =
|
|
562
|
-
|
|
563
|
-
|
|
565
|
+
const mayUseRouteCache = !sourceUsesRequestInput &&
|
|
566
|
+
(cachePolicy === undefined
|
|
567
|
+
? originalAnalysis.usesRuntimeCacheControl
|
|
568
|
+
: cachePolicy.revalidateSeconds !== 0);
|
|
564
569
|
const reloadRouteCache = isNavigationRouteCacheReloadRequest(options.request);
|
|
565
570
|
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
566
571
|
const cachedResponse = !mayUseRouteCache || reloadRouteCache
|
|
@@ -574,6 +579,17 @@ async function renderAppRequestInternal(options) {
|
|
|
574
579
|
if (cachedResponse !== undefined) {
|
|
575
580
|
return cachedResponse;
|
|
576
581
|
}
|
|
582
|
+
// Only a route whose result may be stored pays for header-read tracking;
|
|
583
|
+
// cache hits returned above never construct it. Application code reads
|
|
584
|
+
// headers through this request so that a render depending on one is never
|
|
585
|
+
// stored under a key that ignores it. Callers that store by path alone ask
|
|
586
|
+
// for tracking through `renderSignals` even when the route itself declares
|
|
587
|
+
// no cache policy.
|
|
588
|
+
trackedRequest =
|
|
589
|
+
mayUseRouteCache || options.renderSignals !== undefined
|
|
590
|
+
? trackRequestHeaderReads(options.request)
|
|
591
|
+
: undefined;
|
|
592
|
+
const appRequest = trackedRequest?.request ?? options.request;
|
|
577
593
|
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
578
594
|
const preparedActions = await prepareRouteServerActions({
|
|
579
595
|
appDir: options.appDir,
|
|
@@ -614,12 +630,11 @@ async function renderAppRequestInternal(options) {
|
|
|
614
630
|
? loadRouteDataWithInstrumentation({
|
|
615
631
|
appDir: options.appDir,
|
|
616
632
|
code: originalCode,
|
|
617
|
-
context: {
|
|
633
|
+
context: withTrackedRequest({
|
|
618
634
|
env: options.env,
|
|
619
635
|
params: matched.params,
|
|
620
636
|
queryClient,
|
|
621
|
-
|
|
622
|
-
},
|
|
637
|
+
}, appRequest, trackedRequest),
|
|
623
638
|
filename: matched.route.file,
|
|
624
639
|
importPolicy: options.importPolicy,
|
|
625
640
|
instrumentation: options.instrumentation,
|
|
@@ -698,12 +713,11 @@ async function renderAppRequestInternal(options) {
|
|
|
698
713
|
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
699
714
|
});
|
|
700
715
|
}
|
|
701
|
-
const renderedPage = await runWithQueryClient(queryClient, () => runServerModuleWithSlots(stringOutput.code, {
|
|
716
|
+
const renderedPage = await runWithQueryClient(queryClient, () => runServerModuleWithSlots(stringOutput.code, withTrackedRequest({
|
|
702
717
|
data,
|
|
703
718
|
params: matched.params,
|
|
704
719
|
queryClient,
|
|
705
|
-
|
|
706
|
-
}, matched.route.file, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, undefined, options.importPolicy, options.devServerModuleCacheVersion));
|
|
720
|
+
}, appRequest, trackedRequest), matched.route.file, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, undefined, options.importPolicy, options.devServerModuleCacheVersion));
|
|
707
721
|
const pageHtml = renderedPage.html;
|
|
708
722
|
const pageHtmlForLayout = clientRoute
|
|
709
723
|
? withHydrationMarkers({
|
|
@@ -728,12 +742,11 @@ async function renderAppRequestInternal(options) {
|
|
|
728
742
|
appDir: options.appDir,
|
|
729
743
|
pageFile: matched.route.file,
|
|
730
744
|
html: pageHtmlForLayout,
|
|
731
|
-
props: {
|
|
745
|
+
props: withTrackedRequest({
|
|
732
746
|
data,
|
|
733
747
|
params: matched.params,
|
|
734
748
|
queryClient,
|
|
735
|
-
|
|
736
|
-
},
|
|
749
|
+
}, appRequest, trackedRequest),
|
|
737
750
|
slots: renderedPage.slots,
|
|
738
751
|
serverModules: options.serverModules,
|
|
739
752
|
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
@@ -746,11 +759,10 @@ async function renderAppRequestInternal(options) {
|
|
|
746
759
|
const metadata = await loadComposedRouteMetadata({
|
|
747
760
|
appDir: options.appDir,
|
|
748
761
|
code: originalCode,
|
|
749
|
-
context: {
|
|
762
|
+
context: withTrackedRequest({
|
|
750
763
|
data,
|
|
751
764
|
params: matched.params,
|
|
752
|
-
|
|
753
|
-
},
|
|
765
|
+
}, appRequest, trackedRequest),
|
|
754
766
|
filename: matched.route.file,
|
|
755
767
|
importPolicy: options.importPolicy,
|
|
756
768
|
routes,
|
|
@@ -851,7 +863,8 @@ async function renderAppRequestInternal(options) {
|
|
|
851
863
|
pageFile: matched.route.file,
|
|
852
864
|
params: matched.params,
|
|
853
865
|
queryClient,
|
|
854
|
-
request:
|
|
866
|
+
request: appRequest,
|
|
867
|
+
trackedRequest,
|
|
855
868
|
routePath: matched.route.path,
|
|
856
869
|
routeScripts: options.clientScripts,
|
|
857
870
|
serverModules: options.serverModules,
|
|
@@ -871,12 +884,11 @@ async function renderAppRequestInternal(options) {
|
|
|
871
884
|
return response;
|
|
872
885
|
}
|
|
873
886
|
const data = streamData;
|
|
874
|
-
const props = {
|
|
887
|
+
const props = withTrackedRequest({
|
|
875
888
|
data,
|
|
876
889
|
params: matched.params,
|
|
877
890
|
queryClient,
|
|
878
|
-
|
|
879
|
-
};
|
|
891
|
+
}, appRequest, trackedRequest);
|
|
880
892
|
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
881
893
|
const stream = runServerStreamModule(output.code, {
|
|
882
894
|
appDir: options.appDir,
|
|
@@ -915,6 +927,12 @@ async function renderAppRequestInternal(options) {
|
|
|
915
927
|
emitRenderTiming(options, timing, data.status);
|
|
916
928
|
return data;
|
|
917
929
|
}
|
|
930
|
+
sourceUsesRequestInput ||= await routeShellsMayUseRequestInput({
|
|
931
|
+
appDir: options.appDir,
|
|
932
|
+
pageFile: matched.route.file,
|
|
933
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
934
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
935
|
+
});
|
|
918
936
|
await waitForRenderPreload(options, timing);
|
|
919
937
|
await loadServerRenderArtifacts(options, matched.route.file, timing);
|
|
920
938
|
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
@@ -935,12 +953,11 @@ async function renderAppRequestInternal(options) {
|
|
|
935
953
|
});
|
|
936
954
|
}
|
|
937
955
|
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
938
|
-
const renderedPage = await runWithQueryClient(queryClient, () => runServerModuleWithSlots(output.code, {
|
|
956
|
+
const renderedPage = await runWithQueryClient(queryClient, () => runServerModuleWithSlots(output.code, withTrackedRequest({
|
|
939
957
|
data,
|
|
940
958
|
params: matched.params,
|
|
941
959
|
queryClient,
|
|
942
|
-
|
|
943
|
-
}, matched.route.file, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, timing, options.importPolicy, options.devServerModuleCacheVersion));
|
|
960
|
+
}, appRequest, trackedRequest), matched.route.file, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, timing, options.importPolicy, options.devServerModuleCacheVersion));
|
|
944
961
|
finishRenderTimingPhase(timing, phaseStartedAt, "pageRenderMs");
|
|
945
962
|
const pageHtml = renderedPage.html;
|
|
946
963
|
// Wrap the page (not the full document) with the hydration marker so
|
|
@@ -972,12 +989,11 @@ async function renderAppRequestInternal(options) {
|
|
|
972
989
|
appDir: options.appDir,
|
|
973
990
|
pageFile: matched.route.file,
|
|
974
991
|
html: pageHtmlForLayout,
|
|
975
|
-
props: {
|
|
992
|
+
props: withTrackedRequest({
|
|
976
993
|
data,
|
|
977
994
|
params: matched.params,
|
|
978
995
|
queryClient,
|
|
979
|
-
|
|
980
|
-
},
|
|
996
|
+
}, appRequest, trackedRequest),
|
|
981
997
|
slots: renderedPage.slots,
|
|
982
998
|
serverModules: options.serverModules,
|
|
983
999
|
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
@@ -991,11 +1007,10 @@ async function renderAppRequestInternal(options) {
|
|
|
991
1007
|
const metadata = await loadComposedRouteMetadata({
|
|
992
1008
|
appDir: options.appDir,
|
|
993
1009
|
code: originalCode,
|
|
994
|
-
context: {
|
|
1010
|
+
context: withTrackedRequest({
|
|
995
1011
|
data,
|
|
996
1012
|
params: matched.params,
|
|
997
|
-
|
|
998
|
-
},
|
|
1013
|
+
}, appRequest, trackedRequest),
|
|
999
1014
|
filename: matched.route.file,
|
|
1000
1015
|
importPolicy: options.importPolicy,
|
|
1001
1016
|
routes,
|
|
@@ -1028,11 +1043,21 @@ async function renderAppRequestInternal(options) {
|
|
|
1028
1043
|
headers: responseHeadersForMetadata(metadata, options.request),
|
|
1029
1044
|
}), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
|
|
1030
1045
|
const effectiveCachePolicy = cachePolicy ?? activeRouteCacheContext()?.cachePolicy;
|
|
1046
|
+
const configuredHsts = configuredHstsHeader(metadata?.security);
|
|
1047
|
+
invalidConfiguredHsts = hasInvalidConfiguredHsts(metadata?.security);
|
|
1031
1048
|
const finalResponse = preparedActions.hasFormActions
|
|
1032
1049
|
? withRouteCacheHeader(response, effectiveCachePolicy)
|
|
1033
1050
|
: await cacheRouteResponse({
|
|
1034
1051
|
key: cacheKey,
|
|
1035
1052
|
cache: options.routeCache,
|
|
1053
|
+
// A policy can still arrive from a runtime cacheControl() call made
|
|
1054
|
+
// outside the page source, which is what `mayUseRouteCache` scans.
|
|
1055
|
+
// Without a tracker there is no evidence the render ignored request
|
|
1056
|
+
// headers, so the entry is treated as header dependent.
|
|
1057
|
+
headerDependent: sourceUsesRequestInput ||
|
|
1058
|
+
invalidConfiguredHsts ||
|
|
1059
|
+
(trackedRequest?.requestDependent() ?? true),
|
|
1060
|
+
...(configuredHsts === undefined ? {} : { strictTransportSecurity: configuredHsts }),
|
|
1036
1061
|
path: matched.route.path,
|
|
1037
1062
|
policy: effectiveCachePolicy,
|
|
1038
1063
|
request: options.request,
|
|
@@ -1103,13 +1128,31 @@ async function renderAppRequestInternal(options) {
|
|
|
1103
1128
|
emitRenderTiming(options, timing, response.status);
|
|
1104
1129
|
return response;
|
|
1105
1130
|
}
|
|
1131
|
+
finally {
|
|
1132
|
+
// Reported from every return path, including the streaming ones, so that a
|
|
1133
|
+
// caller storing by path alone never mistakes an unreported render for a
|
|
1134
|
+
// shareable one. The value stays lazy because a streamed render can still
|
|
1135
|
+
// read request headers while its deferred parts resolve; callers read it
|
|
1136
|
+
// after draining the body.
|
|
1137
|
+
if (options.renderSignals !== undefined) {
|
|
1138
|
+
const tracked = trackedRequest;
|
|
1139
|
+
options.renderSignals.headerDependent = () => sourceUsesRequestInput || invalidConfiguredHsts || (tracked?.requestDependent() ?? true);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1106
1142
|
})).value;
|
|
1107
1143
|
}
|
|
1108
1144
|
async function applyAppRouterResponseHook(response, options) {
|
|
1109
1145
|
const onResponse = await resolveAppRouterResponseHook(options);
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
}
|
|
1146
|
+
if (onResponse === undefined) {
|
|
1147
|
+
return response;
|
|
1148
|
+
}
|
|
1149
|
+
const renderSignals = options.renderSignals;
|
|
1150
|
+
const trackedRequest = renderSignals === undefined ? undefined : trackRequestHeaderReads(options.request);
|
|
1151
|
+
const hooked = await onResponse(response, withTrackedRequest({}, trackedRequest?.request ?? options.request, trackedRequest));
|
|
1152
|
+
if (renderSignals !== undefined && trackedRequest !== undefined) {
|
|
1153
|
+
const renderHeaderDependent = renderSignals.headerDependent;
|
|
1154
|
+
renderSignals.headerDependent = () => renderHeaderDependent() || trackedRequest.requestDependent();
|
|
1155
|
+
}
|
|
1113
1156
|
return hooked instanceof Response ? hooked : response;
|
|
1114
1157
|
}
|
|
1115
1158
|
export async function resolveAppRouterResponseHook(options) {
|
|
@@ -1934,6 +1977,9 @@ function routeSourceAnalysisFromArtifact(artifact) {
|
|
|
1934
1977
|
hasLoader: artifact.hasLoader,
|
|
1935
1978
|
routeCode: artifact.routeCode,
|
|
1936
1979
|
streamRoute: artifact.streamRoute,
|
|
1980
|
+
// Older built artifacts predate request-input safety analysis. Treat them
|
|
1981
|
+
// as dependent until rebuilt rather than trusting an absent safety signal.
|
|
1982
|
+
usesRequestInput: artifact.usesRequestInput ?? true,
|
|
1937
1983
|
usesRuntimeCacheControl: artifact.usesRuntimeCacheControl,
|
|
1938
1984
|
};
|
|
1939
1985
|
}
|
|
@@ -1962,11 +2008,52 @@ async function analyzeRouteSourceUncached(options) {
|
|
|
1962
2008
|
projectRoot: options.appDir,
|
|
1963
2009
|
source: options.code,
|
|
1964
2010
|
}),
|
|
2011
|
+
usesRequestInput: routeClosureMayUseRequestInput({
|
|
2012
|
+
filename: options.filename,
|
|
2013
|
+
files: routeSourceFilesForAnalysis(options),
|
|
2014
|
+
projectRoot: options.appDir,
|
|
2015
|
+
source: options.code,
|
|
2016
|
+
}),
|
|
1965
2017
|
usesRuntimeCacheControl: usesRuntimeCacheControl(options.code),
|
|
1966
2018
|
};
|
|
1967
2019
|
}
|
|
1968
2020
|
function routeSourceFilesForAnalysis(options) {
|
|
1969
|
-
return (file) =>
|
|
2021
|
+
return (file) => {
|
|
2022
|
+
const known = file === options.filename ? options.code : options.serverSourceFiles?.get(file);
|
|
2023
|
+
if (known !== undefined) {
|
|
2024
|
+
return known;
|
|
2025
|
+
}
|
|
2026
|
+
const relativeFile = relative(resolve(options.appDir), resolve(file));
|
|
2027
|
+
if (relativeFile === "" || relativeFile === ".." || relativeFile.startsWith(`..${sep}`)) {
|
|
2028
|
+
return undefined;
|
|
2029
|
+
}
|
|
2030
|
+
try {
|
|
2031
|
+
return readFileSync(file, "utf8");
|
|
2032
|
+
}
|
|
2033
|
+
catch {
|
|
2034
|
+
return undefined;
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
async function routeShellsMayUseRequestInput(options) {
|
|
2039
|
+
const shells = await shellFilesForPage(options.appDir, options.pageFile, options.serverModuleCacheVersion);
|
|
2040
|
+
for (const shell of shells) {
|
|
2041
|
+
const source = await readServerSourceFile(shell.file, options.serverModuleCacheVersion, options.serverSourceFiles);
|
|
2042
|
+
if (routeClosureMayUseRequestInput({
|
|
2043
|
+
filename: shell.file,
|
|
2044
|
+
files: routeSourceFilesForAnalysis({
|
|
2045
|
+
appDir: options.appDir,
|
|
2046
|
+
code: source,
|
|
2047
|
+
filename: shell.file,
|
|
2048
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
2049
|
+
}),
|
|
2050
|
+
projectRoot: options.appDir,
|
|
2051
|
+
source,
|
|
2052
|
+
})) {
|
|
2053
|
+
return true;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return false;
|
|
1970
2057
|
}
|
|
1971
2058
|
async function runServerModule(code, props, sourcefile, serverModules, serverModuleCacheVersion, define, vitePlugins) {
|
|
1972
2059
|
const component = await loadServerComponent(code, sourcefile, serverModules, serverModuleCacheVersion, define, vitePlugins);
|
|
@@ -2215,12 +2302,11 @@ function localServerSourceImportCandidates(base) {
|
|
|
2215
2302
|
return candidates;
|
|
2216
2303
|
}
|
|
2217
2304
|
async function runServerStreamModuleWithLoading(code, options) {
|
|
2218
|
-
const loadingProps = {
|
|
2305
|
+
const loadingProps = withTrackedRequest({
|
|
2219
2306
|
data: undefined,
|
|
2220
2307
|
params: options.params,
|
|
2221
2308
|
queryClient: options.queryClient,
|
|
2222
|
-
|
|
2223
|
-
};
|
|
2309
|
+
}, options.request, options.trackedRequest);
|
|
2224
2310
|
const layoutShells = await layoutShellsForPage(options.appDir, options.pageFile, loadingProps, {}, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles, options.clientRouteInferenceCache, options.define, options.vitePlugins, options.importPolicy);
|
|
2225
2311
|
const loadingHtml = await renderServerFileToHtml(options.loadingFile, loadingProps, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles, options.define, options.vitePlugins);
|
|
2226
2312
|
const marker = options.clientRoute
|
|
@@ -2247,12 +2333,11 @@ async function runServerStreamModuleWithLoading(code, options) {
|
|
|
2247
2333
|
}
|
|
2248
2334
|
sink.append(marker?.prefix ?? "");
|
|
2249
2335
|
renderVisibleOutOfOrderBoundary(sink, "mreact-route", options.data, async (boundarySink, data) => {
|
|
2250
|
-
await appendServerStreamModule(code, boundarySink, {
|
|
2336
|
+
await appendServerStreamModule(code, boundarySink, withTrackedRequest({
|
|
2251
2337
|
data,
|
|
2252
2338
|
params: options.params,
|
|
2253
2339
|
queryClient: options.queryClient,
|
|
2254
|
-
|
|
2255
|
-
}, options.pageFile, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, options.importPolicy);
|
|
2340
|
+
}, options.request, options.trackedRequest), options.pageFile, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, options.importPolicy);
|
|
2256
2341
|
}, {
|
|
2257
2342
|
placeholder(boundarySink) {
|
|
2258
2343
|
boundarySink.append(loadingHtml);
|
|
@@ -2441,12 +2526,6 @@ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, server
|
|
|
2441
2526
|
const cacheKey = serverModuleCacheVersion === undefined || hasNamedSlots || shell.kind === "template"
|
|
2442
2527
|
? undefined
|
|
2443
2528
|
: `${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
2529
|
const staticEntry = cacheKey === undefined ? undefined : shellStaticRenderCache.get(cacheKey);
|
|
2451
2530
|
const loadedStaticEntry = staticEntry ??
|
|
2452
2531
|
(await loadShellStaticRenderEntry({
|
|
@@ -2472,33 +2551,6 @@ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, server
|
|
|
2472
2551
|
hasOutOfOrderBoundary: loadedStaticEntry.hasOutOfOrderBoundary,
|
|
2473
2552
|
};
|
|
2474
2553
|
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
2554
|
return rendered;
|
|
2503
2555
|
}
|
|
2504
2556
|
async function loadShellStaticRenderEntry(options) {
|