rsbuild-plugin-react-router 0.3.1 → 0.4.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
@@ -98,7 +98,7 @@ async exports before passing the build to React Router's request handler.
98
98
  Put React Router framework settings in `react-router.config.*`:
99
99
 
100
100
  ```ts
101
- import type { Config } from '@react-router/dev/config';
101
+ import type { ReactRouterRsbuildConfig } from 'rsbuild-plugin-react-router';
102
102
 
103
103
  export default {
104
104
  ssr: true,
@@ -107,9 +107,15 @@ export default {
107
107
  basename: '/',
108
108
  splitRouteModules: true,
109
109
  subResourceIntegrity: false,
110
- } satisfies Config;
110
+ } satisfies ReactRouterRsbuildConfig;
111
111
  ```
112
112
 
113
+ Use `ReactRouterRsbuildConfig` for Rsbuild projects so plugin-supported
114
+ configuration such as `splitRouteModules` stays typed. The underlying route
115
+ and config types come from `@react-router/dev`, which framework-mode apps
116
+ already install for `routes.ts` helpers and typegen; it is declared as an
117
+ optional peer dependency.
118
+
113
119
  Commonly used options:
114
120
 
115
121
  | Option | Default | Notes |
@@ -178,7 +184,7 @@ For static sites with multiple pages, you can prerender specific routes at build
178
184
 
179
185
  ```ts
180
186
  // react-router.config.ts
181
- import type { Config } from '@react-router/dev/config';
187
+ import type { ReactRouterRsbuildConfig } from 'rsbuild-plugin-react-router';
182
188
 
183
189
  export default {
184
190
  ssr: false,
@@ -190,7 +196,7 @@ export default {
190
196
  '/docs/advanced',
191
197
  '/projects',
192
198
  ],
193
- } satisfies Config;
199
+ } satisfies ReactRouterRsbuildConfig;
194
200
  ```
195
201
 
196
202
  When `prerender` is specified:
@@ -209,7 +215,7 @@ export default {
209
215
  ssr: false,
210
216
  prerender: ({ getStaticPaths }) =>
211
217
  getStaticPaths().filter(path => path !== '/admin'),
212
- } satisfies Config;
218
+ } satisfies ReactRouterRsbuildConfig;
213
219
  ```
214
220
 
215
221
  Prerendering defaults to one path at a time, matching React Router. Use
@@ -223,7 +229,7 @@ export default {
223
229
  paths: ['/', '/about'],
224
230
  concurrency: 4,
225
231
  },
226
- } satisfies Config;
232
+ } satisfies ReactRouterRsbuildConfig;
227
233
  ```
228
234
 
229
235
  For builds with 256+ routes, detailed file-size reporting is compacted to totals
@@ -584,14 +590,14 @@ export default {
584
590
  ```json
585
591
  {
586
592
  "dependencies": {
587
- "@react-router/node": "^7.1.3",
588
- "@react-router/serve": "^7.1.3",
589
- "react-router": "^7.1.3"
593
+ "@react-router/node": "^7.13.0",
594
+ "@react-router/serve": "^7.13.0",
595
+ "react-router": "^7.13.0"
590
596
  },
591
597
  "devDependencies": {
592
598
  "@cloudflare/workers-types": "^4.20241112.0",
593
- "@react-router/cloudflare": "^7.1.3",
594
- "@react-router/dev": "^7.1.3",
599
+ "@react-router/cloudflare": "^7.13.0",
600
+ "@react-router/dev": "^7.13.0",
595
601
  "wrangler": "^3.106.0"
596
602
  }
597
603
  }
@@ -661,7 +667,23 @@ pnpm bench:synthetic-app -- --profile all --runs 2
661
667
  ```
662
668
 
663
669
  The PR benchmark workflow reports production build, dev route-load, HMR/update,
664
- and embedded synthetic app timings in the same benchmark comment.
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.
665
687
 
666
688
  ## React Router Framework Mode
667
689
 
package/dist/451.js CHANGED
@@ -757,17 +757,67 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
757
757
  }
758
758
  if (unresolvedExportAll.size > 0) throw Error(`[${PLUGIN_NAME}] Client-only module uses \`export * from\` with unresolvable specifier(s): ${Array.from(unresolvedExportAll).map((spec)=>`\`${spec}\``).join(', ')}. Please explicitly re-export named bindings in \`${relative(process.cwd(), resourcePath)}\`.`);
759
759
  return exportNames;
760
- }, createRouteClientEntryArtifact = async ({ code, resourcePath, environmentName, isBuild, routeChunkCache, routeChunkConfig })=>{
760
+ }, HMR_PATCHABLE_ROUTE_FLAGS = [
761
+ 'hasAction',
762
+ 'hasClientAction',
763
+ 'hasClientLoader',
764
+ 'hasClientMiddleware',
765
+ 'hasErrorBoundary',
766
+ 'hasLoader'
767
+ ], HMR_FLAG_EXPORT_NAME = {
768
+ hasAction: SERVER_EXPORTS.action,
769
+ hasClientAction: CLIENT_EXPORTS.clientAction,
770
+ hasClientLoader: CLIENT_EXPORTS.clientLoader,
771
+ hasClientMiddleware: CLIENT_EXPORTS.clientMiddleware,
772
+ hasErrorBoundary: CLIENT_EXPORTS.ErrorBoundary,
773
+ hasLoader: SERVER_EXPORTS.loader
774
+ }, createRouteClientEntryArtifact = async ({ code, resourcePath, environmentName, isBuild, routeChunkCache, routeChunkConfig, routeId, devHmr })=>{
761
775
  let isServer = 'node' === environmentName, routeChunkInfo = !isServer && isBuild && shouldAnalyzeRouteChunks(routeChunkConfig, resourcePath, code) ? await detectRouteChunksIfEnabled(routeChunkCache, routeChunkConfig, resourcePath, code) : null;
762
776
  return {
763
- code: (({ exportNames, chunkedExports, isServer, resourcePath })=>{
764
- let chunkedExportSet = chunkedExports.length > 0 ? new Set(chunkedExports) : void 0, reexports = exportNames.filter((exp)=>!chunkedExportSet?.has(exp) && (CLIENT_ROUTE_EXPORTS_SET.has(exp) || isServer && SERVER_ONLY_ROUTE_EXPORTS_SET.has(exp))).sort(), target = `${resourcePath}?react-router-route`;
765
- return `export { ${reexports.join(', ')} } from ${JSON.stringify(target)};`;
777
+ code: (({ exportNames, chunkedExports, isServer, resourcePath, routeId, devHmr })=>{
778
+ let exports, flags, chunkedExportSet = chunkedExports.length > 0 ? new Set(chunkedExports) : void 0, reexports = exportNames.filter((exp)=>!chunkedExportSet?.has(exp) && (CLIENT_ROUTE_EXPORTS_SET.has(exp) || isServer && SERVER_ONLY_ROUTE_EXPORTS_SET.has(exp))).sort(), target = `${resourcePath}?react-router-route`, reexportCode = `export { ${reexports.join(', ')} } from ${JSON.stringify(target)};`;
779
+ return !devHmr || isServer || void 0 === routeId ? reexportCode : reexportCode + (({ routeId, target, acceptTarget, flags })=>{
780
+ let targetJson = JSON.stringify(target), acceptTargetJson = JSON.stringify(acceptTarget);
781
+ return `
782
+ import * as __rrm from ${targetJson};
783
+ import {
784
+ registerReactRouterRouteExports as __rrr,
785
+ scheduleReactRouterRouteUpdate as __rru,
786
+ } from "virtual/react-router/hmr-runtime";
787
+
788
+ const __rrid = ${JSON.stringify(routeId)};
789
+ const __rrf = ${flags};
790
+ const __rrg = () => __rrm;
791
+ const __rru0 = () => {
792
+ __rrr(__rrid, __rrm);
793
+ __rru(__rrid, __rrf, __rrg);
794
+ };
795
+
796
+ __rrr(__rrid, __rrm);
797
+
798
+ if (import.meta.webpackHot) {
799
+ const __rrh = import.meta.webpackHot;
800
+ __rrh.accept(${acceptTargetJson}, __rru0);
801
+ __rrh.accept();
802
+ __rrh.dispose(data => { data.__rr = true; });
803
+ if (__rrh.data && __rrh.data.__rr) __rru0();
804
+ }
805
+ `;
806
+ })({
807
+ routeId,
808
+ target,
809
+ acceptTarget: `./${basename(resourcePath)}?react-router-route`,
810
+ flags: (exports = new Set(exportNames), flags = 0, HMR_PATCHABLE_ROUTE_FLAGS.forEach((flag, index)=>{
811
+ exports.has(HMR_FLAG_EXPORT_NAME[flag]) && (flags |= 1 << index);
812
+ }), flags)
813
+ });
766
814
  })({
767
815
  exportNames: routeChunkInfo?.exportNames ?? await getExportNames(code, resourcePath),
768
816
  chunkedExports: routeChunkInfo?.chunkedExports ?? [],
769
817
  isServer,
770
- resourcePath
818
+ resourcePath,
819
+ routeId,
820
+ devHmr: devHmr && !isBuild
771
821
  })
772
822
  };
773
823
  }, createRouteChunkArtifact = async ({ code, resource, resourcePath, isBuild, routeChunkCache, routeChunkConfig })=>{
@@ -817,7 +867,50 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
817
867
  }, createClientOnlyStub = async (task)=>({
818
868
  code: Array.from(await collectClientOnlyStubExportNames(task.code, task.resourcePath, task.resolveExportAllModule)).map((name)=>'default' === name ? 'export default undefined;' : `export const ${name} = undefined;`).join('\n'),
819
869
  map: null
820
- }), transformRouteModule = async (task)=>{
870
+ }), callResolvesToComponent = (node)=>{
871
+ let args = node.arguments ?? [];
872
+ if (0 === args.length) return !1;
873
+ let callee = node.callee;
874
+ if (!callee || 'Import' === callee.type) return !1;
875
+ if ('Identifier' === callee.type) {
876
+ let calleeName = callee.name ?? '';
877
+ if (calleeName.startsWith('require') || calleeName.startsWith('import')) return !1;
878
+ } else if ('MemberExpression' !== callee.type) return !1;
879
+ var node1 = args[0];
880
+ switch(node1?.type){
881
+ case 'FunctionExpression':
882
+ return !0;
883
+ case 'ArrowFunctionExpression':
884
+ return node1.body?.type !== 'ArrowFunctionExpression';
885
+ case 'Identifier':
886
+ let name;
887
+ return !!node1.name && (name = node1.name, /^[A-Z]/.test(name));
888
+ case 'CallExpression':
889
+ return callResolvesToComponent(node1);
890
+ default:
891
+ return !1;
892
+ }
893
+ }, collectDeclaredComponentNames = (declaration, names)=>{
894
+ let name, name1;
895
+ if ('FunctionDeclaration' === declaration.type && declaration.id?.name && (name = declaration.id.name, /^[A-Z]/.test(name))) return void names.add(declaration.id.name);
896
+ if ('VariableDeclaration' !== declaration.type) return;
897
+ let declarators = declaration.declarations ?? [];
898
+ if (1 !== declarators.length) return;
899
+ let [declarator] = declarators;
900
+ declarator?.id?.type === 'Identifier' && declarator.id.name && (name1 = declarator.id.name, /^[A-Z]/.test(name1)) && declarator.init && ((init)=>{
901
+ switch(init.type){
902
+ case 'FunctionExpression':
903
+ case 'TaggedTemplateExpression':
904
+ return !0;
905
+ case 'ArrowFunctionExpression':
906
+ return init.body?.type !== 'ArrowFunctionExpression';
907
+ case 'CallExpression':
908
+ return callResolvesToComponent(init);
909
+ default:
910
+ return !1;
911
+ }
912
+ })(declarator.init) && names.add(declarator.id.name);
913
+ }, transformRouteModule = async (task)=>{
821
914
  let code = task.code, defaultExportMatch = code.match(/\n\s{0,}([\w\d_]+)\sas default,?/);
822
915
  defaultExportMatch && 'number' == typeof defaultExportMatch.index && (code = code.slice(0, defaultExportMatch.index) + code.slice(defaultExportMatch.index + defaultExportMatch[0].length) + `\nexport default ${defaultExportMatch[1]};`);
823
916
  let ast = ((code, options = {})=>{
@@ -940,7 +1033,7 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
940
1033
  return !(!declaration || currentlyLive.has(declaration)) && (previouslyLive.has(declaration) || declarationReferencesName(declaration, removedExportReferencedNames, declarationGraph, removedReferenceCache));
941
1034
  }, program.body = program.body.filter((statement)=>'VariableDeclaration' === statement.type ? (statement.declarations = (statement.declarations ?? []).filter((declarator)=>!isRemovableDeadDeclaration(declarator)), statement.declarations.length > 0) : !isRemovableDeadDeclaration(statement))), exportsChanged;
942
1035
  })(ast, SERVER_ONLY_ROUTE_EXPORTS, SERVER_ONLY_ROUTE_EXPORTS_SET);
943
- return ((ast)=>{
1036
+ ((ast)=>{
944
1037
  let program = ast.program ?? ast, usedNames = new Set(), hocs = [], componentWrapperDeclarations = [];
945
1038
  function getUid(name) {
946
1039
  let uid = `_${name}`, index = 2;
@@ -1044,7 +1137,8 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
1044
1137
  for (let statement of [
1045
1138
  ...program.body
1046
1139
  ])'ImportDeclaration' === statement.type && 0 !== (statement.specifiers ?? []).length && (statement.specifiers = (statement.specifiers ?? []).filter((specifier)=>'type' !== specifier.importKind && (!specifier.local?.name || referenced.has(specifier.local.name))), 0 === statement.specifiers.length && removeFromArray(program.body, statement));
1047
- })(ast), ((ast, options = {})=>{
1140
+ })(ast);
1141
+ let result = ((ast, options = {})=>{
1048
1142
  let result = 'program' in ast ? ast : {
1049
1143
  program: ast,
1050
1144
  lineStarts: []
@@ -1072,6 +1166,28 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
1072
1166
  filename: task.resource,
1073
1167
  sourceFileName: task.resourcePath
1074
1168
  });
1169
+ if (task.devHmr && 'web' === task.environmentName && !task.isBuild) {
1170
+ let registrations, unregisteredComponents = ((program)=>{
1171
+ let declared = new Set(), registered = new Set();
1172
+ for (let statement of program.body ?? []){
1173
+ if ('ExportNamedDeclaration' === statement.type && statement.declaration) {
1174
+ collectDeclaredComponentNames(statement.declaration, declared);
1175
+ continue;
1176
+ }
1177
+ if ('ExpressionStatement' === statement.type && statement.expression?.type === 'CallExpression' && statement.expression.callee?.type === 'Identifier' && '$RefreshReg$' === statement.expression.callee.name) {
1178
+ let nameArgument = statement.expression.arguments?.[1];
1179
+ 'string' == typeof nameArgument?.value && registered.add(nameArgument.value);
1180
+ continue;
1181
+ }
1182
+ collectDeclaredComponentNames(statement, declared);
1183
+ }
1184
+ return [
1185
+ ...declared
1186
+ ].filter((name)=>!registered.has(name));
1187
+ })(ast.program ?? ast);
1188
+ unregisteredComponents.length > 0 && (result.code += (registrations = unregisteredComponents.map((name)=>` if (typeof ${name} === 'function' || (typeof ${name} === 'object' && ${name} !== null)) $RefreshReg$(${name}, ${JSON.stringify(name)});`).join('\n'), `\nif (typeof $RefreshReg$ === 'function') {\n${registrations}\n}\n`));
1189
+ }
1190
+ return result;
1075
1191
  }, executeRouteTransformTask = async (task, options)=>{
1076
1192
  switch(task.kind){
1077
1193
  case 'routeClientEntry':
@@ -1081,7 +1197,9 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
1081
1197
  environmentName: task.environmentName,
1082
1198
  isBuild: task.isBuild,
1083
1199
  routeChunkCache: getRouteChunkCache(options),
1084
- routeChunkConfig: task.routeChunkConfig
1200
+ routeChunkConfig: task.routeChunkConfig,
1201
+ routeId: task.routeId,
1202
+ devHmr: task.devHmr
1085
1203
  });
1086
1204
  case 'routeChunk':
1087
1205
  return createRouteChunkArtifact({
@@ -1100,4 +1218,4 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
1100
1218
  return transformRouteModule(task);
1101
1219
  }
1102
1220
  };
1103
- export { BUILD_CLIENT_ROUTE_QUERY_STRING, CLIENT_EXPORTS, JS_EXTENSIONS, PLUGIN_NAME, SERVER_EXPORTS, buildManifestChunkValidity, combineURLs, createBundlerRouteExportResolver, createEmptyRouteChunkByExportName, createRouteId, detectRouteChunksIfEnabled, executeRouteTransformTask, findEntryFile, generateWithProps, getRouteChunkEntryName, getRouteChunkModuleId, getRouteModuleAnalysis, normalizeAssetPrefix, routeChunkExportNames, setBoundedCacheEntry, validateRouteChunks };
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 };
@@ -25,6 +25,7 @@ type RegisterBuildOutputTransformsOptions = {
25
25
  ssr: boolean;
26
26
  isSpaMode: boolean;
27
27
  rootRoutePath: string;
28
+ isDevHmrEnabled?: () => boolean;
28
29
  };
29
- export declare const registerBuildOutputTransforms: ({ api, resolvedServerOutput, performanceProfiler, getLatestServerManifest, getLatestServerManifestByBundleId, routes, pluginOptions, getClientStats, appDirectory, getAssetPrefix, routeChunkOptions, routeTransformExecutor, routeByFilePath, routeChunkConfig, isBuild, splitRouteModules, ssr, isSpaMode, rootRoutePath, }: RegisterBuildOutputTransformsOptions) => void;
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;
30
31
  export {};
@@ -0,0 +1,45 @@
1
+ import type { Rspack } from '@rsbuild/core';
2
+ export declare const DEV_HMR_RUNTIME_MODULE_ID = "virtual/react-router/hmr-runtime";
3
+ export declare const isRspackSwcReactRefreshEnabled: (rspackConfig: Rspack.Configuration) => boolean;
4
+ /**
5
+ * Resolves the `react-refresh/runtime` module that
6
+ * `@rspack/plugin-react-refresh` injects into the web bundle. The resolution
7
+ * walks the same dependency chain the refresh plugin uses so the returned file
8
+ * is the exact runtime instance already present in the browser module graph.
9
+ * Returns `undefined` when React Fast Refresh is unavailable, in which case
10
+ * dev HMR falls back to full reloads.
11
+ */
12
+ export declare const resolveReactRefreshRuntimePath: (rootPath: string) => string | undefined;
13
+ /**
14
+ * The HDR revision module is a real file (not a virtual module) because it
15
+ * must wake the web compiler through the regular file watcher: the browser
16
+ * HMR runtime imports it, so bumping the revision produces a web hot update
17
+ * whenever server code changes, which the client answers by revalidating
18
+ * React Router loader data.
19
+ */
20
+ export declare const DEV_HDR_REVISION_RELATIVE_PATH = ".react-router/hdr-revision.mjs";
21
+ export declare const getDevHdrRevisionFilePath: (rootPath: string) => string;
22
+ export type DevHdrRevisionSignal = {
23
+ /** Writes the initial revision module so the first compile can resolve it. */
24
+ ensure: () => void;
25
+ /** Increments the revision, signaling hot data revalidation to the client. */
26
+ bump: () => void;
27
+ };
28
+ export declare const createDevHdrRevisionSignal: ({ filePath, onError, }: {
29
+ filePath: string;
30
+ onError?: (error: Error) => void;
31
+ }) => DevHdrRevisionSignal;
32
+ /**
33
+ * Browser-side HMR runtime shared by all route client entries in development.
34
+ *
35
+ * This mirrors React Router's Vite HMR contract (see `refresh-utils.mjs` in
36
+ * `@react-router/dev`): route module updates are applied by patching
37
+ * `window.__reactRouterRouteModules` while preserving the previous component
38
+ * identities (React Fast Refresh swaps their implementations in place),
39
+ * recreating the client routes with revalidation opt-out, revalidating loader
40
+ * data, and finally performing a React refresh.
41
+ */
42
+ export declare const generateDevHmrRuntimeModule: ({ reactRefreshRuntimePath, hdrRevisionFilePath, }: {
43
+ reactRefreshRuntimePath: string;
44
+ hdrRevisionFilePath: string;
45
+ }) => string;
@@ -9,6 +9,11 @@ type CreateControllerOptions = {
9
9
  api: RsbuildPluginAPI;
10
10
  isBuild: boolean;
11
11
  buildPlan: ReactRouterDevBuildPlan;
12
+ /**
13
+ * Invoked after a development attempt commits a re-evaluated node build for
14
+ * changed server files. Used to signal hot data revalidation to the client.
15
+ */
16
+ onNodeRebuildCommitted?: () => void;
12
17
  };
13
- export declare const createReactRouterDevRuntimeController: ({ api, isBuild, buildPlan, }: CreateControllerOptions) => ReactRouterDevRuntimeController;
18
+ export declare const createReactRouterDevRuntimeController: ({ api, isBuild, buildPlan, onNodeRebuildCommitted, }: CreateControllerOptions) => ReactRouterDevRuntimeController;
14
19
  export {};