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
@@ -1,6 +1,9 @@
1
1
  import type { ModuleCache } from 'jiti';
2
+ import { createJiti } from 'jiti';
2
3
  import { resolve } from 'pathe';
3
4
 
5
+ type ConfigImporter = Pick<ReturnType<typeof createJiti>, 'import'>;
6
+
4
7
  const normalizePath = (filePath: string): string => resolve(filePath);
5
8
 
6
9
  const isNodeModulePath = (filePath: string): boolean =>
@@ -43,3 +46,38 @@ export const clearConfigImportCache = (
43
46
  }
44
47
  }
45
48
  };
49
+
50
+ export const importConfigWithWatchPaths = async <T>(
51
+ configPath: string,
52
+ load: (importer: ConfigImporter) => PromiseLike<T> | T = async importer =>
53
+ importer.import<T>(configPath, { default: true })
54
+ ): Promise<{ value: Awaited<T>; watchPaths: string | string[] }> => {
55
+ const jiti = createJiti(process.cwd(), {
56
+ moduleCache: true,
57
+ });
58
+ const previousCacheKeys = new Set(Object.keys(jiti.cache));
59
+ let importPaths: string[] = [];
60
+
61
+ try {
62
+ const value = await load(jiti);
63
+ importPaths = collectConfigImportWatchPaths(
64
+ configPath,
65
+ jiti.cache,
66
+ previousCacheKeys
67
+ );
68
+ return {
69
+ value,
70
+ watchPaths:
71
+ importPaths.length > 0 ? [configPath, ...importPaths] : configPath,
72
+ };
73
+ } finally {
74
+ if (importPaths.length === 0) {
75
+ importPaths = collectConfigImportWatchPaths(
76
+ configPath,
77
+ jiti.cache,
78
+ previousCacheKeys
79
+ );
80
+ }
81
+ clearConfigImportCache(jiti.cache, [configPath, ...importPaths]);
82
+ }
83
+ };
@@ -0,0 +1,255 @@
1
+ import type { RsbuildPluginAPI } from '@rsbuild/core';
2
+ import type { RouteConfigEntry } from '@react-router/dev/routes';
3
+ import * as Effect from 'effect/Effect';
4
+ import { relative } from 'pathe';
5
+ import { PLUGIN_NAME } from './constants.js';
6
+ import {
7
+ createDelayedPluginTask,
8
+ DEV_BACKGROUND_STARTUP_DELAY_MS,
9
+ normalizeEffectError,
10
+ runPluginEffect,
11
+ tryPluginPromise,
12
+ } from './effect-runtime.js';
13
+ import {
14
+ createLazyCompilationPrewarmController,
15
+ normalizeLazyCompilationPrewarmOptions,
16
+ } from './lazy-compilation-prewarm.js';
17
+ import {
18
+ configRoutesToRouteManifestEntries,
19
+ type ReactRouterManifestForDev,
20
+ } from './manifest.js';
21
+ import type { RouteTransformExecutor } from './parallel-route-transforms.js';
22
+ import {
23
+ createRouteManifestSnapshot,
24
+ createRouteTopologyWatcher,
25
+ ensureDevRestartMarker,
26
+ type WatchFileConfig,
27
+ } from './route-watch.js';
28
+ import type { PluginOptions } from './types.js';
29
+
30
+ type RegisterReactRouterDevBackgroundResourcesOptions = {
31
+ api: RsbuildPluginAPI;
32
+ isBuild: boolean;
33
+ lazyCompilationPrewarm: PluginOptions['unstableLazyCompilationPrewarm'];
34
+ routeTransformExecutor: RouteTransformExecutor;
35
+ routeRestartMarkerPath: string;
36
+ watchDirectory: string;
37
+ getRouteTopology: () => Promise<Set<string>>;
38
+ initialRouteTopology: Set<string>;
39
+ onRouteTopologyChange: PluginOptions['onRouteTopologyChange'];
40
+ };
41
+
42
+ type ReactRouterDevBackgroundResources = {
43
+ setManifest(manifest: ReactRouterManifestForDev): void;
44
+ };
45
+
46
+ export const createReactRouterRouteTopology = ({
47
+ appDirectory,
48
+ rootRouteFile,
49
+ routeConfig,
50
+ loadRouteConfig,
51
+ getRootRoutePath,
52
+ }: {
53
+ appDirectory: string;
54
+ rootRouteFile: string;
55
+ routeConfig: RouteConfigEntry[];
56
+ loadRouteConfig: () => Promise<RouteConfigEntry[]>;
57
+ getRootRoutePath: () => string;
58
+ }): {
59
+ initialRouteTopology: Set<string>;
60
+ getRouteTopology: () => Promise<Set<string>>;
61
+ } => {
62
+ const createSnapshot = (
63
+ routeFile: string,
64
+ routeConfigEntries: RouteConfigEntry[]
65
+ ): Set<string> =>
66
+ createRouteManifestSnapshot([
67
+ ['root', { path: '', id: 'root', file: routeFile }],
68
+ ...configRoutesToRouteManifestEntries(appDirectory, routeConfigEntries),
69
+ ]);
70
+
71
+ return {
72
+ initialRouteTopology: createSnapshot(rootRouteFile, routeConfig),
73
+ async getRouteTopology() {
74
+ const latestRouteConfig = await loadRouteConfig();
75
+ const latestRootRouteFile = relative(appDirectory, getRootRoutePath());
76
+ return createSnapshot(latestRootRouteFile, latestRouteConfig);
77
+ },
78
+ };
79
+ };
80
+
81
+ export const createReactRouterRouteWatchFiles = ({
82
+ configWatchPaths,
83
+ routeConfigWatchPaths,
84
+ routeRestartMarkerPath,
85
+ onRouteTopologyChange,
86
+ }: {
87
+ configWatchPaths: string | string[];
88
+ routeConfigWatchPaths: string | string[];
89
+ routeRestartMarkerPath: string;
90
+ onRouteTopologyChange: PluginOptions['onRouteTopologyChange'];
91
+ }): WatchFileConfig[] => {
92
+ const watchFiles: WatchFileConfig[] = [
93
+ {
94
+ paths: configWatchPaths,
95
+ type: 'reload-server',
96
+ },
97
+ ];
98
+
99
+ if (!onRouteTopologyChange) {
100
+ watchFiles.push(
101
+ {
102
+ paths: routeConfigWatchPaths,
103
+ type: 'reload-server',
104
+ },
105
+ {
106
+ paths: routeRestartMarkerPath,
107
+ type: 'reload-server',
108
+ }
109
+ );
110
+ }
111
+
112
+ return watchFiles;
113
+ };
114
+
115
+ const closeAll = async (
116
+ message: string,
117
+ closers: Array<() => Promise<void>>
118
+ ): Promise<void> => {
119
+ const results = await Promise.allSettled(closers.map(closer => closer()));
120
+ const errors = results
121
+ .filter(
122
+ (result): result is PromiseRejectedResult => result.status === 'rejected'
123
+ )
124
+ .map(result => normalizeEffectError(result.reason));
125
+
126
+ if (errors.length === 1) {
127
+ throw errors[0];
128
+ }
129
+ if (errors.length > 1) {
130
+ throw new AggregateError(errors, message);
131
+ }
132
+ };
133
+
134
+ export const registerReactRouterDevBackgroundResources = ({
135
+ api,
136
+ isBuild,
137
+ lazyCompilationPrewarm,
138
+ routeTransformExecutor,
139
+ routeRestartMarkerPath,
140
+ watchDirectory,
141
+ getRouteTopology,
142
+ initialRouteTopology,
143
+ onRouteTopologyChange,
144
+ }: RegisterReactRouterDevBackgroundResourcesOptions): ReactRouterDevBackgroundResources => {
145
+ let closeActiveRouteTopologyWatcher: (() => Promise<void>) | undefined;
146
+ let routeTopologyWatcherClosed = false;
147
+
148
+ const reportRouteTopologyWatcherError = (error: unknown): void => {
149
+ api.logger.warn(
150
+ `[${PLUGIN_NAME}] Failed to watch route topology changes: ${String(normalizeEffectError(error))}`
151
+ );
152
+ };
153
+
154
+ const routeTopologyWatcherTask = createDelayedPluginTask({
155
+ delayMs: DEV_BACKGROUND_STARTUP_DELAY_MS,
156
+ run: () =>
157
+ Effect.gen(function* () {
158
+ yield* tryPluginPromise(() =>
159
+ ensureDevRestartMarker(routeRestartMarkerPath)
160
+ );
161
+ const closeWatcher = yield* tryPluginPromise(() =>
162
+ createRouteTopologyWatcher({
163
+ watchDirectory,
164
+ getRouteTopology,
165
+ initialRouteTopology,
166
+ restartMarkerPath: routeRestartMarkerPath,
167
+ onRouteTopologyChange,
168
+ onError: reportRouteTopologyWatcherError,
169
+ })
170
+ );
171
+ if (routeTopologyWatcherClosed) {
172
+ yield* tryPluginPromise(() => closeWatcher());
173
+ return;
174
+ }
175
+ closeActiveRouteTopologyWatcher = closeWatcher;
176
+ }),
177
+ onError: reportRouteTopologyWatcherError,
178
+ });
179
+
180
+ const scheduleRouteTopologyWatcher = (): void => {
181
+ if (routeTopologyWatcherClosed || closeActiveRouteTopologyWatcher) {
182
+ return;
183
+ }
184
+ routeTopologyWatcherTask.schedule();
185
+ };
186
+
187
+ const lazyCompilationPrewarmConfig = normalizeLazyCompilationPrewarmOptions(
188
+ lazyCompilationPrewarm
189
+ );
190
+ const lazyCompilationPrewarmController = lazyCompilationPrewarmConfig
191
+ ? createLazyCompilationPrewarmController({
192
+ config: lazyCompilationPrewarmConfig,
193
+ onError: error =>
194
+ api.logger.warn(
195
+ `[${PLUGIN_NAME}] Lazy compilation prewarm skipped: ${error.message}`
196
+ ),
197
+ })
198
+ : null;
199
+
200
+ if (!isBuild) {
201
+ api.onBeforeStartDevServer(() => {
202
+ routeTopologyWatcherClosed = false;
203
+ });
204
+
205
+ api.onAfterStartDevServer(({ port }) => {
206
+ lazyCompilationPrewarmController?.setServerOrigin(
207
+ `http://localhost:${port}`
208
+ );
209
+ lazyCompilationPrewarmController?.schedule();
210
+ });
211
+
212
+ api.onAfterDevCompile(() => {
213
+ scheduleRouteTopologyWatcher();
214
+ lazyCompilationPrewarmController?.schedule();
215
+ });
216
+
217
+ // Spawn transform workers now so thread startup overlaps Rsbuild's own
218
+ // compiler creation instead of delaying the first route transform.
219
+ routeTransformExecutor.prewarm();
220
+ }
221
+
222
+ const closeRouteTopologyWatcher = async (): Promise<void> => {
223
+ routeTopologyWatcherClosed = true;
224
+ await runPluginEffect(routeTopologyWatcherTask.cancelEffect());
225
+ await closeActiveRouteTopologyWatcher?.();
226
+ closeActiveRouteTopologyWatcher = undefined;
227
+ };
228
+
229
+ const closeLazyCompilationPrewarm = async (): Promise<void> => {
230
+ await runPluginEffect(
231
+ lazyCompilationPrewarmController?.cancelEffect() ?? Effect.void
232
+ );
233
+ };
234
+
235
+ const closeRouteTransformExecutor = (): Promise<void> =>
236
+ routeTransformExecutor.close();
237
+
238
+ api.onCloseDevServer(() =>
239
+ closeAll(
240
+ '[rsbuild-plugin-react-router] Failed to close dev server resources.',
241
+ [
242
+ closeRouteTopologyWatcher,
243
+ closeLazyCompilationPrewarm,
244
+ closeRouteTransformExecutor,
245
+ ]
246
+ )
247
+ );
248
+ api.onCloseBuild(closeRouteTransformExecutor);
249
+
250
+ return {
251
+ setManifest(manifest) {
252
+ lazyCompilationPrewarmController?.setManifest(manifest);
253
+ },
254
+ };
255
+ };
@@ -1,4 +1,6 @@
1
1
  import type { RsbuildDevServer, Rspack } from '@rsbuild/core';
2
+ import * as EffectDeferred from 'effect/Deferred';
3
+ import * as Effect from 'effect/Effect';
2
4
  import type { ServerBuild } from 'react-router';
3
5
  import {
4
6
  evaluateServerBuilds,
@@ -10,11 +12,13 @@ import {
10
12
  type DevCompilationIdentity,
11
13
  type DevGraphChanges,
12
14
  type DevGraphIdentity,
15
+ type DevRuntimeStats,
13
16
  type ReactRouterDevBuildPlan,
14
17
  type ReactRouterDevManifestSet,
15
18
  type ReactRouterServerBuilds,
16
19
  type WebArtifact,
17
20
  } from './dev-runtime-artifacts.js';
21
+ import { normalizeEffectError, runPluginEffect } from './effect-runtime.js';
18
22
 
19
23
  export { snapshotDevChangedFiles } from './dev-runtime-artifacts.js';
20
24
  export type {
@@ -26,12 +30,6 @@ export type {
26
30
  ReactRouterDevManifestSet,
27
31
  } from './dev-runtime-artifacts.js';
28
32
 
29
- type Deferred<T> = {
30
- promise: Promise<T>;
31
- resolve: (value: T) => void;
32
- reject: (error: Error) => void;
33
- };
34
-
35
33
  type CommittedGeneration = {
36
34
  buildsByEntryName: ReactRouterServerBuilds;
37
35
  webIdentity: DevCompilationIdentity;
@@ -44,7 +42,7 @@ type RuntimeState =
44
42
  | {
45
43
  kind: 'starting';
46
44
  attemptId: number;
47
- readiness: Deferred<CommittedGeneration>;
45
+ readiness: EffectDeferred.Deferred<CommittedGeneration, Error>;
48
46
  }
49
47
  | { kind: 'failed'; attemptId: number; error: Error }
50
48
  | {
@@ -61,10 +59,10 @@ export type ReactRouterDevRuntime = {
61
59
  manifestsByEntryName: ReactRouterDevManifestSet
62
60
  ) => void;
63
61
  finishAttempt: (
64
- stats: Rspack.Stats | Rspack.MultiStats,
62
+ stats: DevRuntimeStats,
65
63
  changes: DevGraphChanges,
66
64
  identity: DevGraphIdentity
67
- ) => Promise<void>;
65
+ ) => Promise<'committed' | 'ignored' | 'retry-node'>;
68
66
  failAttempt: (error: Error) => void;
69
67
  load: (entryName?: string) => Promise<ServerBuild>;
70
68
  close: (error?: Error) => void;
@@ -250,18 +248,10 @@ const hasRouteManifestMetadataChanges = (
250
248
  return false;
251
249
  };
252
250
 
253
- const createDeferred = <T>(): Deferred<T> => {
254
- let resolve!: (value: T) => void;
255
- let reject!: (error: Error) => void;
256
- const promise = new Promise<T>((resolvePromise, rejectPromise) => {
257
- resolve = resolvePromise;
258
- reject = rejectPromise;
259
- });
260
- // Compilation can fail before a request asks for the build. Observe the
261
- // rejection now while returning the same promise to future callers.
262
- void promise.catch(() => undefined);
263
- return { promise, resolve, reject };
264
- };
251
+ const createReadinessDeferred = (): EffectDeferred.Deferred<
252
+ CommittedGeneration,
253
+ Error
254
+ > => Effect.runSync(EffectDeferred.make<CommittedGeneration, Error>());
265
255
 
266
256
  export const createReactRouterDevRuntime = ({
267
257
  server,
@@ -276,7 +266,7 @@ export const createReactRouterDevRuntime = ({
276
266
  let state: RuntimeState = {
277
267
  kind: 'starting',
278
268
  attemptId: 0,
279
- readiness: createDeferred(),
269
+ readiness: createReadinessDeferred(),
280
270
  };
281
271
  const manifestsByCompilation = new WeakMap<
282
272
  Rspack.Compilation,
@@ -289,9 +279,8 @@ export const createReactRouterDevRuntime = ({
289
279
  try {
290
280
  onCssAssetOwnershipChanged(change);
291
281
  } catch (cause) {
292
- const reason = cause instanceof Error ? cause.message : String(cause);
293
282
  onWarning(
294
- `[rsbuild-plugin-react-router] Failed to notify the browser after CSS asset ownership changed: ${reason}`
283
+ `[rsbuild-plugin-react-router] Failed to notify the browser after CSS asset ownership changed: ${normalizeEffectError(cause).message}`
295
284
  );
296
285
  }
297
286
  };
@@ -300,9 +289,8 @@ export const createReactRouterDevRuntime = ({
300
289
  try {
301
290
  onRouteManifestChanged();
302
291
  } catch (cause) {
303
- const reason = cause instanceof Error ? cause.message : String(cause);
304
292
  onWarning(
305
- `[rsbuild-plugin-react-router] Failed to notify the browser after route manifest metadata changed: ${reason}`
293
+ `[rsbuild-plugin-react-router] Failed to notify the browser after route manifest metadata changed: ${normalizeEffectError(cause).message}`
306
294
  );
307
295
  }
308
296
  };
@@ -352,7 +340,7 @@ export const createReactRouterDevRuntime = ({
352
340
  if (state.kind === 'starting') {
353
341
  const { readiness } = state;
354
342
  state = { kind: 'failed', attemptId, error };
355
- readiness.reject(error);
343
+ Effect.runSync(EffectDeferred.fail(readiness, error));
356
344
  } else if (state.kind === 'ready') {
357
345
  state = { ...state, pendingAttemptId: null };
358
346
  }
@@ -371,7 +359,7 @@ export const createReactRouterDevRuntime = ({
371
359
  if (state.kind === 'starting') {
372
360
  const { readiness } = state;
373
361
  state = { kind: 'ready', committed, pendingAttemptId: null };
374
- readiness.resolve(committed);
362
+ Effect.runSync(EffectDeferred.succeed(readiness, committed));
375
363
  } else if (state.kind === 'ready') {
376
364
  state = { kind: 'ready', committed, pendingAttemptId: null };
377
365
  }
@@ -413,7 +401,7 @@ export const createReactRouterDevRuntime = ({
413
401
  state = {
414
402
  kind: 'starting',
415
403
  attemptId,
416
- readiness: createDeferred(),
404
+ readiness: createReadinessDeferred(),
417
405
  };
418
406
  } else if (state.kind === 'starting') {
419
407
  state = { ...state, attemptId };
@@ -431,10 +419,10 @@ export const createReactRouterDevRuntime = ({
431
419
  }
432
420
  },
433
421
 
434
- async finishAttempt(stats, changes, identity): Promise<void> {
422
+ async finishAttempt(stats, changes, identity) {
435
423
  const attemptId = getCurrentAttemptId();
436
424
  if (attemptId === null) {
437
- return;
425
+ return 'ignored';
438
426
  }
439
427
  const webStats = getEnvironmentStats(stats, 'web');
440
428
  const nodeStats = getEnvironmentStats(stats, 'node');
@@ -446,13 +434,13 @@ export const createReactRouterDevRuntime = ({
446
434
  ),
447
435
  true
448
436
  );
449
- return;
437
+ return 'ignored';
450
438
  }
451
439
  if (
452
440
  webStats.compilation.needAdditionalPass ||
453
441
  nodeStats.compilation.needAdditionalPass
454
442
  ) {
455
- return;
443
+ return 'ignored';
456
444
  }
457
445
  if (webStats.hasErrors() || nodeStats.hasErrors()) {
458
446
  rejectAttempt(
@@ -462,7 +450,7 @@ export const createReactRouterDevRuntime = ({
462
450
  ),
463
451
  false
464
452
  );
465
- return;
453
+ return 'ignored';
466
454
  }
467
455
 
468
456
  const webCompilation = webStats.compilation;
@@ -477,14 +465,14 @@ export const createReactRouterDevRuntime = ({
477
465
  ),
478
466
  true
479
467
  );
480
- return;
468
+ return 'ignored';
481
469
  }
482
470
  const previous = state.kind === 'ready' ? state.committed : undefined;
483
471
  const webChanged = !previous || previous.webIdentity !== webIdentity;
484
472
  const nodeChanged = !previous || previous.nodeIdentity !== nodeIdentity;
485
473
 
486
474
  if (!webChanged && !nodeChanged) {
487
- return;
475
+ return 'ignored';
488
476
  }
489
477
 
490
478
  const manifestsByEntryName = webChanged
@@ -498,7 +486,7 @@ export const createReactRouterDevRuntime = ({
498
486
  ),
499
487
  true
500
488
  );
501
- return;
489
+ return 'ignored';
502
490
  }
503
491
  const cssAssetsRemoved =
504
492
  !!previous &&
@@ -527,21 +515,25 @@ export const createReactRouterDevRuntime = ({
527
515
  previous.web.manifestsByEntryName,
528
516
  manifestsByEntryName
529
517
  );
530
- const reusePreviousNodeBuild = !!previous && cssOnlyWebManifestChange;
518
+ const reusePreviousNodeBuild =
519
+ !!previous &&
520
+ cssOnlyWebManifestChange &&
521
+ (!nodeChanged || identity.nodeWeb !== webIdentity);
531
522
 
532
523
  if (
533
524
  nodeChanged &&
534
525
  identity.nodeWeb !== webIdentity &&
526
+ !identity.attempt &&
535
527
  !reusePreviousNodeBuild
536
528
  ) {
537
529
  const message =
538
530
  '[rsbuild-plugin-react-router] Discarded web and node results from different compiler cycles and kept the last-good build.';
539
531
  if (!previous) {
540
- return;
532
+ return 'retry-node';
541
533
  }
542
534
  onWarning(message);
543
535
  rejectAttempt(attemptId, new Error(message), false);
544
- return;
536
+ return 'retry-node';
545
537
  }
546
538
 
547
539
  const shouldEvaluateNode = nodeChanged && !reusePreviousNodeBuild;
@@ -551,7 +543,7 @@ export const createReactRouterDevRuntime = ({
551
543
  !cssOnlyWebManifestChange &&
552
544
  discardUnsafeOneSidedResult(attemptId, previous, webChanged, changes)
553
545
  ) {
554
- return;
546
+ return 'ignored';
555
547
  }
556
548
 
557
549
  try {
@@ -559,7 +551,7 @@ export const createReactRouterDevRuntime = ({
559
551
  ? await evaluateServerBuilds(server, buildPlan.entryNames)
560
552
  : previous!.buildsByEntryName;
561
553
  if (!isCurrentAttempt(attemptId)) {
562
- return;
554
+ return 'ignored';
563
555
  }
564
556
  const web = webChanged
565
557
  ? {
@@ -583,7 +575,7 @@ export const createReactRouterDevRuntime = ({
583
575
  : previous!.nodeDependencies,
584
576
  });
585
577
  if (!committed) {
586
- return;
578
+ return 'ignored';
587
579
  }
588
580
  if (cssAssetsRemoved) {
589
581
  reloadAfterCssRemoval = !cssAssetsAdded;
@@ -597,12 +589,10 @@ export const createReactRouterDevRuntime = ({
597
589
  if (routeManifestMetadataChanged) {
598
590
  notifyRouteManifestChanged();
599
591
  }
592
+ return 'committed';
600
593
  } catch (cause) {
601
- rejectAttempt(
602
- attemptId,
603
- cause instanceof Error ? cause : new Error(String(cause)),
604
- true
605
- );
594
+ rejectAttempt(attemptId, normalizeEffectError(cause), true);
595
+ return 'ignored';
606
596
  }
607
597
  },
608
598
 
@@ -625,11 +615,11 @@ export const createReactRouterDevRuntime = ({
625
615
  return Promise.resolve(selectBuild(state.committed, entryName));
626
616
  }
627
617
  if (state.kind === 'starting') {
628
- const selected = state.readiness.promise.then(generation =>
629
- selectBuild(generation, entryName)
618
+ const selected = runPluginEffect(
619
+ EffectDeferred.await(state.readiness).pipe(
620
+ Effect.map(generation => selectBuild(generation, entryName))
621
+ )
630
622
  );
631
- // Compilation may fail before the request awaiting this selection has
632
- // a chance to attach its own rejection handler.
633
623
  void selected.catch(() => undefined);
634
624
  return selected;
635
625
  }
@@ -646,7 +636,7 @@ export const createReactRouterDevRuntime = ({
646
636
  '[rsbuild-plugin-react-router] The development server closed before a React Router build was ready.'
647
637
  );
648
638
  if (state.kind === 'starting') {
649
- state.readiness.reject(closeError);
639
+ Effect.runSync(EffectDeferred.fail(state.readiness, closeError));
650
640
  }
651
641
  state = { kind: 'closed', error: closeError };
652
642
  },