@reckona/mreact-router 0.0.138 → 0.0.139

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 +49 -12
  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 +49 -12
  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
@@ -142,7 +142,7 @@ export function mayUseAwaitBoundarySource(code: string): boolean {
142
142
 
143
143
  export function routeClosureMayUseAwaitBoundary(options: {
144
144
  filename: string;
145
- files: Record<string, string>;
145
+ files: RouteSourceLookup;
146
146
  projectRoot: string;
147
147
  seen?: Set<string> | undefined;
148
148
  source: string;
@@ -184,16 +184,13 @@ export function routeClosureMayUseAwaitBoundary(options: {
184
184
  continue;
185
185
  }
186
186
 
187
- const importedSource = options.files[resolved];
188
-
189
187
  if (
190
- importedSource !== undefined &&
191
188
  routeClosureMayUseAwaitBoundary({
192
- filename: resolved,
189
+ filename: resolved.filename,
193
190
  files: options.files,
194
191
  projectRoot: options.projectRoot,
195
192
  seen,
196
- source: importedSource,
193
+ source: resolved.source,
197
194
  })
198
195
  ) {
199
196
  return true;
@@ -243,10 +240,10 @@ function isRenderedStaticImportReference(
243
240
  }
244
241
 
245
242
  function resolveLocalSourceImport(
246
- files: Record<string, string>,
243
+ files: RouteSourceLookup,
247
244
  importer: string,
248
245
  specifier: string,
249
- ): string | undefined {
246
+ ): { filename: string; source: string } | undefined {
250
247
  if (!specifier.startsWith(".")) {
251
248
  return undefined;
252
249
  }
@@ -254,14 +251,21 @@ function resolveLocalSourceImport(
254
251
  const base = join(dirname(importer), specifier);
255
252
 
256
253
  for (const candidate of sourceModuleCandidates(base)) {
257
- if (files[candidate] !== undefined) {
258
- return candidate;
254
+ const source = readRouteSourceLookup(files, candidate);
255
+ if (source !== undefined) {
256
+ return { filename: candidate, source };
259
257
  }
260
258
  }
261
259
 
262
260
  return undefined;
263
261
  }
264
262
 
263
+ type RouteSourceLookup = Record<string, string> | ((file: string) => string | undefined);
264
+
265
+ function readRouteSourceLookup(files: RouteSourceLookup, file: string): string | undefined {
266
+ return typeof files === "function" ? files(file) : files[file];
267
+ }
268
+
265
269
  function sourceFilenameForCompiler(projectRoot: string, filename: string): string {
266
270
  return isAbsolute(filename) ? filename : join(projectRoot, filename);
267
271
  }
@@ -7,21 +7,27 @@ export async function collectRouteCssFiles(options: {
7
7
  appDir: string;
8
8
  pageFile: string;
9
9
  projectRoot: string;
10
+ readSource?: ((file: string) => Promise<string | undefined> | string | undefined) | undefined;
10
11
  }): Promise<string[]> {
11
12
  return await collectRouteCssFilesFromSources({
12
13
  ...options,
13
- readSource: (file) => readFile(file, "utf8"),
14
+ readSource: options.readSource ?? ((file) => readFile(file, "utf8")),
14
15
  });
15
16
  }
16
17
 
17
18
  export async function collectRouteCssFilesFromSources(options: {
18
19
  appDir: string;
20
+ isFile?: ((file: string) => Promise<boolean> | boolean) | undefined;
19
21
  pageFile: string;
20
22
  projectRoot: string;
21
23
  readSource: (file: string) => Promise<string | undefined> | string | undefined;
22
24
  }): Promise<string[]> {
23
- const shellFiles = (await existingRouteShellCandidates(options.appDir, options.pageFile, isFile))
24
- .map((candidate) => candidate.file);
25
+ const fileExists = options.isFile ?? isFile;
26
+ const shellFiles = (
27
+ await existingRouteShellCandidates(options.appDir, options.pageFile, async (file) =>
28
+ fileExists(file),
29
+ )
30
+ ).map((candidate) => candidate.file);
25
31
  const files = [...shellFiles, options.pageFile];
26
32
  const seen = new Set<string>();
27
33
  const cssFiles: string[] = [];
@@ -57,6 +63,7 @@ export async function collectRouteCssHrefs(options: {
57
63
  hrefPrefix?: string | undefined;
58
64
  pageFile: string;
59
65
  projectRoot: string;
66
+ readSource?: ((file: string) => Promise<string | undefined> | string | undefined) | undefined;
60
67
  }): Promise<string[]> {
61
68
  return (await collectRouteCssFiles(options)).map((file) => {
62
69
  const href = `/${relative(options.projectRoot, file).split(sep).join("/")}`;
@@ -65,6 +72,24 @@ export async function collectRouteCssHrefs(options: {
65
72
  });
66
73
  }
67
74
 
75
+ export function createCachedRouteSourceReader(
76
+ readSource: (file: string) => Promise<string | undefined> | string | undefined = (file) =>
77
+ readFile(file, "utf8"),
78
+ ): (file: string) => Promise<string | undefined> {
79
+ const cache = new Map<string, Promise<string | undefined>>();
80
+
81
+ return (file) => {
82
+ const cached = cache.get(file);
83
+ if (cached !== undefined) {
84
+ return cached;
85
+ }
86
+
87
+ const loaded = Promise.resolve(readSource(file));
88
+ cache.set(file, loaded);
89
+ return loaded;
90
+ };
91
+ }
92
+
68
93
  export async function collectSpecialBoundaryFiles(directory: string): Promise<string[]> {
69
94
  const entries = await readdir(directory, { withFileTypes: true });
70
95
  const files: string[] = [];
package/src/serve.ts CHANGED
@@ -9,7 +9,12 @@ import type {
9
9
  } from "./build.js";
10
10
  import type { AppRouterCache } from "./cache.js";
11
11
  import type { ClientRouteManifestEntry } from "./client-route-inference.js";
12
- import { createRouteMatcher, type AppRoute, type RouteMatcher } from "./routes.js";
12
+ import {
13
+ createRouteMatcher,
14
+ type AppRoute,
15
+ type MatchedRoute,
16
+ type RouteMatcher,
17
+ } from "./routes.js";
13
18
  import type { AppRouterServerActionOptions } from "./actions.js";
14
19
  import type { AppRouterImportPolicy } from "./import-policy.js";
15
20
  import {
@@ -76,6 +81,7 @@ interface BuiltRuntime {
76
81
  >;
77
82
  serverActionManifest?: readonly { moduleId: string; exportName: string; inferred?: boolean }[] | undefined;
78
83
  serverModuleArtifactLoads: Map<string, Promise<void>>;
84
+ serverModuleClosureFiles: Map<string, readonly string[]>;
79
85
  serverModuleFiles: ReadonlyMap<string, string>;
80
86
  serverModuleRenderFiles: ReadonlyMap<string, string>;
81
87
  serverModuleRequestFiles: ReadonlyMap<string, string>;
@@ -436,9 +442,11 @@ async function renderBuiltAppRequestWithRuntime(
436
442
  emitBuiltRenderTiming(options, request, timing, middlewareResult.response.status);
437
443
  return middlewareResult.response;
438
444
  }
439
- request = middlewareResult.request;
440
- normalizedPath = normalizeRoutePath(new URL(request.url).pathname);
441
- matched = options.runtime.routeMatcher.match(normalizedPath);
445
+ if (middlewareResult.request !== request) {
446
+ request = middlewareResult.request;
447
+ normalizedPath = normalizeRoutePath(new URL(request.url).pathname);
448
+ matched = options.runtime.routeMatcher.match(normalizedPath);
449
+ }
442
450
 
443
451
  if (request.method === "GET" && options.runtime.prerenderableRoutes.has(normalizedPath)) {
444
452
  await loadBuiltServerModuleArtifactsForRequest(options.runtime, matched?.route.file, {
@@ -450,7 +458,12 @@ async function renderBuiltAppRequestWithRuntime(
450
458
  await loadBuiltServerModuleArtifactsForRequest(options.runtime, matched?.route.file, {
451
459
  includeRender: false,
452
460
  });
453
- const response = await renderBuiltDynamicResponse({ ...options, request });
461
+ const response = await renderBuiltDynamicResponse({
462
+ ...options,
463
+ matchedRoute: matched,
464
+ request,
465
+ requestUrl: new URL(request.url),
466
+ });
454
467
 
455
468
  await applyBuiltPrerenderInvalidations(
456
469
  options.runtime,
@@ -1002,6 +1015,7 @@ async function materializeBuiltRuntime(options: {
1002
1015
  ? {}
1003
1016
  : { serverActionManifest: serverManifest.serverActionManifest }),
1004
1017
  serverModuleArtifactLoads: new Map(),
1018
+ serverModuleClosureFiles: new Map(),
1005
1019
  serverModuleFiles,
1006
1020
  serverModuleRenderFiles,
1007
1021
  serverModuleRequestFiles,
@@ -1273,34 +1287,63 @@ async function loadBuiltServerModuleArtifactClosure(
1273
1287
  seen: Set<string>,
1274
1288
  kind: BuiltServerModuleArtifactKind,
1275
1289
  ): Promise<void> {
1290
+ for (const closureFile of builtServerModuleClosureFiles(runtime, file)) {
1291
+ if (seen.has(closureFile)) {
1292
+ continue;
1293
+ }
1294
+ seen.add(closureFile);
1295
+ await loadBuiltServerModuleArtifact(runtime, closureFile, kind);
1296
+ }
1297
+ }
1298
+
1299
+ function builtServerModuleClosureFiles(
1300
+ runtime: BuiltRuntime,
1301
+ file: string,
1302
+ ): readonly string[] {
1303
+ const cached = runtime.serverModuleClosureFiles.get(file);
1304
+ if (cached !== undefined) {
1305
+ return cached;
1306
+ }
1307
+
1308
+ const closure: string[] = [];
1309
+ collectBuiltServerModuleClosureFiles(runtime, file, new Set(), closure);
1310
+ runtime.serverModuleClosureFiles.set(file, closure);
1311
+ return closure;
1312
+ }
1313
+
1314
+ function collectBuiltServerModuleClosureFiles(
1315
+ runtime: BuiltRuntime,
1316
+ file: string,
1317
+ seen: Set<string>,
1318
+ closure: string[],
1319
+ ): void {
1276
1320
  if (seen.has(file)) {
1277
1321
  return;
1278
1322
  }
1279
1323
  seen.add(file);
1280
-
1281
- await loadBuiltServerModuleArtifact(runtime, file, kind);
1324
+ closure.push(file);
1282
1325
 
1283
1326
  const source = runtime.serverSourceFiles.get(file);
1284
-
1285
1327
  if (source === undefined) {
1286
1328
  return;
1287
1329
  }
1288
1330
 
1289
1331
  for (const specifier of localServerModuleSpecifiers(source)) {
1290
1332
  const resolved = resolveBuiltLocalServerSourceImport(runtime, file, specifier);
1291
-
1292
1333
  if (resolved !== undefined) {
1293
- await loadBuiltServerModuleArtifactClosure(runtime, resolved, seen, kind);
1334
+ collectBuiltServerModuleClosureFiles(runtime, resolved, seen, closure);
1294
1335
  }
1295
1336
  }
1296
1337
  }
1297
1338
 
1339
+ const localServerModuleImportPattern =
1340
+ /\b(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s*)?["'](?<source>\.{1,2}\/[^"']+)["']/g;
1341
+
1298
1342
  function localServerModuleSpecifiers(code: string): string[] {
1299
1343
  const specifiers = new Set<string>();
1300
- const importPattern =
1301
- /\b(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s*)?["'](?<source>\.{1,2}\/[^"']+)["']/g;
1344
+ localServerModuleImportPattern.lastIndex = 0;
1302
1345
 
1303
- for (const match of code.matchAll(importPattern)) {
1346
+ for (const match of code.matchAll(localServerModuleImportPattern)) {
1304
1347
  const source = match.groups?.source;
1305
1348
 
1306
1349
  if (source !== undefined) {
@@ -1380,13 +1423,21 @@ async function readPrerenderedRoute(
1380
1423
  }
1381
1424
 
1382
1425
  function renderBuiltDynamicResponse(
1383
- options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
1426
+ options: RenderBuiltAppRequestOptions & {
1427
+ matchedRoute?: MatchedRoute | undefined;
1428
+ requestUrl?: URL | undefined;
1429
+ runtime: BuiltRuntime;
1430
+ },
1384
1431
  ): Promise<Response> {
1385
1432
  return renderAppRequest(builtRenderAppRequestOptions(options));
1386
1433
  }
1387
1434
 
1388
1435
  function builtRenderAppRequestOptions(
1389
- options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
1436
+ options: RenderBuiltAppRequestOptions & {
1437
+ matchedRoute?: MatchedRoute | undefined;
1438
+ requestUrl?: URL | undefined;
1439
+ runtime: BuiltRuntime;
1440
+ },
1390
1441
  ): RenderAppRequestOptions {
1391
1442
  const renderOptions: RenderAppRequestOptions & {
1392
1443
  __mreactLoadServerRenderArtifacts?: ((routeFile: string) => Promise<void>) | undefined;
@@ -1404,9 +1455,11 @@ function builtRenderAppRequestOptions(
1404
1455
  request: options.request,
1405
1456
  instrumentation: options.instrumentation,
1406
1457
  logger: options.logger,
1458
+ matchedRoute: options.matchedRoute,
1407
1459
  navigationScripts: options.runtime.navigationScripts,
1408
1460
  routeCache: options.routeCache,
1409
1461
  routeMatcher: options.runtime.routeMatcher,
1462
+ requestUrl: options.requestUrl,
1410
1463
  routes: options.runtime.routes,
1411
1464
  __mreactLoadServerRenderArtifacts: async (routeFile: string) => {
1412
1465
  await loadBuiltServerModuleArtifactsForRequest(options.runtime, routeFile, {
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { stat } from "node:fs/promises";
2
3
  import { dirname, relative, sep } from "node:path";
3
4
  import {
4
5
  analyzeBoundaryGraph,
@@ -10,6 +11,8 @@ import type * as Ts from "typescript";
10
11
 
11
12
  let ts = undefined as unknown as typeof Ts;
12
13
  let typescriptLoaded = false;
14
+ const runtimeInferenceCache = new Map<string, RuntimeInferenceCacheEntry>();
15
+ const maxRuntimeInferenceCacheEntries = 512;
13
16
 
14
17
  export interface InferredServerActionReference {
15
18
  exportName: string;
@@ -63,6 +66,16 @@ export async function collectRuntimeInferredServerActions(options: {
63
66
  return { diagnostics: [], references: new Map() };
64
67
  }
65
68
 
69
+ const sourceHash = formActionSourceHash(options.code);
70
+ const cacheKey = `${options.appDir}\0${options.pageFile}\0${sourceHash}`;
71
+ const cached = runtimeInferenceCache.get(cacheKey);
72
+ if (cached !== undefined && (await runtimeInferenceCacheEntryIsFresh(cached))) {
73
+ return {
74
+ diagnostics: cached.diagnostics.map((diagnostic) => ({ ...diagnostic })),
75
+ references: new Map(cached.references),
76
+ };
77
+ }
78
+
66
79
  await loadTypeScript();
67
80
  const program = ts.createProgram({
68
81
  host: createRuntimeProgramHost({
@@ -121,6 +134,17 @@ export async function collectRuntimeInferredServerActions(options: {
121
134
  }
122
135
  }
123
136
 
137
+ const dependencies = await runtimeProgramDependencyStats({
138
+ appDir: options.appDir,
139
+ pageFile: options.pageFile,
140
+ program,
141
+ });
142
+ setRuntimeInferenceCacheEntry(cacheKey, {
143
+ dependencies,
144
+ diagnostics: diagnostics.map((diagnostic) => ({ ...diagnostic })),
145
+ references: new Map(references),
146
+ });
147
+
124
148
  return { diagnostics, references };
125
149
  }
126
150
 
@@ -792,6 +816,67 @@ function createRuntimeProgramHost(options: {
792
816
  };
793
817
  }
794
818
 
819
+ interface RuntimeInferenceCacheEntry {
820
+ dependencies: readonly RuntimeInferenceDependency[];
821
+ diagnostics: readonly ServerActionInferenceDiagnostic[];
822
+ references: ReadonlyMap<string, InferredServerActionReference>;
823
+ }
824
+
825
+ interface RuntimeInferenceDependency {
826
+ file: string;
827
+ mtimeMs: number;
828
+ size: number;
829
+ }
830
+
831
+ async function runtimeProgramDependencyStats(options: {
832
+ appDir: string;
833
+ pageFile: string;
834
+ program: Ts.Program;
835
+ }): Promise<RuntimeInferenceDependency[]> {
836
+ const dependencies = options.program
837
+ .getSourceFiles()
838
+ .map((sourceFile) => sourceFile.fileName)
839
+ .filter((file) => file !== options.pageFile && isInsideAppDir(options.appDir, file));
840
+ const uniqueDependencies = [...new Set(dependencies)];
841
+
842
+ return await Promise.all(
843
+ uniqueDependencies.map(async (file) => {
844
+ const stats = await stat(file);
845
+ return { file, mtimeMs: stats.mtimeMs, size: stats.size };
846
+ }),
847
+ );
848
+ }
849
+
850
+ async function runtimeInferenceCacheEntryIsFresh(
851
+ entry: RuntimeInferenceCacheEntry,
852
+ ): Promise<boolean> {
853
+ for (const dependency of entry.dependencies) {
854
+ try {
855
+ const stats = await stat(dependency.file);
856
+ if (stats.mtimeMs !== dependency.mtimeMs || stats.size !== dependency.size) {
857
+ return false;
858
+ }
859
+ } catch {
860
+ return false;
861
+ }
862
+ }
863
+
864
+ return true;
865
+ }
866
+
867
+ function setRuntimeInferenceCacheEntry(
868
+ key: string,
869
+ entry: RuntimeInferenceCacheEntry,
870
+ ): void {
871
+ if (runtimeInferenceCache.size >= maxRuntimeInferenceCacheEntries) {
872
+ const oldestKey = runtimeInferenceCache.keys().next().value;
873
+ if (oldestKey !== undefined) {
874
+ runtimeInferenceCache.delete(oldestKey);
875
+ }
876
+ }
877
+ runtimeInferenceCache.set(key, entry);
878
+ }
879
+
795
880
  function defaultCompilerOptions(): Ts.CompilerOptions {
796
881
  return {
797
882
  allowJs: true,
@@ -819,6 +904,7 @@ export function __readServerActionInferenceTypeScriptLoadedForTests(): boolean {
819
904
  export function __resetServerActionInferenceTypeScriptForTests(): void {
820
905
  ts = undefined as unknown as typeof Ts;
821
906
  typescriptLoaded = false;
907
+ runtimeInferenceCache.clear();
822
908
  }
823
909
 
824
910
  function formActionOccurrenceKey(reference: {
package/src/vite.ts CHANGED
@@ -39,8 +39,12 @@ import {
39
39
  import { nodeRequestToWebRequest, sendResponse } from "./http.js";
40
40
  import { renderAppRequest } from "./render.js";
41
41
  import { stripRouteClientOnlyExports } from "./route-source.js";
42
- import { collectRouteCssHrefs, collectSpecialBoundaryFiles } from "./route-styles.js";
43
- import { scanAppRoutes } from "./routes.js";
42
+ import {
43
+ collectRouteCssHrefs,
44
+ collectSpecialBoundaryFiles,
45
+ createCachedRouteSourceReader,
46
+ } from "./route-styles.js";
47
+ import { createRouteMatcher, scanAppRoutes, type AppRoute } from "./routes.js";
44
48
  import { resolveRequestHost, type RequestHostPolicy } from "./serve.js";
45
49
  import { hasJsxSyntax } from "./source-jsx.js";
46
50
  import { workspacePackageFile } from "./workspace-packages.js";
@@ -521,6 +525,20 @@ async function handleAppRouterViteRequest(
521
525
 
522
526
  const request = nodeRequestToWebRequest(incoming, origin);
523
527
  const routeTransformPlugins = options.navigationScanVitePlugins ?? options.vitePlugins;
528
+ const routes = await scanAppRoutes({ appDir: project.routesDir });
529
+ const routeMatcher = createRouteMatcher(routes);
530
+ const readRouteSource = createCachedRouteSourceReader();
531
+ const [clientStyles, clientStylesByFile, navigationScripts] = await Promise.all([
532
+ devRouteStyles(project, routes, readRouteSource),
533
+ devSpecialRouteStyles(project, readRouteSource),
534
+ devNavigationScripts(
535
+ project.routesDir,
536
+ routes,
537
+ readRouteSource,
538
+ options.clientRouteInferenceCache,
539
+ routeTransformPlugins,
540
+ ),
541
+ ]);
524
542
 
525
543
  await sendResponse(
526
544
  outgoing,
@@ -532,15 +550,14 @@ async function handleAppRouterViteRequest(
532
550
  allowedSourceDirs: project.allowedSourceDirs,
533
551
  projectRoot: project.projectRoot,
534
552
  },
535
- clientStyles: await devRouteStyles(project),
536
- clientStylesByFile: await devSpecialRouteStyles(project),
537
- navigationScripts: await devNavigationScripts(
538
- project.routesDir,
539
- options.clientRouteInferenceCache,
540
- routeTransformPlugins,
541
- ),
553
+ clientStyles,
554
+ clientStylesByFile,
555
+ clientRouteInferenceCache: options.clientRouteInferenceCache,
556
+ navigationScripts,
542
557
  request,
543
558
  routeCache: options.routeCache,
559
+ routeMatcher,
560
+ routes,
544
561
  serverActions: options.serverActions,
545
562
  vitePlugins: routeTransformPlugins,
546
563
  }),
@@ -796,9 +813,11 @@ function clientAssetBuildErrorResponse(filename: string, error: unknown): Respon
796
813
 
797
814
  async function devRouteStyles(
798
815
  project: ResolvedAppRouterProject,
816
+ routes: readonly AppRoute[],
817
+ readSource: (file: string) => Promise<string | undefined>,
799
818
  ): Promise<ReadonlyMap<string, readonly string[]>> {
800
819
  const entries = await Promise.all(
801
- (await scanAppRoutes({ appDir: project.routesDir })).map(async (route) => {
820
+ routes.map(async (route) => {
802
821
  if (route.kind !== "page") {
803
822
  return undefined;
804
823
  }
@@ -808,6 +827,7 @@ async function devRouteStyles(
808
827
  hrefPrefix: devCssPrefix,
809
828
  pageFile: route.file,
810
829
  projectRoot: project.projectRoot,
830
+ readSource,
811
831
  });
812
832
 
813
833
  return hrefs.length === 0 ? undefined : ([route.path, hrefs as readonly string[]] as const);
@@ -822,6 +842,7 @@ async function devRouteStyles(
822
842
 
823
843
  async function devSpecialRouteStyles(
824
844
  project: ResolvedAppRouterProject,
845
+ readSource: (file: string) => Promise<string | undefined>,
825
846
  ): Promise<ReadonlyMap<string, readonly string[]>> {
826
847
  const entries = await Promise.all(
827
848
  (await collectSpecialBoundaryFiles(project.routesDir)).map(async (file) => {
@@ -830,6 +851,7 @@ async function devSpecialRouteStyles(
830
851
  hrefPrefix: devCssPrefix,
831
852
  pageFile: file,
832
853
  projectRoot: project.projectRoot,
854
+ readSource,
833
855
  });
834
856
 
835
857
  return hrefs.length === 0 ? undefined : ([file, hrefs as readonly string[]] as const);
@@ -844,17 +866,22 @@ async function devSpecialRouteStyles(
844
866
 
845
867
  async function devNavigationScripts(
846
868
  appDir: string,
869
+ routes: readonly AppRoute[],
870
+ readSource: (file: string) => Promise<string | undefined>,
847
871
  inferenceCache?: ClientRouteInferenceCache | undefined,
848
872
  vitePlugins?: readonly PluginOption[] | undefined,
849
873
  ): Promise<ReadonlyMap<string, string>> {
850
874
  const cache = inferenceCache ?? createClientRouteInferenceCache();
851
875
  const entries = await Promise.all(
852
- (await scanAppRoutes({ appDir })).map(async (route) => {
876
+ routes.map(async (route) => {
853
877
  if (route.kind !== "page") {
854
878
  return undefined;
855
879
  }
856
880
 
857
- const source = await readFile(route.file, "utf8");
881
+ const source = await readSource(route.file);
882
+ if (source === undefined) {
883
+ return undefined;
884
+ }
858
885
  const navigation = await resolveNavigationRuntime({
859
886
  appDir,
860
887
  cache,