rsbuild-plugin-react-router 0.3.0 → 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.
Files changed (64) hide show
  1. package/README.md +88 -35
  2. package/dist/451.js +164 -30
  3. package/dist/build-manifest.d.ts +6 -3
  4. package/dist/build-output-transforms.d.ts +2 -1
  5. package/dist/concurrency.d.ts +2 -1
  6. package/dist/config-imports.d.ts +7 -0
  7. package/dist/dev-background-resources.d.ts +38 -0
  8. package/dist/dev-generation.d.ts +2 -2
  9. package/dist/dev-hmr.d.ts +45 -0
  10. package/dist/dev-runtime-artifacts.d.ts +9 -1
  11. package/dist/dev-runtime-compilation.d.ts +17 -2
  12. package/dist/dev-runtime-controller.d.ts +6 -1
  13. package/dist/dev-server.d.ts +5 -0
  14. package/dist/effect-runtime.d.ts +18 -0
  15. package/dist/export-utils.d.ts +2 -2
  16. package/dist/index.cjs +11852 -848
  17. package/dist/index.d.ts +5 -0
  18. package/dist/index.js +8289 -773
  19. package/dist/lazy-compilation-prewarm.d.ts +25 -0
  20. package/dist/lazy-compilation.d.ts +2 -1
  21. package/dist/manifest.d.ts +19 -4
  22. package/dist/parallel-route-transforms.d.ts +17 -2
  23. package/dist/prerender-build.d.ts +4 -0
  24. package/dist/prerender.d.ts +0 -1
  25. package/dist/react-router-config.d.ts +8 -5
  26. package/dist/route-artifacts.d.ts +10 -2
  27. package/dist/route-transform-tasks.d.ts +3 -0
  28. package/dist/server-build-resolution.d.ts +3 -0
  29. package/dist/ssr-externals.d.ts +1 -0
  30. package/dist/typegen.d.ts +14 -1
  31. package/dist/types.d.ts +15 -6
  32. package/package.json +18 -10
  33. package/src/build-manifest.ts +110 -73
  34. package/src/build-output-transforms.ts +5 -0
  35. package/src/concurrency.ts +3 -22
  36. package/src/config-imports.ts +38 -0
  37. package/src/dev-background-resources.ts +255 -0
  38. package/src/dev-generation.ts +43 -53
  39. package/src/dev-hmr.ts +431 -0
  40. package/src/dev-runtime-artifacts.ts +50 -18
  41. package/src/dev-runtime-compilation.ts +80 -1
  42. package/src/dev-runtime-controller.ts +155 -31
  43. package/src/dev-runtime-session.ts +18 -11
  44. package/src/dev-server.ts +31 -1
  45. package/src/effect-runtime.ts +130 -0
  46. package/src/export-utils.ts +82 -23
  47. package/src/index.ts +166 -153
  48. package/src/lazy-compilation-prewarm.ts +279 -0
  49. package/src/lazy-compilation.ts +12 -5
  50. package/src/manifest.ts +366 -255
  51. package/src/modify-browser-manifest.ts +2 -1
  52. package/src/parallel-route-transforms.ts +195 -69
  53. package/src/prerender-build.ts +147 -63
  54. package/src/prerender.ts +1 -19
  55. package/src/react-router-config.ts +98 -73
  56. package/src/route-artifacts.ts +122 -3
  57. package/src/route-export-resolution.ts +3 -3
  58. package/src/route-transform-tasks.ts +173 -2
  59. package/src/route-watch.ts +119 -84
  60. package/src/server-build-resolution.ts +131 -0
  61. package/src/server-utils.ts +7 -106
  62. package/src/ssr-externals.ts +1 -1
  63. package/src/typegen.ts +162 -33
  64. package/src/types.ts +16 -6
package/README.md CHANGED
@@ -70,24 +70,23 @@ plugin only needs options for Rsbuild-specific behavior.
70
70
  ```ts
71
71
  pluginReactRouter({
72
72
  customServer: false,
73
- serverOutput: 'module',
74
- lazyCompilation: undefined,
73
+ lazyCompilation: true,
74
+ unstableLazyCompilationPrewarm: false,
75
75
  logPerformance: false,
76
- parallelRouteTransform: undefined,
77
- onRouteTopologyChange: undefined,
78
76
  federation: false,
79
77
  });
80
78
  ```
81
79
 
82
- | Option | Default | Description |
83
- | ------------------------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
- | `customServer` | `false` | Disables the built-in development SSR middleware. Enable this when an app owns the server with `createDevServer()` or an adapter. |
85
- | `serverOutput` | `'module'` | Emitted Rsbuild server format: `'module'` or `'commonjs'`. When omitted, React Router's `serverModuleFormat` selects the format (`'esm'` -> `'module'`, `'cjs'` -> `'commonjs'`); setting `serverOutput` overrides it. |
86
- | `lazyCompilation` | `undefined` | 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. |
87
- | `logPerformance` | `false` | Logs structured React Router plugin timing information through the Rsbuild logger. |
88
- | `parallelRouteTransform` | `undefined` | Controls worker-thread route transforms. `undefined` auto-enables workers for 256+ routes, `true` forces the default worker count, a positive integer sets the worker count, and `false` keeps transforms inline. |
89
- | `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. |
90
- | `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 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. |
91
90
 
92
91
  When `federation` is enabled, configure the Module Federation plugin with
93
92
  `experiments.asyncStartup: true`. The dev server resolves async server build
@@ -99,7 +98,7 @@ async exports before passing the build to React Router's request handler.
99
98
  Put React Router framework settings in `react-router.config.*`:
100
99
 
101
100
  ```ts
102
- import type { Config } from '@react-router/dev/config';
101
+ import type { ReactRouterRsbuildConfig } from 'rsbuild-plugin-react-router';
103
102
 
104
103
  export default {
105
104
  ssr: true,
@@ -108,9 +107,15 @@ export default {
108
107
  basename: '/',
109
108
  splitRouteModules: true,
110
109
  subResourceIntegrity: false,
111
- } satisfies Config;
110
+ } satisfies ReactRouterRsbuildConfig;
112
111
  ```
113
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
+
114
119
  Commonly used options:
115
120
 
116
121
  | Option | Default | Notes |
@@ -179,7 +184,7 @@ For static sites with multiple pages, you can prerender specific routes at build
179
184
 
180
185
  ```ts
181
186
  // react-router.config.ts
182
- import type { Config } from '@react-router/dev/config';
187
+ import type { ReactRouterRsbuildConfig } from 'rsbuild-plugin-react-router';
183
188
 
184
189
  export default {
185
190
  ssr: false,
@@ -191,7 +196,7 @@ export default {
191
196
  '/docs/advanced',
192
197
  '/projects',
193
198
  ],
194
- } satisfies Config;
199
+ } satisfies ReactRouterRsbuildConfig;
195
200
  ```
196
201
 
197
202
  When `prerender` is specified:
@@ -210,7 +215,7 @@ export default {
210
215
  ssr: false,
211
216
  prerender: ({ getStaticPaths }) =>
212
217
  getStaticPaths().filter(path => path !== '/admin'),
213
- } satisfies Config;
218
+ } satisfies ReactRouterRsbuildConfig;
214
219
  ```
215
220
 
216
221
  Prerendering defaults to one path at a time, matching React Router. Use
@@ -224,7 +229,7 @@ export default {
224
229
  paths: ['/', '/about'],
225
230
  concurrency: 4,
226
231
  },
227
- } satisfies Config;
232
+ } satisfies ReactRouterRsbuildConfig;
228
233
  ```
229
234
 
230
235
  For builds with 256+ routes, detailed file-size reporting is compacted to totals
@@ -235,6 +240,20 @@ Route transform source maps are generated in development only. If you enable
235
240
  Rsbuild source maps for faster local debugging, prefer a cheap JS map:
236
241
  `output.sourceMap: { js: 'cheap-module-source-map', css: false }`.
237
242
 
243
+ Lazy compilation prewarming is disabled by default. When enabled alongside
244
+ `lazyCompilation`, the plugin fetches emitted browser entry and route JS assets,
245
+ extracts activation keys from Rspack's generated lazy-compilation client calls,
246
+ and POSTs those keys to Rspack's configured lazy trigger endpoint after dev
247
+ compiles. It does not request application routes or run route loaders. Because
248
+ the key extraction depends on Rspack's generated client code shape, opt in with
249
+ `unstableLazyCompilationPrewarm: true`.
250
+
251
+ Subresource Integrity is disabled by default. Enable it with
252
+ `subResourceIntegrity: true` in `react-router.config.*` when the deployed app
253
+ should emit integrity metadata for browser scripts. The legacy
254
+ `future.unstable_subResourceIntegrity` flag is still accepted and is normalized
255
+ to the stable option.
256
+
238
257
  ### Route Configuration
239
258
 
240
259
  Routes can be defined in `app/routes.ts` using the helper functions from `@react-router/dev/routes`:
@@ -339,6 +358,11 @@ export default defineConfig(() => {
339
358
  plugins: [
340
359
  pluginReactRouter({
341
360
  customServer: true,
361
+ onRouteTopologyChange() {
362
+ console.warn('Route topology changed; restart the dev server.');
363
+ process.exitCode = 75;
364
+ setTimeout(() => process.exit(75), 0);
365
+ },
342
366
  }),
343
367
  pluginReact(),
344
368
  ],
@@ -346,17 +370,14 @@ export default defineConfig(() => {
346
370
  });
347
371
  ```
348
372
 
349
- If the server is created programmatically with `createDevServer()`, pass
350
- `onRouteTopologyChange` and use it to recreate that server. Rsbuild's
351
- `reload-server` watcher is owned by the CLI and is not installed by the
352
- programmatic API. The callback is a notification and is not awaited, so it can
353
- safely start a serialized replacement task. Always `await` the active server's
354
- `close()` before calling `createDevServer()` again; the plugin rejects overlapping
355
- or out-of-order replacement instead of closing one server from inside another
356
- server's startup hooks. If startup fails before returning a server, or if
357
- `close()` rejects, restart the process before retrying unless you can externally
358
- prove and force complete teardown; a fresh Rsbuild instance alone is not
359
- sufficient. Do not launch concurrent `createDevServer()` calls.
373
+ Rsbuild's `reload-server` watcher is owned by the CLI and is not installed by
374
+ the programmatic `createDevServer()` API. The sample below therefore treats
375
+ route topology changes as a full process restart: do not call `startServer()`
376
+ again inside the same process or mount a second dev server on the same Express
377
+ app. If you implement in-process replacement instead, route requests through
378
+ replaceable middleware and request-handler delegates, always `await` the active
379
+ server's `close()` before calling `createDevServer()` again, and do not launch
380
+ concurrent replacements.
360
381
 
361
382
  Create one server entry point (`server.js`) and let it own the React Router
362
383
  request handler in both development and production. Only the build provider
@@ -569,14 +590,14 @@ export default {
569
590
  ```json
570
591
  {
571
592
  "dependencies": {
572
- "@react-router/node": "^7.1.3",
573
- "@react-router/serve": "^7.1.3",
574
- "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"
575
596
  },
576
597
  "devDependencies": {
577
598
  "@cloudflare/workers-types": "^4.20241112.0",
578
- "@react-router/cloudflare": "^7.1.3",
579
- "@react-router/dev": "^7.1.3",
599
+ "@react-router/cloudflare": "^7.13.0",
600
+ "@react-router/dev": "^7.13.0",
580
601
  "wrangler": "^3.106.0"
581
602
  }
582
603
  }
@@ -632,6 +653,38 @@ The plugin automatically:
632
653
  - Handles route-based code splitting
633
654
  - Manages client and server builds
634
655
 
656
+ ### Benchmarking
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.
663
+
664
+ ```bash
665
+ pnpm bench:large
666
+ pnpm bench:synthetic-app -- --profile all --runs 2
667
+ ```
668
+
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.
687
+
635
688
  ## React Router Framework Mode
636
689
 
637
690
  React Router "Framework Mode" wraps Data Mode using a Vite plugin. This Rsbuild
package/dist/451.js CHANGED
@@ -4,6 +4,7 @@ import { langFromPath, parse, walk as external_yuku_parser_walk } from "yuku-par
4
4
  import { Analyzer } from "yuku-analyzer";
5
5
  import { print } from "yuku-codegen";
6
6
  import { readFile, stat } from "node:fs/promises";
7
+ import { rspack } from "@rsbuild/core";
7
8
  import { createRequire } from "node:module";
8
9
  let PLUGIN_NAME = 'rsbuild:react-router', JS_EXTENSIONS = [
9
10
  '.tsx',
@@ -557,16 +558,30 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
557
558
  oldestEntry.done || cache.delete(oldestEntry.value);
558
559
  }
559
560
  cache.set(key, value);
560
- }, exportInfoCache = new Map(), routeModuleAnalysisCache = new Map(), cachePromiseOnReject = (promise, invalidate)=>promise.catch((error)=>{
561
- throw invalidate(), error;
562
- }), parseProgram = (code, resourcePath)=>{
563
- let result = parse(code, {
561
+ }, exportInfoCache = new Map(), routeModuleAnalysisCache = new Map(), getParseErrors = (result)=>result.diagnostics.filter((diagnostic)=>'error' === diagnostic.severity), getParseErrorMessage = (errors)=>errors.map((error)=>error.message).join('\n'), parseProgram = (code, resourcePath)=>{
562
+ let sourcePath = resourcePath ? resourcePath.replace(/[?#].*$/, '') : void 0, lang = sourcePath ? langFromPath(sourcePath) : 'tsx', result = parse(code, {
563
+ sourceType: 'module',
564
+ lang
565
+ }), errors = getParseErrors(result);
566
+ if (0 === errors.length) return result.program ?? result;
567
+ if (!sourcePath || 'ts' !== lang && 'tsx' !== lang) throw Error(getParseErrorMessage(errors));
568
+ let normalizedResult = parse(rspack.experiments.swc.transformSync(code, {
569
+ filename: sourcePath,
570
+ jsc: {
571
+ parser: {
572
+ syntax: "typescript",
573
+ tsx: 'tsx' === lang
574
+ }
575
+ }
576
+ }).code, {
564
577
  sourceType: 'module',
565
- lang: resourcePath ? langFromPath(resourcePath) : 'tsx'
566
- }), errors = result.diagnostics.filter((diagnostic)=>'error' === diagnostic.severity);
567
- if (errors.length > 0) throw Error(errors.map((error)=>error.message).join('\n'));
568
- return result.program ?? result;
569
- }, isTypeOnlyExport = (node)=>'type' === node.exportKind || 'TSExportAssignment' === node.type || node.declaration?.declare === !0 || 'ExportDefaultDeclaration' === node.type && node.declaration?.type === 'TSInterfaceDeclaration', collectProgramExportNames = (program)=>{
578
+ lang: 'js'
579
+ }), normalizedErrors = getParseErrors(normalizedResult);
580
+ if (normalizedErrors.length > 0) throw Error(getParseErrorMessage(normalizedErrors));
581
+ return normalizedResult.program ?? normalizedResult;
582
+ }, cachePromiseOnReject = (promise, invalidate)=>promise.catch((error)=>{
583
+ throw invalidate(), error;
584
+ }), isTypeOnlyExport = (node)=>'type' === node.exportKind || 'TSExportAssignment' === node.type || node.declaration?.declare === !0 || 'ExportDefaultDeclaration' === node.type && node.declaration?.type === 'TSInterfaceDeclaration', collectProgramExportNames = (program)=>{
570
585
  let exportNames = new Set();
571
586
  for (let statement of program.body ?? []){
572
587
  if (isTypeOnlyExport(statement)) continue;
@@ -601,17 +616,18 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
601
616
  'string' == typeof source && modules.push(source);
602
617
  }
603
618
  return modules;
604
- }, getExportNames = async (code)=>(await getExportNamesAndExportAll(code)).exportNames, getExportNamesAndExportAll = async (code)=>{
605
- let trackedExportInfo, cached = exportInfoCache.get(code);
619
+ }, getExportNames = async (code, resourcePath)=>(await getExportNamesAndExportAll(code, resourcePath)).exportNames, getExportNamesAndExportAll = async (code, resourcePath)=>{
620
+ var code1, resourcePath1;
621
+ let lang, trackedExportInfo, cacheKey = (code1 = code, lang = (resourcePath1 = resourcePath) ? langFromPath(resourcePath1.replace(/[?#].*$/, '')) : 'inline', `${lang}\0${code1}`), cached = exportInfoCache.get(cacheKey);
606
622
  return cached || (trackedExportInfo = cachePromiseOnReject((async ()=>{
607
- let program = parseProgram(code);
623
+ let program = parseProgram(code, resourcePath);
608
624
  return {
609
625
  exportNames: collectProgramExportNames(program),
610
626
  exportAllModules: collectExportAllModules(program)
611
627
  };
612
628
  })(), ()=>{
613
- exportInfoCache.get(code) === trackedExportInfo && exportInfoCache.delete(code);
614
- }), setBoundedCacheEntry(exportInfoCache, code, trackedExportInfo, 2048), trackedExportInfo);
629
+ exportInfoCache.get(cacheKey) === trackedExportInfo && exportInfoCache.delete(cacheKey);
630
+ }), setBoundedCacheEntry(exportInfoCache, cacheKey, trackedExportInfo, 2048), trackedExportInfo);
615
631
  }, getRouteModuleAnalysis = async (resourcePath)=>{
616
632
  let trackedAnalysis, stats = await stat(resourcePath), cached = routeModuleAnalysisCache.get(resourcePath);
617
633
  return cached?.mtimeMs === stats.mtimeMs && cached.size === stats.size ? cached.analysis : (trackedAnalysis = cachePromiseOnReject((async ()=>{
@@ -712,12 +728,12 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
712
728
  } catch {
713
729
  return null;
714
730
  }
715
- }, createBundlerRouteExportResolver = (resolveModule)=>(specifier, importerPath)=>new Promise((resolveResolvedPath)=>{
731
+ }, createBundlerRouteExportResolver = (resolveModule)=>(specifier, importerPath)=>new Promise((resolvePromise)=>{
716
732
  resolveModule(dirname(importerPath), specifier, (error, resolved)=>{
717
- resolveResolvedPath(error || !resolved ? null : resolved);
733
+ resolvePromise(error || !resolved ? null : resolved);
718
734
  });
719
735
  }), collectClientOnlyStubExportNames = async (code, resourcePath, resolveModule = resolveExportAllModule)=>{
720
- let { exportNames: directExportNames, exportAllModules } = await getExportNamesAndExportAll(code), exportNames = new Set(directExportNames), unresolvedExportAll = new Set(), visitedModules = new Set(), collectExportNamesFromModule = async (modulePath)=>{
736
+ let { exportNames: directExportNames, exportAllModules } = await getExportNamesAndExportAll(code, resourcePath), exportNames = new Set(directExportNames), unresolvedExportAll = new Set(), visitedModules = new Set(), collectExportNamesFromModule = async (modulePath)=>{
721
737
  if (visitedModules.has(modulePath)) return;
722
738
  visitedModules.add(modulePath);
723
739
  let { exports: moduleExportNames, exportAllModules: moduleExportAll } = await getRouteModuleAnalysis(modulePath);
@@ -741,17 +757,67 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
741
757
  }
742
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)}\`.`);
743
759
  return exportNames;
744
- }, 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 })=>{
745
775
  let isServer = 'node' === environmentName, routeChunkInfo = !isServer && isBuild && shouldAnalyzeRouteChunks(routeChunkConfig, resourcePath, code) ? await detectRouteChunksIfEnabled(routeChunkCache, routeChunkConfig, resourcePath, code) : null;
746
776
  return {
747
- code: (({ exportNames, chunkedExports, isServer, resourcePath })=>{
748
- 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`;
749
- 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
+ });
750
814
  })({
751
- exportNames: routeChunkInfo?.exportNames ?? await getExportNames(code),
815
+ exportNames: routeChunkInfo?.exportNames ?? await getExportNames(code, resourcePath),
752
816
  chunkedExports: routeChunkInfo?.chunkedExports ?? [],
753
817
  isServer,
754
- resourcePath
818
+ resourcePath,
819
+ routeId,
820
+ devHmr: devHmr && !isBuild
755
821
  })
756
822
  };
757
823
  }, createRouteChunkArtifact = async ({ code, resource, resourcePath, isBuild, routeChunkCache, routeChunkConfig })=>{
@@ -773,11 +839,11 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
773
839
  };
774
840
  let chunk = await getRouteChunkIfEnabled(routeChunkCache, routeChunkConfig, resourcePath, chunkName, code);
775
841
  if ('enforce' === splitRouteModules && 'main' === chunkName && chunk) {
776
- let exportNameSet;
842
+ let exportNameSet, exportNames = await getExportNames(chunk, resourcePath);
777
843
  validateRouteChunks({
778
844
  config: routeChunkConfig,
779
845
  id: resourcePath,
780
- valid: (exportNameSet = new Set(await getExportNames(chunk)), createRouteChunkExportMap((exportName)=>!exportNameSet.has(exportName)))
846
+ valid: (exportNameSet = new Set(exportNames), createRouteChunkExportMap((exportName)=>!exportNameSet.has(exportName)))
781
847
  });
782
848
  }
783
849
  return {
@@ -801,7 +867,50 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
801
867
  }, createClientOnlyStub = async (task)=>({
802
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'),
803
869
  map: null
804
- }), 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)=>{
805
914
  let code = task.code, defaultExportMatch = code.match(/\n\s{0,}([\w\d_]+)\sas default,?/);
806
915
  defaultExportMatch && 'number' == typeof defaultExportMatch.index && (code = code.slice(0, defaultExportMatch.index) + code.slice(defaultExportMatch.index + defaultExportMatch[0].length) + `\nexport default ${defaultExportMatch[1]};`);
807
916
  let ast = ((code, options = {})=>{
@@ -924,7 +1033,7 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
924
1033
  return !(!declaration || currentlyLive.has(declaration)) && (previouslyLive.has(declaration) || declarationReferencesName(declaration, removedExportReferencedNames, declarationGraph, removedReferenceCache));
925
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;
926
1035
  })(ast, SERVER_ONLY_ROUTE_EXPORTS, SERVER_ONLY_ROUTE_EXPORTS_SET);
927
- return ((ast)=>{
1036
+ ((ast)=>{
928
1037
  let program = ast.program ?? ast, usedNames = new Set(), hocs = [], componentWrapperDeclarations = [];
929
1038
  function getUid(name) {
930
1039
  let uid = `_${name}`, index = 2;
@@ -1028,7 +1137,8 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
1028
1137
  for (let statement of [
1029
1138
  ...program.body
1030
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));
1031
- })(ast), ((ast, options = {})=>{
1140
+ })(ast);
1141
+ let result = ((ast, options = {})=>{
1032
1142
  let result = 'program' in ast ? ast : {
1033
1143
  program: ast,
1034
1144
  lineStarts: []
@@ -1056,6 +1166,28 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
1056
1166
  filename: task.resource,
1057
1167
  sourceFileName: task.resourcePath
1058
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;
1059
1191
  }, executeRouteTransformTask = async (task, options)=>{
1060
1192
  switch(task.kind){
1061
1193
  case 'routeClientEntry':
@@ -1065,7 +1197,9 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
1065
1197
  environmentName: task.environmentName,
1066
1198
  isBuild: task.isBuild,
1067
1199
  routeChunkCache: getRouteChunkCache(options),
1068
- routeChunkConfig: task.routeChunkConfig
1200
+ routeChunkConfig: task.routeChunkConfig,
1201
+ routeId: task.routeId,
1202
+ devHmr: task.devHmr
1069
1203
  });
1070
1204
  case 'routeChunk':
1071
1205
  return createRouteChunkArtifact({
@@ -1084,4 +1218,4 @@ let mightContainRouteChunkExportName = (source)=>routeChunkExportNames.some((exp
1084
1218
  return transformRouteModule(task);
1085
1219
  }
1086
1220
  };
1087
- 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 };
@@ -1,3 +1,4 @@
1
+ import * as Effect from 'effect/Effect';
1
2
  import type { Config } from './react-router-config.js';
2
3
  import type { Route } from './types.js';
3
4
  type BuildManifest = {
@@ -10,10 +11,12 @@ type BuildManifest = {
10
11
  }>;
11
12
  routeIdToServerBundleId: Record<string, string>;
12
13
  };
13
- export declare const getBuildManifest: ({ reactRouterConfig, routes, rootDirectory, }: {
14
- reactRouterConfig: Required<Pick<Config, "appDirectory" | "buildDirectory" | "serverBuildFile" | "future">> & Pick<Config, "serverBundles">;
14
+ type GetBuildManifestOptions = {
15
+ reactRouterConfig: Required<Pick<Config, 'appDirectory' | 'buildDirectory' | 'serverBuildFile' | 'future'>> & Pick<Config, 'serverBundles'>;
15
16
  routes: Record<string, Route>;
16
17
  rootDirectory: string;
17
- }) => Promise<BuildManifest | undefined>;
18
+ };
19
+ export declare const getBuildManifestEffect: ({ reactRouterConfig, routes, rootDirectory, }: GetBuildManifestOptions) => Effect.Effect<BuildManifest | undefined, Error, never>;
20
+ export declare const getBuildManifest: (options: GetBuildManifestOptions) => Promise<BuildManifest | undefined>;
18
21
  export declare const getRoutesByServerBundleId: (buildManifest: BuildManifest | undefined, sourceRoutes: Record<string, Route>) => Record<string, Record<string, Route>>;
19
22
  export {};
@@ -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 {};
@@ -1,2 +1,3 @@
1
+ export declare const getAvailableCpuCount: () => number;
1
2
  export declare const getDefaultConcurrency: (cpuCount?: number) => number;
2
- export declare const mapWithConcurrency: <Item, Result>(items: readonly Item[], concurrency: number, worker: (item: Item, index: number) => Promise<Result>) => Promise<Result[]>;
3
+ export declare const getCappedPluginConcurrency: (cap?: number) => number;
@@ -1,3 +1,10 @@
1
1
  import type { ModuleCache } from 'jiti';
2
+ import { createJiti } from 'jiti';
3
+ type ConfigImporter = Pick<ReturnType<typeof createJiti>, 'import'>;
2
4
  export declare const collectConfigImportWatchPaths: (configPath: string, moduleCache: ModuleCache, previousCacheKeys: ReadonlySet<string>) => string[];
3
5
  export declare const clearConfigImportCache: (moduleCache: ModuleCache, filePaths: readonly string[]) => void;
6
+ export declare const importConfigWithWatchPaths: <T>(configPath: string, load?: (importer: ConfigImporter) => PromiseLike<T> | T) => Promise<{
7
+ value: Awaited<T>;
8
+ watchPaths: string | string[];
9
+ }>;
10
+ export {};
@@ -0,0 +1,38 @@
1
+ import type { RsbuildPluginAPI } from '@rsbuild/core';
2
+ import type { RouteConfigEntry } from '@react-router/dev/routes';
3
+ import { type ReactRouterManifestForDev } from './manifest.js';
4
+ import type { RouteTransformExecutor } from './parallel-route-transforms.js';
5
+ import { type WatchFileConfig } from './route-watch.js';
6
+ import type { PluginOptions } from './types.js';
7
+ type RegisterReactRouterDevBackgroundResourcesOptions = {
8
+ api: RsbuildPluginAPI;
9
+ isBuild: boolean;
10
+ lazyCompilationPrewarm: PluginOptions['unstableLazyCompilationPrewarm'];
11
+ routeTransformExecutor: RouteTransformExecutor;
12
+ routeRestartMarkerPath: string;
13
+ watchDirectory: string;
14
+ getRouteTopology: () => Promise<Set<string>>;
15
+ initialRouteTopology: Set<string>;
16
+ onRouteTopologyChange: PluginOptions['onRouteTopologyChange'];
17
+ };
18
+ type ReactRouterDevBackgroundResources = {
19
+ setManifest(manifest: ReactRouterManifestForDev): void;
20
+ };
21
+ export declare const createReactRouterRouteTopology: ({ appDirectory, rootRouteFile, routeConfig, loadRouteConfig, getRootRoutePath, }: {
22
+ appDirectory: string;
23
+ rootRouteFile: string;
24
+ routeConfig: RouteConfigEntry[];
25
+ loadRouteConfig: () => Promise<RouteConfigEntry[]>;
26
+ getRootRoutePath: () => string;
27
+ }) => {
28
+ initialRouteTopology: Set<string>;
29
+ getRouteTopology: () => Promise<Set<string>>;
30
+ };
31
+ export declare const createReactRouterRouteWatchFiles: ({ configWatchPaths, routeConfigWatchPaths, routeRestartMarkerPath, onRouteTopologyChange, }: {
32
+ configWatchPaths: string | string[];
33
+ routeConfigWatchPaths: string | string[];
34
+ routeRestartMarkerPath: string;
35
+ onRouteTopologyChange: PluginOptions["onRouteTopologyChange"];
36
+ }) => WatchFileConfig[];
37
+ export declare const registerReactRouterDevBackgroundResources: ({ api, isBuild, lazyCompilationPrewarm, routeTransformExecutor, routeRestartMarkerPath, watchDirectory, getRouteTopology, initialRouteTopology, onRouteTopologyChange, }: RegisterReactRouterDevBackgroundResourcesOptions) => ReactRouterDevBackgroundResources;
38
+ export {};
@@ -1,12 +1,12 @@
1
1
  import type { RsbuildDevServer, Rspack } from '@rsbuild/core';
2
2
  import type { ServerBuild } from 'react-router';
3
- import { type DevGraphChanges, type DevGraphIdentity, type ReactRouterDevBuildPlan, type ReactRouterDevManifestSet } from './dev-runtime-artifacts.js';
3
+ import { type DevGraphChanges, type DevGraphIdentity, type DevRuntimeStats, type ReactRouterDevBuildPlan, type ReactRouterDevManifestSet } from './dev-runtime-artifacts.js';
4
4
  export { snapshotDevChangedFiles } from './dev-runtime-artifacts.js';
5
5
  export type { DevChangedFiles, DevGraphChanges, DevGraphIdentity, ReactRouterDevBuildPlan, ReactRouterDevManifest, ReactRouterDevManifestSet, } from './dev-runtime-artifacts.js';
6
6
  export type ReactRouterDevRuntime = {
7
7
  beginAttempt: () => void;
8
8
  captureWeb: (compilation: Rspack.Compilation, manifestsByEntryName: ReactRouterDevManifestSet) => void;
9
- finishAttempt: (stats: Rspack.Stats | Rspack.MultiStats, changes: DevGraphChanges, identity: DevGraphIdentity) => Promise<void>;
9
+ finishAttempt: (stats: DevRuntimeStats, changes: DevGraphChanges, identity: DevGraphIdentity) => Promise<'committed' | 'ignored' | 'retry-node'>;
10
10
  failAttempt: (error: Error) => void;
11
11
  load: (entryName?: string) => Promise<ServerBuild>;
12
12
  close: (error?: Error) => void;