rsbuild-plugin-react-router 0.7.2 → 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.
Files changed (53) hide show
  1. package/README.md +14 -2
  2. package/dist/468.js +48 -0
  3. package/dist/511.js +60 -86
  4. package/dist/819.js +64 -0
  5. package/dist/build-output-transforms.d.ts +3 -1
  6. package/dist/constants.d.ts +1 -0
  7. package/dist/dev-hdr-channel.d.ts +9 -0
  8. package/dist/dev-hmr.d.ts +1 -22
  9. package/dist/dev-runtime-controller.d.ts +1 -6
  10. package/dist/dev-server.d.ts +3 -1
  11. package/dist/index.cjs +5417 -5080
  12. package/dist/index.js +1701 -1441
  13. package/dist/manifest-assets.d.ts +34 -0
  14. package/dist/manifest-snapshot.d.ts +17 -0
  15. package/dist/manifest-state.d.ts +12 -0
  16. package/dist/manifest.d.ts +2 -22
  17. package/dist/modify-browser-manifest.d.ts +2 -0
  18. package/dist/node-only-manifest.d.ts +8 -0
  19. package/dist/plugin-utils.d.ts +1 -1
  20. package/dist/rsc-prerender.d.ts +3 -2
  21. package/dist/server-build-worker-client.d.ts +20 -0
  22. package/dist/server-build-worker-protocol.d.ts +84 -0
  23. package/dist/server-build-worker.d.ts +1 -0
  24. package/dist/server-build-worker.js +123 -0
  25. package/dist/server-utils.d.ts +1 -2
  26. package/dist/types.d.ts +6 -0
  27. package/package.json +4 -4
  28. package/src/build-output-transforms.ts +22 -1
  29. package/src/classic-mode.ts +0 -1
  30. package/src/constants.ts +3 -0
  31. package/src/dev-hdr-channel.ts +38 -0
  32. package/src/dev-hmr.ts +57 -100
  33. package/src/dev-runtime-controller.ts +23 -18
  34. package/src/dev-server.ts +24 -4
  35. package/src/index.ts +229 -144
  36. package/src/lazy-compilation.ts +7 -2
  37. package/src/manifest-assets.ts +228 -0
  38. package/src/manifest-snapshot.ts +80 -0
  39. package/src/manifest-state.ts +71 -0
  40. package/src/manifest.ts +23 -161
  41. package/src/mode-plan.ts +12 -4
  42. package/src/modify-browser-manifest.ts +47 -18
  43. package/src/node-only-manifest.ts +52 -0
  44. package/src/plugin-utils.ts +6 -2
  45. package/src/prerender-build.ts +76 -86
  46. package/src/route-chunks.ts +152 -63
  47. package/src/rsc-prerender.ts +7 -35
  48. package/src/server-build-resolution.ts +1 -2
  49. package/src/server-build-worker-client.ts +221 -0
  50. package/src/server-build-worker-protocol.ts +69 -0
  51. package/src/server-build-worker.ts +192 -0
  52. package/src/server-utils.ts +0 -2
  53. package/src/types.ts +7 -0
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,48 @@ 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
- {};
535
+ // The node `server-manifest` module's source is a constant; its real
536
+ // content is injected by a transform from the web compilation's emitted
537
+ // asset names. Rspack's persistent cache would therefore reuse a previous
538
+ // build's module even when those names changed (#136). The transform
539
+ // declares this file, which holds the captured manifests, as a file
540
+ // dependency so the cache invalidates exactly when the manifest changes.
541
+ const serverManifestStampPath = resolve(
542
+ api.context.cachePath,
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'),
548
+ 'server-manifest.json'
549
+ );
550
+ // Bundle manifests also depend on the route partition, which can change
551
+ // independently of browser assets (for example via deployment inputs).
552
+ // Only rewrite on change: a bumped mtime would otherwise invalidate the
553
+ // module on every build and, if the cache dir is watched, rebuild node
554
+ // after every web rebuild in dev.
555
+ const writeServerManifestStamp = (
556
+ snapshot: ReactRouterManifestSnapshot
557
+ ): void => {
558
+ const stamp = JSON.stringify({
559
+ schema: 1,
560
+ isBuild,
561
+ appDirectory,
562
+ outputClientPath,
563
+ routes,
564
+ assetPrefix,
565
+ snapshot,
566
+ });
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
+ }
573
+ if (stamp !== previous) {
574
+ fsExtra.outputFileSync(serverManifestStampPath, stamp);
575
+ }
576
+ };
533
577
 
534
578
  const routeByFilePath = new Map(
535
579
  Object.values(routes).map(route => [
@@ -544,17 +588,8 @@ export const pluginReactRouter = (
544
588
  isBuild || isRscMode
545
589
  ? undefined
546
590
  : resolveReactRefreshRuntimePath(api.context.rootPath);
547
- const devHdrSignal = devHmrRefreshRuntimePath
548
- ? createDevHdrRevisionSignal({
549
- filePath: getDevHdrRevisionFilePath(api.context.rootPath),
550
- onError: error =>
551
- api.logger.debug(
552
- `[${PLUGIN_NAME}] Failed to signal hot data revalidation: ${error.message}`
553
- ),
554
- })
555
- : undefined;
556
591
  let devHmrEnabled = false;
557
- if (devHmrRefreshRuntimePath && devHdrSignal) {
592
+ if (devHmrRefreshRuntimePath) {
558
593
  api.modifyEnvironmentConfig(
559
594
  async (environmentConfig, { name, mergeEnvironmentConfig }) => {
560
595
  if (name !== 'web') return environmentConfig;
@@ -562,7 +597,28 @@ export const pluginReactRouter = (
562
597
  tools: {
563
598
  rspack: rspackConfig => {
564
599
  devHmrEnabled = isRspackSwcReactRefreshEnabled(rspackConfig);
565
- 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
+ }
566
622
  return rspackConfig;
567
623
  },
568
624
  },
@@ -612,19 +668,14 @@ export const pluginReactRouter = (
612
668
  routeChunkCache,
613
669
  serverAppPath,
614
670
  shouldDependOnWebCompiler,
615
- devHmr:
616
- devHmrRefreshRuntimePath && devHdrSignal
617
- ? {
618
- isEnabled: () => devHmrEnabled,
619
- runtimeModule: generateDevHmrRuntimeModule({
620
- reactRefreshRuntimePath: devHmrRefreshRuntimePath,
621
- hdrRevisionFilePath: devHdrSignal.filePath,
622
- }),
623
- onNodeRebuildCommitted: () => {
624
- if (devHmrEnabled) devHdrSignal.bump();
625
- },
626
- }
627
- : undefined,
671
+ devHmr: devHmrRefreshRuntimePath
672
+ ? {
673
+ isEnabled: () => devHmrEnabled,
674
+ runtimeModule: generateDevHmrRuntimeModule({
675
+ reactRefreshRuntimePath: devHmrRefreshRuntimePath,
676
+ }),
677
+ }
678
+ : undefined,
628
679
  }));
629
680
 
630
681
  const { manifestChunkNames } = modePlan;
@@ -708,64 +759,76 @@ export const pluginReactRouter = (
708
759
  onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
709
760
  });
710
761
 
711
- const stageLatestManifests = (
712
- manifest: ReactRouterManifest,
713
- sri: ReactRouterManifest['sri'],
714
- moduleExportsByRouteId: RouteManifestModuleExports,
715
- compilation: Rspack.Compilation
716
- ) => {
717
- performanceProfiler.recordSync(
718
- 'web',
719
- 'manifest:stage',
720
- 'virtual/react-router/browser-manifest',
721
- () => {
722
- latestBrowserManifest = manifest;
723
- devBackgroundResources.setManifest(manifest);
724
- latestBrowserManifestModuleExports = moduleExportsByRouteId;
725
- const baseServerManifest = {
726
- ...manifest,
727
- sri,
728
- };
729
- latestServerManifest = baseServerManifest;
730
- const manifestsByEntryName: Record<string, ReactRouterManifest> = {
731
- [devServerBuildEntryName]: baseServerManifest,
732
- };
733
-
734
- if (modePlan.kind !== 'classic') {
735
- return;
736
- }
737
-
738
- for (const { bundleId, entryName } of modePlan.artifacts
739
- .serverBundleEntries) {
740
- const bundleRoutes =
741
- modePlan.artifacts.routesByServerBundleId[bundleId];
742
- if (!bundleRoutes) {
743
- continue;
744
- }
745
-
746
- const routeIds = new Set(Object.keys(bundleRoutes));
747
- const filteredRoutes = Object.fromEntries(
748
- Object.entries(manifest.routes).filter(([routeId]) =>
749
- routeIds.has(routeId)
750
- )
751
- );
752
- const bundleManifest = {
753
- ...baseServerManifest,
754
- routes: filteredRoutes,
755
- };
756
- latestServerManifestsByBundleId[bundleId] = bundleManifest;
757
- manifestsByEntryName[entryName] = bundleManifest;
758
- }
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
+ });
759
776
 
760
- if (!isBuild) {
761
- modePlan.artifacts.devRuntime.captureWeb(
762
- compilation,
763
- manifestsByEntryName
764
- );
765
- }
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);
766
803
  }
767
- );
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;
768
824
  };
825
+ if (isBuild && modePlan.kind === 'classic') {
826
+ registerNodeOnlyManifestValidation({
827
+ api,
828
+ routeByFilePath,
829
+ getSnapshot: () => persistedSnapshot,
830
+ });
831
+ }
769
832
 
770
833
  let clientStats: ReactRouterManifestStats | undefined;
771
834
  api.onAfterEnvironmentCompile(({ stats, environment }) => {
@@ -812,52 +875,58 @@ export const pluginReactRouter = (
812
875
  });
813
876
 
814
877
  if (modePlan.kind === 'classic') {
815
- api.onAfterBuild(({ environments }) =>
816
- effectRuntime.runPromise(
817
- tryPluginPromise(() =>
818
- runReactRouterPrerenderBuild({
819
- api,
820
- hasWebEnvironment: Boolean(environments.web),
821
- buildDirectory,
822
- serverBuildFile,
823
- ssr,
824
- isPrerenderEnabled,
825
- prerenderConfig,
826
- prerenderPaths: modePlan.artifacts.prerenderPaths,
827
- basename,
828
- future,
829
- routes,
830
- latestBrowserManifest,
831
- latestBrowserManifestModuleExports,
832
- clientStats,
833
- pluginOptions,
834
- appDirectory,
835
- assetPrefix,
836
- routeChunkOptions: modePlan.routeChunkOptions,
837
- routeModuleAnalysis,
838
- buildManifest: modePlan.artifacts.buildManifest,
839
- buildEndReactRouterConfig,
840
- buildEnd,
841
- })
842
- )
843
- )
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
+ )
844
911
  );
845
912
  } else {
846
- api.onAfterBuild(({ environments }) =>
847
- effectRuntime.runPromise(
848
- tryPluginPromise(() =>
849
- runReactRouterRscPrerenderBuild({
850
- api,
851
- hasWebEnvironment: Boolean(environments.web),
852
- buildDirectory,
853
- serverBuildFile,
854
- ssr,
855
- prerenderConfig,
856
- prerenderPaths: modePlan.prerenderPaths,
857
- basename,
858
- })
859
- )
860
- )
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
+ )
861
930
  );
862
931
  }
863
932
 
@@ -1145,11 +1214,26 @@ export const pluginReactRouter = (
1145
1214
  manifestChunkNames,
1146
1215
  routeModuleAnalysis,
1147
1216
  onManifest: (manifest, sri, moduleExportsByRouteId, context) =>
1148
- stageLatestManifests(
1149
- manifest,
1150
- sri,
1151
- moduleExportsByRouteId,
1152
- 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
+ )
1153
1237
  ),
1154
1238
  }
1155
1239
  );
@@ -1158,9 +1242,10 @@ export const pluginReactRouter = (
1158
1242
  api,
1159
1243
  resolvedServerOutput,
1160
1244
  performanceProfiler,
1161
- getLatestServerManifest: () => latestServerManifest,
1245
+ getLatestServerManifest: () => readManifestSnapshot()?.server ?? null,
1246
+ serverManifestStampPath,
1162
1247
  getLatestServerManifestByBundleId: bundleId =>
1163
- latestServerManifestsByBundleId[bundleId],
1248
+ readManifestSnapshot()?.serverByBundleId[bundleId],
1164
1249
  routes,
1165
1250
  pluginOptions,
1166
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),