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