@reckona/mreact-router 0.0.120 → 0.0.122

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/client.ts CHANGED
@@ -31,10 +31,7 @@ import {
31
31
  type RouterBundleAssetOutput,
32
32
  type RouterBundleChunkOutput,
33
33
  } from "./bundle-pipeline.js";
34
- import {
35
- resolveClientConsolePureFunctions,
36
- type AppRouterProductionOptions,
37
- } from "./config.js";
34
+ import { resolveClientConsolePureFunctions, type AppRouterProductionOptions } from "./config.js";
38
35
  import type { AppRoute } from "./routes.js";
39
36
  import { existingRouteShellCandidates } from "./route-shells.js";
40
37
  import {
@@ -116,6 +113,7 @@ interface ClientRouteSourceTransform {
116
113
  export interface ClientRouteInferenceResult {
117
114
  client: boolean;
118
115
  clientBoundaryImports: string[];
116
+ clientBoundaryFallbackImports: string[];
119
117
  diagnostics: ClientRouteInferenceDiagnostic[];
120
118
  }
121
119
 
@@ -265,6 +263,7 @@ export async function inferClientRouteModule(options: {
265
263
  client:
266
264
  mergedRouteInference.client || shellInferences.some((inference) => inference.client),
267
265
  clientBoundaryImports: mergedRouteInference.clientBoundaryImports,
266
+ clientBoundaryFallbackImports: mergedRouteInference.clientBoundaryFallbackImports,
268
267
  diagnostics: [
269
268
  ...mergedRouteInference.diagnostics,
270
269
  ...shellInferences.flatMap((inference) => inference.diagnostics),
@@ -325,6 +324,7 @@ function clientRouteInferenceFromBoundaryGraph(
325
324
  return {
326
325
  client: clientRoute || clientBoundaryImports.length > 0,
327
326
  clientBoundaryImports,
327
+ clientBoundaryFallbackImports: [],
328
328
  diagnostics: [],
329
329
  };
330
330
  }
@@ -338,6 +338,9 @@ function mergeClientRouteInference(
338
338
  clientBoundaryImports: Array.from(
339
339
  new Set([...left.clientBoundaryImports, ...right.clientBoundaryImports]),
340
340
  ),
341
+ clientBoundaryFallbackImports: Array.from(
342
+ new Set([...left.clientBoundaryFallbackImports, ...right.clientBoundaryFallbackImports]),
343
+ ),
341
344
  diagnostics: [...left.diagnostics, ...right.diagnostics],
342
345
  };
343
346
  }
@@ -466,6 +469,7 @@ export async function collectClientRouteReferences(options: {
466
469
  const output = transformCompilerModuleContext({
467
470
  code: source.code,
468
471
  clientBoundaryImports: source.inference.clientBoundaryImports,
472
+ clientBoundaryFallbackImports: source.inference.clientBoundaryFallbackImports,
469
473
  dev: false,
470
474
  filename: source.filename,
471
475
  moduleContext: source.moduleContext,
@@ -511,6 +515,7 @@ export async function collectClientRouteReferences(options: {
511
515
  routeInference.client ||
512
516
  sources.some((source) => source.filename !== options.filename && source.inference.client),
513
517
  clientBoundaryImports: routeInference.clientBoundaryImports,
518
+ clientBoundaryFallbackImports: routeInference.clientBoundaryFallbackImports,
514
519
  clientReferenceImports,
515
520
  clientReferenceManifest,
516
521
  diagnostics: sources
@@ -685,6 +690,7 @@ async function inferClientRouteModuleSource(options: {
685
690
 
686
691
  try {
687
692
  const clientBoundaryImports: string[] = [];
693
+ const clientBoundaryFallbackImports: string[] = [];
688
694
  const clientBoundaryExportNames = new Set<string>();
689
695
  const nestedClientExportNames = new Set<string>();
690
696
  const clientReferenceSourceFiles: string[] = [];
@@ -779,7 +785,10 @@ async function inferClientRouteModuleSource(options: {
779
785
  renderedComponentRoots,
780
786
  );
781
787
  if (
782
- matchesInferredExportNames(importedNavigationExportNames, imported.navigationLinkExportNames)
788
+ matchesInferredExportNames(
789
+ importedNavigationExportNames,
790
+ imported.navigationLinkExportNames,
791
+ )
783
792
  ) {
784
793
  usesNavigationLink = true;
785
794
  for (const exportName of renderedLocalExportNames(reference, exportInfo)) {
@@ -841,6 +850,9 @@ async function inferClientRouteModuleSource(options: {
841
850
  }
842
851
 
843
852
  clientBoundaryImports.push(reference.source);
853
+ if (!imported.clientBoundaryModule && isClientBoundaryFallbackEligibleSource(source)) {
854
+ clientBoundaryFallbackImports.push(reference.source);
855
+ }
844
856
  continue;
845
857
  }
846
858
 
@@ -933,6 +945,7 @@ async function inferClientRouteModuleSource(options: {
933
945
  clientProxy ||
934
946
  nestedClient,
935
947
  clientBoundaryImports,
948
+ clientBoundaryFallbackImports,
936
949
  clientBoundaryExportNames: Array.from(clientBoundaryExportNames),
937
950
  clientBoundaryModule: clientProxy || implicitModuleClient,
938
951
  nestedClientExportNames: Array.from(nestedClientExportNames),
@@ -956,6 +969,7 @@ function emptyClientRouteModuleInferenceResult(
956
969
  boundaryGraphFallbackRequired: false,
957
970
  client: false,
958
971
  clientBoundaryImports: [],
972
+ clientBoundaryFallbackImports: [],
959
973
  clientBoundaryExportNames: [],
960
974
  clientBoundaryModule: false,
961
975
  nestedClientExportNames: [],
@@ -992,7 +1006,12 @@ async function clientRouteModuleAnalysisForSource(options: {
992
1006
  })),
993
1007
  ),
994
1008
  );
995
- setLatestModuleCacheEntry(options.cache.moduleAnalysisByFile, options.filename, cacheKey, analysis);
1009
+ setLatestModuleCacheEntry(
1010
+ options.cache.moduleAnalysisByFile,
1011
+ options.filename,
1012
+ cacheKey,
1013
+ analysis,
1014
+ );
996
1015
  return analysis;
997
1016
  }
998
1017
 
@@ -1090,6 +1109,10 @@ function isStyleModuleSpecifier(source: string): boolean {
1090
1109
 
1091
1110
  const styleModuleExtensions = new Set([".css", ".less", ".sass", ".scss", ".styl", ".stylus"]);
1092
1111
 
1112
+ function isClientBoundaryFallbackEligibleSource(source: string): boolean {
1113
+ return !/\bon[A-Z][A-Za-z0-9_$]*\s*=/u.test(source) && !/\bglobalThis\b/u.test(source);
1114
+ }
1115
+
1093
1116
  function renderedImportedExportNames(
1094
1117
  reference: ClientRouteStaticImportReference,
1095
1118
  componentRoots: ReadonlySet<string>,
@@ -1227,14 +1250,14 @@ function startsUppercase(value: string): boolean {
1227
1250
  export function formatClientRouteInferenceDiagnostic(
1228
1251
  diagnostic: ClientRouteInferenceDiagnostic,
1229
1252
  ): string {
1230
- const route = diagnostic.routePath === undefined ? "" : ` on route ${JSON.stringify(diagnostic.routePath)}`;
1253
+ const route =
1254
+ diagnostic.routePath === undefined ? "" : ` on route ${JSON.stringify(diagnostic.routePath)}`;
1231
1255
  return `${diagnostic.code}: ${diagnostic.message}${route}`;
1232
1256
  }
1233
1257
 
1234
- function withClientRouteDiagnosticPath<T extends ClientRouteInferenceDiagnostic | ClientRouteInferenceResult>(
1235
- value: T,
1236
- routePath: string | undefined,
1237
- ): T {
1258
+ function withClientRouteDiagnosticPath<
1259
+ T extends ClientRouteInferenceDiagnostic | ClientRouteInferenceResult,
1260
+ >(value: T, routePath: string | undefined): T {
1238
1261
  if (routePath === undefined) {
1239
1262
  return value;
1240
1263
  }
@@ -411,7 +411,7 @@ async function transformServerSourceFile(
411
411
  console.warn(formatClientRouteInferenceDiagnostic(diagnostic));
412
412
  }
413
413
 
414
- const cacheKey = `${options.serverOutput}\0${options.dev ? "dev" : "prod"}\0${options.filename}\0${transformedSourceHash}\0${clientInference.clientBoundaryImports.join("\0")}\0${vitePluginsCacheKey(options.vitePlugins)}`;
414
+ const cacheKey = `${options.serverOutput}\0${options.dev ? "dev" : "prod"}\0${options.filename}\0${transformedSourceHash}\0${clientInference.clientBoundaryImports.join("\0")}\0${clientInference.clientBoundaryFallbackImports.join("\0")}\0${vitePluginsCacheKey(options.vitePlugins)}`;
415
415
  const cached = readRouterRuntimeCacheEntry(
416
416
  serverSourceTransformCache,
417
417
  cacheKey,
@@ -425,6 +425,7 @@ async function transformServerSourceFile(
425
425
  const output = transformCompilerModuleContext({
426
426
  code: source,
427
427
  clientBoundaryImports: clientInference.clientBoundaryImports,
428
+ clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
428
429
  dev: options.dev,
429
430
  filename: options.filename,
430
431
  moduleContext,
@@ -488,12 +489,20 @@ function withNodeRequireShimForEsmBundle(options: {
488
489
  return code;
489
490
  }
490
491
 
491
- return `${rewritten.needsNativeImport ? 'const __mreactNativeImport = Function("specifier", "return import(specifier)");\n' : ""}${needsFilenameGlobalShim ? `const __filename = ${JSON.stringify(options.filename)};
492
+ return `${rewritten.needsNativeImport ? 'const __mreactNativeImport = Function("specifier", "return import(specifier)");\n' : ""}${
493
+ needsFilenameGlobalShim
494
+ ? `const __filename = ${JSON.stringify(options.filename)};
492
495
  const __dirname = ${JSON.stringify(dirname(options.filename))};
493
- ` : ""}${rewritten.needsRequire || needsRequireShim ? `import { createRequire as __mreactCreateRequire } from "node:module";
496
+ `
497
+ : ""
498
+ }${
499
+ rewritten.needsRequire || needsRequireShim
500
+ ? `import { createRequire as __mreactCreateRequire } from "node:module";
494
501
  const __mreactRequire = __mreactCreateRequire(${JSON.stringify(requireBaseUrl)});
495
502
  const require = __mreactRequire;
496
- ` : ""}${code}`;
503
+ `
504
+ : ""
505
+ }${code}`;
497
506
  }
498
507
 
499
508
  function needsCommonJsFilenameGlobalShim(code: string): boolean {
@@ -535,7 +544,11 @@ function rewriteNodeModulesExternalImports(code: string): {
535
544
  }
536
545
 
537
546
  needsNativeImport = true;
538
- const rewritten = esmImportClauseToNativeImportStatements(clause.trim(), specifier, importIndex++);
547
+ const rewritten = esmImportClauseToNativeImportStatements(
548
+ clause.trim(),
549
+ specifier,
550
+ importIndex++,
551
+ );
539
552
  for (const [name, replacement] of rewritten.bindings) {
540
553
  nativeImportBindings.set(name, replacement);
541
554
  }
@@ -601,7 +614,9 @@ function isNodeModulesPath(file: string): boolean {
601
614
  function isNodeImportableModuleFile(file: string): boolean {
602
615
  const extension = extname(file);
603
616
 
604
- return extension === ".cjs" || extension === ".mjs" || extension === ".js" || extension === ".node";
617
+ return (
618
+ extension === ".cjs" || extension === ".mjs" || extension === ".js" || extension === ".node"
619
+ );
605
620
  }
606
621
 
607
622
  function isNodeCommonJsModuleFile(file: string): boolean {
@@ -676,7 +691,10 @@ function commonJsImportClauseToRequireStatements(
676
691
  const defaultName = clause.slice(0, commaIndex).trim();
677
692
  const namedOrNamespace = clause.slice(commaIndex + 1).trim();
678
693
  const temporaryName = `__mreactExternalCommonJs${importIndex}`;
679
- const statements = [`const ${temporaryName} = ${requireExpression};`, `const ${defaultName} = ${temporaryName};`];
694
+ const statements = [
695
+ `const ${temporaryName} = ${requireExpression};`,
696
+ `const ${defaultName} = ${temporaryName};`,
697
+ ];
680
698
 
681
699
  if (namedOrNamespace.startsWith("* as ")) {
682
700
  statements.push(`const ${namedOrNamespace.slice(5).trim()} = ${temporaryName};`);
@@ -737,10 +755,7 @@ function collectNamedEsmImportBindings(
737
755
  }
738
756
  }
739
757
 
740
- function namedCommonJsImportsToRequireStatements(
741
- clause: string,
742
- sourceExpression: string,
743
- ): string {
758
+ function namedCommonJsImportsToRequireStatements(clause: string, sourceExpression: string): string {
744
759
  const objectBindings: string[] = [];
745
760
  const statements: string[] = [];
746
761
 
package/src/render.ts CHANGED
@@ -159,6 +159,7 @@ export interface RenderAppRequestOptions {
159
159
  appDir: string;
160
160
  assetBaseUrl?: string | undefined;
161
161
  clientScripts?: ReadonlyMap<string, string>;
162
+ clientStylesByFile?: ReadonlyMap<string, readonly string[]>;
162
163
  clientStyles?: ReadonlyMap<string, readonly string[]>;
163
164
  env?: unknown;
164
165
  importPolicy?: AppRouterImportPolicy | undefined;
@@ -303,6 +304,7 @@ async function preloadBuiltPageRouteModules(options: {
303
304
  const stringOutput = transformServerModule({
304
305
  code: routeCode,
305
306
  clientBoundaryImports: options.analysis.clientInference.clientBoundaryImports,
307
+ clientBoundaryFallbackImports: options.analysis.clientInference.clientBoundaryFallbackImports,
306
308
  filename: options.file,
307
309
  serverModules: options.serverModules,
308
310
  serverOutput: "string",
@@ -321,6 +323,7 @@ async function preloadBuiltPageRouteModules(options: {
321
323
  const streamOutput = transformServerModule({
322
324
  code: routeCode,
323
325
  clientBoundaryImports: options.analysis.clientInference.clientBoundaryImports,
326
+ clientBoundaryFallbackImports: options.analysis.clientInference.clientBoundaryFallbackImports,
324
327
  filename: options.file,
325
328
  serverModules: options.serverModules,
326
329
  serverOutput: "stream",
@@ -800,6 +803,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
800
803
  const response = await renderSpecialRoute({
801
804
  appDir: options.appDir,
802
805
  assetBaseUrl: options.assetBaseUrl,
806
+ currentStyleSheets: options.clientStylesByFile?.get(notFoundFile),
803
807
  error: undefined,
804
808
  request: options.request,
805
809
  routePath: url.pathname,
@@ -1036,6 +1040,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1036
1040
  const stringOutput = transformServerModule({
1037
1041
  code: routeCode,
1038
1042
  clientBoundaryImports: clientInference.clientBoundaryImports,
1043
+ clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
1039
1044
  filename: matched.route.file,
1040
1045
  serverModules: options.serverModules,
1041
1046
  serverOutput: "string",
@@ -1179,6 +1184,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1179
1184
  const output = transformServerModule({
1180
1185
  code: routeCode,
1181
1186
  clientBoundaryImports: clientInference.clientBoundaryImports,
1187
+ clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
1182
1188
  filename: matched.route.file,
1183
1189
  serverModules: options.serverModules,
1184
1190
  serverOutput: "stream",
@@ -1285,6 +1291,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1285
1291
  const output = transformServerModule({
1286
1292
  code: routeCode,
1287
1293
  clientBoundaryImports: clientInference.clientBoundaryImports,
1294
+ clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
1288
1295
  filename: matched.route.file,
1289
1296
  serverModules: options.serverModules,
1290
1297
  serverOutput: "string",
@@ -1449,6 +1456,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1449
1456
  const response = await renderSpecialRoute({
1450
1457
  appDir: options.appDir,
1451
1458
  assetBaseUrl: options.assetBaseUrl,
1459
+ currentStyleSheets: options.clientStylesByFile?.get(notFoundFile),
1452
1460
  error: undefined,
1453
1461
  request: options.request,
1454
1462
  routePath: matched.route.path,
@@ -1475,6 +1483,7 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
1475
1483
  const response = await renderSpecialRoute({
1476
1484
  appDir: options.appDir,
1477
1485
  assetBaseUrl: options.assetBaseUrl,
1486
+ currentStyleSheets: options.clientStylesByFile?.get(errorFile),
1478
1487
  error,
1479
1488
  request: options.request,
1480
1489
  routePath: matched.route.path,
@@ -1737,6 +1746,7 @@ function boundaryFilenameCandidates(filename: string): string[] {
1737
1746
  async function renderSpecialRoute(options: {
1738
1747
  appDir: string;
1739
1748
  assetBaseUrl?: string | undefined;
1749
+ currentStyleSheets?: readonly string[] | undefined;
1740
1750
  error: unknown;
1741
1751
  navigation?:
1742
1752
  | {
@@ -1807,6 +1817,7 @@ async function renderSpecialRoute(options: {
1807
1817
  return new Response(
1808
1818
  `<!DOCTYPE html>${clientNavigationHeadTags({
1809
1819
  assetBaseUrl: options.assetBaseUrl,
1820
+ currentStyleSheets: options.currentStyleSheets,
1810
1821
  currentScript:
1811
1822
  options.navigation?.clientRoute === true ? options.navigation.script : undefined,
1812
1823
  routeScripts: options.routeScripts,
@@ -2396,6 +2407,7 @@ async function loadBundledMiddlewareModule(options: {
2396
2407
  function transformServerModule(options: {
2397
2408
  code: string;
2398
2409
  clientBoundaryImports?: readonly string[];
2410
+ clientBoundaryFallbackImports?: readonly string[];
2399
2411
  filename: string;
2400
2412
  serverModules?: ReadonlyMap<string, BuiltServerModuleArtifact> | undefined;
2401
2413
  serverOutput: ServerOutputMode;
@@ -2428,7 +2440,9 @@ function transformServerModule(options: {
2428
2440
  }
2429
2441
 
2430
2442
  const awaitHydrationKey = options.serverAwaitHydration === true ? "1" : "0";
2431
- const key = `${options.filename}\0${options.serverOutput}\0${sourceHash}\0${awaitHydrationKey}`;
2443
+ const boundaryKey = options.clientBoundaryImports?.join("\0") ?? "";
2444
+ const fallbackKey = options.clientBoundaryFallbackImports?.join("\0") ?? "";
2445
+ const key = `${options.filename}\0${options.serverOutput}\0${sourceHash}\0${awaitHydrationKey}\0${boundaryKey}\0${fallbackKey}`;
2432
2446
  const cached = readRouterRuntimeCacheEntry(
2433
2447
  serverTransformCache,
2434
2448
  key,
@@ -2444,6 +2458,9 @@ function transformServerModule(options: {
2444
2458
  ...(options.clientBoundaryImports === undefined
2445
2459
  ? {}
2446
2460
  : { clientBoundaryImports: options.clientBoundaryImports }),
2461
+ ...(options.clientBoundaryFallbackImports === undefined
2462
+ ? {}
2463
+ : { clientBoundaryFallbackImports: options.clientBoundaryFallbackImports }),
2447
2464
  dev: true,
2448
2465
  filename: options.filename,
2449
2466
  serverEscape: nativeEscapeTransform,
@@ -2523,6 +2540,7 @@ function routeSourceAnalysisFromArtifact(
2523
2540
  clientInference: {
2524
2541
  client: artifact.clientRoute,
2525
2542
  clientBoundaryImports: [...artifact.clientBoundaryImports],
2543
+ clientBoundaryFallbackImports: [...(artifact.clientBoundaryFallbackImports ?? [])],
2526
2544
  diagnostics: [],
2527
2545
  },
2528
2546
  hasLoader: artifact.hasLoader,
@@ -3477,7 +3495,12 @@ async function renderShellPrefixSuffix(
3477
3495
  const artifact = serverModules?.get(shell.file)?.[serverOutput];
3478
3496
  const clientInference =
3479
3497
  artifact !== undefined && artifact.sourceHash === memoizedHashText(code)
3480
- ? { client: false, clientBoundaryImports: [], diagnostics: [] }
3498
+ ? {
3499
+ client: false,
3500
+ clientBoundaryImports: [],
3501
+ clientBoundaryFallbackImports: [],
3502
+ diagnostics: [],
3503
+ }
3481
3504
  : await inferClientRouteModule({
3482
3505
  cache: clientRouteInferenceCache,
3483
3506
  code: stripRouteClientOnlyExports(code, shell.file),
@@ -3490,6 +3513,7 @@ async function renderShellPrefixSuffix(
3490
3513
  const output = transformServerModule({
3491
3514
  code,
3492
3515
  clientBoundaryImports: clientInference.clientBoundaryImports,
3516
+ clientBoundaryFallbackImports: clientInference.clientBoundaryFallbackImports,
3493
3517
  filename: shell.file,
3494
3518
  serverModules,
3495
3519
  serverOutput,
@@ -1,5 +1,5 @@
1
- import { access, readFile } from "node:fs/promises";
2
- import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
1
+ import { access, readdir, readFile } from "node:fs/promises";
2
+ import { dirname, join, isAbsolute, relative, resolve, sep } from "node:path";
3
3
  import { collectStaticImportReferences } from "@reckona/mreact-compiler";
4
4
  import { existingRouteShellCandidates } from "./route-shells.js";
5
5
 
@@ -65,6 +65,34 @@ export async function collectRouteCssHrefs(options: {
65
65
  });
66
66
  }
67
67
 
68
+ export async function collectSpecialBoundaryFiles(directory: string): Promise<string[]> {
69
+ const entries = await readdir(directory, { withFileTypes: true });
70
+ const files: string[] = [];
71
+
72
+ for (const entry of entries) {
73
+ const path = join(directory, entry.name);
74
+
75
+ if (entry.isDirectory()) {
76
+ if (entry.name === ".vite" || entry.name === "node_modules") {
77
+ continue;
78
+ }
79
+
80
+ files.push(...(await collectSpecialBoundaryFiles(path)));
81
+ continue;
82
+ }
83
+
84
+ if (entry.isFile() && isSpecialBoundaryFilename(entry.name)) {
85
+ files.push(path);
86
+ }
87
+ }
88
+
89
+ return files.sort();
90
+ }
91
+
92
+ function isSpecialBoundaryFilename(filename: string): boolean {
93
+ return /^(?:error|not-found)(?:\.mreact)?\.tsx$/u.test(filename);
94
+ }
95
+
68
96
  function resolveCssImport(options: {
69
97
  importer: string;
70
98
  projectRoot: string;
package/src/serve.ts CHANGED
@@ -41,6 +41,7 @@ interface BuiltRuntime {
41
41
  allowedSourceDirs: readonly string[];
42
42
  assetBaseUrl?: string | undefined;
43
43
  clientScripts: ReadonlyMap<string, string>;
44
+ clientStylesByFile: ReadonlyMap<string, readonly string[]>;
44
45
  clientStyles: ReadonlyMap<string, readonly string[]>;
45
46
  hasMiddleware: boolean;
46
47
  navigationScripts: ReadonlyMap<string, string>;
@@ -763,6 +764,7 @@ async function materializeBuiltRuntime(options: {
763
764
  const serverManifest = JSON.parse(options.serverManifestText) as BuiltServerManifest;
764
765
  const clientManifest = JSON.parse(options.clientManifestText) as {
765
766
  routes: ClientRouteManifestEntry[];
767
+ styles?: Array<{ css?: readonly string[]; file: string }>;
766
768
  };
767
769
  const appDir = await materializeBuiltServerApp(options.runtimeDir, serverManifest);
768
770
  const projectRoot = appDir;
@@ -818,6 +820,13 @@ async function materializeBuiltRuntime(options: {
818
820
  route.css !== undefined && route.css.length > 0 ? [[route.path, route.css]] : [],
819
821
  ),
820
822
  );
823
+ const clientStylesByFile = new Map(
824
+ (clientManifest.styles ?? []).flatMap((style) =>
825
+ style.css !== undefined && style.css.length > 0
826
+ ? [[join(routesDir, style.file), style.css] as const]
827
+ : [],
828
+ ),
829
+ );
821
830
  const navigationScripts = new Map(
822
831
  clientManifest.routes.flatMap((route) =>
823
832
  route.navigation === true && route.navigationScript !== undefined
@@ -846,6 +855,7 @@ async function materializeBuiltRuntime(options: {
846
855
  ? {}
847
856
  : { assetBaseUrl: serverManifest.assetBaseUrl }),
848
857
  clientScripts,
858
+ clientStylesByFile,
849
859
  clientStyles,
850
860
  hasMiddleware,
851
861
  navigationScripts,
@@ -1156,6 +1166,7 @@ function builtRenderAppRequestOptions(
1156
1166
  appDir: options.runtime.appDir,
1157
1167
  assetBaseUrl: options.runtime.assetBaseUrl,
1158
1168
  clientScripts: options.runtime.clientScripts,
1169
+ clientStylesByFile: options.runtime.clientStylesByFile,
1159
1170
  clientStyles: options.runtime.clientStyles,
1160
1171
  importPolicy: {
1161
1172
  ...options.importPolicy,
package/src/vite.ts CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  import { nodeRequestToWebRequest, sendResponse } from "./http.js";
39
39
  import { renderAppRequest } from "./render.js";
40
40
  import { stripRouteClientOnlyExports } from "./route-source.js";
41
- import { collectRouteCssHrefs } from "./route-styles.js";
41
+ import { collectRouteCssHrefs, collectSpecialBoundaryFiles } from "./route-styles.js";
42
42
  import { scanAppRoutes } from "./routes.js";
43
43
  import { resolveRequestHost, type RequestHostPolicy } from "./serve.js";
44
44
  import { hasJsxSyntax } from "./source-jsx.js";
@@ -504,6 +504,7 @@ async function handleAppRouterViteRequest(
504
504
  projectRoot: project.projectRoot,
505
505
  },
506
506
  clientStyles: await devRouteStyles(project),
507
+ clientStylesByFile: await devSpecialRouteStyles(project),
507
508
  navigationScripts: await devNavigationScripts(
508
509
  project.routesDir,
509
510
  options.clientRouteInferenceCache,
@@ -790,6 +791,28 @@ async function devRouteStyles(
790
791
  return new Map<string, readonly string[]>(routeStyles);
791
792
  }
792
793
 
794
+ async function devSpecialRouteStyles(
795
+ project: ResolvedAppRouterProject,
796
+ ): Promise<ReadonlyMap<string, readonly string[]>> {
797
+ const entries = await Promise.all(
798
+ (await collectSpecialBoundaryFiles(project.routesDir)).map(async (file) => {
799
+ const hrefs = await collectRouteCssHrefs({
800
+ appDir: project.routesDir,
801
+ hrefPrefix: devCssPrefix,
802
+ pageFile: file,
803
+ projectRoot: project.projectRoot,
804
+ });
805
+
806
+ return hrefs.length === 0 ? undefined : ([file, hrefs as readonly string[]] as const);
807
+ }),
808
+ );
809
+ const routeStyles = entries.filter(
810
+ (entry): entry is readonly [string, readonly string[]] => entry !== undefined,
811
+ );
812
+
813
+ return new Map<string, readonly string[]>(routeStyles);
814
+ }
815
+
793
816
  async function devNavigationScripts(
794
817
  appDir: string,
795
818
  inferenceCache?: ClientRouteInferenceCache | undefined,