rsbuild-plugin-react-router 0.3.0 → 0.3.1

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 (56) hide show
  1. package/README.md +55 -24
  2. package/dist/451.js +36 -20
  3. package/dist/build-manifest.d.ts +6 -3
  4. package/dist/concurrency.d.ts +2 -1
  5. package/dist/config-imports.d.ts +7 -0
  6. package/dist/dev-background-resources.d.ts +38 -0
  7. package/dist/dev-generation.d.ts +2 -2
  8. package/dist/dev-runtime-artifacts.d.ts +9 -1
  9. package/dist/dev-runtime-compilation.d.ts +17 -2
  10. package/dist/dev-server.d.ts +5 -0
  11. package/dist/effect-runtime.d.ts +18 -0
  12. package/dist/export-utils.d.ts +2 -2
  13. package/dist/index.cjs +11448 -891
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.js +7950 -758
  16. package/dist/lazy-compilation-prewarm.d.ts +25 -0
  17. package/dist/lazy-compilation.d.ts +2 -1
  18. package/dist/manifest.d.ts +19 -4
  19. package/dist/parallel-route-transforms.d.ts +17 -2
  20. package/dist/prerender-build.d.ts +4 -0
  21. package/dist/prerender.d.ts +0 -1
  22. package/dist/react-router-config.d.ts +8 -5
  23. package/dist/server-build-resolution.d.ts +3 -0
  24. package/dist/ssr-externals.d.ts +1 -0
  25. package/dist/typegen.d.ts +14 -1
  26. package/dist/types.d.ts +15 -6
  27. package/package.json +11 -9
  28. package/src/build-manifest.ts +110 -73
  29. package/src/concurrency.ts +3 -22
  30. package/src/config-imports.ts +38 -0
  31. package/src/dev-background-resources.ts +255 -0
  32. package/src/dev-generation.ts +43 -53
  33. package/src/dev-runtime-artifacts.ts +50 -18
  34. package/src/dev-runtime-compilation.ts +80 -1
  35. package/src/dev-runtime-controller.ts +111 -31
  36. package/src/dev-runtime-session.ts +18 -11
  37. package/src/dev-server.ts +31 -1
  38. package/src/effect-runtime.ts +130 -0
  39. package/src/export-utils.ts +82 -23
  40. package/src/index.ts +118 -153
  41. package/src/lazy-compilation-prewarm.ts +279 -0
  42. package/src/lazy-compilation.ts +12 -5
  43. package/src/manifest.ts +366 -255
  44. package/src/modify-browser-manifest.ts +2 -1
  45. package/src/parallel-route-transforms.ts +195 -69
  46. package/src/prerender-build.ts +125 -61
  47. package/src/prerender.ts +0 -18
  48. package/src/react-router-config.ts +98 -73
  49. package/src/route-artifacts.ts +2 -2
  50. package/src/route-export-resolution.ts +3 -3
  51. package/src/route-watch.ts +119 -84
  52. package/src/server-build-resolution.ts +131 -0
  53. package/src/server-utils.ts +7 -106
  54. package/src/ssr-externals.ts +1 -1
  55. package/src/typegen.ts +162 -33
  56. package/src/types.ts +16 -6
@@ -1,5 +1,6 @@
1
1
  import { readFile, stat } from 'node:fs/promises';
2
- import { langFromPath, parse } from 'yuku-parser';
2
+ import { rspack } from '@rsbuild/core';
3
+ import { langFromPath, parse, type ParseOptions } from 'yuku-parser';
3
4
  import { setBoundedCacheEntry } from './bounded-cache.js';
4
5
  import {
5
6
  getExportedName,
@@ -34,6 +35,75 @@ const routeModuleAnalysisCache = new Map<
34
35
 
35
36
  const MAX_EXPORT_UTILS_CACHE_ENTRIES = 2048;
36
37
 
38
+ const stripResourcePathQuery = (resourcePath: string): string =>
39
+ resourcePath.replace(/[?#].*$/, '');
40
+
41
+ type TypeScriptParseLang = Extract<
42
+ NonNullable<ParseOptions['lang']>,
43
+ 'ts' | 'tsx'
44
+ >;
45
+
46
+ const getExportAnalysisCode = (
47
+ code: string,
48
+ resourcePath: string,
49
+ lang: TypeScriptParseLang
50
+ ): string =>
51
+ rspack.experiments.swc.transformSync(code, {
52
+ filename: resourcePath,
53
+ jsc: {
54
+ parser: {
55
+ syntax: 'typescript',
56
+ tsx: lang === 'tsx',
57
+ },
58
+ },
59
+ }).code;
60
+
61
+ const getParseErrors = (result: ReturnType<typeof parse>) =>
62
+ result.diagnostics.filter(diagnostic => diagnostic.severity === 'error');
63
+
64
+ const getParseErrorMessage = (
65
+ errors: ReturnType<typeof getParseErrors>
66
+ ): string => errors.map(error => error.message).join('\n');
67
+
68
+ const parseProgram = (code: string, resourcePath?: string): ProgramNode => {
69
+ const sourcePath = resourcePath
70
+ ? stripResourcePathQuery(resourcePath)
71
+ : undefined;
72
+ const lang = sourcePath ? langFromPath(sourcePath) : 'tsx';
73
+ const result = parse(code, {
74
+ sourceType: 'module',
75
+ lang,
76
+ });
77
+ const errors = getParseErrors(result);
78
+ if (errors.length === 0) {
79
+ return getProgram(result);
80
+ }
81
+ if (!sourcePath || (lang !== 'ts' && lang !== 'tsx')) {
82
+ throw new Error(getParseErrorMessage(errors));
83
+ }
84
+
85
+ const normalizedCode = getExportAnalysisCode(code, sourcePath, lang);
86
+ const normalizedResult = parse(normalizedCode, {
87
+ sourceType: 'module',
88
+ lang: 'js',
89
+ });
90
+ const normalizedErrors = getParseErrors(normalizedResult);
91
+ if (normalizedErrors.length > 0) {
92
+ throw new Error(getParseErrorMessage(normalizedErrors));
93
+ }
94
+ return getProgram(normalizedResult);
95
+ };
96
+
97
+ const getExportInfoCacheKey = (
98
+ code: string,
99
+ resourcePath?: string
100
+ ): string => {
101
+ const lang = resourcePath
102
+ ? langFromPath(stripResourcePathQuery(resourcePath))
103
+ : 'inline';
104
+ return `${lang}\0${code}`;
105
+ };
106
+
37
107
  const cachePromiseOnReject = <T>(
38
108
  promise: Promise<T>,
39
109
  invalidate: () => void
@@ -43,20 +113,6 @@ const cachePromiseOnReject = <T>(
43
113
  throw error;
44
114
  });
45
115
 
46
- const parseProgram = (code: string, resourcePath?: string): ProgramNode => {
47
- const result = parse(code, {
48
- sourceType: 'module',
49
- lang: resourcePath ? langFromPath(resourcePath) : 'tsx',
50
- });
51
- const errors = result.diagnostics.filter(
52
- diagnostic => diagnostic.severity === 'error'
53
- );
54
- if (errors.length > 0) {
55
- throw new Error(errors.map(error => error.message).join('\n'));
56
- }
57
- return getProgram(result);
58
- };
59
-
60
116
  const isTypeOnlyExport = (node: AnyNode): boolean =>
61
117
  node.exportKind === 'type' ||
62
118
  node.type === 'TSExportAssignment' ||
@@ -140,21 +196,24 @@ const collectExportAllModules = (program: AnyNode): string[] => {
140
196
  };
141
197
 
142
198
  export const getExportNames = async (
143
- code: string
199
+ code: string,
200
+ resourcePath?: string
144
201
  ): Promise<readonly string[]> => {
145
- return (await getExportNamesAndExportAll(code)).exportNames;
202
+ return (await getExportNamesAndExportAll(code, resourcePath)).exportNames;
146
203
  };
147
204
 
148
205
  export const getExportNamesAndExportAll = async (
149
- code: string
206
+ code: string,
207
+ resourcePath?: string
150
208
  ): Promise<ExportInfo> => {
151
- const cached = exportInfoCache.get(code);
209
+ const cacheKey = getExportInfoCacheKey(code, resourcePath);
210
+ const cached = exportInfoCache.get(cacheKey);
152
211
  if (cached) {
153
212
  return cached;
154
213
  }
155
214
 
156
215
  const exportInfo = (async () => {
157
- const program = parseProgram(code);
216
+ const program = parseProgram(code, resourcePath);
158
217
  return {
159
218
  exportNames: collectProgramExportNames(program),
160
219
  exportAllModules: collectExportAllModules(program),
@@ -163,14 +222,14 @@ export const getExportNamesAndExportAll = async (
163
222
 
164
223
  let trackedExportInfo: Promise<ExportInfo>;
165
224
  trackedExportInfo = cachePromiseOnReject(exportInfo, () => {
166
- if (exportInfoCache.get(code) === trackedExportInfo) {
167
- exportInfoCache.delete(code);
225
+ if (exportInfoCache.get(cacheKey) === trackedExportInfo) {
226
+ exportInfoCache.delete(cacheKey);
168
227
  }
169
228
  });
170
229
 
171
230
  setBoundedCacheEntry(
172
231
  exportInfoCache,
173
- code,
232
+ cacheKey,
174
233
  trackedExportInfo,
175
234
  MAX_EXPORT_UTILS_CACHE_ENTRIES
176
235
  );
package/src/index.ts CHANGED
@@ -11,13 +11,14 @@ import {
11
11
  import { createJiti } from 'jiti';
12
12
  import { relative, resolve } from 'pathe';
13
13
 
14
+ import { getDefaultConcurrency } from './concurrency.js';
14
15
  import {
15
16
  BUILD_CLIENT_ROUTE_QUERY_STRING,
16
17
  JS_EXTENSIONS,
17
18
  PLUGIN_NAME,
18
19
  } from './constants.js';
19
20
  import { guardReactRouterLazyCompilation } from './lazy-compilation.js';
20
- import { createDevServerMiddleware } from './dev-server.js';
21
+ import { createReactRouterDevServerSetup } from './dev-server.js';
21
22
  import {
22
23
  generateWithProps,
23
24
  findEntryFile,
@@ -37,7 +38,6 @@ import {
37
38
  import {
38
39
  getReactRouterManifestForDev,
39
40
  configRoutesToRouteManifest,
40
- configRoutesToRouteManifestEntries,
41
41
  createReactRouterManifestStats,
42
42
  type ReactRouterManifestStats,
43
43
  type RouteManifestModuleExports,
@@ -55,14 +55,7 @@ import {
55
55
  createRouteTransformExecutor,
56
56
  shouldParallelizeRouteTransforms,
57
57
  } from './parallel-route-transforms.js';
58
- import {
59
- createRouteTopologyWatcher,
60
- createRouteManifestSnapshot,
61
- ensureDevRestartMarker,
62
- getRouteRestartMarkerPath,
63
- mergeWatchFiles,
64
- type WatchFileConfig,
65
- } from './route-watch.js';
58
+ import { getRouteRestartMarkerPath, mergeWatchFiles } from './route-watch.js';
66
59
  import { validateRouteConfig } from './route-config.js';
67
60
  import {
68
61
  getBuildManifest,
@@ -81,15 +74,29 @@ import {
81
74
  } from './performance.js';
82
75
  import { mapVirtualModules } from './virtual-modules.js';
83
76
  import { createReactRouterDevRuntimeController } from './dev-runtime-controller.js';
77
+ import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
84
78
  import { registerReactRouterTypegen } from './typegen.js';
79
+ import { importConfigWithWatchPaths } from './config-imports.js';
85
80
  import {
86
- clearConfigImportCache,
87
- collectConfigImportWatchPaths,
88
- } from './config-imports.js';
81
+ createReactRouterRouteTopology,
82
+ createReactRouterRouteWatchFiles,
83
+ registerReactRouterDevBackgroundResources,
84
+ } from './dev-background-resources.js';
89
85
 
90
86
  export { loadReactRouterServerBuild } from './dev-generation.js';
91
87
  export { resolveReactRouterServerBuild };
92
88
 
89
+ const MIN_PARALLEL_ENVIRONMENT_BUILD_SPARE_CORES = 4;
90
+
91
+ export const shouldParallelizeEnvironmentBuilds = ({
92
+ isBuild,
93
+ spareCoreCount = getDefaultConcurrency(),
94
+ }: {
95
+ isBuild: boolean;
96
+ spareCoreCount?: number;
97
+ }): boolean =>
98
+ !isBuild && spareCoreCount >= MIN_PARALLEL_ENVIRONMENT_BUILD_SPARE_CORES;
99
+
93
100
  type ModuleFederationPluginLike = {
94
101
  name?: string;
95
102
  _options?: { experiments?: { asyncStartup?: boolean } };
@@ -139,6 +146,7 @@ export const pluginReactRouter = (
139
146
  async setup(api) {
140
147
  const defaultOptions = {
141
148
  customServer: false,
149
+ lazyCompilation: true,
142
150
  serverOutput: 'module' as const,
143
151
  };
144
152
 
@@ -181,8 +189,6 @@ export const pluginReactRouter = (
181
189
  warnOnClientSourceMaps(normalized, msg => api.logger.warn(msg), 'web');
182
190
  });
183
191
 
184
- registerReactRouterTypegen(api);
185
-
186
192
  const configPath = findEntryFile(resolve('react-router.config'));
187
193
  const configExists = existsSync(configPath);
188
194
  let configWatchPaths: string | string[] = configExists
@@ -197,22 +203,10 @@ export const pluginReactRouter = (
197
203
  );
198
204
  } else {
199
205
  const displayPath = relative(process.cwd(), configPath);
200
- const configJiti = createJiti(process.cwd(), {
201
- moduleCache: true,
202
- });
203
- const cacheKeysBeforeImport = new Set(Object.keys(configJiti.cache));
204
206
  try {
205
- const imported = await configJiti.import<Config>(configPath, {
206
- default: true,
207
- });
208
- const importedConfigPaths = collectConfigImportWatchPaths(
209
- configPath,
210
- configJiti.cache,
211
- cacheKeysBeforeImport
212
- );
213
- if (importedConfigPaths.length > 0) {
214
- configWatchPaths = [configPath, ...importedConfigPaths];
215
- }
207
+ const { value: imported, watchPaths } =
208
+ await importConfigWithWatchPaths<Config>(configPath);
209
+ configWatchPaths = watchPaths;
216
210
  if (imported === undefined) {
217
211
  throw new Error(`${displayPath} must provide a default export`);
218
212
  }
@@ -222,22 +216,9 @@ export const pluginReactRouter = (
222
216
  reactRouterUserConfig = imported;
223
217
  } catch (error) {
224
218
  throw new Error(`Error loading ${displayPath}: ${error}`);
225
- } finally {
226
- clearConfigImportCache(configJiti.cache, [
227
- configPath,
228
- ...collectConfigImportWatchPaths(
229
- configPath,
230
- configJiti.cache,
231
- cacheKeysBeforeImport
232
- ),
233
- ]);
234
219
  }
235
220
  }
236
221
 
237
- const jiti = createJiti(process.cwd(), {
238
- moduleCache: false,
239
- });
240
-
241
222
  const {
242
223
  resolved: resolvedConfig,
243
224
  presets: configPresets,
@@ -259,6 +240,8 @@ export const pluginReactRouter = (
259
240
  buildEnd,
260
241
  } = resolvedConfig;
261
242
 
243
+ registerReactRouterTypegen(api, { appDirectory });
244
+
262
245
  const hasExplicitServerOutput = Object.prototype.hasOwnProperty.call(
263
246
  options,
264
247
  'serverOutput'
@@ -334,8 +317,13 @@ export const pluginReactRouter = (
334
317
  );
335
318
  }
336
319
 
337
- const loadRouteConfig = async (): Promise<RouteConfigEntry[]> => {
338
- const routeConfigExport = await jiti.import<RouteConfigEntry[]>(
320
+ const jiti = createJiti(process.cwd(), {
321
+ moduleCache: false,
322
+ });
323
+ const importRouteConfig = async (
324
+ importer: Pick<typeof jiti, 'import'>
325
+ ): Promise<RouteConfigEntry[]> => {
326
+ const routeConfigExport = await importer.import<RouteConfigEntry[]>(
339
327
  routesPath,
340
328
  {
341
329
  default: true,
@@ -351,7 +339,9 @@ export const pluginReactRouter = (
351
339
  }
352
340
  return validation.routeConfig;
353
341
  };
354
- const routeConfig = await loadRouteConfig();
342
+ const loadRouteConfig = () => importRouteConfig(jiti);
343
+ const { value: routeConfig, watchPaths: routeConfigWatchPaths } =
344
+ await importConfigWithWatchPaths(routesPath, importRouteConfig);
355
345
 
356
346
  const entryClientPath = findEntryFile(
357
347
  resolve(appDirectory, 'entry.client')
@@ -384,22 +374,13 @@ export const pluginReactRouter = (
384
374
  // React Router's server build expects route files relative to `appDirectory`
385
375
  // so it can resolve them correctly during compilation.
386
376
  const rootRouteFile = relative(appDirectory, rootRoutePath);
387
- const createRouteTopologySnapshot = (
388
- routeFile: string,
389
- routeConfig: RouteConfigEntry[]
390
- ) =>
391
- createRouteManifestSnapshot([
392
- ['root', { path: '', id: 'root', file: routeFile }],
393
- ...configRoutesToRouteManifestEntries(appDirectory, routeConfig),
394
- ]);
395
- const getWatchedRouteTopology = async (): Promise<Set<string>> => {
396
- const latestRouteConfig = await loadRouteConfig();
397
- const latestRootRouteFile = relative(appDirectory, getRootRoutePath());
398
- return createRouteTopologySnapshot(
399
- latestRootRouteFile,
400
- latestRouteConfig
401
- );
402
- };
377
+ const routeTopology = createReactRouterRouteTopology({
378
+ appDirectory,
379
+ rootRouteFile,
380
+ routeConfig,
381
+ loadRouteConfig,
382
+ getRootRoutePath,
383
+ });
403
384
 
404
385
  const routes = {
405
386
  root: { path: '', id: 'root', file: rootRouteFile },
@@ -426,6 +407,9 @@ export const pluginReactRouter = (
426
407
  }
427
408
 
428
409
  const isBuild = api.context.action === 'build';
410
+ const shouldDependOnWebCompiler = !shouldParallelizeEnvironmentBuilds({
411
+ isBuild,
412
+ });
429
413
  const isPrerenderEnabled =
430
414
  prerenderConfig !== undefined && prerenderConfig !== false;
431
415
  const isSpaMode = !ssr && !isPrerenderEnabled;
@@ -442,6 +426,7 @@ export const pluginReactRouter = (
442
426
  shouldParallelizeRouteTransforms(routeCount),
443
427
  routeChunkCache,
444
428
  splitRouteModules: Boolean(splitRouteModules),
429
+ isBuild,
445
430
  });
446
431
  const routeChunkOptions = {
447
432
  splitRouteModules,
@@ -453,56 +438,22 @@ export const pluginReactRouter = (
453
438
  const assetsBuildDirectory = relative(process.cwd(), outputClientPath);
454
439
  const watchDirectory = resolve(appDirectory);
455
440
  const routeRestartMarkerPath = getRouteRestartMarkerPath(outputClientPath);
456
- const routeTopologyWatchFiles: WatchFileConfig[] =
457
- pluginOptions.onRouteTopologyChange
458
- ? []
459
- : [
460
- {
461
- paths: routesPath,
462
- type: 'reload-server',
463
- },
464
- {
465
- paths: routeRestartMarkerPath,
466
- type: 'reload-server',
467
- },
468
- ];
469
- const routeWatchFiles: WatchFileConfig[] = [
470
- {
471
- paths: configWatchPaths,
472
- type: 'reload-server',
473
- },
474
- ...routeTopologyWatchFiles,
475
- ];
476
- let closeRouteTopologyWatcher: (() => Promise<void>) | undefined;
477
-
478
- api.onBeforeStartDevServer(async () => {
479
- await ensureDevRestartMarker(routeRestartMarkerPath);
480
- closeRouteTopologyWatcher = await createRouteTopologyWatcher({
481
- watchDirectory,
482
- getRouteTopology: getWatchedRouteTopology,
483
- initialRouteTopology: createRouteTopologySnapshot(
484
- rootRouteFile,
485
- routeConfig
486
- ),
487
- restartMarkerPath: routeRestartMarkerPath,
488
- onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
489
- onError: error => {
490
- api.logger.warn(
491
- `[${PLUGIN_NAME}] Failed to watch route topology changes: ${error}`
492
- );
493
- },
494
- });
495
- });
496
-
497
- api.onCloseDevServer(async () => {
498
- await closeRouteTopologyWatcher?.();
499
- closeRouteTopologyWatcher = undefined;
500
- });
501
- api.onCloseBuild(async () => {
502
- await routeTransformExecutor.close();
441
+ const routeWatchFiles = createReactRouterRouteWatchFiles({
442
+ configWatchPaths,
443
+ routeConfigWatchPaths,
444
+ routeRestartMarkerPath,
445
+ onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
503
446
  });
504
- api.onCloseDevServer(async () => {
505
- await routeTransformExecutor.close();
447
+ const devBackgroundResources = registerReactRouterDevBackgroundResources({
448
+ api,
449
+ isBuild,
450
+ lazyCompilationPrewarm: pluginOptions.unstableLazyCompilationPrewarm,
451
+ routeTransformExecutor,
452
+ routeRestartMarkerPath,
453
+ watchDirectory,
454
+ getRouteTopology: routeTopology.getRouteTopology,
455
+ initialRouteTopology: routeTopology.initialRouteTopology,
456
+ onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
506
457
  });
507
458
 
508
459
  type ReactRouterManifest = Awaited<
@@ -526,6 +477,7 @@ export const pluginReactRouter = (
526
477
  'virtual/react-router/browser-manifest',
527
478
  () => {
528
479
  latestBrowserManifest = manifest;
480
+ devBackgroundResources.setManifest(manifest);
529
481
  latestBrowserManifestModuleExports = moduleExportsByRouteId;
530
482
  const baseServerManifest = {
531
483
  ...manifest,
@@ -662,31 +614,35 @@ export const pluginReactRouter = (
662
614
  }
663
615
  );
664
616
 
665
- api.onAfterBuild(async ({ environments }) => {
666
- await runReactRouterPrerenderBuild({
667
- api,
668
- hasWebEnvironment: Boolean(environments.web),
669
- buildDirectory,
670
- serverBuildFile,
671
- ssr,
672
- isPrerenderEnabled,
673
- prerenderConfig,
674
- prerenderPaths,
675
- basename,
676
- future,
677
- routes,
678
- latestBrowserManifest,
679
- latestBrowserManifestModuleExports,
680
- clientStats,
681
- pluginOptions,
682
- appDirectory,
683
- assetPrefix,
684
- routeChunkOptions,
685
- buildManifest,
686
- resolvedConfigWithRoutes,
687
- buildEnd,
688
- });
689
- });
617
+ api.onAfterBuild(({ environments }) =>
618
+ runPluginEffect(
619
+ tryPluginPromise(() =>
620
+ runReactRouterPrerenderBuild({
621
+ api,
622
+ hasWebEnvironment: Boolean(environments.web),
623
+ buildDirectory,
624
+ serverBuildFile,
625
+ ssr,
626
+ isPrerenderEnabled,
627
+ prerenderConfig,
628
+ prerenderPaths,
629
+ basename,
630
+ future,
631
+ routes,
632
+ latestBrowserManifest,
633
+ latestBrowserManifestModuleExports,
634
+ clientStats,
635
+ pluginOptions,
636
+ appDirectory,
637
+ assetPrefix,
638
+ routeChunkOptions,
639
+ buildManifest,
640
+ resolvedConfigWithRoutes,
641
+ buildEnd,
642
+ })
643
+ )
644
+ )
645
+ );
690
646
 
691
647
  const allowedActionOriginsForBuild =
692
648
  allowedActionOrigins === false ? undefined : allowedActionOrigins;
@@ -770,13 +726,18 @@ export const pluginReactRouter = (
770
726
  serverBundleEntries,
771
727
  });
772
728
 
773
- const configuredLazyCompilation =
774
- pluginOptions.lazyCompilation === undefined
775
- ? config.dev?.lazyCompilation
776
- : pluginOptions.lazyCompilation;
729
+ const configuredLazyCompilation = Object.prototype.hasOwnProperty.call(
730
+ options,
731
+ 'lazyCompilation'
732
+ )
733
+ ? pluginOptions.lazyCompilation
734
+ : (config.dev?.lazyCompilation ?? pluginOptions.lazyCompilation);
777
735
  const guardedLazyCompilation = guardReactRouterLazyCompilation({
778
736
  lazyCompilation: configuredLazyCompilation,
779
737
  entryClientPath: finalEntryClientPath,
738
+ prewarmReactRouterModules: Boolean(
739
+ pluginOptions.unstableLazyCompilationPrewarm
740
+ ),
780
741
  });
781
742
  const lazyCompilation =
782
743
  guardedLazyCompilation === undefined
@@ -807,19 +768,23 @@ export const pluginReactRouter = (
807
768
  writeToDisk: true,
808
769
  ...lazyCompilation,
809
770
  watchFiles: mergeWatchFiles(config.dev?.watchFiles, routeWatchFiles),
810
- setupMiddlewares:
811
- pluginOptions.customServer || !ssr
812
- ? []
813
- : [
814
- middlewares => {
815
- middlewares.push(
816
- createDevServerMiddleware({
817
- loadBuild: devRuntime.createBuildLoader(),
818
- })
819
- );
820
- },
821
- ],
822
771
  },
772
+ // React Router's request handler natively supports `ssr:false`
773
+ // builds (it renders the SPA shell for document requests), so the
774
+ // middleware is registered for SPA mode too — without it, dev
775
+ // requests would 404 because no HTML entry exists.
776
+ ...(pluginOptions.customServer
777
+ ? {}
778
+ : {
779
+ server: {
780
+ setup: [
781
+ createReactRouterDevServerSetup({
782
+ // Lazy: the dev runtime binding does not exist yet here.
783
+ loadBuild: () => devRuntime.createBuildLoader()(),
784
+ }),
785
+ ],
786
+ },
787
+ }),
823
788
  tools: {
824
789
  rspack: {
825
790
  plugins: [vmodPlugin],
@@ -919,7 +884,7 @@ export const pluginReactRouter = (
919
884
  ],
920
885
  },
921
886
  externals: nodeExternals,
922
- dependencies: ['web'],
887
+ ...(shouldDependOnWebCompiler ? { dependencies: ['web'] } : {}),
923
888
  externalsType: resolvedServerOutput,
924
889
  output: {
925
890
  chunkFormat: resolvedServerOutput,