rsbuild-plugin-react-router 0.4.1 → 0.6.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 (126) hide show
  1. package/README.md +72 -240
  2. package/dist/{451.js → 511.js} +461 -331
  3. package/dist/build-output-transforms.d.ts +32 -7
  4. package/dist/classic-mode.d.ts +56 -0
  5. package/dist/config-imports.d.ts +8 -2
  6. package/dist/constants.d.ts +3 -0
  7. package/dist/dev-background-resources.d.ts +3 -3
  8. package/dist/dev-generation.d.ts +2 -3
  9. package/dist/dev-hmr.d.ts +8 -2
  10. package/dist/dev-runtime-controller.d.ts +6 -1
  11. package/dist/dev-source-maps.d.ts +4 -0
  12. package/dist/effect-runtime.d.ts +17 -5
  13. package/dist/entry-paths.d.ts +16 -0
  14. package/dist/environment-output.d.ts +6 -0
  15. package/dist/export-utils.d.ts +2 -1
  16. package/dist/federation.d.ts +2 -0
  17. package/dist/index.cjs +3675 -2107
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.js +3614 -2214
  20. package/dist/lazy-compilation-prewarm.d.ts +8 -5
  21. package/dist/manifest.d.ts +16 -4
  22. package/dist/mode-plan.d.ts +82 -0
  23. package/dist/modify-browser-manifest.d.ts +8 -4
  24. package/dist/performance.d.ts +4 -1
  25. package/dist/plugin-utils.d.ts +27 -1
  26. package/dist/prerender-build.d.ts +5 -5
  27. package/dist/prerender.d.ts +1 -5
  28. package/dist/react-router-config.d.ts +11 -3
  29. package/dist/route-artifacts.d.ts +4 -2
  30. package/dist/route-chunks.d.ts +2 -1
  31. package/dist/route-component-transform.d.ts +0 -2
  32. package/dist/route-export-pruning.d.ts +4 -1
  33. package/dist/route-imports.d.ts +18 -0
  34. package/dist/route-module-transform-loader.d.ts +14 -0
  35. package/dist/route-module-transform-loader.js +44 -0
  36. package/dist/route-module-transform-rules.d.ts +14 -0
  37. package/dist/route-transform-tasks.d.ts +6 -0
  38. package/dist/route-watch.d.ts +11 -6
  39. package/dist/rsc-dev-server.d.ts +26 -0
  40. package/dist/rsc-prerender.d.ts +54 -0
  41. package/dist/rsc-route-config.d.ts +6 -0
  42. package/dist/rsc-route-exports.d.ts +15 -0
  43. package/dist/rsc-route-transform-loader.cjs +43 -0
  44. package/dist/rsc-route-transform-loader.d.ts +30 -0
  45. package/dist/rsc-route-transform-loader.js +16 -0
  46. package/dist/rsc-route-transform-registration.d.ts +12 -0
  47. package/dist/rsc-route-transforms.d.ts +21 -0
  48. package/dist/rsc-support.d.ts +23 -0
  49. package/dist/rsc-virtual-modules.d.ts +19 -0
  50. package/dist/server-build-plan.d.ts +2 -1
  51. package/dist/server-build-resolution.d.ts +1 -2
  52. package/dist/server-utils.d.ts +4 -5
  53. package/dist/ssr-asset-relocation.d.ts +98 -0
  54. package/dist/templates/entry.rsc.client.d.ts +1 -0
  55. package/dist/templates/entry.rsc.client.js +61 -0
  56. package/dist/templates/entry.rsc.d.ts +9 -0
  57. package/dist/templates/entry.rsc.js +38 -0
  58. package/dist/templates/entry.rsc.ssr.d.ts +4 -0
  59. package/dist/templates/entry.rsc.ssr.js +24 -0
  60. package/dist/typegen.d.ts +4 -2
  61. package/dist/types.d.ts +34 -6
  62. package/package.json +76 -22
  63. package/src/build-output-transforms.ts +181 -43
  64. package/src/classic-mode.ts +253 -0
  65. package/src/config-imports.ts +153 -6
  66. package/src/constants.ts +6 -2
  67. package/src/dev-background-resources.ts +46 -100
  68. package/src/dev-generation.ts +52 -33
  69. package/src/dev-hmr.ts +112 -80
  70. package/src/dev-runtime-artifacts.ts +11 -12
  71. package/src/dev-runtime-controller.ts +75 -85
  72. package/src/dev-runtime-session.ts +14 -18
  73. package/src/dev-server.ts +2 -0
  74. package/src/dev-source-maps.ts +257 -0
  75. package/src/effect-runtime.ts +105 -57
  76. package/src/entry-paths.ts +80 -0
  77. package/src/environment-output.ts +55 -0
  78. package/src/export-utils.ts +15 -11
  79. package/src/federation.ts +38 -0
  80. package/src/index.ts +666 -519
  81. package/src/lazy-compilation-prewarm.ts +20 -4
  82. package/src/manifest.ts +157 -82
  83. package/src/mode-plan.ts +367 -0
  84. package/src/modify-browser-manifest.ts +130 -124
  85. package/src/performance.ts +11 -2
  86. package/src/plugin-utils.ts +103 -39
  87. package/src/prerender-build.ts +82 -104
  88. package/src/prerender.ts +23 -24
  89. package/src/react-router-config.ts +72 -26
  90. package/src/route-artifacts.ts +96 -66
  91. package/src/route-chunks.ts +167 -51
  92. package/src/route-component-transform.ts +14 -21
  93. package/src/route-export-pruning.ts +4 -3
  94. package/src/route-imports.ts +100 -0
  95. package/src/route-module-transform-loader.ts +149 -0
  96. package/src/route-module-transform-rules.ts +115 -0
  97. package/src/route-transform-tasks.ts +44 -29
  98. package/src/route-watch.ts +166 -188
  99. package/src/rsc-dev-server.ts +112 -0
  100. package/src/rsc-prerender.ts +362 -0
  101. package/src/rsc-route-config.ts +175 -0
  102. package/src/rsc-route-exports.ts +67 -0
  103. package/src/rsc-route-transform-loader.ts +65 -0
  104. package/src/rsc-route-transform-registration.ts +145 -0
  105. package/src/rsc-route-transforms.ts +995 -0
  106. package/src/rsc-runtime.d.ts +143 -0
  107. package/src/rsc-support.ts +116 -0
  108. package/src/rsc-virtual-modules.ts +113 -0
  109. package/src/server-build-plan.ts +14 -3
  110. package/src/server-build-resolution.ts +37 -47
  111. package/src/server-utils.ts +21 -35
  112. package/src/ssr-asset-relocation.ts +183 -0
  113. package/src/ssr-externals.ts +8 -26
  114. package/src/templates/entry.rsc.client.tsx +168 -0
  115. package/src/templates/entry.rsc.ssr.tsx +45 -0
  116. package/src/templates/entry.rsc.tsx +80 -0
  117. package/src/typegen.ts +40 -23
  118. package/src/types.ts +43 -6
  119. package/src/warnings/warn-on-client-source-maps.ts +6 -10
  120. package/dist/parallel-route-transform-protocol.d.ts +0 -25
  121. package/dist/parallel-route-transform-worker.d.ts +0 -1
  122. package/dist/parallel-route-transform-worker.js +0 -38
  123. package/dist/parallel-route-transforms.d.ts +0 -29
  124. package/src/parallel-route-transform-protocol.ts +0 -35
  125. package/src/parallel-route-transform-worker.ts +0 -82
  126. package/src/parallel-route-transforms.ts +0 -458
package/src/index.ts CHANGED
@@ -2,101 +2,109 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import fsExtra from 'fs-extra';
3
3
  import type { Config } from './react-router-config.js';
4
4
  import type { RouteConfigEntry } from '@react-router/dev/routes';
5
- import {
6
- rspack,
7
- type RsbuildEntryDescription,
8
- type RsbuildPlugin,
9
- type Rspack,
10
- } from '@rsbuild/core';
11
- import { createJiti } from 'jiti';
5
+ import { rspack, type RsbuildPlugin, type Rspack } from '@rsbuild/core';
12
6
  import { relative, resolve } from 'pathe';
13
7
 
14
8
  import { getDefaultConcurrency } from './concurrency.js';
15
9
  import {
16
- BUILD_CLIENT_ROUTE_QUERY_STRING,
10
+ DEFAULT_JS_DIST_PATH,
17
11
  JS_EXTENSIONS,
18
12
  PLUGIN_NAME,
19
13
  } from './constants.js';
20
14
  import { guardReactRouterLazyCompilation } from './lazy-compilation.js';
21
- import { createReactRouterDevServerSetup } from './dev-server.js';
22
15
  import {
23
- generateWithProps,
24
16
  findEntryFile,
25
- normalizeAssetPrefix,
17
+ resolveAppPackagePath,
18
+ resolveEffectiveAssetPrefix,
26
19
  } from './plugin-utils.js';
27
- import type { PluginOptions } from './types.js';
28
- import {
29
- generateServerBuild,
30
- resolveReactRouterServerBuild,
31
- } from './server-utils.js';
32
- import { resolvePrerenderPaths, validatePrerenderConfig } from './prerender.js';
20
+ import { resolveReactRouterEntryPaths } from './entry-paths.js';
21
+ import { registerReactRouterEnvironmentOutput } from './environment-output.js';
22
+ import type { PluginOptions, ReactRouterRSCPluginOptions } from './types.js';
23
+ import { resolveReactRouterServerBuild } from './server-utils.js';
24
+ import { validatePrerenderConfig } from './prerender.js';
33
25
  import { runReactRouterPrerenderBuild } from './prerender-build.js';
26
+ import { runReactRouterRscPrerenderBuild } from './rsc-prerender.js';
34
27
  import {
35
- resolveReactRouterConfig,
28
+ resolveReactRouterConfigEffect,
29
+ resolveRouteDiscoveryConfig,
36
30
  type ResolvedReactRouterConfig,
37
31
  } from './react-router-config.js';
38
32
  import {
39
- getReactRouterManifestForDev,
40
33
  configRoutesToRouteManifest,
41
34
  createReactRouterManifestStats,
35
+ type ReactRouterManifestForDev as ReactRouterManifest,
42
36
  type ReactRouterManifestStats,
43
37
  type RouteManifestModuleExports,
44
38
  } from './manifest.js';
39
+ import type { RouteModuleAnalysis } from './export-utils.js';
45
40
  import { registerModifyBrowserManifestAssets } from './modify-browser-manifest.js';
46
- import { registerBuildOutputTransforms } from './build-output-transforms.js';
47
41
  import {
48
- getRouteChunkEntryName,
49
- getRouteChunkModuleId,
50
- routeChunkExportNames,
51
- type RouteChunkCache,
52
- type RouteChunkConfig,
53
- } from './route-chunks.js';
42
+ registerBuildOutputTransforms,
43
+ registerSsrAssetRelocation,
44
+ } from './build-output-transforms.js';
45
+ import { type RouteChunkCache } from './route-chunks.js';
46
+ import {
47
+ registerRouteModuleTransformRules,
48
+ shouldUseRouteModuleTransformLoader,
49
+ } from './route-module-transform-rules.js';
54
50
  import {
55
- createRouteTransformExecutor,
56
- shouldParallelizeRouteTransforms,
57
- } from './parallel-route-transforms.js';
51
+ executeRouteTransformTask,
52
+ type RouteTransformRunner,
53
+ } from './route-transform-tasks.js';
58
54
  import { getRouteRestartMarkerPath, mergeWatchFiles } from './route-watch.js';
59
55
  import { validateRouteConfig } from './route-config.js';
60
- import {
61
- getBuildManifest,
62
- getRoutesByServerBundleId,
63
- } from './build-manifest.js';
64
- import {
65
- createReactRouterNodeEntries,
66
- createReactRouterServerBuildPlan,
67
- } from './server-build-plan.js';
68
56
  import { warnOnClientSourceMaps } from './warnings/warn-on-client-source-maps.js';
69
57
  import { validatePluginOrderFromConfig } from './validation/validate-plugin-order.js';
70
- import { getSsrExternals } from './ssr-externals.js';
71
58
  import {
72
59
  createReactRouterPerformanceProfiler,
73
60
  roundMs,
74
61
  } from './performance.js';
75
62
  import { mapVirtualModules } from './virtual-modules.js';
76
- import { createReactRouterDevRuntimeController } from './dev-runtime-controller.js';
77
- import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
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
63
  import {
86
64
  createDevHdrRevisionSignal,
87
65
  generateDevHmrRuntimeModule,
88
66
  getDevHdrRevisionFilePath,
89
67
  isRspackSwcReactRefreshEnabled,
90
68
  resolveReactRefreshRuntimePath,
91
- DEV_HMR_RUNTIME_MODULE_ID,
92
69
  } from './dev-hmr.js';
70
+ import {
71
+ createPluginEffectRuntime,
72
+ tryPluginPromise,
73
+ } from './effect-runtime.js';
74
+ import { registerReactRouterTypegen } from './typegen.js';
75
+ import {
76
+ createConfigImporter,
77
+ type ConfigImporter,
78
+ importConfigWithWatchPaths,
79
+ } from './config-imports.js';
80
+ import {
81
+ createReactRouterRouteTopology,
82
+ createReactRouterRouteWatchFiles,
83
+ registerReactRouterDevBackgroundResources,
84
+ } from './dev-background-resources.js';
85
+ import {
86
+ assertReactRouterRscConfigSupport,
87
+ assertReactRouterRscSupport,
88
+ registerReactRouterRscRouteTransforms,
89
+ setupReactRouterRscPlugin,
90
+ } from './rsc-support.js';
91
+ import { createReactRouterModePlan } from './mode-plan.js';
92
+ import { createQuerylessRouteImportPlugin } from './route-imports.js';
93
+ import { registerDevServerSourceMaps } from './dev-source-maps.js';
93
94
 
94
95
  export type { Config as ReactRouterRsbuildConfig } from './react-router-config.js';
95
96
  export { loadReactRouterServerBuild } from './dev-generation.js';
96
97
  export { resolveReactRouterServerBuild };
98
+ export type { PluginOptions, ReactRouterRSCPluginOptions } from './types.js';
97
99
 
98
100
  const MIN_PARALLEL_ENVIRONMENT_BUILD_SPARE_CORES = 4;
99
101
 
102
+ type ReactRouterPresetResolvedConfig = Parameters<
103
+ NonNullable<
104
+ NonNullable<Config['presets']>[number]['reactRouterConfigResolved']
105
+ >
106
+ >[0]['reactRouterConfig'];
107
+
100
108
  export const shouldParallelizeEnvironmentBuilds = ({
101
109
  isBuild,
102
110
  spareCoreCount = getDefaultConcurrency(),
@@ -106,46 +114,60 @@ export const shouldParallelizeEnvironmentBuilds = ({
106
114
  }): boolean =>
107
115
  !isBuild && spareCoreCount >= MIN_PARALLEL_ENVIRONMENT_BUILD_SPARE_CORES;
108
116
 
109
- type ModuleFederationPluginLike = {
110
- name?: string;
111
- _options?: { experiments?: { asyncStartup?: boolean } };
112
- options?: { experiments?: { asyncStartup?: boolean } };
113
- };
114
-
115
- const ensureFederationAsyncStartup = (
116
- rspackConfig: Rspack.Configuration | undefined
117
- ): void => {
118
- if (!rspackConfig?.plugins?.length) {
119
- return;
117
+ const cssUrlAssetExtensions =
118
+ /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)$/;
119
+ const urlAssetResourceQuery =
120
+ /^(?=.*(?:\?|&)url(?:&|$))(?!.*(?:\?|&)(?:raw|inline)(?:&|$))/;
121
+ const javascriptWhitespace = /\s/u;
122
+
123
+ const hasUseClientDirective = (code: string): boolean => {
124
+ let index = code.charCodeAt(0) === 0xfeff ? 1 : 0;
125
+ if (code.startsWith('#!', index)) {
126
+ const lineEnd = code.indexOf('\n', index + 2);
127
+ if (lineEnd === -1) return false;
128
+ index = lineEnd + 1;
120
129
  }
121
130
 
122
- for (const plugin of rspackConfig.plugins) {
123
- if (!plugin || typeof plugin !== 'object') {
124
- continue;
131
+ while (index < code.length) {
132
+ while (
133
+ index < code.length &&
134
+ javascriptWhitespace.test(code.charAt(index))
135
+ ) {
136
+ index += 1;
125
137
  }
126
- const pluginName = (plugin as ModuleFederationPluginLike).name;
127
- if (pluginName !== 'ModuleFederationPlugin') {
138
+ if (code.startsWith('//', index)) {
139
+ const lineEnd = code.indexOf('\n', index + 2);
140
+ if (lineEnd === -1) return false;
141
+ index = lineEnd + 1;
128
142
  continue;
129
143
  }
130
-
131
- const pluginOptions =
132
- (plugin as ModuleFederationPluginLike)._options ??
133
- (plugin as ModuleFederationPluginLike).options;
134
- if (!pluginOptions) {
144
+ if (code.startsWith('/*', index)) {
145
+ const commentEnd = code.indexOf('*/', index + 2);
146
+ if (commentEnd === -1) return false;
147
+ index = commentEnd + 2;
135
148
  continue;
136
149
  }
150
+ break;
151
+ }
137
152
 
138
- pluginOptions.experiments = {
139
- ...pluginOptions.experiments,
140
- asyncStartup: true,
141
- };
153
+ const quote = code.charAt(index);
154
+ if (quote !== '"' && quote !== "'") return false;
155
+ const directive = `${quote}use client${quote}`;
156
+ if (!code.startsWith(directive, index)) return false;
157
+ index += directive.length;
158
+ while (index < code.length && javascriptWhitespace.test(code.charAt(index))) {
159
+ index += 1;
142
160
  }
161
+ return code.charAt(index) === ';';
143
162
  };
144
163
 
145
- const cssUrlAssetExtensions =
146
- /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)$/;
147
- const urlAssetResourceQuery =
148
- /^(?=.*(?:\?|&)url(?:&|$))(?!.*(?:\?|&)(?:raw|inline)(?:&|$))/;
164
+ const isRscClientModule = (filePath: string): boolean => {
165
+ try {
166
+ return hasUseClientDirective(readFileSync(filePath, 'utf8'));
167
+ } catch {
168
+ return false;
169
+ }
170
+ };
149
171
 
150
172
  export const pluginReactRouter = (
151
173
  options: PluginOptions = {}
@@ -153,9 +175,15 @@ export const pluginReactRouter = (
153
175
  name: PLUGIN_NAME,
154
176
 
155
177
  async setup(api) {
178
+ const effectRuntime = createPluginEffectRuntime();
179
+ api.onCloseBuild(effectRuntime.dispose);
180
+ api.onCloseDevServer(effectRuntime.dispose);
181
+ api.onExit(effectRuntime.dispose);
182
+
156
183
  const defaultOptions = {
157
184
  customServer: false,
158
185
  lazyCompilation: true,
186
+ rsc: false,
159
187
  serverOutput: 'module' as const,
160
188
  };
161
189
 
@@ -163,16 +191,13 @@ export const pluginReactRouter = (
163
191
  ...defaultOptions,
164
192
  ...options,
165
193
  };
194
+ const isRscMode = Boolean(pluginOptions.rsc);
166
195
  const logPerformance = pluginOptions.logPerformance === true;
167
196
  const setupStartMs = logPerformance ? performance.now() : 0;
168
197
  const performanceProfiler = createReactRouterPerformanceProfiler({
169
198
  enabled: logPerformance,
170
199
  log: message => api.logger.info(message),
171
200
  });
172
- const nodeExternals = Array.from(
173
- new Set(['express', ...getSsrExternals(process.cwd())])
174
- );
175
-
176
201
  let assetPrefix = '/';
177
202
 
178
203
  // Best-effort configuration validation (upstream: validate-plugin-order).
@@ -188,7 +213,6 @@ export const pluginReactRouter = (
188
213
  }
189
214
  api.logger.warn(issue.message);
190
215
  }
191
- assetPrefix = normalizeAssetPrefix(config.output?.assetPrefix);
192
216
  return config;
193
217
  },
194
218
  });
@@ -198,6 +222,15 @@ export const pluginReactRouter = (
198
222
  warnOnClientSourceMaps(normalized, msg => api.logger.warn(msg), 'web');
199
223
  });
200
224
 
225
+ api.onBeforeCreateCompiler(() => {
226
+ const normalized = api.getNormalizedConfig();
227
+ assetPrefix = resolveEffectiveAssetPrefix({
228
+ dev: normalized.dev,
229
+ output: normalized.output,
230
+ isBuild: api.context.action === 'build',
231
+ });
232
+ });
233
+
201
234
  const configPath = findEntryFile(resolve('react-router.config'));
202
235
  const configExists = existsSync(configPath);
203
236
  let configWatchPaths: string | string[] = configExists
@@ -230,9 +263,12 @@ export const pluginReactRouter = (
230
263
 
231
264
  const {
232
265
  resolved: resolvedConfig,
266
+ userAndPresetConfig,
233
267
  presets: configPresets,
234
268
  hasConfiguredServerModuleFormat,
235
- } = await resolveReactRouterConfig(reactRouterUserConfig);
269
+ } = await effectRuntime.runPromise(
270
+ resolveReactRouterConfigEffect(reactRouterUserConfig)
271
+ );
236
272
 
237
273
  const {
238
274
  appDirectory,
@@ -244,12 +280,17 @@ export const pluginReactRouter = (
244
280
  ssr,
245
281
  prerender: prerenderConfig,
246
282
  serverBuildFile,
283
+ serverBundles,
247
284
  serverModuleFormat,
248
285
  splitRouteModules,
286
+ subResourceIntegrity,
249
287
  buildEnd,
250
288
  } = resolvedConfig;
251
289
 
252
- registerReactRouterTypegen(api, { appDirectory });
290
+ await registerReactRouterTypegen(api, {
291
+ runtime: effectRuntime,
292
+ appDirectory,
293
+ });
253
294
 
254
295
  const hasExplicitServerOutput = Object.prototype.hasOwnProperty.call(
255
296
  options,
@@ -290,57 +331,57 @@ export const pluginReactRouter = (
290
331
  throw new Error(prerenderConfigError);
291
332
  }
292
333
 
293
- // React Router defaults to "lazy" route discovery, but "ssr:false" builds
294
- // have no runtime server to serve manifest patch requests, so we force
295
- // `mode:"initial"` in SPA mode to avoid any `/__manifest` fetches.
296
- let routeDiscovery: Config['routeDiscovery'];
297
- if (!userRouteDiscovery) {
298
- routeDiscovery = ssr
299
- ? ({ mode: 'lazy', manifestPath: '/__manifest' } as const)
300
- : ({ mode: 'initial' } as const);
301
- } else if (userRouteDiscovery.mode === 'initial') {
302
- routeDiscovery = userRouteDiscovery;
303
- } else if (userRouteDiscovery.mode === 'lazy') {
304
- if (!ssr) {
305
- throw new Error(
306
- 'The `routeDiscovery.mode` config cannot be set to "lazy" when setting `ssr:false`'
307
- );
308
- }
309
- const manifestPath = userRouteDiscovery.manifestPath;
310
- if (manifestPath && !manifestPath.startsWith('/')) {
311
- throw new Error(
312
- 'The `routeDiscovery.manifestPath` config must be a root-relative pathname beginning with a slash (i.e., "/__manifest")'
313
- );
314
- }
315
- routeDiscovery = userRouteDiscovery;
316
- }
334
+ const routeDiscovery = resolveRouteDiscoveryConfig({
335
+ ssr,
336
+ userRouteDiscovery,
337
+ });
317
338
 
318
339
  (globalThis as any).__reactRouterAppDirectory = resolve(appDirectory);
319
340
  const routesPath = findEntryFile(resolve(appDirectory, 'routes'));
320
341
  if (!existsSync(routesPath)) {
321
- throw new Error(
322
- `Route config file not found at "${relative(
323
- process.cwd(),
324
- routesPath
325
- )}".`
342
+ const missingRoutesPath = relative(
343
+ process.cwd(),
344
+ resolve(appDirectory, 'routes.ts')
326
345
  );
346
+ throw new Error(`Route config file not found at "${missingRoutesPath}".`);
327
347
  }
328
348
 
329
- const jiti = createJiti(process.cwd(), {
349
+ const routeConfigDefine =
350
+ typeof api.getRsbuildConfig === 'function'
351
+ ? api.getRsbuildConfig().source?.define
352
+ : undefined;
353
+ const jiti = createConfigImporter({
354
+ define: routeConfigDefine,
330
355
  moduleCache: false,
331
356
  });
332
357
  const importRouteConfig = async (
333
- importer: Pick<typeof jiti, 'import'>
358
+ importer: ConfigImporter
334
359
  ): Promise<RouteConfigEntry[]> => {
335
- const routeConfigExport = await importer.import<RouteConfigEntry[]>(
336
- routesPath,
337
- {
338
- default: true,
339
- }
340
- );
341
- const routeConfigValue = await routeConfigExport;
360
+ const routeConfigFile = relative(resolve(appDirectory), routesPath);
361
+ let routeConfigValue: RouteConfigEntry[];
362
+ try {
363
+ const routeConfigExport = await importer.import<RouteConfigEntry[]>(
364
+ routesPath,
365
+ {
366
+ default: true,
367
+ }
368
+ );
369
+ routeConfigValue = await routeConfigExport;
370
+ } catch (error) {
371
+ // Match upstream: import/evaluation failures (e.g. syntax errors) are
372
+ // reported as an invalid route config rather than a raw loader error.
373
+ throw new Error(
374
+ [
375
+ `Route config in "${routeConfigFile}" is invalid.`,
376
+ '',
377
+ error instanceof Error
378
+ ? (error.stack ?? error.message)
379
+ : String(error),
380
+ ].join('\n')
381
+ );
382
+ }
342
383
  const validation = validateRouteConfig({
343
- routeConfigFile: relative(process.cwd(), routesPath),
384
+ routeConfigFile,
344
385
  routeConfig: routeConfigValue,
345
386
  });
346
387
  if (!validation.valid) {
@@ -350,33 +391,41 @@ export const pluginReactRouter = (
350
391
  };
351
392
  const loadRouteConfig = () => importRouteConfig(jiti);
352
393
  const { value: routeConfig, watchPaths: routeConfigWatchPaths } =
353
- await importConfigWithWatchPaths(routesPath, importRouteConfig);
354
-
355
- const entryClientPath = findEntryFile(
356
- resolve(appDirectory, 'entry.client')
357
- );
358
- const entryServerPath = findEntryFile(
359
- resolve(appDirectory, 'entry.server')
360
- );
361
-
362
- const serverAppPath = findEntryFile(
363
- resolve(appDirectory, '../server/index')
364
- );
365
- const hasServerApp = existsSync(serverAppPath);
366
- const devServerBuildEntryName = hasServerApp
367
- ? 'static/js/react-router-server-build'
368
- : 'static/js/app';
394
+ await importConfigWithWatchPaths(routesPath, importRouteConfig, {
395
+ define: routeConfigDefine,
396
+ });
369
397
 
370
- const templateDir = resolve(__dirname, 'templates');
371
- const templateClientPath = resolve(templateDir, 'entry.client.js');
372
- const templateServerPath = resolve(templateDir, 'entry.server.js');
398
+ const {
399
+ devServerBuildEntryName,
400
+ finalEntryClientPath,
401
+ finalEntryRscClientPath,
402
+ finalEntryRscPath,
403
+ finalEntryRscSsrPath,
404
+ finalEntryServerPath,
405
+ hasServerApp,
406
+ serverAppPath,
407
+ } = resolveReactRouterEntryPaths({
408
+ appDirectory,
409
+ templatesDirectory: resolve(__dirname, 'templates'),
410
+ });
373
411
 
374
- const finalEntryClientPath = existsSync(entryClientPath)
375
- ? entryClientPath
376
- : templateClientPath;
377
- const finalEntryServerPath = existsSync(entryServerPath)
378
- ? entryServerPath
379
- : templateServerPath;
412
+ if (isRscMode) {
413
+ assertReactRouterRscSupport({
414
+ pluginName: PLUGIN_NAME,
415
+ resolvePackagePath: resolveAppPackagePath,
416
+ });
417
+ assertReactRouterRscConfigSupport({
418
+ pluginName: PLUGIN_NAME,
419
+ userConfig: resolvedConfig,
420
+ });
421
+ await setupReactRouterRscPlugin({
422
+ api,
423
+ entryRscPath: finalEntryRscPath,
424
+ entrySsrPath: finalEntryRscSsrPath,
425
+ pluginName: PLUGIN_NAME,
426
+ rsc: typeof pluginOptions.rsc === 'object' ? pluginOptions.rsc : {},
427
+ });
428
+ }
380
429
 
381
430
  const getRootRoutePath = () => findEntryFile(resolve(appDirectory, 'root'));
382
431
  const rootRoutePath = getRootRoutePath();
@@ -397,25 +446,45 @@ export const pluginReactRouter = (
397
446
  };
398
447
 
399
448
  const resolvedConfigWithRoutes: ResolvedReactRouterConfig = {
400
- ...resolvedConfig,
401
449
  appDirectory: resolve(appDirectory),
450
+ basename,
402
451
  buildDirectory: resolve(buildDirectory),
403
- routeDiscovery,
452
+ buildEnd,
453
+ future,
404
454
  prerender: prerenderConfig,
405
455
  routes,
406
- unstable_routeConfig: routeConfig,
456
+ routeDiscovery,
457
+ serverBuildFile,
458
+ serverBundles,
459
+ serverModuleFormat,
460
+ ssr,
461
+ splitRouteModules,
462
+ subResourceIntegrity,
407
463
  allowedActionOrigins: allowedActionOrigins ?? false,
464
+ unstable_routeConfig: routeConfig,
408
465
  };
409
466
 
410
467
  const { buildEnd: _buildEnd, ...resolvedConfigForPreset } =
411
468
  resolvedConfigWithRoutes;
412
469
  for (const preset of configPresets) {
413
470
  await preset.reactRouterConfigResolved?.({
414
- reactRouterConfig: resolvedConfigForPreset,
471
+ reactRouterConfig:
472
+ resolvedConfigForPreset as ReactRouterPresetResolvedConfig,
415
473
  });
416
474
  }
475
+ const buildEndReactRouterConfig: ResolvedReactRouterConfig = {
476
+ ...resolvedConfigWithRoutes,
477
+ future: userAndPresetConfig.future ?? {},
478
+ } as ResolvedReactRouterConfig;
417
479
 
418
480
  const isBuild = api.context.action === 'build';
481
+ if (!isBuild) {
482
+ api.onAfterEnvironmentCompile(({ environment, stats }) => {
483
+ if (environment.name === 'node' && stats && !stats.hasErrors()) {
484
+ registerDevServerSourceMaps(stats.compilation);
485
+ }
486
+ });
487
+ }
419
488
  const shouldDependOnWebCompiler = !shouldParallelizeEnvironmentBuilds({
420
489
  isBuild,
421
490
  });
@@ -423,57 +492,216 @@ export const pluginReactRouter = (
423
492
  prerenderConfig !== undefined && prerenderConfig !== false;
424
493
  const isSpaMode = !ssr && !isPrerenderEnabled;
425
494
  const routeCount = Object.keys(routes).length;
426
- const routeChunkConfig: RouteChunkConfig = {
427
- splitRouteModules,
428
- appDirectory,
429
- rootRouteFile,
430
- };
431
495
  const routeChunkCache: RouteChunkCache = new Map();
432
- const routeTransformExecutor = createRouteTransformExecutor({
433
- parallelRouteTransform:
434
- pluginOptions.parallelRouteTransform ??
435
- shouldParallelizeRouteTransforms(routeCount),
436
- routeChunkCache,
437
- splitRouteModules: Boolean(splitRouteModules),
438
- isBuild,
439
- });
440
- const routeChunkOptions = {
441
- splitRouteModules,
442
- rootRouteFile,
443
- isBuild,
444
- cache: routeChunkCache,
496
+ const useRouteModuleTransformLoader =
497
+ !isRscMode &&
498
+ shouldUseRouteModuleTransformLoader(pluginOptions.parallelRouteTransform);
499
+ const routeTransformRunner: RouteTransformRunner = task =>
500
+ executeRouteTransformTask(task, { routeChunkCache });
501
+ const transformedRouteModuleAnalyses = new Map<
502
+ string,
503
+ RouteModuleAnalysis
504
+ >();
505
+ const rememberRouteModuleAnalysis = (
506
+ resourcePath: string,
507
+ analysis: RouteModuleAnalysis
508
+ ) => {
509
+ transformedRouteModuleAnalyses.set(resolve(resourcePath), analysis);
445
510
  };
511
+ const routeModuleAnalysis = async (routeFilePath: string) =>
512
+ transformedRouteModuleAnalyses.get(resolve(routeFilePath));
446
513
  const outputClientPath = resolve(buildDirectory, 'client');
447
514
  const assetsBuildDirectory = relative(process.cwd(), outputClientPath);
448
515
  const watchDirectory = resolve(appDirectory);
449
- const routeRestartMarkerPath = getRouteRestartMarkerPath(outputClientPath);
516
+ const routeRestartMarkerPath = getRouteRestartMarkerPath(appDirectory);
450
517
  const routeWatchFiles = createReactRouterRouteWatchFiles({
451
518
  configWatchPaths,
452
519
  routeConfigWatchPaths,
453
520
  routeRestartMarkerPath,
454
521
  onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
455
522
  });
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,
466
- });
467
-
468
- type ReactRouterManifest = Awaited<
469
- ReturnType<typeof getReactRouterManifestForDev>
470
- >;
471
523
  let latestBrowserManifest: ReactRouterManifest | null = null;
472
524
  let latestBrowserManifestModuleExports: RouteManifestModuleExports = {};
473
525
  let latestServerManifest: ReactRouterManifest | null = null;
474
526
  const latestServerManifestsByBundleId: Record<string, ReactRouterManifest> =
475
527
  {};
476
528
 
529
+ const routeByFilePath = new Map(
530
+ Object.values(routes).map(route => [
531
+ resolve(appDirectory, route.file),
532
+ route,
533
+ ])
534
+ );
535
+ const allowedActionOriginsForBuild =
536
+ allowedActionOrigins === false ? undefined : allowedActionOrigins;
537
+
538
+ const devHmrRefreshRuntimePath =
539
+ isBuild || isRscMode
540
+ ? undefined
541
+ : resolveReactRefreshRuntimePath(api.context.rootPath);
542
+ const devHdrSignal = devHmrRefreshRuntimePath
543
+ ? createDevHdrRevisionSignal({
544
+ filePath: getDevHdrRevisionFilePath(api.context.rootPath),
545
+ onError: error =>
546
+ api.logger.debug(
547
+ `[${PLUGIN_NAME}] Failed to signal hot data revalidation: ${error.message}`
548
+ ),
549
+ })
550
+ : undefined;
551
+ let devHmrEnabled = false;
552
+ if (devHmrRefreshRuntimePath && devHdrSignal) {
553
+ api.modifyEnvironmentConfig(
554
+ async (environmentConfig, { name, mergeEnvironmentConfig }) => {
555
+ if (name !== 'web') return environmentConfig;
556
+ return mergeEnvironmentConfig(environmentConfig, {
557
+ tools: {
558
+ rspack: rspackConfig => {
559
+ devHmrEnabled = isRspackSwcReactRefreshEnabled(rspackConfig);
560
+ if (devHmrEnabled) devHdrSignal.ensure();
561
+ return rspackConfig;
562
+ },
563
+ },
564
+ });
565
+ }
566
+ );
567
+ }
568
+
569
+ const commonModeOptions = {
570
+ api,
571
+ allowedActionOriginsForBuild,
572
+ appDirectory,
573
+ basename,
574
+ customServer: pluginOptions.customServer,
575
+ isBuild,
576
+ isSpaMode,
577
+ prerenderConfig,
578
+ routeConfig,
579
+ routeDiscovery,
580
+ routes,
581
+ rootRouteFile,
582
+ splitRouteModules,
583
+ ssr,
584
+ };
585
+ const modePlan = await (isRscMode
586
+ ? createReactRouterModePlan({
587
+ ...commonModeOptions,
588
+ isRscMode: true,
589
+ buildDirectory,
590
+ finalEntryRscClientPath,
591
+ finalEntryRscPath,
592
+ outputClientPath,
593
+ pluginName: PLUGIN_NAME,
594
+ serverBuildFile,
595
+ })
596
+ : createReactRouterModePlan({
597
+ ...commonModeOptions,
598
+ isRscMode: false,
599
+ assetsBuildDirectory,
600
+ defaultEntryName: devServerBuildEntryName,
601
+ entryServerPath: finalEntryServerPath,
602
+ finalEntryClientPath,
603
+ future,
604
+ hasServerApp,
605
+ reactRouterConfig: resolvedConfigWithRoutes,
606
+ routeChunkCache,
607
+ serverAppPath,
608
+ shouldDependOnWebCompiler,
609
+ devHmr:
610
+ devHmrRefreshRuntimePath && devHdrSignal
611
+ ? {
612
+ isEnabled: () => devHmrEnabled,
613
+ runtimeModule: generateDevHmrRuntimeModule({
614
+ reactRefreshRuntimePath: devHmrRefreshRuntimePath,
615
+ hdrRevisionFilePath: devHdrSignal.filePath,
616
+ }),
617
+ onNodeRebuildCommitted: () => {
618
+ if (devHmrEnabled) devHdrSignal.bump();
619
+ },
620
+ }
621
+ : undefined,
622
+ }));
623
+
624
+ const { manifestChunkNames } = modePlan;
625
+
626
+ let sendRscDevUpdate: (() => void) | undefined;
627
+ let scheduledRscDevUpdate: ReturnType<typeof setTimeout> | undefined;
628
+ let hasPendingRscNodeUpdate = false;
629
+ let pendingRscNodeFiles = new Set<string>();
630
+ if (isRscMode && !isBuild) {
631
+ api.onBeforeStartDevServer(({ server }) => {
632
+ sendRscDevUpdate = () =>
633
+ server.sockWrite('custom', {
634
+ event: 'rsc:update',
635
+ data: { revalidate: true },
636
+ });
637
+ });
638
+ api.onCloseDevServer(() => {
639
+ if (scheduledRscDevUpdate) {
640
+ clearTimeout(scheduledRscDevUpdate);
641
+ scheduledRscDevUpdate = undefined;
642
+ }
643
+ hasPendingRscNodeUpdate = false;
644
+ pendingRscNodeFiles.clear();
645
+ sendRscDevUpdate = undefined;
646
+ });
647
+ api.onAfterEnvironmentCompile(({ environment, stats }) => {
648
+ if (
649
+ (environment.name !== 'node' && environment.name !== 'web') ||
650
+ stats?.hasErrors()
651
+ ) {
652
+ return;
653
+ }
654
+ if (environment.name === 'node') {
655
+ const compiler = stats?.compilation.compiler;
656
+ const changedFiles = new Set([
657
+ ...(compiler?.modifiedFiles ?? []),
658
+ ...(compiler?.removedFiles ?? []),
659
+ ]);
660
+ // Initial and lazy compilations do not represent source edits. Sending
661
+ // an RSC revalidation for them can race and abort the navigation that
662
+ // requested the lazy module.
663
+ if (changedFiles.size === 0) {
664
+ return;
665
+ }
666
+ hasPendingRscNodeUpdate = true;
667
+ pendingRscNodeFiles = changedFiles;
668
+ }
669
+ if (!hasPendingRscNodeUpdate) {
670
+ return;
671
+ }
672
+ if (scheduledRscDevUpdate) {
673
+ clearTimeout(scheduledRscDevUpdate);
674
+ }
675
+ scheduledRscDevUpdate = setTimeout(() => {
676
+ scheduledRscDevUpdate = undefined;
677
+ hasPendingRscNodeUpdate = false;
678
+ const clientHotUpdateHandlesChange =
679
+ pendingRscNodeFiles.size > 0 &&
680
+ [...pendingRscNodeFiles].every(isRscClientModule);
681
+ const routeHotUpdateHandlesChange = [...pendingRscNodeFiles].some(
682
+ filePath => routeByFilePath.has(resolve(filePath))
683
+ );
684
+ pendingRscNodeFiles.clear();
685
+ if (!clientHotUpdateHandlesChange && !routeHotUpdateHandlesChange) {
686
+ sendRscDevUpdate?.();
687
+ }
688
+ }, 1000);
689
+ });
690
+ }
691
+
692
+ const devBackgroundResources =
693
+ await registerReactRouterDevBackgroundResources({
694
+ api,
695
+ runtime: effectRuntime,
696
+ isBuild,
697
+ lazyCompilationPrewarm: pluginOptions.unstableLazyCompilationPrewarm,
698
+ routeRestartMarkerPath,
699
+ watchDirectory,
700
+ getRouteTopology: routeTopology.getRouteTopology,
701
+ initialRouteTopology: routeTopology.initialRouteTopology,
702
+ onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
703
+ });
704
+
477
705
  const stageLatestManifests = (
478
706
  manifest: ReactRouterManifest,
479
707
  sri: ReactRouterManifest['sri'],
@@ -497,8 +725,14 @@ export const pluginReactRouter = (
497
725
  [devServerBuildEntryName]: baseServerManifest,
498
726
  };
499
727
 
500
- for (const { bundleId, entryName } of serverBundleEntries) {
501
- const bundleRoutes = routesByServerBundleId[bundleId];
728
+ if (modePlan.kind !== 'classic') {
729
+ return;
730
+ }
731
+
732
+ for (const { bundleId, entryName } of modePlan.artifacts
733
+ .serverBundleEntries) {
734
+ const bundleRoutes =
735
+ modePlan.artifacts.routesByServerBundleId[bundleId];
502
736
  if (!bundleRoutes) {
503
737
  continue;
504
738
  }
@@ -518,94 +752,15 @@ export const pluginReactRouter = (
518
752
  }
519
753
 
520
754
  if (!isBuild) {
521
- devRuntime.captureWeb(compilation, manifestsByEntryName);
755
+ modePlan.artifacts.devRuntime.captureWeb(
756
+ compilation,
757
+ manifestsByEntryName
758
+ );
522
759
  }
523
760
  }
524
761
  );
525
762
  };
526
763
 
527
- const routeByFilePath = new Map(
528
- Object.values(routes).map(route => [
529
- resolve(appDirectory, route.file),
530
- route,
531
- ])
532
- );
533
-
534
- const manifestChunkNames = new Set<string>(['entry.client']);
535
- const webRouteEntries = Object.values(routes).reduce(
536
- (acc, route) => {
537
- const entryName = route.file.slice(0, route.file.lastIndexOf('.'));
538
- const routeFilePath = resolve(appDirectory, route.file);
539
- manifestChunkNames.add(entryName);
540
- acc[entryName] = {
541
- import: `${routeFilePath}${BUILD_CLIENT_ROUTE_QUERY_STRING}`,
542
- html: false,
543
- };
544
-
545
- if (isBuild && splitRouteModules && route.id !== 'root') {
546
- let source: string;
547
- try {
548
- source = readFileSync(routeFilePath, 'utf8');
549
- } catch (error) {
550
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
551
- return acc;
552
- }
553
- throw error;
554
- }
555
- for (const exportName of routeChunkExportNames) {
556
- if (!source.includes(exportName)) {
557
- continue;
558
- }
559
- const chunkEntryName = getRouteChunkEntryName(route.id, exportName);
560
- manifestChunkNames.add(chunkEntryName);
561
- acc[chunkEntryName] = {
562
- import: getRouteChunkModuleId(routeFilePath, exportName),
563
- html: false,
564
- };
565
- }
566
- }
567
-
568
- return acc;
569
- },
570
- {} as Record<string, RsbuildEntryDescription>
571
- );
572
- const buildManifest = await getBuildManifest({
573
- reactRouterConfig: resolvedConfigWithRoutes,
574
- routes,
575
- rootDirectory: process.cwd(),
576
- });
577
- const routesByServerBundleId = getRoutesByServerBundleId(
578
- buildManifest,
579
- routes
580
- );
581
- const serverBuildPlan = createReactRouterServerBuildPlan({
582
- routesByServerBundleId,
583
- serverBuildFile,
584
- defaultEntryName: devServerBuildEntryName,
585
- });
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
-
602
- const devRuntime = createReactRouterDevRuntimeController({
603
- api,
604
- isBuild,
605
- buildPlan: serverBuildPlan,
606
- onNodeRebuildCommitted: () => devHdrSignal?.bump(),
607
- });
608
-
609
764
  let clientStats: ReactRouterManifestStats | undefined;
610
765
  api.onAfterEnvironmentCompile(({ stats, environment }) => {
611
766
  if (environment.name === 'web') {
@@ -629,121 +784,82 @@ export const pluginReactRouter = (
629
784
  }
630
785
  });
631
786
 
632
- const prerenderPaths = await resolvePrerenderPaths(
633
- prerenderConfig,
634
- ssr,
635
- routeConfig,
636
- {
637
- logWarning: true,
638
- warn: message => api.logger.warn(message),
639
- }
640
- );
641
-
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
- );
671
-
672
- const allowedActionOriginsForBuild =
673
- allowedActionOrigins === false ? undefined : allowedActionOrigins;
674
-
675
- // Public requests stay bare while Rspack resolves seeded virtual files.
676
- const createVirtualModulePlugin = (publicPath: string) => {
677
- const bundleVirtualModules = Object.fromEntries(
678
- Object.entries(routesByServerBundleId).map(
679
- ([bundleId, bundleRoutes]) => [
680
- `virtual/react-router/server-build-${bundleId}`,
681
- generateServerBuild(bundleRoutes, {
682
- entryServerPath: finalEntryServerPath,
683
- assetsBuildDirectory,
684
- basename,
685
- appDirectory,
787
+ if (modePlan.kind === 'classic') {
788
+ api.onAfterBuild(({ environments }) =>
789
+ effectRuntime.runPromise(
790
+ tryPluginPromise(() =>
791
+ runReactRouterPrerenderBuild({
792
+ api,
793
+ hasWebEnvironment: Boolean(environments.web),
794
+ buildDirectory,
795
+ serverBuildFile,
686
796
  ssr,
687
- federation: options.federation,
797
+ isPrerenderEnabled,
798
+ prerenderConfig,
799
+ prerenderPaths: modePlan.artifacts.prerenderPaths,
800
+ basename,
688
801
  future,
689
- allowedActionOrigins: allowedActionOriginsForBuild,
690
- prerender: prerenderPaths,
691
- routeDiscovery,
692
- publicPath,
693
- serverManifestId: `virtual/react-router/server-manifest-${bundleId}`,
694
- }),
695
- ]
802
+ routes,
803
+ latestBrowserManifest,
804
+ latestBrowserManifestModuleExports,
805
+ clientStats,
806
+ pluginOptions,
807
+ appDirectory,
808
+ assetPrefix,
809
+ routeChunkOptions: modePlan.routeChunkOptions,
810
+ routeModuleAnalysis,
811
+ buildManifest: modePlan.artifacts.buildManifest,
812
+ buildEndReactRouterConfig,
813
+ buildEnd,
814
+ })
815
+ )
696
816
  )
697
817
  );
698
- const bundleManifestModules = Object.fromEntries(
699
- Object.entries(routesByServerBundleId)
700
- .filter(
701
- ([, bundleRoutes]) =>
702
- bundleRoutes && Object.keys(bundleRoutes).length > 0
818
+ } else {
819
+ api.onAfterBuild(({ environments }) =>
820
+ effectRuntime.runPromise(
821
+ tryPluginPromise(() =>
822
+ runReactRouterRscPrerenderBuild({
823
+ api,
824
+ hasWebEnvironment: Boolean(environments.web),
825
+ buildDirectory,
826
+ serverBuildFile,
827
+ ssr,
828
+ prerenderConfig,
829
+ prerenderPaths: modePlan.prerenderPaths,
830
+ basename,
831
+ })
703
832
  )
704
- .map(([bundleId]) => [
705
- `virtual/react-router/server-manifest-${bundleId}`,
706
- 'export default {};',
707
- ])
833
+ )
708
834
  );
835
+ }
709
836
 
837
+ // Public requests stay bare while Rspack resolves seeded virtual files.
838
+ const createVirtualModulePlugin = (
839
+ publicPath: string,
840
+ jsDistPath: string
841
+ ) => {
710
842
  return new rspack.experiments.VirtualModulesPlugin(
711
- mapVirtualModules({
712
- 'virtual/react-router/browser-manifest': 'export default {};',
713
- 'virtual/react-router/server-manifest': 'export default {};',
714
- 'virtual/react-router/server-build': generateServerBuild(routes, {
715
- entryServerPath: finalEntryServerPath,
716
- assetsBuildDirectory,
717
- basename,
718
- appDirectory,
719
- ssr,
720
- federation: options.federation,
721
- future,
722
- allowedActionOrigins: allowedActionOriginsForBuild,
723
- prerender: prerenderPaths,
724
- routeDiscovery,
725
- publicPath,
726
- }),
727
- ...bundleVirtualModules,
728
- ...bundleManifestModules,
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
- : {}),
740
- })
843
+ mapVirtualModules(modePlan.createVirtualModules(publicPath, jsDistPath))
741
844
  );
742
845
  };
743
846
 
744
847
  api.modifyRsbuildConfig(async (config, { mergeRsbuildConfig }) => {
745
- assetPrefix = normalizeAssetPrefix(config.output?.assetPrefix);
746
- const vmodPlugin = createVirtualModulePlugin(assetPrefix);
848
+ // The RSC bootstrap script URL must reflect the user's web js distPath;
849
+ // the entry filename itself is deterministic because the plugin forces
850
+ // web `output.filename.js` to '[name].js' below.
851
+ const webDistPath = config.environments?.web?.output?.distPath;
852
+ const rootDistPath = config.output?.distPath;
853
+ const jsDistPath =
854
+ (typeof webDistPath === 'object' ? webDistPath.js : undefined) ??
855
+ (typeof rootDistPath === 'object' ? rootDistPath.js : undefined) ??
856
+ DEFAULT_JS_DIST_PATH;
857
+ const assetPrefix = resolveEffectiveAssetPrefix({
858
+ dev: config.dev,
859
+ output: config.output,
860
+ isBuild,
861
+ });
862
+ const vmodPlugin = createVirtualModulePlugin(assetPrefix, jsDistPath);
747
863
  const useAsyncNodeChunkLoading =
748
864
  options.federation && resolvedServerOutput === 'commonjs';
749
865
  let nodeChunkLoading: 'import' | 'async-node' | 'require' = 'require';
@@ -752,15 +868,6 @@ export const pluginReactRouter = (
752
868
  } else if (useAsyncNodeChunkLoading) {
753
869
  nodeChunkLoading = 'async-node';
754
870
  }
755
- const nodeEntries = createReactRouterNodeEntries({
756
- hasServerApp,
757
- isBuild,
758
- serverAppPath,
759
- entryServerPath: finalEntryServerPath,
760
- defaultEntryName: devServerBuildEntryName,
761
- serverBundleEntries,
762
- });
763
-
764
871
  const configuredLazyCompilation = Object.prototype.hasOwnProperty.call(
765
872
  options,
766
873
  'lazyCompilation'
@@ -769,7 +876,9 @@ export const pluginReactRouter = (
769
876
  : (config.dev?.lazyCompilation ?? pluginOptions.lazyCompilation);
770
877
  const guardedLazyCompilation = guardReactRouterLazyCompilation({
771
878
  lazyCompilation: configuredLazyCompilation,
772
- entryClientPath: finalEntryClientPath,
879
+ entryClientPath: isRscMode
880
+ ? finalEntryRscClientPath
881
+ : finalEntryClientPath,
773
882
  prewarmReactRouterModules: Boolean(
774
883
  pluginOptions.unstableLazyCompilationPrewarm
775
884
  ),
@@ -783,6 +892,27 @@ export const pluginReactRouter = (
783
892
  routeCount >= 256 &&
784
893
  (config.performance?.printFileSize === undefined ||
785
894
  config.performance.printFileSize === true);
895
+ const resolveConfig = modePlan.createResolveConfig(api.context.rootPath);
896
+
897
+ // Browser code (React itself) reads `process.env.NODE_ENV`. Rsbuild only
898
+ // emits the define for its recognized modes; an unrecognized NODE_ENV
899
+ // (e.g. the string "undefined" leaking from a misconfigured shell)
900
+ // resolves mode 'none' and leaves the bare reference in the web bundle,
901
+ // which throws `process is not defined` at runtime. Always define it for
902
+ // the web environment — mirroring the Vite plugin — unless the user
903
+ // supplies their own define.
904
+ const userDefinesNodeEnv =
905
+ config.source?.define?.['process.env.NODE_ENV'] !== undefined ||
906
+ config.environments?.web?.source?.define?.['process.env.NODE_ENV'] !==
907
+ undefined;
908
+ const webNodeEnv =
909
+ process.env.NODE_ENV === 'production' ||
910
+ process.env.NODE_ENV === 'development' ||
911
+ process.env.NODE_ENV === 'test'
912
+ ? process.env.NODE_ENV
913
+ : isBuild
914
+ ? 'production'
915
+ : 'development';
786
916
 
787
917
  return mergeRsbuildConfig(config, {
788
918
  ...(shouldCompactFileSizeReport
@@ -799,29 +929,20 @@ export const pluginReactRouter = (
799
929
  output: {
800
930
  assetPrefix: config.output?.assetPrefix || '/',
801
931
  },
932
+ server: modePlan.server,
802
933
  dev: {
803
934
  ...lazyCompilation,
804
935
  watchFiles: mergeWatchFiles(config.dev?.watchFiles, routeWatchFiles),
805
936
  },
806
- // React Router's request handler natively supports `ssr:false`
807
- // builds (it renders the SPA shell for document requests), so the
808
- // middleware is registered for SPA mode too — without it, dev
809
- // requests would 404 because no HTML entry exists.
810
- ...(pluginOptions.customServer
811
- ? {}
812
- : {
813
- server: {
814
- setup: [
815
- createReactRouterDevServerSetup({
816
- // Lazy: the dev runtime binding does not exist yet here.
817
- loadBuild: () => devRuntime.createBuildLoader()(),
818
- }),
819
- ],
820
- },
821
- }),
822
937
  tools: {
823
938
  rspack: {
824
- plugins: [vmodPlugin],
939
+ resolve: resolveConfig,
940
+ plugins: [
941
+ vmodPlugin,
942
+ createQuerylessRouteImportPlugin(routeByFilePath, {
943
+ rsc: isRscMode,
944
+ }),
945
+ ],
825
946
  },
826
947
  },
827
948
  environments: {
@@ -836,15 +957,14 @@ export const pluginReactRouter = (
836
957
  }
837
958
  : {}),
838
959
  source: {
839
- entry: {
840
- // no query needed when federation is disabled
841
- 'entry.client': finalEntryClientPath,
842
- 'virtual/react-router/browser-manifest': {
843
- import: 'virtual/react-router/browser-manifest',
844
- html: false,
845
- },
846
- ...webRouteEntries,
847
- },
960
+ entry: modePlan.webEntries,
961
+ ...(userDefinesNodeEnv
962
+ ? {}
963
+ : {
964
+ define: {
965
+ 'process.env.NODE_ENV': JSON.stringify(webNodeEnv),
966
+ },
967
+ }),
848
968
  },
849
969
  output: {
850
970
  filename: {
@@ -856,6 +976,7 @@ export const pluginReactRouter = (
856
976
  },
857
977
  tools: {
858
978
  rspack: {
979
+ resolve: resolveConfig,
859
980
  name: 'web',
860
981
  module: {
861
982
  rules: [
@@ -866,26 +987,17 @@ export const pluginReactRouter = (
866
987
  },
867
988
  ],
868
989
  },
869
- ...(options.federation
870
- ? {
871
- output: {
872
- chunkLoading: 'import',
873
- },
874
- }
875
- : {}),
876
- externalsType: 'module',
990
+ externalsType: modePlan.webExternalsType,
877
991
  output: {
878
- chunkFormat: 'module',
879
- chunkLoading: 'import',
880
- workerChunkLoading: 'import',
881
- wasmLoading: 'fetch',
882
- library: { type: 'module' },
883
- module: true,
884
- },
885
- optimization: {
886
- avoidEntryIife: true,
887
- runtimeChunk: 'single',
992
+ ...modePlan.webOutput,
993
+ publicPath: assetPrefix,
994
+ ...(options.federation
995
+ ? {
996
+ chunkLoading: 'import',
997
+ }
998
+ : {}),
888
999
  },
1000
+ optimization: modePlan.webOptimization,
889
1001
  },
890
1002
  },
891
1003
  },
@@ -894,7 +1006,7 @@ export const pluginReactRouter = (
894
1006
  // root route into a hydratable `index.html` at build time.
895
1007
  node: {
896
1008
  source: {
897
- entry: nodeEntries,
1009
+ entry: modePlan.nodeEntries,
898
1010
  },
899
1011
  output: {
900
1012
  distPath: {
@@ -903,6 +1015,15 @@ export const pluginReactRouter = (
903
1015
  target: config.environments?.node?.output?.target || 'node',
904
1016
  filename: {
905
1017
  js: '[name].js',
1018
+ css: (pathData: Rspack.PathData) => {
1019
+ const sourceName = pathData.chunk?.name ?? '[name]';
1020
+ if (!isBuild) {
1021
+ return `${sourceName}.css`;
1022
+ }
1023
+ const baseName =
1024
+ sourceName.split(/[\\/]/).pop() || sourceName;
1025
+ return `../assets/${baseName}.[contenthash:10].css`;
1026
+ },
906
1027
  },
907
1028
  },
908
1029
  tools: {
@@ -917,15 +1038,19 @@ export const pluginReactRouter = (
917
1038
  },
918
1039
  ],
919
1040
  },
920
- externals: nodeExternals,
921
- ...(shouldDependOnWebCompiler ? { dependencies: ['web'] } : {}),
1041
+ externals: modePlan.nodeExternals,
1042
+ ...modePlan.nodeDependencies,
922
1043
  externalsType: resolvedServerOutput,
923
1044
  output: {
924
1045
  chunkFormat: resolvedServerOutput,
925
1046
  chunkLoading: nodeChunkLoading,
1047
+ devtoolModuleFilenameTemplate: '[absolute-resource-path]',
1048
+ devtoolFallbackModuleFilenameTemplate:
1049
+ '[absolute-resource-path]?[hash]',
926
1050
  workerChunkLoading: nodeChunkLoading,
927
1051
  wasmLoading: 'fetch',
928
1052
  module: resolvedServerOutput === 'module',
1053
+ chunkFilename: 'static/js/async/[name].js',
929
1054
  },
930
1055
  },
931
1056
  },
@@ -934,104 +1059,126 @@ export const pluginReactRouter = (
934
1059
  });
935
1060
  });
936
1061
 
937
- api.modifyEnvironmentConfig(
938
- async (config, { name, mergeEnvironmentConfig }) => {
939
- if (name !== 'web' && name !== 'node') {
940
- return config;
941
- }
1062
+ registerReactRouterEnvironmentOutput({
1063
+ api,
1064
+ federation: pluginOptions.federation,
1065
+ resolvedServerOutput,
1066
+ });
942
1067
 
943
- return mergeEnvironmentConfig(config, {
944
- tools: {
945
- rspack: rspackConfig => {
946
- if (pluginOptions.federation) {
947
- ensureFederationAsyncStartup(rspackConfig);
948
- }
1068
+ if (modePlan.kind === 'classic' && useRouteModuleTransformLoader) {
1069
+ api.modifyEnvironmentConfig(
1070
+ async (config, { name, mergeEnvironmentConfig }) => {
1071
+ if (name !== 'web' && name !== 'node') {
1072
+ return config;
1073
+ }
949
1074
 
950
- if (name === 'web') {
951
- devHmrEnabled =
1075
+ return mergeEnvironmentConfig(config, {
1076
+ tools: {
1077
+ rspack: rspackConfig => {
1078
+ const environmentDevHmrEnabled =
1079
+ name === 'web' &&
952
1080
  !isBuild &&
953
1081
  devHmrRefreshRuntimePath !== undefined &&
954
1082
  config.mode === 'development' &&
955
1083
  config.dev?.hmr !== false &&
956
1084
  isRspackSwcReactRefreshEnabled(rspackConfig);
957
- if (devHmrEnabled) {
958
- devHdrSignal?.ensure();
959
- }
960
- }
961
1085
 
962
- if (name === 'node') {
963
- const output = rspackConfig.output;
964
- if (output) {
965
- const library = output.library;
966
- const libraryOptions =
967
- library &&
968
- typeof library === 'object' &&
969
- !Array.isArray(library)
970
- ? library
971
- : {};
972
- rspackConfig.output = {
973
- ...output,
974
- library: {
975
- ...libraryOptions,
976
- type:
977
- resolvedServerOutput === 'module'
978
- ? 'module'
979
- : 'commonjs2',
980
- },
981
- };
982
- }
983
- }
984
-
985
- return rspackConfig;
1086
+ registerRouteModuleTransformRules(rspackConfig, {
1087
+ environmentName: name,
1088
+ ssr,
1089
+ isBuild,
1090
+ isSpaMode,
1091
+ rootRoutePath,
1092
+ devHmr: environmentDevHmrEnabled,
1093
+ logPerformance,
1094
+ routeByFilePath,
1095
+ parallelRouteTransform: pluginOptions.parallelRouteTransform,
1096
+ });
1097
+ return rspackConfig;
1098
+ },
986
1099
  },
987
- },
988
- });
989
- }
990
- );
1100
+ });
1101
+ }
1102
+ );
1103
+ }
991
1104
 
992
- registerModifyBrowserManifestAssets(
993
- api,
994
- routes,
995
- pluginOptions,
996
- appDirectory,
997
- () => assetPrefix,
998
- routeChunkOptions,
999
- {
1000
- subResourceIntegrity: resolvedConfigWithRoutes.subResourceIntegrity,
1001
- future,
1002
- manifestChunkNames,
1003
- onManifest: (manifest, sri, moduleExportsByRouteId, context) =>
1004
- stageLatestManifests(
1005
- manifest,
1006
- sri,
1007
- moduleExportsByRouteId,
1008
- context.compilation
1009
- ),
1010
- }
1011
- );
1105
+ if (modePlan.kind === 'rsc') {
1106
+ registerReactRouterRscRouteTransforms({
1107
+ api,
1108
+ isBuild,
1109
+ performanceProfiler,
1110
+ routeByFilePath,
1111
+ routeChunkCache,
1112
+ routeChunkConfig: modePlan.routeChunkConfig,
1113
+ });
1012
1114
 
1013
- registerBuildOutputTransforms({
1014
- api,
1015
- resolvedServerOutput,
1016
- performanceProfiler,
1017
- getLatestServerManifest: () => latestServerManifest,
1018
- getLatestServerManifestByBundleId: bundleId =>
1019
- latestServerManifestsByBundleId[bundleId],
1020
- routes,
1021
- pluginOptions,
1022
- getClientStats: () => clientStats,
1023
- appDirectory,
1024
- getAssetPrefix: () => assetPrefix,
1025
- routeChunkOptions,
1026
- routeTransformExecutor,
1027
- routeByFilePath,
1028
- routeChunkConfig,
1029
- isBuild,
1030
- splitRouteModules: Boolean(splitRouteModules),
1031
- ssr,
1032
- isSpaMode,
1033
- rootRoutePath,
1034
- isDevHmrEnabled: () => devHmrEnabled,
1035
- });
1115
+ // RSC mode has no `registerBuildOutputTransforms` pass, so relocate the
1116
+ // node-emitted `?url`/`.css?url` static assets into the client build here.
1117
+ // Without this the href baked into `links()` (resolved in the node env)
1118
+ // 404s in the browser because the file only exists under `build/server`.
1119
+ registerSsrAssetRelocation({
1120
+ api,
1121
+ outputClientPath,
1122
+ performanceProfiler,
1123
+ });
1124
+ } else {
1125
+ registerModifyBrowserManifestAssets(
1126
+ api,
1127
+ routes,
1128
+ pluginOptions,
1129
+ appDirectory,
1130
+ () => assetPrefix,
1131
+ modePlan.routeChunkOptions,
1132
+ {
1133
+ subResourceIntegrity: resolvedConfigWithRoutes.subResourceIntegrity,
1134
+ future,
1135
+ manifestChunkNames,
1136
+ routeModuleAnalysis,
1137
+ onManifest: (manifest, sri, moduleExportsByRouteId, context) =>
1138
+ stageLatestManifests(
1139
+ manifest,
1140
+ sri,
1141
+ moduleExportsByRouteId,
1142
+ context.compilation
1143
+ ),
1144
+ }
1145
+ );
1146
+
1147
+ registerBuildOutputTransforms({
1148
+ api,
1149
+ resolvedServerOutput,
1150
+ performanceProfiler,
1151
+ getLatestServerManifest: () => latestServerManifest,
1152
+ getLatestServerManifestByBundleId: bundleId =>
1153
+ latestServerManifestsByBundleId[bundleId],
1154
+ routes,
1155
+ pluginOptions,
1156
+ getClientStats: () => clientStats,
1157
+ appDirectory,
1158
+ getAssetPrefix: () => assetPrefix,
1159
+ routeChunkOptions: modePlan.routeChunkOptions,
1160
+ routeModuleAnalysis,
1161
+ routeTransformRunner,
1162
+ routeByFilePath,
1163
+ routeChunkConfig: modePlan.routeChunkConfig,
1164
+ isBuild,
1165
+ splitRouteModules: Boolean(modePlan.routeChunkConfig.splitRouteModules),
1166
+ useRouteModuleTransformApi: !useRouteModuleTransformLoader,
1167
+ ssr,
1168
+ isSpaMode,
1169
+ rootRoutePath,
1170
+ outputClientPath,
1171
+ isDevHmrEnabled: () => devHmrEnabled,
1172
+ onRouteModuleAnalysis: rememberRouteModuleAnalysis,
1173
+ });
1174
+ }
1036
1175
  },
1037
1176
  });
1177
+
1178
+ export const pluginReactRouterRSC = (
1179
+ options: ReactRouterRSCPluginOptions = {}
1180
+ ): RsbuildPlugin =>
1181
+ pluginReactRouter({
1182
+ ...options,
1183
+ rsc: options.rsc ?? true,
1184
+ });