rsbuild-plugin-react-router 0.3.0 → 0.4.0

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 (64) hide show
  1. package/README.md +88 -35
  2. package/dist/451.js +164 -30
  3. package/dist/build-manifest.d.ts +6 -3
  4. package/dist/build-output-transforms.d.ts +2 -1
  5. package/dist/concurrency.d.ts +2 -1
  6. package/dist/config-imports.d.ts +7 -0
  7. package/dist/dev-background-resources.d.ts +38 -0
  8. package/dist/dev-generation.d.ts +2 -2
  9. package/dist/dev-hmr.d.ts +45 -0
  10. package/dist/dev-runtime-artifacts.d.ts +9 -1
  11. package/dist/dev-runtime-compilation.d.ts +17 -2
  12. package/dist/dev-runtime-controller.d.ts +6 -1
  13. package/dist/dev-server.d.ts +5 -0
  14. package/dist/effect-runtime.d.ts +18 -0
  15. package/dist/export-utils.d.ts +2 -2
  16. package/dist/index.cjs +11852 -848
  17. package/dist/index.d.ts +5 -0
  18. package/dist/index.js +8289 -773
  19. package/dist/lazy-compilation-prewarm.d.ts +25 -0
  20. package/dist/lazy-compilation.d.ts +2 -1
  21. package/dist/manifest.d.ts +19 -4
  22. package/dist/parallel-route-transforms.d.ts +17 -2
  23. package/dist/prerender-build.d.ts +4 -0
  24. package/dist/prerender.d.ts +0 -1
  25. package/dist/react-router-config.d.ts +8 -5
  26. package/dist/route-artifacts.d.ts +10 -2
  27. package/dist/route-transform-tasks.d.ts +3 -0
  28. package/dist/server-build-resolution.d.ts +3 -0
  29. package/dist/ssr-externals.d.ts +1 -0
  30. package/dist/typegen.d.ts +14 -1
  31. package/dist/types.d.ts +15 -6
  32. package/package.json +18 -10
  33. package/src/build-manifest.ts +110 -73
  34. package/src/build-output-transforms.ts +5 -0
  35. package/src/concurrency.ts +3 -22
  36. package/src/config-imports.ts +38 -0
  37. package/src/dev-background-resources.ts +255 -0
  38. package/src/dev-generation.ts +43 -53
  39. package/src/dev-hmr.ts +431 -0
  40. package/src/dev-runtime-artifacts.ts +50 -18
  41. package/src/dev-runtime-compilation.ts +80 -1
  42. package/src/dev-runtime-controller.ts +155 -31
  43. package/src/dev-runtime-session.ts +18 -11
  44. package/src/dev-server.ts +31 -1
  45. package/src/effect-runtime.ts +130 -0
  46. package/src/export-utils.ts +82 -23
  47. package/src/index.ts +166 -153
  48. package/src/lazy-compilation-prewarm.ts +279 -0
  49. package/src/lazy-compilation.ts +12 -5
  50. package/src/manifest.ts +366 -255
  51. package/src/modify-browser-manifest.ts +2 -1
  52. package/src/parallel-route-transforms.ts +195 -69
  53. package/src/prerender-build.ts +147 -63
  54. package/src/prerender.ts +1 -19
  55. package/src/react-router-config.ts +98 -73
  56. package/src/route-artifacts.ts +122 -3
  57. package/src/route-export-resolution.ts +3 -3
  58. package/src/route-transform-tasks.ts +173 -2
  59. package/src/route-watch.ts +119 -84
  60. package/src/server-build-resolution.ts +131 -0
  61. package/src/server-utils.ts +7 -106
  62. package/src/ssr-externals.ts +1 -1
  63. package/src/typegen.ts +162 -33
  64. package/src/types.ts +16 -6
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,38 @@ 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';
80
+ import {
81
+ createReactRouterRouteTopology,
82
+ createReactRouterRouteWatchFiles,
83
+ registerReactRouterDevBackgroundResources,
84
+ } from './dev-background-resources.js';
85
85
  import {
86
- clearConfigImportCache,
87
- collectConfigImportWatchPaths,
88
- } from './config-imports.js';
86
+ createDevHdrRevisionSignal,
87
+ generateDevHmrRuntimeModule,
88
+ getDevHdrRevisionFilePath,
89
+ isRspackSwcReactRefreshEnabled,
90
+ resolveReactRefreshRuntimePath,
91
+ DEV_HMR_RUNTIME_MODULE_ID,
92
+ } from './dev-hmr.js';
89
93
 
94
+ export type { Config as ReactRouterRsbuildConfig } from './react-router-config.js';
90
95
  export { loadReactRouterServerBuild } from './dev-generation.js';
91
96
  export { resolveReactRouterServerBuild };
92
97
 
98
+ const MIN_PARALLEL_ENVIRONMENT_BUILD_SPARE_CORES = 4;
99
+
100
+ export const shouldParallelizeEnvironmentBuilds = ({
101
+ isBuild,
102
+ spareCoreCount = getDefaultConcurrency(),
103
+ }: {
104
+ isBuild: boolean;
105
+ spareCoreCount?: number;
106
+ }): boolean =>
107
+ !isBuild && spareCoreCount >= MIN_PARALLEL_ENVIRONMENT_BUILD_SPARE_CORES;
108
+
93
109
  type ModuleFederationPluginLike = {
94
110
  name?: string;
95
111
  _options?: { experiments?: { asyncStartup?: boolean } };
@@ -139,6 +155,7 @@ export const pluginReactRouter = (
139
155
  async setup(api) {
140
156
  const defaultOptions = {
141
157
  customServer: false,
158
+ lazyCompilation: true,
142
159
  serverOutput: 'module' as const,
143
160
  };
144
161
 
@@ -181,8 +198,6 @@ export const pluginReactRouter = (
181
198
  warnOnClientSourceMaps(normalized, msg => api.logger.warn(msg), 'web');
182
199
  });
183
200
 
184
- registerReactRouterTypegen(api);
185
-
186
201
  const configPath = findEntryFile(resolve('react-router.config'));
187
202
  const configExists = existsSync(configPath);
188
203
  let configWatchPaths: string | string[] = configExists
@@ -197,22 +212,10 @@ export const pluginReactRouter = (
197
212
  );
198
213
  } else {
199
214
  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
215
  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
- }
216
+ const { value: imported, watchPaths } =
217
+ await importConfigWithWatchPaths<Config>(configPath);
218
+ configWatchPaths = watchPaths;
216
219
  if (imported === undefined) {
217
220
  throw new Error(`${displayPath} must provide a default export`);
218
221
  }
@@ -222,22 +225,9 @@ export const pluginReactRouter = (
222
225
  reactRouterUserConfig = imported;
223
226
  } catch (error) {
224
227
  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
228
  }
235
229
  }
236
230
 
237
- const jiti = createJiti(process.cwd(), {
238
- moduleCache: false,
239
- });
240
-
241
231
  const {
242
232
  resolved: resolvedConfig,
243
233
  presets: configPresets,
@@ -259,6 +249,8 @@ export const pluginReactRouter = (
259
249
  buildEnd,
260
250
  } = resolvedConfig;
261
251
 
252
+ registerReactRouterTypegen(api, { appDirectory });
253
+
262
254
  const hasExplicitServerOutput = Object.prototype.hasOwnProperty.call(
263
255
  options,
264
256
  'serverOutput'
@@ -334,8 +326,13 @@ export const pluginReactRouter = (
334
326
  );
335
327
  }
336
328
 
337
- const loadRouteConfig = async (): Promise<RouteConfigEntry[]> => {
338
- const routeConfigExport = await jiti.import<RouteConfigEntry[]>(
329
+ const jiti = createJiti(process.cwd(), {
330
+ moduleCache: false,
331
+ });
332
+ const importRouteConfig = async (
333
+ importer: Pick<typeof jiti, 'import'>
334
+ ): Promise<RouteConfigEntry[]> => {
335
+ const routeConfigExport = await importer.import<RouteConfigEntry[]>(
339
336
  routesPath,
340
337
  {
341
338
  default: true,
@@ -351,7 +348,9 @@ export const pluginReactRouter = (
351
348
  }
352
349
  return validation.routeConfig;
353
350
  };
354
- const routeConfig = await loadRouteConfig();
351
+ const loadRouteConfig = () => importRouteConfig(jiti);
352
+ const { value: routeConfig, watchPaths: routeConfigWatchPaths } =
353
+ await importConfigWithWatchPaths(routesPath, importRouteConfig);
355
354
 
356
355
  const entryClientPath = findEntryFile(
357
356
  resolve(appDirectory, 'entry.client')
@@ -384,22 +383,13 @@ export const pluginReactRouter = (
384
383
  // React Router's server build expects route files relative to `appDirectory`
385
384
  // so it can resolve them correctly during compilation.
386
385
  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
- };
386
+ const routeTopology = createReactRouterRouteTopology({
387
+ appDirectory,
388
+ rootRouteFile,
389
+ routeConfig,
390
+ loadRouteConfig,
391
+ getRootRoutePath,
392
+ });
403
393
 
404
394
  const routes = {
405
395
  root: { path: '', id: 'root', file: rootRouteFile },
@@ -426,6 +416,9 @@ export const pluginReactRouter = (
426
416
  }
427
417
 
428
418
  const isBuild = api.context.action === 'build';
419
+ const shouldDependOnWebCompiler = !shouldParallelizeEnvironmentBuilds({
420
+ isBuild,
421
+ });
429
422
  const isPrerenderEnabled =
430
423
  prerenderConfig !== undefined && prerenderConfig !== false;
431
424
  const isSpaMode = !ssr && !isPrerenderEnabled;
@@ -442,6 +435,7 @@ export const pluginReactRouter = (
442
435
  shouldParallelizeRouteTransforms(routeCount),
443
436
  routeChunkCache,
444
437
  splitRouteModules: Boolean(splitRouteModules),
438
+ isBuild,
445
439
  });
446
440
  const routeChunkOptions = {
447
441
  splitRouteModules,
@@ -453,56 +447,22 @@ export const pluginReactRouter = (
453
447
  const assetsBuildDirectory = relative(process.cwd(), outputClientPath);
454
448
  const watchDirectory = resolve(appDirectory);
455
449
  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();
450
+ const routeWatchFiles = createReactRouterRouteWatchFiles({
451
+ configWatchPaths,
452
+ routeConfigWatchPaths,
453
+ routeRestartMarkerPath,
454
+ onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
503
455
  });
504
- api.onCloseDevServer(async () => {
505
- await routeTransformExecutor.close();
456
+ const devBackgroundResources = registerReactRouterDevBackgroundResources({
457
+ api,
458
+ isBuild,
459
+ lazyCompilationPrewarm: pluginOptions.unstableLazyCompilationPrewarm,
460
+ routeTransformExecutor,
461
+ routeRestartMarkerPath,
462
+ watchDirectory,
463
+ getRouteTopology: routeTopology.getRouteTopology,
464
+ initialRouteTopology: routeTopology.initialRouteTopology,
465
+ onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
506
466
  });
507
467
 
508
468
  type ReactRouterManifest = Awaited<
@@ -526,6 +486,7 @@ export const pluginReactRouter = (
526
486
  'virtual/react-router/browser-manifest',
527
487
  () => {
528
488
  latestBrowserManifest = manifest;
489
+ devBackgroundResources.setManifest(manifest);
529
490
  latestBrowserManifestModuleExports = moduleExportsByRouteId;
530
491
  const baseServerManifest = {
531
492
  ...manifest,
@@ -623,10 +584,26 @@ export const pluginReactRouter = (
623
584
  defaultEntryName: devServerBuildEntryName,
624
585
  });
625
586
  const { serverBundleEntries } = serverBuildPlan;
587
+
588
+ const devHmrRefreshRuntimePath = isBuild
589
+ ? undefined
590
+ : resolveReactRefreshRuntimePath(api.context.rootPath);
591
+ const devHdrSignal = devHmrRefreshRuntimePath
592
+ ? createDevHdrRevisionSignal({
593
+ filePath: getDevHdrRevisionFilePath(api.context.rootPath),
594
+ onError: error =>
595
+ api.logger.debug(
596
+ `[${PLUGIN_NAME}] Failed to signal hot data revalidation: ${error.message}`
597
+ ),
598
+ })
599
+ : undefined;
600
+ let devHmrEnabled = false;
601
+
626
602
  const devRuntime = createReactRouterDevRuntimeController({
627
603
  api,
628
604
  isBuild,
629
605
  buildPlan: serverBuildPlan,
606
+ onNodeRebuildCommitted: () => devHdrSignal?.bump(),
630
607
  });
631
608
 
632
609
  let clientStats: ReactRouterManifestStats | undefined;
@@ -662,31 +639,35 @@ export const pluginReactRouter = (
662
639
  }
663
640
  );
664
641
 
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
- });
642
+ api.onAfterBuild(({ environments }) =>
643
+ runPluginEffect(
644
+ tryPluginPromise(() =>
645
+ runReactRouterPrerenderBuild({
646
+ api,
647
+ hasWebEnvironment: Boolean(environments.web),
648
+ buildDirectory,
649
+ serverBuildFile,
650
+ ssr,
651
+ isPrerenderEnabled,
652
+ prerenderConfig,
653
+ prerenderPaths,
654
+ basename,
655
+ future,
656
+ routes,
657
+ latestBrowserManifest,
658
+ latestBrowserManifestModuleExports,
659
+ clientStats,
660
+ pluginOptions,
661
+ appDirectory,
662
+ assetPrefix,
663
+ routeChunkOptions,
664
+ buildManifest,
665
+ resolvedConfigWithRoutes,
666
+ buildEnd,
667
+ })
668
+ )
669
+ )
670
+ );
690
671
 
691
672
  const allowedActionOriginsForBuild =
692
673
  allowedActionOrigins === false ? undefined : allowedActionOrigins;
@@ -746,6 +727,16 @@ export const pluginReactRouter = (
746
727
  ...bundleVirtualModules,
747
728
  ...bundleManifestModules,
748
729
  'virtual/react-router/with-props': generateWithProps(),
730
+ ...(devHmrRefreshRuntimePath
731
+ ? {
732
+ [DEV_HMR_RUNTIME_MODULE_ID]: generateDevHmrRuntimeModule({
733
+ reactRefreshRuntimePath: devHmrRefreshRuntimePath,
734
+ hdrRevisionFilePath: getDevHdrRevisionFilePath(
735
+ api.context.rootPath
736
+ ),
737
+ }),
738
+ }
739
+ : {}),
749
740
  })
750
741
  );
751
742
  };
@@ -770,13 +761,18 @@ export const pluginReactRouter = (
770
761
  serverBundleEntries,
771
762
  });
772
763
 
773
- const configuredLazyCompilation =
774
- pluginOptions.lazyCompilation === undefined
775
- ? config.dev?.lazyCompilation
776
- : pluginOptions.lazyCompilation;
764
+ const configuredLazyCompilation = Object.prototype.hasOwnProperty.call(
765
+ options,
766
+ 'lazyCompilation'
767
+ )
768
+ ? pluginOptions.lazyCompilation
769
+ : (config.dev?.lazyCompilation ?? pluginOptions.lazyCompilation);
777
770
  const guardedLazyCompilation = guardReactRouterLazyCompilation({
778
771
  lazyCompilation: configuredLazyCompilation,
779
772
  entryClientPath: finalEntryClientPath,
773
+ prewarmReactRouterModules: Boolean(
774
+ pluginOptions.unstableLazyCompilationPrewarm
775
+ ),
780
776
  });
781
777
  const lazyCompilation =
782
778
  guardedLazyCompilation === undefined
@@ -807,19 +803,23 @@ export const pluginReactRouter = (
807
803
  writeToDisk: true,
808
804
  ...lazyCompilation,
809
805
  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
806
  },
807
+ // React Router's request handler natively supports `ssr:false`
808
+ // builds (it renders the SPA shell for document requests), so the
809
+ // middleware is registered for SPA mode too — without it, dev
810
+ // requests would 404 because no HTML entry exists.
811
+ ...(pluginOptions.customServer
812
+ ? {}
813
+ : {
814
+ server: {
815
+ setup: [
816
+ createReactRouterDevServerSetup({
817
+ // Lazy: the dev runtime binding does not exist yet here.
818
+ loadBuild: () => devRuntime.createBuildLoader()(),
819
+ }),
820
+ ],
821
+ },
822
+ }),
823
823
  tools: {
824
824
  rspack: {
825
825
  plugins: [vmodPlugin],
@@ -919,7 +919,7 @@ export const pluginReactRouter = (
919
919
  ],
920
920
  },
921
921
  externals: nodeExternals,
922
- dependencies: ['web'],
922
+ ...(shouldDependOnWebCompiler ? { dependencies: ['web'] } : {}),
923
923
  externalsType: resolvedServerOutput,
924
924
  output: {
925
925
  chunkFormat: resolvedServerOutput,
@@ -948,6 +948,18 @@ export const pluginReactRouter = (
948
948
  ensureFederationAsyncStartup(rspackConfig);
949
949
  }
950
950
 
951
+ if (name === 'web') {
952
+ devHmrEnabled =
953
+ !isBuild &&
954
+ devHmrRefreshRuntimePath !== undefined &&
955
+ config.mode === 'development' &&
956
+ config.dev?.hmr !== false &&
957
+ isRspackSwcReactRefreshEnabled(rspackConfig);
958
+ if (devHmrEnabled) {
959
+ devHdrSignal?.ensure();
960
+ }
961
+ }
962
+
951
963
  if (name === 'node') {
952
964
  const output = rspackConfig.output;
953
965
  if (output) {
@@ -1020,6 +1032,7 @@ export const pluginReactRouter = (
1020
1032
  ssr,
1021
1033
  isSpaMode,
1022
1034
  rootRoutePath,
1035
+ isDevHmrEnabled: () => devHmrEnabled,
1023
1036
  });
1024
1037
  },
1025
1038
  });