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