rsbuild-plugin-react-router 0.4.1 → 0.5.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.
package/README.md CHANGED
@@ -77,16 +77,16 @@ pluginReactRouter({
77
77
  });
78
78
  ```
79
79
 
80
- | Option | Default | Description |
81
- | -------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
- | `customServer` | `false` | Disables the built-in development SSR middleware. Enable this when an app owns the server with `createDevServer()` or an adapter. |
83
- | `serverOutput` | Derived | Emitted Rsbuild server format: `'module'` or `'commonjs'`. When omitted, React Router's `serverModuleFormat` selects the format (`'esm'` -> `'module'`, `'cjs'` -> `'commonjs'`); setting `serverOutput` overrides it. |
84
- | `lazyCompilation` | `true` | Optional Rsbuild dev lazy-compilation config. When enabled here or through `dev.lazyCompilation`, React Router hydration-critical modules stay eager so the browser manifest and route modules are not replaced by lazy proxies. |
85
- | `unstableLazyCompilationPrewarm` | `false` | Experimental prewarm for emitted Rspack lazy-compilation proxy modules after dev compiles. Enable with `true` when route JS proxy startup should happen shortly after compiler readiness. |
86
- | `logPerformance` | `false` | Logs structured React Router plugin timing information through the Rsbuild logger. |
87
- | `parallelRouteTransform` | `undefined` | Controls worker-thread route transforms. `undefined` auto-enables workers for 256+ routes, `true` forces the default worker count (in dev this is 0 on machines with 4 or fewer cores, where workers cost more than they save; production builds always use workers), a positive integer sets the worker count, and `false` keeps transforms inline. |
88
- | `onRouteTopologyChange` | `undefined` | Notification for programmatic/custom dev servers. Recreate the Rsbuild server when route files are added, removed, or moved. The callback is not awaited. |
89
- | `federation` | `false` | Enables the plugin's experimental Module Federation integration. |
80
+ | Option | Default | Description |
81
+ | -------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
+ | `customServer` | `false` | Disables the built-in development SSR middleware. Enable this when an app owns the server with `createDevServer()` or an adapter. |
83
+ | `serverOutput` | Derived | Emitted Rsbuild server format: `'module'` or `'commonjs'`. When omitted, React Router's `serverModuleFormat` selects the format (`'esm'` -> `'module'`, `'cjs'` -> `'commonjs'`); setting `serverOutput` overrides it. |
84
+ | `lazyCompilation` | `true` | Optional Rsbuild dev lazy-compilation config. When enabled here or through `dev.lazyCompilation`, React Router hydration-critical modules stay eager so the browser manifest and route modules are not replaced by lazy proxies. |
85
+ | `unstableLazyCompilationPrewarm` | `false` | Experimental prewarm for emitted Rspack lazy-compilation proxy modules after dev compiles. Enable with `true` when route JS proxy startup should happen shortly after compiler readiness. |
86
+ | `logPerformance` | `false` | Logs structured React Router plugin timing information. |
87
+ | `parallelRouteTransform` | `undefined` | Controls worker-thread route transforms. `undefined` and `false` keep transforms inline, `true` uses Rspack's default worker count, and a positive integer sets the maximum worker count. |
88
+ | `onRouteTopologyChange` | `undefined` | Notification for programmatic/custom dev servers. Recreate the Rsbuild server when route files are added, removed, or moved. The callback is not awaited. |
89
+ | `federation` | `false` | Enables the plugin's experimental Module Federation integration. |
90
90
 
91
91
  When `federation` is enabled, configure the Module Federation plugin with
92
92
  `experiments.asyncStartup: true`. The dev server resolves async server build
@@ -655,35 +655,17 @@ The plugin automatically:
655
655
 
656
656
  ### Benchmarking
657
657
 
658
- `pnpm bench:large` runs this repository's generated stress fixture for quick
659
- regression checks. `pnpm bench:synthetic-app` runs the embedded complex Rsbuild
660
- app under `benchmarks/synthetic-web-bundler-benchmark`, which adds heavier
661
- loader and transform contention for benchmark coverage closer to a large
662
- real-world application.
658
+ Run the focused local suite for plugin regression checks:
663
659
 
664
660
  ```bash
665
- pnpm bench:large
666
- pnpm bench:synthetic-app -- --profile all --runs 2
661
+ pnpm bench
662
+ pnpm bench:smoke
663
+ pnpm bench:codspeed
667
664
  ```
668
665
 
669
- The PR benchmark workflow reports production build, dev route-load, HMR/update,
670
- and embedded synthetic app timings in the same benchmark comment. It measures
671
- the PR and its base on the same runner instead of reusing cached timing data,
672
- counterbalances which side runs first, and excludes one warmup iteration. Small
673
- fixtures use five measured iterations; expensive large fixtures use three.
674
-
675
- Every raw median delta remains visible. The comment also reports each side's
676
- relative median absolute deviation (rMAD), a conservative noise band, and a
677
- signal label:
678
-
679
- - `regression` or `improvement` means the median delta exceeds the observed
680
- run-to-run noise band.
681
- - `inconclusive` means the raw delta is not clearly separated from that noise.
682
- - `insufficient data` means either side has fewer than three finite samples.
683
-
684
- The labels are triage aids, not pass/fail gates. Use the uploaded diagnostics
685
- and raw per-run samples to investigate important changes, and rerun an
686
- inconclusive comparison before treating it as a performance result.
666
+ See the [benchmark guide](./benchmarks/README.md) for JSON output, pull-request
667
+ comparisons and comments, `BENCHMARK_PLUGIN_ROOT`, and optional CodSpeed
668
+ publication.
687
669
 
688
670
  ## React Router Framework Mode
689
671
 
@@ -1217,5 +1217,96 @@ if (import.meta.webpackHot) {
1217
1217
  case 'routeModule':
1218
1218
  return transformRouteModule(task);
1219
1219
  }
1220
+ }, roundMs = (value)=>Math.round(10 * value) / 10, createReactRouterPerformanceProfiler = ({ enabled, log })=>{
1221
+ let timingsByEnvironment = new Map(), recordDuration = (environment, operation, resource, startMs, endMs)=>{
1222
+ let timings, timing, duration = endMs - startMs, timing1 = ((timings = timingsByEnvironment.get(environment)) || (timings = new Map(), timingsByEnvironment.set(environment, timings)), (timing = timings.get(operation)) || (timing = {
1223
+ count: 0,
1224
+ totalMs: 0,
1225
+ maxMs: 0,
1226
+ slowest: [],
1227
+ intervals: []
1228
+ }, timings.set(operation, timing)), timing);
1229
+ timing1.count += 1, timing1.totalMs += duration, timing1.maxMs = Math.max(timing1.maxMs, duration), timing1.intervals.push({
1230
+ startMs,
1231
+ endMs
1232
+ }), ((slowest, entry)=>{
1233
+ if (5 === slowest.length && entry.durationMs <= slowest[slowest.length - 1].durationMs) return;
1234
+ let insertIndex = slowest.length;
1235
+ for(; insertIndex > 0 && entry.durationMs > slowest[insertIndex - 1].durationMs;)insertIndex -= 1;
1236
+ slowest.splice(insertIndex, 0, entry), slowest.length > 5 && slowest.pop();
1237
+ })(timing1.slowest, {
1238
+ durationMs: duration,
1239
+ resource
1240
+ });
1241
+ };
1242
+ return {
1243
+ record (environment, operation, resource, callback) {
1244
+ if (!enabled) try {
1245
+ return Promise.resolve(callback());
1246
+ } catch (error) {
1247
+ return Promise.reject(error);
1248
+ }
1249
+ let resolvedEnvironment = environment ?? 'unknown', start = performance.now();
1250
+ try {
1251
+ return callback().then((result)=>{
1252
+ let end = performance.now();
1253
+ return recordDuration(resolvedEnvironment, operation, resource, start, end), result;
1254
+ }, (error)=>{
1255
+ let end = performance.now();
1256
+ throw recordDuration(resolvedEnvironment, operation, resource, start, end), error;
1257
+ });
1258
+ } catch (error) {
1259
+ return recordDuration(resolvedEnvironment, operation, resource, start, performance.now()), Promise.reject(error);
1260
+ }
1261
+ },
1262
+ recordSync (environment, operation, resource, callback) {
1263
+ if (!enabled) return callback();
1264
+ let start = performance.now();
1265
+ try {
1266
+ return callback();
1267
+ } finally{
1268
+ recordDuration(environment ?? 'unknown', operation, resource, start, performance.now());
1269
+ }
1270
+ },
1271
+ flush (environment, details = {}) {
1272
+ let report;
1273
+ if (!enabled) return;
1274
+ let resolvedEnvironment = environment ?? 'unknown', timings = timingsByEnvironment.get(resolvedEnvironment);
1275
+ if (!timings || 0 === timings.size) return;
1276
+ let operations = Object.fromEntries([
1277
+ ...timings.entries()
1278
+ ].map(([operation, timing])=>[
1279
+ operation,
1280
+ {
1281
+ count: timing.count,
1282
+ totalMs: roundMs(timing.totalMs),
1283
+ wallMs: ((intervals)=>{
1284
+ if (0 === intervals.length) return 0;
1285
+ let sortedIntervals = [
1286
+ ...intervals
1287
+ ].sort((a, b)=>a.startMs - b.startMs || a.endMs - b.endMs), mergedStart = sortedIntervals[0].startMs, mergedEnd = sortedIntervals[0].endMs, wallMs = 0;
1288
+ for (let interval of sortedIntervals.slice(1)){
1289
+ if (interval.startMs <= mergedEnd) {
1290
+ mergedEnd = Math.max(mergedEnd, interval.endMs);
1291
+ continue;
1292
+ }
1293
+ wallMs += mergedEnd - mergedStart, mergedStart = interval.startMs, mergedEnd = interval.endMs;
1294
+ }
1295
+ return roundMs(wallMs += mergedEnd - mergedStart);
1296
+ })(timing.intervals),
1297
+ maxMs: roundMs(timing.maxMs),
1298
+ slowest: timing.slowest.map((entry)=>({
1299
+ durationMs: roundMs(entry.durationMs),
1300
+ resource: entry.resource
1301
+ }))
1302
+ }
1303
+ ]));
1304
+ log((report = {
1305
+ environment: resolvedEnvironment,
1306
+ ...details,
1307
+ operations
1308
+ }, `[react-router:performance] ${JSON.stringify(report)}`)), timingsByEnvironment.delete(resolvedEnvironment);
1309
+ }
1310
+ };
1220
1311
  };
1221
- export { BUILD_CLIENT_ROUTE_QUERY_STRING, CLIENT_EXPORTS, HMR_PATCHABLE_ROUTE_FLAGS, JS_EXTENSIONS, PLUGIN_NAME, SERVER_EXPORTS, buildManifestChunkValidity, combineURLs, createBundlerRouteExportResolver, createEmptyRouteChunkByExportName, createRouteId, detectRouteChunksIfEnabled, executeRouteTransformTask, findEntryFile, generateWithProps, getRouteChunkEntryName, getRouteChunkModuleId, getRouteModuleAnalysis, normalizeAssetPrefix, routeChunkExportNames, setBoundedCacheEntry, validateRouteChunks };
1312
+ export { BUILD_CLIENT_ROUTE_QUERY_STRING, CLIENT_EXPORTS, HMR_PATCHABLE_ROUTE_FLAGS, JS_EXTENSIONS, PLUGIN_NAME, SERVER_EXPORTS, buildManifestChunkValidity, combineURLs, createBundlerRouteExportResolver, createEmptyRouteChunkByExportName, createReactRouterPerformanceProfiler, createRouteId, detectRouteChunksIfEnabled, executeRouteTransformTask, findEntryFile, generateWithProps, getRouteChunkEntryName, getRouteChunkModuleId, getRouteModuleAnalysis, normalizeAssetPrefix, roundMs, routeChunkExportNames, validateRouteChunks };
@@ -1,8 +1,8 @@
1
1
  import type { RsbuildPluginAPI } from '@rsbuild/core';
2
2
  import { getReactRouterManifestForDev, type ReactRouterManifestStats } from './manifest.js';
3
- import type { RouteTransformExecutor } from './parallel-route-transforms.js';
4
3
  import type { ReactRouterPerformanceProfiler } from './performance.js';
5
4
  import type { RouteChunkConfig } from './route-chunks.js';
5
+ import type { RouteTransformRunner } from './route-transform-tasks.js';
6
6
  import type { PluginOptions, Route } from './types.js';
7
7
  type ReactRouterManifest = Awaited<ReturnType<typeof getReactRouterManifestForDev>>;
8
8
  type RegisterBuildOutputTransformsOptions = {
@@ -17,15 +17,16 @@ type RegisterBuildOutputTransformsOptions = {
17
17
  appDirectory: string;
18
18
  getAssetPrefix: () => string;
19
19
  routeChunkOptions: Parameters<typeof getReactRouterManifestForDev>[5];
20
- routeTransformExecutor: RouteTransformExecutor;
20
+ routeTransformRunner: RouteTransformRunner;
21
21
  routeByFilePath: Map<string, Route>;
22
22
  routeChunkConfig: RouteChunkConfig;
23
23
  isBuild: boolean;
24
24
  splitRouteModules: boolean;
25
+ useRouteModuleTransformApi: boolean;
25
26
  ssr: boolean;
26
27
  isSpaMode: boolean;
27
28
  rootRoutePath: string;
28
29
  isDevHmrEnabled?: () => boolean;
29
30
  };
30
- export declare const registerBuildOutputTransforms: ({ api, resolvedServerOutput, performanceProfiler, getLatestServerManifest, getLatestServerManifestByBundleId, routes, pluginOptions, getClientStats, appDirectory, getAssetPrefix, routeChunkOptions, routeTransformExecutor, routeByFilePath, routeChunkConfig, isBuild, splitRouteModules, ssr, isSpaMode, rootRoutePath, isDevHmrEnabled, }: RegisterBuildOutputTransformsOptions) => void;
31
+ export declare const registerBuildOutputTransforms: ({ api, resolvedServerOutput, performanceProfiler, getLatestServerManifest, getLatestServerManifestByBundleId, routes, pluginOptions, getClientStats, appDirectory, getAssetPrefix, routeChunkOptions, routeTransformRunner, routeByFilePath, routeChunkConfig, isBuild, splitRouteModules, useRouteModuleTransformApi, ssr, isSpaMode, rootRoutePath, isDevHmrEnabled, }: RegisterBuildOutputTransformsOptions) => void;
31
32
  export {};
@@ -1,14 +1,12 @@
1
1
  import type { RsbuildPluginAPI } from '@rsbuild/core';
2
2
  import type { RouteConfigEntry } from '@react-router/dev/routes';
3
3
  import { type ReactRouterManifestForDev } from './manifest.js';
4
- import type { RouteTransformExecutor } from './parallel-route-transforms.js';
5
4
  import { type WatchFileConfig } from './route-watch.js';
6
5
  import type { PluginOptions } from './types.js';
7
6
  type RegisterReactRouterDevBackgroundResourcesOptions = {
8
7
  api: RsbuildPluginAPI;
9
8
  isBuild: boolean;
10
9
  lazyCompilationPrewarm: PluginOptions['unstableLazyCompilationPrewarm'];
11
- routeTransformExecutor: RouteTransformExecutor;
12
10
  routeRestartMarkerPath: string;
13
11
  watchDirectory: string;
14
12
  getRouteTopology: () => Promise<Set<string>>;
@@ -34,5 +32,5 @@ export declare const createReactRouterRouteWatchFiles: ({ configWatchPaths, rout
34
32
  routeRestartMarkerPath: string;
35
33
  onRouteTopologyChange: PluginOptions["onRouteTopologyChange"];
36
34
  }) => WatchFileConfig[];
37
- export declare const registerReactRouterDevBackgroundResources: ({ api, isBuild, lazyCompilationPrewarm, routeTransformExecutor, routeRestartMarkerPath, watchDirectory, getRouteTopology, initialRouteTopology, onRouteTopologyChange, }: RegisterReactRouterDevBackgroundResourcesOptions) => ReactRouterDevBackgroundResources;
35
+ export declare const registerReactRouterDevBackgroundResources: ({ api, isBuild, lazyCompilationPrewarm, routeRestartMarkerPath, watchDirectory, getRouteTopology, initialRouteTopology, onRouteTopologyChange, }: RegisterReactRouterDevBackgroundResourcesOptions) => ReactRouterDevBackgroundResources;
38
36
  export {};
@@ -0,0 +1,2 @@
1
+ import type { Rspack } from '@rsbuild/core';
2
+ export declare const ensureFederationAsyncStartup: (rspackConfig: Rspack.Configuration | undefined) => void;