@reckona/mreact-router 0.0.138 → 0.0.140

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 (76) hide show
  1. package/README.md +1 -1
  2. package/dist/actions.d.ts +1 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +32 -12
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/static.js +10 -4
  7. package/dist/adapters/static.js.map +1 -1
  8. package/dist/build.d.ts.map +1 -1
  9. package/dist/build.js +28 -21
  10. package/dist/build.js.map +1 -1
  11. package/dist/cache.d.ts +2 -0
  12. package/dist/cache.d.ts.map +1 -1
  13. package/dist/cache.js +23 -1
  14. package/dist/cache.js.map +1 -1
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +53 -14
  17. package/dist/client.js.map +1 -1
  18. package/dist/dev-server.d.ts +1 -0
  19. package/dist/dev-server.d.ts.map +1 -1
  20. package/dist/dev-server.js +6 -4
  21. package/dist/dev-server.js.map +1 -1
  22. package/dist/link.js +17 -3
  23. package/dist/link.js.map +1 -1
  24. package/dist/middleware.d.ts.map +1 -1
  25. package/dist/middleware.js +10 -7
  26. package/dist/middleware.js.map +1 -1
  27. package/dist/multipart.d.ts +5 -0
  28. package/dist/multipart.d.ts.map +1 -1
  29. package/dist/multipart.js +24 -1
  30. package/dist/multipart.js.map +1 -1
  31. package/dist/native-route-matcher.d.ts +9 -0
  32. package/dist/native-route-matcher.d.ts.map +1 -1
  33. package/dist/native-route-matcher.js +23 -11
  34. package/dist/native-route-matcher.js.map +1 -1
  35. package/dist/navigation.d.ts.map +1 -1
  36. package/dist/navigation.js +8 -1
  37. package/dist/navigation.js.map +1 -1
  38. package/dist/render.d.ts +5 -1
  39. package/dist/render.d.ts.map +1 -1
  40. package/dist/render.js +239 -95
  41. package/dist/render.js.map +1 -1
  42. package/dist/route-source.d.ts +3 -1
  43. package/dist/route-source.d.ts.map +1 -1
  44. package/dist/route-source.js +13 -11
  45. package/dist/route-source.js.map +1 -1
  46. package/dist/route-styles.d.ts +4 -0
  47. package/dist/route-styles.d.ts.map +1 -1
  48. package/dist/route-styles.js +15 -3
  49. package/dist/route-styles.js.map +1 -1
  50. package/dist/serve.d.ts.map +1 -1
  51. package/dist/serve.js +39 -9
  52. package/dist/serve.js.map +1 -1
  53. package/dist/server-action-inference.d.ts.map +1 -1
  54. package/dist/server-action-inference.js +57 -0
  55. package/dist/server-action-inference.js.map +1 -1
  56. package/dist/vite.d.ts.map +1 -1
  57. package/dist/vite.js +27 -11
  58. package/dist/vite.js.map +1 -1
  59. package/package.json +11 -11
  60. package/src/actions.ts +44 -10
  61. package/src/adapters/static.ts +11 -4
  62. package/src/build.ts +28 -17
  63. package/src/cache.ts +33 -1
  64. package/src/client.ts +53 -14
  65. package/src/dev-server.ts +11 -4
  66. package/src/link.ts +24 -3
  67. package/src/middleware.ts +12 -7
  68. package/src/multipart.ts +32 -1
  69. package/src/native-route-matcher.ts +34 -13
  70. package/src/navigation.ts +11 -2
  71. package/src/render.ts +321 -113
  72. package/src/route-source.ts +14 -10
  73. package/src/route-styles.ts +28 -3
  74. package/src/serve.ts +68 -15
  75. package/src/server-action-inference.ts +86 -0
  76. package/src/vite.ts +39 -12
package/dist/render.js CHANGED
@@ -3,7 +3,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { access, readFile, stat } from "node:fs/promises";
4
4
  import { dirname, join, relative, resolve, sep } from "node:path";
5
5
  import { formatDiagnostic, transform } from "@reckona/mreact-compiler";
6
- import { createQueryClient, dehydrate, __MREACT_QUERY_STATE_SCRIPT_ID, runWithQueryClient, } from "@reckona/mreact-query";
6
+ import { createQueryClient, dehydrate, __MREACT_QUERY_STATE_SCRIPT_ID, installQueryAsyncStorage, runWithQueryClient, } from "@reckona/mreact-query";
7
7
  import { createStringSink, renderAsyncBoundary, renderOutOfOrderReorderScript, renderReactNodeToString, renderToReadableStream, } from "@reckona/mreact-server";
8
8
  import { createClientRouteInferenceCache, formatClientRouteInferenceDiagnostic, inferClientRouteModule, } from "./client-route-inference.js";
9
9
  import { hydrationMarkerParts, routeIdForPath, withHydrationMarkers, withRouteMarkers, } from "./navigation-runtime.js";
@@ -36,6 +36,7 @@ const nativeEscapeTransform = {
36
36
  };
37
37
  const authRuntimeStateKey = "__mreactAuthRuntimeState";
38
38
  const authSessionScriptId = "__mreact_auth_session";
39
+ installQueryAsyncStorage(new AsyncLocalStorage());
39
40
  export async function preloadBuiltRequestModules(options) {
40
41
  const clientRouteInferenceCache = createClientRouteInferenceCache();
41
42
  const middlewareFiles = [
@@ -247,7 +248,7 @@ export async function renderAppRequest(options) {
247
248
  return authStorage.run({}, () => renderAppRequest(options));
248
249
  }
249
250
  const trace = traceContextFromRequest(options.request);
250
- const url = new URL(options.request.url);
251
+ const url = options.requestUrl ?? new URL(options.request.url);
251
252
  const requestEvent = {
252
253
  method: options.request.method,
253
254
  path: url.pathname,
@@ -255,7 +256,7 @@ export async function renderAppRequest(options) {
255
256
  ...(trace === undefined ? {} : { trace }),
256
257
  };
257
258
  invokeRouterInstrumentation(options.instrumentation?.onRequestStart, requestEvent);
258
- const response = await renderAppRequestInternal(options);
259
+ const response = await renderAppRequestInternal({ ...options, requestUrl: url });
259
260
  invokeRouterInstrumentation(options.instrumentation?.onRequestEnd, {
260
261
  ...requestEvent,
261
262
  status: response.status,
@@ -333,13 +334,13 @@ export async function resolveAppRouterMiddleware(options) {
333
334
  }
334
335
  async function renderAppRequestInternal(options) {
335
336
  const timing = createRenderTiming(options.logger);
336
- const clientRouteInferenceCache = createClientRouteInferenceCache();
337
+ const clientRouteInferenceCache = options.clientRouteInferenceCache ?? createClientRouteInferenceCache();
337
338
  let phaseStartedAt = renderTimingPhaseStartedAt(timing);
338
339
  const routes = options.routes ?? (await scanAppRoutes({ appDir: options.appDir }));
339
340
  finishRenderTimingPhase(timing, phaseStartedAt, "routeScanMs");
340
- const url = new URL(options.request.url);
341
+ const url = options.requestUrl ?? new URL(options.request.url);
341
342
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
342
- const matched = options.routeMatcher?.match(url.pathname) ?? matchRoute(routes, url.pathname);
343
+ const matched = options.matchedRoute ?? options.routeMatcher?.match(url.pathname) ?? matchRoute(routes, url.pathname);
343
344
  finishRenderTimingPhase(timing, phaseStartedAt, "routeMatchMs");
344
345
  const hasMiddleware = options.skipMiddleware === true
345
346
  ? false
@@ -380,7 +381,9 @@ async function renderAppRequestInternal(options) {
380
381
  if (middlewareResult.request !== options.request) {
381
382
  return renderAppRequestInternal({
382
383
  ...options,
384
+ matchedRoute: undefined,
383
385
  request: middlewareResult.request,
386
+ requestUrl: new URL(middlewareResult.request.url),
384
387
  skipMiddleware: true,
385
388
  });
386
389
  }
@@ -511,6 +514,7 @@ async function renderAppRequestInternal(options) {
511
514
  : await cachedRouteResponse({
512
515
  cache: options.routeCache,
513
516
  key: cacheKey,
517
+ request: options.request,
514
518
  });
515
519
  finishRenderTimingPhase(timing, phaseStartedAt, "routeCacheMs");
516
520
  if (cachedResponse !== undefined) {
@@ -694,6 +698,7 @@ async function renderAppRequestInternal(options) {
694
698
  serverModules: options.serverModules,
695
699
  serverModuleCacheVersion: options.serverModuleCacheVersion,
696
700
  serverSourceFiles: options.serverSourceFiles,
701
+ define: options.define,
697
702
  vitePlugins: options.vitePlugins,
698
703
  });
699
704
  html = injectHeadMetadata(html, metadata);
@@ -949,6 +954,7 @@ async function renderAppRequestInternal(options) {
949
954
  cache: options.routeCache,
950
955
  path: matched.route.path,
951
956
  policy: effectiveCachePolicy,
957
+ request: options.request,
952
958
  response,
953
959
  });
954
960
  finishRenderTimingPhase(timing, phaseStartedAt, "responseBuildMs");
@@ -1010,7 +1016,7 @@ async function renderAppRequestInternal(options) {
1010
1016
  vitePlugins: options.vitePlugins,
1011
1017
  navigation: recoveryRoute,
1012
1018
  status: 500,
1013
- textFallback: error instanceof Error ? error.message : String(error),
1019
+ textFallback: "Internal Server Error",
1014
1020
  });
1015
1021
  emitRenderTiming(options, timing, response.status);
1016
1022
  return response;
@@ -1716,9 +1722,7 @@ async function analyzeRouteSourceUncached(options) {
1716
1722
  };
1717
1723
  }
1718
1724
  function routeSourceFilesForAnalysis(options) {
1719
- return options.serverSourceFiles === undefined
1720
- ? { [options.filename]: options.code }
1721
- : { ...Object.fromEntries(options.serverSourceFiles), [options.filename]: options.code };
1725
+ return (file) => file === options.filename ? options.code : options.serverSourceFiles?.get(file);
1722
1726
  }
1723
1727
  async function runServerModule(code, props, sourcefile, serverModules, serverModuleCacheVersion, define, vitePlugins) {
1724
1728
  const component = await loadServerComponent(code, sourcefile, serverModules, serverModuleCacheVersion, define, vitePlugins);
@@ -2126,8 +2130,16 @@ async function applyLayouts(options) {
2126
2130
  let html = options.html;
2127
2131
  let shellHasOutOfOrderBoundary = false;
2128
2132
  const slotContext = createSlotRenderContext(options.slots);
2129
- for (const shell of layoutFiles.reverse()) {
2130
- const rendered = await renderShellPrefixSuffix(options.appDir, shell, options.props, slotContext, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles, options.clientRouteInferenceCache, options.timing, options.define, options.vitePlugins, options.importPolicy);
2133
+ const renderShell = (shell) => renderShellPrefixSuffix(options.appDir, shell, options.props, slotContext, options.serverModules, options.serverModuleCacheVersion, options.serverSourceFiles, options.clientRouteInferenceCache, options.timing, options.define, options.vitePlugins, options.importPolicy);
2134
+ const renderedShells = Object.keys(slotContext.namedSlots).length === 0
2135
+ ? await Promise.all(layoutFiles.map(renderShell))
2136
+ : undefined;
2137
+ for (let index = layoutFiles.length - 1; index >= 0; index -= 1) {
2138
+ const shell = layoutFiles[index];
2139
+ if (shell === undefined) {
2140
+ continue;
2141
+ }
2142
+ const rendered = renderedShells?.[index] ?? (await renderShell(shell));
2131
2143
  shellHasOutOfOrderBoundary ||= rendered.hasOutOfOrderBoundary;
2132
2144
  html = `${rendered.prefix}${html}${rendered.suffix}`;
2133
2145
  }
@@ -2148,10 +2160,15 @@ async function applyLayouts(options) {
2148
2160
  }
2149
2161
  async function layoutShellsForPage(appDir, pageFile, props, slots, serverModules, serverModuleCacheVersion, serverSourceFiles, clientRouteInferenceCache, define, vitePlugins, importPolicy) {
2150
2162
  const layoutFiles = await shellFilesForPage(appDir, pageFile, serverModuleCacheVersion);
2151
- const shells = [];
2152
2163
  const slotContext = createSlotRenderContext(slots);
2153
- for (const shell of layoutFiles) {
2154
- shells.push(await renderShellPrefixSuffix(appDir, shell, props, slotContext, serverModules, serverModuleCacheVersion, serverSourceFiles, clientRouteInferenceCache, undefined, define, vitePlugins, importPolicy));
2164
+ const renderShell = (shell) => renderShellPrefixSuffix(appDir, shell, props, slotContext, serverModules, serverModuleCacheVersion, serverSourceFiles, clientRouteInferenceCache, undefined, define, vitePlugins, importPolicy);
2165
+ const shells = Object.keys(slotContext.namedSlots).length === 0
2166
+ ? await Promise.all(layoutFiles.map(renderShell))
2167
+ : [];
2168
+ if (shells.length === 0 && layoutFiles.length > 0) {
2169
+ for (const shell of layoutFiles) {
2170
+ shells.push(await renderShell(shell));
2171
+ }
2155
2172
  }
2156
2173
  warnUnconsumedRouteSlots({
2157
2174
  appDir,
@@ -2172,64 +2189,32 @@ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, server
2172
2189
  return cached;
2173
2190
  }
2174
2191
  }
2175
- let phaseStartedAt = renderTimingPhaseStartedAt(timing);
2176
- const code = await readServerSourceFile(shell.file, serverModuleCacheVersion, serverSourceFiles);
2177
- addRenderTimingPhaseDuration(timing, phaseStartedAt, "layoutSourceReadMs");
2178
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
2179
- const shellUsesAwait = await mayRenderOutOfOrderBoundaryDeep({
2180
- code,
2181
- filename: shell.file,
2182
- serverModuleCacheVersion,
2183
- serverSourceFiles,
2184
- });
2185
- const serverOutput = shellUsesAwait ? "stream" : "string";
2186
- const artifact = serverModules?.get(shell.file)?.[serverOutput];
2187
- const clientInference = artifact !== undefined && artifact.sourceHash === memoizedHashText(code)
2188
- ? {
2189
- client: false,
2190
- clientBoundaryImports: [],
2191
- clientBoundaryFallbackImports: [],
2192
- diagnostics: [],
2193
- }
2194
- : await inferClientRouteModule({
2195
- cache: clientRouteInferenceCache,
2196
- code: stripRouteClientOnlyExports(code, shell.file),
2197
- filename: shell.file,
2192
+ const staticEntry = cacheKey === undefined ? undefined : shellStaticRenderCache.get(cacheKey);
2193
+ const loadedStaticEntry = staticEntry ??
2194
+ (await loadShellStaticRenderEntry({
2195
+ cacheKey,
2196
+ clientRouteInferenceCache,
2197
+ define,
2198
+ importPolicy,
2199
+ serverModuleCacheVersion,
2200
+ serverModules,
2201
+ serverSourceFiles,
2202
+ shell,
2203
+ timing,
2198
2204
  vitePlugins,
2199
- });
2200
- for (const diagnostic of clientInference.diagnostics) {
2201
- console.warn(formatClientRouteInferenceDiagnostic(diagnostic));
2202
- }
2203
- const output = transformServerModule({
2204
- code,
2205
- clientBoundaryImports: clientInference.clientBoundaryImports,
2206
- clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
2207
- filename: shell.file,
2208
- serverModules,
2209
- serverOutput,
2210
- });
2211
- addRenderTimingPhaseDuration(timing, phaseStartedAt, "layoutTransformMs");
2212
- const fatalDiagnostics = fatalServerDiagnostics(output.diagnostics);
2213
- if (fatalDiagnostics.length > 0) {
2214
- throw new Error(formatServerDiagnostics(shell.file, fatalDiagnostics));
2215
- }
2216
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
2217
- const component = shellUsesAwait
2218
- ? selectStreamComponent(await loadServerStreamModule(output.code, shell.file, serverModules, serverModuleCacheVersion, define, vitePlugins, importPolicy))
2219
- : await loadServerComponent(output.code, shell.file, serverModules, serverModuleCacheVersion, define, vitePlugins, importPolicy);
2220
- addRenderTimingPhaseDuration(timing, phaseStartedAt, "layoutModuleLoadMs");
2221
- phaseStartedAt = renderTimingPhaseStartedAt(timing);
2222
- const layoutHtml = shellUsesAwait
2223
- ? await renderShellStreamComponent(component, props)
2224
- : await component(props);
2205
+ }));
2206
+ let phaseStartedAt = renderTimingPhaseStartedAt(timing);
2207
+ const layoutHtml = loadedStaticEntry.shellUsesAwait
2208
+ ? await renderShellStreamComponent(loadedStaticEntry.component, props)
2209
+ : await loadedStaticEntry.component(props);
2225
2210
  addRenderTimingPhaseDuration(timing, phaseStartedAt, "layoutComponentRenderMs");
2226
2211
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
2227
2212
  const rendered = {
2228
2213
  ...splitLayoutSlot(markShellBoundary(layoutHtml, shell), slotContext),
2229
- hasOutOfOrderBoundary: hasOutOfOrderBoundary(output.code),
2214
+ hasOutOfOrderBoundary: loadedStaticEntry.hasOutOfOrderBoundary,
2230
2215
  };
2231
2216
  addRenderTimingPhaseDuration(timing, phaseStartedAt, "layoutSlotSplitMs");
2232
- const shellCacheKey = shellUsesAwait ? undefined : cacheKey;
2217
+ const shellCacheKey = loadedStaticEntry.shellUsesAwait ? undefined : cacheKey;
2233
2218
  const cached = shellCacheKey !== undefined
2234
2219
  ? readRouterRuntimeCacheEntry(renderedShellCache, shellCacheKey, renderedShellCacheCounters)
2235
2220
  : undefined;
@@ -2239,7 +2224,7 @@ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, server
2239
2224
  // entry on the first request that observes the function arity; on
2240
2225
  // an "impure" tag we never overwrite it.
2241
2226
  if (shellCacheKey !== undefined && cached !== "impure") {
2242
- if (component.length === 0) {
2227
+ if (loadedStaticEntry.component.length === 0) {
2243
2228
  if (renderedShellCache.size >= MAX_RENDERED_SHELL_CACHE_ENTRIES) {
2244
2229
  const oldestKey = renderedShellCache.keys().next().value;
2245
2230
  if (oldestKey !== undefined) {
@@ -2258,6 +2243,69 @@ async function renderShellPrefixSuffix(appDir, shell, props, slotContext, server
2258
2243
  }
2259
2244
  return rendered;
2260
2245
  }
2246
+ async function loadShellStaticRenderEntry(options) {
2247
+ let phaseStartedAt = renderTimingPhaseStartedAt(options.timing);
2248
+ const code = await readServerSourceFile(options.shell.file, options.serverModuleCacheVersion, options.serverSourceFiles);
2249
+ addRenderTimingPhaseDuration(options.timing, phaseStartedAt, "layoutSourceReadMs");
2250
+ phaseStartedAt = renderTimingPhaseStartedAt(options.timing);
2251
+ const shellUsesAwait = await mayRenderOutOfOrderBoundaryDeep({
2252
+ code,
2253
+ filename: options.shell.file,
2254
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
2255
+ serverSourceFiles: options.serverSourceFiles,
2256
+ });
2257
+ const serverOutput = shellUsesAwait ? "stream" : "string";
2258
+ const artifact = options.serverModules?.get(options.shell.file)?.[serverOutput];
2259
+ const clientInference = artifact !== undefined && artifact.sourceHash === memoizedHashText(code)
2260
+ ? {
2261
+ client: false,
2262
+ clientBoundaryImports: [],
2263
+ clientBoundaryFallbackImports: [],
2264
+ diagnostics: [],
2265
+ }
2266
+ : await inferClientRouteModule({
2267
+ cache: options.clientRouteInferenceCache,
2268
+ code: stripRouteClientOnlyExports(code, options.shell.file),
2269
+ filename: options.shell.file,
2270
+ vitePlugins: options.vitePlugins,
2271
+ });
2272
+ for (const diagnostic of clientInference.diagnostics) {
2273
+ console.warn(formatClientRouteInferenceDiagnostic(diagnostic));
2274
+ }
2275
+ const output = transformServerModule({
2276
+ code,
2277
+ clientBoundaryImports: clientInference.clientBoundaryImports,
2278
+ clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
2279
+ filename: options.shell.file,
2280
+ serverModules: options.serverModules,
2281
+ serverOutput,
2282
+ });
2283
+ addRenderTimingPhaseDuration(options.timing, phaseStartedAt, "layoutTransformMs");
2284
+ const fatalDiagnostics = fatalServerDiagnostics(output.diagnostics);
2285
+ if (fatalDiagnostics.length > 0) {
2286
+ throw new Error(formatServerDiagnostics(options.shell.file, fatalDiagnostics));
2287
+ }
2288
+ phaseStartedAt = renderTimingPhaseStartedAt(options.timing);
2289
+ const component = shellUsesAwait
2290
+ ? selectStreamComponent(await loadServerStreamModule(output.code, options.shell.file, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, options.importPolicy))
2291
+ : await loadServerComponent(output.code, options.shell.file, options.serverModules, options.serverModuleCacheVersion, options.define, options.vitePlugins, options.importPolicy);
2292
+ addRenderTimingPhaseDuration(options.timing, phaseStartedAt, "layoutModuleLoadMs");
2293
+ const entry = {
2294
+ component,
2295
+ hasOutOfOrderBoundary: hasOutOfOrderBoundary(output.code),
2296
+ shellUsesAwait,
2297
+ };
2298
+ if (options.cacheKey !== undefined) {
2299
+ if (shellStaticRenderCache.size >= MAX_SHELL_STATIC_RENDER_CACHE_ENTRIES) {
2300
+ const oldestKey = shellStaticRenderCache.keys().next().value;
2301
+ if (oldestKey !== undefined) {
2302
+ shellStaticRenderCache.delete(oldestKey);
2303
+ }
2304
+ }
2305
+ shellStaticRenderCache.set(options.cacheKey, entry);
2306
+ }
2307
+ return entry;
2308
+ }
2261
2309
  async function renderShellStreamComponent(component, props) {
2262
2310
  const sink = createStringSink();
2263
2311
  await component(sink, props);
@@ -2276,11 +2324,17 @@ async function renderShellStreamComponent(component, props) {
2276
2324
  // next request.
2277
2325
  const shellFilesCache = new Map();
2278
2326
  const MAX_SHELL_FILES_CACHE_ENTRIES = 1024;
2327
+ const devShellFilesCache = new Map();
2328
+ const shellStaticRenderCache = new Map();
2329
+ const MAX_SHELL_STATIC_RENDER_CACHE_ENTRIES = 1024;
2279
2330
  const routeMiddlewareControlCache = new Map();
2280
2331
  const MAX_ROUTE_MIDDLEWARE_CONTROL_CACHE_ENTRIES = 1024;
2281
2332
  const appMiddlewareFileCache = new Map();
2282
2333
  const MAX_APP_MIDDLEWARE_FILE_CACHE_ENTRIES = 1024;
2334
+ const devAppMiddlewareFileCache = new Map();
2283
2335
  const routeMiddlewareControlSourceCache = new Map();
2336
+ const devServerSourceFileCache = new Map();
2337
+ const MAX_DEV_SERVER_SOURCE_FILE_CACHE_ENTRIES = 2048;
2284
2338
  async function shellFilesForPage(appDir, pageFile, serverModuleCacheVersion) {
2285
2339
  const cacheKey = serverModuleCacheVersion === undefined
2286
2340
  ? undefined
@@ -2291,6 +2345,31 @@ async function shellFilesForPage(appDir, pageFile, serverModuleCacheVersion) {
2291
2345
  return cached;
2292
2346
  }
2293
2347
  }
2348
+ const devCacheKey = cacheKey === undefined ? `${appDir}\0${pageFile}` : undefined;
2349
+ if (devCacheKey !== undefined) {
2350
+ const directoryStats = await routeShellDirectoryStats(appDir, pageFile);
2351
+ const cached = devShellFilesCache.get(devCacheKey);
2352
+ if (cached !== undefined &&
2353
+ sameFileStats(cached.directoryStats, directoryStats)) {
2354
+ return cached.files;
2355
+ }
2356
+ const files = await shellFilesForPageUncached(appDir, pageFile);
2357
+ devShellFilesCache.set(devCacheKey, { directoryStats, files });
2358
+ return files;
2359
+ }
2360
+ const files = await shellFilesForPageUncached(appDir, pageFile);
2361
+ if (cacheKey !== undefined) {
2362
+ if (shellFilesCache.size >= MAX_SHELL_FILES_CACHE_ENTRIES) {
2363
+ const oldestKey = shellFilesCache.keys().next().value;
2364
+ if (oldestKey !== undefined) {
2365
+ shellFilesCache.delete(oldestKey);
2366
+ }
2367
+ }
2368
+ shellFilesCache.set(cacheKey, files);
2369
+ }
2370
+ return files;
2371
+ }
2372
+ async function shellFilesForPageUncached(appDir, pageFile) {
2294
2373
  const shells = await existingRouteShellCandidates(appDir, pageFile, async (file) => {
2295
2374
  try {
2296
2375
  await access(file);
@@ -2305,15 +2384,6 @@ async function shellFilesForPage(appDir, pageFile, serverModuleCacheVersion) {
2305
2384
  id: shellBoundaryId(appDir, shell.directory),
2306
2385
  kind: shell.kind,
2307
2386
  }));
2308
- if (cacheKey !== undefined) {
2309
- if (shellFilesCache.size >= MAX_SHELL_FILES_CACHE_ENTRIES) {
2310
- const oldestKey = shellFilesCache.keys().next().value;
2311
- if (oldestKey !== undefined) {
2312
- shellFilesCache.delete(oldestKey);
2313
- }
2314
- }
2315
- shellFilesCache.set(cacheKey, files);
2316
- }
2317
2387
  return files;
2318
2388
  }
2319
2389
  function withRouteCacheHeader(response, policy) {
@@ -2339,21 +2409,28 @@ async function hasAppMiddleware(options) {
2339
2409
  return cached;
2340
2410
  }
2341
2411
  }
2342
- const loaded = hasAppMiddlewareUncached(middlewareFiles).catch((error) => {
2343
- if (cacheKey !== undefined) {
2344
- appMiddlewareFileCache.delete(cacheKey);
2412
+ if (cacheKey === undefined) {
2413
+ const directoryStat = await fileStat(options.appDir);
2414
+ const cached = devAppMiddlewareFileCache.get(options.appDir);
2415
+ if (cached !== undefined &&
2416
+ sameFileStat(cached.directoryStat, directoryStat)) {
2417
+ return cached.hasMiddleware;
2345
2418
  }
2419
+ const hasMiddleware = await hasAppMiddlewareUncached(middlewareFiles);
2420
+ devAppMiddlewareFileCache.set(options.appDir, { directoryStat, hasMiddleware });
2421
+ return hasMiddleware;
2422
+ }
2423
+ const loaded = hasAppMiddlewareUncached(middlewareFiles).catch((error) => {
2424
+ appMiddlewareFileCache.delete(cacheKey);
2346
2425
  throw error;
2347
2426
  });
2348
- if (cacheKey !== undefined) {
2349
- if (appMiddlewareFileCache.size >= MAX_APP_MIDDLEWARE_FILE_CACHE_ENTRIES) {
2350
- const oldestKey = appMiddlewareFileCache.keys().next().value;
2351
- if (oldestKey !== undefined) {
2352
- appMiddlewareFileCache.delete(oldestKey);
2353
- }
2427
+ if (appMiddlewareFileCache.size >= MAX_APP_MIDDLEWARE_FILE_CACHE_ENTRIES) {
2428
+ const oldestKey = appMiddlewareFileCache.keys().next().value;
2429
+ if (oldestKey !== undefined) {
2430
+ appMiddlewareFileCache.delete(oldestKey);
2354
2431
  }
2355
- appMiddlewareFileCache.set(cacheKey, loaded);
2356
2432
  }
2433
+ appMiddlewareFileCache.set(cacheKey, loaded);
2357
2434
  return loaded;
2358
2435
  }
2359
2436
  async function hasAppMiddlewareUncached(files) {
@@ -2368,6 +2445,48 @@ async function hasAppMiddlewareUncached(files) {
2368
2445
  }
2369
2446
  return false;
2370
2447
  }
2448
+ async function routeShellDirectoryStats(appDir, pageFile) {
2449
+ const directories = [];
2450
+ let current = dirname(pageFile);
2451
+ const resolvedAppDir = resolve(appDir);
2452
+ while (true) {
2453
+ directories.push(current);
2454
+ if (resolve(current) === resolvedAppDir) {
2455
+ break;
2456
+ }
2457
+ const parent = dirname(current);
2458
+ if (parent === current) {
2459
+ break;
2460
+ }
2461
+ current = parent;
2462
+ }
2463
+ return await Promise.all(directories.map((directory) => fileStatOrMissing(directory)));
2464
+ }
2465
+ async function fileStat(file) {
2466
+ const stats = await stat(file);
2467
+ return { mtimeMs: stats.mtimeMs, size: stats.size };
2468
+ }
2469
+ async function fileStatOrMissing(file) {
2470
+ try {
2471
+ return await fileStat(file);
2472
+ }
2473
+ catch (error) {
2474
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
2475
+ return { mtimeMs: -1, size: -1 };
2476
+ }
2477
+ throw error;
2478
+ }
2479
+ }
2480
+ function sameFileStats(left, right) {
2481
+ return (left.length === right.length &&
2482
+ left.every((value, index) => {
2483
+ const other = right[index];
2484
+ return other !== undefined && sameFileStat(value, other);
2485
+ }));
2486
+ }
2487
+ function sameFileStat(left, right) {
2488
+ return left.mtimeMs === right.mtimeMs && left.size === right.size;
2489
+ }
2371
2490
  async function loadRouteMiddlewareControl(options) {
2372
2491
  const cacheKey = options.serverModuleCacheVersion === undefined
2373
2492
  ? undefined
@@ -2658,12 +2777,11 @@ async function loadComposedRouteMetadataUncached(options) {
2658
2777
  const layoutFiles = await shellFilesForPage(options.appDir, options.filename, options.serverModuleCacheVersion);
2659
2778
  const metadata = [];
2660
2779
  let dynamic = hasGenerateMetadataExport(options.code);
2661
- for (const shell of layoutFiles) {
2780
+ const layoutMetadataTasks = layoutFiles.map(async (shell) => {
2662
2781
  if (shell.kind !== "layout") {
2663
- continue;
2782
+ return { code: undefined, metadata: undefined };
2664
2783
  }
2665
2784
  const code = await readServerSourceFile(shell.file, options.serverModuleCacheVersion, options.serverSourceFiles);
2666
- dynamic ||= hasGenerateMetadataExport(code);
2667
2785
  const shellMetadata = await loadRouteMetadata({
2668
2786
  appDir: options.appDir,
2669
2787
  code,
@@ -2675,11 +2793,9 @@ async function loadComposedRouteMetadataUncached(options) {
2675
2793
  define: options.define,
2676
2794
  vitePlugins: options.vitePlugins,
2677
2795
  });
2678
- if (shellMetadata !== undefined) {
2679
- metadata.push(shellMetadata);
2680
- }
2681
- }
2682
- const pageMetadata = await loadRouteMetadata({
2796
+ return { code, metadata: shellMetadata };
2797
+ });
2798
+ const pageMetadataTask = loadRouteMetadata({
2683
2799
  appDir: options.appDir,
2684
2800
  code: options.code,
2685
2801
  context: options.context,
@@ -2690,6 +2806,16 @@ async function loadComposedRouteMetadataUncached(options) {
2690
2806
  define: options.define,
2691
2807
  vitePlugins: options.vitePlugins,
2692
2808
  });
2809
+ for (const task of layoutMetadataTasks) {
2810
+ const shell = await task;
2811
+ if (shell.code !== undefined) {
2812
+ dynamic ||= hasGenerateMetadataExport(shell.code);
2813
+ }
2814
+ if (shell.metadata !== undefined) {
2815
+ metadata.push(shell.metadata);
2816
+ }
2817
+ }
2818
+ const pageMetadata = await pageMetadataTask;
2693
2819
  if (pageMetadata !== undefined) {
2694
2820
  metadata.push(pageMetadata);
2695
2821
  }
@@ -2852,7 +2978,7 @@ function readServerSourceFile(file, serverModuleCacheVersion, serverSourceFiles)
2852
2978
  return Promise.resolve(manifestSource);
2853
2979
  }
2854
2980
  if (serverModuleCacheVersion === undefined) {
2855
- return readFile(file, "utf8");
2981
+ return readDevServerSourceFile(file);
2856
2982
  }
2857
2983
  const key = `${serverModuleCacheVersion}:${file}`;
2858
2984
  const cached = readRouterRuntimeCacheEntry(serverSourceFileCache, key, serverSourceFileCacheCounters);
@@ -2866,6 +2992,24 @@ function readServerSourceFile(file, serverModuleCacheVersion, serverSourceFiles)
2866
2992
  setBoundedCacheEntry(serverSourceFileCache, key, loaded, maxServerSourceFileCacheEntries, serverSourceFileCacheCounters);
2867
2993
  return loaded;
2868
2994
  }
2995
+ async function readDevServerSourceFile(file) {
2996
+ const stats = await fileStat(file);
2997
+ const cached = devServerSourceFileCache.get(file);
2998
+ if (cached !== undefined &&
2999
+ cached.mtimeMs === stats.mtimeMs &&
3000
+ cached.size === stats.size) {
3001
+ return cached.source;
3002
+ }
3003
+ const source = await readFile(file, "utf8");
3004
+ if (devServerSourceFileCache.size >= MAX_DEV_SERVER_SOURCE_FILE_CACHE_ENTRIES) {
3005
+ const oldestKey = devServerSourceFileCache.keys().next().value;
3006
+ if (oldestKey !== undefined) {
3007
+ devServerSourceFileCache.delete(oldestKey);
3008
+ }
3009
+ }
3010
+ devServerSourceFileCache.set(file, { ...stats, source });
3011
+ return source;
3012
+ }
2869
3013
  function setBoundedCacheEntry(cache, key, value, maxEntries, counters) {
2870
3014
  if (cache.size >= maxEntries) {
2871
3015
  const oldestKey = cache.keys().next().value;