rsbuild-plugin-react-router 0.2.0 → 0.3.1

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 (105) hide show
  1. package/README.md +213 -201
  2. package/dist/451.js +1103 -0
  3. package/dist/bounded-cache.d.ts +1 -0
  4. package/dist/build-manifest.d.ts +7 -4
  5. package/dist/build-output-transforms.d.ts +30 -0
  6. package/dist/concurrency.d.ts +3 -0
  7. package/dist/config-imports.d.ts +10 -0
  8. package/dist/constants.d.ts +3 -0
  9. package/dist/dev-background-resources.d.ts +38 -0
  10. package/dist/dev-generation.d.ts +25 -0
  11. package/dist/dev-runtime-artifacts.d.ts +47 -0
  12. package/dist/dev-runtime-compilation.d.ts +47 -0
  13. package/dist/dev-runtime-controller.d.ts +14 -0
  14. package/dist/dev-runtime-session.d.ts +34 -0
  15. package/dist/dev-server.d.ts +16 -2
  16. package/dist/effect-runtime.d.ts +18 -0
  17. package/dist/export-utils.d.ts +15 -7
  18. package/dist/index.cjs +13775 -1412
  19. package/dist/index.d.ts +8 -1
  20. package/dist/index.js +9429 -1501
  21. package/dist/lazy-compilation-prewarm.d.ts +25 -0
  22. package/dist/lazy-compilation.d.ts +6 -0
  23. package/dist/manifest.d.ts +48 -10
  24. package/dist/modify-browser-manifest.d.ts +30 -13
  25. package/dist/parallel-route-transform-protocol.d.ts +25 -0
  26. package/dist/parallel-route-transform-worker.d.ts +1 -0
  27. package/dist/parallel-route-transform-worker.js +38 -0
  28. package/dist/parallel-route-transforms.d.ts +29 -0
  29. package/dist/performance.d.ts +26 -0
  30. package/dist/plugin-utils.d.ts +2 -12
  31. package/dist/prerender-build.d.ts +36 -0
  32. package/dist/prerender.d.ts +27 -2
  33. package/dist/react-router-config.d.ts +15 -5
  34. package/dist/route-artifacts.d.ts +33 -0
  35. package/dist/route-ast.d.ts +21 -0
  36. package/dist/route-chunks.d.ts +9 -1
  37. package/dist/route-component-transform.d.ts +5 -0
  38. package/dist/route-export-pruning.d.ts +6 -0
  39. package/dist/route-export-resolution.d.ts +5 -0
  40. package/dist/route-transform-tasks.d.ts +47 -0
  41. package/dist/route-watch.d.ts +27 -0
  42. package/dist/server-build-plan.d.ts +22 -0
  43. package/dist/server-build-resolution.d.ts +3 -0
  44. package/dist/server-utils.d.ts +3 -2
  45. package/dist/ssr-externals.d.ts +1 -0
  46. package/dist/templates/entry.server.cjs +3 -3
  47. package/dist/templates/entry.server.js +3 -3
  48. package/dist/typegen.d.ts +15 -0
  49. package/dist/types.d.ts +41 -14
  50. package/dist/virtual-modules.d.ts +2 -0
  51. package/dist/warnings/warn-on-client-source-maps.d.ts +1 -0
  52. package/dist/yuku.d.ts +15 -0
  53. package/package.json +24 -19
  54. package/src/bounded-cache.ts +18 -0
  55. package/src/build-manifest.ts +205 -0
  56. package/src/build-output-transforms.ts +273 -0
  57. package/src/concurrency.ts +15 -0
  58. package/src/config-imports.ts +83 -0
  59. package/src/constants.ts +91 -0
  60. package/src/dev-background-resources.ts +255 -0
  61. package/src/dev-generation.ts +690 -0
  62. package/src/dev-runtime-artifacts.ts +239 -0
  63. package/src/dev-runtime-compilation.ts +171 -0
  64. package/src/dev-runtime-controller.ts +542 -0
  65. package/src/dev-runtime-session.ts +191 -0
  66. package/src/dev-server.ts +88 -0
  67. package/src/effect-runtime.ts +130 -0
  68. package/src/export-utils.ts +278 -0
  69. package/src/index.ts +990 -0
  70. package/src/lazy-compilation-prewarm.ts +279 -0
  71. package/src/lazy-compilation.ts +101 -0
  72. package/src/manifest.ts +591 -0
  73. package/src/modify-browser-manifest.ts +246 -0
  74. package/src/parallel-route-transform-protocol.ts +35 -0
  75. package/src/parallel-route-transform-worker.ts +82 -0
  76. package/src/parallel-route-transforms.ts +458 -0
  77. package/src/performance.ts +254 -0
  78. package/src/plugin-utils.ts +82 -0
  79. package/src/prerender-build.ts +687 -0
  80. package/src/prerender.ts +349 -0
  81. package/src/react-router-config.ts +245 -0
  82. package/src/route-artifacts.ts +155 -0
  83. package/src/route-ast.ts +163 -0
  84. package/src/route-chunks.ts +857 -0
  85. package/src/route-component-transform.ts +314 -0
  86. package/src/route-config.ts +106 -0
  87. package/src/route-export-pruning.ts +668 -0
  88. package/src/route-export-resolution.ts +329 -0
  89. package/src/route-transform-tasks.ts +249 -0
  90. package/src/route-watch.ts +357 -0
  91. package/src/server-build-plan.ts +91 -0
  92. package/src/server-build-resolution.ts +131 -0
  93. package/src/server-utils.ts +111 -0
  94. package/src/ssr-externals.ts +59 -0
  95. package/src/templates/context.ts +12 -0
  96. package/src/templates/entry.client.tsx +12 -0
  97. package/src/templates/entry.server.tsx +76 -0
  98. package/src/typegen.ts +178 -0
  99. package/src/types.ts +92 -0
  100. package/src/validation/validate-plugin-order.ts +76 -0
  101. package/src/virtual-modules.ts +30 -0
  102. package/src/warnings/warn-on-client-source-maps.ts +96 -0
  103. package/src/yuku.ts +67 -0
  104. package/dist/0~rslib-runtime.js +0 -16
  105. package/dist/babel.d.ts +0 -8
package/src/index.ts ADDED
@@ -0,0 +1,990 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import fsExtra from 'fs-extra';
3
+ import type { Config } from './react-router-config.js';
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';
12
+ import { relative, resolve } from 'pathe';
13
+
14
+ import { getDefaultConcurrency } from './concurrency.js';
15
+ import {
16
+ BUILD_CLIENT_ROUTE_QUERY_STRING,
17
+ JS_EXTENSIONS,
18
+ PLUGIN_NAME,
19
+ } from './constants.js';
20
+ import { guardReactRouterLazyCompilation } from './lazy-compilation.js';
21
+ import { createReactRouterDevServerSetup } from './dev-server.js';
22
+ import {
23
+ generateWithProps,
24
+ findEntryFile,
25
+ normalizeAssetPrefix,
26
+ } 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';
33
+ import { runReactRouterPrerenderBuild } from './prerender-build.js';
34
+ import {
35
+ resolveReactRouterConfig,
36
+ type ResolvedReactRouterConfig,
37
+ } from './react-router-config.js';
38
+ import {
39
+ getReactRouterManifestForDev,
40
+ configRoutesToRouteManifest,
41
+ createReactRouterManifestStats,
42
+ type ReactRouterManifestStats,
43
+ type RouteManifestModuleExports,
44
+ } from './manifest.js';
45
+ import { registerModifyBrowserManifestAssets } from './modify-browser-manifest.js';
46
+ import { registerBuildOutputTransforms } from './build-output-transforms.js';
47
+ import {
48
+ getRouteChunkEntryName,
49
+ getRouteChunkModuleId,
50
+ routeChunkExportNames,
51
+ type RouteChunkCache,
52
+ type RouteChunkConfig,
53
+ } from './route-chunks.js';
54
+ import {
55
+ createRouteTransformExecutor,
56
+ shouldParallelizeRouteTransforms,
57
+ } from './parallel-route-transforms.js';
58
+ import { getRouteRestartMarkerPath, mergeWatchFiles } from './route-watch.js';
59
+ 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
+ import { warnOnClientSourceMaps } from './warnings/warn-on-client-source-maps.js';
69
+ import { validatePluginOrderFromConfig } from './validation/validate-plugin-order.js';
70
+ import { getSsrExternals } from './ssr-externals.js';
71
+ import {
72
+ createReactRouterPerformanceProfiler,
73
+ roundMs,
74
+ } from './performance.js';
75
+ 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
+
86
+ export { loadReactRouterServerBuild } from './dev-generation.js';
87
+ export { resolveReactRouterServerBuild };
88
+
89
+ const MIN_PARALLEL_ENVIRONMENT_BUILD_SPARE_CORES = 4;
90
+
91
+ export const shouldParallelizeEnvironmentBuilds = ({
92
+ isBuild,
93
+ spareCoreCount = getDefaultConcurrency(),
94
+ }: {
95
+ isBuild: boolean;
96
+ spareCoreCount?: number;
97
+ }): boolean =>
98
+ !isBuild && spareCoreCount >= MIN_PARALLEL_ENVIRONMENT_BUILD_SPARE_CORES;
99
+
100
+ type ModuleFederationPluginLike = {
101
+ name?: string;
102
+ _options?: { experiments?: { asyncStartup?: boolean } };
103
+ options?: { experiments?: { asyncStartup?: boolean } };
104
+ };
105
+
106
+ const ensureFederationAsyncStartup = (
107
+ rspackConfig: Rspack.Configuration | undefined
108
+ ): void => {
109
+ if (!rspackConfig?.plugins?.length) {
110
+ return;
111
+ }
112
+
113
+ for (const plugin of rspackConfig.plugins) {
114
+ if (!plugin || typeof plugin !== 'object') {
115
+ continue;
116
+ }
117
+ const pluginName = (plugin as ModuleFederationPluginLike).name;
118
+ if (pluginName !== 'ModuleFederationPlugin') {
119
+ continue;
120
+ }
121
+
122
+ const pluginOptions =
123
+ (plugin as ModuleFederationPluginLike)._options ??
124
+ (plugin as ModuleFederationPluginLike).options;
125
+ if (!pluginOptions) {
126
+ continue;
127
+ }
128
+
129
+ pluginOptions.experiments = {
130
+ ...pluginOptions.experiments,
131
+ asyncStartup: true,
132
+ };
133
+ }
134
+ };
135
+
136
+ const cssUrlAssetExtensions =
137
+ /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)$/;
138
+ const urlAssetResourceQuery =
139
+ /^(?=.*(?:\?|&)url(?:&|$))(?!.*(?:\?|&)(?:raw|inline)(?:&|$))/;
140
+
141
+ export const pluginReactRouter = (
142
+ options: PluginOptions = {}
143
+ ): RsbuildPlugin => ({
144
+ name: PLUGIN_NAME,
145
+
146
+ async setup(api) {
147
+ const defaultOptions = {
148
+ customServer: false,
149
+ lazyCompilation: true,
150
+ serverOutput: 'module' as const,
151
+ };
152
+
153
+ const pluginOptions = {
154
+ ...defaultOptions,
155
+ ...options,
156
+ };
157
+ const logPerformance = pluginOptions.logPerformance === true;
158
+ const setupStartMs = logPerformance ? performance.now() : 0;
159
+ const performanceProfiler = createReactRouterPerformanceProfiler({
160
+ enabled: logPerformance,
161
+ log: message => api.logger.info(message),
162
+ });
163
+ const nodeExternals = Array.from(
164
+ new Set(['express', ...getSsrExternals(process.cwd())])
165
+ );
166
+
167
+ let assetPrefix = '/';
168
+
169
+ // Best-effort configuration validation (upstream: validate-plugin-order).
170
+ // Run during config modification phase so we don't rely on `getRsbuildConfig()`
171
+ // being available during `setup()`.
172
+ api.modifyRsbuildConfig({
173
+ order: 'pre',
174
+ handler(config) {
175
+ const issues = validatePluginOrderFromConfig(config);
176
+ for (const issue of issues) {
177
+ if (issue.kind === 'error') {
178
+ throw new Error(issue.message);
179
+ }
180
+ api.logger.warn(issue.message);
181
+ }
182
+ assetPrefix = normalizeAssetPrefix(config.output?.assetPrefix);
183
+ return config;
184
+ },
185
+ });
186
+
187
+ api.onBeforeBuild(() => {
188
+ const normalized = api.getNormalizedConfig();
189
+ warnOnClientSourceMaps(normalized, msg => api.logger.warn(msg), 'web');
190
+ });
191
+
192
+ const configPath = findEntryFile(resolve('react-router.config'));
193
+ const configExists = existsSync(configPath);
194
+ let configWatchPaths: string | string[] = configExists
195
+ ? configPath
196
+ : JS_EXTENSIONS.map(extension =>
197
+ resolve(`react-router.config${extension}`)
198
+ );
199
+ let reactRouterUserConfig: Config = {};
200
+ if (!configExists) {
201
+ console.warn(
202
+ 'No react-router.config found, using default configuration.'
203
+ );
204
+ } else {
205
+ const displayPath = relative(process.cwd(), configPath);
206
+ try {
207
+ const { value: imported, watchPaths } =
208
+ await importConfigWithWatchPaths<Config>(configPath);
209
+ configWatchPaths = watchPaths;
210
+ if (imported === undefined) {
211
+ throw new Error(`${displayPath} must provide a default export`);
212
+ }
213
+ if (typeof imported !== 'object') {
214
+ throw new Error(`${displayPath} must export a config`);
215
+ }
216
+ reactRouterUserConfig = imported;
217
+ } catch (error) {
218
+ throw new Error(`Error loading ${displayPath}: ${error}`);
219
+ }
220
+ }
221
+
222
+ const {
223
+ resolved: resolvedConfig,
224
+ presets: configPresets,
225
+ hasConfiguredServerModuleFormat,
226
+ } = await resolveReactRouterConfig(reactRouterUserConfig);
227
+
228
+ const {
229
+ appDirectory,
230
+ basename,
231
+ buildDirectory,
232
+ future,
233
+ allowedActionOrigins,
234
+ routeDiscovery: userRouteDiscovery,
235
+ ssr,
236
+ prerender: prerenderConfig,
237
+ serverBuildFile,
238
+ serverModuleFormat,
239
+ splitRouteModules,
240
+ buildEnd,
241
+ } = resolvedConfig;
242
+
243
+ registerReactRouterTypegen(api, { appDirectory });
244
+
245
+ const hasExplicitServerOutput = Object.prototype.hasOwnProperty.call(
246
+ options,
247
+ 'serverOutput'
248
+ );
249
+ let resolvedServerOutput = pluginOptions.serverOutput;
250
+ if (!hasExplicitServerOutput) {
251
+ resolvedServerOutput =
252
+ serverModuleFormat === 'cjs' ? 'commonjs' : 'module';
253
+ }
254
+
255
+ if (
256
+ hasExplicitServerOutput &&
257
+ hasConfiguredServerModuleFormat &&
258
+ serverModuleFormat &&
259
+ (resolvedServerOutput === 'commonjs' ? 'cjs' : 'esm') !==
260
+ serverModuleFormat
261
+ ) {
262
+ api.logger.warn(
263
+ `[${PLUGIN_NAME}] Both \`serverOutput\` and \`serverModuleFormat\` are set. ` +
264
+ `Using \`serverOutput=${resolvedServerOutput}\` and ignoring ` +
265
+ `\`serverModuleFormat=${serverModuleFormat}\`.`
266
+ );
267
+ }
268
+
269
+ if (serverBuildFile && !serverBuildFile.endsWith('.js')) {
270
+ throw new Error('The `serverBuildFile` config must end in `.js`.');
271
+ }
272
+
273
+ if (serverModuleFormat !== 'esm' && serverModuleFormat !== 'cjs') {
274
+ throw new Error(
275
+ 'The `serverModuleFormat` config must be "esm" or "cjs".'
276
+ );
277
+ }
278
+
279
+ const prerenderConfigError = validatePrerenderConfig(prerenderConfig);
280
+ if (prerenderConfigError) {
281
+ throw new Error(prerenderConfigError);
282
+ }
283
+
284
+ // React Router defaults to "lazy" route discovery, but "ssr:false" builds
285
+ // have no runtime server to serve manifest patch requests, so we force
286
+ // `mode:"initial"` in SPA mode to avoid any `/__manifest` fetches.
287
+ let routeDiscovery: Config['routeDiscovery'];
288
+ if (!userRouteDiscovery) {
289
+ routeDiscovery = ssr
290
+ ? ({ mode: 'lazy', manifestPath: '/__manifest' } as const)
291
+ : ({ mode: 'initial' } as const);
292
+ } else if (userRouteDiscovery.mode === 'initial') {
293
+ routeDiscovery = userRouteDiscovery;
294
+ } else if (userRouteDiscovery.mode === 'lazy') {
295
+ if (!ssr) {
296
+ throw new Error(
297
+ 'The `routeDiscovery.mode` config cannot be set to "lazy" when setting `ssr:false`'
298
+ );
299
+ }
300
+ const manifestPath = userRouteDiscovery.manifestPath;
301
+ if (manifestPath && !manifestPath.startsWith('/')) {
302
+ throw new Error(
303
+ 'The `routeDiscovery.manifestPath` config must be a root-relative pathname beginning with a slash (i.e., "/__manifest")'
304
+ );
305
+ }
306
+ routeDiscovery = userRouteDiscovery;
307
+ }
308
+
309
+ (globalThis as any).__reactRouterAppDirectory = resolve(appDirectory);
310
+ const routesPath = findEntryFile(resolve(appDirectory, 'routes'));
311
+ if (!existsSync(routesPath)) {
312
+ throw new Error(
313
+ `Route config file not found at "${relative(
314
+ process.cwd(),
315
+ routesPath
316
+ )}".`
317
+ );
318
+ }
319
+
320
+ const jiti = createJiti(process.cwd(), {
321
+ moduleCache: false,
322
+ });
323
+ const importRouteConfig = async (
324
+ importer: Pick<typeof jiti, 'import'>
325
+ ): Promise<RouteConfigEntry[]> => {
326
+ const routeConfigExport = await importer.import<RouteConfigEntry[]>(
327
+ routesPath,
328
+ {
329
+ default: true,
330
+ }
331
+ );
332
+ const routeConfigValue = await routeConfigExport;
333
+ const validation = validateRouteConfig({
334
+ routeConfigFile: relative(process.cwd(), routesPath),
335
+ routeConfig: routeConfigValue,
336
+ });
337
+ if (!validation.valid) {
338
+ throw new Error(validation.message);
339
+ }
340
+ return validation.routeConfig;
341
+ };
342
+ const loadRouteConfig = () => importRouteConfig(jiti);
343
+ const { value: routeConfig, watchPaths: routeConfigWatchPaths } =
344
+ await importConfigWithWatchPaths(routesPath, importRouteConfig);
345
+
346
+ const entryClientPath = findEntryFile(
347
+ resolve(appDirectory, 'entry.client')
348
+ );
349
+ const entryServerPath = findEntryFile(
350
+ resolve(appDirectory, 'entry.server')
351
+ );
352
+
353
+ const serverAppPath = findEntryFile(
354
+ resolve(appDirectory, '../server/index')
355
+ );
356
+ const hasServerApp = existsSync(serverAppPath);
357
+ const devServerBuildEntryName = hasServerApp
358
+ ? 'static/js/react-router-server-build'
359
+ : 'static/js/app';
360
+
361
+ const templateDir = resolve(__dirname, 'templates');
362
+ const templateClientPath = resolve(templateDir, 'entry.client.js');
363
+ const templateServerPath = resolve(templateDir, 'entry.server.js');
364
+
365
+ const finalEntryClientPath = existsSync(entryClientPath)
366
+ ? entryClientPath
367
+ : templateClientPath;
368
+ const finalEntryServerPath = existsSync(entryServerPath)
369
+ ? entryServerPath
370
+ : templateServerPath;
371
+
372
+ const getRootRoutePath = () => findEntryFile(resolve(appDirectory, 'root'));
373
+ const rootRoutePath = getRootRoutePath();
374
+ // React Router's server build expects route files relative to `appDirectory`
375
+ // so it can resolve them correctly during compilation.
376
+ const rootRouteFile = relative(appDirectory, rootRoutePath);
377
+ const routeTopology = createReactRouterRouteTopology({
378
+ appDirectory,
379
+ rootRouteFile,
380
+ routeConfig,
381
+ loadRouteConfig,
382
+ getRootRoutePath,
383
+ });
384
+
385
+ const routes = {
386
+ root: { path: '', id: 'root', file: rootRouteFile },
387
+ ...configRoutesToRouteManifest(appDirectory, routeConfig),
388
+ };
389
+
390
+ const resolvedConfigWithRoutes: ResolvedReactRouterConfig = {
391
+ ...resolvedConfig,
392
+ appDirectory: resolve(appDirectory),
393
+ buildDirectory: resolve(buildDirectory),
394
+ routeDiscovery,
395
+ prerender: prerenderConfig,
396
+ routes,
397
+ unstable_routeConfig: routeConfig,
398
+ allowedActionOrigins: allowedActionOrigins ?? false,
399
+ };
400
+
401
+ const { buildEnd: _buildEnd, ...resolvedConfigForPreset } =
402
+ resolvedConfigWithRoutes;
403
+ for (const preset of configPresets) {
404
+ await preset.reactRouterConfigResolved?.({
405
+ reactRouterConfig: resolvedConfigForPreset,
406
+ });
407
+ }
408
+
409
+ const isBuild = api.context.action === 'build';
410
+ const shouldDependOnWebCompiler = !shouldParallelizeEnvironmentBuilds({
411
+ isBuild,
412
+ });
413
+ const isPrerenderEnabled =
414
+ prerenderConfig !== undefined && prerenderConfig !== false;
415
+ const isSpaMode = !ssr && !isPrerenderEnabled;
416
+ const routeCount = Object.keys(routes).length;
417
+ const routeChunkConfig: RouteChunkConfig = {
418
+ splitRouteModules,
419
+ appDirectory,
420
+ rootRouteFile,
421
+ };
422
+ const routeChunkCache: RouteChunkCache = new Map();
423
+ const routeTransformExecutor = createRouteTransformExecutor({
424
+ parallelRouteTransform:
425
+ pluginOptions.parallelRouteTransform ??
426
+ shouldParallelizeRouteTransforms(routeCount),
427
+ routeChunkCache,
428
+ splitRouteModules: Boolean(splitRouteModules),
429
+ isBuild,
430
+ });
431
+ const routeChunkOptions = {
432
+ splitRouteModules,
433
+ rootRouteFile,
434
+ isBuild,
435
+ cache: routeChunkCache,
436
+ };
437
+ const outputClientPath = resolve(buildDirectory, 'client');
438
+ const assetsBuildDirectory = relative(process.cwd(), outputClientPath);
439
+ const watchDirectory = resolve(appDirectory);
440
+ const routeRestartMarkerPath = getRouteRestartMarkerPath(outputClientPath);
441
+ const routeWatchFiles = createReactRouterRouteWatchFiles({
442
+ configWatchPaths,
443
+ routeConfigWatchPaths,
444
+ routeRestartMarkerPath,
445
+ onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
446
+ });
447
+ const devBackgroundResources = registerReactRouterDevBackgroundResources({
448
+ api,
449
+ isBuild,
450
+ lazyCompilationPrewarm: pluginOptions.unstableLazyCompilationPrewarm,
451
+ routeTransformExecutor,
452
+ routeRestartMarkerPath,
453
+ watchDirectory,
454
+ getRouteTopology: routeTopology.getRouteTopology,
455
+ initialRouteTopology: routeTopology.initialRouteTopology,
456
+ onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
457
+ });
458
+
459
+ type ReactRouterManifest = Awaited<
460
+ ReturnType<typeof getReactRouterManifestForDev>
461
+ >;
462
+ let latestBrowserManifest: ReactRouterManifest | null = null;
463
+ let latestBrowserManifestModuleExports: RouteManifestModuleExports = {};
464
+ let latestServerManifest: ReactRouterManifest | null = null;
465
+ const latestServerManifestsByBundleId: Record<string, ReactRouterManifest> =
466
+ {};
467
+
468
+ const stageLatestManifests = (
469
+ manifest: ReactRouterManifest,
470
+ sri: ReactRouterManifest['sri'],
471
+ moduleExportsByRouteId: RouteManifestModuleExports,
472
+ compilation: Rspack.Compilation
473
+ ) => {
474
+ performanceProfiler.recordSync(
475
+ 'web',
476
+ 'manifest:stage',
477
+ 'virtual/react-router/browser-manifest',
478
+ () => {
479
+ latestBrowserManifest = manifest;
480
+ devBackgroundResources.setManifest(manifest);
481
+ latestBrowserManifestModuleExports = moduleExportsByRouteId;
482
+ const baseServerManifest = {
483
+ ...manifest,
484
+ sri,
485
+ };
486
+ latestServerManifest = baseServerManifest;
487
+ const manifestsByEntryName: Record<string, ReactRouterManifest> = {
488
+ [devServerBuildEntryName]: baseServerManifest,
489
+ };
490
+
491
+ for (const { bundleId, entryName } of serverBundleEntries) {
492
+ const bundleRoutes = routesByServerBundleId[bundleId];
493
+ if (!bundleRoutes) {
494
+ continue;
495
+ }
496
+
497
+ const routeIds = new Set(Object.keys(bundleRoutes));
498
+ const filteredRoutes = Object.fromEntries(
499
+ Object.entries(manifest.routes).filter(([routeId]) =>
500
+ routeIds.has(routeId)
501
+ )
502
+ );
503
+ const bundleManifest = {
504
+ ...baseServerManifest,
505
+ routes: filteredRoutes,
506
+ };
507
+ latestServerManifestsByBundleId[bundleId] = bundleManifest;
508
+ manifestsByEntryName[entryName] = bundleManifest;
509
+ }
510
+
511
+ if (!isBuild) {
512
+ devRuntime.captureWeb(compilation, manifestsByEntryName);
513
+ }
514
+ }
515
+ );
516
+ };
517
+
518
+ const routeByFilePath = new Map(
519
+ Object.values(routes).map(route => [
520
+ resolve(appDirectory, route.file),
521
+ route,
522
+ ])
523
+ );
524
+
525
+ const manifestChunkNames = new Set<string>(['entry.client']);
526
+ const webRouteEntries = Object.values(routes).reduce(
527
+ (acc, route) => {
528
+ const entryName = route.file.slice(0, route.file.lastIndexOf('.'));
529
+ const routeFilePath = resolve(appDirectory, route.file);
530
+ manifestChunkNames.add(entryName);
531
+ acc[entryName] = {
532
+ import: `${routeFilePath}${BUILD_CLIENT_ROUTE_QUERY_STRING}`,
533
+ html: false,
534
+ };
535
+
536
+ if (isBuild && splitRouteModules && route.id !== 'root') {
537
+ let source: string;
538
+ try {
539
+ source = readFileSync(routeFilePath, 'utf8');
540
+ } catch (error) {
541
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
542
+ return acc;
543
+ }
544
+ throw error;
545
+ }
546
+ for (const exportName of routeChunkExportNames) {
547
+ if (!source.includes(exportName)) {
548
+ continue;
549
+ }
550
+ const chunkEntryName = getRouteChunkEntryName(route.id, exportName);
551
+ manifestChunkNames.add(chunkEntryName);
552
+ acc[chunkEntryName] = {
553
+ import: getRouteChunkModuleId(routeFilePath, exportName),
554
+ html: false,
555
+ };
556
+ }
557
+ }
558
+
559
+ return acc;
560
+ },
561
+ {} as Record<string, RsbuildEntryDescription>
562
+ );
563
+ const buildManifest = await getBuildManifest({
564
+ reactRouterConfig: resolvedConfigWithRoutes,
565
+ routes,
566
+ rootDirectory: process.cwd(),
567
+ });
568
+ const routesByServerBundleId = getRoutesByServerBundleId(
569
+ buildManifest,
570
+ routes
571
+ );
572
+ const serverBuildPlan = createReactRouterServerBuildPlan({
573
+ routesByServerBundleId,
574
+ serverBuildFile,
575
+ defaultEntryName: devServerBuildEntryName,
576
+ });
577
+ const { serverBundleEntries } = serverBuildPlan;
578
+ const devRuntime = createReactRouterDevRuntimeController({
579
+ api,
580
+ isBuild,
581
+ buildPlan: serverBuildPlan,
582
+ });
583
+
584
+ let clientStats: ReactRouterManifestStats | undefined;
585
+ api.onAfterEnvironmentCompile(({ stats, environment }) => {
586
+ if (environment.name === 'web') {
587
+ clientStats = createReactRouterManifestStats(
588
+ stats?.compilation,
589
+ manifestChunkNames
590
+ );
591
+ }
592
+ if (pluginOptions.federation && ssr) {
593
+ const serverBuildDir = resolve(buildDirectory, 'server');
594
+ const clientBuildDir = resolve(buildDirectory, 'client');
595
+ if (existsSync(serverBuildDir)) {
596
+ const ssrDir = resolve(clientBuildDir, 'static');
597
+ fsExtra.copySync(serverBuildDir, ssrDir);
598
+ }
599
+ }
600
+ if (logPerformance) {
601
+ performanceProfiler.flush(environment.name, {
602
+ compilerLifecycleMs: roundMs(performance.now() - setupStartMs),
603
+ });
604
+ }
605
+ });
606
+
607
+ const prerenderPaths = await resolvePrerenderPaths(
608
+ prerenderConfig,
609
+ ssr,
610
+ routeConfig,
611
+ {
612
+ logWarning: true,
613
+ warn: message => api.logger.warn(message),
614
+ }
615
+ );
616
+
617
+ api.onAfterBuild(({ environments }) =>
618
+ runPluginEffect(
619
+ tryPluginPromise(() =>
620
+ runReactRouterPrerenderBuild({
621
+ api,
622
+ hasWebEnvironment: Boolean(environments.web),
623
+ buildDirectory,
624
+ serverBuildFile,
625
+ ssr,
626
+ isPrerenderEnabled,
627
+ prerenderConfig,
628
+ prerenderPaths,
629
+ basename,
630
+ future,
631
+ routes,
632
+ latestBrowserManifest,
633
+ latestBrowserManifestModuleExports,
634
+ clientStats,
635
+ pluginOptions,
636
+ appDirectory,
637
+ assetPrefix,
638
+ routeChunkOptions,
639
+ buildManifest,
640
+ resolvedConfigWithRoutes,
641
+ buildEnd,
642
+ })
643
+ )
644
+ )
645
+ );
646
+
647
+ const allowedActionOriginsForBuild =
648
+ allowedActionOrigins === false ? undefined : allowedActionOrigins;
649
+
650
+ // Public requests stay bare while Rspack resolves seeded virtual files.
651
+ const createVirtualModulePlugin = (publicPath: string) => {
652
+ const bundleVirtualModules = Object.fromEntries(
653
+ Object.entries(routesByServerBundleId).map(
654
+ ([bundleId, bundleRoutes]) => [
655
+ `virtual/react-router/server-build-${bundleId}`,
656
+ generateServerBuild(bundleRoutes, {
657
+ entryServerPath: finalEntryServerPath,
658
+ assetsBuildDirectory,
659
+ basename,
660
+ appDirectory,
661
+ ssr,
662
+ federation: options.federation,
663
+ future,
664
+ allowedActionOrigins: allowedActionOriginsForBuild,
665
+ prerender: prerenderPaths,
666
+ routeDiscovery,
667
+ publicPath,
668
+ serverManifestId: `virtual/react-router/server-manifest-${bundleId}`,
669
+ }),
670
+ ]
671
+ )
672
+ );
673
+ const bundleManifestModules = Object.fromEntries(
674
+ Object.entries(routesByServerBundleId)
675
+ .filter(
676
+ ([, bundleRoutes]) =>
677
+ bundleRoutes && Object.keys(bundleRoutes).length > 0
678
+ )
679
+ .map(([bundleId]) => [
680
+ `virtual/react-router/server-manifest-${bundleId}`,
681
+ 'export default {};',
682
+ ])
683
+ );
684
+
685
+ return new rspack.experiments.VirtualModulesPlugin(
686
+ mapVirtualModules({
687
+ 'virtual/react-router/browser-manifest': 'export default {};',
688
+ 'virtual/react-router/server-manifest': 'export default {};',
689
+ 'virtual/react-router/server-build': generateServerBuild(routes, {
690
+ entryServerPath: finalEntryServerPath,
691
+ assetsBuildDirectory,
692
+ basename,
693
+ appDirectory,
694
+ ssr,
695
+ federation: options.federation,
696
+ future,
697
+ allowedActionOrigins: allowedActionOriginsForBuild,
698
+ prerender: prerenderPaths,
699
+ routeDiscovery,
700
+ publicPath,
701
+ }),
702
+ ...bundleVirtualModules,
703
+ ...bundleManifestModules,
704
+ 'virtual/react-router/with-props': generateWithProps(),
705
+ })
706
+ );
707
+ };
708
+
709
+ api.modifyRsbuildConfig(async (config, { mergeRsbuildConfig }) => {
710
+ assetPrefix = normalizeAssetPrefix(config.output?.assetPrefix);
711
+ const vmodPlugin = createVirtualModulePlugin(assetPrefix);
712
+ const useAsyncNodeChunkLoading =
713
+ options.federation && resolvedServerOutput === 'commonjs';
714
+ let nodeChunkLoading: 'import' | 'async-node' | 'require' = 'require';
715
+ if (resolvedServerOutput === 'module') {
716
+ nodeChunkLoading = 'import';
717
+ } else if (useAsyncNodeChunkLoading) {
718
+ nodeChunkLoading = 'async-node';
719
+ }
720
+ const nodeEntries = createReactRouterNodeEntries({
721
+ hasServerApp,
722
+ isBuild,
723
+ serverAppPath,
724
+ entryServerPath: finalEntryServerPath,
725
+ defaultEntryName: devServerBuildEntryName,
726
+ serverBundleEntries,
727
+ });
728
+
729
+ const configuredLazyCompilation = Object.prototype.hasOwnProperty.call(
730
+ options,
731
+ 'lazyCompilation'
732
+ )
733
+ ? pluginOptions.lazyCompilation
734
+ : (config.dev?.lazyCompilation ?? pluginOptions.lazyCompilation);
735
+ const guardedLazyCompilation = guardReactRouterLazyCompilation({
736
+ lazyCompilation: configuredLazyCompilation,
737
+ entryClientPath: finalEntryClientPath,
738
+ prewarmReactRouterModules: Boolean(
739
+ pluginOptions.unstableLazyCompilationPrewarm
740
+ ),
741
+ });
742
+ const lazyCompilation =
743
+ guardedLazyCompilation === undefined
744
+ ? {}
745
+ : { lazyCompilation: guardedLazyCompilation };
746
+ const shouldCompactFileSizeReport =
747
+ isBuild &&
748
+ routeCount >= 256 &&
749
+ (config.performance?.printFileSize === undefined ||
750
+ config.performance.printFileSize === true);
751
+
752
+ return mergeRsbuildConfig(config, {
753
+ ...(shouldCompactFileSizeReport
754
+ ? {
755
+ performance: {
756
+ printFileSize: {
757
+ total: true,
758
+ detail: false,
759
+ compressed: false,
760
+ },
761
+ },
762
+ }
763
+ : {}),
764
+ output: {
765
+ assetPrefix: config.output?.assetPrefix || '/',
766
+ },
767
+ dev: {
768
+ writeToDisk: true,
769
+ ...lazyCompilation,
770
+ watchFiles: mergeWatchFiles(config.dev?.watchFiles, routeWatchFiles),
771
+ },
772
+ // React Router's request handler natively supports `ssr:false`
773
+ // builds (it renders the SPA shell for document requests), so the
774
+ // middleware is registered for SPA mode too — without it, dev
775
+ // requests would 404 because no HTML entry exists.
776
+ ...(pluginOptions.customServer
777
+ ? {}
778
+ : {
779
+ server: {
780
+ setup: [
781
+ createReactRouterDevServerSetup({
782
+ // Lazy: the dev runtime binding does not exist yet here.
783
+ loadBuild: () => devRuntime.createBuildLoader()(),
784
+ }),
785
+ ],
786
+ },
787
+ }),
788
+ tools: {
789
+ rspack: {
790
+ plugins: [vmodPlugin],
791
+ },
792
+ },
793
+ environments: {
794
+ web: {
795
+ ...(resolvedConfigWithRoutes.subResourceIntegrity
796
+ ? {
797
+ security: {
798
+ sri: {
799
+ enable: true,
800
+ },
801
+ },
802
+ }
803
+ : {}),
804
+ source: {
805
+ entry: {
806
+ // no query needed when federation is disabled
807
+ 'entry.client': finalEntryClientPath,
808
+ 'virtual/react-router/browser-manifest': {
809
+ import: 'virtual/react-router/browser-manifest',
810
+ html: false,
811
+ },
812
+ ...webRouteEntries,
813
+ },
814
+ },
815
+ output: {
816
+ filename: {
817
+ js: '[name].js',
818
+ },
819
+ distPath: {
820
+ root: outputClientPath,
821
+ },
822
+ },
823
+ tools: {
824
+ rspack: {
825
+ name: 'web',
826
+ module: {
827
+ rules: [
828
+ {
829
+ resourceQuery: urlAssetResourceQuery,
830
+ exclude: cssUrlAssetExtensions,
831
+ type: 'asset/resource',
832
+ },
833
+ ],
834
+ },
835
+ ...(options.federation
836
+ ? {
837
+ output: {
838
+ chunkLoading: 'import',
839
+ },
840
+ }
841
+ : {}),
842
+ externalsType: 'module',
843
+ output: {
844
+ chunkFormat: 'module',
845
+ chunkLoading: 'import',
846
+ workerChunkLoading: 'import',
847
+ wasmLoading: 'fetch',
848
+ library: { type: 'module' },
849
+ module: true,
850
+ },
851
+ optimization: {
852
+ avoidEntryIife: true,
853
+ runtimeChunk: 'single',
854
+ },
855
+ },
856
+ },
857
+ },
858
+ // Always include node environment, even for SPA mode (`ssr:false`),
859
+ // because React Router still needs a server build to prerender the
860
+ // root route into a hydratable `index.html` at build time.
861
+ node: {
862
+ source: {
863
+ entry: nodeEntries,
864
+ },
865
+ output: {
866
+ distPath: {
867
+ root: resolve(buildDirectory, 'server'),
868
+ },
869
+ target: config.environments?.node?.output?.target || 'node',
870
+ filename: {
871
+ js: '[name].js',
872
+ },
873
+ },
874
+ tools: {
875
+ rspack: {
876
+ target: options.federation ? 'async-node' : 'node',
877
+ module: {
878
+ rules: [
879
+ {
880
+ resourceQuery: urlAssetResourceQuery,
881
+ exclude: cssUrlAssetExtensions,
882
+ type: 'asset/resource',
883
+ },
884
+ ],
885
+ },
886
+ externals: nodeExternals,
887
+ ...(shouldDependOnWebCompiler ? { dependencies: ['web'] } : {}),
888
+ externalsType: resolvedServerOutput,
889
+ output: {
890
+ chunkFormat: resolvedServerOutput,
891
+ chunkLoading: nodeChunkLoading,
892
+ workerChunkLoading: nodeChunkLoading,
893
+ wasmLoading: 'fetch',
894
+ module: resolvedServerOutput === 'module',
895
+ },
896
+ },
897
+ },
898
+ },
899
+ },
900
+ });
901
+ });
902
+
903
+ api.modifyEnvironmentConfig(
904
+ async (config, { name, mergeEnvironmentConfig }) => {
905
+ if (name !== 'web' && name !== 'node') {
906
+ return config;
907
+ }
908
+
909
+ return mergeEnvironmentConfig(config, {
910
+ tools: {
911
+ rspack: rspackConfig => {
912
+ if (pluginOptions.federation) {
913
+ ensureFederationAsyncStartup(rspackConfig);
914
+ }
915
+
916
+ if (name === 'node') {
917
+ const output = rspackConfig.output;
918
+ if (output) {
919
+ const library = output.library;
920
+ const libraryOptions =
921
+ library &&
922
+ typeof library === 'object' &&
923
+ !Array.isArray(library)
924
+ ? library
925
+ : {};
926
+ rspackConfig.output = {
927
+ ...output,
928
+ library: {
929
+ ...libraryOptions,
930
+ type:
931
+ resolvedServerOutput === 'module'
932
+ ? 'module'
933
+ : 'commonjs2',
934
+ },
935
+ };
936
+ }
937
+ }
938
+
939
+ return rspackConfig;
940
+ },
941
+ },
942
+ });
943
+ }
944
+ );
945
+
946
+ registerModifyBrowserManifestAssets(
947
+ api,
948
+ routes,
949
+ pluginOptions,
950
+ appDirectory,
951
+ () => assetPrefix,
952
+ routeChunkOptions,
953
+ {
954
+ subResourceIntegrity: resolvedConfigWithRoutes.subResourceIntegrity,
955
+ future,
956
+ manifestChunkNames,
957
+ onManifest: (manifest, sri, moduleExportsByRouteId, context) =>
958
+ stageLatestManifests(
959
+ manifest,
960
+ sri,
961
+ moduleExportsByRouteId,
962
+ context.compilation
963
+ ),
964
+ }
965
+ );
966
+
967
+ registerBuildOutputTransforms({
968
+ api,
969
+ resolvedServerOutput,
970
+ performanceProfiler,
971
+ getLatestServerManifest: () => latestServerManifest,
972
+ getLatestServerManifestByBundleId: bundleId =>
973
+ latestServerManifestsByBundleId[bundleId],
974
+ routes,
975
+ pluginOptions,
976
+ getClientStats: () => clientStats,
977
+ appDirectory,
978
+ getAssetPrefix: () => assetPrefix,
979
+ routeChunkOptions,
980
+ routeTransformExecutor,
981
+ routeByFilePath,
982
+ routeChunkConfig,
983
+ isBuild,
984
+ splitRouteModules: Boolean(splitRouteModules),
985
+ ssr,
986
+ isSpaMode,
987
+ rootRoutePath,
988
+ });
989
+ },
990
+ });