@reckona/mreact-router 0.0.182 → 0.0.183

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/build.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import type { Dirent } from "node:fs";
3
4
  import {
4
5
  copyFile,
@@ -108,7 +109,8 @@ const nativeEscapeTransform = {
108
109
  batchImportSource: "@reckona/mreact-router/native-escape",
109
110
  } as const;
110
111
 
111
- const defaultBuildConcurrency = Math.max(1, Math.min(2, availableParallelism()));
112
+ const maxDefaultBuildConcurrency = 8;
113
+ const buildConcurrencyStorage = new AsyncLocalStorage<number>();
112
114
  const serverArtifactFilesystemConcurrency = 2;
113
115
 
114
116
  type ServerTransformOutput = ReturnType<typeof transform>;
@@ -219,6 +221,29 @@ export interface BuildAppResult {
219
221
  routes: AppRoute[];
220
222
  }
221
223
 
224
+ interface IncrementalBuildCacheManifest {
225
+ fingerprint: string;
226
+ version: 1;
227
+ }
228
+
229
+ interface IncrementalBuildServerManifestOutputs {
230
+ serverModuleFiles?: Record<string, string>;
231
+ serverModuleRenderFiles?: Record<string, string>;
232
+ serverModuleRequestFiles?: Record<string, string>;
233
+ }
234
+
235
+ interface IncrementalBuildClientManifestOutputs {
236
+ assets?: readonly string[];
237
+ publicAssets?: readonly string[];
238
+ routes: readonly {
239
+ css?: readonly string[];
240
+ imports?: readonly string[];
241
+ navigationScript?: string | undefined;
242
+ script?: string | undefined;
243
+ sourceMap?: string | undefined;
244
+ }[];
245
+ }
246
+
222
247
  /**
223
248
  * Describes the generated import policy artifact consumed by built request handlers.
224
249
  */
@@ -377,10 +402,24 @@ type StaticParams = Record<string, string | number | boolean | readonly string[]
377
402
  * Use this from custom build scripts when the CLI is too coarse-grained; the returned manifest paths describe the files written under `outDir`. The build reads route files, loaders, middleware, metadata, server actions, and client references, so callers should pass the same project root and source allow-list they expect to deploy.
378
403
  */
379
404
  export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult> {
405
+ const project = resolveAppRouterProjectOptions(options);
406
+ const buildConcurrency = resolveBuildConcurrency(
407
+ options.buildConcurrency ?? project.buildConcurrency,
408
+ );
409
+
410
+ return await buildConcurrencyStorage.run(
411
+ buildConcurrency,
412
+ async () => await buildAppWithResolvedProject(options, project),
413
+ );
414
+ }
415
+
416
+ async function buildAppWithResolvedProject(
417
+ options: BuildAppOptions,
418
+ project: ResolvedAppRouterProject,
419
+ ): Promise<BuildAppResult> {
380
420
  const timingSink = options.onBuildPhaseTiming;
381
421
  const progressSink = options.onBuildProgress;
382
422
  const shouldTrackBuildPhases = timingSink !== undefined || progressSink !== undefined;
383
- const project = resolveAppRouterProjectOptions(options);
384
423
  const buildTargets = resolveBuildTargets(options.targets ?? project.buildTargets);
385
424
  const shouldBuildCloudflare = buildTargets.includes("cloudflare");
386
425
  const shouldBuildAwsLambda = buildTargets.includes("aws-lambda");
@@ -405,6 +444,14 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
405
444
  const serverDir = join(options.outDir, "server");
406
445
  const clientDir = join(options.outDir, "client");
407
446
  const cloudflareDir = join(options.outDir, "cloudflare");
447
+ const incrementalBuildFingerprint = await createIncrementalBuildCacheFingerprint({
448
+ buildTargets,
449
+ files,
450
+ project,
451
+ routes,
452
+ viteDefine,
453
+ vitePlugins,
454
+ });
408
455
  const sourceAnalysis =
409
456
  shouldTrackBuildPhases === false
410
457
  ? await analyzeBuildRouteSources({
@@ -452,6 +499,17 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
452
499
  );
453
500
  }
454
501
 
502
+ if (
503
+ incrementalBuildFingerprint !== undefined &&
504
+ (await isIncrementalBuildCacheHit({
505
+ buildTargets,
506
+ fingerprint: incrementalBuildFingerprint,
507
+ outDir: options.outDir,
508
+ }))
509
+ ) {
510
+ return { routes };
511
+ }
512
+
455
513
  if (shouldTrackBuildPhases === false) {
456
514
  await rm(options.outDir, { force: true, recursive: true });
457
515
  await Promise.all([
@@ -783,9 +841,284 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
783
841
  });
784
842
  }
785
843
 
844
+ if (incrementalBuildFingerprint !== undefined) {
845
+ await writeIncrementalBuildCacheManifest(options.outDir, incrementalBuildFingerprint);
846
+ }
847
+
786
848
  return { routes };
787
849
  }
788
850
 
851
+ const incrementalBuildCacheFilename = "build-cache.json";
852
+ const incrementalBuildCacheVersion = 1;
853
+
854
+ async function createIncrementalBuildCacheFingerprint(options: {
855
+ buildTargets: readonly AppRouterBuildTarget[];
856
+ files: Record<string, string>;
857
+ project: ResolvedAppRouterProject;
858
+ routes: readonly AppRoute[];
859
+ viteDefine: UserConfig["define"] | undefined;
860
+ vitePlugins: readonly PluginOption[] | undefined;
861
+ }): Promise<string | undefined> {
862
+ if (options.vitePlugins !== undefined && options.vitePlugins.length > 0) {
863
+ return undefined;
864
+ }
865
+
866
+ let define: string;
867
+ try {
868
+ const serializedDefine = JSON.stringify(options.viteDefine ?? null);
869
+ if (serializedDefine === undefined) {
870
+ return undefined;
871
+ }
872
+ define = serializedDefine;
873
+ } catch {
874
+ return undefined;
875
+ }
876
+
877
+ const [publicAssets, appConventionAssets] = await Promise.all([
878
+ collectBuildInputFileHashes(options.project.publicDir, options.project.projectRoot),
879
+ collectAppConventionAssetInputHashes(options.project.routesDir, options.project.projectRoot),
880
+ ]);
881
+ const sourceFiles = Object.keys(options.files)
882
+ .sort()
883
+ .map((file) => [normalizeBuildInputPath(file), hashText(options.files[file] ?? "")] as const);
884
+ const routes = options.routes
885
+ .map((route) => ({
886
+ file: normalizeBuildInputPath(relative(options.project.projectRoot, route.file)),
887
+ kind: route.kind,
888
+ path: route.path,
889
+ }))
890
+ .sort((left, right) => left.file.localeCompare(right.file));
891
+ const payload = {
892
+ appDir: normalizeBuildInputPath(relative(options.project.projectRoot, options.project.routesDir)),
893
+ assetBaseUrl: options.project.assetBaseUrl ?? null,
894
+ clientConsolePureFunctions: options.project.clientConsolePureFunctions ?? [],
895
+ clientSourceMaps: options.project.clientSourceMaps,
896
+ define,
897
+ nodeEnv: process.env.NODE_ENV ?? null,
898
+ publicAssetBaseUrl: options.project.publicAssetBaseUrl ?? null,
899
+ publicDir: normalizeBuildInputPath(relative(options.project.projectRoot, options.project.publicDir)),
900
+ routes,
901
+ sourceDirs: options.project.allowedSourceDirs
902
+ .map((directory) => normalizeBuildInputPath(relative(options.project.projectRoot, directory)))
903
+ .sort(),
904
+ sourceFiles,
905
+ targets: [...options.buildTargets].sort(),
906
+ version: incrementalBuildCacheVersion,
907
+ publicAssets,
908
+ appConventionAssets,
909
+ };
910
+
911
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
912
+ }
913
+
914
+ async function isIncrementalBuildCacheHit(options: {
915
+ buildTargets: readonly AppRouterBuildTarget[];
916
+ fingerprint: string;
917
+ outDir: string;
918
+ }): Promise<boolean> {
919
+ const cache = await readJsonBuildOutput<IncrementalBuildCacheManifest>(
920
+ join(options.outDir, incrementalBuildCacheFilename),
921
+ );
922
+ if (cache === undefined) {
923
+ return false;
924
+ }
925
+
926
+ return (
927
+ cache.version === incrementalBuildCacheVersion &&
928
+ cache.fingerprint === options.fingerprint &&
929
+ (await hasRequiredIncrementalBuildOutputs(options.outDir, options.buildTargets))
930
+ );
931
+ }
932
+
933
+ async function hasRequiredIncrementalBuildOutputs(
934
+ outDir: string,
935
+ buildTargets: readonly AppRouterBuildTarget[],
936
+ ): Promise<boolean> {
937
+ const serverManifestFile = join(outDir, "server", "manifest.json");
938
+ const clientManifestFile = join(outDir, "client", "manifest.json");
939
+ const requiredFiles = [
940
+ serverManifestFile,
941
+ join(outDir, "server", "import-policy.json"),
942
+ clientManifestFile,
943
+ join(outDir, "client", ".vite", "manifest.json"),
944
+ join(outDir, "routes.d.ts"),
945
+ join(outDir, "public-assets.d.ts"),
946
+ ...(buildTargets.includes("aws-lambda")
947
+ ? [join(outDir, "aws-lambda", "mreact-handler.mjs")]
948
+ : []),
949
+ ...(buildTargets.includes("cloudflare") ? [join(outDir, "cloudflare", "worker.mjs")] : []),
950
+ ];
951
+ const [serverManifest, clientManifest] = await Promise.all([
952
+ readJsonBuildOutput<IncrementalBuildServerManifestOutputs>(serverManifestFile),
953
+ readJsonBuildOutput<IncrementalBuildClientManifestOutputs>(clientManifestFile),
954
+ ]);
955
+ if (serverManifest === undefined || clientManifest === undefined) {
956
+ return false;
957
+ }
958
+
959
+ for (const file of [
960
+ ...Object.values(serverManifest.serverModuleFiles ?? {}),
961
+ ...Object.values(serverManifest.serverModuleRenderFiles ?? {}),
962
+ ...Object.values(serverManifest.serverModuleRequestFiles ?? {}),
963
+ ]) {
964
+ requiredFiles.push(join(outDir, "server", file));
965
+ }
966
+
967
+ for (const file of collectClientManifestOutputFiles(clientManifest)) {
968
+ requiredFiles.push(join(outDir, "client", file));
969
+ }
970
+
971
+ for (const file of requiredFiles) {
972
+ if (!(await isExistingFile(file))) {
973
+ return false;
974
+ }
975
+ }
976
+
977
+ return true;
978
+ }
979
+
980
+ function collectClientManifestOutputFiles(
981
+ manifest: IncrementalBuildClientManifestOutputs,
982
+ ): string[] {
983
+ const files = new Set<string>();
984
+
985
+ for (const asset of manifest.assets ?? []) {
986
+ files.add(asset);
987
+ }
988
+ for (const asset of manifest.publicAssets ?? []) {
989
+ files.add(asset);
990
+ files.add(`public/${asset}`);
991
+ }
992
+ for (const route of manifest.routes) {
993
+ for (const file of [
994
+ route.script,
995
+ route.sourceMap,
996
+ route.navigationScript,
997
+ ...(route.css ?? []),
998
+ ...(route.imports ?? []),
999
+ ]) {
1000
+ if (file !== undefined) {
1001
+ files.add(file);
1002
+ }
1003
+ }
1004
+ }
1005
+
1006
+ return [...files];
1007
+ }
1008
+
1009
+ async function readJsonBuildOutput<T>(file: string): Promise<T | undefined> {
1010
+ try {
1011
+ return JSON.parse(await readFile(file, "utf8")) as T;
1012
+ } catch (error) {
1013
+ if (
1014
+ (isNodeError(error) && error.code === "ENOENT") ||
1015
+ error instanceof SyntaxError
1016
+ ) {
1017
+ return undefined;
1018
+ }
1019
+
1020
+ throw error;
1021
+ }
1022
+ }
1023
+
1024
+ async function writeIncrementalBuildCacheManifest(
1025
+ outDir: string,
1026
+ fingerprint: string,
1027
+ ): Promise<void> {
1028
+ const cache = {
1029
+ fingerprint,
1030
+ version: incrementalBuildCacheVersion,
1031
+ } satisfies IncrementalBuildCacheManifest;
1032
+
1033
+ await writeFile(
1034
+ join(outDir, incrementalBuildCacheFilename),
1035
+ `${JSON.stringify(cache, null, 2)}\n`,
1036
+ );
1037
+ }
1038
+
1039
+ async function collectBuildInputFileHashes(
1040
+ directory: string,
1041
+ projectRoot: string,
1042
+ ): Promise<readonly (readonly [string, string])[]> {
1043
+ if (!(await isPublicAssetDirectory(directory))) {
1044
+ return [];
1045
+ }
1046
+
1047
+ const files = await collectFiles(directory);
1048
+ const hashes = await mapWithBuildConcurrency(
1049
+ files,
1050
+ async (file) =>
1051
+ [
1052
+ normalizeBuildInputPath(relative(projectRoot, file)),
1053
+ hashBuffer(await readFile(file)),
1054
+ ] as const,
1055
+ );
1056
+
1057
+ return hashes.sort(([left], [right]) => left.localeCompare(right));
1058
+ }
1059
+
1060
+ async function collectAppConventionAssetInputHashes(
1061
+ appDir: string,
1062
+ projectRoot: string,
1063
+ ): Promise<readonly (readonly [string, string])[]> {
1064
+ const entries = await readdir(appDir, { withFileTypes: true });
1065
+ const assetFiles = entries
1066
+ .filter((entry) => entry.isFile())
1067
+ .map((entry) => join(appDir, entry.name))
1068
+ .filter((file) => isAppFileConventionAsset(file, appDir));
1069
+ const hashes = await mapWithBuildConcurrency(
1070
+ assetFiles,
1071
+ async (file) =>
1072
+ [
1073
+ normalizeBuildInputPath(relative(projectRoot, file)),
1074
+ hashBuffer(await readFile(file)),
1075
+ ] as const,
1076
+ );
1077
+
1078
+ return hashes.sort(([left], [right]) => left.localeCompare(right));
1079
+ }
1080
+
1081
+ async function isExistingFile(file: string): Promise<boolean> {
1082
+ try {
1083
+ return (await stat(file)).isFile();
1084
+ } catch (error) {
1085
+ if (isNodeError(error) && error.code === "ENOENT") {
1086
+ return false;
1087
+ }
1088
+
1089
+ throw error;
1090
+ }
1091
+ }
1092
+
1093
+ function normalizeBuildInputPath(path: string): string {
1094
+ return path.split(sep).join("/");
1095
+ }
1096
+
1097
+ function hashBuffer(buffer: Buffer): string {
1098
+ return createHash("sha256").update(buffer).digest("hex").slice(0, 16);
1099
+ }
1100
+
1101
+ function defaultBuildConcurrency(): number {
1102
+ return Math.max(1, Math.min(maxDefaultBuildConcurrency, availableParallelism()));
1103
+ }
1104
+
1105
+ function currentBuildConcurrency(): number {
1106
+ return buildConcurrencyStorage.getStore() ?? defaultBuildConcurrency();
1107
+ }
1108
+
1109
+ function resolveBuildConcurrency(
1110
+ value: number | undefined,
1111
+ cores = availableParallelism(),
1112
+ ): number {
1113
+ const resolved = value ?? Math.max(1, Math.min(maxDefaultBuildConcurrency, cores));
1114
+
1115
+ if (!Number.isSafeInteger(resolved) || resolved < 1) {
1116
+ throw new Error("mreactRouter buildConcurrency must be a positive integer.");
1117
+ }
1118
+
1119
+ return resolved;
1120
+ }
1121
+
789
1122
  function typedRoutesDeclaration(routes: readonly AppRoute[]): string {
790
1123
  const routePaths = Array.from(
791
1124
  new Set(
@@ -803,21 +1136,60 @@ function typedRoutesDeclaration(routes: readonly AppRoute[]): string {
803
1136
  return left.localeCompare(right);
804
1137
  });
805
1138
  const routeUnion = routePaths.map((routePath) => JSON.stringify(routePath)).join(" | ");
1139
+ const routeParamEntries = routePaths.map(
1140
+ (routePath) => ` readonly ${JSON.stringify(routePath)}: ${routeParamsType(routePath)};`,
1141
+ );
806
1142
  return [
807
- `import type { AppRouteLinkHref as MreactAppRouteLinkHref } from "@reckona/mreact-router";`,
1143
+ `import type { AppRouteLinkHref as MreactAppRouteLinkHref, RouteParamsFor as MreactRouteParamsFor } from "@reckona/mreact-router";`,
808
1144
  ``,
809
1145
  `export type AppRoutePath = ${routeUnion === "" ? "never" : routeUnion};`,
810
1146
  `export type AppRouteHref = MreactAppRouteLinkHref<AppRoutePath>;`,
1147
+ `export type AppRouteParams<Path extends AppRoutePath> = MreactRouteParamsFor<Path>;`,
1148
+ `export interface AppRouteParamMap {`,
1149
+ ...(routeParamEntries.length === 0 ? [` readonly [path: string]: never;`] : routeParamEntries),
1150
+ `}`,
811
1151
  ``,
812
1152
  `declare module "@reckona/mreact-router/link" {`,
813
1153
  ` interface AppRouteDeclarations {`,
814
1154
  ` readonly path: AppRoutePath;`,
1155
+ ` readonly params: AppRouteParamMap;`,
815
1156
  ` }`,
816
1157
  `}`,
817
1158
  ``,
818
1159
  ].join("\n");
819
1160
  }
820
1161
 
1162
+ function routeParamsType(routePath: string): string {
1163
+ const params = routePath
1164
+ .split("/")
1165
+ .flatMap((segment): Array<{ catchAll: boolean; name: string }> => {
1166
+ if (segment.startsWith(":...")) {
1167
+ return [{ catchAll: true, name: segment.slice(4) }];
1168
+ }
1169
+
1170
+ if (segment.startsWith(":")) {
1171
+ return [{ catchAll: false, name: segment.slice(1) }];
1172
+ }
1173
+
1174
+ return [];
1175
+ });
1176
+
1177
+ if (params.length === 0) {
1178
+ return "Record<never, never>";
1179
+ }
1180
+
1181
+ return `{ ${params
1182
+ .map(
1183
+ (param) =>
1184
+ `readonly ${propertyKeyForType(param.name)}: ${param.catchAll ? "readonly string[]" : "string"}`,
1185
+ )
1186
+ .join("; ")} }`;
1187
+ }
1188
+
1189
+ function propertyKeyForType(value: string): string {
1190
+ return /^[A-Za-z_$][\w$]*$/.test(value) ? value : JSON.stringify(value);
1191
+ }
1192
+
821
1193
  function typedPublicAssetsDeclaration(publicAssets: readonly string[]): string {
822
1194
  const assetUnion = publicAssets.map((assetPath) => JSON.stringify(assetPath)).join(" | ");
823
1195
 
@@ -861,7 +1233,7 @@ function roundBuildPhaseMs(value: number): number {
861
1233
  async function mapWithBuildConcurrency<T, R>(
862
1234
  items: readonly T[],
863
1235
  map: (item: T, index: number) => Promise<R>,
864
- concurrency = defaultBuildConcurrency,
1236
+ concurrency = currentBuildConcurrency(),
865
1237
  ): Promise<R[]> {
866
1238
  if (items.length === 0) {
867
1239
  return [];
@@ -897,6 +1269,27 @@ export async function __mapWithBuildConcurrencyForTests<T, R>(
897
1269
  return await mapWithBuildConcurrency(items, map, concurrency);
898
1270
  }
899
1271
 
1272
+ async function mapServerOutputsWithBuildConcurrency<R>(
1273
+ serverOutputs: readonly ServerOutputMode[],
1274
+ map: (serverOutput: ServerOutputMode, index: number) => Promise<R>,
1275
+ ): Promise<R[]> {
1276
+ return await mapWithBuildConcurrency(serverOutputs, map, 1);
1277
+ }
1278
+
1279
+ export async function __mapServerOutputsWithBuildConcurrencyForTests<R>(
1280
+ serverOutputs: readonly ServerOutputMode[],
1281
+ map: (serverOutput: ServerOutputMode, index: number) => Promise<R>,
1282
+ ): Promise<R[]> {
1283
+ return await mapServerOutputsWithBuildConcurrency(serverOutputs, map);
1284
+ }
1285
+
1286
+ export function __resolveBuildConcurrencyForTests(
1287
+ value: number | undefined,
1288
+ cores: number,
1289
+ ): number {
1290
+ return resolveBuildConcurrency(value, cores);
1291
+ }
1292
+
900
1293
  async function analyzeBuildRouteSources(options: {
901
1294
  clientRouteInferenceCache: ClientRouteInferenceCache;
902
1295
  files: Record<string, string>;
@@ -2479,7 +2872,7 @@ async function buildServerModuleArtifacts(options: {
2479
2872
  });
2480
2873
  }
2481
2874
 
2482
- const serverOutputArtifacts = await mapWithBuildConcurrency(
2875
+ const serverOutputArtifacts = await mapServerOutputsWithBuildConcurrency(
2483
2876
  serverOutputs,
2484
2877
  async (serverOutput) => {
2485
2878
  const output = await transformServerRouteSource({
package/src/config.ts CHANGED
@@ -26,6 +26,7 @@ export interface AppRouterProductionOptions {
26
26
 
27
27
  export interface AppRouterProjectOptions {
28
28
  assetBaseUrl?: string | undefined;
29
+ buildConcurrency?: number | undefined;
29
30
  buildTargets?: readonly AppRouterBuildTarget[] | undefined;
30
31
  clientSourceMaps?: AppRouterClientSourceMapOption | undefined;
31
32
  /**
@@ -48,6 +49,7 @@ export interface AppRouterProjectOptions {
48
49
  export interface ResolvedAppRouterProject {
49
50
  allowedSourceDirs: readonly string[];
50
51
  assetBaseUrl?: string | undefined;
52
+ buildConcurrency?: number | undefined;
51
53
  buildTargets: readonly AppRouterBuildTarget[];
52
54
  clientSourceMaps: AppRouterClientSourceMapMode;
53
55
  clientConsolePureFunctions?: readonly string[] | undefined;
@@ -76,6 +78,9 @@ export function resolveAppRouterProjectOptions(
76
78
  resolveProjectPath(appDir, directory, "allowedSourceDirs"),
77
79
  ),
78
80
  ...(options.assetBaseUrl === undefined ? {} : { assetBaseUrl: options.assetBaseUrl }),
81
+ ...(options.buildConcurrency === undefined
82
+ ? {}
83
+ : { buildConcurrency: options.buildConcurrency }),
79
84
  buildTargets: resolveBuildTargets(options.buildTargets),
80
85
  clientSourceMaps: resolveClientSourceMapMode(options.clientSourceMaps),
81
86
  ...(clientConsolePureFunctions === undefined ? {} : { clientConsolePureFunctions }),
@@ -98,6 +103,9 @@ export function resolveAppRouterProjectOptions(
98
103
  resolveProjectPath(projectRoot, directory, "allowedSourceDirs"),
99
104
  ),
100
105
  ...(options.assetBaseUrl === undefined ? {} : { assetBaseUrl: options.assetBaseUrl }),
106
+ ...(options.buildConcurrency === undefined
107
+ ? {}
108
+ : { buildConcurrency: options.buildConcurrency }),
101
109
  buildTargets: resolveBuildTargets(options.buildTargets),
102
110
  clientSourceMaps: resolveClientSourceMapMode(options.clientSourceMaps),
103
111
  ...(clientConsolePureFunctions === undefined ? {} : { clientConsolePureFunctions }),
package/src/render.ts CHANGED
@@ -195,6 +195,7 @@ export interface RenderAppRequestOptions {
195
195
  instrumentation?: RouterInstrumentation | undefined;
196
196
  logger?: AppRouterLogger | undefined;
197
197
  navigationScripts?: ReadonlyMap<string, string> | undefined;
198
+ onRenderError?: ((error: unknown) => void) | undefined;
198
199
  onResponse?: AppRouterResponseHook | undefined;
199
200
  queryClient?: QueryClient | undefined;
200
201
  request: Request;
@@ -794,6 +795,7 @@ export type AppRouterMiddlewareResult =
794
795
  export async function resolveAppRouterMiddleware(options: {
795
796
  appDir: string;
796
797
  define?: UserConfig["define"] | undefined;
798
+ devServerModuleCacheVersion?: string | undefined;
797
799
  importPolicy?: AppRouterImportPolicy | undefined;
798
800
  instrumentation?: RouterInstrumentation | undefined;
799
801
  middlewareControl?: RouteMiddlewareControl | undefined;
@@ -862,6 +864,7 @@ async function renderAppRequestInternal(
862
864
  ? ({ request: options.request, type: "continue" } satisfies AppRouterMiddlewareResult)
863
865
  : await resolveAppRouterMiddleware({
864
866
  appDir: options.appDir,
867
+ devServerModuleCacheVersion: options.devServerModuleCacheVersion,
865
868
  importPolicy: options.importPolicy,
866
869
  instrumentation: options.instrumentation,
867
870
  middlewareControl,
@@ -1097,6 +1100,7 @@ async function renderAppRequestInternal(
1097
1100
  filename: matched.route.file,
1098
1101
  importPolicy: options.importPolicy,
1099
1102
  instrumentation: options.instrumentation,
1103
+ devServerModuleCacheVersion: options.devServerModuleCacheVersion,
1100
1104
  request: options.request,
1101
1105
  routeId: routeIdForPath(matched.route.path),
1102
1106
  routePath: matched.route.path,
@@ -1629,6 +1633,7 @@ async function renderAppRequestInternal(
1629
1633
  filename: "error.mreact.tsx",
1630
1634
  pageFile: matched.route.file,
1631
1635
  });
1636
+ options.onRenderError?.(error);
1632
1637
 
1633
1638
  const response = await renderSpecialRoute({
1634
1639
  appDir: options.appDir,
@@ -2370,6 +2375,7 @@ function devExternalSourceDirs(
2370
2375
  async function runMiddleware(options: {
2371
2376
  appDir: string;
2372
2377
  define?: UserConfig["define"] | undefined;
2378
+ devServerModuleCacheVersion?: string | undefined;
2373
2379
  importPolicy?: AppRouterImportPolicy | undefined;
2374
2380
  instrumentation?: RouterInstrumentation | undefined;
2375
2381
  middlewareControl?: RouteMiddlewareControl | undefined;
@@ -2440,6 +2446,7 @@ async function runMiddleware(options: {
2440
2446
  serverModules: options.serverModules,
2441
2447
  serverModuleCacheVersion: options.serverModuleCacheVersion,
2442
2448
  serverSourceFiles: options.serverSourceFiles,
2449
+ devServerModuleCacheVersion: options.devServerModuleCacheVersion,
2443
2450
  vitePlugins: options.vitePlugins,
2444
2451
  });
2445
2452
  } finally {
@@ -2538,6 +2545,7 @@ async function loadMiddlewareModule(options: {
2538
2545
  appDir: string;
2539
2546
  code?: string | undefined;
2540
2547
  define?: UserConfig["define"] | undefined;
2548
+ devServerModuleCacheVersion?: string | undefined;
2541
2549
  file: string;
2542
2550
  importPolicy?: AppRouterImportPolicy | undefined;
2543
2551
  serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
@@ -2552,10 +2560,12 @@ async function loadMiddlewareModule(options: {
2552
2560
  options.serverModuleCacheVersion,
2553
2561
  options.serverSourceFiles,
2554
2562
  ));
2563
+ const moduleCacheVersion =
2564
+ options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion;
2555
2565
  const cacheKey =
2556
- options.serverModuleCacheVersion === undefined
2566
+ moduleCacheVersion === undefined
2557
2567
  ? undefined
2558
- : `middleware\0${options.appDir}\0${options.file}\0${options.serverModuleCacheVersion}\0${memoizedHashText(code)}\0${importPolicyCacheKey(options.importPolicy)}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
2568
+ : `middleware\0${options.serverModuleCacheVersion === undefined ? "dev" : "build"}\0${options.appDir}\0${options.file}\0${moduleCacheVersion}\0${memoizedHashText(code)}\0${importPolicyCacheKey(options.importPolicy)}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
2559
2569
 
2560
2570
  if (cacheKey !== undefined) {
2561
2571
  const cached = readRouterRuntimeCacheEntry(
@@ -2576,6 +2586,7 @@ async function loadMiddlewareModule(options: {
2576
2586
  file: options.file,
2577
2587
  importPolicy: options.importPolicy,
2578
2588
  prebuiltArtifact: prebuiltRequestModuleArtifact(options.serverModules, options.file, code),
2589
+ devServerModuleCacheVersion: options.devServerModuleCacheVersion,
2579
2590
  serverModuleCacheVersion: options.serverModuleCacheVersion,
2580
2591
  vitePlugins: options.vitePlugins,
2581
2592
  }).catch((error) => {
@@ -2637,6 +2648,7 @@ async function loadBundledMiddlewareModule(options: {
2637
2648
  appDir: string;
2638
2649
  code: string;
2639
2650
  define?: UserConfig["define"] | undefined;
2651
+ devServerModuleCacheVersion?: string | undefined;
2640
2652
  file: string;
2641
2653
  importPolicy?: AppRouterImportPolicy | undefined;
2642
2654
  prebuiltArtifact?: BuiltServerModuleOutputLike | undefined;
@@ -2663,10 +2675,10 @@ async function loadBundledMiddlewareModule(options: {
2663
2675
  }));
2664
2676
 
2665
2677
  return importAppRouterSourceModule<MiddlewareModule>({
2666
- ...(options.serverModuleCacheVersion === undefined
2678
+ ...((options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion) === undefined
2667
2679
  ? {}
2668
2680
  : {
2669
- cacheKey: `middleware:${options.file}:${options.serverModuleCacheVersion}:${memoizedHashText(compiled)}:${viteDefineCacheKey(options.define)}:${vitePluginsCacheKey(options.vitePlugins)}`,
2681
+ cacheKey: `middleware:${options.serverModuleCacheVersion === undefined ? "dev" : "build"}:${options.file}:${options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion}:${memoizedHashText(compiled)}:${viteDefineCacheKey(options.define)}:${vitePluginsCacheKey(options.vitePlugins)}`,
2670
2682
  }),
2671
2683
  code: compiled,
2672
2684
  define: options.define,
@@ -4447,6 +4459,7 @@ async function loadRouteData(options: {
4447
4459
  code: string;
4448
4460
  context: RouteDataContext;
4449
4461
  define?: UserConfig["define"] | undefined;
4462
+ devServerModuleCacheVersion?: string | undefined;
4450
4463
  filename: string;
4451
4464
  importPolicy?: AppRouterImportPolicy | undefined;
4452
4465
  serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
@@ -4474,6 +4487,7 @@ async function loadRouteDataWithInstrumentation(options: {
4474
4487
  code: string;
4475
4488
  context: RouteDataContext;
4476
4489
  define?: UserConfig["define"] | undefined;
4490
+ devServerModuleCacheVersion?: string | undefined;
4477
4491
  filename: string;
4478
4492
  importPolicy?: AppRouterImportPolicy | undefined;
4479
4493
  instrumentation?: RouterInstrumentation | undefined;
@@ -4514,16 +4528,19 @@ async function loadRouteLoaderModule(options: {
4514
4528
  appDir: string;
4515
4529
  code: string;
4516
4530
  define?: UserConfig["define"] | undefined;
4531
+ devServerModuleCacheVersion?: string | undefined;
4517
4532
  filename: string;
4518
4533
  importPolicy?: AppRouterImportPolicy | undefined;
4519
4534
  serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
4520
4535
  serverModuleCacheVersion?: string | undefined;
4521
4536
  vitePlugins?: readonly PluginOption[] | undefined;
4522
4537
  }): Promise<RouteLoaderModule> {
4538
+ const moduleCacheVersion =
4539
+ options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion;
4523
4540
  const cacheKey =
4524
- options.serverModuleCacheVersion === undefined
4541
+ moduleCacheVersion === undefined
4525
4542
  ? undefined
4526
- : `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}\0${importPolicyCacheKey(options.importPolicy)}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
4543
+ : `${options.serverModuleCacheVersion === undefined ? "dev" : "build"}\0${options.appDir}\0${options.filename}\0${moduleCacheVersion}\0${memoizedHashText(options.code)}\0${importPolicyCacheKey(options.importPolicy)}\0${viteDefineCacheKey(options.define)}\0${vitePluginsCacheKey(options.vitePlugins)}`;
4527
4544
 
4528
4545
  if (cacheKey !== undefined) {
4529
4546
  const cached = readRouterRuntimeCacheEntry(
@@ -4602,6 +4619,7 @@ async function loadBundledRouteLoaderModule(options: {
4602
4619
  appDir: string;
4603
4620
  code: string;
4604
4621
  define?: UserConfig["define"] | undefined;
4622
+ devServerModuleCacheVersion?: string | undefined;
4605
4623
  filename: string;
4606
4624
  importPolicy?: AppRouterImportPolicy | undefined;
4607
4625
  prebuiltArtifact?: BuiltServerModuleOutputLike | undefined;
@@ -4632,10 +4650,10 @@ async function loadBundledRouteLoaderModule(options: {
4632
4650
  }));
4633
4651
 
4634
4652
  return await importAppRouterSourceModule<RouteLoaderModule>({
4635
- ...(options.serverModuleCacheVersion === undefined
4653
+ ...((options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion) === undefined
4636
4654
  ? {}
4637
4655
  : {
4638
- cacheKey: `loader:${options.filename}:${options.serverModuleCacheVersion}:${memoizedHashText(code)}:${viteDefineCacheKey(options.define)}`,
4656
+ cacheKey: `loader:${options.serverModuleCacheVersion === undefined ? "dev" : "build"}:${options.filename}:${options.serverModuleCacheVersion ?? options.devServerModuleCacheVersion}:${memoizedHashText(code)}:${viteDefineCacheKey(options.define)}:${vitePluginsCacheKey(options.vitePlugins)}`,
4639
4657
  }),
4640
4658
  code,
4641
4659
  define: options.define,