rsbuild-plugin-react-router 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +88 -35
  2. package/dist/451.js +164 -30
  3. package/dist/build-manifest.d.ts +6 -3
  4. package/dist/build-output-transforms.d.ts +2 -1
  5. package/dist/concurrency.d.ts +2 -1
  6. package/dist/config-imports.d.ts +7 -0
  7. package/dist/dev-background-resources.d.ts +38 -0
  8. package/dist/dev-generation.d.ts +2 -2
  9. package/dist/dev-hmr.d.ts +45 -0
  10. package/dist/dev-runtime-artifacts.d.ts +9 -1
  11. package/dist/dev-runtime-compilation.d.ts +17 -2
  12. package/dist/dev-runtime-controller.d.ts +6 -1
  13. package/dist/dev-server.d.ts +5 -0
  14. package/dist/effect-runtime.d.ts +18 -0
  15. package/dist/export-utils.d.ts +2 -2
  16. package/dist/index.cjs +11852 -848
  17. package/dist/index.d.ts +5 -0
  18. package/dist/index.js +8289 -773
  19. package/dist/lazy-compilation-prewarm.d.ts +25 -0
  20. package/dist/lazy-compilation.d.ts +2 -1
  21. package/dist/manifest.d.ts +19 -4
  22. package/dist/parallel-route-transforms.d.ts +17 -2
  23. package/dist/prerender-build.d.ts +4 -0
  24. package/dist/prerender.d.ts +0 -1
  25. package/dist/react-router-config.d.ts +8 -5
  26. package/dist/route-artifacts.d.ts +10 -2
  27. package/dist/route-transform-tasks.d.ts +3 -0
  28. package/dist/server-build-resolution.d.ts +3 -0
  29. package/dist/ssr-externals.d.ts +1 -0
  30. package/dist/typegen.d.ts +14 -1
  31. package/dist/types.d.ts +15 -6
  32. package/package.json +18 -10
  33. package/src/build-manifest.ts +110 -73
  34. package/src/build-output-transforms.ts +5 -0
  35. package/src/concurrency.ts +3 -22
  36. package/src/config-imports.ts +38 -0
  37. package/src/dev-background-resources.ts +255 -0
  38. package/src/dev-generation.ts +43 -53
  39. package/src/dev-hmr.ts +431 -0
  40. package/src/dev-runtime-artifacts.ts +50 -18
  41. package/src/dev-runtime-compilation.ts +80 -1
  42. package/src/dev-runtime-controller.ts +155 -31
  43. package/src/dev-runtime-session.ts +18 -11
  44. package/src/dev-server.ts +31 -1
  45. package/src/effect-runtime.ts +130 -0
  46. package/src/export-utils.ts +82 -23
  47. package/src/index.ts +166 -153
  48. package/src/lazy-compilation-prewarm.ts +279 -0
  49. package/src/lazy-compilation.ts +12 -5
  50. package/src/manifest.ts +366 -255
  51. package/src/modify-browser-manifest.ts +2 -1
  52. package/src/parallel-route-transforms.ts +195 -69
  53. package/src/prerender-build.ts +147 -63
  54. package/src/prerender.ts +1 -19
  55. package/src/react-router-config.ts +98 -73
  56. package/src/route-artifacts.ts +122 -3
  57. package/src/route-export-resolution.ts +3 -3
  58. package/src/route-transform-tasks.ts +173 -2
  59. package/src/route-watch.ts +119 -84
  60. package/src/server-build-resolution.ts +131 -0
  61. package/src/server-utils.ts +7 -106
  62. package/src/ssr-externals.ts +1 -1
  63. package/src/typegen.ts +162 -33
  64. package/src/types.ts +16 -6
package/src/dev-hmr.ts ADDED
@@ -0,0 +1,431 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import type { Rspack } from '@rsbuild/core';
4
+ import { dirname, join } from 'pathe';
5
+
6
+ import { HMR_PATCHABLE_ROUTE_FLAGS } from './route-artifacts.js';
7
+
8
+ export const DEV_HMR_RUNTIME_MODULE_ID = 'virtual/react-router/hmr-runtime';
9
+
10
+ type SwcLoaderOptions = {
11
+ jsc?: { transform?: { react?: { refresh?: boolean } } };
12
+ };
13
+
14
+ const isObject = (value: unknown): value is Record<string, unknown> =>
15
+ value !== null && typeof value === 'object';
16
+
17
+ const isSwcLoader = (loader: unknown): boolean =>
18
+ typeof loader === 'string' && loader.includes('builtin:swc-loader');
19
+
20
+ const hasReactRefresh = (options: unknown): boolean =>
21
+ (options as SwcLoaderOptions | undefined)?.jsc?.transform?.react?.refresh ===
22
+ true;
23
+
24
+ const readSwcLoaderRefresh = (value: unknown): boolean => {
25
+ if (Array.isArray(value)) {
26
+ return value.some(readSwcLoaderRefresh);
27
+ }
28
+ if (!isObject(value)) {
29
+ return false;
30
+ }
31
+ if (isSwcLoader(value.loader)) {
32
+ return hasReactRefresh(value.options);
33
+ }
34
+ return Object.values(value).some(readSwcLoaderRefresh);
35
+ };
36
+
37
+ const readRuleSwcRefresh = (rule: unknown): boolean => {
38
+ if (!isObject(rule)) {
39
+ return false;
40
+ }
41
+ if (isSwcLoader(rule.loader)) {
42
+ return hasReactRefresh(rule.options);
43
+ }
44
+ return (
45
+ readSwcLoaderRefresh(rule.use) ||
46
+ readRuleSetSwcRefresh(rule.oneOf) ||
47
+ readRuleSetSwcRefresh(rule.rules)
48
+ );
49
+ };
50
+
51
+ const readRuleSetSwcRefresh = (rules: unknown): boolean =>
52
+ Array.isArray(rules) && rules.some(readRuleSwcRefresh);
53
+
54
+ export const isRspackSwcReactRefreshEnabled = (
55
+ rspackConfig: Rspack.Configuration
56
+ ): boolean => readRuleSetSwcRefresh(rspackConfig.module?.rules);
57
+
58
+ /**
59
+ * Resolves the `react-refresh/runtime` module that
60
+ * `@rspack/plugin-react-refresh` injects into the web bundle. The resolution
61
+ * walks the same dependency chain the refresh plugin uses so the returned file
62
+ * is the exact runtime instance already present in the browser module graph.
63
+ * Returns `undefined` when React Fast Refresh is unavailable, in which case
64
+ * dev HMR falls back to full reloads.
65
+ */
66
+ export const resolveReactRefreshRuntimePath = (
67
+ rootPath: string
68
+ ): string | undefined => {
69
+ const resolveFrom = (base: string, request: string): string =>
70
+ createRequire(base).resolve(request);
71
+ const rootPackageJson = join(rootPath, 'package.json');
72
+ try {
73
+ const pluginReactEntry = resolveFrom(
74
+ rootPackageJson,
75
+ '@rsbuild/plugin-react'
76
+ );
77
+ const refreshPluginEntry = resolveFrom(
78
+ pluginReactEntry,
79
+ '@rspack/plugin-react-refresh'
80
+ );
81
+ return resolveFrom(refreshPluginEntry, 'react-refresh/runtime');
82
+ } catch {
83
+ return undefined;
84
+ }
85
+ };
86
+
87
+ const hdrRevisionModuleContent = (revision: number): string =>
88
+ `export default ${revision};\n`;
89
+
90
+ /**
91
+ * The HDR revision module is a real file (not a virtual module) because it
92
+ * must wake the web compiler through the regular file watcher: the browser
93
+ * HMR runtime imports it, so bumping the revision produces a web hot update
94
+ * whenever server code changes, which the client answers by revalidating
95
+ * React Router loader data.
96
+ */
97
+ export const DEV_HDR_REVISION_RELATIVE_PATH = '.react-router/hdr-revision.mjs';
98
+
99
+ export const getDevHdrRevisionFilePath = (rootPath: string): string =>
100
+ join(rootPath, DEV_HDR_REVISION_RELATIVE_PATH);
101
+
102
+ export type DevHdrRevisionSignal = {
103
+ /** Writes the initial revision module so the first compile can resolve it. */
104
+ ensure: () => void;
105
+ /** Increments the revision, signaling hot data revalidation to the client. */
106
+ bump: () => void;
107
+ };
108
+
109
+ export const createDevHdrRevisionSignal = ({
110
+ filePath,
111
+ onError,
112
+ }: {
113
+ filePath: string;
114
+ onError?: (error: Error) => void;
115
+ }): DevHdrRevisionSignal => {
116
+ let revision = 0;
117
+ let dirEnsured = false;
118
+ const write = (): void => {
119
+ try {
120
+ if (!dirEnsured) {
121
+ mkdirSync(dirname(filePath), { recursive: true });
122
+ dirEnsured = true;
123
+ }
124
+ writeFileSync(filePath, hdrRevisionModuleContent(revision));
125
+ } catch (error) {
126
+ onError?.(error instanceof Error ? error : new Error(String(error)));
127
+ }
128
+ };
129
+ return {
130
+ ensure: write,
131
+ bump() {
132
+ revision += 1;
133
+ write();
134
+ },
135
+ };
136
+ };
137
+
138
+ /**
139
+ * Browser-side HMR runtime shared by all route client entries in development.
140
+ *
141
+ * This mirrors React Router's Vite HMR contract (see `refresh-utils.mjs` in
142
+ * `@react-router/dev`): route module updates are applied by patching
143
+ * `window.__reactRouterRouteModules` while preserving the previous component
144
+ * identities (React Fast Refresh swaps their implementations in place),
145
+ * recreating the client routes with revalidation opt-out, revalidating loader
146
+ * data, and finally performing a React refresh.
147
+ */
148
+ export const generateDevHmrRuntimeModule = ({
149
+ reactRefreshRuntimePath,
150
+ hdrRevisionFilePath,
151
+ }: {
152
+ reactRefreshRuntimePath: string;
153
+ hdrRevisionFilePath: string;
154
+ }): string => `
155
+ import * as __refreshRuntimeModule from ${JSON.stringify(reactRefreshRuntimePath)};
156
+ // Read revision so the import survives sideEffects: false tree-shaking.
157
+ import __hdrRevision from ${JSON.stringify(hdrRevisionFilePath)};
158
+
159
+ void __hdrRevision;
160
+
161
+ const RefreshRuntime =
162
+ __refreshRuntimeModule && __refreshRuntimeModule.performReactRefresh
163
+ ? __refreshRuntimeModule
164
+ : __refreshRuntimeModule.default;
165
+
166
+ const pendingRouteUpdates = new Map();
167
+ let flushTimeout;
168
+ let pendingRevalidation = false;
169
+
170
+ function getCurrentRouterPath(router) {
171
+ const basename = router.basename || '/';
172
+ let pathname = window.location.pathname;
173
+ if (basename !== '/' && pathname.startsWith(basename)) {
174
+ pathname = pathname.slice(basename.length) || '/';
175
+ // A trailing-slash basename (e.g. "/mybase/") consumes the leading slash,
176
+ // leaving a relative path that react-router resolves against the current
177
+ // location and doubles. Force it back to absolute.
178
+ if (pathname[0] !== '/') pathname = '/' + pathname;
179
+ }
180
+ return pathname + window.location.search + window.location.hash;
181
+ }
182
+
183
+ export function registerReactRouterRouteExports(routeId, moduleExports) {
184
+ if (
185
+ typeof window === 'undefined' ||
186
+ !RefreshRuntime ||
187
+ typeof RefreshRuntime.register !== 'function'
188
+ ) {
189
+ return;
190
+ }
191
+ for (const key in moduleExports) {
192
+ if (key === '__esModule') continue;
193
+ const exportValue = moduleExports[key];
194
+ if (RefreshRuntime.isLikelyComponentType(exportValue)) {
195
+ RefreshRuntime.register(exportValue, routeId + ' export ' + key);
196
+ }
197
+ }
198
+ }
199
+
200
+ export function scheduleReactRouterRouteUpdate(
201
+ routeId,
202
+ routeFlags,
203
+ getRouteModuleExports
204
+ ) {
205
+ pendingRouteUpdates.set(routeId, { routeFlags, getRouteModuleExports });
206
+ scheduleFlush();
207
+ }
208
+
209
+ export function scheduleReactRouterRevalidation() {
210
+ pendingRevalidation = true;
211
+ scheduleFlush();
212
+ }
213
+
214
+ function scheduleFlush() {
215
+ if (typeof window === 'undefined') {
216
+ return;
217
+ }
218
+ clearTimeout(flushTimeout);
219
+ flushTimeout = setTimeout(flush, 16);
220
+ }
221
+
222
+ function takePendingRouteUpdates() {
223
+ const updates = Array.from(pendingRouteUpdates, ([routeId, update]) => ({
224
+ routeId,
225
+ update,
226
+ }));
227
+ pendingRouteUpdates.clear();
228
+ return updates;
229
+ }
230
+
231
+ function getRouteMetadata(routeFlags) {
232
+ return {
233
+ ${HMR_PATCHABLE_ROUTE_FLAGS.map(
234
+ (flag, index) => ` ${flag}: Boolean(routeFlags & ${1 << index}),`
235
+ ).join('\n')}
236
+ };
237
+ }
238
+
239
+ function applyRouteModuleUpdate(routeId, update, routeEntry, routeModules) {
240
+ Object.assign(routeEntry, getRouteMetadata(update.routeFlags));
241
+ const imported = update.getRouteModuleExports();
242
+ registerReactRouterRouteExports(routeId, imported);
243
+ const current = routeModules[routeId];
244
+ const preserveIdentity = key =>
245
+ imported[key] ? (current && current[key]) || imported[key] : imported[key];
246
+ routeModules[routeId] = {
247
+ ...imported,
248
+ default: preserveIdentity('default'),
249
+ ErrorBoundary: preserveIdentity('ErrorBoundary'),
250
+ HydrateFallback: preserveIdentity('HydrateFallback'),
251
+ };
252
+ }
253
+
254
+ function getRouteById(routes, routeId) {
255
+ for (const route of routes) {
256
+ if (route.id === routeId) {
257
+ return route;
258
+ }
259
+ if (route.children) {
260
+ const child = getRouteById(route.children, routeId);
261
+ if (child) {
262
+ return child;
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ // Deliberate coupling to React Router's private dev API: patching the live
269
+ // match objects is the only way to swap route implementations without a
270
+ // navigation. The typeof guard below degrades to a no-op if RR removes it.
271
+ function patchCurrentRouteMatches(router, routes) {
272
+ if (
273
+ !router.state ||
274
+ !Array.isArray(router.state.matches) ||
275
+ typeof router._internalSetStateDoNotUseOrYouWillBreakYourApp !== 'function'
276
+ ) {
277
+ return;
278
+ }
279
+
280
+ let changed = false;
281
+ const matches = router.state.matches.map(match => {
282
+ const route = getRouteById(routes, match.route.id);
283
+ if (!route || route === match.route) {
284
+ return match;
285
+ }
286
+ changed = true;
287
+ return { ...match, route };
288
+ });
289
+
290
+ if (changed) {
291
+ router._internalSetStateDoNotUseOrYouWillBreakYourApp({ matches });
292
+ }
293
+ }
294
+
295
+ function applyPendingRouteUpdates(router, routeModules, manifest, context) {
296
+ if (pendingRouteUpdates.size === 0) {
297
+ return {
298
+ nextManifest: undefined,
299
+ shouldRefreshRouteState: false,
300
+ routesToRevalidate: new Set(),
301
+ };
302
+ }
303
+
304
+ // Clone only entries mutated before the manifest is committed in flush().
305
+ const nextManifest = { ...manifest, routes: { ...manifest.routes } };
306
+ const routesToRevalidate = new Set();
307
+ let shouldRefreshRouteState = false;
308
+ for (const { routeId, update } of takePendingRouteUpdates()) {
309
+ const existingEntry = nextManifest.routes[routeId];
310
+ if (!existingEntry) continue;
311
+
312
+ // Shallow clone is enough: only top-level flags are mutated below.
313
+ const routeEntry = { ...existingEntry };
314
+ nextManifest.routes[routeId] = routeEntry;
315
+ applyRouteModuleUpdate(routeId, update, routeEntry, routeModules);
316
+ if (
317
+ routeEntry.hasLoader ||
318
+ routeEntry.hasClientLoader ||
319
+ routeEntry.hasClientMiddleware
320
+ ) {
321
+ routesToRevalidate.add(routeId);
322
+ }
323
+ if (
324
+ existingEntry.hasLoader ||
325
+ existingEntry.hasClientLoader ||
326
+ existingEntry.hasClientMiddleware ||
327
+ routeEntry.hasLoader ||
328
+ routeEntry.hasClientLoader ||
329
+ routeEntry.hasClientMiddleware
330
+ ) {
331
+ shouldRefreshRouteState = true;
332
+ }
333
+ }
334
+
335
+ if (
336
+ typeof router.createRoutesForHMR === 'function' &&
337
+ typeof router._internalSetRoutes === 'function'
338
+ ) {
339
+ const routes = router.createRoutesForHMR(
340
+ routesToRevalidate,
341
+ nextManifest.routes,
342
+ routeModules,
343
+ context.ssr,
344
+ context.isSpaMode
345
+ );
346
+ router._internalSetRoutes(routes);
347
+ patchCurrentRouteMatches(router, routes);
348
+ }
349
+
350
+ return { nextManifest, shouldRefreshRouteState, routesToRevalidate };
351
+ }
352
+
353
+ async function withHdrActive(fn) {
354
+ try {
355
+ window.__reactRouterHdrActive = true;
356
+ await fn();
357
+ } finally {
358
+ window.__reactRouterHdrActive = false;
359
+ }
360
+ }
361
+
362
+ async function revalidateRouter(router) {
363
+ if (typeof router.revalidate === 'function') {
364
+ await withHdrActive(() => router.revalidate());
365
+ return;
366
+ }
367
+ if (typeof router.navigate === 'function') {
368
+ await withHdrActive(() =>
369
+ router.navigate(getCurrentRouterPath(router), {
370
+ replace: true,
371
+ preventScrollReset: true,
372
+ })
373
+ );
374
+ }
375
+ }
376
+
377
+ async function refreshRouteState(router) {
378
+ if (typeof router.revalidate === 'function') {
379
+ await withHdrActive(() => router.revalidate());
380
+ return true;
381
+ }
382
+ return false;
383
+ }
384
+
385
+ function performReactRefresh() {
386
+ if (
387
+ RefreshRuntime &&
388
+ typeof RefreshRuntime.performReactRefresh === 'function'
389
+ ) {
390
+ RefreshRuntime.performReactRefresh();
391
+ }
392
+ }
393
+
394
+ async function flush() {
395
+ const router = window.__reactRouterDataRouter;
396
+ const routeModules = window.__reactRouterRouteModules;
397
+ const manifest = window.__reactRouterManifest;
398
+ const context = window.__reactRouterContext;
399
+ if (!router || !routeModules || !manifest || !context) {
400
+ return;
401
+ }
402
+
403
+ let shouldRevalidate = pendingRevalidation;
404
+ pendingRevalidation = false;
405
+ const { nextManifest, shouldRefreshRouteState, routesToRevalidate } =
406
+ applyPendingRouteUpdates(router, routeModules, manifest, context);
407
+ if (nextManifest) {
408
+ Object.assign(manifest, nextManifest);
409
+ }
410
+ // Component-only updates do not need a full loader revalidation.
411
+ if (
412
+ shouldRefreshRouteState &&
413
+ (routesToRevalidate.size > 0 || shouldRevalidate)
414
+ ) {
415
+ if (await refreshRouteState(router)) {
416
+ shouldRevalidate = false;
417
+ }
418
+ }
419
+ if (shouldRevalidate) {
420
+ await revalidateRouter(router);
421
+ }
422
+ performReactRefresh();
423
+ }
424
+
425
+ if (typeof window !== 'undefined' && import.meta.webpackHot) {
426
+ import.meta.webpackHot.accept(
427
+ ${JSON.stringify(hdrRevisionFilePath)},
428
+ scheduleReactRouterRevalidation
429
+ );
430
+ }
431
+ `;
@@ -1,8 +1,11 @@
1
1
  import { isAbsolute, relative } from 'node:path';
2
2
  import type { RsbuildDevServer, Rspack } from '@rsbuild/core';
3
+ import * as Effect from 'effect/Effect';
3
4
  import type { ServerBuild } from 'react-router';
4
5
  import type { ReactRouterManifestForDev } from './manifest.js';
5
- import { resolveServerBuildModule } from './server-utils.js';
6
+ import { getCappedPluginConcurrency } from './concurrency.js';
7
+ import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
8
+ import { resolveServerBuildModuleEffect } from './server-build-resolution.js';
6
9
 
7
10
  export type ReactRouterDevManifest = ReactRouterManifestForDev;
8
11
 
@@ -17,6 +20,13 @@ export type ReactRouterDevManifestSet = Readonly<
17
20
 
18
21
  export type ReactRouterServerBuilds = Readonly<Record<string, ServerBuild>>;
19
22
 
23
+ export type PairedDevStats = {
24
+ web: Rspack.Stats;
25
+ node: Rspack.Stats;
26
+ };
27
+
28
+ export type DevRuntimeStats = Rspack.Stats | Rspack.MultiStats | PairedDevStats;
29
+
20
30
  export type DependencySnapshot = {
21
31
  files: ReadonlySet<string>;
22
32
  contexts: ReadonlySet<string>;
@@ -34,11 +44,13 @@ export type DevGraphChanges = {
34
44
  };
35
45
 
36
46
  export type DevCompilationIdentity = symbol;
47
+ export type DevCompileAttemptIdentity = symbol;
37
48
 
38
49
  export type DevGraphIdentity = {
39
50
  web: DevCompilationIdentity | undefined;
40
51
  node: DevCompilationIdentity | undefined;
41
52
  nodeWeb: DevCompilationIdentity | undefined;
53
+ attempt: DevCompileAttemptIdentity | undefined;
42
54
  };
43
55
 
44
56
  export type WebArtifact = {
@@ -102,10 +114,17 @@ export const isSafeOneSidedChange = (
102
114
  return true;
103
115
  };
104
116
 
117
+ export const isPairedDevStats = (
118
+ stats: DevRuntimeStats
119
+ ): stats is PairedDevStats => 'web' in stats && 'node' in stats;
120
+
105
121
  export const getEnvironmentStats = (
106
- stats: Rspack.Stats | Rspack.MultiStats,
122
+ stats: DevRuntimeStats,
107
123
  name: 'web' | 'node'
108
124
  ): Rspack.Stats | undefined => {
125
+ if (isPairedDevStats(stats)) {
126
+ return stats[name];
127
+ }
109
128
  const children = Array.isArray((stats as Rspack.MultiStats).stats)
110
129
  ? (stats as Rspack.MultiStats).stats
111
130
  : [stats as Rspack.Stats];
@@ -115,29 +134,42 @@ export const getEnvironmentStats = (
115
134
  });
116
135
  };
117
136
 
118
- const evaluateServerBuild = async (
137
+ const startServerBuildEvaluationEffect = (
119
138
  server: RsbuildDevServer,
120
139
  entryName: string
121
- ): Promise<ServerBuild> => {
122
- const loaded = await server.environments.node.loadBundle(entryName);
123
- return resolveServerBuildModule(
124
- loaded,
125
- `Server entry ${JSON.stringify(entryName)}`
140
+ ): Effect.Effect<ServerBuild, Error, never> =>
141
+ tryPluginPromise(() => server.environments.node.loadBundle(entryName)).pipe(
142
+ Effect.flatMap(buildModule =>
143
+ resolveServerBuildModuleEffect(
144
+ buildModule,
145
+ `Server entry ${JSON.stringify(entryName)}`
146
+ )
147
+ )
126
148
  );
127
- };
128
149
 
129
- export const evaluateServerBuilds = async (
150
+ const evaluateServerBuildsEffect = (
130
151
  server: RsbuildDevServer,
131
152
  entryNames: readonly string[]
132
- ): Promise<ReactRouterServerBuilds> => {
133
- const evaluated = await Promise.all(
134
- entryNames.map(async entryName => [
135
- entryName,
136
- await evaluateServerBuild(server, entryName),
137
- ])
153
+ ): Effect.Effect<ReactRouterServerBuilds, Error, never> =>
154
+ Effect.forEach(
155
+ entryNames.map(entryName =>
156
+ startServerBuildEvaluationEffect(server, entryName).pipe(
157
+ Effect.map(build => [entryName, build] as const)
158
+ )
159
+ ),
160
+ evaluation => evaluation,
161
+ { concurrency: getCappedPluginConcurrency() }
162
+ ).pipe(
163
+ Effect.map(
164
+ evaluated => Object.fromEntries(evaluated) as Record<string, ServerBuild>
165
+ )
138
166
  );
139
- return Object.fromEntries(evaluated) as Record<string, ServerBuild>;
140
- };
167
+
168
+ export const evaluateServerBuilds = (
169
+ server: RsbuildDevServer,
170
+ entryNames: readonly string[]
171
+ ): Promise<ReactRouterServerBuilds> =>
172
+ runPluginEffect(evaluateServerBuildsEffect(server, entryNames));
141
173
 
142
174
  const assertBuildMatchesManifest = (
143
175
  entryName: string,
@@ -1,6 +1,8 @@
1
1
  import type { Rspack } from '@rsbuild/core';
2
2
  import type {
3
+ DevCompileAttemptIdentity,
3
4
  DevCompilationIdentity,
5
+ DevRuntimeStats,
4
6
  DevGraphChanges,
5
7
  DevGraphIdentity,
6
8
  } from './dev-runtime-artifacts.js';
@@ -10,13 +12,16 @@ export type DevCompilerPair = {
10
12
  node: Rspack.Compiler;
11
13
  settledCompilations: WeakSet<Rspack.Compilation>;
12
14
  pendingAttempt?: PendingDevCompilation;
15
+ currentAttemptIdentity?: DevCompileAttemptIdentity;
13
16
  latestCompletedWebIdentity?: DevCompilationIdentity;
17
+ latestCompletedWebStats?: Rspack.Stats;
18
+ latestCompletedNodeStats?: Rspack.Stats;
14
19
  latestWebStart?: CompilationStart;
15
20
  latestNodeStart?: CompilationStart;
16
21
  };
17
22
 
18
23
  export type PendingDevCompilation = {
19
- stats: Rspack.Stats | Rspack.MultiStats;
24
+ stats: DevRuntimeStats;
20
25
  changes: DevGraphChanges;
21
26
  identity: DevGraphIdentity;
22
27
  webCompilation: Rspack.Compilation;
@@ -27,6 +32,30 @@ export type CompilationStart =
27
32
  | { status: 'pending' }
28
33
  | { status: 'started'; identity: DevCompilationIdentity };
29
34
 
35
+ type CompilerPairStartSide = 'latestWebStart' | 'latestNodeStart';
36
+
37
+ export const createDevCompilerPair = ({
38
+ web,
39
+ node,
40
+ }: {
41
+ web: Rspack.Compiler;
42
+ node: Rspack.Compiler;
43
+ }): DevCompilerPair => ({
44
+ web,
45
+ node,
46
+ settledCompilations: new WeakSet(),
47
+ });
48
+
49
+ export const resetDevCompilerPair = (pair: DevCompilerPair): void => {
50
+ pair.pendingAttempt = undefined;
51
+ pair.currentAttemptIdentity = undefined;
52
+ pair.latestCompletedWebIdentity = undefined;
53
+ pair.latestCompletedWebStats = undefined;
54
+ pair.latestCompletedNodeStats = undefined;
55
+ pair.latestWebStart = undefined;
56
+ pair.latestNodeStart = undefined;
57
+ };
58
+
30
59
  export const isLatestStartedCompilation = (
31
60
  identity: DevCompilationIdentity | undefined,
32
61
  start: CompilationStart | undefined
@@ -37,6 +66,32 @@ export const hasPendingCompilation = (pair: DevCompilerPair): boolean =>
37
66
  pair.latestWebStart?.status === 'pending' ||
38
67
  pair.latestNodeStart?.status === 'pending';
39
68
 
69
+ export const beginDevCompilerAttempt = (pair: DevCompilerPair): void => {
70
+ pair.pendingAttempt = undefined;
71
+ pair.currentAttemptIdentity = Symbol();
72
+ };
73
+
74
+ export const markDevCompilerPending = (
75
+ pair: DevCompilerPair,
76
+ side: CompilerPairStartSide
77
+ ): boolean => {
78
+ const attemptAlreadyPending = hasPendingCompilation(pair);
79
+ pair[side] = { status: 'pending' };
80
+ pair.pendingAttempt = undefined;
81
+ if (!attemptAlreadyPending) {
82
+ pair.currentAttemptIdentity = Symbol();
83
+ }
84
+ return !attemptAlreadyPending;
85
+ };
86
+
87
+ export const clearDevCompilerStart = (
88
+ pair: DevCompilerPair,
89
+ side: CompilerPairStartSide
90
+ ): void => {
91
+ pair[side] = undefined;
92
+ pair.pendingAttempt = undefined;
93
+ };
94
+
40
95
  export type CompilationIdentityTracker = {
41
96
  getCompilationIdentity(
42
97
  compilation: Rspack.Compilation
@@ -44,6 +99,13 @@ export type CompilationIdentityTracker = {
44
99
  getWebIdentityForNodeCompilation(
45
100
  compilation: Rspack.Compilation
46
101
  ): DevCompilationIdentity | undefined;
102
+ getAttemptIdentityForCompilation(
103
+ compilation: Rspack.Compilation
104
+ ): DevCompileAttemptIdentity | undefined;
105
+ setAttemptIdentityForCompilation(
106
+ compilation: Rspack.Compilation,
107
+ identity: DevCompileAttemptIdentity
108
+ ): void;
47
109
  setWebIdentityForNodeCompilation(
48
110
  compilation: Rspack.Compilation,
49
111
  identity: DevCompilationIdentity
@@ -60,6 +122,10 @@ export const createCompilationIdentityTracker =
60
122
  Rspack.Compilation,
61
123
  DevCompilationIdentity
62
124
  >();
125
+ const attemptIdentityByCompilation = new WeakMap<
126
+ Rspack.Compilation,
127
+ DevCompileAttemptIdentity
128
+ >();
63
129
 
64
130
  return {
65
131
  getCompilationIdentity(
@@ -82,6 +148,19 @@ export const createCompilationIdentityTracker =
82
148
  return webIdentityByNodeCompilation.get(compilation);
83
149
  },
84
150
 
151
+ getAttemptIdentityForCompilation(
152
+ compilation: Rspack.Compilation
153
+ ): DevCompileAttemptIdentity | undefined {
154
+ return attemptIdentityByCompilation.get(compilation);
155
+ },
156
+
157
+ setAttemptIdentityForCompilation(
158
+ compilation: Rspack.Compilation,
159
+ identity: DevCompileAttemptIdentity
160
+ ): void {
161
+ attemptIdentityByCompilation.set(compilation, identity);
162
+ },
163
+
85
164
  setWebIdentityForNodeCompilation(
86
165
  compilation: Rspack.Compilation,
87
166
  identity: DevCompilationIdentity