@reckona/mreact-router 0.0.15 → 0.0.17

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 +72 -5
  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 +50 -9
  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 +35 -12
  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 +26 -1
  53. package/dist/render.d.ts.map +1 -1
  54. package/dist/render.js +530 -60
  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 +72 -12
  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,38 +190,113 @@ 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
+ }
239
+ export async function resolveAppRouterMiddleware(options) {
240
+ const middlewareResponse = await runMiddleware(options);
241
+ if (middlewareResponse === undefined) {
242
+ return { request: options.request, type: "continue" };
243
+ }
244
+ const location = rewriteLocation(middlewareResponse);
245
+ if (location !== undefined) {
246
+ return {
247
+ request: new Request(new URL(location, options.request.url), options.request),
248
+ type: "continue",
249
+ };
250
+ }
251
+ return { response: middlewareResponse, type: "response" };
252
+ }
104
253
  async function renderAppRequestInternal(options) {
254
+ const timing = createRenderTiming(options.logger);
255
+ let phaseStartedAt = renderTimingPhaseStartedAt(timing);
105
256
  const routes = options.routes ?? (await scanAppRoutes({ appDir: options.appDir }));
257
+ finishRenderTimingPhase(timing, phaseStartedAt, "routeScanMs");
106
258
  const url = new URL(options.request.url);
107
- const middlewareResponse = options.skipMiddleware === true
108
- ? undefined
109
- : await runMiddleware({
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
279
+ ? { request: options.request, type: "continue" }
280
+ : await resolveAppRouterMiddleware({
110
281
  appDir: options.appDir,
111
282
  importPolicy: options.importPolicy,
283
+ instrumentation: options.instrumentation,
284
+ middlewareControl,
112
285
  request: options.request,
113
286
  serverModules: options.serverModules,
114
287
  serverModuleCacheVersion: options.serverModuleCacheVersion,
115
288
  serverSourceFiles: options.serverSourceFiles,
116
289
  });
117
- if (middlewareResponse !== undefined) {
118
- const location = rewriteLocation(middlewareResponse);
119
- if (location !== undefined) {
120
- const rewriteUrl = new URL(location, options.request.url);
121
- return renderAppRequestInternal({
122
- ...options,
123
- request: new Request(rewriteUrl, options.request),
124
- skipMiddleware: true,
125
- });
126
- }
127
- return middlewareResponse;
290
+ finishRenderTimingPhase(timing, phaseStartedAt, "middlewareMs");
291
+ if (middlewareResult.type === "response") {
292
+ return middlewareResult.response;
293
+ }
294
+ if (middlewareResult.request !== options.request) {
295
+ return renderAppRequestInternal({
296
+ ...options,
297
+ request: middlewareResult.request,
298
+ skipMiddleware: true,
299
+ });
128
300
  }
129
301
  if (url.pathname === "/_mreact/actions") {
130
302
  return dispatchServerActionRequest({
@@ -138,7 +310,6 @@ async function renderAppRequestInternal(options) {
138
310
  serverActions: options.serverActions,
139
311
  });
140
312
  }
141
- const matched = options.routeMatcher?.match(url.pathname) ?? matchRoute(routes, url.pathname);
142
313
  if (matched === undefined) {
143
314
  const notFoundFile = await nearestBoundaryFileForPath({
144
315
  appDir: options.appDir,
@@ -150,6 +321,7 @@ async function renderAppRequestInternal(options) {
150
321
  assetBaseUrl: options.assetBaseUrl,
151
322
  error: undefined,
152
323
  request: options.request,
324
+ routePath: url.pathname,
153
325
  routeFile: notFoundFile,
154
326
  routeScripts: options.clientScripts,
155
327
  serverModules: options.serverModules,
@@ -192,35 +364,44 @@ async function renderAppRequestInternal(options) {
192
364
  }
193
365
  routeCacheContext = beginRouteCacheContext(options.routeCache);
194
366
  const clientScript = options.clientScripts?.get(matched.route.path);
367
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
195
368
  const originalCode = await readServerSourceFile(matched.route.file, options.serverModuleCacheVersion, options.serverSourceFiles);
369
+ finishRenderTimingPhase(timing, phaseStartedAt, "readSourceMs");
370
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
196
371
  const originalAnalysis = await analyzeRouteSource({
197
372
  code: originalCode,
198
373
  filename: matched.route.file,
199
374
  routePath: matched.route.path,
200
375
  serverModuleCacheVersion: options.serverModuleCacheVersion,
201
376
  });
377
+ finishRenderTimingPhase(timing, phaseStartedAt, "sourceAnalysisMs");
202
378
  const cachePolicy = originalAnalysis.cachePolicy;
203
379
  const navigationScript = options.navigationScripts?.get(matched.route.path);
204
380
  const cacheKey = routeCacheKey(options.appDir, matched.route.path, url);
205
381
  const mayUseRouteCache = cachePolicy === undefined
206
382
  ? originalAnalysis.usesRuntimeCacheControl
207
383
  : cachePolicy.revalidateSeconds !== 0;
384
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
208
385
  const cachedResponse = !mayUseRouteCache
209
386
  ? undefined
210
387
  : await cachedRouteResponse({
211
388
  cache: options.routeCache,
212
389
  key: cacheKey,
213
390
  });
391
+ finishRenderTimingPhase(timing, phaseStartedAt, "routeCacheMs");
214
392
  if (cachedResponse !== undefined) {
215
393
  return cachedResponse;
216
394
  }
395
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
217
396
  const preparedActions = await prepareRouteServerActions({
218
397
  appDir: options.appDir,
219
398
  code: originalCode,
220
399
  pageFile: matched.route.file,
221
400
  request: options.request,
222
401
  });
402
+ finishRenderTimingPhase(timing, phaseStartedAt, "serverActionsMs");
223
403
  const code = preparedActions.code;
404
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
224
405
  const routeAnalysis = code === originalCode
225
406
  ? originalAnalysis
226
407
  : await analyzeRouteSource({
@@ -229,25 +410,32 @@ async function renderAppRequestInternal(options) {
229
410
  routePath: matched.route.path,
230
411
  serverModuleCacheVersion: undefined,
231
412
  });
413
+ finishRenderTimingPhase(timing, phaseStartedAt, "routeCodeAnalysisMs");
232
414
  const routeCode = routeAnalysis.routeCode;
233
415
  const streamRoute = routeAnalysis.streamRoute;
234
416
  const clientInference = routeAnalysis.clientInference;
235
417
  const clientRoute = clientInference.client;
418
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
236
419
  const dataPromise = routeAnalysis.hasLoader
237
- ? loadRouteData({
420
+ ? loadRouteDataWithInstrumentation({
421
+ appDir: options.appDir,
238
422
  code,
239
423
  context: {
240
424
  params: matched.params,
241
425
  queryClient,
242
426
  request: options.request,
243
427
  },
244
- appDir: options.appDir,
245
428
  filename: matched.route.file,
246
429
  importPolicy: options.importPolicy,
430
+ instrumentation: options.instrumentation,
431
+ request: options.request,
432
+ routeId: routeIdForPath(matched.route.path),
433
+ routePath: matched.route.path,
247
434
  serverModules: options.serverModules,
248
435
  serverModuleCacheVersion: options.serverModuleCacheVersion,
249
436
  })
250
437
  : undefined;
438
+ finishRenderTimingPhase(timing, phaseStartedAt, "loaderStartMs");
251
439
  recoveryRoute = {
252
440
  clientRoute,
253
441
  props: {
@@ -258,22 +446,28 @@ async function renderAppRequestInternal(options) {
258
446
  script: clientScript,
259
447
  };
260
448
  if (streamRoute) {
449
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
261
450
  const loadingFile = await nearestExistingBoundaryFileForPage({
262
451
  appDir: options.appDir,
263
452
  filename: "loading.mreact.tsx",
264
453
  pageFile: matched.route.file,
454
+ serverSourceFiles: options.serverSourceFiles,
265
455
  });
456
+ finishRenderTimingPhase(timing, phaseStartedAt, "loadingBoundaryLookupMs");
266
457
  const streamShellResponseHeaders = {
267
458
  "content-type": "text/html; charset=utf-8",
268
459
  "x-mreact-stream": "1",
269
460
  };
461
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
270
462
  const mayRenderOutOfOrder = await mayRenderOutOfOrderBoundaryDeep({
271
463
  code: routeCode,
272
464
  filename: matched.route.file,
273
465
  serverModuleCacheVersion: options.serverModuleCacheVersion,
274
466
  serverSourceFiles: options.serverSourceFiles,
275
467
  });
468
+ finishRenderTimingPhase(timing, phaseStartedAt, "outOfOrderAnalysisMs");
276
469
  if (loadingFile === undefined && !mayRenderOutOfOrder) {
470
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
277
471
  const stringOutput = transformServerModule({
278
472
  code: routeCode,
279
473
  clientBoundaryImports: clientInference.clientBoundaryImports,
@@ -281,6 +475,7 @@ async function renderAppRequestInternal(options) {
281
475
  serverModules: options.serverModules,
282
476
  serverOutput: "string",
283
477
  });
478
+ finishRenderTimingPhase(timing, phaseStartedAt, "stringTransformMs");
284
479
  const stringFatalDiagnostics = stringOutput.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
285
480
  if (stringFatalDiagnostics.length > 0) {
286
481
  return new Response(stringFatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"), {
@@ -345,15 +540,20 @@ async function renderAppRequestInternal(options) {
345
540
  html = injectHeadMetadata(html, metadata);
346
541
  html = injectAuthSessionClaims(html, originalAnalysis.authIncludesClaims ? currentAuthClaims() : undefined);
347
542
  html = injectQueryState(html, dehydrate(queryClient));
348
- const headers = new Headers(responseHeadersForMetadata(metadata));
349
- headers.set("x-mreact-stream", "1");
350
- return withOptionalActionCookie(htmlResponse(`<!DOCTYPE html>${clientNavigationHeadTags({
543
+ const response = withOptionalActionCookie(htmlResponse(`<!DOCTYPE html>${clientNavigationHeadTags({
351
544
  assetBaseUrl: options.assetBaseUrl,
352
545
  currentScript: clientRoute ? clientScript : undefined,
353
546
  currentNavigationScript: clientRoute ? undefined : navigationScript,
354
547
  routeScripts: options.clientScripts,
355
- })}${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;
356
555
  }
556
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
357
557
  const output = transformServerModule({
358
558
  code: routeCode,
359
559
  clientBoundaryImports: clientInference.clientBoundaryImports,
@@ -362,6 +562,7 @@ async function renderAppRequestInternal(options) {
362
562
  serverOutput: "stream",
363
563
  serverAwaitHydration: clientRoute,
364
564
  });
565
+ finishRenderTimingPhase(timing, phaseStartedAt, "streamTransformMs");
365
566
  const fatalDiagnostics = output.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER");
366
567
  if (fatalDiagnostics.length > 0) {
367
568
  return new Response(fatalDiagnostics.map((diagnostic) => diagnostic.message).join("\n"), {
@@ -370,6 +571,7 @@ async function renderAppRequestInternal(options) {
370
571
  });
371
572
  }
372
573
  if (loadingFile !== undefined) {
574
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
373
575
  const stream = await runServerStreamModuleWithLoading(output.code, {
374
576
  appDir: options.appDir,
375
577
  assetBaseUrl: options.assetBaseUrl,
@@ -388,9 +590,12 @@ async function renderAppRequestInternal(options) {
388
590
  script: clientScript,
389
591
  clientReferenceManifest: output.metadata.clientReferenceManifest,
390
592
  });
391
- return withOptionalActionCookie(new Response(stream, {
593
+ finishRenderTimingPhase(timing, phaseStartedAt, "streamConstructionMs");
594
+ const response = withOptionalActionCookie(new Response(stream, {
392
595
  headers: streamShellResponseHeaders,
393
596
  }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
597
+ emitRenderTiming(options, timing, response.status);
598
+ return response;
394
599
  }
395
600
  const data = dataPromise === undefined ? undefined : await dataPromise;
396
601
  if (data instanceof Response) {
@@ -402,6 +607,7 @@ async function renderAppRequestInternal(options) {
402
607
  queryClient,
403
608
  request: options.request,
404
609
  };
610
+ phaseStartedAt = renderTimingPhaseStartedAt(timing);
405
611
  const stream = runServerStreamModule(output.code, {
406
612
  appDir: options.appDir,
407
613
  assetBaseUrl: options.assetBaseUrl,
@@ -416,9 +622,12 @@ async function renderAppRequestInternal(options) {
416
622
  script: clientScript,
417
623
  clientReferenceManifest: output.metadata.clientReferenceManifest,
418
624
  });
419
- return withOptionalActionCookie(new Response(stream, {
625
+ finishRenderTimingPhase(timing, phaseStartedAt, "streamConstructionMs");
626
+ const response = withOptionalActionCookie(new Response(stream, {
420
627
  headers: streamShellResponseHeaders,
421
628
  }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
629
+ emitRenderTiming(options, timing, response.status);
630
+ return response;
422
631
  }
423
632
  const output = transformServerModule({
424
633
  code: routeCode,
@@ -533,6 +742,7 @@ async function renderAppRequestInternal(options) {
533
742
  assetBaseUrl: options.assetBaseUrl,
534
743
  error: undefined,
535
744
  request: options.request,
745
+ routePath: matched.route.path,
536
746
  routeFile: notFoundFile,
537
747
  routeScripts: options.clientScripts,
538
748
  serverModules: options.serverModules,
@@ -553,6 +763,7 @@ async function renderAppRequestInternal(options) {
553
763
  assetBaseUrl: options.assetBaseUrl,
554
764
  error,
555
765
  request: options.request,
766
+ routePath: matched.route.path,
556
767
  routeFile: errorFile,
557
768
  routeScripts: options.clientScripts,
558
769
  serverModules: options.serverModules,
@@ -629,6 +840,7 @@ async function nearestExistingBoundaryFileForPage(options) {
629
840
  appDir: options.appDir,
630
841
  filename: options.filename,
631
842
  parts,
843
+ serverSourceFiles: options.serverSourceFiles,
632
844
  });
633
845
  }
634
846
  async function nearestBoundaryFileForPath(options) {
@@ -661,6 +873,12 @@ async function nearestExistingBoundaryFileFromParts(options) {
661
873
  for (let count = options.parts.length; count >= 0; count -= 1) {
662
874
  for (const filename of boundaryFilenameCandidates(options.filename)) {
663
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
+ }
664
882
  try {
665
883
  await access(candidate);
666
884
  return candidate;
@@ -688,10 +906,14 @@ async function renderSpecialRoute(options) {
688
906
  }
689
907
  const props = {
690
908
  data: undefined,
909
+ debug: errorDebugContext(options.error, options.routePath),
691
910
  error: normalizeErrorForProps(options.error),
692
911
  params: {},
693
912
  queryClient: createQueryClient(),
694
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,
695
917
  };
696
918
  const pageHtml = await renderServerFileToHtml(options.routeFile, props, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles);
697
919
  const pageHtmlForLayout = options.navigation?.clientRoute === true
@@ -742,6 +964,19 @@ function normalizeErrorForProps(error) {
742
964
  }
743
965
  return { message: String(error) };
744
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
+ }
745
980
  async function dispatchServerRoute(options) {
746
981
  const module = await loadServerRouteModule(options);
747
982
  const handler = module[options.request.method] ?? module.ALL ?? module.default;
@@ -771,7 +1006,7 @@ async function loadServerRouteModule(options) {
771
1006
  const codeHash = memoizedHashText(code);
772
1007
  const moduleCode = artifactCode !== undefined && artifactCode.sourceHash === codeHash ? artifactCode.code : code;
773
1008
  const cacheKey = `server-route\0${options.file}\0${options.serverModuleCacheVersion}\0${codeHash}\0${memoizedHashText(moduleCode)}`;
774
- const cached = serverRouteModuleCache.get(cacheKey);
1009
+ const cached = readRouterRuntimeCacheEntry(serverRouteModuleCache, cacheKey, serverRouteModuleCacheCounters);
775
1010
  if (cached !== undefined) {
776
1011
  return cached;
777
1012
  }
@@ -785,7 +1020,7 @@ async function loadServerRouteModule(options) {
785
1020
  serverRouteModuleCache.delete(cacheKey);
786
1021
  throw error;
787
1022
  });
788
- setBoundedCacheEntry(serverRouteModuleCache, cacheKey, loaded, maxServerRouteModuleCacheEntries);
1023
+ setBoundedCacheEntry(serverRouteModuleCache, cacheKey, loaded, maxServerRouteModuleCacheEntries, serverRouteModuleCacheCounters);
789
1024
  return loaded;
790
1025
  }
791
1026
  async function runMiddleware(options) {
@@ -808,6 +1043,9 @@ async function runMiddleware(options) {
808
1043
  serverModuleCacheVersion: options.serverModuleCacheVersion,
809
1044
  serverSourceFiles: options.serverSourceFiles,
810
1045
  });
1046
+ if (shouldSkipMiddleware(module.config, options.middlewareControl)) {
1047
+ return undefined;
1048
+ }
811
1049
  if (!middlewareMatches(module.config, new URL(options.request.url).pathname)) {
812
1050
  return undefined;
813
1051
  }
@@ -815,11 +1053,25 @@ async function runMiddleware(options) {
815
1053
  if (typeof middleware !== "function") {
816
1054
  return undefined;
817
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);
818
1065
  try {
819
1066
  const response = await middleware(options.request);
1067
+ invokeRouterInstrumentation(options.instrumentation?.onMiddlewareEnd, event);
820
1068
  return response instanceof Response ? response : undefined;
821
1069
  }
822
1070
  catch (error) {
1071
+ invokeRouterInstrumentation(options.instrumentation?.onMiddlewareEnd, {
1072
+ ...event,
1073
+ error,
1074
+ });
823
1075
  if (isRedirectError(error)) {
824
1076
  return new Response(null, {
825
1077
  headers: { location: error.location },
@@ -834,13 +1086,22 @@ async function runMiddleware(options) {
834
1086
  }
835
1087
  return undefined;
836
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
+ }
837
1098
  async function loadMiddlewareModule(options) {
838
1099
  const code = await readServerSourceFile(options.file, options.serverModuleCacheVersion, options.serverSourceFiles);
839
1100
  const cacheKey = options.serverModuleCacheVersion === undefined
840
1101
  ? undefined
841
1102
  : `middleware\0${options.appDir}\0${options.file}\0${options.serverModuleCacheVersion}\0${memoizedHashText(code)}\0${importPolicyCacheKey(options.importPolicy)}`;
842
1103
  if (cacheKey !== undefined) {
843
- const cached = middlewareModuleCache.get(cacheKey);
1104
+ const cached = readRouterRuntimeCacheEntry(middlewareModuleCache, cacheKey, middlewareModuleCacheCounters);
844
1105
  if (cached !== undefined) {
845
1106
  return cached;
846
1107
  }
@@ -859,7 +1120,7 @@ async function loadMiddlewareModule(options) {
859
1120
  throw error;
860
1121
  });
861
1122
  if (cacheKey !== undefined) {
862
- setBoundedCacheEntry(middlewareModuleCache, cacheKey, loaded, maxMiddlewareModuleCacheEntries);
1123
+ setBoundedCacheEntry(middlewareModuleCache, cacheKey, loaded, maxMiddlewareModuleCacheEntries, middlewareModuleCacheCounters);
863
1124
  }
864
1125
  return loaded;
865
1126
  }
@@ -963,7 +1224,7 @@ function transformServerModule(options) {
963
1224
  }
964
1225
  const awaitHydrationKey = options.serverAwaitHydration === true ? "1" : "0";
965
1226
  const key = `${options.filename}\0${options.serverOutput}\0${sourceHash}\0${awaitHydrationKey}`;
966
- const cached = serverTransformCache.get(key);
1227
+ const cached = readRouterRuntimeCacheEntry(serverTransformCache, key, serverTransformCacheCounters);
967
1228
  if (cached !== undefined) {
968
1229
  return cached;
969
1230
  }
@@ -979,13 +1240,13 @@ function transformServerModule(options) {
979
1240
  target: "server",
980
1241
  ...(options.serverAwaitHydration === true ? { serverAwaitHydration: true } : {}),
981
1242
  });
982
- setBoundedCacheEntry(serverTransformCache, key, output, maxServerTransformCacheEntries);
1243
+ setBoundedCacheEntry(serverTransformCache, key, output, maxServerTransformCacheEntries, serverTransformCacheCounters);
983
1244
  return output;
984
1245
  }
985
1246
  async function analyzeRouteSource(options) {
986
1247
  const sourceHash = memoizedHashText(options.code);
987
1248
  const cacheKey = `${options.serverModuleCacheVersion ?? "dev"}\0${options.filename}\0${sourceHash}`;
988
- const cached = routeSourceAnalysisCache.get(cacheKey);
1249
+ const cached = readRouterRuntimeCacheEntry(routeSourceAnalysisCache, cacheKey, routeSourceAnalysisCacheCounters);
989
1250
  if (cached !== undefined) {
990
1251
  return cached;
991
1252
  }
@@ -993,7 +1254,7 @@ async function analyzeRouteSource(options) {
993
1254
  routeSourceAnalysisCache.delete(cacheKey);
994
1255
  throw error;
995
1256
  });
996
- setBoundedCacheEntry(routeSourceAnalysisCache, cacheKey, pending, maxRouteSourceAnalysisCacheEntries);
1257
+ setBoundedCacheEntry(routeSourceAnalysisCache, cacheKey, pending, maxRouteSourceAnalysisCacheEntries, routeSourceAnalysisCacheCounters);
997
1258
  return pending;
998
1259
  }
999
1260
  async function analyzeRouteSourceUncached(options) {
@@ -1150,7 +1411,7 @@ async function mayRenderOutOfOrderBoundaryDeepInner(options, seen) {
1150
1411
  seen.add(options.filename);
1151
1412
  const sourceHash = memoizedHashText(options.code);
1152
1413
  const cacheKey = `${options.serverModuleCacheVersion ?? "dev"}\0${options.filename}\0${sourceHash}`;
1153
- const cached = routeOutOfOrderBoundaryAnalysisCache.get(cacheKey);
1414
+ const cached = readRouterRuntimeCacheEntry(routeOutOfOrderBoundaryAnalysisCache, cacheKey, routeOutOfOrderBoundaryAnalysisCacheCounters);
1154
1415
  if (cached !== undefined) {
1155
1416
  return cached;
1156
1417
  }
@@ -1158,7 +1419,7 @@ async function mayRenderOutOfOrderBoundaryDeepInner(options, seen) {
1158
1419
  routeOutOfOrderBoundaryAnalysisCache.delete(cacheKey);
1159
1420
  throw error;
1160
1421
  });
1161
- setBoundedCacheEntry(routeOutOfOrderBoundaryAnalysisCache, cacheKey, pending, maxRouteOutOfOrderBoundaryAnalysisCacheEntries);
1422
+ setBoundedCacheEntry(routeOutOfOrderBoundaryAnalysisCache, cacheKey, pending, maxRouteOutOfOrderBoundaryAnalysisCacheEntries, routeOutOfOrderBoundaryAnalysisCacheCounters);
1162
1423
  return pending;
1163
1424
  }
1164
1425
  async function mayRenderImportedOutOfOrderBoundary(options, seen) {
@@ -1382,7 +1643,7 @@ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, server
1382
1643
  ? undefined
1383
1644
  : `${appDir}\0${shell.file}\0${serverModuleCacheVersion}`;
1384
1645
  if (cacheKey !== undefined) {
1385
- const cached = renderedShellCache.get(cacheKey);
1646
+ const cached = readRouterRuntimeCacheEntry(renderedShellCache, cacheKey, renderedShellCacheCounters);
1386
1647
  if (cached !== undefined && cached !== "impure") {
1387
1648
  return cached;
1388
1649
  }
@@ -1400,7 +1661,9 @@ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, server
1400
1661
  }
1401
1662
  const component = await loadServerComponent(output.code, shell.file, serverModules, serverModuleCacheVersion);
1402
1663
  const rendered = splitLayoutSlot(markShellBoundary(await component(props), shell), slotContext);
1403
- const cached = cacheKey !== undefined ? renderedShellCache.get(cacheKey) : undefined;
1664
+ const cached = cacheKey !== undefined
1665
+ ? readRouterRuntimeCacheEntry(renderedShellCache, cacheKey, renderedShellCacheCounters)
1666
+ : undefined;
1404
1667
  // Detect purity: a zero-arg component cannot depend on props. The
1405
1668
  // markShellBoundary + splitLayoutSlot output is then constant for
1406
1669
  // the (appDir, shellFile, version) tuple. We only set the cache
@@ -1412,6 +1675,7 @@ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, server
1412
1675
  const oldestKey = renderedShellCache.keys().next().value;
1413
1676
  if (oldestKey !== undefined) {
1414
1677
  renderedShellCache.delete(oldestKey);
1678
+ renderedShellCacheCounters.evictions += 1;
1415
1679
  }
1416
1680
  }
1417
1681
  renderedShellCache.set(cacheKey, rendered);
@@ -1448,6 +1712,11 @@ function splitLayoutSlot(layoutHtml, slotContext = createSlotRenderContext()) {
1448
1712
  // next request.
1449
1713
  const shellFilesCache = new Map();
1450
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();
1451
1720
  async function shellFilesForPage(appDir, pageFile, serverModuleCacheVersion) {
1452
1721
  const cacheKey = serverModuleCacheVersion === undefined
1453
1722
  ? undefined
@@ -1506,6 +1775,144 @@ function shellBoundaryId(appDir, directory) {
1506
1775
  ? "root"
1507
1776
  : relativeDirectory.replaceAll(sep, "/").replace(/[^A-Za-z0-9_$/-]/g, "_");
1508
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
+ }
1509
1916
  function markShellBoundary(html, shell) {
1510
1917
  const attributeName = shell.kind === "layout" ? "data-mreact-layout-boundary" : "data-mreact-template-boundary";
1511
1918
  if (html.includes(`${attributeName}=`)) {
@@ -1576,12 +1983,36 @@ async function loadRouteData(options) {
1576
1983
  const module = await loadRouteLoaderModule(options);
1577
1984
  return module.loader === undefined ? undefined : await module.loader(options.context);
1578
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
+ }
1579
2010
  async function loadRouteLoaderModule(options) {
1580
2011
  const cacheKey = options.serverModuleCacheVersion === undefined
1581
2012
  ? undefined
1582
2013
  : `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}\0${importPolicyCacheKey(options.importPolicy)}`;
1583
2014
  if (cacheKey !== undefined) {
1584
- const cached = routeLoaderModuleCache.get(cacheKey);
2015
+ const cached = readRouterRuntimeCacheEntry(routeLoaderModuleCache, cacheKey, routeLoaderModuleCacheCounters);
1585
2016
  if (cached !== undefined) {
1586
2017
  return cached;
1587
2018
  }
@@ -1596,7 +2027,7 @@ async function loadRouteLoaderModule(options) {
1596
2027
  throw error;
1597
2028
  });
1598
2029
  if (cacheKey !== undefined) {
1599
- setBoundedCacheEntry(routeLoaderModuleCache, cacheKey, loaded, maxRouteLoaderModuleCacheEntries);
2030
+ setBoundedCacheEntry(routeLoaderModuleCache, cacheKey, loaded, maxRouteLoaderModuleCacheEntries, routeLoaderModuleCacheCounters);
1600
2031
  }
1601
2032
  return loaded;
1602
2033
  }
@@ -1716,7 +2147,7 @@ async function loadComposedRouteMetadata(options) {
1716
2147
  ? undefined
1717
2148
  : `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}`;
1718
2149
  if (cacheKey !== undefined) {
1719
- const cached = composedRouteMetadataCache.get(cacheKey);
2150
+ const cached = readRouterRuntimeCacheEntry(composedRouteMetadataCache, cacheKey, composedRouteMetadataCacheCounters);
1720
2151
  if (cached !== undefined) {
1721
2152
  return cached;
1722
2153
  }
@@ -1728,7 +2159,7 @@ async function loadComposedRouteMetadata(options) {
1728
2159
  throw error;
1729
2160
  });
1730
2161
  if (cacheKey !== undefined) {
1731
- setBoundedCacheEntry(composedRouteMetadataCache, cacheKey, loaded, maxComposedRouteMetadataCacheEntries);
2162
+ setBoundedCacheEntry(composedRouteMetadataCache, cacheKey, loaded, maxComposedRouteMetadataCacheEntries, composedRouteMetadataCacheCounters);
1732
2163
  }
1733
2164
  return loaded;
1734
2165
  }
@@ -1813,8 +2244,22 @@ function mergeReadonlyArrays(left, right) {
1813
2244
  return [...left, ...right];
1814
2245
  }
1815
2246
  function mergeCspMetadata(left, right) {
2247
+ if (right?.disable === true) {
2248
+ return { disable: true };
2249
+ }
1816
2250
  if (left === undefined) {
1817
- 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;
1818
2263
  }
1819
2264
  if (right === undefined) {
1820
2265
  return left;
@@ -1823,12 +2268,28 @@ function mergeCspMetadata(left, right) {
1823
2268
  ...left,
1824
2269
  ...right,
1825
2270
  };
1826
- const directives = mergeObject(left.directives, right.directives);
2271
+ const directives = applyCspOverrides(left.directives, right);
1827
2272
  if (directives !== undefined) {
1828
2273
  merged.directives = directives;
1829
2274
  }
2275
+ else {
2276
+ delete merged.directives;
2277
+ }
1830
2278
  return merged;
1831
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
+ }
1832
2293
  function mergeOpenGraphMetadata(left, right) {
1833
2294
  if (left === undefined) {
1834
2295
  return right;
@@ -1901,13 +2362,19 @@ function injectHeadMetadata(html, metadata) {
1901
2362
  }
1902
2363
  return `<head>${tags}</head>${html}`;
1903
2364
  }
1904
- function responseHeadersForMetadata(metadata) {
1905
- 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) {
1906
2369
  const csp = contentSecurityPolicy(metadata?.csp);
1907
- if (csp !== undefined) {
1908
- headers.set("content-security-policy", csp);
2370
+ if (csp === undefined && extra === undefined) {
2371
+ return DEFAULT_HTML_RESPONSE_HEADERS;
1909
2372
  }
1910
- return headers;
2373
+ return {
2374
+ ...DEFAULT_HTML_RESPONSE_HEADERS,
2375
+ ...(csp === undefined ? undefined : { "content-security-policy": csp }),
2376
+ ...extra,
2377
+ };
1911
2378
  }
1912
2379
  function injectQueryState(html, state) {
1913
2380
  if (state.queries.length === 0) {
@@ -2042,7 +2509,7 @@ function readServerSourceFile(file, serverModuleCacheVersion, serverSourceFiles)
2042
2509
  return readFile(file, "utf8");
2043
2510
  }
2044
2511
  const key = `${serverModuleCacheVersion}:${file}`;
2045
- const cached = serverSourceFileCache.get(key);
2512
+ const cached = readRouterRuntimeCacheEntry(serverSourceFileCache, key, serverSourceFileCacheCounters);
2046
2513
  if (cached !== undefined) {
2047
2514
  return cached;
2048
2515
  }
@@ -2050,17 +2517,20 @@ function readServerSourceFile(file, serverModuleCacheVersion, serverSourceFiles)
2050
2517
  serverSourceFileCache.delete(key);
2051
2518
  throw error;
2052
2519
  });
2053
- setBoundedCacheEntry(serverSourceFileCache, key, loaded, maxServerSourceFileCacheEntries);
2520
+ setBoundedCacheEntry(serverSourceFileCache, key, loaded, maxServerSourceFileCacheEntries, serverSourceFileCacheCounters);
2054
2521
  return loaded;
2055
2522
  }
2056
2523
  function hashText(text) {
2057
2524
  return createHash("sha256").update(text).digest("hex").slice(0, 16);
2058
2525
  }
2059
- function setBoundedCacheEntry(cache, key, value, maxEntries) {
2526
+ function setBoundedCacheEntry(cache, key, value, maxEntries, counters) {
2060
2527
  if (cache.size >= maxEntries) {
2061
2528
  const oldestKey = cache.keys().next().value;
2062
2529
  if (oldestKey !== undefined) {
2063
2530
  cache.delete(oldestKey);
2531
+ if (counters !== undefined) {
2532
+ counters.evictions += 1;
2533
+ }
2064
2534
  }
2065
2535
  }
2066
2536
  cache.set(key, value);