rsbuild-plugin-react-router 0.7.3 → 0.8.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.
package/src/index.ts CHANGED
@@ -1,3 +1,10 @@
1
+ import {
2
+ createReactRouterManifestSnapshot,
3
+ type ReactRouterManifestSnapshot,
4
+ } from './manifest-snapshot.js';
5
+ import { createReactRouterManifestState } from './manifest-state.js';
6
+ import { registerNodeOnlyManifestValidation } from './node-only-manifest.js';
7
+ import { createHash } from 'node:crypto';
1
8
  import { existsSync, readFileSync } from 'node:fs';
2
9
  import fsExtra from 'fs-extra';
3
10
  import type { Config } from './react-router-config.js';
@@ -29,9 +36,7 @@ import {
29
36
  collectUnsupportedRscScriptAssets,
30
37
  configRoutesToRouteManifest,
31
38
  createReactRouterManifestStats,
32
- type ReactRouterManifestForDev as ReactRouterManifest,
33
39
  type ReactRouterManifestStats,
34
- type RouteManifestModuleExports,
35
40
  } from './manifest.js';
36
41
  import type { RouteModuleAnalysis } from './export-utils.js';
37
42
  import { registerModifyBrowserManifestAssets } from './modify-browser-manifest.js';
@@ -58,9 +63,8 @@ import {
58
63
  } from './performance.js';
59
64
  import { mapVirtualModules } from './virtual-modules.js';
60
65
  import {
61
- createDevHdrRevisionSignal,
66
+ DEV_HMR_RUNTIME_MODULE_ID,
62
67
  generateDevHmrRuntimeModule,
63
- getDevHdrRevisionFilePath,
64
68
  isRspackSwcReactRefreshEnabled,
65
69
  resolveReactRefreshRuntimePath,
66
70
  } from './dev-hmr.js';
@@ -229,7 +233,8 @@ export const pluginReactRouter = (
229
233
  // `getNormalizedConfig({ environment: 'web' })` throws when the build was
230
234
  // narrowed to other environments (`--environment node`), so look the web
231
235
  // environment up on the root config instead.
232
- const web = root.environments.web;
236
+ const web =
237
+ root.environments.web ?? api.getRsbuildConfig().environments?.web;
233
238
  assetPrefix = resolveEffectiveAssetPrefix(
234
239
  {
235
240
  dev: web?.dev,
@@ -295,10 +300,12 @@ export const pluginReactRouter = (
295
300
  buildEnd,
296
301
  } = resolvedConfig;
297
302
 
298
- await registerReactRouterTypegen(api, {
299
- runtime: effectRuntime,
300
- appDirectory,
301
- });
303
+ if (pluginOptions.typegen !== false) {
304
+ await registerReactRouterTypegen(api, {
305
+ runtime: effectRuntime,
306
+ appDirectory,
307
+ });
308
+ }
302
309
 
303
310
  const hasExplicitServerOutput = Object.prototype.hasOwnProperty.call(
304
311
  options,
@@ -525,11 +532,6 @@ export const pluginReactRouter = (
525
532
  routeRestartMarkerPath,
526
533
  onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
527
534
  });
528
- let latestBrowserManifest: ReactRouterManifest | null = null;
529
- let latestBrowserManifestModuleExports: RouteManifestModuleExports = {};
530
- let latestServerManifest: ReactRouterManifest | null = null;
531
- const latestServerManifestsByBundleId: Record<string, ReactRouterManifest> =
532
- {};
533
535
  // The node `server-manifest` module's source is a constant; its real
534
536
  // content is injected by a transform from the web compilation's emitted
535
537
  // asset names. Rspack's persistent cache would therefore reuse a previous
@@ -539,6 +541,10 @@ export const pluginReactRouter = (
539
541
  const serverManifestStampPath = resolve(
540
542
  api.context.cachePath,
541
543
  'react-router',
544
+ // Projects can share node_modules, and therefore Rsbuild's cache path.
545
+ createHash('sha256')
546
+ .update(JSON.stringify([appDirectory, outputClientPath]))
547
+ .digest('hex'),
542
548
  'server-manifest.json'
543
549
  );
544
550
  // Bundle manifests also depend on the route partition, which can change
@@ -546,14 +552,24 @@ export const pluginReactRouter = (
546
552
  // Only rewrite on change: a bumped mtime would otherwise invalidate the
547
553
  // module on every build and, if the cache dir is watched, rebuild node
548
554
  // after every web rebuild in dev.
549
- const writeServerManifestStamp = (): void => {
555
+ const writeServerManifestStamp = (
556
+ snapshot: ReactRouterManifestSnapshot
557
+ ): void => {
550
558
  const stamp = JSON.stringify({
551
- manifest: latestServerManifest,
552
- bundles: latestServerManifestsByBundleId,
559
+ schema: 1,
560
+ isBuild,
561
+ appDirectory,
562
+ outputClientPath,
563
+ routes,
564
+ assetPrefix,
565
+ snapshot,
553
566
  });
554
- const previous = existsSync(serverManifestStampPath)
555
- ? readFileSync(serverManifestStampPath, 'utf8')
556
- : undefined;
567
+ let previous: string | undefined;
568
+ try {
569
+ previous = readFileSync(serverManifestStampPath, 'utf8');
570
+ } catch (error) {
571
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
572
+ }
557
573
  if (stamp !== previous) {
558
574
  fsExtra.outputFileSync(serverManifestStampPath, stamp);
559
575
  }
@@ -572,17 +588,8 @@ export const pluginReactRouter = (
572
588
  isBuild || isRscMode
573
589
  ? undefined
574
590
  : resolveReactRefreshRuntimePath(api.context.rootPath);
575
- const devHdrSignal = devHmrRefreshRuntimePath
576
- ? createDevHdrRevisionSignal({
577
- filePath: getDevHdrRevisionFilePath(api.context.rootPath),
578
- onError: error =>
579
- api.logger.debug(
580
- `[${PLUGIN_NAME}] Failed to signal hot data revalidation: ${error.message}`
581
- ),
582
- })
583
- : undefined;
584
591
  let devHmrEnabled = false;
585
- if (devHmrRefreshRuntimePath && devHdrSignal) {
592
+ if (devHmrRefreshRuntimePath) {
586
593
  api.modifyEnvironmentConfig(
587
594
  async (environmentConfig, { name, mergeEnvironmentConfig }) => {
588
595
  if (name !== 'web') return environmentConfig;
@@ -590,7 +597,28 @@ export const pluginReactRouter = (
590
597
  tools: {
591
598
  rspack: rspackConfig => {
592
599
  devHmrEnabled = isRspackSwcReactRefreshEnabled(rspackConfig);
593
- if (devHmrEnabled) devHdrSignal.ensure();
600
+ if (devHmrEnabled) {
601
+ const entries = rspackConfig.entry;
602
+ if (
603
+ entries &&
604
+ typeof entries === 'object' &&
605
+ !Array.isArray(entries)
606
+ ) {
607
+ for (const [name, entry] of Object.entries(entries)) {
608
+ const description =
609
+ typeof entry === 'string' || Array.isArray(entry)
610
+ ? { import: entry }
611
+ : entry;
612
+ entries[name] = {
613
+ ...description,
614
+ import: [
615
+ DEV_HMR_RUNTIME_MODULE_ID,
616
+ ...[description.import ?? []].flat(),
617
+ ],
618
+ };
619
+ }
620
+ }
621
+ }
594
622
  return rspackConfig;
595
623
  },
596
624
  },
@@ -640,19 +668,14 @@ export const pluginReactRouter = (
640
668
  routeChunkCache,
641
669
  serverAppPath,
642
670
  shouldDependOnWebCompiler,
643
- devHmr:
644
- devHmrRefreshRuntimePath && devHdrSignal
645
- ? {
646
- isEnabled: () => devHmrEnabled,
647
- runtimeModule: generateDevHmrRuntimeModule({
648
- reactRefreshRuntimePath: devHmrRefreshRuntimePath,
649
- hdrRevisionFilePath: devHdrSignal.filePath,
650
- }),
651
- onNodeRebuildCommitted: () => {
652
- if (devHmrEnabled) devHdrSignal.bump();
653
- },
654
- }
655
- : undefined,
671
+ devHmr: devHmrRefreshRuntimePath
672
+ ? {
673
+ isEnabled: () => devHmrEnabled,
674
+ runtimeModule: generateDevHmrRuntimeModule({
675
+ reactRefreshRuntimePath: devHmrRefreshRuntimePath,
676
+ }),
677
+ }
678
+ : undefined,
656
679
  }));
657
680
 
658
681
  const { manifestChunkNames } = modePlan;
@@ -736,65 +759,76 @@ export const pluginReactRouter = (
736
759
  onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
737
760
  });
738
761
 
739
- const stageLatestManifests = (
740
- manifest: ReactRouterManifest,
741
- sri: ReactRouterManifest['sri'],
742
- moduleExportsByRouteId: RouteManifestModuleExports,
743
- compilation: Rspack.Compilation
744
- ) => {
745
- performanceProfiler.recordSync(
746
- 'web',
747
- 'manifest:stage',
748
- 'virtual/react-router/browser-manifest',
749
- () => {
750
- latestBrowserManifest = manifest;
751
- devBackgroundResources.setManifest(manifest);
752
- latestBrowserManifestModuleExports = moduleExportsByRouteId;
753
- const baseServerManifest = {
754
- ...manifest,
755
- sri,
756
- };
757
- latestServerManifest = baseServerManifest;
758
- const manifestsByEntryName: Record<string, ReactRouterManifest> = {
759
- [devServerBuildEntryName]: baseServerManifest,
760
- };
761
-
762
- if (modePlan.kind !== 'classic') {
763
- return;
764
- }
765
-
766
- for (const { bundleId, entryName } of modePlan.artifacts
767
- .serverBundleEntries) {
768
- const bundleRoutes =
769
- modePlan.artifacts.routesByServerBundleId[bundleId];
770
- if (!bundleRoutes) {
771
- continue;
772
- }
773
-
774
- const routeIds = new Set(Object.keys(bundleRoutes));
775
- const filteredRoutes = Object.fromEntries(
776
- Object.entries(manifest.routes).filter(([routeId]) =>
777
- routeIds.has(routeId)
778
- )
779
- );
780
- const bundleManifest = {
781
- ...baseServerManifest,
782
- routes: filteredRoutes,
783
- };
784
- latestServerManifestsByBundleId[bundleId] = bundleManifest;
785
- manifestsByEntryName[entryName] = bundleManifest;
786
- }
787
- writeServerManifestStamp();
762
+ const manifestState = createReactRouterManifestState({
763
+ api,
764
+ isBuild,
765
+ onPublish: (compilation, snapshot) => {
766
+ writeServerManifestStamp(snapshot);
767
+ devBackgroundResources.setManifest(snapshot.browser);
768
+ if (!isBuild && modePlan.kind === 'classic') {
769
+ modePlan.artifacts.devRuntime.captureWeb(
770
+ compilation,
771
+ snapshot.serverByEntryName
772
+ );
773
+ }
774
+ },
775
+ });
788
776
 
789
- if (!isBuild) {
790
- modePlan.artifacts.devRuntime.captureWeb(
791
- compilation,
792
- manifestsByEntryName
793
- );
794
- }
777
+ let persistedSnapshot: ReactRouterManifestSnapshot | null = null;
778
+ let persistedSnapshotError: Error | undefined;
779
+ // A separate node-only invocation has no browser compilation to publish a
780
+ // snapshot. Reuse finalized output from a compatible successful web build.
781
+ api.onBeforeCreateCompiler(() => {
782
+ if (
783
+ !isBuild ||
784
+ modePlan.kind !== 'classic' ||
785
+ api.getNormalizedConfig().environments.web
786
+ )
787
+ return;
788
+ const rebuildMessage = `[${PLUGIN_NAME}] Run a full build before building only the node environment; no compatible finalized browser manifest is available.`;
789
+ try {
790
+ const stamp = JSON.parse(readFileSync(serverManifestStampPath, 'utf8'));
791
+ if (
792
+ stamp.schema !== 1 ||
793
+ stamp.isBuild !== true ||
794
+ stamp.appDirectory !== appDirectory ||
795
+ stamp.outputClientPath !== outputClientPath ||
796
+ stamp.assetPrefix !== assetPrefix ||
797
+ JSON.stringify(stamp.routes) !== JSON.stringify(routes) ||
798
+ typeof stamp.snapshot?.server?.version !== 'string' ||
799
+ !stamp.snapshot?.serverByBundleId ||
800
+ !stamp.snapshot?.browser
801
+ ) {
802
+ throw new Error(rebuildMessage);
795
803
  }
796
- );
804
+ persistedSnapshot = createReactRouterManifestSnapshot({
805
+ manifest: stamp.snapshot.browser,
806
+ sri: stamp.snapshot.server.sri,
807
+ moduleExportsByRouteId: stamp.snapshot.moduleExportsByRouteId,
808
+ serverBuildPlan: {
809
+ defaultEntryName: devServerBuildEntryName,
810
+ serverBundleEntries: modePlan.artifacts.serverBundleEntries,
811
+ },
812
+ routesByServerBundleId: modePlan.artifacts.routesByServerBundleId,
813
+ });
814
+ // A node-only deployment can change server bundle partitions without
815
+ // changing browser assets; invalidate cached virtual modules too.
816
+ writeServerManifestStamp(persistedSnapshot);
817
+ } catch (cause) {
818
+ persistedSnapshotError = new Error(rebuildMessage, { cause });
819
+ }
820
+ });
821
+ const readManifestSnapshot = () => {
822
+ if (persistedSnapshotError) throw persistedSnapshotError;
823
+ return manifestState.read() ?? persistedSnapshot;
797
824
  };
825
+ if (isBuild && modePlan.kind === 'classic') {
826
+ registerNodeOnlyManifestValidation({
827
+ api,
828
+ routeByFilePath,
829
+ getSnapshot: () => persistedSnapshot,
830
+ });
831
+ }
798
832
 
799
833
  let clientStats: ReactRouterManifestStats | undefined;
800
834
  api.onAfterEnvironmentCompile(({ stats, environment }) => {
@@ -841,52 +875,58 @@ export const pluginReactRouter = (
841
875
  });
842
876
 
843
877
  if (modePlan.kind === 'classic') {
844
- api.onAfterBuild(({ environments }) =>
845
- effectRuntime.runPromise(
846
- tryPluginPromise(() =>
847
- runReactRouterPrerenderBuild({
848
- api,
849
- hasWebEnvironment: Boolean(environments.web),
850
- buildDirectory,
851
- serverBuildFile,
852
- ssr,
853
- isPrerenderEnabled,
854
- prerenderConfig,
855
- prerenderPaths: modePlan.artifacts.prerenderPaths,
856
- basename,
857
- future,
858
- routes,
859
- latestBrowserManifest,
860
- latestBrowserManifestModuleExports,
861
- clientStats,
862
- pluginOptions,
863
- appDirectory,
864
- assetPrefix,
865
- routeChunkOptions: modePlan.routeChunkOptions,
866
- routeModuleAnalysis,
867
- buildManifest: modePlan.artifacts.buildManifest,
868
- buildEndReactRouterConfig,
869
- buildEnd,
870
- })
871
- )
872
- )
878
+ api.onAfterBuild(({ environments, stats }) =>
879
+ stats?.hasErrors()
880
+ ? undefined
881
+ : effectRuntime.runPromise(
882
+ tryPluginPromise(() =>
883
+ runReactRouterPrerenderBuild({
884
+ api,
885
+ hasWebEnvironment: Boolean(environments.web),
886
+ buildDirectory,
887
+ serverBuildFile,
888
+ ssr,
889
+ isPrerenderEnabled,
890
+ prerenderConfig,
891
+ prerenderPaths: modePlan.artifacts.prerenderPaths,
892
+ basename,
893
+ future,
894
+ routes,
895
+ latestBrowserManifest:
896
+ readManifestSnapshot()?.browser ?? null,
897
+ latestBrowserManifestModuleExports:
898
+ readManifestSnapshot()?.moduleExportsByRouteId ?? {},
899
+ clientStats,
900
+ pluginOptions,
901
+ appDirectory,
902
+ assetPrefix,
903
+ routeChunkOptions: modePlan.routeChunkOptions,
904
+ routeModuleAnalysis,
905
+ buildManifest: modePlan.artifacts.buildManifest,
906
+ buildEndReactRouterConfig,
907
+ buildEnd,
908
+ })
909
+ )
910
+ )
873
911
  );
874
912
  } else {
875
- api.onAfterBuild(({ environments }) =>
876
- effectRuntime.runPromise(
877
- tryPluginPromise(() =>
878
- runReactRouterRscPrerenderBuild({
879
- api,
880
- hasWebEnvironment: Boolean(environments.web),
881
- buildDirectory,
882
- serverBuildFile,
883
- ssr,
884
- prerenderConfig,
885
- prerenderPaths: modePlan.prerenderPaths,
886
- basename,
887
- })
888
- )
889
- )
913
+ api.onAfterBuild(({ environments, stats }) =>
914
+ stats?.hasErrors()
915
+ ? undefined
916
+ : effectRuntime.runPromise(
917
+ tryPluginPromise(() =>
918
+ runReactRouterRscPrerenderBuild({
919
+ api,
920
+ hasWebEnvironment: Boolean(environments.web),
921
+ buildDirectory,
922
+ serverBuildFile,
923
+ ssr,
924
+ prerenderConfig,
925
+ prerenderPaths: modePlan.prerenderPaths,
926
+ basename,
927
+ })
928
+ )
929
+ )
890
930
  );
891
931
  }
892
932
 
@@ -1174,11 +1214,26 @@ export const pluginReactRouter = (
1174
1214
  manifestChunkNames,
1175
1215
  routeModuleAnalysis,
1176
1216
  onManifest: (manifest, sri, moduleExportsByRouteId, context) =>
1177
- stageLatestManifests(
1178
- manifest,
1179
- sri,
1180
- moduleExportsByRouteId,
1181
- context.compilation
1217
+ performanceProfiler.recordSync(
1218
+ 'web',
1219
+ 'manifest:stage',
1220
+ 'virtual/react-router/browser-manifest',
1221
+ () =>
1222
+ manifestState.stage(
1223
+ context.compilation,
1224
+ createReactRouterManifestSnapshot({
1225
+ manifest,
1226
+ sri,
1227
+ moduleExportsByRouteId,
1228
+ serverBuildPlan: {
1229
+ defaultEntryName: devServerBuildEntryName,
1230
+ serverBundleEntries:
1231
+ modePlan.artifacts.serverBundleEntries,
1232
+ },
1233
+ routesByServerBundleId:
1234
+ modePlan.artifacts.routesByServerBundleId,
1235
+ })
1236
+ )
1182
1237
  ),
1183
1238
  }
1184
1239
  );
@@ -1187,10 +1242,10 @@ export const pluginReactRouter = (
1187
1242
  api,
1188
1243
  resolvedServerOutput,
1189
1244
  performanceProfiler,
1190
- getLatestServerManifest: () => latestServerManifest,
1245
+ getLatestServerManifest: () => readManifestSnapshot()?.server ?? null,
1191
1246
  serverManifestStampPath,
1192
1247
  getLatestServerManifestByBundleId: bundleId =>
1193
- latestServerManifestsByBundleId[bundleId],
1248
+ readManifestSnapshot()?.serverByBundleId[bundleId],
1194
1249
  routes,
1195
1250
  pluginOptions,
1196
1251
  getClientStats: () => clientStats,
@@ -1,5 +1,9 @@
1
- import { BUILD_CLIENT_ROUTE_QUERY_STRING } from './constants.js';
1
+ import {
2
+ BROWSER_MANIFEST_ENTRY_NAME,
3
+ BUILD_CLIENT_ROUTE_QUERY_STRING,
4
+ } from './constants.js';
2
5
  import type { PluginOptions } from './types.js';
6
+ import { DEV_HMR_RUNTIME_MODULE_ID } from './dev-hmr.js';
3
7
 
4
8
  type LazyCompilationOptions = Exclude<
5
9
  NonNullable<PluginOptions['lazyCompilation']>,
@@ -49,7 +53,8 @@ const matchesLazyCompilationTest = (
49
53
 
50
54
  const createReactRouterHydrationModuleTest = (entryClientPath: string) => {
51
55
  const eagerPatterns = [
52
- 'virtual/react-router/browser-manifest',
56
+ BROWSER_MANIFEST_ENTRY_NAME,
57
+ DEV_HMR_RUNTIME_MODULE_ID,
53
58
  ...(entryClientPath
54
59
  ? [
55
60
  normalizeSlashes(entryClientPath),