rsbuild-plugin-react-router 0.5.0 → 0.6.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 (110) hide show
  1. package/README.md +58 -208
  2. package/dist/511.js +370 -331
  3. package/dist/build-output-transforms.d.ts +30 -6
  4. package/dist/classic-mode.d.ts +56 -0
  5. package/dist/config-imports.d.ts +8 -2
  6. package/dist/constants.d.ts +3 -0
  7. package/dist/dev-background-resources.d.ts +3 -1
  8. package/dist/dev-generation.d.ts +2 -3
  9. package/dist/dev-hmr.d.ts +8 -2
  10. package/dist/dev-runtime-controller.d.ts +6 -1
  11. package/dist/dev-source-maps.d.ts +4 -0
  12. package/dist/effect-runtime.d.ts +17 -5
  13. package/dist/entry-paths.d.ts +16 -0
  14. package/dist/environment-output.d.ts +6 -0
  15. package/dist/export-utils.d.ts +2 -1
  16. package/dist/index.cjs +3957 -2208
  17. package/dist/index.d.ts +3 -1
  18. package/dist/index.js +3470 -1793
  19. package/dist/lazy-compilation-prewarm.d.ts +8 -5
  20. package/dist/manifest.d.ts +16 -4
  21. package/dist/mode-plan.d.ts +82 -0
  22. package/dist/modify-browser-manifest.d.ts +8 -4
  23. package/dist/plugin-utils.d.ts +27 -1
  24. package/dist/prerender-build.d.ts +5 -5
  25. package/dist/prerender.d.ts +1 -5
  26. package/dist/react-router-config.d.ts +11 -3
  27. package/dist/route-artifacts.d.ts +4 -2
  28. package/dist/route-chunks.d.ts +2 -1
  29. package/dist/route-component-transform.d.ts +0 -2
  30. package/dist/route-export-pruning.d.ts +4 -1
  31. package/dist/route-imports.d.ts +18 -0
  32. package/dist/route-transform-tasks.d.ts +5 -0
  33. package/dist/route-watch.d.ts +11 -6
  34. package/dist/rsc-dev-server.d.ts +26 -0
  35. package/dist/rsc-prerender.d.ts +54 -0
  36. package/dist/rsc-route-config.d.ts +6 -0
  37. package/dist/rsc-route-exports.d.ts +15 -0
  38. package/dist/rsc-route-transform-loader.cjs +43 -0
  39. package/dist/rsc-route-transform-loader.d.ts +30 -0
  40. package/dist/rsc-route-transform-loader.js +16 -0
  41. package/dist/rsc-route-transform-registration.d.ts +12 -0
  42. package/dist/rsc-route-transforms.d.ts +21 -0
  43. package/dist/rsc-support.d.ts +23 -0
  44. package/dist/rsc-virtual-modules.d.ts +19 -0
  45. package/dist/server-build-plan.d.ts +2 -1
  46. package/dist/server-build-resolution.d.ts +1 -2
  47. package/dist/server-utils.d.ts +4 -5
  48. package/dist/ssr-asset-relocation.d.ts +98 -0
  49. package/dist/templates/entry.rsc.client.d.ts +1 -0
  50. package/dist/templates/entry.rsc.client.js +61 -0
  51. package/dist/templates/entry.rsc.d.ts +9 -0
  52. package/dist/templates/entry.rsc.js +38 -0
  53. package/dist/templates/entry.rsc.ssr.d.ts +4 -0
  54. package/dist/templates/entry.rsc.ssr.js +24 -0
  55. package/dist/typegen.d.ts +4 -2
  56. package/dist/types.d.ts +30 -1
  57. package/package.json +69 -14
  58. package/src/build-output-transforms.ts +155 -21
  59. package/src/classic-mode.ts +253 -0
  60. package/src/config-imports.ts +153 -6
  61. package/src/constants.ts +6 -2
  62. package/src/dev-background-resources.ts +46 -85
  63. package/src/dev-generation.ts +52 -33
  64. package/src/dev-hmr.ts +112 -80
  65. package/src/dev-runtime-artifacts.ts +11 -12
  66. package/src/dev-runtime-controller.ts +75 -85
  67. package/src/dev-runtime-session.ts +14 -18
  68. package/src/dev-server.ts +2 -0
  69. package/src/dev-source-maps.ts +257 -0
  70. package/src/effect-runtime.ts +105 -57
  71. package/src/entry-paths.ts +80 -0
  72. package/src/environment-output.ts +55 -0
  73. package/src/export-utils.ts +15 -11
  74. package/src/index.ts +661 -496
  75. package/src/lazy-compilation-prewarm.ts +20 -4
  76. package/src/manifest.ts +157 -82
  77. package/src/mode-plan.ts +367 -0
  78. package/src/modify-browser-manifest.ts +130 -124
  79. package/src/plugin-utils.ts +103 -39
  80. package/src/prerender-build.ts +82 -104
  81. package/src/prerender.ts +23 -24
  82. package/src/react-router-config.ts +72 -26
  83. package/src/route-artifacts.ts +96 -66
  84. package/src/route-chunks.ts +167 -51
  85. package/src/route-component-transform.ts +14 -21
  86. package/src/route-export-pruning.ts +4 -3
  87. package/src/route-imports.ts +100 -0
  88. package/src/route-transform-tasks.ts +39 -25
  89. package/src/route-watch.ts +166 -188
  90. package/src/rsc-dev-server.ts +112 -0
  91. package/src/rsc-prerender.ts +362 -0
  92. package/src/rsc-route-config.ts +175 -0
  93. package/src/rsc-route-exports.ts +67 -0
  94. package/src/rsc-route-transform-loader.ts +65 -0
  95. package/src/rsc-route-transform-registration.ts +145 -0
  96. package/src/rsc-route-transforms.ts +995 -0
  97. package/src/rsc-runtime.d.ts +143 -0
  98. package/src/rsc-support.ts +116 -0
  99. package/src/rsc-virtual-modules.ts +113 -0
  100. package/src/server-build-plan.ts +14 -3
  101. package/src/server-build-resolution.ts +37 -47
  102. package/src/server-utils.ts +21 -35
  103. package/src/ssr-asset-relocation.ts +183 -0
  104. package/src/ssr-externals.ts +8 -26
  105. package/src/templates/entry.rsc.client.tsx +168 -0
  106. package/src/templates/entry.rsc.ssr.tsx +45 -0
  107. package/src/templates/entry.rsc.tsx +80 -0
  108. package/src/typegen.ts +40 -23
  109. package/src/types.ts +39 -1
  110. package/src/warnings/warn-on-client-source-maps.ts +6 -10
@@ -1,14 +1,162 @@
1
- import type { ModuleCache } from 'jiti';
1
+ import type { JitiOptions, ModuleCache, TransformOptions } from 'jiti';
2
2
  import { createJiti } from 'jiti';
3
3
  import { resolve } from 'pathe';
4
+ import { parse } from './yuku.js';
4
5
 
5
- type ConfigImporter = Pick<ReturnType<typeof createJiti>, 'import'>;
6
+ export type ConfigImporter = Pick<ReturnType<typeof createJiti>, 'import'>;
7
+
8
+ type ConfigImportOptions = {
9
+ define?: Record<string, unknown>;
10
+ moduleCache?: boolean;
11
+ };
6
12
 
7
13
  const normalizePath = (filePath: string): string => resolve(filePath);
8
14
 
9
15
  const isNodeModulePath = (filePath: string): boolean =>
10
16
  filePath.split(/[\\/]/).includes('node_modules');
11
17
 
18
+ type AstNode = {
19
+ type?: string;
20
+ start?: number;
21
+ end?: number;
22
+ object?: AstNode;
23
+ property?: AstNode;
24
+ meta?: AstNode;
25
+ name?: string;
26
+ value?: unknown;
27
+ computed?: boolean;
28
+ };
29
+
30
+ const normalizeDefineValue = (value: unknown): string | undefined => {
31
+ if (value === undefined) {
32
+ return undefined;
33
+ }
34
+ if (typeof value === 'string') {
35
+ return value;
36
+ }
37
+ return JSON.stringify(value);
38
+ };
39
+
40
+ const getMemberExpressionName = (node: AstNode | undefined): string | null => {
41
+ if (!node) {
42
+ return null;
43
+ }
44
+ if (node.type === 'Identifier') {
45
+ return node.name ?? null;
46
+ }
47
+ if (node.type === 'MetaProperty') {
48
+ const meta = getMemberExpressionName(node.meta);
49
+ const property = getMemberExpressionName(node.property);
50
+ return meta && property ? `${meta}.${property}` : null;
51
+ }
52
+ if (node.type !== 'MemberExpression') {
53
+ return null;
54
+ }
55
+ const object = getMemberExpressionName(node.object);
56
+ const property =
57
+ node.computed && typeof node.property?.value === 'string'
58
+ ? node.property.value
59
+ : getMemberExpressionName(node.property);
60
+ return object && property ? `${object}.${property}` : null;
61
+ };
62
+
63
+ const collectDefineReplacements = (
64
+ node: unknown,
65
+ define: ReadonlyMap<string, string>,
66
+ replacements: Array<{ start: number; end: number; value: string }>
67
+ ): void => {
68
+ if (!node || typeof node !== 'object') {
69
+ return;
70
+ }
71
+
72
+ const astNode = node as AstNode;
73
+ const memberName = getMemberExpressionName(astNode);
74
+ const value = memberName ? define.get(memberName) : undefined;
75
+ if (
76
+ value !== undefined &&
77
+ typeof astNode.start === 'number' &&
78
+ typeof astNode.end === 'number'
79
+ ) {
80
+ replacements.push({
81
+ start: astNode.start,
82
+ end: astNode.end,
83
+ value,
84
+ });
85
+ return;
86
+ }
87
+
88
+ for (const child of Object.values(node)) {
89
+ if (Array.isArray(child)) {
90
+ for (const item of child) {
91
+ collectDefineReplacements(item, define, replacements);
92
+ }
93
+ continue;
94
+ }
95
+ collectDefineReplacements(child, define, replacements);
96
+ }
97
+ };
98
+
99
+ export const applyConfigDefines = (
100
+ source: string,
101
+ define: Record<string, unknown> | undefined
102
+ ): string => {
103
+ if (!define) {
104
+ return source;
105
+ }
106
+
107
+ const normalizedDefine = new Map(
108
+ Object.entries(define).flatMap(([key, value]) => {
109
+ const normalized = normalizeDefineValue(value);
110
+ return normalized === undefined ? [] : [[key, normalized]];
111
+ })
112
+ );
113
+ if (normalizedDefine.size === 0) {
114
+ return source;
115
+ }
116
+
117
+ const replacements: Array<{ start: number; end: number; value: string }> = [];
118
+ collectDefineReplacements(
119
+ parse(source, { sourceType: 'module' }),
120
+ normalizedDefine,
121
+ replacements
122
+ );
123
+
124
+ return replacements
125
+ .sort((left, right) => right.start - left.start)
126
+ .reduce(
127
+ (contents, replacement) =>
128
+ `${contents.slice(0, replacement.start)}${replacement.value}${contents.slice(
129
+ replacement.end
130
+ )}`,
131
+ source
132
+ );
133
+ };
134
+
135
+ export const createConfigImporter = ({
136
+ define,
137
+ moduleCache = true,
138
+ }: ConfigImportOptions = {}): ReturnType<typeof createJiti> => {
139
+ const defaultJiti = createJiti(process.cwd(), { moduleCache });
140
+ if (!define || Object.keys(define).length === 0) {
141
+ return defaultJiti;
142
+ }
143
+
144
+ const defaultTransform = defaultJiti.transform;
145
+ const options = {
146
+ moduleCache,
147
+ transform(transformOptions: TransformOptions) {
148
+ return {
149
+ code: defaultTransform({
150
+ ...transformOptions,
151
+ source: applyConfigDefines(transformOptions.source, define),
152
+ }),
153
+ };
154
+ },
155
+ } satisfies JitiOptions;
156
+
157
+ return createJiti(process.cwd(), options);
158
+ };
159
+
12
160
  export const collectConfigImportWatchPaths = (
13
161
  configPath: string,
14
162
  moduleCache: ModuleCache,
@@ -50,11 +198,10 @@ export const clearConfigImportCache = (
50
198
  export const importConfigWithWatchPaths = async <T>(
51
199
  configPath: string,
52
200
  load: (importer: ConfigImporter) => PromiseLike<T> | T = async importer =>
53
- importer.import<T>(configPath, { default: true })
201
+ importer.import<T>(configPath, { default: true }),
202
+ options?: ConfigImportOptions
54
203
  ): Promise<{ value: Awaited<T>; watchPaths: string | string[] }> => {
55
- const jiti = createJiti(process.cwd(), {
56
- moduleCache: true,
57
- });
204
+ const jiti = createConfigImporter({ moduleCache: true, ...options });
58
205
  const previousCacheKeys = new Set(Object.keys(jiti.cache));
59
206
  let importPaths: string[] = [];
60
207
 
package/src/constants.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export const PLUGIN_NAME = 'rsbuild:react-router';
2
2
 
3
+ /** Default web `output.distPath.js` segment when the user has not customized it. */
4
+ export const DEFAULT_JS_DIST_PATH = 'static/js';
5
+
3
6
  export const JS_EXTENSIONS = [
4
7
  '.tsx',
5
8
  '.ts',
@@ -32,8 +35,6 @@ export const SERVER_ONLY_ROUTE_EXPORTS_SET: ReadonlySet<string> = new Set(
32
35
  SERVER_ONLY_ROUTE_EXPORTS
33
36
  );
34
37
 
35
- // Client route exports are split into non-component exports and component exports.
36
- // This mirrors upstream React Router Vite plugin intent and is used for export filtering.
37
38
  export const CLIENT_NON_COMPONENT_EXPORTS = [
38
39
  'clientAction',
39
40
  'clientLoader',
@@ -89,3 +90,6 @@ export const CLIENT_EXPORTS = {
89
90
  meta: 'meta',
90
91
  shouldRevalidate: 'shouldRevalidate',
91
92
  } as const;
93
+
94
+ // SPA-mode prerender fallback document, served when no prerendered page matches.
95
+ export const SPA_FALLBACK_HTML_FILE = '__spa-fallback.html';
@@ -7,11 +7,10 @@ import {
7
7
  createDelayedPluginTask,
8
8
  DEV_BACKGROUND_STARTUP_DELAY_MS,
9
9
  normalizeEffectError,
10
- runPluginEffect,
11
- tryPluginPromise,
10
+ type PluginEffectRuntime,
12
11
  } from './effect-runtime.js';
13
12
  import {
14
- createLazyCompilationPrewarmController,
13
+ acquireLazyCompilationPrewarm,
15
14
  normalizeLazyCompilationPrewarmOptions,
16
15
  } from './lazy-compilation-prewarm.js';
17
16
  import {
@@ -19,8 +18,8 @@ import {
19
18
  type ReactRouterManifestForDev,
20
19
  } from './manifest.js';
21
20
  import {
21
+ acquireRouteTopologyWatcher,
22
22
  createRouteManifestSnapshot,
23
- createRouteTopologyWatcher,
24
23
  ensureDevRestartMarker,
25
24
  type WatchFileConfig,
26
25
  } from './route-watch.js';
@@ -28,6 +27,7 @@ import type { PluginOptions } from './types.js';
28
27
 
29
28
  type RegisterReactRouterDevBackgroundResourcesOptions = {
30
29
  api: RsbuildPluginAPI;
30
+ runtime: PluginEffectRuntime;
31
31
  isBuild: boolean;
32
32
  lazyCompilationPrewarm: PluginOptions['unstableLazyCompilationPrewarm'];
33
33
  routeRestartMarkerPath: string;
@@ -98,7 +98,10 @@ export const createReactRouterRouteWatchFiles = ({
98
98
  watchFiles.push(
99
99
  {
100
100
  paths: routeConfigWatchPaths,
101
- type: 'reload-server',
101
+ // Keep the current compiler alive while a route config is temporarily
102
+ // invalid. The topology watcher touches the dedicated restart marker
103
+ // once a valid edit actually changes the route graph.
104
+ type: 'reload-page',
102
105
  },
103
106
  {
104
107
  paths: routeRestartMarkerPath,
@@ -110,27 +113,9 @@ export const createReactRouterRouteWatchFiles = ({
110
113
  return watchFiles;
111
114
  };
112
115
 
113
- const closeAll = async (
114
- message: string,
115
- closers: Array<() => Promise<void>>
116
- ): Promise<void> => {
117
- const results = await Promise.allSettled(closers.map(closer => closer()));
118
- const errors = results
119
- .filter(
120
- (result): result is PromiseRejectedResult => result.status === 'rejected'
121
- )
122
- .map(result => normalizeEffectError(result.reason));
123
-
124
- if (errors.length === 1) {
125
- throw errors[0];
126
- }
127
- if (errors.length > 1) {
128
- throw new AggregateError(errors, message);
129
- }
130
- };
131
-
132
- export const registerReactRouterDevBackgroundResources = ({
116
+ export const registerReactRouterDevBackgroundResources = async ({
133
117
  api,
118
+ runtime,
134
119
  isBuild,
135
120
  lazyCompilationPrewarm,
136
121
  routeRestartMarkerPath,
@@ -138,9 +123,8 @@ export const registerReactRouterDevBackgroundResources = ({
138
123
  getRouteTopology,
139
124
  initialRouteTopology,
140
125
  onRouteTopologyChange,
141
- }: RegisterReactRouterDevBackgroundResourcesOptions): ReactRouterDevBackgroundResources => {
142
- let closeActiveRouteTopologyWatcher: (() => Promise<void>) | undefined;
143
- let routeTopologyWatcherClosed = false;
126
+ }: RegisterReactRouterDevBackgroundResourcesOptions): Promise<ReactRouterDevBackgroundResources> => {
127
+ let routeTopologyWatcherStarted = false;
144
128
 
145
129
  const reportRouteTopologyWatcherError = (error: unknown): void => {
146
130
  api.logger.warn(
@@ -149,54 +133,49 @@ export const registerReactRouterDevBackgroundResources = ({
149
133
  };
150
134
 
151
135
  const routeTopologyWatcherTask = createDelayedPluginTask({
136
+ runtime,
152
137
  delayMs: DEV_BACKGROUND_STARTUP_DELAY_MS,
153
138
  run: () =>
154
- Effect.gen(function* () {
155
- yield* tryPluginPromise(() =>
156
- ensureDevRestartMarker(routeRestartMarkerPath)
157
- );
158
- const closeWatcher = yield* tryPluginPromise(() =>
159
- createRouteTopologyWatcher({
160
- watchDirectory,
161
- getRouteTopology,
162
- initialRouteTopology,
163
- restartMarkerPath: routeRestartMarkerPath,
164
- onRouteTopologyChange,
165
- onError: reportRouteTopologyWatcherError,
166
- })
167
- );
168
- if (routeTopologyWatcherClosed) {
169
- yield* tryPluginPromise(() => closeWatcher());
170
- return;
171
- }
172
- closeActiveRouteTopologyWatcher = closeWatcher;
173
- }),
139
+ acquireRouteTopologyWatcher({
140
+ runtime,
141
+ watchDirectory,
142
+ getRouteTopology,
143
+ initialRouteTopology,
144
+ restartMarkerPath: routeRestartMarkerPath,
145
+ onRouteTopologyChange,
146
+ onError: reportRouteTopologyWatcherError,
147
+ }).pipe(
148
+ Effect.tap(() =>
149
+ Effect.sync(() => (routeTopologyWatcherStarted = true))
150
+ ),
151
+ Effect.asVoid
152
+ ),
174
153
  onError: reportRouteTopologyWatcherError,
175
154
  });
176
155
 
177
- const scheduleRouteTopologyWatcher = (): void => {
178
- if (routeTopologyWatcherClosed || closeActiveRouteTopologyWatcher) {
179
- return;
180
- }
181
- routeTopologyWatcherTask.schedule();
182
- };
183
-
184
156
  const lazyCompilationPrewarmConfig = normalizeLazyCompilationPrewarmOptions(
185
157
  lazyCompilationPrewarm
186
158
  );
187
159
  const lazyCompilationPrewarmController = lazyCompilationPrewarmConfig
188
- ? createLazyCompilationPrewarmController({
189
- config: lazyCompilationPrewarmConfig,
190
- onError: error =>
191
- api.logger.warn(
192
- `[${PLUGIN_NAME}] Lazy compilation prewarm skipped: ${error.message}`
193
- ),
194
- })
160
+ ? await runtime.runPromise(
161
+ acquireLazyCompilationPrewarm({
162
+ runtime,
163
+ config: lazyCompilationPrewarmConfig,
164
+ onError: error =>
165
+ api.logger.warn(
166
+ `[${PLUGIN_NAME}] Lazy compilation prewarm skipped: ${error.message}`
167
+ ),
168
+ })
169
+ )
195
170
  : null;
196
171
 
197
172
  if (!isBuild) {
198
- api.onBeforeStartDevServer(() => {
199
- routeTopologyWatcherClosed = false;
173
+ await runtime.runPromise(
174
+ Effect.acquireRelease(Effect.void, routeTopologyWatcherTask.cancelEffect)
175
+ );
176
+
177
+ api.onBeforeStartDevServer(async () => {
178
+ await ensureDevRestartMarker(routeRestartMarkerPath);
200
179
  });
201
180
 
202
181
  api.onAfterStartDevServer(({ port }) => {
@@ -207,31 +186,13 @@ export const registerReactRouterDevBackgroundResources = ({
207
186
  });
208
187
 
209
188
  api.onAfterDevCompile(() => {
210
- scheduleRouteTopologyWatcher();
189
+ if (!routeTopologyWatcherStarted) {
190
+ routeTopologyWatcherTask.schedule();
191
+ }
211
192
  lazyCompilationPrewarmController?.schedule();
212
193
  });
213
194
  }
214
195
 
215
- const closeRouteTopologyWatcher = async (): Promise<void> => {
216
- routeTopologyWatcherClosed = true;
217
- await runPluginEffect(routeTopologyWatcherTask.cancelEffect());
218
- await closeActiveRouteTopologyWatcher?.();
219
- closeActiveRouteTopologyWatcher = undefined;
220
- };
221
-
222
- const closeLazyCompilationPrewarm = async (): Promise<void> => {
223
- await runPluginEffect(
224
- lazyCompilationPrewarmController?.cancelEffect() ?? Effect.void
225
- );
226
- };
227
-
228
- api.onCloseDevServer(() =>
229
- closeAll(
230
- '[rsbuild-plugin-react-router] Failed to close dev server resources.',
231
- [closeRouteTopologyWatcher, closeLazyCompilationPrewarm]
232
- )
233
- );
234
-
235
196
  return {
236
197
  setManifest(manifest) {
237
198
  lazyCompilationPrewarmController?.setManifest(manifest);
@@ -1,7 +1,9 @@
1
1
  import type { RsbuildDevServer, Rspack } from '@rsbuild/core';
2
2
  import * as EffectDeferred from 'effect/Deferred';
3
3
  import * as Effect from 'effect/Effect';
4
+ import * as FiberId from 'effect/FiberId';
4
5
  import type { ServerBuild } from 'react-router';
6
+ import { HMR_PATCHABLE_ROUTE_FLAGS } from './route-artifacts.js';
5
7
  import {
6
8
  evaluateServerBuilds,
7
9
  getEnvironmentStats,
@@ -20,14 +22,10 @@ import {
20
22
  } from './dev-runtime-artifacts.js';
21
23
  import { normalizeEffectError, runPluginEffect } from './effect-runtime.js';
22
24
 
23
- export { snapshotDevChangedFiles } from './dev-runtime-artifacts.js';
24
25
  export type {
25
- DevChangedFiles,
26
26
  DevGraphChanges,
27
27
  DevGraphIdentity,
28
- ReactRouterDevBuildPlan,
29
28
  ReactRouterDevManifest,
30
- ReactRouterDevManifestSet,
31
29
  } from './dev-runtime-artifacts.js';
32
30
 
33
31
  type CommittedGeneration = {
@@ -73,7 +71,9 @@ type CreateReactRouterDevRuntimeOptions = {
73
71
  buildPlan: ReactRouterDevBuildPlan;
74
72
  onEvaluationError: (error: Error) => void;
75
73
  onCssAssetOwnershipChanged?: (change: 'removed' | 'restored') => void;
76
- onRouteManifestChanged?: () => void;
74
+ onRouteManifestChanged?: (
75
+ manifest: ReactRouterDevManifestSet[string]
76
+ ) => void;
77
77
  onWarning?: (message: string) => void;
78
78
  };
79
79
 
@@ -199,28 +199,35 @@ type DevRouteManifestEntry = NonNullable<
199
199
 
200
200
  const hasSameRouteMetadata = (
201
201
  previous: DevRouteManifestEntry,
202
- next: DevRouteManifestEntry
203
- ): boolean =>
204
- previous.caseSensitive === next.caseSensitive &&
205
- previous.clientActionModule === next.clientActionModule &&
206
- previous.clientLoaderModule === next.clientLoaderModule &&
207
- previous.clientMiddlewareModule === next.clientMiddlewareModule &&
208
- previous.hasErrorBoundary === next.hasErrorBoundary &&
209
- previous.hasAction === next.hasAction &&
210
- previous.hasClientAction === next.hasClientAction &&
211
- previous.hasClientLoader === next.hasClientLoader &&
212
- previous.hasClientMiddleware === next.hasClientMiddleware &&
213
- previous.hasDefaultExport === next.hasDefaultExport &&
214
- previous.hasLoader === next.hasLoader &&
215
- previous.hydrateFallbackModule === next.hydrateFallbackModule &&
216
- previous.id === next.id &&
217
- previous.index === next.index &&
218
- previous.parentId === next.parentId &&
219
- previous.path === next.path;
202
+ next: DevRouteManifestEntry,
203
+ includeHmrPatchableFlags: boolean
204
+ ): boolean => {
205
+ const structuralEqual =
206
+ previous.caseSensitive === next.caseSensitive &&
207
+ previous.clientActionModule === next.clientActionModule &&
208
+ previous.clientLoaderModule === next.clientLoaderModule &&
209
+ previous.clientMiddlewareModule === next.clientMiddlewareModule &&
210
+ previous.hasDefaultExport === next.hasDefaultExport &&
211
+ previous.hydrateFallbackModule === next.hydrateFallbackModule &&
212
+ previous.id === next.id &&
213
+ previous.index === next.index &&
214
+ previous.parentId === next.parentId &&
215
+ previous.path === next.path;
216
+ if (!structuralEqual) {
217
+ return false;
218
+ }
219
+ if (!includeHmrPatchableFlags) {
220
+ return true;
221
+ }
222
+ // Without a client-side HMR runtime these flags can only reach the browser
223
+ // through a full reload.
224
+ return HMR_PATCHABLE_ROUTE_FLAGS.every(flag => previous[flag] === next[flag]);
225
+ };
220
226
 
221
227
  const hasRouteManifestMetadataChanges = (
222
228
  previous: ReactRouterDevManifestSet,
223
- next: ReactRouterDevManifestSet
229
+ next: ReactRouterDevManifestSet,
230
+ includeHmrPatchableFlags: boolean
224
231
  ): boolean => {
225
232
  const previousEntryNames = Object.keys(previous);
226
233
  if (previousEntryNames.length !== Object.keys(next).length) {
@@ -240,7 +247,14 @@ const hasRouteManifestMetadataChanges = (
240
247
  for (const routeId of previousRouteIds) {
241
248
  const previousRoute = previousRoutes[routeId];
242
249
  const nextRoute = nextRoutes[routeId];
243
- if (!nextRoute || !hasSameRouteMetadata(previousRoute, nextRoute)) {
250
+ if (
251
+ !nextRoute ||
252
+ !hasSameRouteMetadata(
253
+ previousRoute,
254
+ nextRoute,
255
+ includeHmrPatchableFlags
256
+ )
257
+ ) {
244
258
  return true;
245
259
  }
246
260
  }
@@ -251,7 +265,7 @@ const hasRouteManifestMetadataChanges = (
251
265
  const createReadinessDeferred = (): EffectDeferred.Deferred<
252
266
  CommittedGeneration,
253
267
  Error
254
- > => Effect.runSync(EffectDeferred.make<CommittedGeneration, Error>());
268
+ > => EffectDeferred.unsafeMake<CommittedGeneration, Error>(FiberId.none);
255
269
 
256
270
  export const createReactRouterDevRuntime = ({
257
271
  server,
@@ -285,9 +299,11 @@ export const createReactRouterDevRuntime = ({
285
299
  }
286
300
  };
287
301
 
288
- const notifyRouteManifestChanged = (): void => {
302
+ const notifyRouteManifestChanged = (
303
+ manifest: ReactRouterDevManifestSet[string]
304
+ ): void => {
289
305
  try {
290
- onRouteManifestChanged();
306
+ onRouteManifestChanged(manifest);
291
307
  } catch (cause) {
292
308
  onWarning(
293
309
  `[rsbuild-plugin-react-router] Failed to notify the browser after route manifest metadata changed: ${normalizeEffectError(cause).message}`
@@ -340,7 +356,7 @@ export const createReactRouterDevRuntime = ({
340
356
  if (state.kind === 'starting') {
341
357
  const { readiness } = state;
342
358
  state = { kind: 'failed', attemptId, error };
343
- Effect.runSync(EffectDeferred.fail(readiness, error));
359
+ EffectDeferred.unsafeDone(readiness, Effect.fail(error));
344
360
  } else if (state.kind === 'ready') {
345
361
  state = { ...state, pendingAttemptId: null };
346
362
  }
@@ -359,7 +375,7 @@ export const createReactRouterDevRuntime = ({
359
375
  if (state.kind === 'starting') {
360
376
  const { readiness } = state;
361
377
  state = { kind: 'ready', committed, pendingAttemptId: null };
362
- Effect.runSync(EffectDeferred.succeed(readiness, committed));
378
+ EffectDeferred.unsafeDone(readiness, Effect.succeed(committed));
363
379
  } else if (state.kind === 'ready') {
364
380
  state = { kind: 'ready', committed, pendingAttemptId: null };
365
381
  }
@@ -513,7 +529,8 @@ export const createReactRouterDevRuntime = ({
513
529
  webChanged &&
514
530
  hasRouteManifestMetadataChanges(
515
531
  previous.web.manifestsByEntryName,
516
- manifestsByEntryName
532
+ manifestsByEntryName,
533
+ true
517
534
  );
518
535
  const reusePreviousNodeBuild =
519
536
  !!previous &&
@@ -587,7 +604,9 @@ export const createReactRouterDevRuntime = ({
587
604
  reloadAfterCssRemoval = false;
588
605
  }
589
606
  if (routeManifestMetadataChanged) {
590
- notifyRouteManifestChanged();
607
+ notifyRouteManifestChanged(
608
+ web.manifestsByEntryName[buildPlan.defaultEntryName]
609
+ );
591
610
  }
592
611
  return 'committed';
593
612
  } catch (cause) {
@@ -636,7 +655,7 @@ export const createReactRouterDevRuntime = ({
636
655
  '[rsbuild-plugin-react-router] The development server closed before a React Router build was ready.'
637
656
  );
638
657
  if (state.kind === 'starting') {
639
- Effect.runSync(EffectDeferred.fail(state.readiness, closeError));
658
+ EffectDeferred.unsafeDone(state.readiness, Effect.fail(closeError));
640
659
  }
641
660
  state = { kind: 'closed', error: closeError };
642
661
  },