rsbuild-plugin-react-router 0.2.0 → 0.3.1

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 (105) hide show
  1. package/README.md +213 -201
  2. package/dist/451.js +1103 -0
  3. package/dist/bounded-cache.d.ts +1 -0
  4. package/dist/build-manifest.d.ts +7 -4
  5. package/dist/build-output-transforms.d.ts +30 -0
  6. package/dist/concurrency.d.ts +3 -0
  7. package/dist/config-imports.d.ts +10 -0
  8. package/dist/constants.d.ts +3 -0
  9. package/dist/dev-background-resources.d.ts +38 -0
  10. package/dist/dev-generation.d.ts +25 -0
  11. package/dist/dev-runtime-artifacts.d.ts +47 -0
  12. package/dist/dev-runtime-compilation.d.ts +47 -0
  13. package/dist/dev-runtime-controller.d.ts +14 -0
  14. package/dist/dev-runtime-session.d.ts +34 -0
  15. package/dist/dev-server.d.ts +16 -2
  16. package/dist/effect-runtime.d.ts +18 -0
  17. package/dist/export-utils.d.ts +15 -7
  18. package/dist/index.cjs +13775 -1412
  19. package/dist/index.d.ts +8 -1
  20. package/dist/index.js +9429 -1501
  21. package/dist/lazy-compilation-prewarm.d.ts +25 -0
  22. package/dist/lazy-compilation.d.ts +6 -0
  23. package/dist/manifest.d.ts +48 -10
  24. package/dist/modify-browser-manifest.d.ts +30 -13
  25. package/dist/parallel-route-transform-protocol.d.ts +25 -0
  26. package/dist/parallel-route-transform-worker.d.ts +1 -0
  27. package/dist/parallel-route-transform-worker.js +38 -0
  28. package/dist/parallel-route-transforms.d.ts +29 -0
  29. package/dist/performance.d.ts +26 -0
  30. package/dist/plugin-utils.d.ts +2 -12
  31. package/dist/prerender-build.d.ts +36 -0
  32. package/dist/prerender.d.ts +27 -2
  33. package/dist/react-router-config.d.ts +15 -5
  34. package/dist/route-artifacts.d.ts +33 -0
  35. package/dist/route-ast.d.ts +21 -0
  36. package/dist/route-chunks.d.ts +9 -1
  37. package/dist/route-component-transform.d.ts +5 -0
  38. package/dist/route-export-pruning.d.ts +6 -0
  39. package/dist/route-export-resolution.d.ts +5 -0
  40. package/dist/route-transform-tasks.d.ts +47 -0
  41. package/dist/route-watch.d.ts +27 -0
  42. package/dist/server-build-plan.d.ts +22 -0
  43. package/dist/server-build-resolution.d.ts +3 -0
  44. package/dist/server-utils.d.ts +3 -2
  45. package/dist/ssr-externals.d.ts +1 -0
  46. package/dist/templates/entry.server.cjs +3 -3
  47. package/dist/templates/entry.server.js +3 -3
  48. package/dist/typegen.d.ts +15 -0
  49. package/dist/types.d.ts +41 -14
  50. package/dist/virtual-modules.d.ts +2 -0
  51. package/dist/warnings/warn-on-client-source-maps.d.ts +1 -0
  52. package/dist/yuku.d.ts +15 -0
  53. package/package.json +24 -19
  54. package/src/bounded-cache.ts +18 -0
  55. package/src/build-manifest.ts +205 -0
  56. package/src/build-output-transforms.ts +273 -0
  57. package/src/concurrency.ts +15 -0
  58. package/src/config-imports.ts +83 -0
  59. package/src/constants.ts +91 -0
  60. package/src/dev-background-resources.ts +255 -0
  61. package/src/dev-generation.ts +690 -0
  62. package/src/dev-runtime-artifacts.ts +239 -0
  63. package/src/dev-runtime-compilation.ts +171 -0
  64. package/src/dev-runtime-controller.ts +542 -0
  65. package/src/dev-runtime-session.ts +191 -0
  66. package/src/dev-server.ts +88 -0
  67. package/src/effect-runtime.ts +130 -0
  68. package/src/export-utils.ts +278 -0
  69. package/src/index.ts +990 -0
  70. package/src/lazy-compilation-prewarm.ts +279 -0
  71. package/src/lazy-compilation.ts +101 -0
  72. package/src/manifest.ts +591 -0
  73. package/src/modify-browser-manifest.ts +246 -0
  74. package/src/parallel-route-transform-protocol.ts +35 -0
  75. package/src/parallel-route-transform-worker.ts +82 -0
  76. package/src/parallel-route-transforms.ts +458 -0
  77. package/src/performance.ts +254 -0
  78. package/src/plugin-utils.ts +82 -0
  79. package/src/prerender-build.ts +687 -0
  80. package/src/prerender.ts +349 -0
  81. package/src/react-router-config.ts +245 -0
  82. package/src/route-artifacts.ts +155 -0
  83. package/src/route-ast.ts +163 -0
  84. package/src/route-chunks.ts +857 -0
  85. package/src/route-component-transform.ts +314 -0
  86. package/src/route-config.ts +106 -0
  87. package/src/route-export-pruning.ts +668 -0
  88. package/src/route-export-resolution.ts +329 -0
  89. package/src/route-transform-tasks.ts +249 -0
  90. package/src/route-watch.ts +357 -0
  91. package/src/server-build-plan.ts +91 -0
  92. package/src/server-build-resolution.ts +131 -0
  93. package/src/server-utils.ts +111 -0
  94. package/src/ssr-externals.ts +59 -0
  95. package/src/templates/context.ts +12 -0
  96. package/src/templates/entry.client.tsx +12 -0
  97. package/src/templates/entry.server.tsx +76 -0
  98. package/src/typegen.ts +178 -0
  99. package/src/types.ts +92 -0
  100. package/src/validation/validate-plugin-order.ts +76 -0
  101. package/src/virtual-modules.ts +30 -0
  102. package/src/warnings/warn-on-client-source-maps.ts +96 -0
  103. package/src/yuku.ts +67 -0
  104. package/dist/0~rslib-runtime.js +0 -16
  105. package/dist/babel.d.ts +0 -8
@@ -0,0 +1,690 @@
1
+ import type { RsbuildDevServer, Rspack } from '@rsbuild/core';
2
+ import * as EffectDeferred from 'effect/Deferred';
3
+ import * as Effect from 'effect/Effect';
4
+ import type { ServerBuild } from 'react-router';
5
+ import {
6
+ evaluateServerBuilds,
7
+ getEnvironmentStats,
8
+ isSafeOneSidedChange,
9
+ pinServerBuildsToManifests,
10
+ snapshotDependencies,
11
+ type DependencySnapshot,
12
+ type DevCompilationIdentity,
13
+ type DevGraphChanges,
14
+ type DevGraphIdentity,
15
+ type DevRuntimeStats,
16
+ type ReactRouterDevBuildPlan,
17
+ type ReactRouterDevManifestSet,
18
+ type ReactRouterServerBuilds,
19
+ type WebArtifact,
20
+ } from './dev-runtime-artifacts.js';
21
+ import { normalizeEffectError, runPluginEffect } from './effect-runtime.js';
22
+
23
+ export { snapshotDevChangedFiles } from './dev-runtime-artifacts.js';
24
+ export type {
25
+ DevChangedFiles,
26
+ DevGraphChanges,
27
+ DevGraphIdentity,
28
+ ReactRouterDevBuildPlan,
29
+ ReactRouterDevManifest,
30
+ ReactRouterDevManifestSet,
31
+ } from './dev-runtime-artifacts.js';
32
+
33
+ type CommittedGeneration = {
34
+ buildsByEntryName: ReactRouterServerBuilds;
35
+ webIdentity: DevCompilationIdentity;
36
+ nodeIdentity: DevCompilationIdentity;
37
+ web: WebArtifact;
38
+ nodeDependencies: DependencySnapshot;
39
+ };
40
+
41
+ type RuntimeState =
42
+ | {
43
+ kind: 'starting';
44
+ attemptId: number;
45
+ readiness: EffectDeferred.Deferred<CommittedGeneration, Error>;
46
+ }
47
+ | { kind: 'failed'; attemptId: number; error: Error }
48
+ | {
49
+ kind: 'ready';
50
+ committed: CommittedGeneration;
51
+ pendingAttemptId: number | null;
52
+ }
53
+ | { kind: 'closed'; error: Error };
54
+
55
+ export type ReactRouterDevRuntime = {
56
+ beginAttempt: () => void;
57
+ captureWeb: (
58
+ compilation: Rspack.Compilation,
59
+ manifestsByEntryName: ReactRouterDevManifestSet
60
+ ) => void;
61
+ finishAttempt: (
62
+ stats: DevRuntimeStats,
63
+ changes: DevGraphChanges,
64
+ identity: DevGraphIdentity
65
+ ) => Promise<'committed' | 'ignored' | 'retry-node'>;
66
+ failAttempt: (error: Error) => void;
67
+ load: (entryName?: string) => Promise<ServerBuild>;
68
+ close: (error?: Error) => void;
69
+ };
70
+
71
+ type CreateReactRouterDevRuntimeOptions = {
72
+ server: RsbuildDevServer;
73
+ buildPlan: ReactRouterDevBuildPlan;
74
+ onEvaluationError: (error: Error) => void;
75
+ onCssAssetOwnershipChanged?: (change: 'removed' | 'restored') => void;
76
+ onRouteManifestChanged?: () => void;
77
+ onWarning?: (message: string) => void;
78
+ };
79
+
80
+ const collectManifestCssAssetOwnership = (
81
+ manifest: ReactRouterDevManifestSet[string]
82
+ ): Set<string> => {
83
+ const ownership = new Set<string>();
84
+ for (const asset of manifest.entry?.css ?? []) {
85
+ ownership.add(`entry\0${asset}`);
86
+ }
87
+ for (const [routeId, route] of Object.entries(manifest.routes ?? {})) {
88
+ for (const asset of route.css ?? []) {
89
+ ownership.add(`route\0${routeId}\0${asset}`);
90
+ }
91
+ }
92
+ return ownership;
93
+ };
94
+
95
+ const hasRemovedCssAssetOwnership = (
96
+ previous: ReactRouterDevManifestSet,
97
+ next: ReactRouterDevManifestSet
98
+ ): boolean => {
99
+ for (const [entryName, previousManifest] of Object.entries(previous)) {
100
+ const previousOwnership =
101
+ collectManifestCssAssetOwnership(previousManifest);
102
+ if (previousOwnership.size === 0) {
103
+ continue;
104
+ }
105
+ const nextManifest = next[entryName];
106
+ if (!nextManifest) {
107
+ return true;
108
+ }
109
+ const nextOwnership = collectManifestCssAssetOwnership(nextManifest);
110
+ for (const owner of previousOwnership) {
111
+ if (!nextOwnership.has(owner)) {
112
+ return true;
113
+ }
114
+ }
115
+ }
116
+ return false;
117
+ };
118
+
119
+ const hasAddedCssAssetOwnership = (
120
+ previous: ReactRouterDevManifestSet,
121
+ next: ReactRouterDevManifestSet
122
+ ): boolean => hasRemovedCssAssetOwnership(next, previous);
123
+
124
+ const collectManifestCssAssets = (
125
+ manifest: ReactRouterDevManifestSet[string]
126
+ ): Set<string> => {
127
+ const assets = new Set(manifest.entry?.css ?? []);
128
+ for (const route of Object.values(manifest.routes ?? {})) {
129
+ for (const asset of route.css ?? []) {
130
+ assets.add(asset);
131
+ }
132
+ }
133
+ return assets;
134
+ };
135
+
136
+ const normalizeManifestForCssOwnershipCheck = (
137
+ manifest: ReactRouterDevManifestSet[string]
138
+ ) => {
139
+ const cssAssets = collectManifestCssAssets(manifest);
140
+ const nonCssImports = (imports: string[] = []) =>
141
+ imports.filter(importPath => !cssAssets.has(importPath));
142
+
143
+ return {
144
+ entry: {
145
+ imports: nonCssImports(manifest.entry?.imports),
146
+ module: manifest.entry?.module,
147
+ },
148
+ routes: Object.fromEntries(
149
+ Object.entries(manifest.routes ?? {})
150
+ .sort(([left], [right]) => left.localeCompare(right))
151
+ .map(([routeId, route]) => [
152
+ routeId,
153
+ {
154
+ caseSensitive: route.caseSensitive,
155
+ clientActionModule: route.clientActionModule,
156
+ clientLoaderModule: route.clientLoaderModule,
157
+ clientMiddlewareModule: route.clientMiddlewareModule,
158
+ errorBoundary: route.hasErrorBoundary,
159
+ hasAction: route.hasAction,
160
+ hasClientAction: route.hasClientAction,
161
+ hasClientLoader: route.hasClientLoader,
162
+ hasClientMiddleware: route.hasClientMiddleware,
163
+ hasDefaultExport: route.hasDefaultExport,
164
+ hasLoader: route.hasLoader,
165
+ hydrateFallbackModule: route.hydrateFallbackModule,
166
+ id: route.id,
167
+ imports: nonCssImports(route.imports),
168
+ index: route.index,
169
+ module: route.module,
170
+ parentId: route.parentId,
171
+ path: route.path,
172
+ },
173
+ ])
174
+ ),
175
+ };
176
+ };
177
+
178
+ const hasOnlyCssAssetOwnershipChanges = (
179
+ previous: ReactRouterDevManifestSet,
180
+ next: ReactRouterDevManifestSet
181
+ ): boolean => {
182
+ const previousEntryNames = Object.keys(previous).sort();
183
+ const nextEntryNames = Object.keys(next).sort();
184
+ if (previousEntryNames.join('\0') !== nextEntryNames.join('\0')) {
185
+ return false;
186
+ }
187
+ return previousEntryNames.every(entryName => {
188
+ const previousManifest = normalizeManifestForCssOwnershipCheck(
189
+ previous[entryName]
190
+ );
191
+ const nextManifest = normalizeManifestForCssOwnershipCheck(next[entryName]);
192
+ return JSON.stringify(previousManifest) === JSON.stringify(nextManifest);
193
+ });
194
+ };
195
+
196
+ type DevRouteManifestEntry = NonNullable<
197
+ ReactRouterDevManifestSet[string]['routes']
198
+ >[string];
199
+
200
+ const hasSameRouteMetadata = (
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;
220
+
221
+ const hasRouteManifestMetadataChanges = (
222
+ previous: ReactRouterDevManifestSet,
223
+ next: ReactRouterDevManifestSet
224
+ ): boolean => {
225
+ const previousEntryNames = Object.keys(previous);
226
+ if (previousEntryNames.length !== Object.keys(next).length) {
227
+ return true;
228
+ }
229
+ for (const entryName of previousEntryNames) {
230
+ const nextManifest = next[entryName];
231
+ if (!nextManifest) {
232
+ return true;
233
+ }
234
+ const previousRoutes = previous[entryName].routes ?? {};
235
+ const nextRoutes = nextManifest.routes ?? {};
236
+ const previousRouteIds = Object.keys(previousRoutes);
237
+ if (previousRouteIds.length !== Object.keys(nextRoutes).length) {
238
+ return true;
239
+ }
240
+ for (const routeId of previousRouteIds) {
241
+ const previousRoute = previousRoutes[routeId];
242
+ const nextRoute = nextRoutes[routeId];
243
+ if (!nextRoute || !hasSameRouteMetadata(previousRoute, nextRoute)) {
244
+ return true;
245
+ }
246
+ }
247
+ }
248
+ return false;
249
+ };
250
+
251
+ const createReadinessDeferred = (): EffectDeferred.Deferred<
252
+ CommittedGeneration,
253
+ Error
254
+ > => Effect.runSync(EffectDeferred.make<CommittedGeneration, Error>());
255
+
256
+ export const createReactRouterDevRuntime = ({
257
+ server,
258
+ buildPlan,
259
+ onEvaluationError,
260
+ onCssAssetOwnershipChanged = () => undefined,
261
+ onRouteManifestChanged = () => undefined,
262
+ onWarning = () => undefined,
263
+ }: CreateReactRouterDevRuntimeOptions): ReactRouterDevRuntime => {
264
+ let nextAttemptId = 1;
265
+ let reloadAfterCssRemoval = false;
266
+ let state: RuntimeState = {
267
+ kind: 'starting',
268
+ attemptId: 0,
269
+ readiness: createReadinessDeferred(),
270
+ };
271
+ const manifestsByCompilation = new WeakMap<
272
+ Rspack.Compilation,
273
+ ReactRouterDevManifestSet
274
+ >();
275
+
276
+ const notifyCssAssetOwnershipChanged = (
277
+ change: 'removed' | 'restored'
278
+ ): void => {
279
+ try {
280
+ onCssAssetOwnershipChanged(change);
281
+ } catch (cause) {
282
+ onWarning(
283
+ `[rsbuild-plugin-react-router] Failed to notify the browser after CSS asset ownership changed: ${normalizeEffectError(cause).message}`
284
+ );
285
+ }
286
+ };
287
+
288
+ const notifyRouteManifestChanged = (): void => {
289
+ try {
290
+ onRouteManifestChanged();
291
+ } catch (cause) {
292
+ onWarning(
293
+ `[rsbuild-plugin-react-router] Failed to notify the browser after route manifest metadata changed: ${normalizeEffectError(cause).message}`
294
+ );
295
+ }
296
+ };
297
+
298
+ const uniqueEntryNames = new Set(buildPlan.entryNames);
299
+ if (
300
+ uniqueEntryNames.size !== buildPlan.entryNames.length ||
301
+ !uniqueEntryNames.has(buildPlan.defaultEntryName)
302
+ ) {
303
+ throw new Error(
304
+ '[rsbuild-plugin-react-router] The development server build plan must contain unique entries and include its default entry.'
305
+ );
306
+ }
307
+
308
+ const selectBuild = (
309
+ generation: CommittedGeneration,
310
+ requestedEntryName?: string
311
+ ): ServerBuild => {
312
+ const entryName = requestedEntryName ?? buildPlan.defaultEntryName;
313
+ const build = generation.buildsByEntryName[entryName];
314
+ if (!build) {
315
+ throw new Error(
316
+ `[rsbuild-plugin-react-router] Committed React Router server build ${JSON.stringify(entryName)} was not found.`
317
+ );
318
+ }
319
+ return build;
320
+ };
321
+
322
+ const getCurrentAttemptId = (): number | null => {
323
+ if (state.kind === 'starting') {
324
+ return state.attemptId;
325
+ }
326
+ return state.kind === 'ready' ? state.pendingAttemptId : null;
327
+ };
328
+
329
+ const isCurrentAttempt = (attemptId: number): boolean =>
330
+ getCurrentAttemptId() === attemptId;
331
+
332
+ const rejectAttempt = (
333
+ attemptId: number,
334
+ error: Error,
335
+ report: boolean
336
+ ): void => {
337
+ if (!isCurrentAttempt(attemptId)) {
338
+ return;
339
+ }
340
+ if (state.kind === 'starting') {
341
+ const { readiness } = state;
342
+ state = { kind: 'failed', attemptId, error };
343
+ Effect.runSync(EffectDeferred.fail(readiness, error));
344
+ } else if (state.kind === 'ready') {
345
+ state = { ...state, pendingAttemptId: null };
346
+ }
347
+ if (report) {
348
+ onEvaluationError(error);
349
+ }
350
+ };
351
+
352
+ const commit = (
353
+ attemptId: number,
354
+ committed: CommittedGeneration
355
+ ): boolean => {
356
+ if (!isCurrentAttempt(attemptId)) {
357
+ return false;
358
+ }
359
+ if (state.kind === 'starting') {
360
+ const { readiness } = state;
361
+ state = { kind: 'ready', committed, pendingAttemptId: null };
362
+ Effect.runSync(EffectDeferred.succeed(readiness, committed));
363
+ } else if (state.kind === 'ready') {
364
+ state = { kind: 'ready', committed, pendingAttemptId: null };
365
+ }
366
+ return true;
367
+ };
368
+
369
+ const discardUnsafeOneSidedResult = (
370
+ attemptId: number,
371
+ previous: CommittedGeneration,
372
+ webChanged: boolean,
373
+ changes: DevGraphChanges
374
+ ): boolean => {
375
+ const side = webChanged ? 'web-only' : 'node-only';
376
+ const changedFiles = webChanged ? changes.web : changes.node;
377
+ const unchangedDependencies = webChanged
378
+ ? previous.nodeDependencies
379
+ : previous.web.dependencies;
380
+ if (isSafeOneSidedChange(changedFiles, unchangedDependencies)) {
381
+ return false;
382
+ }
383
+ onWarning(
384
+ `[rsbuild-plugin-react-router] Discarded an incomplete ${side} development result and kept the last-good build.`
385
+ );
386
+ rejectAttempt(
387
+ attemptId,
388
+ new Error(`Incomplete ${side} development result.`),
389
+ false
390
+ );
391
+ return true;
392
+ };
393
+
394
+ return {
395
+ beginAttempt(): void {
396
+ if (state.kind === 'closed') {
397
+ return;
398
+ }
399
+ const attemptId = nextAttemptId++;
400
+ if (state.kind === 'failed') {
401
+ state = {
402
+ kind: 'starting',
403
+ attemptId,
404
+ readiness: createReadinessDeferred(),
405
+ };
406
+ } else if (state.kind === 'starting') {
407
+ state = { ...state, attemptId };
408
+ } else {
409
+ state = { ...state, pendingAttemptId: attemptId };
410
+ }
411
+ },
412
+
413
+ captureWeb(compilation, manifestsByEntryName): void {
414
+ if (state.kind !== 'closed') {
415
+ manifestsByCompilation.set(
416
+ compilation,
417
+ structuredClone(manifestsByEntryName)
418
+ );
419
+ }
420
+ },
421
+
422
+ async finishAttempt(stats, changes, identity) {
423
+ const attemptId = getCurrentAttemptId();
424
+ if (attemptId === null) {
425
+ return 'ignored';
426
+ }
427
+ const webStats = getEnvironmentStats(stats, 'web');
428
+ const nodeStats = getEnvironmentStats(stats, 'node');
429
+ if (!webStats || !nodeStats) {
430
+ rejectAttempt(
431
+ attemptId,
432
+ new Error(
433
+ '[rsbuild-plugin-react-router] Development compilation did not provide both web and node results.'
434
+ ),
435
+ true
436
+ );
437
+ return 'ignored';
438
+ }
439
+ if (
440
+ webStats.compilation.needAdditionalPass ||
441
+ nodeStats.compilation.needAdditionalPass
442
+ ) {
443
+ return 'ignored';
444
+ }
445
+ if (webStats.hasErrors() || nodeStats.hasErrors()) {
446
+ rejectAttempt(
447
+ attemptId,
448
+ new Error(
449
+ '[rsbuild-plugin-react-router] The React Router development compilation failed.'
450
+ ),
451
+ false
452
+ );
453
+ return 'ignored';
454
+ }
455
+
456
+ const webCompilation = webStats.compilation;
457
+ const nodeCompilation = nodeStats.compilation;
458
+ const webIdentity = identity.web;
459
+ const nodeIdentity = identity.node;
460
+ if (!webIdentity || !nodeIdentity) {
461
+ rejectAttempt(
462
+ attemptId,
463
+ new Error(
464
+ '[rsbuild-plugin-react-router] Development compilation identity was unavailable.'
465
+ ),
466
+ true
467
+ );
468
+ return 'ignored';
469
+ }
470
+ const previous = state.kind === 'ready' ? state.committed : undefined;
471
+ const webChanged = !previous || previous.webIdentity !== webIdentity;
472
+ const nodeChanged = !previous || previous.nodeIdentity !== nodeIdentity;
473
+
474
+ if (!webChanged && !nodeChanged) {
475
+ return 'ignored';
476
+ }
477
+
478
+ const manifestsByEntryName = webChanged
479
+ ? manifestsByCompilation.get(webCompilation)
480
+ : previous?.web.manifestsByEntryName;
481
+ if (!manifestsByEntryName) {
482
+ rejectAttempt(
483
+ attemptId,
484
+ new Error(
485
+ '[rsbuild-plugin-react-router] The web compilation completed without a matching React Router manifest. Keeping the last-good development build.'
486
+ ),
487
+ true
488
+ );
489
+ return 'ignored';
490
+ }
491
+ const cssAssetsRemoved =
492
+ !!previous &&
493
+ webChanged &&
494
+ hasRemovedCssAssetOwnership(
495
+ previous.web.manifestsByEntryName,
496
+ manifestsByEntryName
497
+ );
498
+ const cssAssetsAdded =
499
+ !!previous &&
500
+ webChanged &&
501
+ hasAddedCssAssetOwnership(
502
+ previous.web.manifestsByEntryName,
503
+ manifestsByEntryName
504
+ );
505
+ const cssOnlyWebManifestChange =
506
+ (cssAssetsRemoved || cssAssetsAdded) &&
507
+ hasOnlyCssAssetOwnershipChanges(
508
+ previous.web.manifestsByEntryName,
509
+ manifestsByEntryName
510
+ );
511
+ const routeManifestMetadataChanged =
512
+ !!previous &&
513
+ webChanged &&
514
+ hasRouteManifestMetadataChanges(
515
+ previous.web.manifestsByEntryName,
516
+ manifestsByEntryName
517
+ );
518
+ const reusePreviousNodeBuild =
519
+ !!previous &&
520
+ cssOnlyWebManifestChange &&
521
+ (!nodeChanged || identity.nodeWeb !== webIdentity);
522
+
523
+ if (
524
+ nodeChanged &&
525
+ identity.nodeWeb !== webIdentity &&
526
+ !identity.attempt &&
527
+ !reusePreviousNodeBuild
528
+ ) {
529
+ const message =
530
+ '[rsbuild-plugin-react-router] Discarded web and node results from different compiler cycles and kept the last-good build.';
531
+ if (!previous) {
532
+ return 'retry-node';
533
+ }
534
+ onWarning(message);
535
+ rejectAttempt(attemptId, new Error(message), false);
536
+ return 'retry-node';
537
+ }
538
+
539
+ const shouldEvaluateNode = nodeChanged && !reusePreviousNodeBuild;
540
+ if (
541
+ previous &&
542
+ webChanged !== shouldEvaluateNode &&
543
+ !cssOnlyWebManifestChange &&
544
+ discardUnsafeOneSidedResult(attemptId, previous, webChanged, changes)
545
+ ) {
546
+ return 'ignored';
547
+ }
548
+
549
+ try {
550
+ const buildsByEntryName = shouldEvaluateNode
551
+ ? await evaluateServerBuilds(server, buildPlan.entryNames)
552
+ : previous!.buildsByEntryName;
553
+ if (!isCurrentAttempt(attemptId)) {
554
+ return 'ignored';
555
+ }
556
+ const web = webChanged
557
+ ? {
558
+ manifestsByEntryName,
559
+ dependencies: snapshotDependencies(webCompilation),
560
+ }
561
+ : previous!.web;
562
+ const committed = commit(attemptId, {
563
+ buildsByEntryName: pinServerBuildsToManifests(
564
+ buildsByEntryName,
565
+ buildPlan.entryNames,
566
+ web.manifestsByEntryName
567
+ ),
568
+ webIdentity,
569
+ nodeIdentity: shouldEvaluateNode
570
+ ? nodeIdentity
571
+ : previous!.nodeIdentity,
572
+ web,
573
+ nodeDependencies: shouldEvaluateNode
574
+ ? snapshotDependencies(nodeCompilation)
575
+ : previous!.nodeDependencies,
576
+ });
577
+ if (!committed) {
578
+ return 'ignored';
579
+ }
580
+ if (cssAssetsRemoved) {
581
+ reloadAfterCssRemoval = !cssAssetsAdded;
582
+ notifyCssAssetOwnershipChanged('removed');
583
+ } else if (cssAssetsAdded) {
584
+ if (reloadAfterCssRemoval) {
585
+ notifyCssAssetOwnershipChanged('restored');
586
+ }
587
+ reloadAfterCssRemoval = false;
588
+ }
589
+ if (routeManifestMetadataChanged) {
590
+ notifyRouteManifestChanged();
591
+ }
592
+ return 'committed';
593
+ } catch (cause) {
594
+ rejectAttempt(attemptId, normalizeEffectError(cause), true);
595
+ return 'ignored';
596
+ }
597
+ },
598
+
599
+ failAttempt(error): void {
600
+ const attemptId = getCurrentAttemptId();
601
+ if (attemptId !== null) {
602
+ rejectAttempt(attemptId, error, false);
603
+ }
604
+ },
605
+
606
+ load(entryName?: string): Promise<ServerBuild> {
607
+ if (entryName && !uniqueEntryNames.has(entryName)) {
608
+ return Promise.reject(
609
+ new Error(
610
+ `[rsbuild-plugin-react-router] React Router server build ${JSON.stringify(entryName)} is not part of this development server build plan.`
611
+ )
612
+ );
613
+ }
614
+ if (state.kind === 'ready') {
615
+ return Promise.resolve(selectBuild(state.committed, entryName));
616
+ }
617
+ if (state.kind === 'starting') {
618
+ const selected = runPluginEffect(
619
+ EffectDeferred.await(state.readiness).pipe(
620
+ Effect.map(generation => selectBuild(generation, entryName))
621
+ )
622
+ );
623
+ void selected.catch(() => undefined);
624
+ return selected;
625
+ }
626
+ return Promise.reject(state.error);
627
+ },
628
+
629
+ close(error?: Error): void {
630
+ if (state.kind === 'closed') {
631
+ return;
632
+ }
633
+ const closeError =
634
+ error ??
635
+ new Error(
636
+ '[rsbuild-plugin-react-router] The development server closed before a React Router build was ready.'
637
+ );
638
+ if (state.kind === 'starting') {
639
+ Effect.runSync(EffectDeferred.fail(state.readiness, closeError));
640
+ }
641
+ state = { kind: 'closed', error: closeError };
642
+ },
643
+ };
644
+ };
645
+
646
+ const DEV_RUNTIME_KEY = Symbol.for(
647
+ 'rsbuild-plugin-react-router.dev-runtime.v1'
648
+ );
649
+
650
+ const getRegisteredRuntime = (
651
+ server: RsbuildDevServer
652
+ ): ReactRouterDevRuntime | undefined =>
653
+ Reflect.get(server, DEV_RUNTIME_KEY) as ReactRouterDevRuntime | undefined;
654
+
655
+ export const registerReactRouterDevRuntime = (
656
+ server: RsbuildDevServer,
657
+ runtime: ReactRouterDevRuntime
658
+ ): void => {
659
+ // Symbol.for keeps registration shared when the plugin and public helper are
660
+ // loaded through different ESM and CommonJS package entrypoints.
661
+ Object.defineProperty(server, DEV_RUNTIME_KEY, {
662
+ configurable: true,
663
+ enumerable: false,
664
+ value: runtime,
665
+ });
666
+ };
667
+
668
+ export const unregisterReactRouterDevRuntime = (
669
+ server: RsbuildDevServer,
670
+ runtime: ReactRouterDevRuntime
671
+ ): void => {
672
+ if (getRegisteredRuntime(server) === runtime) {
673
+ Reflect.deleteProperty(server, DEV_RUNTIME_KEY);
674
+ }
675
+ };
676
+
677
+ export const loadReactRouterServerBuild = (
678
+ server: RsbuildDevServer,
679
+ entryName?: string
680
+ ): Promise<ServerBuild> => {
681
+ const runtime = getRegisteredRuntime(server);
682
+ if (!runtime) {
683
+ return Promise.reject(
684
+ new Error(
685
+ '[rsbuild-plugin-react-router] This Rsbuild development server is not registered with the React Router plugin. Add pluginReactRouter() before calling loadReactRouterServerBuild().'
686
+ )
687
+ );
688
+ }
689
+ return runtime.load(entryName);
690
+ };