rsbuild-plugin-react-router 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +58 -208
  2. package/dist/511.js +370 -331
  3. package/dist/build-output-transforms.d.ts +30 -6
  4. package/dist/classic-mode.d.ts +56 -0
  5. package/dist/config-imports.d.ts +8 -2
  6. package/dist/constants.d.ts +3 -0
  7. package/dist/dev-background-resources.d.ts +3 -1
  8. package/dist/dev-generation.d.ts +2 -3
  9. package/dist/dev-hmr.d.ts +8 -2
  10. package/dist/dev-runtime-controller.d.ts +6 -1
  11. package/dist/dev-source-maps.d.ts +4 -0
  12. package/dist/effect-runtime.d.ts +17 -5
  13. package/dist/entry-paths.d.ts +16 -0
  14. package/dist/environment-output.d.ts +6 -0
  15. package/dist/export-utils.d.ts +2 -1
  16. package/dist/index.cjs +3957 -2208
  17. package/dist/index.d.ts +3 -1
  18. package/dist/index.js +3470 -1793
  19. package/dist/lazy-compilation-prewarm.d.ts +8 -5
  20. package/dist/manifest.d.ts +16 -4
  21. package/dist/mode-plan.d.ts +82 -0
  22. package/dist/modify-browser-manifest.d.ts +8 -4
  23. package/dist/plugin-utils.d.ts +27 -1
  24. package/dist/prerender-build.d.ts +5 -5
  25. package/dist/prerender.d.ts +1 -5
  26. package/dist/react-router-config.d.ts +11 -3
  27. package/dist/route-artifacts.d.ts +4 -2
  28. package/dist/route-chunks.d.ts +2 -1
  29. package/dist/route-component-transform.d.ts +0 -2
  30. package/dist/route-export-pruning.d.ts +4 -1
  31. package/dist/route-imports.d.ts +18 -0
  32. package/dist/route-transform-tasks.d.ts +5 -0
  33. package/dist/route-watch.d.ts +11 -6
  34. package/dist/rsc-dev-server.d.ts +26 -0
  35. package/dist/rsc-prerender.d.ts +54 -0
  36. package/dist/rsc-route-config.d.ts +6 -0
  37. package/dist/rsc-route-exports.d.ts +15 -0
  38. package/dist/rsc-route-transform-loader.cjs +43 -0
  39. package/dist/rsc-route-transform-loader.d.ts +30 -0
  40. package/dist/rsc-route-transform-loader.js +16 -0
  41. package/dist/rsc-route-transform-registration.d.ts +12 -0
  42. package/dist/rsc-route-transforms.d.ts +21 -0
  43. package/dist/rsc-support.d.ts +23 -0
  44. package/dist/rsc-virtual-modules.d.ts +19 -0
  45. package/dist/server-build-plan.d.ts +2 -1
  46. package/dist/server-build-resolution.d.ts +1 -2
  47. package/dist/server-utils.d.ts +4 -5
  48. package/dist/ssr-asset-relocation.d.ts +98 -0
  49. package/dist/templates/entry.rsc.client.d.ts +1 -0
  50. package/dist/templates/entry.rsc.client.js +61 -0
  51. package/dist/templates/entry.rsc.d.ts +9 -0
  52. package/dist/templates/entry.rsc.js +38 -0
  53. package/dist/templates/entry.rsc.ssr.d.ts +4 -0
  54. package/dist/templates/entry.rsc.ssr.js +24 -0
  55. package/dist/typegen.d.ts +4 -2
  56. package/dist/types.d.ts +30 -1
  57. package/package.json +69 -14
  58. package/src/build-output-transforms.ts +155 -21
  59. package/src/classic-mode.ts +253 -0
  60. package/src/config-imports.ts +153 -6
  61. package/src/constants.ts +6 -2
  62. package/src/dev-background-resources.ts +46 -85
  63. package/src/dev-generation.ts +52 -33
  64. package/src/dev-hmr.ts +112 -80
  65. package/src/dev-runtime-artifacts.ts +11 -12
  66. package/src/dev-runtime-controller.ts +75 -85
  67. package/src/dev-runtime-session.ts +14 -18
  68. package/src/dev-server.ts +2 -0
  69. package/src/dev-source-maps.ts +257 -0
  70. package/src/effect-runtime.ts +105 -57
  71. package/src/entry-paths.ts +80 -0
  72. package/src/environment-output.ts +55 -0
  73. package/src/export-utils.ts +15 -11
  74. package/src/index.ts +661 -496
  75. package/src/lazy-compilation-prewarm.ts +20 -4
  76. package/src/manifest.ts +157 -82
  77. package/src/mode-plan.ts +367 -0
  78. package/src/modify-browser-manifest.ts +130 -124
  79. package/src/plugin-utils.ts +103 -39
  80. package/src/prerender-build.ts +82 -104
  81. package/src/prerender.ts +23 -24
  82. package/src/react-router-config.ts +72 -26
  83. package/src/route-artifacts.ts +96 -66
  84. package/src/route-chunks.ts +167 -51
  85. package/src/route-component-transform.ts +14 -21
  86. package/src/route-export-pruning.ts +4 -3
  87. package/src/route-imports.ts +100 -0
  88. package/src/route-transform-tasks.ts +39 -25
  89. package/src/route-watch.ts +166 -188
  90. package/src/rsc-dev-server.ts +112 -0
  91. package/src/rsc-prerender.ts +362 -0
  92. package/src/rsc-route-config.ts +175 -0
  93. package/src/rsc-route-exports.ts +67 -0
  94. package/src/rsc-route-transform-loader.ts +65 -0
  95. package/src/rsc-route-transform-registration.ts +145 -0
  96. package/src/rsc-route-transforms.ts +995 -0
  97. package/src/rsc-runtime.d.ts +143 -0
  98. package/src/rsc-support.ts +116 -0
  99. package/src/rsc-virtual-modules.ts +113 -0
  100. package/src/server-build-plan.ts +14 -3
  101. package/src/server-build-resolution.ts +37 -47
  102. package/src/server-utils.ts +21 -35
  103. package/src/ssr-asset-relocation.ts +183 -0
  104. package/src/ssr-externals.ts +8 -26
  105. package/src/templates/entry.rsc.client.tsx +168 -0
  106. package/src/templates/entry.rsc.ssr.tsx +45 -0
  107. package/src/templates/entry.rsc.tsx +80 -0
  108. package/src/typegen.ts +40 -23
  109. package/src/types.ts +39 -1
  110. package/src/warnings/warn-on-client-source-maps.ts +6 -10
@@ -2,11 +2,12 @@ import { watch, type FSWatcher } from 'node:fs';
2
2
  import { access, mkdir, readdir, writeFile } from 'node:fs/promises';
3
3
  import type { RsbuildConfig } from '@rsbuild/core';
4
4
  import * as Effect from 'effect/Effect';
5
+ import type * as Scope from 'effect/Scope';
5
6
  import { dirname, resolve } from 'pathe';
6
7
  import { getCappedPluginConcurrency } from './concurrency.js';
7
8
  import {
8
9
  createDelayedPluginTask,
9
- runPluginEffect,
10
+ type PluginEffectRuntime,
10
11
  tryPluginPromise,
11
12
  } from './effect-runtime.js';
12
13
  import type { Route } from './types.js';
@@ -47,6 +48,17 @@ type WatchDirectoryEntry = (
47
48
  onError: (error: unknown) => void
48
49
  ) => DirectoryWatcher;
49
50
 
51
+ export type CreateRouteTopologyWatcherOptions = {
52
+ runtime: PluginEffectRuntime;
53
+ watchDirectory: string;
54
+ getRouteTopology: () => Promise<Set<string>>;
55
+ initialRouteTopology?: Set<string>;
56
+ restartMarkerPath: string;
57
+ onError: (error: unknown) => void;
58
+ onRouteTopologyChange?: () => void | Promise<void>;
59
+ watchDirectoryEntry?: WatchDirectoryEntry;
60
+ };
61
+
50
62
  const defaultWatchDirectoryEntry: WatchDirectoryEntry = (
51
63
  directory,
52
64
  onChange,
@@ -70,8 +82,8 @@ export const mergeWatchFiles = (
70
82
  ] as WatchFilesConfig;
71
83
  };
72
84
 
73
- export const getRouteRestartMarkerPath = (outputClientPath: string): string =>
74
- resolve(outputClientPath, ROUTE_RESTART_MARKER_ASSET);
85
+ export const getRouteRestartMarkerPath = (appDirectory: string): string =>
86
+ resolve(appDirectory, '..', ROUTE_RESTART_MARKER_ASSET);
75
87
 
76
88
  export const createRouteManifestSnapshot = (
77
89
  routes: RouteManifestSnapshotEntries
@@ -120,10 +132,6 @@ const areSetsEqual = <T>(left: Set<T>, right: Set<T>): boolean => {
120
132
  return true;
121
133
  };
122
134
 
123
- const readRouteDirectories = (watchDirectory: string): Promise<Set<string>> => {
124
- return runPluginEffect(readRouteDirectoriesEffect(watchDirectory));
125
- };
126
-
127
135
  const readRouteDirectoriesEffect = (
128
136
  watchDirectory: string
129
137
  ): Effect.Effect<Set<string>, Error, never> => {
@@ -148,7 +156,8 @@ const readRouteDirectoriesEffect = (
148
156
  return walkDirectory(watchDirectory).pipe(Effect.as(directories));
149
157
  };
150
158
 
151
- export const createRouteTopologyWatcher = async ({
159
+ const createRouteTopologyWatcherEffect = ({
160
+ runtime,
152
161
  watchDirectory,
153
162
  getRouteTopology,
154
163
  initialRouteTopology,
@@ -156,202 +165,171 @@ export const createRouteTopologyWatcher = async ({
156
165
  onError,
157
166
  onRouteTopologyChange,
158
167
  watchDirectoryEntry: watchDirectoryOverride = defaultWatchDirectoryEntry,
159
- }: {
160
- watchDirectory: string;
161
- getRouteTopology: () => Promise<Set<string>>;
162
- initialRouteTopology?: Set<string>;
163
- restartMarkerPath: string;
164
- onError: (error: unknown) => void;
165
- onRouteTopologyChange?: () => void | Promise<void>;
166
- watchDirectoryEntry?: WatchDirectoryEntry;
167
- }): Promise<() => Promise<void>> => {
168
- const discoveredDirectories = await readRouteDirectories(watchDirectory);
169
- let discoveredState: RouteDirectoryState;
170
- try {
171
- discoveredState = {
168
+ }: CreateRouteTopologyWatcherOptions) =>
169
+ Effect.gen(function* () {
170
+ const discoveredDirectories =
171
+ yield* readRouteDirectoriesEffect(watchDirectory);
172
+ const routeTopology = yield* tryPluginPromise(getRouteTopology).pipe(
173
+ Effect.catchAll(error => {
174
+ if (!initialRouteTopology) return Effect.fail(error);
175
+ onError(error);
176
+ return Effect.succeed(initialRouteTopology);
177
+ })
178
+ );
179
+ const discoveredState: RouteDirectoryState = {
172
180
  directories: discoveredDirectories,
173
- routeTopology: await getRouteTopology(),
181
+ routeTopology,
174
182
  };
175
- } catch (error) {
176
- if (!initialRouteTopology) {
177
- throw error;
178
- }
179
- onError(error);
180
- discoveredState = {
181
- directories: discoveredDirectories,
182
- routeTopology: initialRouteTopology,
183
+ let state = {
184
+ ...discoveredState,
185
+ routeTopology: initialRouteTopology ?? discoveredState.routeTopology,
183
186
  };
184
- }
185
- let state = {
186
- ...discoveredState,
187
- routeTopology: initialRouteTopology ?? discoveredState.routeTopology,
188
- };
189
- let closed = false;
190
- let rescanQueue = Promise.resolve();
191
- const directoryWatchers = new Map<string, DirectoryWatcher>();
187
+ let closed = false;
188
+ const directoryWatchers = new Map<string, DirectoryWatcher>();
192
189
 
193
- const touchRestartMarkerEffect = (): Effect.Effect<void, Error, never> =>
194
- tryPluginPromise(() =>
195
- mkdir(dirname(restartMarkerPath), { recursive: true })
196
- ).pipe(
197
- Effect.zipRight(
198
- tryPluginPromise(() => writeFile(restartMarkerPath, String(Date.now())))
199
- )
200
- );
201
-
202
- const closeRemovedDirectoryWatchers = (
203
- nextDirectories: Set<string>
204
- ): void => {
205
- for (const [directory, watcher] of directoryWatchers) {
206
- if (!nextDirectories.has(directory)) {
207
- watcher.close();
208
- directoryWatchers.delete(directory);
209
- }
210
- }
211
- };
190
+ const touchRestartMarkerEffect = (): Effect.Effect<void, Error, never> =>
191
+ tryPluginPromise(() =>
192
+ mkdir(dirname(restartMarkerPath), { recursive: true })
193
+ ).pipe(
194
+ Effect.zipRight(
195
+ tryPluginPromise(() =>
196
+ writeFile(restartMarkerPath, String(Date.now()))
197
+ )
198
+ )
199
+ );
212
200
 
213
- const watchNewDirectories = (nextDirectories: Set<string>): void => {
214
- for (const directory of nextDirectories) {
215
- if (directoryWatchers.has(directory)) {
216
- continue;
217
- }
218
- try {
219
- let watcher: DirectoryWatcher;
220
- watcher = watchDirectoryOverride(
221
- directory,
222
- () => rescanTask.reschedule(),
223
- error => {
224
- if (directoryWatchers.get(directory) === watcher) {
225
- watcher.close();
226
- directoryWatchers.delete(directory);
227
- }
228
- onError(error);
229
- }
230
- );
231
- directoryWatchers.set(directory, watcher);
232
- } catch (error) {
233
- onError(error);
201
+ const closeRemovedDirectoryWatchers = (
202
+ nextDirectories: Set<string>
203
+ ): void => {
204
+ for (const [directory, watcher] of directoryWatchers) {
205
+ if (!nextDirectories.has(directory)) {
206
+ watcher.close();
207
+ directoryWatchers.delete(directory);
208
+ }
234
209
  }
235
- }
236
- };
237
-
238
- const syncDirectoryWatchers = (nextDirectories: Set<string>): void => {
239
- closeRemovedDirectoryWatchers(nextDirectories);
240
- watchNewDirectories(nextDirectories);
241
- };
210
+ };
242
211
 
243
- const applyNextStateEffect = (
244
- nextState: RouteDirectoryState
245
- ): Effect.Effect<void, Error, never> =>
246
- Effect.suspend(() => {
247
- if (closed) {
248
- return Effect.void;
249
- }
250
- syncDirectoryWatchers(nextState.directories);
251
- if (!areSetsEqual(state.routeTopology, nextState.routeTopology)) {
252
- if (onRouteTopologyChange) {
253
- // This is a notification boundary, not part of the rescan
254
- // transaction. A custom-server callback may close this watcher while
255
- // replacing its compiler, so awaiting it here would deadlock close().
256
- state = nextState;
257
- return Effect.sync(() => {
258
- try {
259
- void Promise.resolve(onRouteTopologyChange()).catch(onError);
260
- } catch (error) {
212
+ const watchNewDirectories = (nextDirectories: Set<string>): void => {
213
+ for (const directory of nextDirectories) {
214
+ if (directoryWatchers.has(directory)) {
215
+ continue;
216
+ }
217
+ try {
218
+ let watcher: DirectoryWatcher;
219
+ watcher = watchDirectoryOverride(
220
+ directory,
221
+ () => {
222
+ if (!closed) rescanTask.reschedule();
223
+ },
224
+ error => {
225
+ if (closed) return;
226
+ if (directoryWatchers.get(directory) === watcher) {
227
+ watcher.close();
228
+ directoryWatchers.delete(directory);
229
+ }
261
230
  onError(error);
262
231
  }
263
- });
232
+ );
233
+ directoryWatchers.set(directory, watcher);
234
+ } catch (error) {
235
+ onError(error);
264
236
  }
265
- return touchRestartMarkerEffect().pipe(
266
- Effect.zipRight(
267
- Effect.sync(() => {
268
- if (!closed) {
269
- state = nextState;
270
- }
271
- })
272
- )
273
- );
274
237
  }
275
- state = nextState;
276
- return Effect.void;
277
- });
238
+ };
278
239
 
279
- const runRescanEffect = (): Effect.Effect<void, never, never> => {
280
- let nextDirectories: Set<string> | undefined;
281
- return Effect.gen(function* () {
282
- if (closed) {
283
- return;
284
- }
285
- nextDirectories = yield* readRouteDirectoriesEffect(watchDirectory);
286
- if (closed) {
287
- return;
288
- }
289
- const nextState = {
290
- directories: nextDirectories,
291
- routeTopology: yield* tryPluginPromise(getRouteTopology),
292
- };
293
- if (closed) {
294
- return;
295
- }
296
- yield* applyNextStateEffect(nextState);
297
- }).pipe(
298
- Effect.catchAll(error =>
299
- Effect.sync(() => {
300
- if (nextDirectories && !closed) {
301
- syncDirectoryWatchers(nextDirectories);
240
+ const syncDirectoryWatchers = (nextDirectories: Set<string>): void => {
241
+ closeRemovedDirectoryWatchers(nextDirectories);
242
+ watchNewDirectories(nextDirectories);
243
+ };
244
+
245
+ const applyNextStateEffect = (
246
+ nextState: RouteDirectoryState
247
+ ): Effect.Effect<void, Error, never> =>
248
+ Effect.suspend(() => {
249
+ if (closed) return Effect.void;
250
+ syncDirectoryWatchers(nextState.directories);
251
+ if (!areSetsEqual(state.routeTopology, nextState.routeTopology)) {
252
+ if (onRouteTopologyChange) {
253
+ // This is a notification boundary, not part of the rescan
254
+ // transaction. A custom-server callback may close this watcher while
255
+ // replacing its compiler, so awaiting it here would deadlock close().
256
+ state = nextState;
257
+ return Effect.sync(() => {
258
+ try {
259
+ void Promise.resolve(onRouteTopologyChange()).catch(onError);
260
+ } catch (error) {
261
+ onError(error);
262
+ }
263
+ });
302
264
  }
303
- onError(error);
304
- })
305
- )
306
- );
307
- };
265
+ return touchRestartMarkerEffect().pipe(
266
+ Effect.zipRight(
267
+ Effect.sync(() => {
268
+ if (!closed) {
269
+ state = nextState;
270
+ }
271
+ })
272
+ )
273
+ );
274
+ }
275
+ state = nextState;
276
+ return Effect.void;
277
+ });
308
278
 
309
- const rescan = (): Promise<void> => {
310
- rescanQueue = rescanQueue.then(
311
- () => runPluginEffect(runRescanEffect()),
312
- () => runPluginEffect(runRescanEffect())
313
- );
314
- return rescanQueue;
315
- };
279
+ const runRescanEffect = (): Effect.Effect<void, never, never> => {
280
+ let nextDirectories: Set<string> | undefined;
281
+ return Effect.gen(function* () {
282
+ if (closed) return;
283
+ nextDirectories = yield* readRouteDirectoriesEffect(watchDirectory);
284
+ if (closed) return;
285
+ const nextState = {
286
+ directories: nextDirectories,
287
+ routeTopology: yield* tryPluginPromise(getRouteTopology),
288
+ };
289
+ if (closed) return;
290
+ yield* applyNextStateEffect(nextState);
291
+ }).pipe(
292
+ Effect.catchAll(error =>
293
+ Effect.sync(() => {
294
+ if (nextDirectories && !closed)
295
+ syncDirectoryWatchers(nextDirectories);
296
+ onError(error);
297
+ })
298
+ )
299
+ );
300
+ };
316
301
 
317
- const rescanTask = createDelayedPluginTask({
318
- delayMs: ROUTE_TOPOLOGY_RESCAN_DEBOUNCE_MS,
319
- run: () =>
320
- Effect.suspend(() =>
321
- closed ? Effect.void : tryPluginPromise(rescan).pipe(Effect.asVoid)
322
- ),
323
- onError,
324
- });
302
+ const rescanTask = createDelayedPluginTask({
303
+ runtime,
304
+ delayMs: ROUTE_TOPOLOGY_RESCAN_DEBOUNCE_MS,
305
+ run: () =>
306
+ Effect.suspend(() => (closed ? Effect.void : runRescanEffect())),
307
+ onError,
308
+ });
325
309
 
326
- const cancelScheduledRescan = (): Promise<void> =>
327
- runPluginEffect(rescanTask.cancelEffect());
310
+ const closeEffect = (): Effect.Effect<void, Error> =>
311
+ tryPluginPromise(() => {
312
+ if (closed) return;
313
+ closed = true;
314
+ for (const watcher of directoryWatchers.values()) {
315
+ watcher.close();
316
+ }
317
+ directoryWatchers.clear();
318
+ }).pipe(Effect.ensuring(rescanTask.cancelEffect()));
328
319
 
329
- const applyNextState = async (nextState: RouteDirectoryState) => {
330
- await runPluginEffect(applyNextStateEffect(nextState));
331
- };
320
+ yield* applyNextStateEffect(discoveredState).pipe(
321
+ Effect.catchAll(error => Effect.sync(() => onError(error))),
322
+ Effect.onInterrupt(() => Effect.ignore(closeEffect()))
323
+ );
332
324
 
333
- try {
334
- await applyNextState(discoveredState);
335
- } catch (error) {
336
- onError(error);
337
- }
325
+ return closeEffect;
326
+ });
338
327
 
339
- return async () => {
340
- if (closed) {
341
- await cancelScheduledRescan();
342
- await rescanQueue;
343
- return;
344
- }
345
- closed = true;
346
- await cancelScheduledRescan();
347
- for (const watcher of directoryWatchers.values()) {
348
- watcher.close();
349
- }
350
- directoryWatchers.clear();
351
- await rescanQueue;
352
- for (const watcher of directoryWatchers.values()) {
353
- watcher.close();
354
- }
355
- directoryWatchers.clear();
356
- };
357
- };
328
+ export const acquireRouteTopologyWatcher = (
329
+ options: CreateRouteTopologyWatcherOptions
330
+ ): Effect.Effect<() => Effect.Effect<void, Error>, Error, Scope.Scope> =>
331
+ Effect.acquireRelease(createRouteTopologyWatcherEffect(options), close =>
332
+ close().pipe(
333
+ Effect.catchAll(error => Effect.sync(() => options.onError(error)))
334
+ )
335
+ );
@@ -0,0 +1,112 @@
1
+ import { createRequestListener } from '@remix-run/node-fetch-server';
2
+ import type { RsbuildConfig } from '@rsbuild/core';
3
+ import { installDevServerSourceMapSupport } from './dev-source-maps.js';
4
+ import { escapeHtml } from './plugin-utils.js';
5
+
6
+ /**
7
+ * Turns an error thrown by the RSC server build into a source-mapped 500 HTML
8
+ * document. `@remix-run/node-fetch-server`'s request listener catches handler
9
+ * throws internally and, without an `onError`, emits a bare 500 with no body
10
+ * markup. React Router's classic dev path instead returns an ErrorBoundary
11
+ * document whose `<main>` carries the stack, and the integration test asserts
12
+ * on that `<main>`. Reading `error.stack` here triggers the installed
13
+ * `prepareStackTrace`, remapping generated `.js` frames back to the original
14
+ * `.tsx` source.
15
+ */
16
+ const renderDevServerError = (error: unknown): Response => {
17
+ const stack =
18
+ error instanceof Error ? (error.stack ?? String(error)) : String(error);
19
+ return new Response(
20
+ `<!doctype html><html><body><main><pre>${escapeHtml(
21
+ stack
22
+ )}</pre></main></body></html>`,
23
+ { status: 500, headers: { 'content-type': 'text/html' } }
24
+ );
25
+ };
26
+
27
+ type RscServerBuild = {
28
+ default?: {
29
+ fetch?: (request: Request) => Promise<Response>;
30
+ };
31
+ };
32
+
33
+ type RscDevServerSetup = NonNullable<
34
+ NonNullable<RsbuildConfig['server']>['setup']
35
+ >;
36
+
37
+ type RscDevServerSetupOptions = {
38
+ entryName: string;
39
+ pluginName: string;
40
+ };
41
+
42
+ /**
43
+ * Decides whether a dev-server request should skip the RSC request handler
44
+ * and fall through to the rest of the middleware chain.
45
+ *
46
+ * Only Rsbuild-internal endpoints (`/__rsbuild_*`, e.g. HMR web socket
47
+ * fallbacks) are bypassed. Everything else — any method, any pathname,
48
+ * with or without a file extension — is handled by the RSC server build,
49
+ * matching production where the RSC server handles every request.
50
+ *
51
+ * Static assets need no bypass here: the RSC middleware is registered via
52
+ * the callback returned from `server.setup`, which Rsbuild invokes only
53
+ * after its built-in middlewares (compiled assets, public dir) are
54
+ * registered. A request that reaches this middleware was already declined
55
+ * by static asset serving.
56
+ */
57
+ export const shouldBypassRscDevRequest = (request: {
58
+ url?: string;
59
+ }): boolean => {
60
+ if (!request.url) {
61
+ return true;
62
+ }
63
+ const { pathname } = new URL(request.url, 'http://localhost');
64
+ return pathname.startsWith('/__rsbuild_');
65
+ };
66
+
67
+ export function createReactRouterRscDevServerSetup({
68
+ entryName,
69
+ pluginName,
70
+ }: RscDevServerSetupOptions): RscDevServerSetup {
71
+ return context => {
72
+ if (context.action === 'preview') {
73
+ return;
74
+ }
75
+ const { server } = context;
76
+ // Remap generated `.js` stack frames back to original `.tsx` sources so a
77
+ // loader/render error surfaced below carries source-mapped locations, the
78
+ // same support the classic dev path installs.
79
+ installDevServerSourceMapSupport();
80
+ const listener = createRequestListener(
81
+ async request => {
82
+ const build =
83
+ await server.environments.node.loadBundle<RscServerBuild>(entryName);
84
+ const handler = build.default?.fetch;
85
+ if (typeof handler !== 'function') {
86
+ throw new Error(
87
+ `[${pluginName}] RSC server build must default-export an object with a fetch function.`
88
+ );
89
+ }
90
+ return handler(request);
91
+ },
92
+ { onError: renderDevServerError }
93
+ );
94
+
95
+ // Register the RSC middleware in the callback returned from
96
+ // `server.setup`. Rsbuild runs middlewares registered synchronously in
97
+ // `setup` BEFORE its built-ins, but runs returned callbacks AFTER
98
+ // built-in middlewares (compiled assets, public dir) are registered.
99
+ // Registering here makes the RSC handler the fallback for every request
100
+ // that static asset serving did not handle — the same routing semantics
101
+ // as production, where the RSC server handles everything.
102
+ return () => {
103
+ server.middlewares.use((req, res, next) => {
104
+ if (shouldBypassRscDevRequest(req)) {
105
+ next();
106
+ return;
107
+ }
108
+ Promise.resolve(listener(req, res)).catch(next);
109
+ });
110
+ };
111
+ };
112
+ }