@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/src/render.ts
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";
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
installQueryAsyncStorage,
|
|
16
17
|
runWithQueryClient,
|
|
17
18
|
type DehydratedQueryClient,
|
|
19
|
+
type DehydrateOptions,
|
|
18
20
|
type QueryClient,
|
|
19
21
|
} from "@reckona/mreact-query";
|
|
20
22
|
import {
|
|
@@ -34,9 +36,9 @@ import {
|
|
|
34
36
|
} from "./client-route-inference.js";
|
|
35
37
|
import {
|
|
36
38
|
hydrationMarkerParts,
|
|
39
|
+
routeMarkerParts,
|
|
37
40
|
routeIdForPath,
|
|
38
41
|
withHydrationMarkers,
|
|
39
|
-
withRouteMarkers,
|
|
40
42
|
} from "./navigation-runtime.js";
|
|
41
43
|
import { assetPath } from "./assets.js";
|
|
42
44
|
import { escapeHtmlAttribute } from "@reckona/mreact-shared/html-escape";
|
|
@@ -60,6 +62,15 @@ import {
|
|
|
60
62
|
routeCacheKey,
|
|
61
63
|
routeCachePolicyFromSource,
|
|
62
64
|
} from "./cache.js";
|
|
65
|
+
import {
|
|
66
|
+
trackRequestHeaderReads,
|
|
67
|
+
type TrackedHeaderRequest,
|
|
68
|
+
withTrackedRequest,
|
|
69
|
+
} from "./request-header-tracking.js";
|
|
70
|
+
import {
|
|
71
|
+
configuredHstsHeader,
|
|
72
|
+
hasInvalidConfiguredHsts,
|
|
73
|
+
} from "./security-headers.js";
|
|
63
74
|
import { resolveRouterCacheLimit } from "./cache-config.js";
|
|
64
75
|
import {
|
|
65
76
|
importAppRouterBuiltFileModule,
|
|
@@ -78,6 +89,7 @@ import {
|
|
|
78
89
|
hasLoaderExport,
|
|
79
90
|
isStreamRouteSource,
|
|
80
91
|
routeClosureMayUseAwaitBoundary,
|
|
92
|
+
routeClosureMayUseRequestInput,
|
|
81
93
|
stripRouteBuildExports,
|
|
82
94
|
stripRouteModuleExports,
|
|
83
95
|
stripRouteClientOnlyExports,
|
|
@@ -191,6 +203,7 @@ export interface RenderAppRequestOptions {
|
|
|
191
203
|
clientStylesByFile?: ReadonlyMap<string, readonly string[]>;
|
|
192
204
|
clientStyles?: ReadonlyMap<string, readonly string[]>;
|
|
193
205
|
define?: UserConfig["define"] | undefined;
|
|
206
|
+
dehydrateOptions?: DehydrateOptions | undefined;
|
|
194
207
|
env?: unknown;
|
|
195
208
|
importPolicy?: AppRouterImportPolicy | undefined;
|
|
196
209
|
instrumentation?: RouterInstrumentation | undefined;
|
|
@@ -222,6 +235,22 @@ export interface RenderAppRequestOptions {
|
|
|
222
235
|
}
|
|
223
236
|
|
|
224
237
|
export interface RenderAppRequestRuntimeOptions extends RenderAppRequestOptions {
|
|
238
|
+
/**
|
|
239
|
+
* Receives whether the render depended on the incoming request headers.
|
|
240
|
+
*
|
|
241
|
+
* Callers that store the rendered HTML under a key which ignores headers,
|
|
242
|
+
* such as prerender regeneration, pass this to learn whether the result is
|
|
243
|
+
* only correct for the current visitor. Initialize it to a function returning
|
|
244
|
+
* `true`: a render that returns without reporting back stays header
|
|
245
|
+
* dependent. Call it only after draining the response body, because a
|
|
246
|
+
* streamed render can read headers while its deferred parts resolve.
|
|
247
|
+
*/
|
|
248
|
+
renderSignals?:
|
|
249
|
+
| {
|
|
250
|
+
headerDependent: () => boolean;
|
|
251
|
+
strictTransportSecurity: () => string | undefined;
|
|
252
|
+
}
|
|
253
|
+
| undefined;
|
|
225
254
|
serverRenderArtifactLoader?: AppRouterServerRenderArtifactLoader | undefined;
|
|
226
255
|
}
|
|
227
256
|
|
|
@@ -617,30 +646,18 @@ export function routerRenderRuntimeCacheStats(): RouterRuntimeCacheStat[] {
|
|
|
617
646
|
maxComposedRouteMetadataCacheEntries,
|
|
618
647
|
composedRouteMetadataCacheCounters,
|
|
619
648
|
),
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
649
|
+
// Preserve the public diagnostics entry while making explicit that rendered HTML reuse is disabled.
|
|
650
|
+
{
|
|
651
|
+
evictions: 0,
|
|
652
|
+
hits: 0,
|
|
653
|
+
maxEntries: 0,
|
|
654
|
+
misses: 0,
|
|
655
|
+
name: "rendered-shell",
|
|
656
|
+
size: 0,
|
|
657
|
+
},
|
|
626
658
|
];
|
|
627
659
|
}
|
|
628
660
|
|
|
629
|
-
// Issue 086: per-shell prefix/suffix cache. Pure layouts (whose
|
|
630
|
-
// exported component takes zero arguments and therefore cannot
|
|
631
|
-
// depend on the request props) produce the same HTML for every
|
|
632
|
-
// request, so we cache the already-split { prefix, suffix } strings
|
|
633
|
-
// keyed by appDir + shellFile + serverModuleCacheVersion. Impure
|
|
634
|
-
// layouts (function.length > 0) are tagged "impure" so we skip the
|
|
635
|
-
// detection on subsequent requests but still render per-request.
|
|
636
|
-
//
|
|
637
|
-
// The cache is only active when a version is present (production
|
|
638
|
-
// builds); dev mode keeps the previous behaviour so reloads pick up
|
|
639
|
-
// edits without server restart.
|
|
640
|
-
const renderedShellCache = new Map<string, RenderedShell | "impure">();
|
|
641
|
-
const MAX_RENDERED_SHELL_CACHE_ENTRIES = 1024;
|
|
642
|
-
const renderedShellCacheCounters = createRouterRuntimeCacheCounters();
|
|
643
|
-
|
|
644
661
|
interface RenderedShell {
|
|
645
662
|
hasOutOfOrderBoundary: boolean;
|
|
646
663
|
prefix: string;
|
|
@@ -654,6 +671,7 @@ interface RouteSourceAnalysis {
|
|
|
654
671
|
hasLoader: boolean;
|
|
655
672
|
routeCode: string;
|
|
656
673
|
streamRoute: boolean;
|
|
674
|
+
usesRequestInput: boolean;
|
|
657
675
|
usesRuntimeCacheControl: boolean;
|
|
658
676
|
}
|
|
659
677
|
|
|
@@ -932,6 +950,9 @@ async function renderAppRequestInternal(
|
|
|
932
950
|
renderAppRequestInternal({
|
|
933
951
|
...options,
|
|
934
952
|
matchedRoute: undefined,
|
|
953
|
+
// This renders a different route than the caller asked about, so its
|
|
954
|
+
// header dependence says nothing about the caller's request.
|
|
955
|
+
renderSignals: undefined,
|
|
935
956
|
request: singleFlightNavigationRequest({
|
|
936
957
|
path: singleFlight.path,
|
|
937
958
|
request: singleFlight.request,
|
|
@@ -983,6 +1004,10 @@ async function renderAppRequestInternal(
|
|
|
983
1004
|
script: string | undefined;
|
|
984
1005
|
}
|
|
985
1006
|
| undefined;
|
|
1007
|
+
let trackedRequest: TrackedHeaderRequest | undefined;
|
|
1008
|
+
let sourceUsesRequestInput = true;
|
|
1009
|
+
let invalidConfiguredHsts = false;
|
|
1010
|
+
let renderedStrictTransportSecurity: string | undefined;
|
|
986
1011
|
return (await withRouteCacheContext(options.routeCache, async () => {
|
|
987
1012
|
try {
|
|
988
1013
|
if (matched.route.kind === "asset") {
|
|
@@ -1055,14 +1080,22 @@ async function renderAppRequestInternal(
|
|
|
1055
1080
|
timing,
|
|
1056
1081
|
vitePlugins: options.vitePlugins,
|
|
1057
1082
|
});
|
|
1083
|
+
sourceUsesRequestInput = originalAnalysis.usesRequestInput;
|
|
1058
1084
|
finishRenderTimingPhase(timing, phaseStartedAt, "sourceAnalysisMs");
|
|
1059
1085
|
const cachePolicy = originalAnalysis.cachePolicy;
|
|
1060
1086
|
const navigationScript = options.navigationScripts?.get(matched.route.path);
|
|
1061
|
-
const
|
|
1087
|
+
const navigationRequest = isNavigationRequest(options.request);
|
|
1088
|
+
const cacheKey = routeCacheKey(
|
|
1089
|
+
options.appDir,
|
|
1090
|
+
matched.route.path,
|
|
1091
|
+
url,
|
|
1092
|
+
navigationRequest ? "navigation" : "document",
|
|
1093
|
+
);
|
|
1062
1094
|
const mayUseRouteCache =
|
|
1063
|
-
|
|
1095
|
+
!sourceUsesRequestInput &&
|
|
1096
|
+
(cachePolicy === undefined
|
|
1064
1097
|
? originalAnalysis.usesRuntimeCacheControl
|
|
1065
|
-
: cachePolicy.revalidateSeconds !== 0;
|
|
1098
|
+
: cachePolicy.revalidateSeconds !== 0);
|
|
1066
1099
|
const reloadRouteCache = isNavigationRouteCacheReloadRequest(options.request);
|
|
1067
1100
|
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
1068
1101
|
const cachedResponse =
|
|
@@ -1079,6 +1112,18 @@ async function renderAppRequestInternal(
|
|
|
1079
1112
|
return cachedResponse;
|
|
1080
1113
|
}
|
|
1081
1114
|
|
|
1115
|
+
// Only a route whose result may be stored pays for header-read tracking;
|
|
1116
|
+
// cache hits returned above never construct it. Application code reads
|
|
1117
|
+
// headers through this request so that a render depending on one is never
|
|
1118
|
+
// stored under a key that ignores it. Callers that store by path alone ask
|
|
1119
|
+
// for tracking through `renderSignals` even when the route itself declares
|
|
1120
|
+
// no cache policy.
|
|
1121
|
+
trackedRequest =
|
|
1122
|
+
mayUseRouteCache || options.renderSignals !== undefined
|
|
1123
|
+
? trackRequestHeaderReads(options.request)
|
|
1124
|
+
: undefined;
|
|
1125
|
+
const appRequest = trackedRequest?.request ?? options.request;
|
|
1126
|
+
|
|
1082
1127
|
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
1083
1128
|
const preparedActions = await prepareRouteServerActions({
|
|
1084
1129
|
appDir: options.appDir,
|
|
@@ -1120,12 +1165,11 @@ async function renderAppRequestInternal(
|
|
|
1120
1165
|
? loadRouteDataWithInstrumentation({
|
|
1121
1166
|
appDir: options.appDir,
|
|
1122
1167
|
code: originalCode,
|
|
1123
|
-
context: {
|
|
1168
|
+
context: withTrackedRequest({
|
|
1124
1169
|
env: options.env,
|
|
1125
1170
|
params: matched.params,
|
|
1126
1171
|
queryClient,
|
|
1127
|
-
|
|
1128
|
-
},
|
|
1172
|
+
}, appRequest, trackedRequest),
|
|
1129
1173
|
filename: matched.route.file,
|
|
1130
1174
|
importPolicy: options.importPolicy,
|
|
1131
1175
|
instrumentation: options.instrumentation,
|
|
@@ -1146,7 +1190,7 @@ async function renderAppRequestInternal(
|
|
|
1146
1190
|
clientRoute,
|
|
1147
1191
|
props: {
|
|
1148
1192
|
params: matched.params,
|
|
1149
|
-
request: { url:
|
|
1193
|
+
request: { url: url.pathname },
|
|
1150
1194
|
},
|
|
1151
1195
|
routePath: matched.route.path,
|
|
1152
1196
|
script: clientScript,
|
|
@@ -1210,12 +1254,11 @@ async function renderAppRequestInternal(
|
|
|
1210
1254
|
const renderedPage = await runWithQueryClient(queryClient, () =>
|
|
1211
1255
|
runServerModuleWithSlots(
|
|
1212
1256
|
stringOutput.code,
|
|
1213
|
-
{
|
|
1257
|
+
withTrackedRequest({
|
|
1214
1258
|
data,
|
|
1215
1259
|
params: matched.params,
|
|
1216
1260
|
queryClient,
|
|
1217
|
-
|
|
1218
|
-
},
|
|
1261
|
+
}, appRequest, trackedRequest),
|
|
1219
1262
|
matched.route.file,
|
|
1220
1263
|
options.serverModules,
|
|
1221
1264
|
options.serverModuleCacheVersion,
|
|
@@ -1236,27 +1279,21 @@ async function renderAppRequestInternal(
|
|
|
1236
1279
|
script: clientScript,
|
|
1237
1280
|
props: {
|
|
1238
1281
|
params: matched.params,
|
|
1239
|
-
request: { url:
|
|
1282
|
+
request: { url: url.pathname },
|
|
1240
1283
|
data,
|
|
1241
1284
|
},
|
|
1242
1285
|
})
|
|
1243
|
-
:
|
|
1244
|
-
? withRouteMarkers({
|
|
1245
|
-
html: pageHtml,
|
|
1246
|
-
routePath: matched.route.path,
|
|
1247
|
-
})
|
|
1248
|
-
: pageHtml;
|
|
1286
|
+
: withServerOnlyRouteMarkers(pageHtml, matched.route.path, options.request);
|
|
1249
1287
|
let html = await runWithQueryClient(queryClient, () =>
|
|
1250
1288
|
applyLayouts({
|
|
1251
1289
|
appDir: options.appDir,
|
|
1252
1290
|
pageFile: matched.route.file,
|
|
1253
1291
|
html: pageHtmlForLayout,
|
|
1254
|
-
props: {
|
|
1292
|
+
props: withTrackedRequest({
|
|
1255
1293
|
data,
|
|
1256
1294
|
params: matched.params,
|
|
1257
1295
|
queryClient,
|
|
1258
|
-
|
|
1259
|
-
},
|
|
1296
|
+
}, appRequest, trackedRequest),
|
|
1260
1297
|
slots: renderedPage.slots,
|
|
1261
1298
|
serverModules: options.serverModules,
|
|
1262
1299
|
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
@@ -1270,11 +1307,10 @@ async function renderAppRequestInternal(
|
|
|
1270
1307
|
const metadata = await loadComposedRouteMetadata({
|
|
1271
1308
|
appDir: options.appDir,
|
|
1272
1309
|
code: originalCode,
|
|
1273
|
-
context: {
|
|
1310
|
+
context: withTrackedRequest({
|
|
1274
1311
|
data,
|
|
1275
1312
|
params: matched.params,
|
|
1276
|
-
|
|
1277
|
-
},
|
|
1313
|
+
}, appRequest, trackedRequest),
|
|
1278
1314
|
filename: matched.route.file,
|
|
1279
1315
|
importPolicy: options.importPolicy,
|
|
1280
1316
|
routes,
|
|
@@ -1297,7 +1333,7 @@ async function renderAppRequestInternal(
|
|
|
1297
1333
|
html,
|
|
1298
1334
|
originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined,
|
|
1299
1335
|
);
|
|
1300
|
-
html = injectQueryState(html, dehydrate(queryClient));
|
|
1336
|
+
html = injectQueryState(html, dehydrate(queryClient, options.dehydrateOptions));
|
|
1301
1337
|
const response = withOptionalActionCookie(
|
|
1302
1338
|
htmlResponse(
|
|
1303
1339
|
applyActionHtmlReplacements(
|
|
@@ -1319,6 +1355,7 @@ async function renderAppRequestInternal(
|
|
|
1319
1355
|
preparedActions.csrfToken,
|
|
1320
1356
|
preparedActions.csrfTokenIsNew === true,
|
|
1321
1357
|
);
|
|
1358
|
+
appendNavigationVary(response.headers);
|
|
1322
1359
|
emitRenderTiming(options, timing, response.status);
|
|
1323
1360
|
return response;
|
|
1324
1361
|
}
|
|
@@ -1385,10 +1422,13 @@ async function renderAppRequestInternal(
|
|
|
1385
1422
|
data: streamDataPromise,
|
|
1386
1423
|
define: options.define,
|
|
1387
1424
|
loadingFile,
|
|
1425
|
+
markerRequest: options.request,
|
|
1388
1426
|
pageFile: matched.route.file,
|
|
1389
1427
|
params: matched.params,
|
|
1390
1428
|
queryClient,
|
|
1391
|
-
request:
|
|
1429
|
+
request: appRequest,
|
|
1430
|
+
requestUrl: url.pathname,
|
|
1431
|
+
trackedRequest,
|
|
1392
1432
|
routePath: matched.route.path,
|
|
1393
1433
|
routeScripts: options.clientScripts,
|
|
1394
1434
|
serverModules: options.serverModules,
|
|
@@ -1409,23 +1449,25 @@ async function renderAppRequestInternal(
|
|
|
1409
1449
|
preparedActions.csrfToken,
|
|
1410
1450
|
preparedActions.csrfTokenIsNew === true,
|
|
1411
1451
|
);
|
|
1452
|
+
appendNavigationVary(response.headers);
|
|
1412
1453
|
emitRenderTiming(options, timing, response.status);
|
|
1413
1454
|
return response;
|
|
1414
1455
|
}
|
|
1415
1456
|
|
|
1416
1457
|
const data = streamData;
|
|
1417
|
-
const props = {
|
|
1458
|
+
const props = withTrackedRequest({
|
|
1418
1459
|
data,
|
|
1419
1460
|
params: matched.params,
|
|
1420
1461
|
queryClient,
|
|
1421
|
-
|
|
1422
|
-
};
|
|
1462
|
+
}, appRequest, trackedRequest);
|
|
1423
1463
|
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
1424
1464
|
const stream = runServerStreamModule(output.code, {
|
|
1425
1465
|
appDir: options.appDir,
|
|
1426
1466
|
assetBaseUrl: options.assetBaseUrl,
|
|
1427
1467
|
pageFile: matched.route.file,
|
|
1468
|
+
markerRequest: options.request,
|
|
1428
1469
|
props,
|
|
1470
|
+
requestUrl: url.pathname,
|
|
1429
1471
|
routePath: matched.route.path,
|
|
1430
1472
|
routeScripts: options.clientScripts,
|
|
1431
1473
|
serverModules: options.serverModules,
|
|
@@ -1448,6 +1490,7 @@ async function renderAppRequestInternal(
|
|
|
1448
1490
|
preparedActions.csrfToken,
|
|
1449
1491
|
preparedActions.csrfTokenIsNew === true,
|
|
1450
1492
|
);
|
|
1493
|
+
appendNavigationVary(response.headers);
|
|
1451
1494
|
emitRenderTiming(options, timing, response.status);
|
|
1452
1495
|
return response;
|
|
1453
1496
|
}
|
|
@@ -1463,6 +1506,12 @@ async function renderAppRequestInternal(
|
|
|
1463
1506
|
emitRenderTiming(options, timing, data.status);
|
|
1464
1507
|
return data;
|
|
1465
1508
|
}
|
|
1509
|
+
sourceUsesRequestInput ||= await routeShellsMayUseRequestInput({
|
|
1510
|
+
appDir: options.appDir,
|
|
1511
|
+
pageFile: matched.route.file,
|
|
1512
|
+
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
1513
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
1514
|
+
});
|
|
1466
1515
|
await waitForRenderPreload(options, timing);
|
|
1467
1516
|
await loadServerRenderArtifacts(options, matched.route.file, timing);
|
|
1468
1517
|
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
@@ -1488,12 +1537,11 @@ async function renderAppRequestInternal(
|
|
|
1488
1537
|
const renderedPage = await runWithQueryClient(queryClient, () =>
|
|
1489
1538
|
runServerModuleWithSlots(
|
|
1490
1539
|
output.code,
|
|
1491
|
-
{
|
|
1540
|
+
withTrackedRequest({
|
|
1492
1541
|
data,
|
|
1493
1542
|
params: matched.params,
|
|
1494
1543
|
queryClient,
|
|
1495
|
-
|
|
1496
|
-
},
|
|
1544
|
+
}, appRequest, trackedRequest),
|
|
1497
1545
|
matched.route.file,
|
|
1498
1546
|
options.serverModules,
|
|
1499
1547
|
options.serverModuleCacheVersion,
|
|
@@ -1520,28 +1568,22 @@ async function renderAppRequestInternal(
|
|
|
1520
1568
|
script: clientScript,
|
|
1521
1569
|
props: {
|
|
1522
1570
|
params: matched.params,
|
|
1523
|
-
request: { url:
|
|
1571
|
+
request: { url: url.pathname },
|
|
1524
1572
|
data,
|
|
1525
1573
|
},
|
|
1526
1574
|
})
|
|
1527
|
-
:
|
|
1528
|
-
? withRouteMarkers({
|
|
1529
|
-
html: pageHtml,
|
|
1530
|
-
routePath: matched.route.path,
|
|
1531
|
-
})
|
|
1532
|
-
: pageHtml;
|
|
1575
|
+
: withServerOnlyRouteMarkers(pageHtml, matched.route.path, options.request);
|
|
1533
1576
|
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
|
1534
1577
|
let html = await runWithQueryClient(queryClient, () =>
|
|
1535
1578
|
applyLayouts({
|
|
1536
1579
|
appDir: options.appDir,
|
|
1537
1580
|
pageFile: matched.route.file,
|
|
1538
1581
|
html: pageHtmlForLayout,
|
|
1539
|
-
props: {
|
|
1582
|
+
props: withTrackedRequest({
|
|
1540
1583
|
data,
|
|
1541
1584
|
params: matched.params,
|
|
1542
1585
|
queryClient,
|
|
1543
|
-
|
|
1544
|
-
},
|
|
1586
|
+
}, appRequest, trackedRequest),
|
|
1545
1587
|
slots: renderedPage.slots,
|
|
1546
1588
|
serverModules: options.serverModules,
|
|
1547
1589
|
serverModuleCacheVersion: options.serverModuleCacheVersion,
|
|
@@ -1556,11 +1598,10 @@ async function renderAppRequestInternal(
|
|
|
1556
1598
|
const metadata = await loadComposedRouteMetadata({
|
|
1557
1599
|
appDir: options.appDir,
|
|
1558
1600
|
code: originalCode,
|
|
1559
|
-
context: {
|
|
1601
|
+
context: withTrackedRequest({
|
|
1560
1602
|
data,
|
|
1561
1603
|
params: matched.params,
|
|
1562
|
-
|
|
1563
|
-
},
|
|
1604
|
+
}, appRequest, trackedRequest),
|
|
1564
1605
|
filename: matched.route.file,
|
|
1565
1606
|
importPolicy: options.importPolicy,
|
|
1566
1607
|
routes,
|
|
@@ -1585,7 +1626,7 @@ async function renderAppRequestInternal(
|
|
|
1585
1626
|
html,
|
|
1586
1627
|
originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined,
|
|
1587
1628
|
);
|
|
1588
|
-
html = injectQueryState(html, dehydrate(queryClient));
|
|
1629
|
+
html = injectQueryState(html, dehydrate(queryClient, options.dehydrateOptions));
|
|
1589
1630
|
|
|
1590
1631
|
const response = withOptionalActionCookie(
|
|
1591
1632
|
htmlResponse(
|
|
@@ -1606,14 +1647,27 @@ async function renderAppRequestInternal(
|
|
|
1606
1647
|
preparedActions.csrfToken,
|
|
1607
1648
|
preparedActions.csrfTokenIsNew === true,
|
|
1608
1649
|
);
|
|
1650
|
+
appendNavigationVary(response.headers);
|
|
1609
1651
|
|
|
1610
1652
|
const effectiveCachePolicy = cachePolicy ?? activeRouteCacheContext()?.cachePolicy;
|
|
1653
|
+
const configuredHsts = configuredHstsHeader(metadata?.security);
|
|
1654
|
+
renderedStrictTransportSecurity = configuredHsts;
|
|
1655
|
+
invalidConfiguredHsts = hasInvalidConfiguredHsts(metadata?.security);
|
|
1611
1656
|
|
|
1612
1657
|
const finalResponse = preparedActions.hasFormActions
|
|
1613
1658
|
? withRouteCacheHeader(response, effectiveCachePolicy)
|
|
1614
1659
|
: await cacheRouteResponse({
|
|
1615
1660
|
key: cacheKey,
|
|
1616
1661
|
cache: options.routeCache,
|
|
1662
|
+
// A policy can still arrive from a runtime cacheControl() call made
|
|
1663
|
+
// outside the page source, which is what `mayUseRouteCache` scans.
|
|
1664
|
+
// Without a tracker there is no evidence the render ignored request
|
|
1665
|
+
// headers, so the entry is treated as header dependent.
|
|
1666
|
+
headerDependent:
|
|
1667
|
+
sourceUsesRequestInput ||
|
|
1668
|
+
invalidConfiguredHsts ||
|
|
1669
|
+
(trackedRequest?.requestDependent() ?? true),
|
|
1670
|
+
...(configuredHsts === undefined ? {} : { strictTransportSecurity: configuredHsts }),
|
|
1617
1671
|
path: matched.route.path,
|
|
1618
1672
|
policy: effectiveCachePolicy,
|
|
1619
1673
|
request: options.request,
|
|
@@ -1686,6 +1740,18 @@ async function renderAppRequestInternal(
|
|
|
1686
1740
|
});
|
|
1687
1741
|
emitRenderTiming(options, timing, response.status);
|
|
1688
1742
|
return response;
|
|
1743
|
+
} finally {
|
|
1744
|
+
// Reported from every return path, including the streaming ones, so that a
|
|
1745
|
+
// caller storing by path alone never mistakes an unreported render for a
|
|
1746
|
+
// shareable one. The value stays lazy because a streamed render can still
|
|
1747
|
+
// read request headers while its deferred parts resolve; callers read it
|
|
1748
|
+
// after draining the body.
|
|
1749
|
+
if (options.renderSignals !== undefined) {
|
|
1750
|
+
const tracked = trackedRequest;
|
|
1751
|
+
options.renderSignals.headerDependent = () =>
|
|
1752
|
+
sourceUsesRequestInput || invalidConfiguredHsts || (tracked?.requestDependent() ?? true);
|
|
1753
|
+
options.renderSignals.strictTransportSecurity = () => renderedStrictTransportSecurity;
|
|
1754
|
+
}
|
|
1689
1755
|
}
|
|
1690
1756
|
})).value;
|
|
1691
1757
|
}
|
|
@@ -1695,9 +1761,24 @@ async function applyAppRouterResponseHook(
|
|
|
1695
1761
|
options: RenderAppRequestOptions,
|
|
1696
1762
|
): Promise<Response> {
|
|
1697
1763
|
const onResponse = await resolveAppRouterResponseHook(options);
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1764
|
+
|
|
1765
|
+
if (onResponse === undefined) {
|
|
1766
|
+
return response;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
const renderSignals = (options as RenderAppRequestRuntimeOptions).renderSignals;
|
|
1770
|
+
const trackedRequest =
|
|
1771
|
+
renderSignals === undefined ? undefined : trackRequestHeaderReads(options.request);
|
|
1772
|
+
const hooked = await onResponse(
|
|
1773
|
+
response,
|
|
1774
|
+
withTrackedRequest({}, trackedRequest?.request ?? options.request, trackedRequest),
|
|
1775
|
+
);
|
|
1776
|
+
|
|
1777
|
+
if (renderSignals !== undefined && trackedRequest !== undefined) {
|
|
1778
|
+
const renderHeaderDependent = renderSignals.headerDependent;
|
|
1779
|
+
renderSignals.headerDependent = () =>
|
|
1780
|
+
renderHeaderDependent() || trackedRequest.requestDependent();
|
|
1781
|
+
}
|
|
1701
1782
|
|
|
1702
1783
|
return hooked instanceof Response ? hooked : response;
|
|
1703
1784
|
}
|
|
@@ -1899,6 +1980,44 @@ function isNavigationRequest(request: Request): boolean {
|
|
|
1899
1980
|
return request.headers.get("x-mreact-navigation") === "1";
|
|
1900
1981
|
}
|
|
1901
1982
|
|
|
1983
|
+
function withServerOnlyRouteMarkers(html: string, routePath: string, request: Request): string {
|
|
1984
|
+
const marker = serverOnlyRouteMarkerParts(routePath, request);
|
|
1985
|
+
return marker === undefined ? html : `${marker.prefix}${html}${marker.suffix}`;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function serverOnlyRouteMarkerParts(
|
|
1989
|
+
routePath: string,
|
|
1990
|
+
request: Request,
|
|
1991
|
+
): { prefix: string; suffix: string } | undefined {
|
|
1992
|
+
if (request.headers.get("x-mreact-prerender-variant-capture") === "1") {
|
|
1993
|
+
return prerenderVariantMarkerParts(routePath);
|
|
1994
|
+
}
|
|
1995
|
+
return isNavigationRequest(request) ? routeMarkerParts(routePath) : undefined;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
export function prerenderVariantMarkerParts(routePath: string): {
|
|
1999
|
+
prefix: string;
|
|
2000
|
+
suffix: string;
|
|
2001
|
+
} {
|
|
2002
|
+
const routeId = routeIdForPath(routePath);
|
|
2003
|
+
return {
|
|
2004
|
+
prefix: `<!--mreact-route-variant-start:${routeId}-->`,
|
|
2005
|
+
suffix: `<!--mreact-route-variant-end:${routeId}-->`,
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function appendNavigationVary(headers: Headers): void {
|
|
2010
|
+
const vary = headers.get("vary");
|
|
2011
|
+
if (
|
|
2012
|
+
vary
|
|
2013
|
+
?.split(",")
|
|
2014
|
+
.some((value) => value.trim().toLowerCase() === "x-mreact-navigation") === true
|
|
2015
|
+
) {
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
headers.append("vary", "x-mreact-navigation");
|
|
2019
|
+
}
|
|
2020
|
+
|
|
1902
2021
|
function isNavigationRouteCacheReloadRequest(request: Request): boolean {
|
|
1903
2022
|
return (
|
|
1904
2023
|
isNavigationRequest(request) && request.headers.get("x-mreact-navigation-cache") === "reload"
|
|
@@ -3010,6 +3129,9 @@ function routeSourceAnalysisFromArtifact(
|
|
|
3010
3129
|
hasLoader: artifact.hasLoader,
|
|
3011
3130
|
routeCode: artifact.routeCode,
|
|
3012
3131
|
streamRoute: artifact.streamRoute,
|
|
3132
|
+
// Older built artifacts predate request-input safety analysis. Treat them
|
|
3133
|
+
// as dependent until rebuilt rather than trusting an absent safety signal.
|
|
3134
|
+
usesRequestInput: artifact.usesRequestInput ?? true,
|
|
3013
3135
|
usesRuntimeCacheControl: artifact.usesRuntimeCacheControl,
|
|
3014
3136
|
};
|
|
3015
3137
|
}
|
|
@@ -3052,17 +3174,77 @@ async function analyzeRouteSourceUncached(options: {
|
|
|
3052
3174
|
projectRoot: options.appDir,
|
|
3053
3175
|
source: options.code,
|
|
3054
3176
|
}),
|
|
3177
|
+
usesRequestInput: routeClosureMayUseRequestInput({
|
|
3178
|
+
filename: options.filename,
|
|
3179
|
+
files: routeSourceFilesForAnalysis(options),
|
|
3180
|
+
projectRoot: options.appDir,
|
|
3181
|
+
source: options.code,
|
|
3182
|
+
}),
|
|
3055
3183
|
usesRuntimeCacheControl: usesRuntimeCacheControl(options.code),
|
|
3056
3184
|
};
|
|
3057
3185
|
}
|
|
3058
3186
|
|
|
3059
3187
|
function routeSourceFilesForAnalysis(options: {
|
|
3188
|
+
appDir: string;
|
|
3060
3189
|
code: string;
|
|
3061
3190
|
filename: string;
|
|
3062
3191
|
serverSourceFiles?: ReadonlyMap<string, string> | undefined;
|
|
3063
3192
|
}): (file: string) => string | undefined {
|
|
3064
|
-
return (file) =>
|
|
3065
|
-
file === options.filename ? options.code : options.serverSourceFiles?.get(file);
|
|
3193
|
+
return (file) => {
|
|
3194
|
+
const known = file === options.filename ? options.code : options.serverSourceFiles?.get(file);
|
|
3195
|
+
if (known !== undefined) {
|
|
3196
|
+
return known;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
const relativeFile = relative(resolve(options.appDir), resolve(file));
|
|
3200
|
+
if (relativeFile === "" || relativeFile === ".." || relativeFile.startsWith(`..${sep}`)) {
|
|
3201
|
+
return undefined;
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
try {
|
|
3205
|
+
return readFileSync(file, "utf8");
|
|
3206
|
+
} catch {
|
|
3207
|
+
return undefined;
|
|
3208
|
+
}
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
async function routeShellsMayUseRequestInput(options: {
|
|
3213
|
+
appDir: string;
|
|
3214
|
+
pageFile: string;
|
|
3215
|
+
serverModuleCacheVersion: string | undefined;
|
|
3216
|
+
serverSourceFiles: ReadonlyMap<string, string> | undefined;
|
|
3217
|
+
}): Promise<boolean> {
|
|
3218
|
+
const shells = await shellFilesForPage(
|
|
3219
|
+
options.appDir,
|
|
3220
|
+
options.pageFile,
|
|
3221
|
+
options.serverModuleCacheVersion,
|
|
3222
|
+
);
|
|
3223
|
+
|
|
3224
|
+
for (const shell of shells) {
|
|
3225
|
+
const source = await readServerSourceFile(
|
|
3226
|
+
shell.file,
|
|
3227
|
+
options.serverModuleCacheVersion,
|
|
3228
|
+
options.serverSourceFiles,
|
|
3229
|
+
);
|
|
3230
|
+
if (
|
|
3231
|
+
routeClosureMayUseRequestInput({
|
|
3232
|
+
filename: shell.file,
|
|
3233
|
+
files: routeSourceFilesForAnalysis({
|
|
3234
|
+
appDir: options.appDir,
|
|
3235
|
+
code: source,
|
|
3236
|
+
filename: shell.file,
|
|
3237
|
+
serverSourceFiles: options.serverSourceFiles,
|
|
3238
|
+
}),
|
|
3239
|
+
projectRoot: options.appDir,
|
|
3240
|
+
source,
|
|
3241
|
+
})
|
|
3242
|
+
) {
|
|
3243
|
+
return true;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
return false;
|
|
3066
3248
|
}
|
|
3067
3249
|
|
|
3068
3250
|
async function runServerModule(
|
|
@@ -3269,8 +3451,10 @@ function runServerStreamModule(
|
|
|
3269
3451
|
appDir: string;
|
|
3270
3452
|
assetBaseUrl?: string | undefined;
|
|
3271
3453
|
clientRouteInferenceCache: ClientRouteInferenceCache;
|
|
3454
|
+
markerRequest: Request;
|
|
3272
3455
|
pageFile: string;
|
|
3273
3456
|
props: ServerComponentProps;
|
|
3457
|
+
requestUrl: string;
|
|
3274
3458
|
routePath: string;
|
|
3275
3459
|
routeScripts?: ReadonlyMap<string, string> | undefined;
|
|
3276
3460
|
clientRoute: boolean;
|
|
@@ -3315,11 +3499,11 @@ function runServerStreamModule(
|
|
|
3315
3499
|
script: options.script,
|
|
3316
3500
|
props: {
|
|
3317
3501
|
params: options.props.params,
|
|
3318
|
-
request: { url: options.
|
|
3502
|
+
request: { url: options.requestUrl },
|
|
3319
3503
|
data: options.props.data,
|
|
3320
3504
|
},
|
|
3321
3505
|
})
|
|
3322
|
-
:
|
|
3506
|
+
: serverOnlyRouteMarkerParts(options.routePath, options.markerRequest);
|
|
3323
3507
|
|
|
3324
3508
|
sink.append("<!DOCTYPE html>");
|
|
3325
3509
|
sink.append(
|
|
@@ -3537,10 +3721,13 @@ async function runServerStreamModuleWithLoading(
|
|
|
3537
3721
|
data: Promise<unknown>;
|
|
3538
3722
|
define?: UserConfig["define"] | undefined;
|
|
3539
3723
|
loadingFile: string;
|
|
3724
|
+
markerRequest: Request;
|
|
3540
3725
|
pageFile: string;
|
|
3541
3726
|
params: RouteParams;
|
|
3542
3727
|
queryClient: QueryClient;
|
|
3543
3728
|
request: Request;
|
|
3729
|
+
requestUrl: string;
|
|
3730
|
+
trackedRequest?: TrackedHeaderRequest | undefined;
|
|
3544
3731
|
routePath: string;
|
|
3545
3732
|
routeScripts?: ReadonlyMap<string, string> | undefined;
|
|
3546
3733
|
serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
|
|
@@ -3551,12 +3738,11 @@ async function runServerStreamModuleWithLoading(
|
|
|
3551
3738
|
importPolicy?: AppRouterImportPolicy | undefined;
|
|
3552
3739
|
},
|
|
3553
3740
|
): Promise<ReadableStream<Uint8Array>> {
|
|
3554
|
-
const loadingProps = {
|
|
3741
|
+
const loadingProps = withTrackedRequest({
|
|
3555
3742
|
data: undefined,
|
|
3556
3743
|
params: options.params,
|
|
3557
3744
|
queryClient: options.queryClient,
|
|
3558
|
-
|
|
3559
|
-
};
|
|
3745
|
+
}, options.request, options.trackedRequest);
|
|
3560
3746
|
const layoutShells = await layoutShellsForPage(
|
|
3561
3747
|
options.appDir,
|
|
3562
3748
|
options.pageFile,
|
|
@@ -3587,10 +3773,10 @@ async function runServerStreamModuleWithLoading(
|
|
|
3587
3773
|
script: options.script,
|
|
3588
3774
|
props: {
|
|
3589
3775
|
params: options.params,
|
|
3590
|
-
request: { url: options.
|
|
3776
|
+
request: { url: options.requestUrl },
|
|
3591
3777
|
},
|
|
3592
3778
|
})
|
|
3593
|
-
:
|
|
3779
|
+
: serverOnlyRouteMarkerParts(options.routePath, options.markerRequest);
|
|
3594
3780
|
|
|
3595
3781
|
return renderToReadableStream((sink) => {
|
|
3596
3782
|
sink.append("<!DOCTYPE html>");
|
|
@@ -3616,12 +3802,11 @@ async function runServerStreamModuleWithLoading(
|
|
|
3616
3802
|
await appendServerStreamModule(
|
|
3617
3803
|
code,
|
|
3618
3804
|
boundarySink,
|
|
3619
|
-
{
|
|
3805
|
+
withTrackedRequest({
|
|
3620
3806
|
data,
|
|
3621
3807
|
params: options.params,
|
|
3622
3808
|
queryClient: options.queryClient,
|
|
3623
|
-
|
|
3624
|
-
},
|
|
3809
|
+
}, options.request, options.trackedRequest),
|
|
3625
3810
|
options.pageFile,
|
|
3626
3811
|
options.serverModules,
|
|
3627
3812
|
options.serverModuleCacheVersion,
|
|
@@ -4014,17 +4199,6 @@ async function renderShellPrefixSuffix(
|
|
|
4014
4199
|
serverModuleCacheVersion === undefined || hasNamedSlots || shell.kind === "template"
|
|
4015
4200
|
? undefined
|
|
4016
4201
|
: `${appDir}\0${shell.file}\0${serverModuleCacheVersion}\0${importPolicyCacheKey(importPolicy)}\0${viteDefineCacheKey(define)}\0${vitePluginsCacheKey(vitePlugins)}`;
|
|
4017
|
-
if (cacheKey !== undefined) {
|
|
4018
|
-
const cached = readRouterRuntimeCacheEntry(
|
|
4019
|
-
renderedShellCache,
|
|
4020
|
-
cacheKey,
|
|
4021
|
-
renderedShellCacheCounters,
|
|
4022
|
-
);
|
|
4023
|
-
if (cached !== undefined && cached !== "impure") {
|
|
4024
|
-
return cached;
|
|
4025
|
-
}
|
|
4026
|
-
}
|
|
4027
|
-
|
|
4028
4202
|
const staticEntry = cacheKey === undefined ? undefined : shellStaticRenderCache.get(cacheKey);
|
|
4029
4203
|
const loadedStaticEntry =
|
|
4030
4204
|
staticEntry ??
|
|
@@ -4052,34 +4226,6 @@ async function renderShellPrefixSuffix(
|
|
|
4052
4226
|
hasOutOfOrderBoundary: loadedStaticEntry.hasOutOfOrderBoundary,
|
|
4053
4227
|
};
|
|
4054
4228
|
addRenderTimingPhaseDuration(timing, phaseStartedAt, "layoutSlotSplitMs");
|
|
4055
|
-
const shellCacheKey = loadedStaticEntry.shellUsesAwait ? undefined : cacheKey;
|
|
4056
|
-
const cached =
|
|
4057
|
-
shellCacheKey !== undefined
|
|
4058
|
-
? readRouterRuntimeCacheEntry(renderedShellCache, shellCacheKey, renderedShellCacheCounters)
|
|
4059
|
-
: undefined;
|
|
4060
|
-
|
|
4061
|
-
// Detect purity: a zero-arg component cannot depend on props. The
|
|
4062
|
-
// markShellBoundary + splitLayoutSlot output is then constant for
|
|
4063
|
-
// the (appDir, shellFile, version) tuple. We only set the cache
|
|
4064
|
-
// entry on the first request that observes the function arity; on
|
|
4065
|
-
// an "impure" tag we never overwrite it.
|
|
4066
|
-
if (shellCacheKey !== undefined && cached !== "impure") {
|
|
4067
|
-
if (loadedStaticEntry.component.length === 0) {
|
|
4068
|
-
if (renderedShellCache.size >= MAX_RENDERED_SHELL_CACHE_ENTRIES) {
|
|
4069
|
-
const oldestKey = renderedShellCache.keys().next().value;
|
|
4070
|
-
if (oldestKey !== undefined) {
|
|
4071
|
-
renderedShellCache.delete(oldestKey);
|
|
4072
|
-
renderedShellCacheCounters.evictions += 1;
|
|
4073
|
-
}
|
|
4074
|
-
}
|
|
4075
|
-
renderedShellCache.set(shellCacheKey, rendered);
|
|
4076
|
-
} else {
|
|
4077
|
-
// Impure — stamp the cache so subsequent lookups short-circuit
|
|
4078
|
-
// without re-checking arity. We still run the per-request
|
|
4079
|
-
// render path above so the props are honoured.
|
|
4080
|
-
renderedShellCache.set(shellCacheKey, "impure");
|
|
4081
|
-
}
|
|
4082
|
-
}
|
|
4083
4229
|
|
|
4084
4230
|
return rendered;
|
|
4085
4231
|
}
|