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