@reckona/mreact-router 0.0.180 → 0.0.182

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 (45) hide show
  1. package/dist/actions.d.ts.map +1 -1
  2. package/dist/actions.js +20 -8
  3. package/dist/actions.js.map +1 -1
  4. package/dist/adapters/cloudflare.d.ts.map +1 -1
  5. package/dist/adapters/cloudflare.js +53 -18
  6. package/dist/adapters/cloudflare.js.map +1 -1
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +65 -12
  9. package/dist/build.js.map +1 -1
  10. package/dist/built-assets.d.ts.map +1 -1
  11. package/dist/built-assets.js +2 -2
  12. package/dist/built-assets.js.map +1 -1
  13. package/dist/cache-config.d.ts +1 -1
  14. package/dist/cache-config.d.ts.map +1 -1
  15. package/dist/cache-config.js.map +1 -1
  16. package/dist/cache.d.ts.map +1 -1
  17. package/dist/cache.js +18 -1
  18. package/dist/cache.js.map +1 -1
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +16 -6
  21. package/dist/client.js.map +1 -1
  22. package/dist/csrf.js +10 -6
  23. package/dist/csrf.js.map +1 -1
  24. package/dist/navigation.d.ts.map +1 -1
  25. package/dist/navigation.js +14 -1
  26. package/dist/navigation.js.map +1 -1
  27. package/dist/render.d.ts +2 -0
  28. package/dist/render.d.ts.map +1 -1
  29. package/dist/render.js +81 -38
  30. package/dist/render.js.map +1 -1
  31. package/dist/vite.d.ts.map +1 -1
  32. package/dist/vite.js +5 -3
  33. package/dist/vite.js.map +1 -1
  34. package/package.json +11 -11
  35. package/src/actions.ts +26 -9
  36. package/src/adapters/cloudflare.ts +98 -41
  37. package/src/build.ts +93 -30
  38. package/src/built-assets.ts +2 -3
  39. package/src/cache-config.ts +1 -0
  40. package/src/cache.ts +20 -1
  41. package/src/client.ts +16 -6
  42. package/src/csrf.ts +13 -6
  43. package/src/navigation.ts +16 -1
  44. package/src/render.ts +168 -70
  45. package/src/vite.ts +6 -3
package/src/csrf.ts CHANGED
@@ -8,7 +8,7 @@ const formFieldCsrf = "__mreact_csrf";
8
8
  export const formCsrfFieldName = formFieldCsrf;
9
9
 
10
10
  export function serverActionCookie(csrfToken: string): string {
11
- const production = isProductionEnvironment();
11
+ const secure = shouldUseSecureCsrfCookie();
12
12
  const parts = [
13
13
  `${currentCsrfCookieName()}=${encodeURIComponent(csrfToken)}`,
14
14
  "Path=/",
@@ -16,7 +16,7 @@ export function serverActionCookie(csrfToken: string): string {
16
16
  "HttpOnly",
17
17
  ];
18
18
 
19
- if (production) {
19
+ if (secure) {
20
20
  parts.push("Secure");
21
21
  }
22
22
 
@@ -97,16 +97,23 @@ function randomToken(): string {
97
97
  return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
98
98
  }
99
99
 
100
- function isProductionEnvironment(): boolean {
101
- return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
100
+ function isLocalCsrfEnvironment(): boolean {
101
+ return (
102
+ typeof process !== "undefined" &&
103
+ (process.env?.NODE_ENV === "development" || process.env?.NODE_ENV === "test")
104
+ );
105
+ }
106
+
107
+ function shouldUseSecureCsrfCookie(): boolean {
108
+ return !isLocalCsrfEnvironment();
102
109
  }
103
110
 
104
111
  function currentCsrfCookieName(): string {
105
- return isProductionEnvironment() ? csrfCookieNameProduction : csrfCookieNameDevelopment;
112
+ return shouldUseSecureCsrfCookie() ? csrfCookieNameProduction : csrfCookieNameDevelopment;
106
113
  }
107
114
 
108
115
  function csrfCookieNamesRead(): readonly string[] {
109
- return isProductionEnvironment()
116
+ return shouldUseSecureCsrfCookie()
110
117
  ? [csrfCookieNameProduction]
111
118
  : [csrfCookieNameProduction, csrfCookieNameDevelopment];
112
119
  }
package/src/navigation.ts CHANGED
@@ -31,11 +31,23 @@ function stripLeadingControlOrWhitespace(value: string): string {
31
31
  return value.slice(start);
32
32
  }
33
33
 
34
+ function containsControlCharacter(value: string): boolean {
35
+ for (let index = 0; index < value.length; index += 1) {
36
+ const code = value.charCodeAt(index);
37
+ if (code <= 0x1f || code === 0x7f) {
38
+ return true;
39
+ }
40
+ }
41
+
42
+ return false;
43
+ }
44
+
34
45
  // Returns true if `location` is safe to use as a same-origin Location header.
35
46
  // Allowed: path-absolute (`/foo`), query-only (`?x=1`), hash-only (`#x`),
36
47
  // relative (`foo`). Rejected: protocol-relative (`//evil`), backslash variants
37
48
  // (`/\evil`, `\\evil`), and anything with a scheme like `javascript:`.
38
49
  function isSafeInternalRedirect(location: string): boolean {
50
+ if (containsControlCharacter(location)) return false;
39
51
  const trimmed = stripLeadingControlOrWhitespace(location);
40
52
  if (trimmed === "") return false;
41
53
  if (trimmed.startsWith("//")) return false;
@@ -47,6 +59,7 @@ function isSafeInternalRedirect(location: string): boolean {
47
59
  }
48
60
 
49
61
  function isSafeExternalRedirect(location: string): boolean {
62
+ if (containsControlCharacter(location)) return false;
50
63
  const trimmed = stripLeadingControlOrWhitespace(location);
51
64
  const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed);
52
65
  if (match === null || match[1] === undefined) return false;
@@ -61,7 +74,9 @@ function throwUnsafeRedirect(location: string): never {
61
74
  }
62
75
 
63
76
  function throwUnsafeRewrite(location: string): never {
64
- throw new TypeError(`unsafe rewrite target: ${JSON.stringify(location)}`);
77
+ throw new TypeError(
78
+ `unsafe rewrite target: ${JSON.stringify(location)} - rewrite() only accepts same-origin paths, query strings, or hashes; external, protocol-relative, backslash, and control-character targets are rejected`,
79
+ );
65
80
  }
66
81
 
67
82
  /**
package/src/render.ts CHANGED
@@ -205,6 +205,7 @@ export interface RenderAppRequestOptions {
205
205
  routes?: readonly AppRoute[] | undefined;
206
206
  serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
207
207
  serverModuleCacheVersion?: string | undefined;
208
+ devServerModuleCacheVersion?: string | undefined;
208
209
  // True when serving through a dev server: request-time server transforms
209
210
  // are expected there and must not trigger the production prebuild warning.
210
211
  dev?: boolean | undefined;
@@ -251,10 +252,9 @@ export async function preloadBuiltRequestModules(options: {
251
252
  serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
252
253
  serverModuleCacheVersion: string;
253
254
  serverSourceFiles: ReadonlyMap<string, string>;
254
- serverActionReferencesByFile?: ReadonlyMap<
255
- string,
256
- readonly PreparedFormActionReference[]
257
- > | undefined;
255
+ serverActionReferencesByFile?:
256
+ | ReadonlyMap<string, readonly PreparedFormActionReference[]>
257
+ | undefined;
258
258
  vitePlugins?: readonly PluginOption[] | undefined;
259
259
  }): Promise<void> {
260
260
  const clientRouteInferenceCache = createClientRouteInferenceCache();
@@ -535,6 +535,8 @@ const routeOutOfOrderBoundaryAnalysisCache = new Map<string, Promise<boolean>>()
535
535
  const routeOutOfOrderBoundaryAnalysisCacheCounters = createRouterRuntimeCacheCounters();
536
536
  const routeLoaderModuleCache = new Map<string, Promise<RouteLoaderModule>>();
537
537
  const routeLoaderModuleCacheCounters = createRouterRuntimeCacheCounters();
538
+ const routeMetadataModuleCache = new Map<string, Promise<RouteMetadataModule>>();
539
+ const routeMetadataModuleCacheCounters = createRouterRuntimeCacheCounters();
538
540
  const middlewareModuleCache = new Map<string, Promise<MiddlewareModule>>();
539
541
  const middlewareModuleCacheCounters = createRouterRuntimeCacheCounters();
540
542
  const serverRouteModuleCache = new Map<string, Promise<Record<string, unknown>>>();
@@ -549,6 +551,7 @@ const maxRouteOutOfOrderBoundaryAnalysisCacheEntries = resolveRouterCacheLimit(
549
551
  512,
550
552
  );
551
553
  const maxRouteLoaderModuleCacheEntries = resolveRouterCacheLimit("ROUTE_LOADER_MODULE", 512);
554
+ const maxRouteMetadataModuleCacheEntries = resolveRouterCacheLimit("ROUTE_METADATA_MODULE", 512);
552
555
  const maxMiddlewareModuleCacheEntries = resolveRouterCacheLimit("MIDDLEWARE_MODULE", 64);
553
556
  const maxServerRouteModuleCacheEntries = resolveRouterCacheLimit("SERVER_ROUTE_MODULE", 512);
554
557
  const maxComposedRouteMetadataCacheEntries = resolveRouterCacheLimit(
@@ -588,6 +591,12 @@ export function routerRenderRuntimeCacheStats(): RouterRuntimeCacheStat[] {
588
591
  maxRouteLoaderModuleCacheEntries,
589
592
  routeLoaderModuleCacheCounters,
590
593
  ),
594
+ routerRuntimeCacheStat(
595
+ "route-metadata-module",
596
+ routeMetadataModuleCache,
597
+ maxRouteMetadataModuleCacheEntries,
598
+ routeMetadataModuleCacheCounters,
599
+ ),
591
600
  routerRuntimeCacheStat(
592
601
  "middleware-module",
593
602
  middlewareModuleCache,
@@ -718,9 +727,11 @@ function addRenderTimingPhaseDuration(
718
727
  }
719
728
 
720
729
  function routeLoaderMayReturnControlResponse(code: string): boolean {
721
- return /\b(?:redirect|redirectExternal|rewrite|notFound|throwNotFound)\s*\(/.test(code) ||
730
+ return (
731
+ /\b(?:redirect|redirectExternal|rewrite|notFound|throwNotFound)\s*\(/.test(code) ||
722
732
  /\bnew\s+Response\s*\(/.test(code) ||
723
- /\bResponse\.(?:redirect|json)\s*\(/.test(code);
733
+ /\bResponse\.(?:redirect|json)\s*\(/.test(code)
734
+ );
724
735
  }
725
736
 
726
737
  async function waitForRenderPreload(
@@ -811,7 +822,9 @@ export async function resolveAppRouterMiddleware(options: {
811
822
  return { response: middlewareResponse, type: "response" };
812
823
  }
813
824
 
814
- async function renderAppRequestInternal(options: RenderAppRequestRuntimeOptions): Promise<Response> {
825
+ async function renderAppRequestInternal(
826
+ options: RenderAppRequestRuntimeOptions,
827
+ ): Promise<Response> {
815
828
  warnProductionRenderWithoutPrebuiltModules(options);
816
829
  const timing = createRenderTiming(options.logger);
817
830
  const clientRouteInferenceCache =
@@ -822,7 +835,9 @@ async function renderAppRequestInternal(options: RenderAppRequestRuntimeOptions)
822
835
  const url = options.requestUrl ?? new URL(options.request.url);
823
836
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
824
837
  const matched =
825
- options.matchedRoute ?? options.routeMatcher?.match(url.pathname) ?? matchRoute(routes, url.pathname);
838
+ options.matchedRoute ??
839
+ options.routeMatcher?.match(url.pathname) ??
840
+ matchRoute(routes, url.pathname);
826
841
  finishRenderTimingPhase(timing, phaseStartedAt, "routeMatchMs");
827
842
  const hasMiddleware =
828
843
  options.skipMiddleware === true
@@ -1174,6 +1189,7 @@ async function renderAppRequestInternal(options: RenderAppRequestRuntimeOptions)
1174
1189
  options.vitePlugins,
1175
1190
  undefined,
1176
1191
  options.importPolicy,
1192
+ options.devServerModuleCacheVersion,
1177
1193
  ),
1178
1194
  );
1179
1195
  const pageHtml = renderedPage.html;
@@ -1230,6 +1246,7 @@ async function renderAppRequestInternal(options: RenderAppRequestRuntimeOptions)
1230
1246
  routes,
1231
1247
  serverModules: options.serverModules,
1232
1248
  serverModuleCacheVersion: options.serverModuleCacheVersion,
1249
+ devServerModuleCacheVersion: options.devServerModuleCacheVersion,
1233
1250
  serverSourceFiles: options.serverSourceFiles,
1234
1251
  define: options.define,
1235
1252
  vitePlugins: options.vitePlugins,
@@ -1448,6 +1465,7 @@ async function renderAppRequestInternal(options: RenderAppRequestRuntimeOptions)
1448
1465
  options.vitePlugins,
1449
1466
  timing,
1450
1467
  options.importPolicy,
1468
+ options.devServerModuleCacheVersion,
1451
1469
  ),
1452
1470
  );
1453
1471
  finishRenderTimingPhase(timing, phaseStartedAt, "pageRenderMs");
@@ -1510,12 +1528,13 @@ async function renderAppRequestInternal(options: RenderAppRequestRuntimeOptions)
1510
1528
  filename: matched.route.file,
1511
1529
  importPolicy: options.importPolicy,
1512
1530
  routes,
1513
- serverModules: options.serverModules,
1514
- serverModuleCacheVersion: options.serverModuleCacheVersion,
1515
- serverSourceFiles: options.serverSourceFiles,
1516
- define: options.define,
1517
- vitePlugins: options.vitePlugins,
1518
- });
1531
+ serverModules: options.serverModules,
1532
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1533
+ devServerModuleCacheVersion: options.devServerModuleCacheVersion,
1534
+ serverSourceFiles: options.serverSourceFiles,
1535
+ define: options.define,
1536
+ vitePlugins: options.vitePlugins,
1537
+ });
1519
1538
  finishRenderTimingPhase(timing, phaseStartedAt, "metadataMs");
1520
1539
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
1521
1540
  html = injectHeadMetadata(html, metadata);
@@ -2969,6 +2988,7 @@ async function runServerModuleWithSlots(
2969
2988
  vitePlugins?: readonly PluginOption[] | undefined,
2970
2989
  timing?: RenderTiming | undefined,
2971
2990
  importPolicy?: AppRouterImportPolicy | undefined,
2991
+ devServerModuleCacheVersion?: string | undefined,
2972
2992
  ): Promise<{ html: string; slots: Record<string, string> }> {
2973
2993
  const moduleLoadStartedAt = renderTimingPhaseStartedAt(timing);
2974
2994
  const module = await loadServerModule(
@@ -2979,6 +2999,7 @@ async function runServerModuleWithSlots(
2979
2999
  define,
2980
3000
  vitePlugins,
2981
3001
  importPolicy,
3002
+ devServerModuleCacheVersion,
2982
3003
  );
2983
3004
  finishRenderTimingPhase(timing, moduleLoadStartedAt, "pageModuleLoadMs");
2984
3005
  const component = selectServerComponent(module);
@@ -3003,6 +3024,7 @@ async function loadServerModule(
3003
3024
  define?: UserConfig["define"] | undefined,
3004
3025
  vitePlugins?: readonly PluginOption[] | undefined,
3005
3026
  importPolicy?: AppRouterImportPolicy | undefined,
3027
+ devServerModuleCacheVersion?: string | undefined,
3006
3028
  ): Promise<ServerModuleExports> {
3007
3029
  const artifact = serverModules?.get(sourcefile)?.string;
3008
3030
  const codeHash = memoizedHashText(code);
@@ -3020,11 +3042,14 @@ async function loadServerModule(
3020
3042
  });
3021
3043
  }
3022
3044
  const moduleCode =
3023
- prebuiltCode === undefined ? code : rewriteCompatVendorPlaceholderImportsForRunner(prebuiltCode);
3045
+ prebuiltCode === undefined
3046
+ ? code
3047
+ : rewriteCompatVendorPlaceholderImportsForRunner(prebuiltCode);
3048
+ const sourceModuleCacheVersion = serverModuleCacheVersion ?? devServerModuleCacheVersion;
3024
3049
  const cacheKey =
3025
- serverModuleCacheVersion === undefined
3050
+ sourceModuleCacheVersion === undefined
3026
3051
  ? undefined
3027
- : `server-component:${serverModuleCacheVersion}:${sourcefile}:${
3052
+ : `server-component:${serverModuleCacheVersion === undefined ? "dev" : "build"}:${sourceModuleCacheVersion}:${sourcefile}:${
3028
3053
  moduleCode === code ? codeHash : memoizedHashText(moduleCode)
3029
3054
  }:${importPolicyCacheKey(importPolicy)}:${viteDefineCacheKey(define)}:${vitePluginsCacheKey(vitePlugins)}`;
3030
3055
  return await importAppRouterSourceModule<ServerModuleExports>({
@@ -3663,7 +3688,12 @@ async function loadServerStreamModule(
3663
3688
  ): Promise<StreamModuleExports> {
3664
3689
  const artifactCode = serverModules?.get(sourcefile)?.stream;
3665
3690
  const codeHash = memoizedHashText(code);
3666
- const prebuiltCode = prebuiltServerComponentModuleCode(artifactCode, code, codeHash, outputOptions);
3691
+ const prebuiltCode = prebuiltServerComponentModuleCode(
3692
+ artifactCode,
3693
+ code,
3694
+ codeHash,
3695
+ outputOptions,
3696
+ );
3667
3697
  const usesPrebuiltCompiledCode = artifactCode?.code === code;
3668
3698
  if (
3669
3699
  artifactCode !== undefined &&
@@ -3678,7 +3708,9 @@ async function loadServerStreamModule(
3678
3708
  });
3679
3709
  }
3680
3710
  const moduleCode =
3681
- prebuiltCode === undefined ? code : rewriteCompatVendorPlaceholderImportsForRunner(prebuiltCode);
3711
+ prebuiltCode === undefined
3712
+ ? code
3713
+ : rewriteCompatVendorPlaceholderImportsForRunner(prebuiltCode);
3682
3714
  const cacheKey =
3683
3715
  serverModuleCacheVersion === undefined
3684
3716
  ? undefined
@@ -3817,19 +3849,19 @@ async function layoutShellsForPage(
3817
3849
  const slotContext = createSlotRenderContext(slots);
3818
3850
  const renderShell = (shell: ShellFile) =>
3819
3851
  renderShellPrefixSuffix(
3820
- appDir,
3821
- shell,
3822
- props,
3823
- slotContext,
3824
- serverModules,
3825
- serverModuleCacheVersion,
3826
- serverSourceFiles,
3827
- clientRouteInferenceCache,
3828
- undefined,
3829
- define,
3830
- vitePlugins,
3831
- importPolicy,
3832
- );
3852
+ appDir,
3853
+ shell,
3854
+ props,
3855
+ slotContext,
3856
+ serverModules,
3857
+ serverModuleCacheVersion,
3858
+ serverSourceFiles,
3859
+ clientRouteInferenceCache,
3860
+ undefined,
3861
+ define,
3862
+ vitePlugins,
3863
+ importPolicy,
3864
+ );
3833
3865
  const shells =
3834
3866
  Object.keys(slotContext.namedSlots).length === 0
3835
3867
  ? await Promise.all(layoutFiles.map(renderShell))
@@ -3881,8 +3913,7 @@ async function renderShellPrefixSuffix(
3881
3913
  }
3882
3914
  }
3883
3915
 
3884
- const staticEntry =
3885
- cacheKey === undefined ? undefined : shellStaticRenderCache.get(cacheKey);
3916
+ const staticEntry = cacheKey === undefined ? undefined : shellStaticRenderCache.get(cacheKey);
3886
3917
  const loadedStaticEntry =
3887
3918
  staticEntry ??
3888
3919
  (await loadShellStaticRenderEntry({
@@ -4065,19 +4096,28 @@ async function renderShellStreamComponent(
4065
4096
  // next request.
4066
4097
  const shellFilesCache = new Map<string, ShellFile[]>();
4067
4098
  const MAX_SHELL_FILES_CACHE_ENTRIES = 1024;
4068
- const devShellFilesCache = new Map<string, { directoryStats: readonly FileStat[]; files: ShellFile[] }>();
4099
+ const devShellFilesCache = new Map<
4100
+ string,
4101
+ { directoryStats: readonly FileStat[]; files: ShellFile[] }
4102
+ >();
4069
4103
  const shellStaticRenderCache = new Map<string, ShellStaticRenderEntry>();
4070
4104
  const MAX_SHELL_STATIC_RENDER_CACHE_ENTRIES = 1024;
4071
4105
  const routeMiddlewareControlCache = new Map<string, Promise<RouteMiddlewareControl | undefined>>();
4072
4106
  const MAX_ROUTE_MIDDLEWARE_CONTROL_CACHE_ENTRIES = 1024;
4073
4107
  const appMiddlewareFileCache = new Map<string, Promise<boolean>>();
4074
4108
  const MAX_APP_MIDDLEWARE_FILE_CACHE_ENTRIES = 1024;
4075
- const devAppMiddlewareFileCache = new Map<string, { directoryStat: FileStat; hasMiddleware: boolean }>();
4109
+ const devAppMiddlewareFileCache = new Map<
4110
+ string,
4111
+ { directoryStat: FileStat; hasMiddleware: boolean }
4112
+ >();
4076
4113
  const routeMiddlewareControlSourceCache = new Map<
4077
4114
  string,
4078
4115
  { control: RouteMiddlewareControl | undefined; mtimeMs: number }
4079
4116
  >();
4080
- const devServerSourceFileCache = new Map<string, { mtimeMs: number; size: number; source: string }>();
4117
+ const devServerSourceFileCache = new Map<
4118
+ string,
4119
+ { mtimeMs: number; size: number; source: string }
4120
+ >();
4081
4121
  const MAX_DEV_SERVER_SOURCE_FILE_CACHE_ENTRIES = 2048;
4082
4122
 
4083
4123
  interface FileStat {
@@ -4111,10 +4151,7 @@ async function shellFilesForPage(
4111
4151
  if (devCacheKey !== undefined) {
4112
4152
  const directoryStats = await routeShellDirectoryStats(appDir, pageFile);
4113
4153
  const cached = devShellFilesCache.get(devCacheKey);
4114
- if (
4115
- cached !== undefined &&
4116
- sameFileStats(cached.directoryStats, directoryStats)
4117
- ) {
4154
+ if (cached !== undefined && sameFileStats(cached.directoryStats, directoryStats)) {
4118
4155
  return cached.files;
4119
4156
  }
4120
4157
 
@@ -4194,10 +4231,7 @@ async function hasAppMiddleware(options: {
4194
4231
  if (cacheKey === undefined) {
4195
4232
  const directoryStat = await fileStat(options.appDir);
4196
4233
  const cached = devAppMiddlewareFileCache.get(options.appDir);
4197
- if (
4198
- cached !== undefined &&
4199
- sameFileStat(cached.directoryStat, directoryStat)
4200
- ) {
4234
+ if (cached !== undefined && sameFileStat(cached.directoryStat, directoryStat)) {
4201
4235
  return cached.hasMiddleware;
4202
4236
  }
4203
4237
 
@@ -4614,6 +4648,7 @@ async function loadRouteMetadata(options: {
4614
4648
  code: string;
4615
4649
  context: RouteMetadataContext;
4616
4650
  define?: UserConfig["define"] | undefined;
4651
+ devServerModuleCacheVersion?: string | undefined;
4617
4652
  filename: string;
4618
4653
  importPolicy?: AppRouterImportPolicy | undefined;
4619
4654
  serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
@@ -4640,22 +4675,85 @@ async function loadRouteMetadata(options: {
4640
4675
  return await resolveRouteMetadataModule(module, options.context);
4641
4676
  }
4642
4677
 
4643
- const code = prebuiltArtifact?.code ?? (await bundleRouteMetadataModuleCode(options));
4644
-
4645
- const module = await importAppRouterSourceModule<RouteMetadataModule>({
4646
- ...(options.serverModuleCacheVersion === undefined
4647
- ? {}
4648
- : {
4649
- cacheKey: `metadata:${options.filename}:${options.serverModuleCacheVersion}:${memoizedHashText(code)}:${viteDefineCacheKey(options.define)}`,
4650
- }),
4651
- code,
4652
- define: options.define,
4653
- label: `metadata:${options.filename}`,
4654
- });
4678
+ const module = await loadRouteMetadataModule(options, prebuiltArtifact);
4655
4679
 
4656
4680
  return await resolveRouteMetadataModule(module, options.context);
4657
4681
  }
4658
4682
 
4683
+ async function loadRouteMetadataModule(
4684
+ options: {
4685
+ appDir: string;
4686
+ code: string;
4687
+ define?: UserConfig["define"] | undefined;
4688
+ devServerModuleCacheVersion?: string | undefined;
4689
+ filename: string;
4690
+ serverModuleCacheVersion?: string | undefined;
4691
+ vitePlugins?: readonly PluginOption[] | undefined;
4692
+ },
4693
+ prebuiltArtifact: BuiltServerModuleArtifact["routeMetadata"] | undefined,
4694
+ ): Promise<RouteMetadataModule> {
4695
+ if (prebuiltArtifact?.moduleFile !== undefined) {
4696
+ return await importBuiltServerModuleFile<RouteMetadataModule>({
4697
+ file: prebuiltArtifact.moduleFile,
4698
+ label: `metadata:${options.filename}`,
4699
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
4700
+ });
4701
+ }
4702
+
4703
+ const sourceHash = memoizedHashText(options.code);
4704
+ const moduleCacheVersion =
4705
+ options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion;
4706
+ const moduleCacheKey =
4707
+ moduleCacheVersion === undefined
4708
+ ? undefined
4709
+ : `metadata-module:${options.serverModuleCacheVersion === undefined ? "dev" : "build"}:${options.filename}:${moduleCacheVersion}:${sourceHash}:${viteDefineCacheKey(options.define)}:${vitePluginsCacheKey(options.vitePlugins)}`;
4710
+
4711
+ if (moduleCacheKey !== undefined) {
4712
+ const cached = readRouterRuntimeCacheEntry(
4713
+ routeMetadataModuleCache,
4714
+ moduleCacheKey,
4715
+ routeMetadataModuleCacheCounters,
4716
+ );
4717
+ if (cached !== undefined) {
4718
+ return await cached;
4719
+ }
4720
+ }
4721
+
4722
+ const loaded = (async () => {
4723
+ const code = prebuiltArtifact?.code ?? (await bundleRouteMetadataModuleCode(options));
4724
+
4725
+ return await importAppRouterSourceModule<RouteMetadataModule>({
4726
+ ...(moduleCacheVersion === undefined
4727
+ ? {}
4728
+ : {
4729
+ cacheKey: `metadata:${options.serverModuleCacheVersion === undefined ? "dev" : "build"}:${options.filename}:${moduleCacheVersion}:${memoizedHashText(code)}:${viteDefineCacheKey(options.define)}`,
4730
+ }),
4731
+ code,
4732
+ define: options.define,
4733
+ label: `metadata:${options.filename}`,
4734
+ });
4735
+ })();
4736
+
4737
+ if (moduleCacheKey !== undefined) {
4738
+ setBoundedCacheEntry(
4739
+ routeMetadataModuleCache,
4740
+ moduleCacheKey,
4741
+ loaded,
4742
+ maxRouteMetadataModuleCacheEntries,
4743
+ routeMetadataModuleCacheCounters,
4744
+ );
4745
+ }
4746
+
4747
+ try {
4748
+ return await loaded;
4749
+ } catch (error) {
4750
+ if (moduleCacheKey !== undefined) {
4751
+ routeMetadataModuleCache.delete(moduleCacheKey);
4752
+ }
4753
+ throw error;
4754
+ }
4755
+ }
4756
+
4659
4757
  async function resolveRouteMetadataModule(
4660
4758
  module: RouteMetadataModule,
4661
4759
  context: RouteMetadataContext,
@@ -4727,6 +4825,7 @@ async function loadComposedRouteMetadata(options: {
4727
4825
  code: string;
4728
4826
  context: RouteMetadataContext;
4729
4827
  define?: UserConfig["define"] | undefined;
4828
+ devServerModuleCacheVersion?: string | undefined;
4730
4829
  filename: string;
4731
4830
  importPolicy?: AppRouterImportPolicy | undefined;
4732
4831
  routes: readonly AppRoute[];
@@ -4772,14 +4871,18 @@ function composedRouteMetadataCacheKey(options: {
4772
4871
  appDir: string;
4773
4872
  code: string;
4774
4873
  define?: UserConfig["define"] | undefined;
4874
+ devServerModuleCacheVersion?: string | undefined;
4775
4875
  filename: string;
4776
4876
  importPolicy?: AppRouterImportPolicy | undefined;
4777
4877
  serverModuleCacheVersion?: string | undefined;
4778
4878
  vitePlugins?: readonly PluginOption[] | undefined;
4779
4879
  }): string | undefined {
4780
- return options.serverModuleCacheVersion === undefined
4880
+ const moduleCacheVersion =
4881
+ options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion;
4882
+
4883
+ return moduleCacheVersion === undefined
4781
4884
  ? undefined
4782
- : `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}\0${importPolicyCacheKey(options.importPolicy)}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
4885
+ : `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion === undefined ? "dev" : "build"}\0${moduleCacheVersion}\0${memoizedHashText(options.code)}\0${importPolicyCacheKey(options.importPolicy)}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
4783
4886
  }
4784
4887
 
4785
4888
  export const __composedRouteMetadataCacheKeyForTesting = composedRouteMetadataCacheKey;
@@ -4789,6 +4892,7 @@ async function loadComposedRouteMetadataUncached(options: {
4789
4892
  code: string;
4790
4893
  context: RouteMetadataContext;
4791
4894
  define?: UserConfig["define"] | undefined;
4895
+ devServerModuleCacheVersion?: string | undefined;
4792
4896
  filename: string;
4793
4897
  importPolicy?: AppRouterImportPolicy | undefined;
4794
4898
  routes: readonly AppRoute[];
@@ -4822,6 +4926,7 @@ async function loadComposedRouteMetadataUncached(options: {
4822
4926
  importPolicy: options.importPolicy,
4823
4927
  serverModules: options.serverModules,
4824
4928
  serverModuleCacheVersion: options.serverModuleCacheVersion,
4929
+ devServerModuleCacheVersion: options.devServerModuleCacheVersion,
4825
4930
  define: options.define,
4826
4931
  vitePlugins: options.vitePlugins,
4827
4932
  });
@@ -4836,6 +4941,7 @@ async function loadComposedRouteMetadataUncached(options: {
4836
4941
  importPolicy: options.importPolicy,
4837
4942
  serverModules: options.serverModules,
4838
4943
  serverModuleCacheVersion: options.serverModuleCacheVersion,
4944
+ devServerModuleCacheVersion: options.devServerModuleCacheVersion,
4839
4945
  define: options.define,
4840
4946
  vitePlugins: options.vitePlugins,
4841
4947
  });
@@ -5083,9 +5189,7 @@ function injectQueryState(html: string, state: DehydratedQueryClient): string {
5083
5189
  JSON.stringify(state),
5084
5190
  )}</script>`;
5085
5191
 
5086
- return /<\/body>/i.test(html)
5087
- ? replaceFinalBodyCloseTag(html, script)
5088
- : `${html}${script}`;
5192
+ return /<\/body>/i.test(html) ? replaceFinalBodyCloseTag(html, script) : `${html}${script}`;
5089
5193
  }
5090
5194
 
5091
5195
  function injectAuthSessionClaims(html: string, claims: unknown): string {
@@ -5097,9 +5201,7 @@ function injectAuthSessionClaims(html: string, claims: unknown): string {
5097
5201
  JSON.stringify(claims),
5098
5202
  )}</script>`;
5099
5203
 
5100
- return /<\/body>/i.test(html)
5101
- ? replaceFinalBodyCloseTag(html, script)
5102
- : `${html}${script}`;
5204
+ return /<\/body>/i.test(html) ? replaceFinalBodyCloseTag(html, script) : `${html}${script}`;
5103
5205
  }
5104
5206
 
5105
5207
  function replaceFinalBodyCloseTag(html: string, insertion: string): string {
@@ -5183,11 +5285,7 @@ function readServerSourceFile(
5183
5285
  async function readDevServerSourceFile(file: string): Promise<string> {
5184
5286
  const stats = await fileStat(file);
5185
5287
  const cached = devServerSourceFileCache.get(file);
5186
- if (
5187
- cached !== undefined &&
5188
- cached.mtimeMs === stats.mtimeMs &&
5189
- cached.size === stats.size
5190
- ) {
5288
+ if (cached !== undefined && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
5191
5289
  return cached.source;
5192
5290
  }
5193
5291
 
package/src/vite.ts CHANGED
@@ -69,6 +69,7 @@ export interface AppRouterViteMiddlewareOptions extends AppRouterProjectOptions
69
69
 
70
70
  type AppRouterViteRuntimeMiddlewareOptions = AppRouterViteMiddlewareOptions & {
71
71
  clientRouteInferenceCache?: ClientRouteInferenceCache | undefined;
72
+ devServerModuleCacheVersion?: (() => string | undefined) | undefined;
72
73
  navigationScanVitePlugins?: readonly PluginOption[] | undefined;
73
74
  viteDevServer?: ViteDevServer | undefined;
74
75
  };
@@ -193,6 +194,7 @@ export function createAppRouterVitePlugin(options: AppRouterVitePluginOptions):
193
194
  // built-in CSS plugin) whose `transform` requires a dev-server environment the
194
195
  // lightweight navigation scan cannot provide. Mirrors what the build forwards.
195
196
  let userVitePlugins: readonly PluginOption[] | undefined;
197
+ let devServerModuleCacheVersion = 0;
196
198
 
197
199
  const plugin: MreactRouterPlugin = {
198
200
  [mreactRouterConfigKey]: {
@@ -245,6 +247,7 @@ export function createAppRouterVitePlugin(options: AppRouterVitePluginOptions):
245
247
  const middlewareOptions: AppRouterViteRuntimeMiddlewareOptions = {
246
248
  ...options,
247
249
  define: server.config.define,
250
+ devServerModuleCacheVersion: () => `dev:${devServerModuleCacheVersion}`,
248
251
  navigationScanVitePlugins: userVitePlugins ?? [],
249
252
  viteDevServer: server,
250
253
  vitePlugins: server.config.plugins,
@@ -260,6 +263,7 @@ export function createAppRouterVitePlugin(options: AppRouterVitePluginOptions):
260
263
  return;
261
264
  }
262
265
 
266
+ devServerModuleCacheVersion += 1;
263
267
  const timestamp = Date.now();
264
268
  const updates = Array.from(context.server.moduleGraph.idToModuleMap.values())
265
269
  .filter((moduleNode) => isMreactClientDevModuleId(moduleNode.id))
@@ -586,6 +590,7 @@ async function handleAppRouterViteRequest(
586
590
  routeMatcher,
587
591
  routes,
588
592
  serverActions: options.serverActions,
593
+ devServerModuleCacheVersion: options.devServerModuleCacheVersion?.(),
589
594
  vitePlugins: routeTransformPlugins,
590
595
  }),
591
596
  );
@@ -907,9 +912,7 @@ async function devNavigationScripts(
907
912
  console.warn(formatClientRouteInferenceDiagnostic(navigationRuntimeDiagnostic));
908
913
  }
909
914
 
910
- return navigation
911
- ? ([route.path, navigationRuntimeScriptForDev()] as const)
912
- : undefined;
915
+ return navigation ? ([route.path, navigationRuntimeScriptForDev()] as const) : undefined;
913
916
  }),
914
917
  );
915
918