@reckona/mreact-router 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +76 -8
  2. package/dist/adapters/aws-lambda.d.ts +10 -0
  3. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  4. package/dist/adapters/aws-lambda.js +70 -15
  5. package/dist/adapters/aws-lambda.js.map +1 -1
  6. package/dist/adapters/node.d.ts +3 -0
  7. package/dist/adapters/node.d.ts.map +1 -1
  8. package/dist/adapters/node.js +1 -0
  9. package/dist/adapters/node.js.map +1 -1
  10. package/dist/build.d.ts.map +1 -1
  11. package/dist/build.js +355 -29
  12. package/dist/build.js.map +1 -1
  13. package/dist/cache-config.d.ts +3 -0
  14. package/dist/cache-config.d.ts.map +1 -0
  15. package/dist/cache-config.js +15 -0
  16. package/dist/cache-config.js.map +1 -0
  17. package/dist/cache-stats.d.ts +17 -0
  18. package/dist/cache-stats.d.ts.map +1 -0
  19. package/dist/cache-stats.js +28 -0
  20. package/dist/cache-stats.js.map +1 -0
  21. package/dist/cli-options.d.ts +2 -1
  22. package/dist/cli-options.d.ts.map +1 -1
  23. package/dist/cli-options.js +21 -0
  24. package/dist/cli-options.js.map +1 -1
  25. package/dist/cli.js +3 -0
  26. package/dist/cli.js.map +1 -1
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +151 -2
  29. package/dist/client.js.map +1 -1
  30. package/dist/config.d.ts +5 -0
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/config.js +14 -0
  33. package/dist/config.js.map +1 -1
  34. package/dist/csp.d.ts +3 -0
  35. package/dist/csp.d.ts.map +1 -1
  36. package/dist/csp.js +1 -1
  37. package/dist/csp.js.map +1 -1
  38. package/dist/index.d.ts +8 -3
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +2 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/logger.d.ts +8 -1
  43. package/dist/logger.d.ts.map +1 -1
  44. package/dist/logger.js.map +1 -1
  45. package/dist/module-runner.d.ts +2 -0
  46. package/dist/module-runner.d.ts.map +1 -1
  47. package/dist/module-runner.js +18 -7
  48. package/dist/module-runner.js.map +1 -1
  49. package/dist/navigation.d.ts.map +1 -1
  50. package/dist/navigation.js +14 -4
  51. package/dist/navigation.js.map +1 -1
  52. package/dist/render.d.ts +11 -1
  53. package/dist/render.d.ts.map +1 -1
  54. package/dist/render.js +505 -47
  55. package/dist/render.js.map +1 -1
  56. package/dist/route-source.d.ts.map +1 -1
  57. package/dist/route-source.js +1 -0
  58. package/dist/route-source.js.map +1 -1
  59. package/dist/runtime-cache.d.ts +4 -0
  60. package/dist/runtime-cache.d.ts.map +1 -0
  61. package/dist/runtime-cache.js +6 -0
  62. package/dist/runtime-cache.js.map +1 -0
  63. package/dist/serve.d.ts +9 -0
  64. package/dist/serve.d.ts.map +1 -1
  65. package/dist/serve.js +34 -2
  66. package/dist/serve.js.map +1 -1
  67. package/dist/session.d.ts.map +1 -1
  68. package/dist/session.js +7 -2
  69. package/dist/session.js.map +1 -1
  70. package/dist/trace.d.ts +41 -0
  71. package/dist/trace.d.ts.map +1 -0
  72. package/dist/trace.js +46 -0
  73. package/dist/trace.js.map +1 -0
  74. package/dist/types.d.ts +2 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +2 -0
  77. package/dist/types.js.map +1 -0
  78. package/package.json +8 -8
package/dist/render.js CHANGED
@@ -1,23 +1,27 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
- import { access, readFile } from "node:fs/promises";
3
+ import { access, readFile, stat } from "node:fs/promises";
4
4
  import { dirname, join, relative, sep } from "node:path";
5
5
  import { transform, } from "@reckona/mreact-compiler";
6
6
  import { createQueryClient, dehydrate, __MREACT_QUERY_STATE_SCRIPT_ID, runWithQueryClient, } from "@reckona/mreact-query";
7
7
  import { build as bundle } from "esbuild";
8
8
  import { createStringSink, renderAsyncBoundary, renderOutOfOrderReorderScript, renderToReadableStream, } from "@reckona/mreact-server";
9
- import { hydrationMarkerParts, inferClientRouteModule, withHydrationMarkers, withRouteMarkers, } from "./client.js";
9
+ import { hydrationMarkerParts, inferClientRouteModule, routeIdForPath, withHydrationMarkers, withRouteMarkers, } from "./client.js";
10
10
  import { assetPath } from "./assets.js";
11
11
  import { escapeHtmlAttribute, escapeHtmlText as escapeHtml, } from "@reckona/mreact-shared/html-escape";
12
12
  import { matchRoute, scanAppRoutes } from "./routes.js";
13
13
  import { dispatchServerActionRequest, prepareRouteServerActions, serverActionCookie, } from "./actions.js";
14
14
  import { beginRouteCacheContext, cachedRouteResponse, cacheRouteResponse, routeCacheKey, routeCachePolicyFromSource, } from "./cache.js";
15
+ import { resolveRouterCacheLimit } from "./cache-config.js";
15
16
  import { importAppRouterFileModule, importAppRouterSourceModule } from "./module-runner.js";
16
17
  import { contentSecurityPolicy } from "./csp.js";
17
18
  import { htmlResponse } from "./http.js";
18
19
  import { isNotFoundError, isRedirectError, rewriteLocation } from "./navigation.js";
19
20
  import { createAppRouterImportPolicyPlugin } from "./import-policy.js";
20
21
  import { hasLoaderExport, isStreamRouteSource, stripRouteModuleExports } from "./route-source.js";
22
+ import { emitRouterLog, logDurationMs, logNow } from "./logger.js";
23
+ import { createRouterRuntimeCacheCounters, readRouterRuntimeCacheEntry, routerRuntimeCacheStat, } from "./cache-stats.js";
24
+ import { invokeRouterInstrumentation, traceContextFromRequest, } from "./trace.js";
21
25
  const nativeEscapeTransform = {
22
26
  batchImportName: "escapeHtmlBatch",
23
27
  batchImportSource: "@reckona/mreact-router/native-escape",
@@ -52,6 +56,18 @@ export async function preloadBuiltRequestModules(options) {
52
56
  });
53
57
  continue;
54
58
  }
59
+ const analysis = await analyzeRouteSource({
60
+ code,
61
+ filename: route.file,
62
+ routePath: route.path,
63
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
64
+ });
65
+ await preloadBuiltPageRouteModules({
66
+ ...options,
67
+ analysis,
68
+ code,
69
+ file: route.file,
70
+ });
55
71
  if (hasLoaderExport(code)) {
56
72
  await loadRouteLoaderModule({
57
73
  appDir: options.appDir,
@@ -64,22 +80,103 @@ export async function preloadBuiltRequestModules(options) {
64
80
  }
65
81
  }
66
82
  }
83
+ async function preloadBuiltPageRouteModules(options) {
84
+ const routeCode = options.analysis.routeCode;
85
+ const stringOutput = transformServerModule({
86
+ code: routeCode,
87
+ clientBoundaryImports: options.analysis.clientInference.clientBoundaryImports,
88
+ filename: options.file,
89
+ serverModules: options.serverModules,
90
+ serverOutput: "string",
91
+ });
92
+ assertNoFatalServerDiagnostics(stringOutput.diagnostics);
93
+ await loadServerModule(stringOutput.code, options.file, options.serverModules, options.serverModuleCacheVersion);
94
+ if (options.analysis.streamRoute) {
95
+ const streamOutput = transformServerModule({
96
+ code: routeCode,
97
+ clientBoundaryImports: options.analysis.clientInference.clientBoundaryImports,
98
+ filename: options.file,
99
+ serverModules: options.serverModules,
100
+ serverOutput: "stream",
101
+ serverAwaitHydration: options.analysis.clientInference.client,
102
+ });
103
+ assertNoFatalServerDiagnostics(streamOutput.diagnostics);
104
+ await loadServerStreamModule(streamOutput.code, options.file, options.serverModules, options.serverModuleCacheVersion);
105
+ }
106
+ await preloadShellModulesForPage({
107
+ appDir: options.appDir,
108
+ pageFile: options.file,
109
+ serverModules: options.serverModules,
110
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
111
+ serverSourceFiles: options.serverSourceFiles,
112
+ });
113
+ await loadComposedRouteMetadata({
114
+ appDir: options.appDir,
115
+ code: options.code,
116
+ filename: options.file,
117
+ importPolicy: options.importPolicy,
118
+ serverModules: options.serverModules,
119
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
120
+ serverSourceFiles: options.serverSourceFiles,
121
+ });
122
+ }
123
+ async function preloadShellModulesForPage(options) {
124
+ const shellFiles = await shellFilesForPage(options.appDir, options.pageFile, options.serverModuleCacheVersion);
125
+ for (const shell of shellFiles) {
126
+ const code = await readServerSourceFile(shell.file, options.serverModuleCacheVersion, options.serverSourceFiles);
127
+ const output = transformServerModule({
128
+ code,
129
+ filename: shell.file,
130
+ serverModules: options.serverModules,
131
+ serverOutput: "string",
132
+ });
133
+ assertNoFatalServerDiagnostics(output.diagnostics);
134
+ await loadServerModule(output.code, shell.file, options.serverModules, options.serverModuleCacheVersion);
135
+ }
136
+ }
137
+ function assertNoFatalServerDiagnostics(diagnostics) {
138
+ const fatalDiagnostics = diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
139
+ if (fatalDiagnostics.length > 0) {
140
+ throw new Error(fatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"));
141
+ }
142
+ }
67
143
  const serverTransformCache = new Map();
144
+ const serverTransformCacheCounters = createRouterRuntimeCacheCounters();
68
145
  const serverSourceFileCache = new Map();
146
+ const serverSourceFileCacheCounters = createRouterRuntimeCacheCounters();
69
147
  const routeSourceAnalysisCache = new Map();
148
+ const routeSourceAnalysisCacheCounters = createRouterRuntimeCacheCounters();
70
149
  const routeOutOfOrderBoundaryAnalysisCache = new Map();
150
+ const routeOutOfOrderBoundaryAnalysisCacheCounters = createRouterRuntimeCacheCounters();
71
151
  const routeLoaderModuleCache = new Map();
152
+ const routeLoaderModuleCacheCounters = createRouterRuntimeCacheCounters();
72
153
  const middlewareModuleCache = new Map();
154
+ const middlewareModuleCacheCounters = createRouterRuntimeCacheCounters();
73
155
  const serverRouteModuleCache = new Map();
156
+ const serverRouteModuleCacheCounters = createRouterRuntimeCacheCounters();
74
157
  const composedRouteMetadataCache = new Map();
75
- const maxServerTransformCacheEntries = 512;
76
- const maxServerSourceFileCacheEntries = 512;
77
- const maxRouteSourceAnalysisCacheEntries = 512;
78
- const maxRouteOutOfOrderBoundaryAnalysisCacheEntries = 512;
79
- const maxRouteLoaderModuleCacheEntries = 512;
80
- const maxMiddlewareModuleCacheEntries = 64;
81
- const maxServerRouteModuleCacheEntries = 512;
82
- const maxComposedRouteMetadataCacheEntries = 512;
158
+ const composedRouteMetadataCacheCounters = createRouterRuntimeCacheCounters();
159
+ const maxServerTransformCacheEntries = resolveRouterCacheLimit("SERVER_TRANSFORM", 512);
160
+ const maxServerSourceFileCacheEntries = resolveRouterCacheLimit("SERVER_SOURCE_FILE", 512);
161
+ const maxRouteSourceAnalysisCacheEntries = resolveRouterCacheLimit("ROUTE_SOURCE_ANALYSIS", 512);
162
+ const maxRouteOutOfOrderBoundaryAnalysisCacheEntries = resolveRouterCacheLimit("ROUTE_OUT_OF_ORDER_BOUNDARY_ANALYSIS", 512);
163
+ const maxRouteLoaderModuleCacheEntries = resolveRouterCacheLimit("ROUTE_LOADER_MODULE", 512);
164
+ const maxMiddlewareModuleCacheEntries = resolveRouterCacheLimit("MIDDLEWARE_MODULE", 64);
165
+ const maxServerRouteModuleCacheEntries = resolveRouterCacheLimit("SERVER_ROUTE_MODULE", 512);
166
+ const maxComposedRouteMetadataCacheEntries = resolveRouterCacheLimit("COMPOSED_ROUTE_METADATA", 512);
167
+ export function routerRenderRuntimeCacheStats() {
168
+ return [
169
+ routerRuntimeCacheStat("server-transform", serverTransformCache, maxServerTransformCacheEntries, serverTransformCacheCounters),
170
+ routerRuntimeCacheStat("server-source-file", serverSourceFileCache, maxServerSourceFileCacheEntries, serverSourceFileCacheCounters),
171
+ routerRuntimeCacheStat("route-source-analysis", routeSourceAnalysisCache, maxRouteSourceAnalysisCacheEntries, routeSourceAnalysisCacheCounters),
172
+ routerRuntimeCacheStat("route-out-of-order-boundary-analysis", routeOutOfOrderBoundaryAnalysisCache, maxRouteOutOfOrderBoundaryAnalysisCacheEntries, routeOutOfOrderBoundaryAnalysisCacheCounters),
173
+ routerRuntimeCacheStat("route-loader-module", routeLoaderModuleCache, maxRouteLoaderModuleCacheEntries, routeLoaderModuleCacheCounters),
174
+ routerRuntimeCacheStat("middleware-module", middlewareModuleCache, maxMiddlewareModuleCacheEntries, middlewareModuleCacheCounters),
175
+ routerRuntimeCacheStat("server-route-module", serverRouteModuleCache, maxServerRouteModuleCacheEntries, serverRouteModuleCacheCounters),
176
+ routerRuntimeCacheStat("composed-route-metadata", composedRouteMetadataCache, maxComposedRouteMetadataCacheEntries, composedRouteMetadataCacheCounters),
177
+ routerRuntimeCacheStat("rendered-shell", renderedShellCache, MAX_RENDERED_SHELL_CACHE_ENTRIES, renderedShellCacheCounters),
178
+ ];
179
+ }
83
180
  // Issue 086: per-shell prefix/suffix cache. Pure layouts (whose
84
181
  // exported component takes zero arguments and therefore cannot
85
182
  // depend on the request props) produce the same HTML for every
@@ -93,14 +190,52 @@ const maxComposedRouteMetadataCacheEntries = 512;
93
190
  // edits without server restart.
94
191
  const renderedShellCache = new Map();
95
192
  const MAX_RENDERED_SHELL_CACHE_ENTRIES = 1024;
193
+ const renderedShellCacheCounters = createRouterRuntimeCacheCounters();
96
194
  export async function renderAppRequest(options) {
97
195
  const authStorage = authRequestStorage();
98
196
  if (authStorage.getStore() === undefined) {
99
197
  return authStorage.run({}, () => renderAppRequest(options));
100
198
  }
199
+ const trace = traceContextFromRequest(options.request);
200
+ const url = new URL(options.request.url);
201
+ const requestEvent = {
202
+ method: options.request.method,
203
+ path: url.pathname,
204
+ request: options.request,
205
+ ...(trace === undefined ? {} : { trace }),
206
+ };
207
+ invokeRouterInstrumentation(options.instrumentation?.onRequestStart, requestEvent);
101
208
  const response = await renderAppRequestInternal(options);
209
+ invokeRouterInstrumentation(options.instrumentation?.onRequestEnd, {
210
+ ...requestEvent,
211
+ status: response.status,
212
+ });
102
213
  return applyAppRouterResponseHook(response, options);
103
214
  }
215
+ function createRenderTiming(logger) {
216
+ return logger?.debug === undefined ? undefined : { phases: {} };
217
+ }
218
+ function renderTimingPhaseStartedAt(timing) {
219
+ return timing === undefined ? undefined : logNow();
220
+ }
221
+ function finishRenderTimingPhase(timing, startedAt, phaseName) {
222
+ if (timing === undefined || startedAt === undefined) {
223
+ return;
224
+ }
225
+ timing.phases[phaseName] = logDurationMs(startedAt);
226
+ }
227
+ function emitRenderTiming(options, timing, status) {
228
+ if (timing === undefined) {
229
+ return;
230
+ }
231
+ emitRouterLog(options.logger, "debug", {
232
+ method: options.request.method,
233
+ path: new URL(options.request.url).pathname,
234
+ phases: timing.phases,
235
+ status,
236
+ type: "router:render:timing",
237
+ });
238
+ }
104
239
  export async function resolveAppRouterMiddleware(options) {
105
240
  const middlewareResponse = await runMiddleware(options);
106
241
  if (middlewareResponse === undefined) {
@@ -116,18 +251,43 @@ export async function resolveAppRouterMiddleware(options) {
116
251
  return { response: middlewareResponse, type: "response" };
117
252
  }
118
253
  async function renderAppRequestInternal(options) {
254
+ const timing = createRenderTiming(options.logger);
255
+ let phaseStartedAt = renderTimingPhaseStartedAt(timing);
119
256
  const routes = options.routes ?? (await scanAppRoutes({ appDir: options.appDir }));
257
+ finishRenderTimingPhase(timing, phaseStartedAt, "routeScanMs");
120
258
  const url = new URL(options.request.url);
121
- const middlewareResult = options.skipMiddleware === true
259
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
260
+ const matched = options.routeMatcher?.match(url.pathname) ?? matchRoute(routes, url.pathname);
261
+ finishRenderTimingPhase(timing, phaseStartedAt, "routeMatchMs");
262
+ const hasMiddleware = options.skipMiddleware === true
263
+ ? false
264
+ : await hasAppMiddleware({
265
+ appDir: options.appDir,
266
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
267
+ serverSourceFiles: options.serverSourceFiles,
268
+ });
269
+ const middlewareControl = hasMiddleware && matched?.route.kind === "page"
270
+ ? await loadRouteMiddlewareControl({
271
+ appDir: options.appDir,
272
+ pageFile: matched.route.file,
273
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
274
+ serverSourceFiles: options.serverSourceFiles,
275
+ })
276
+ : undefined;
277
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
278
+ const middlewareResult = options.skipMiddleware === true || !hasMiddleware
122
279
  ? { request: options.request, type: "continue" }
123
280
  : await resolveAppRouterMiddleware({
124
281
  appDir: options.appDir,
125
282
  importPolicy: options.importPolicy,
283
+ instrumentation: options.instrumentation,
284
+ middlewareControl,
126
285
  request: options.request,
127
286
  serverModules: options.serverModules,
128
287
  serverModuleCacheVersion: options.serverModuleCacheVersion,
129
288
  serverSourceFiles: options.serverSourceFiles,
130
289
  });
290
+ finishRenderTimingPhase(timing, phaseStartedAt, "middlewareMs");
131
291
  if (middlewareResult.type === "response") {
132
292
  return middlewareResult.response;
133
293
  }
@@ -150,7 +310,6 @@ async function renderAppRequestInternal(options) {
150
310
  serverActions: options.serverActions,
151
311
  });
152
312
  }
153
- const matched = options.routeMatcher?.match(url.pathname) ?? matchRoute(routes, url.pathname);
154
313
  if (matched === undefined) {
155
314
  const notFoundFile = await nearestBoundaryFileForPath({
156
315
  appDir: options.appDir,
@@ -162,6 +321,7 @@ async function renderAppRequestInternal(options) {
162
321
  assetBaseUrl: options.assetBaseUrl,
163
322
  error: undefined,
164
323
  request: options.request,
324
+ routePath: url.pathname,
165
325
  routeFile: notFoundFile,
166
326
  routeScripts: options.clientScripts,
167
327
  serverModules: options.serverModules,
@@ -204,35 +364,44 @@ async function renderAppRequestInternal(options) {
204
364
  }
205
365
  routeCacheContext = beginRouteCacheContext(options.routeCache);
206
366
  const clientScript = options.clientScripts?.get(matched.route.path);
367
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
207
368
  const originalCode = await readServerSourceFile(matched.route.file, options.serverModuleCacheVersion, options.serverSourceFiles);
369
+ finishRenderTimingPhase(timing, phaseStartedAt, "readSourceMs");
370
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
208
371
  const originalAnalysis = await analyzeRouteSource({
209
372
  code: originalCode,
210
373
  filename: matched.route.file,
211
374
  routePath: matched.route.path,
212
375
  serverModuleCacheVersion: options.serverModuleCacheVersion,
213
376
  });
377
+ finishRenderTimingPhase(timing, phaseStartedAt, "sourceAnalysisMs");
214
378
  const cachePolicy = originalAnalysis.cachePolicy;
215
379
  const navigationScript = options.navigationScripts?.get(matched.route.path);
216
380
  const cacheKey = routeCacheKey(options.appDir, matched.route.path, url);
217
381
  const mayUseRouteCache = cachePolicy === undefined
218
382
  ? originalAnalysis.usesRuntimeCacheControl
219
383
  : cachePolicy.revalidateSeconds !== 0;
384
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
220
385
  const cachedResponse = !mayUseRouteCache
221
386
  ? undefined
222
387
  : await cachedRouteResponse({
223
388
  cache: options.routeCache,
224
389
  key: cacheKey,
225
390
  });
391
+ finishRenderTimingPhase(timing, phaseStartedAt, "routeCacheMs");
226
392
  if (cachedResponse !== undefined) {
227
393
  return cachedResponse;
228
394
  }
395
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
229
396
  const preparedActions = await prepareRouteServerActions({
230
397
  appDir: options.appDir,
231
398
  code: originalCode,
232
399
  pageFile: matched.route.file,
233
400
  request: options.request,
234
401
  });
402
+ finishRenderTimingPhase(timing, phaseStartedAt, "serverActionsMs");
235
403
  const code = preparedActions.code;
404
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
236
405
  const routeAnalysis = code === originalCode
237
406
  ? originalAnalysis
238
407
  : await analyzeRouteSource({
@@ -241,25 +410,32 @@ async function renderAppRequestInternal(options) {
241
410
  routePath: matched.route.path,
242
411
  serverModuleCacheVersion: undefined,
243
412
  });
413
+ finishRenderTimingPhase(timing, phaseStartedAt, "routeCodeAnalysisMs");
244
414
  const routeCode = routeAnalysis.routeCode;
245
415
  const streamRoute = routeAnalysis.streamRoute;
246
416
  const clientInference = routeAnalysis.clientInference;
247
417
  const clientRoute = clientInference.client;
418
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
248
419
  const dataPromise = routeAnalysis.hasLoader
249
- ? loadRouteData({
420
+ ? loadRouteDataWithInstrumentation({
421
+ appDir: options.appDir,
250
422
  code,
251
423
  context: {
252
424
  params: matched.params,
253
425
  queryClient,
254
426
  request: options.request,
255
427
  },
256
- appDir: options.appDir,
257
428
  filename: matched.route.file,
258
429
  importPolicy: options.importPolicy,
430
+ instrumentation: options.instrumentation,
431
+ request: options.request,
432
+ routeId: routeIdForPath(matched.route.path),
433
+ routePath: matched.route.path,
259
434
  serverModules: options.serverModules,
260
435
  serverModuleCacheVersion: options.serverModuleCacheVersion,
261
436
  })
262
437
  : undefined;
438
+ finishRenderTimingPhase(timing, phaseStartedAt, "loaderStartMs");
263
439
  recoveryRoute = {
264
440
  clientRoute,
265
441
  props: {
@@ -270,22 +446,28 @@ async function renderAppRequestInternal(options) {
270
446
  script: clientScript,
271
447
  };
272
448
  if (streamRoute) {
449
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
273
450
  const loadingFile = await nearestExistingBoundaryFileForPage({
274
451
  appDir: options.appDir,
275
452
  filename: "loading.mreact.tsx",
276
453
  pageFile: matched.route.file,
454
+ serverSourceFiles: options.serverSourceFiles,
277
455
  });
456
+ finishRenderTimingPhase(timing, phaseStartedAt, "loadingBoundaryLookupMs");
278
457
  const streamShellResponseHeaders = {
279
458
  "content-type": "text/html; charset=utf-8",
280
459
  "x-mreact-stream": "1",
281
460
  };
461
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
282
462
  const mayRenderOutOfOrder = await mayRenderOutOfOrderBoundaryDeep({
283
463
  code: routeCode,
284
464
  filename: matched.route.file,
285
465
  serverModuleCacheVersion: options.serverModuleCacheVersion,
286
466
  serverSourceFiles: options.serverSourceFiles,
287
467
  });
468
+ finishRenderTimingPhase(timing, phaseStartedAt, "outOfOrderAnalysisMs");
288
469
  if (loadingFile === undefined && !mayRenderOutOfOrder) {
470
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
289
471
  const stringOutput = transformServerModule({
290
472
  code: routeCode,
291
473
  clientBoundaryImports: clientInference.clientBoundaryImports,
@@ -293,6 +475,7 @@ async function renderAppRequestInternal(options) {
293
475
  serverModules: options.serverModules,
294
476
  serverOutput: "string",
295
477
  });
478
+ finishRenderTimingPhase(timing, phaseStartedAt, "stringTransformMs");
296
479
  const stringFatalDiagnostics = stringOutput.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
297
480
  if (stringFatalDiagnostics.length > 0) {
298
481
  return new Response(stringFatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"), {
@@ -357,15 +540,20 @@ async function renderAppRequestInternal(options) {
357
540
  html = injectHeadMetadata(html, metadata);
358
541
  html = injectAuthSessionClaims(html, originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined);
359
542
  html = injectQueryState(html, dehydrate(queryClient));
360
- const headers = new Headers(responseHeadersForMetadata(metadata));
361
- headers.set("x-mreact-stream", "1");
362
- return withOptionalActionCookie(htmlResponse(`<!DOCTYPE html>${clientNavigationHeadTags({
543
+ const response = withOptionalActionCookie(htmlResponse(`<!DOCTYPE html>${clientNavigationHeadTags({
363
544
  assetBaseUrl: options.assetBaseUrl,
364
545
  currentScript: clientRoute ? clientScript : undefined,
365
546
  currentNavigationScript: clientRoute ? undefined : navigationScript,
366
547
  routeScripts: options.clientScripts,
367
- })}${html}`, { headers }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
548
+ })}${html}`, {
549
+ headers: responseHeadersForMetadata(metadata, {
550
+ "x-mreact-stream": "1",
551
+ }),
552
+ }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
553
+ emitRenderTiming(options, timing, response.status);
554
+ return response;
368
555
  }
556
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
369
557
  const output = transformServerModule({
370
558
  code: routeCode,
371
559
  clientBoundaryImports: clientInference.clientBoundaryImports,
@@ -374,6 +562,7 @@ async function renderAppRequestInternal(options) {
374
562
  serverOutput: "stream",
375
563
  serverAwaitHydration: clientRoute,
376
564
  });
565
+ finishRenderTimingPhase(timing, phaseStartedAt, "streamTransformMs");
377
566
  const fatalDiagnostics = output.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
378
567
  if (fatalDiagnostics.length > 0) {
379
568
  return new Response(fatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"), {
@@ -382,6 +571,7 @@ async function renderAppRequestInternal(options) {
382
571
  });
383
572
  }
384
573
  if (loadingFile !== undefined) {
574
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
385
575
  const stream = await runServerStreamModuleWithLoading(output.code, {
386
576
  appDir: options.appDir,
387
577
  assetBaseUrl: options.assetBaseUrl,
@@ -400,9 +590,12 @@ async function renderAppRequestInternal(options) {
400
590
  script: clientScript,
401
591
  clientReferenceManifest: output.metadata.clientReferenceManifest,
402
592
  });
403
- return withOptionalActionCookie(new Response(stream, {
593
+ finishRenderTimingPhase(timing, phaseStartedAt, "streamConstructionMs");
594
+ const response = withOptionalActionCookie(new Response(stream, {
404
595
  headers: streamShellResponseHeaders,
405
596
  }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
597
+ emitRenderTiming(options, timing, response.status);
598
+ return response;
406
599
  }
407
600
  const data = dataPromise === undefined ? undefined : await dataPromise;
408
601
  if (data instanceof Response) {
@@ -414,6 +607,7 @@ async function renderAppRequestInternal(options) {
414
607
  queryClient,
415
608
  request: options.request,
416
609
  };
610
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
417
611
  const stream = runServerStreamModule(output.code, {
418
612
  appDir: options.appDir,
419
613
  assetBaseUrl: options.assetBaseUrl,
@@ -428,9 +622,12 @@ async function renderAppRequestInternal(options) {
428
622
  script: clientScript,
429
623
  clientReferenceManifest: output.metadata.clientReferenceManifest,
430
624
  });
431
- return withOptionalActionCookie(new Response(stream, {
625
+ finishRenderTimingPhase(timing, phaseStartedAt, "streamConstructionMs");
626
+ const response = withOptionalActionCookie(new Response(stream, {
432
627
  headers: streamShellResponseHeaders,
433
628
  }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
629
+ emitRenderTiming(options, timing, response.status);
630
+ return response;
434
631
  }
435
632
  const output = transformServerModule({
436
633
  code: routeCode,
@@ -545,6 +742,7 @@ async function renderAppRequestInternal(options) {
545
742
  assetBaseUrl: options.assetBaseUrl,
546
743
  error: undefined,
547
744
  request: options.request,
745
+ routePath: matched.route.path,
548
746
  routeFile: notFoundFile,
549
747
  routeScripts: options.clientScripts,
550
748
  serverModules: options.serverModules,
@@ -565,6 +763,7 @@ async function renderAppRequestInternal(options) {
565
763
  assetBaseUrl: options.assetBaseUrl,
566
764
  error,
567
765
  request: options.request,
766
+ routePath: matched.route.path,
568
767
  routeFile: errorFile,
569
768
  routeScripts: options.clientScripts,
570
769
  serverModules: options.serverModules,
@@ -641,6 +840,7 @@ async function nearestExistingBoundaryFileForPage(options) {
641
840
  appDir: options.appDir,
642
841
  filename: options.filename,
643
842
  parts,
843
+ serverSourceFiles: options.serverSourceFiles,
644
844
  });
645
845
  }
646
846
  async function nearestBoundaryFileForPath(options) {
@@ -673,6 +873,12 @@ async function nearestExistingBoundaryFileFromParts(options) {
673
873
  for (let count = options.parts.length; count >= 0; count -= 1) {
674
874
  for (const filename of boundaryFilenameCandidates(options.filename)) {
675
875
  const candidate = join(options.appDir, ...options.parts.slice(0, count), filename);
876
+ if (options.serverSourceFiles !== undefined) {
877
+ if (options.serverSourceFiles.has(candidate)) {
878
+ return candidate;
879
+ }
880
+ continue;
881
+ }
676
882
  try {
677
883
  await access(candidate);
678
884
  return candidate;
@@ -700,10 +906,14 @@ async function renderSpecialRoute(options) {
700
906
  }
701
907
  const props = {
702
908
  data: undefined,
909
+ debug: errorDebugContext(options.error, options.routePath),
703
910
  error: normalizeErrorForProps(options.error),
704
911
  params: {},
705
912
  queryClient: createQueryClient(),
706
913
  request: options.request,
914
+ requestId: requestIdForErrorContext(options.request),
915
+ routeId: routeIdForPath(options.routePath ?? new URL(options.request.url).pathname),
916
+ traceId: traceContextFromRequest(options.request)?.traceId,
707
917
  };
708
918
  const pageHtml = await renderServerFileToHtml(options.routeFile, props, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles);
709
919
  const pageHtmlForLayout = options.navigation?.clientRoute === true
@@ -754,6 +964,19 @@ function normalizeErrorForProps(error) {
754
964
  }
755
965
  return { message: String(error) };
756
966
  }
967
+ function requestIdForErrorContext(request) {
968
+ return request.headers.get("x-request-id") ?? randomUUID();
969
+ }
970
+ function errorDebugContext(error, routePath) {
971
+ if (process.env.NODE_ENV === "production" || !(error instanceof Error)) {
972
+ return undefined;
973
+ }
974
+ return {
975
+ ...(error.cause === undefined ? {} : { cause: error.cause }),
976
+ ...(routePath === undefined ? {} : { route: { matched: routePath } }),
977
+ ...(error.stack === undefined ? {} : { stack: error.stack }),
978
+ };
979
+ }
757
980
  async function dispatchServerRoute(options) {
758
981
  const module = await loadServerRouteModule(options);
759
982
  const handler = module[options.request.method] ?? module.ALL ?? module.default;
@@ -783,7 +1006,7 @@ async function loadServerRouteModule(options) {
783
1006
  const codeHash = memoizedHashText(code);
784
1007
  const moduleCode = artifactCode !== undefined && artifactCode.sourceHash === codeHash ? artifactCode.code : code;
785
1008
  const cacheKey = `server-route\0${options.file}\0${options.serverModuleCacheVersion}\0${codeHash}\0${memoizedHashText(moduleCode)}`;
786
- const cached = serverRouteModuleCache.get(cacheKey);
1009
+ const cached = readRouterRuntimeCacheEntry(serverRouteModuleCache, cacheKey, serverRouteModuleCacheCounters);
787
1010
  if (cached !== undefined) {
788
1011
  return cached;
789
1012
  }
@@ -797,7 +1020,7 @@ async function loadServerRouteModule(options) {
797
1020
  serverRouteModuleCache.delete(cacheKey);
798
1021
  throw error;
799
1022
  });
800
- setBoundedCacheEntry(serverRouteModuleCache, cacheKey, loaded, maxServerRouteModuleCacheEntries);
1023
+ setBoundedCacheEntry(serverRouteModuleCache, cacheKey, loaded, maxServerRouteModuleCacheEntries, serverRouteModuleCacheCounters);
801
1024
  return loaded;
802
1025
  }
803
1026
  async function runMiddleware(options) {
@@ -820,6 +1043,9 @@ async function runMiddleware(options) {
820
1043
  serverModuleCacheVersion: options.serverModuleCacheVersion,
821
1044
  serverSourceFiles: options.serverSourceFiles,
822
1045
  });
1046
+ if (shouldSkipMiddleware(module.config, options.middlewareControl)) {
1047
+ return undefined;
1048
+ }
823
1049
  if (!middlewareMatches(module.config, new URL(options.request.url).pathname)) {
824
1050
  return undefined;
825
1051
  }
@@ -827,11 +1053,25 @@ async function runMiddleware(options) {
827
1053
  if (typeof middleware !== "function") {
828
1054
  return undefined;
829
1055
  }
1056
+ const trace = traceContextFromRequest(options.request);
1057
+ const event = {
1058
+ method: options.request.method,
1059
+ name: "middleware",
1060
+ path: new URL(options.request.url).pathname,
1061
+ request: options.request,
1062
+ ...(trace === undefined ? {} : { trace }),
1063
+ };
1064
+ invokeRouterInstrumentation(options.instrumentation?.onMiddlewareStart, event);
830
1065
  try {
831
1066
  const response = await middleware(options.request);
1067
+ invokeRouterInstrumentation(options.instrumentation?.onMiddlewareEnd, event);
832
1068
  return response instanceof Response ? response : undefined;
833
1069
  }
834
1070
  catch (error) {
1071
+ invokeRouterInstrumentation(options.instrumentation?.onMiddlewareEnd, {
1072
+ ...event,
1073
+ error,
1074
+ });
835
1075
  if (isRedirectError(error)) {
836
1076
  return new Response(null, {
837
1077
  headers: { location: error.location },
@@ -846,13 +1086,22 @@ async function runMiddleware(options) {
846
1086
  }
847
1087
  return undefined;
848
1088
  }
1089
+ function shouldSkipMiddleware(config, control) {
1090
+ if (control?.skip === true) {
1091
+ return true;
1092
+ }
1093
+ if (!Array.isArray(control?.skip)) {
1094
+ return false;
1095
+ }
1096
+ return typeof config?.id === "string" && control.skip.includes(config.id);
1097
+ }
849
1098
  async function loadMiddlewareModule(options) {
850
1099
  const code = await readServerSourceFile(options.file, options.serverModuleCacheVersion, options.serverSourceFiles);
851
1100
  const cacheKey = options.serverModuleCacheVersion === undefined
852
1101
  ? undefined
853
1102
  : `middleware\0${options.appDir}\0${options.file}\0${options.serverModuleCacheVersion}\0${memoizedHashText(code)}\0${importPolicyCacheKey(options.importPolicy)}`;
854
1103
  if (cacheKey !== undefined) {
855
- const cached = middlewareModuleCache.get(cacheKey);
1104
+ const cached = readRouterRuntimeCacheEntry(middlewareModuleCache, cacheKey, middlewareModuleCacheCounters);
856
1105
  if (cached !== undefined) {
857
1106
  return cached;
858
1107
  }
@@ -871,7 +1120,7 @@ async function loadMiddlewareModule(options) {
871
1120
  throw error;
872
1121
  });
873
1122
  if (cacheKey !== undefined) {
874
- setBoundedCacheEntry(middlewareModuleCache, cacheKey, loaded, maxMiddlewareModuleCacheEntries);
1123
+ setBoundedCacheEntry(middlewareModuleCache, cacheKey, loaded, maxMiddlewareModuleCacheEntries, middlewareModuleCacheCounters);
875
1124
  }
876
1125
  return loaded;
877
1126
  }
@@ -975,7 +1224,7 @@ function transformServerModule(options) {
975
1224
  }
976
1225
  const awaitHydrationKey = options.serverAwaitHydration === true ? "1" : "0";
977
1226
  const key = `${options.filename}\0${options.serverOutput}\0${sourceHash}\0${awaitHydrationKey}`;
978
- const cached = serverTransformCache.get(key);
1227
+ const cached = readRouterRuntimeCacheEntry(serverTransformCache, key, serverTransformCacheCounters);
979
1228
  if (cached !== undefined) {
980
1229
  return cached;
981
1230
  }
@@ -991,13 +1240,13 @@ function transformServerModule(options) {
991
1240
  target: "server",
992
1241
  ...(options.serverAwaitHydration === true ? { serverAwaitHydration: true } : {}),
993
1242
  });
994
- setBoundedCacheEntry(serverTransformCache, key, output, maxServerTransformCacheEntries);
1243
+ setBoundedCacheEntry(serverTransformCache, key, output, maxServerTransformCacheEntries, serverTransformCacheCounters);
995
1244
  return output;
996
1245
  }
997
1246
  async function analyzeRouteSource(options) {
998
1247
  const sourceHash = memoizedHashText(options.code);
999
1248
  const cacheKey = `${options.serverModuleCacheVersion ?? "dev"}\0${options.filename}\0${sourceHash}`;
1000
- const cached = routeSourceAnalysisCache.get(cacheKey);
1249
+ const cached = readRouterRuntimeCacheEntry(routeSourceAnalysisCache, cacheKey, routeSourceAnalysisCacheCounters);
1001
1250
  if (cached !== undefined) {
1002
1251
  return cached;
1003
1252
  }
@@ -1005,7 +1254,7 @@ async function analyzeRouteSource(options) {
1005
1254
  routeSourceAnalysisCache.delete(cacheKey);
1006
1255
  throw error;
1007
1256
  });
1008
- setBoundedCacheEntry(routeSourceAnalysisCache, cacheKey, pending, maxRouteSourceAnalysisCacheEntries);
1257
+ setBoundedCacheEntry(routeSourceAnalysisCache, cacheKey, pending, maxRouteSourceAnalysisCacheEntries, routeSourceAnalysisCacheCounters);
1009
1258
  return pending;
1010
1259
  }
1011
1260
  async function analyzeRouteSourceUncached(options) {
@@ -1162,7 +1411,7 @@ async function mayRenderOutOfOrderBoundaryDeepInner(options, seen) {
1162
1411
  seen.add(options.filename);
1163
1412
  const sourceHash = memoizedHashText(options.code);
1164
1413
  const cacheKey = `${options.serverModuleCacheVersion ?? "dev"}\0${options.filename}\0${sourceHash}`;
1165
- const cached = routeOutOfOrderBoundaryAnalysisCache.get(cacheKey);
1414
+ const cached = readRouterRuntimeCacheEntry(routeOutOfOrderBoundaryAnalysisCache, cacheKey, routeOutOfOrderBoundaryAnalysisCacheCounters);
1166
1415
  if (cached !== undefined) {
1167
1416
  return cached;
1168
1417
  }
@@ -1170,7 +1419,7 @@ async function mayRenderOutOfOrderBoundaryDeepInner(options, seen) {
1170
1419
  routeOutOfOrderBoundaryAnalysisCache.delete(cacheKey);
1171
1420
  throw error;
1172
1421
  });
1173
- setBoundedCacheEntry(routeOutOfOrderBoundaryAnalysisCache, cacheKey, pending, maxRouteOutOfOrderBoundaryAnalysisCacheEntries);
1422
+ setBoundedCacheEntry(routeOutOfOrderBoundaryAnalysisCache, cacheKey, pending, maxRouteOutOfOrderBoundaryAnalysisCacheEntries, routeOutOfOrderBoundaryAnalysisCacheCounters);
1174
1423
  return pending;
1175
1424
  }
1176
1425
  async function mayRenderImportedOutOfOrderBoundary(options, seen) {
@@ -1394,7 +1643,7 @@ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, server
1394
1643
  ? undefined
1395
1644
  : `${appDir}\0${shell.file}\0${serverModuleCacheVersion}`;
1396
1645
  if (cacheKey !== undefined) {
1397
- const cached = renderedShellCache.get(cacheKey);
1646
+ const cached = readRouterRuntimeCacheEntry(renderedShellCache, cacheKey, renderedShellCacheCounters);
1398
1647
  if (cached !== undefined && cached !== "impure") {
1399
1648
  return cached;
1400
1649
  }
@@ -1412,7 +1661,9 @@ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, server
1412
1661
  }
1413
1662
  const component = await loadServerComponent(output.code, shell.file, serverModules, serverModuleCacheVersion);
1414
1663
  const rendered = splitLayoutSlot(markShellBoundary(await component(props), shell), slotContext);
1415
- const cached = cacheKey !== undefined ? renderedShellCache.get(cacheKey) : undefined;
1664
+ const cached = cacheKey !== undefined
1665
+ ? readRouterRuntimeCacheEntry(renderedShellCache, cacheKey, renderedShellCacheCounters)
1666
+ : undefined;
1416
1667
  // Detect purity: a zero-arg component cannot depend on props. The
1417
1668
  // markShellBoundary + splitLayoutSlot output is then constant for
1418
1669
  // the (appDir, shellFile, version) tuple. We only set the cache
@@ -1424,6 +1675,7 @@ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, server
1424
1675
  const oldestKey = renderedShellCache.keys().next().value;
1425
1676
  if (oldestKey !== undefined) {
1426
1677
  renderedShellCache.delete(oldestKey);
1678
+ renderedShellCacheCounters.evictions += 1;
1427
1679
  }
1428
1680
  }
1429
1681
  renderedShellCache.set(cacheKey, rendered);
@@ -1460,6 +1712,11 @@ function splitLayoutSlot(layoutHtml, slotContext = createSlotRenderContext()) {
1460
1712
  // next request.
1461
1713
  const shellFilesCache = new Map();
1462
1714
  const MAX_SHELL_FILES_CACHE_ENTRIES = 1024;
1715
+ const routeMiddlewareControlCache = new Map();
1716
+ const MAX_ROUTE_MIDDLEWARE_CONTROL_CACHE_ENTRIES = 1024;
1717
+ const appMiddlewareFileCache = new Map();
1718
+ const MAX_APP_MIDDLEWARE_FILE_CACHE_ENTRIES = 1024;
1719
+ const routeMiddlewareControlSourceCache = new Map();
1463
1720
  async function shellFilesForPage(appDir, pageFile, serverModuleCacheVersion) {
1464
1721
  const cacheKey = serverModuleCacheVersion === undefined
1465
1722
  ? undefined
@@ -1518,6 +1775,144 @@ function shellBoundaryId(appDir, directory) {
1518
1775
  ? "root"
1519
1776
  : relativeDirectory.replaceAll(sep, "/").replace(/[^A-Za-z0-9_$/-]/g, "_");
1520
1777
  }
1778
+ async function hasAppMiddleware(options) {
1779
+ const middlewareFiles = [
1780
+ join(options.appDir, "middleware.ts"),
1781
+ join(options.appDir, "middleware.mreact.ts"),
1782
+ ];
1783
+ if (options.serverSourceFiles !== undefined) {
1784
+ return middlewareFiles.some((file) => options.serverSourceFiles?.has(file) === true);
1785
+ }
1786
+ const cacheKey = options.serverModuleCacheVersion === undefined
1787
+ ? undefined
1788
+ : `${options.appDir}\0${options.serverModuleCacheVersion}`;
1789
+ if (cacheKey !== undefined) {
1790
+ const cached = appMiddlewareFileCache.get(cacheKey);
1791
+ if (cached !== undefined) {
1792
+ return cached;
1793
+ }
1794
+ }
1795
+ const loaded = hasAppMiddlewareUncached(middlewareFiles).catch((error) => {
1796
+ if (cacheKey !== undefined) {
1797
+ appMiddlewareFileCache.delete(cacheKey);
1798
+ }
1799
+ throw error;
1800
+ });
1801
+ if (cacheKey !== undefined) {
1802
+ if (appMiddlewareFileCache.size >= MAX_APP_MIDDLEWARE_FILE_CACHE_ENTRIES) {
1803
+ const oldestKey = appMiddlewareFileCache.keys().next().value;
1804
+ if (oldestKey !== undefined) {
1805
+ appMiddlewareFileCache.delete(oldestKey);
1806
+ }
1807
+ }
1808
+ appMiddlewareFileCache.set(cacheKey, loaded);
1809
+ }
1810
+ return loaded;
1811
+ }
1812
+ async function hasAppMiddlewareUncached(files) {
1813
+ for (const file of files) {
1814
+ try {
1815
+ await access(file);
1816
+ return true;
1817
+ }
1818
+ catch {
1819
+ // Missing middleware files are allowed.
1820
+ }
1821
+ }
1822
+ return false;
1823
+ }
1824
+ async function loadRouteMiddlewareControl(options) {
1825
+ const cacheKey = options.serverModuleCacheVersion === undefined
1826
+ ? undefined
1827
+ : `${options.appDir}\0${options.pageFile}\0${options.serverModuleCacheVersion}`;
1828
+ if (cacheKey !== undefined) {
1829
+ const cached = routeMiddlewareControlCache.get(cacheKey);
1830
+ if (cached !== undefined) {
1831
+ return cached;
1832
+ }
1833
+ }
1834
+ const loaded = loadRouteMiddlewareControlUncached(options).catch((error) => {
1835
+ if (cacheKey !== undefined) {
1836
+ routeMiddlewareControlCache.delete(cacheKey);
1837
+ }
1838
+ throw error;
1839
+ });
1840
+ if (cacheKey !== undefined) {
1841
+ if (routeMiddlewareControlCache.size >= MAX_ROUTE_MIDDLEWARE_CONTROL_CACHE_ENTRIES) {
1842
+ const oldestKey = routeMiddlewareControlCache.keys().next().value;
1843
+ if (oldestKey !== undefined) {
1844
+ routeMiddlewareControlCache.delete(oldestKey);
1845
+ }
1846
+ }
1847
+ routeMiddlewareControlCache.set(cacheKey, loaded);
1848
+ }
1849
+ return loaded;
1850
+ }
1851
+ async function loadRouteMiddlewareControlUncached(options) {
1852
+ const controls = [];
1853
+ const shellFiles = await shellFilesForPage(options.appDir, options.pageFile, options.serverModuleCacheVersion);
1854
+ for (const shell of shellFiles) {
1855
+ if (shell.kind !== "layout") {
1856
+ continue;
1857
+ }
1858
+ controls.push(await loadRouteMiddlewareControlFile({
1859
+ file: shell.file,
1860
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1861
+ serverSourceFiles: options.serverSourceFiles,
1862
+ }));
1863
+ }
1864
+ controls.push(await loadRouteMiddlewareControlFile({
1865
+ file: options.pageFile,
1866
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1867
+ serverSourceFiles: options.serverSourceFiles,
1868
+ }));
1869
+ return mergeRouteMiddlewareControls(controls);
1870
+ }
1871
+ async function loadRouteMiddlewareControlFile(options) {
1872
+ if (options.serverSourceFiles !== undefined || options.serverModuleCacheVersion !== undefined) {
1873
+ return parseRouteMiddlewareControl(await readServerSourceFile(options.file, options.serverModuleCacheVersion, options.serverSourceFiles));
1874
+ }
1875
+ const fileStat = await stat(options.file);
1876
+ const cached = routeMiddlewareControlSourceCache.get(options.file);
1877
+ if (cached !== undefined && cached.mtimeMs === fileStat.mtimeMs) {
1878
+ return cached.control;
1879
+ }
1880
+ const control = parseRouteMiddlewareControl(await readFile(options.file, "utf8"));
1881
+ routeMiddlewareControlSourceCache.set(options.file, {
1882
+ control,
1883
+ mtimeMs: fileStat.mtimeMs,
1884
+ });
1885
+ return control;
1886
+ }
1887
+ function parseRouteMiddlewareControl(code) {
1888
+ if (!/\bexport\s+const\s+middleware\s*=/.test(code)) {
1889
+ return undefined;
1890
+ }
1891
+ if (/\bmiddleware\s*=\s*\{[\s\S]*?\bskip\s*:\s*true\b/.test(code)) {
1892
+ return { skip: true };
1893
+ }
1894
+ const skipArray = /\bmiddleware\s*=\s*\{[\s\S]*?\bskip\s*:\s*\[([\s\S]*?)\]/.exec(code);
1895
+ if (skipArray === null) {
1896
+ return undefined;
1897
+ }
1898
+ const ids = Array.from(skipArray[1]?.matchAll(/["']([^"']+)["']/g) ?? [], (match) => match[1])
1899
+ .filter((id) => id !== undefined);
1900
+ return ids.length === 0 ? undefined : { skip: ids };
1901
+ }
1902
+ function mergeRouteMiddlewareControls(controls) {
1903
+ const skippedIds = new Set();
1904
+ for (const control of controls) {
1905
+ if (control?.skip === true) {
1906
+ return { skip: true };
1907
+ }
1908
+ if (Array.isArray(control?.skip)) {
1909
+ for (const id of control.skip) {
1910
+ skippedIds.add(id);
1911
+ }
1912
+ }
1913
+ }
1914
+ return skippedIds.size === 0 ? undefined : { skip: [...skippedIds] };
1915
+ }
1521
1916
  function markShellBoundary(html, shell) {
1522
1917
  const attributeName = shell.kind === "layout" ? "data-mreact-layout-boundary" : "data-mreact-template-boundary";
1523
1918
  if (html.includes(`${attributeName}=`)) {
@@ -1588,12 +1983,36 @@ async function loadRouteData(options) {
1588
1983
  const module = await loadRouteLoaderModule(options);
1589
1984
  return module.loader === undefined ? undefined : await module.loader(options.context);
1590
1985
  }
1986
+ async function loadRouteDataWithInstrumentation(options) {
1987
+ const trace = traceContextFromRequest(options.request);
1988
+ const event = {
1989
+ method: options.request.method,
1990
+ path: new URL(options.request.url).pathname,
1991
+ request: options.request,
1992
+ routeId: options.routeId,
1993
+ routePath: options.routePath,
1994
+ ...(trace === undefined ? {} : { trace }),
1995
+ };
1996
+ invokeRouterInstrumentation(options.instrumentation?.onLoaderStart, event);
1997
+ try {
1998
+ const data = await loadRouteData(options);
1999
+ invokeRouterInstrumentation(options.instrumentation?.onLoaderEnd, event);
2000
+ return data;
2001
+ }
2002
+ catch (error) {
2003
+ invokeRouterInstrumentation(options.instrumentation?.onLoaderEnd, {
2004
+ ...event,
2005
+ error,
2006
+ });
2007
+ throw error;
2008
+ }
2009
+ }
1591
2010
  async function loadRouteLoaderModule(options) {
1592
2011
  const cacheKey = options.serverModuleCacheVersion === undefined
1593
2012
  ? undefined
1594
2013
  : `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}\0${importPolicyCacheKey(options.importPolicy)}`;
1595
2014
  if (cacheKey !== undefined) {
1596
- const cached = routeLoaderModuleCache.get(cacheKey);
2015
+ const cached = readRouterRuntimeCacheEntry(routeLoaderModuleCache, cacheKey, routeLoaderModuleCacheCounters);
1597
2016
  if (cached !== undefined) {
1598
2017
  return cached;
1599
2018
  }
@@ -1608,7 +2027,7 @@ async function loadRouteLoaderModule(options) {
1608
2027
  throw error;
1609
2028
  });
1610
2029
  if (cacheKey !== undefined) {
1611
- setBoundedCacheEntry(routeLoaderModuleCache, cacheKey, loaded, maxRouteLoaderModuleCacheEntries);
2030
+ setBoundedCacheEntry(routeLoaderModuleCache, cacheKey, loaded, maxRouteLoaderModuleCacheEntries, routeLoaderModuleCacheCounters);
1612
2031
  }
1613
2032
  return loaded;
1614
2033
  }
@@ -1728,7 +2147,7 @@ async function loadComposedRouteMetadata(options) {
1728
2147
  ? undefined
1729
2148
  : `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}`;
1730
2149
  if (cacheKey !== undefined) {
1731
- const cached = composedRouteMetadataCache.get(cacheKey);
2150
+ const cached = readRouterRuntimeCacheEntry(composedRouteMetadataCache, cacheKey, composedRouteMetadataCacheCounters);
1732
2151
  if (cached !== undefined) {
1733
2152
  return cached;
1734
2153
  }
@@ -1740,7 +2159,7 @@ async function loadComposedRouteMetadata(options) {
1740
2159
  throw error;
1741
2160
  });
1742
2161
  if (cacheKey !== undefined) {
1743
- setBoundedCacheEntry(composedRouteMetadataCache, cacheKey, loaded, maxComposedRouteMetadataCacheEntries);
2162
+ setBoundedCacheEntry(composedRouteMetadataCache, cacheKey, loaded, maxComposedRouteMetadataCacheEntries, composedRouteMetadataCacheCounters);
1744
2163
  }
1745
2164
  return loaded;
1746
2165
  }
@@ -1825,8 +2244,22 @@ function mergeReadonlyArrays(left, right) {
1825
2244
  return [...left, ...right];
1826
2245
  }
1827
2246
  function mergeCspMetadata(left, right) {
2247
+ if (right?.disable === true) {
2248
+ return { disable: true };
2249
+ }
1828
2250
  if (left === undefined) {
1829
- return right;
2251
+ if (right === undefined) {
2252
+ return undefined;
2253
+ }
2254
+ const merged = { ...right };
2255
+ const directives = applyCspOverrides(undefined, right);
2256
+ if (directives !== undefined) {
2257
+ merged.directives = directives;
2258
+ }
2259
+ else {
2260
+ delete merged.directives;
2261
+ }
2262
+ return merged;
1830
2263
  }
1831
2264
  if (right === undefined) {
1832
2265
  return left;
@@ -1835,12 +2268,28 @@ function mergeCspMetadata(left, right) {
1835
2268
  ...left,
1836
2269
  ...right,
1837
2270
  };
1838
- const directives = mergeObject(left.directives, right.directives);
2271
+ const directives = applyCspOverrides(left.directives, right);
1839
2272
  if (directives !== undefined) {
1840
2273
  merged.directives = directives;
1841
2274
  }
2275
+ else {
2276
+ delete merged.directives;
2277
+ }
1842
2278
  return merged;
1843
2279
  }
2280
+ function applyCspOverrides(left, right) {
2281
+ if (right === undefined) {
2282
+ return left;
2283
+ }
2284
+ const merged = { ...left, ...right.directives };
2285
+ for (const [name, value] of Object.entries(right.replace ?? {})) {
2286
+ merged[name] = value;
2287
+ }
2288
+ for (const name of right.remove ?? []) {
2289
+ delete merged[name];
2290
+ }
2291
+ return Object.keys(merged).length === 0 ? undefined : merged;
2292
+ }
1844
2293
  function mergeOpenGraphMetadata(left, right) {
1845
2294
  if (left === undefined) {
1846
2295
  return right;
@@ -1913,13 +2362,19 @@ function injectHeadMetadata(html, metadata) {
1913
2362
  }
1914
2363
  return `<head>${tags}</head>${html}`;
1915
2364
  }
1916
- function responseHeadersForMetadata(metadata) {
1917
- const headers = new Headers({ "content-type": "text/html; charset=utf-8" });
2365
+ const DEFAULT_HTML_RESPONSE_HEADERS = Object.freeze({
2366
+ "content-type": "text/html; charset=utf-8",
2367
+ });
2368
+ function responseHeadersForMetadata(metadata, extra) {
1918
2369
  const csp = contentSecurityPolicy(metadata?.csp);
1919
- if (csp !== undefined) {
1920
- headers.set("content-security-policy", csp);
2370
+ if (csp === undefined && extra === undefined) {
2371
+ return DEFAULT_HTML_RESPONSE_HEADERS;
1921
2372
  }
1922
- return headers;
2373
+ return {
2374
+ ...DEFAULT_HTML_RESPONSE_HEADERS,
2375
+ ...(csp === undefined ? undefined : { "content-security-policy": csp }),
2376
+ ...extra,
2377
+ };
1923
2378
  }
1924
2379
  function injectQueryState(html, state) {
1925
2380
  if (state.queries.length === 0) {
@@ -2054,7 +2509,7 @@ function readServerSourceFile(file, serverModuleCacheVersion, serverSourceFiles)
2054
2509
  return readFile(file, "utf8");
2055
2510
  }
2056
2511
  const key = `${serverModuleCacheVersion}:${file}`;
2057
- const cached = serverSourceFileCache.get(key);
2512
+ const cached = readRouterRuntimeCacheEntry(serverSourceFileCache, key, serverSourceFileCacheCounters);
2058
2513
  if (cached !== undefined) {
2059
2514
  return cached;
2060
2515
  }
@@ -2062,17 +2517,20 @@ function readServerSourceFile(file, serverModuleCacheVersion, serverSourceFiles)
2062
2517
  serverSourceFileCache.delete(key);
2063
2518
  throw error;
2064
2519
  });
2065
- setBoundedCacheEntry(serverSourceFileCache, key, loaded, maxServerSourceFileCacheEntries);
2520
+ setBoundedCacheEntry(serverSourceFileCache, key, loaded, maxServerSourceFileCacheEntries, serverSourceFileCacheCounters);
2066
2521
  return loaded;
2067
2522
  }
2068
2523
  function hashText(text) {
2069
2524
  return createHash("sha256").update(text).digest("hex").slice(0, 16);
2070
2525
  }
2071
- function setBoundedCacheEntry(cache, key, value, maxEntries) {
2526
+ function setBoundedCacheEntry(cache, key, value, maxEntries, counters) {
2072
2527
  if (cache.size >= maxEntries) {
2073
2528
  const oldestKey = cache.keys().next().value;
2074
2529
  if (oldestKey !== undefined) {
2075
2530
  cache.delete(oldestKey);
2531
+ if (counters !== undefined) {
2532
+ counters.evictions += 1;
2533
+ }
2076
2534
  }
2077
2535
  }
2078
2536
  cache.set(key, value);