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,462 @@
1
+ import type { RsbuildConfig, RsbuildPluginAPI, Rspack } from '@rsbuild/core';
2
+ import type { ServerBuild } from 'react-router';
3
+ import { PLUGIN_NAME } from './constants.js';
4
+ import {
5
+ createCompilationIdentityTracker,
6
+ hasPendingCompilation,
7
+ isLatestStartedCompilation,
8
+ type DevCompilerPair,
9
+ } from './dev-runtime-compilation.js';
10
+ import {
11
+ createReactRouterDevRuntime,
12
+ loadReactRouterServerBuild,
13
+ registerReactRouterDevRuntime,
14
+ unregisterReactRouterDevRuntime,
15
+ } from './dev-generation.js';
16
+ import {
17
+ getEnvironmentStats,
18
+ snapshotDevChangedFiles,
19
+ type ReactRouterDevBuildPlan,
20
+ type ReactRouterDevManifestSet,
21
+ } from './dev-runtime-artifacts.js';
22
+ import {
23
+ createDevRuntimeSessionManager,
24
+ type RuntimeBinding,
25
+ } from './dev-runtime-session.js';
26
+
27
+ type ServerSetup = Exclude<
28
+ NonNullable<NonNullable<RsbuildConfig['server']>['setup']>,
29
+ unknown[]
30
+ >;
31
+
32
+ export type ReactRouterDevRuntimeController = {
33
+ captureWeb: (
34
+ compilation: Rspack.Compilation,
35
+ manifestsByEntryName: ReactRouterDevManifestSet
36
+ ) => void;
37
+ createBuildLoader: (entryName?: string) => () => Promise<ServerBuild>;
38
+ };
39
+
40
+ type CreateControllerOptions = {
41
+ api: RsbuildPluginAPI;
42
+ isBuild: boolean;
43
+ buildPlan: ReactRouterDevBuildPlan;
44
+ };
45
+
46
+ const escapeHtml = (value: string): string =>
47
+ value
48
+ .replaceAll('&', '&amp;')
49
+ .replaceAll('<', '&lt;')
50
+ .replaceAll('>', '&gt;');
51
+
52
+ const CSS_SOURCE_RELOAD_DELAY_MS = 1000;
53
+
54
+ export const createReactRouterDevRuntimeController = ({
55
+ api,
56
+ isBuild,
57
+ buildPlan,
58
+ }: CreateControllerOptions): ReactRouterDevRuntimeController => {
59
+ if (isBuild) {
60
+ return {
61
+ captureWeb() {},
62
+ createBuildLoader() {
63
+ return () =>
64
+ Promise.reject(
65
+ new Error(
66
+ `[${PLUGIN_NAME}] The development server runtime is unavailable during a production build.`
67
+ )
68
+ );
69
+ },
70
+ };
71
+ }
72
+
73
+ let scheduledCssAssetOwnershipReload:
74
+ | ReturnType<typeof setTimeout>
75
+ | undefined;
76
+ let lastCssAssetOwnershipReloadAt = 0;
77
+ let reloadAfterCssAssetOwnershipRemoval = false;
78
+
79
+ const sendCssAssetOwnershipReload = (): void => {
80
+ const binding = sessions.getActiveBinding();
81
+ if (!binding) {
82
+ return;
83
+ }
84
+ lastCssAssetOwnershipReloadAt = Date.now();
85
+ binding.server.sockWrite('full-reload', { path: '*' });
86
+ };
87
+
88
+ const scheduleCssAssetOwnershipReload = (): void => {
89
+ if (scheduledCssAssetOwnershipReload) {
90
+ clearTimeout(scheduledCssAssetOwnershipReload);
91
+ }
92
+ const scheduledAt = Date.now();
93
+ scheduledCssAssetOwnershipReload = setTimeout(() => {
94
+ scheduledCssAssetOwnershipReload = undefined;
95
+ if (lastCssAssetOwnershipReloadAt > scheduledAt) {
96
+ return;
97
+ }
98
+ sendCssAssetOwnershipReload();
99
+ }, CSS_SOURCE_RELOAD_DELAY_MS);
100
+ };
101
+
102
+ const closeBinding = (binding: RuntimeBinding, error?: Error): void => {
103
+ if (scheduledCssAssetOwnershipReload) {
104
+ clearTimeout(scheduledCssAssetOwnershipReload);
105
+ scheduledCssAssetOwnershipReload = undefined;
106
+ }
107
+ reloadAfterCssAssetOwnershipRemoval = false;
108
+ const pair = binding.compilers;
109
+ if (pair) {
110
+ pair.pendingAttempt = undefined;
111
+ pair.latestCompletedWebIdentity = undefined;
112
+ pair.latestWebStart = undefined;
113
+ pair.latestNodeStart = undefined;
114
+ }
115
+ binding.compilers = undefined;
116
+ binding.runtime.close(error);
117
+ unregisterReactRouterDevRuntime(binding.server, binding.runtime);
118
+ };
119
+
120
+ const sessions = createDevRuntimeSessionManager(closeBinding);
121
+ const compilationIdentities = createCompilationIdentityTracker();
122
+ const { getCompilationIdentity } = compilationIdentities;
123
+
124
+ const flushSettledAttempt = (
125
+ binding: RuntimeBinding,
126
+ pair: DevCompilerPair
127
+ ): void => {
128
+ const pending = pair.pendingAttempt;
129
+ if (
130
+ !pending ||
131
+ sessions.getActiveBinding()?.id !== binding.id ||
132
+ !pair.settledCompilations.has(pending.webCompilation) ||
133
+ !pair.settledCompilations.has(pending.nodeCompilation)
134
+ ) {
135
+ return;
136
+ }
137
+ pair.pendingAttempt = undefined;
138
+ if (
139
+ !isLatestStartedCompilation(pending.identity.web, pair.latestWebStart) ||
140
+ !isLatestStartedCompilation(pending.identity.node, pair.latestNodeStart)
141
+ ) {
142
+ return;
143
+ }
144
+ void binding.runtime
145
+ .finishAttempt(pending.stats, pending.changes, pending.identity)
146
+ .catch(cause => {
147
+ if (sessions.getActiveBinding()?.id === binding.id) {
148
+ binding.runtime.failAttempt(
149
+ cause instanceof Error ? cause : new Error(String(cause))
150
+ );
151
+ }
152
+ });
153
+ };
154
+
155
+ const rejectUnsupportedCompiler = (reason: string): void => {
156
+ const message = `[${PLUGIN_NAME}] Could not coordinate React Router development output because ${reason}.`;
157
+ api.logger.warn(message);
158
+ const binding = sessions.getActiveBinding();
159
+ if (!binding) {
160
+ return;
161
+ }
162
+ const error = new Error(message);
163
+ sessions.terminate(binding, error);
164
+ };
165
+
166
+ // Rsbuild runs server.setup before onBeforeStartDevServer. Prepending the
167
+ // observer here ensures setup callbacks cannot capture an unobserved close.
168
+ api.modifyRsbuildConfig({
169
+ order: 'post',
170
+ handler(config) {
171
+ const existingSetup = config.server?.setup;
172
+ const setup = existingSetup
173
+ ? Array.isArray(existingSetup)
174
+ ? existingSetup
175
+ : [existingSetup]
176
+ : [];
177
+ const observeServer: ServerSetup = context => {
178
+ if (context.action === 'dev') {
179
+ sessions.observeClose(context.server);
180
+ }
181
+ };
182
+ return {
183
+ ...config,
184
+ server: {
185
+ ...config.server,
186
+ setup: [observeServer, ...setup],
187
+ },
188
+ };
189
+ },
190
+ });
191
+
192
+ api.onBeforeStartDevServer({
193
+ order: 'pre',
194
+ async handler({ server }) {
195
+ sessions.assertCanStart();
196
+ const runtime = createReactRouterDevRuntime({
197
+ server,
198
+ buildPlan,
199
+ onEvaluationError(error) {
200
+ if (sessions.getActiveBinding()?.runtime !== runtime) {
201
+ return;
202
+ }
203
+ api.logger.error(error.message);
204
+ server.sockWrite('errors', {
205
+ text: [error.message],
206
+ html: escapeHtml(error.message),
207
+ });
208
+ },
209
+ onCssAssetOwnershipChanged(change) {
210
+ if (sessions.getActiveBinding()?.runtime !== runtime) {
211
+ return;
212
+ }
213
+ reloadAfterCssAssetOwnershipRemoval = change === 'removed';
214
+ sendCssAssetOwnershipReload();
215
+ },
216
+ onRouteManifestChanged() {
217
+ if (sessions.getActiveBinding()?.runtime !== runtime) {
218
+ return;
219
+ }
220
+ server.sockWrite('full-reload', { path: '*' });
221
+ },
222
+ onWarning: message => api.logger.warn(message),
223
+ });
224
+ const binding = sessions.createBinding(server, runtime);
225
+ registerReactRouterDevRuntime(server, runtime);
226
+ sessions.bindCloseObservation(binding);
227
+ },
228
+ });
229
+
230
+ api.onCloseDevServer({
231
+ order: 'pre',
232
+ handler() {
233
+ const binding = sessions.getActiveBinding();
234
+ if (!binding) {
235
+ return;
236
+ }
237
+ closeBinding(binding);
238
+ sessions.markClosing(binding);
239
+ },
240
+ });
241
+
242
+ api.onBeforeDevCompile({
243
+ order: 'pre',
244
+ handler() {
245
+ const binding = sessions.getActiveBinding();
246
+ const pair = binding?.compilers;
247
+ if (!binding || !pair || hasPendingCompilation(pair)) {
248
+ return;
249
+ }
250
+ pair.pendingAttempt = undefined;
251
+ binding.runtime.beginAttempt();
252
+ },
253
+ });
254
+
255
+ api.onAfterCreateCompiler(({ compiler }) => {
256
+ if (!('compilers' in compiler)) {
257
+ rejectUnsupportedCompiler('Rsbuild did not create a multi-compiler');
258
+ return;
259
+ }
260
+ const web = compiler.compilers.find(item => item.name === 'web');
261
+ const node = compiler.compilers.find(item => item.name === 'node');
262
+ if (!web || !node) {
263
+ rejectUnsupportedCompiler('the web or node compiler was missing');
264
+ return;
265
+ }
266
+ const binding = sessions.getActiveBinding();
267
+ if (!binding) {
268
+ return;
269
+ }
270
+ const pair: DevCompilerPair = {
271
+ web,
272
+ node,
273
+ settledCompilations: new WeakSet(),
274
+ };
275
+ binding.compilers = pair;
276
+ const sessionId = binding.id;
277
+ const runtime = binding.runtime;
278
+ const failCurrentAttempt = (side: 'web' | 'node', error: Error): void => {
279
+ if (sessions.getActiveBinding()?.id === sessionId) {
280
+ if (side === 'web') {
281
+ pair.latestWebStart = undefined;
282
+ } else {
283
+ pair.latestNodeStart = undefined;
284
+ }
285
+ pair.pendingAttempt = undefined;
286
+ runtime.failAttempt(error);
287
+ }
288
+ };
289
+ const beginCompilerAttempt = (
290
+ side: 'latestWebStart' | 'latestNodeStart'
291
+ ): void => {
292
+ if (
293
+ sessions.getActiveBinding()?.id === sessionId &&
294
+ pair[side]?.status !== 'pending'
295
+ ) {
296
+ const attemptAlreadyPending = hasPendingCompilation(pair);
297
+ // Invalidation can arrive before the aggregate before-compile hook.
298
+ // Supersede any evaluation that could resolve in that gap immediately.
299
+ pair[side] = { status: 'pending' };
300
+ pair.pendingAttempt = undefined;
301
+ if (!attemptAlreadyPending) {
302
+ runtime.beginAttempt();
303
+ }
304
+ if (side === 'latestWebStart' && reloadAfterCssAssetOwnershipRemoval) {
305
+ reloadAfterCssAssetOwnershipRemoval = false;
306
+ scheduleCssAssetOwnershipReload();
307
+ }
308
+ }
309
+ };
310
+ web.hooks.invalid.tap(`${PLUGIN_NAME}:dev-web-invalid`, () =>
311
+ beginCompilerAttempt('latestWebStart')
312
+ );
313
+ node.hooks.invalid.tap(`${PLUGIN_NAME}:dev-node-invalid`, () =>
314
+ beginCompilerAttempt('latestNodeStart')
315
+ );
316
+ web.hooks.done.tap(
317
+ { name: `${PLUGIN_NAME}:dev-web-complete`, stage: -1000 },
318
+ stats => {
319
+ if (sessions.getActiveBinding()?.id !== sessionId) {
320
+ return;
321
+ }
322
+ pair.latestCompletedWebIdentity = getCompilationIdentity(
323
+ stats.compilation
324
+ );
325
+ }
326
+ );
327
+ web.hooks.thisCompilation.tap(
328
+ `${PLUGIN_NAME}:dev-web-compilation`,
329
+ compilation => {
330
+ if (sessions.getActiveBinding()?.id === sessionId) {
331
+ pair.latestWebStart = {
332
+ status: 'started',
333
+ identity: getCompilationIdentity(compilation),
334
+ };
335
+ if (reloadAfterCssAssetOwnershipRemoval) {
336
+ reloadAfterCssAssetOwnershipRemoval = false;
337
+ scheduleCssAssetOwnershipReload();
338
+ }
339
+ }
340
+ }
341
+ );
342
+ node.hooks.thisCompilation.tap(
343
+ `${PLUGIN_NAME}:dev-node-web-compilation`,
344
+ compilation => {
345
+ if (sessions.getActiveBinding()?.id !== sessionId) {
346
+ return;
347
+ }
348
+ pair.latestNodeStart = {
349
+ status: 'started',
350
+ identity: getCompilationIdentity(compilation),
351
+ };
352
+ if (pair.latestCompletedWebIdentity) {
353
+ compilationIdentities.setWebIdentityForNodeCompilation(
354
+ compilation,
355
+ pair.latestCompletedWebIdentity
356
+ );
357
+ }
358
+ }
359
+ );
360
+ const settleCompilation = (stats: Rspack.Stats): void => {
361
+ if (sessions.getActiveBinding()?.id !== sessionId) {
362
+ return;
363
+ }
364
+ pair.settledCompilations.add(stats.compilation);
365
+ flushSettledAttempt(binding, pair);
366
+ };
367
+ web.hooks.afterDone.tap(
368
+ `${PLUGIN_NAME}:dev-web-settled`,
369
+ settleCompilation
370
+ );
371
+ node.hooks.afterDone.tap(
372
+ `${PLUGIN_NAME}:dev-node-settled`,
373
+ settleCompilation
374
+ );
375
+ web.hooks.failed.tap(`${PLUGIN_NAME}:dev-web-failed`, error =>
376
+ failCurrentAttempt('web', error)
377
+ );
378
+ node.hooks.failed.tap(`${PLUGIN_NAME}:dev-node-failed`, error =>
379
+ failCurrentAttempt('node', error)
380
+ );
381
+ });
382
+
383
+ api.onAfterDevCompile(async ({ stats }) => {
384
+ const binding = sessions.getActiveBinding();
385
+ const pair = binding?.compilers;
386
+ if (!binding || !pair) {
387
+ return;
388
+ }
389
+ const webStats = getEnvironmentStats(stats, 'web');
390
+ const nodeStats = getEnvironmentStats(stats, 'node');
391
+ if (
392
+ (webStats && webStats.compilation.compiler !== pair.web) ||
393
+ (nodeStats && nodeStats.compilation.compiler !== pair.node)
394
+ ) {
395
+ return;
396
+ }
397
+ const webIdentity = webStats
398
+ ? getCompilationIdentity(webStats.compilation)
399
+ : undefined;
400
+ const nodeIdentity = nodeStats
401
+ ? getCompilationIdentity(nodeStats.compilation)
402
+ : undefined;
403
+ if (
404
+ !isLatestStartedCompilation(webIdentity, pair.latestWebStart) ||
405
+ !isLatestStartedCompilation(nodeIdentity, pair.latestNodeStart)
406
+ ) {
407
+ return;
408
+ }
409
+ const changes = {
410
+ web: snapshotDevChangedFiles(pair.web),
411
+ node: snapshotDevChangedFiles(pair.node),
412
+ };
413
+ const identity = {
414
+ web: webIdentity,
415
+ node: nodeIdentity,
416
+ nodeWeb: nodeStats
417
+ ? compilationIdentities.getWebIdentityForNodeCompilation(
418
+ nodeStats.compilation
419
+ )
420
+ : undefined,
421
+ };
422
+ if (!webStats || !nodeStats) {
423
+ await binding.runtime.finishAttempt(stats, changes, identity);
424
+ return;
425
+ }
426
+ pair.pendingAttempt = {
427
+ stats,
428
+ changes,
429
+ identity,
430
+ webCompilation: webStats.compilation,
431
+ nodeCompilation: nodeStats.compilation,
432
+ };
433
+ flushSettledAttempt(binding, pair);
434
+ });
435
+
436
+ return {
437
+ captureWeb(compilation, manifestsByEntryName): void {
438
+ const binding = sessions.getActiveBinding();
439
+ if (binding?.compilers?.web === compilation.compiler) {
440
+ binding.runtime.captureWeb(compilation, manifestsByEntryName);
441
+ }
442
+ },
443
+
444
+ createBuildLoader(entryName?: string): () => Promise<ServerBuild> {
445
+ const server = sessions.getActiveBinding()?.server;
446
+ if (server) {
447
+ return () => loadReactRouterServerBuild(server, entryName);
448
+ }
449
+ const state = sessions.getState();
450
+ if (state.status === 'terminal') {
451
+ const { error } = state;
452
+ return () => Promise.reject(error);
453
+ }
454
+ return () =>
455
+ Promise.reject(
456
+ new Error(
457
+ `[${PLUGIN_NAME}] The development server runtime is not ready.`
458
+ )
459
+ );
460
+ },
461
+ };
462
+ };
@@ -0,0 +1,184 @@
1
+ import type { RsbuildDevServer } from '@rsbuild/core';
2
+ import { PLUGIN_NAME } from './constants.js';
3
+ import type { ReactRouterDevRuntime } from './dev-generation.js';
4
+ import type { DevCompilerPair } from './dev-runtime-compilation.js';
5
+
6
+ export type RuntimeBinding = {
7
+ id: number;
8
+ server: RsbuildDevServer;
9
+ runtime: ReactRouterDevRuntime;
10
+ compilers?: DevCompilerPair;
11
+ };
12
+
13
+ type CloseOutcome = { ok: true } | { ok: false; cause: unknown };
14
+
15
+ type CloseObservation = {
16
+ binding?: RuntimeBinding;
17
+ promise?: Promise<void>;
18
+ outcome?: CloseOutcome;
19
+ };
20
+
21
+ export type ControllerState =
22
+ | { status: 'idle' }
23
+ | { status: 'active'; binding: RuntimeBinding }
24
+ | { status: 'closing'; binding: RuntimeBinding }
25
+ | { status: 'terminal'; error: Error };
26
+
27
+ type CloseBinding = (binding: RuntimeBinding, error?: Error) => void;
28
+
29
+ export type DevRuntimeSessionManager = {
30
+ getState(): ControllerState;
31
+ getActiveBinding(): RuntimeBinding | undefined;
32
+ observeClose(server: RsbuildDevServer): void;
33
+ assertCanStart(): void;
34
+ createBinding(
35
+ server: RsbuildDevServer,
36
+ runtime: ReactRouterDevRuntime
37
+ ): RuntimeBinding;
38
+ bindCloseObservation(binding: RuntimeBinding): void;
39
+ markClosing(binding: RuntimeBinding): void;
40
+ terminate(binding: RuntimeBinding, error: Error): void;
41
+ };
42
+
43
+ export const createDevRuntimeSessionManager = (
44
+ closeBinding: CloseBinding
45
+ ): DevRuntimeSessionManager => {
46
+ let state: ControllerState = { status: 'idle' };
47
+ let nextSessionId = 1;
48
+ const closeObservationByServer = new WeakMap<
49
+ RsbuildDevServer,
50
+ CloseObservation
51
+ >();
52
+
53
+ const getState = (): ControllerState => state;
54
+
55
+ const getActiveBinding = (): RuntimeBinding | undefined =>
56
+ state.status === 'active' ? state.binding : undefined;
57
+
58
+ const isCurrentBinding = (binding: RuntimeBinding): boolean =>
59
+ (state.status === 'active' || state.status === 'closing') &&
60
+ state.binding === binding;
61
+
62
+ const completeClose = (binding: RuntimeBinding): void => {
63
+ if (!isCurrentBinding(binding)) {
64
+ return;
65
+ }
66
+ if (state.status === 'active') {
67
+ closeBinding(binding);
68
+ }
69
+ state = { status: 'idle' };
70
+ };
71
+
72
+ const failClose = (binding: RuntimeBinding, cause: unknown): void => {
73
+ if (!isCurrentBinding(binding)) {
74
+ return;
75
+ }
76
+ const error = new Error(
77
+ `[${PLUGIN_NAME}] The previous development server failed to close. Restart the process before retrying because Rsbuild may not have finished tearing down its compiler and watchers.`,
78
+ { cause }
79
+ );
80
+ closeBinding(binding, error);
81
+ state = { status: 'terminal', error };
82
+ };
83
+
84
+ const applyCloseOutcome = (
85
+ observation: CloseObservation,
86
+ outcome: CloseOutcome
87
+ ): void => {
88
+ observation.outcome = outcome;
89
+ const { binding } = observation;
90
+ if (!binding) {
91
+ return;
92
+ }
93
+ if (outcome.ok) {
94
+ completeClose(binding);
95
+ } else {
96
+ failClose(binding, outcome.cause);
97
+ }
98
+ observation.binding = undefined;
99
+ };
100
+
101
+ const observeClose = (server: RsbuildDevServer): CloseObservation => {
102
+ const existing = closeObservationByServer.get(server);
103
+ if (existing) {
104
+ return existing;
105
+ }
106
+ const observation: CloseObservation = {};
107
+ const close = server.close.bind(server);
108
+ server.close = () => {
109
+ if (observation.promise) {
110
+ return observation.promise;
111
+ }
112
+ let closePromise: Promise<void>;
113
+ try {
114
+ closePromise = close();
115
+ } catch (cause) {
116
+ closePromise = Promise.reject(cause);
117
+ }
118
+ observation.promise = closePromise;
119
+ void closePromise.then(
120
+ () => applyCloseOutcome(observation, { ok: true }),
121
+ cause => applyCloseOutcome(observation, { ok: false, cause })
122
+ );
123
+ return closePromise;
124
+ };
125
+ closeObservationByServer.set(server, observation);
126
+ return observation;
127
+ };
128
+
129
+ return {
130
+ getState,
131
+ getActiveBinding,
132
+
133
+ observeClose(server: RsbuildDevServer): void {
134
+ observeClose(server);
135
+ },
136
+
137
+ assertCanStart(): void {
138
+ if (state.status === 'terminal') {
139
+ throw state.error;
140
+ }
141
+ if (state.status === 'active') {
142
+ throw new Error(
143
+ `[${PLUGIN_NAME}] A development server is already active. Await its close() before calling createDevServer() again. If startup failed before returning the server, restart the process before retrying.`
144
+ );
145
+ }
146
+ if (state.status === 'closing') {
147
+ throw new Error(
148
+ `[${PLUGIN_NAME}] The previous development server is still closing. Await its close() before calling createDevServer() again.`
149
+ );
150
+ }
151
+ },
152
+
153
+ createBinding(
154
+ server: RsbuildDevServer,
155
+ runtime: ReactRouterDevRuntime
156
+ ): RuntimeBinding {
157
+ const binding = { id: nextSessionId++, server, runtime };
158
+ state = { status: 'active', binding };
159
+ return binding;
160
+ },
161
+
162
+ bindCloseObservation(binding: RuntimeBinding): void {
163
+ const observation = observeClose(binding.server);
164
+ observation.binding = binding;
165
+ if (observation.outcome) {
166
+ applyCloseOutcome(observation, observation.outcome);
167
+ }
168
+ },
169
+
170
+ markClosing(binding: RuntimeBinding): void {
171
+ if (isCurrentBinding(binding)) {
172
+ state = { status: 'closing', binding };
173
+ }
174
+ },
175
+
176
+ terminate(binding: RuntimeBinding, error: Error): void {
177
+ if (!isCurrentBinding(binding)) {
178
+ return;
179
+ }
180
+ closeBinding(binding, error);
181
+ state = { status: 'terminal', error };
182
+ },
183
+ };
184
+ };
@@ -0,0 +1,58 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { ServerBuild } from 'react-router';
3
+
4
+ export type DevServerMiddleware = (
5
+ req: IncomingMessage,
6
+ res: ServerResponse,
7
+ next: (err?: unknown) => void
8
+ ) => Promise<void>;
9
+
10
+ type RequestHandler = (request: Request) => Response | Promise<Response>;
11
+ type BuildProvider = () => Promise<ServerBuild>;
12
+
13
+ export type DevServerMiddlewareDependencies = {
14
+ loadBuild: BuildProvider;
15
+ createRequestHandler?: (
16
+ build: BuildProvider,
17
+ mode: 'development'
18
+ ) => RequestHandler;
19
+ createRequestListener?: (
20
+ handler: RequestHandler
21
+ ) => (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
22
+ };
23
+
24
+ export const createDevServerMiddleware = (
25
+ dependencies: DevServerMiddlewareDependencies
26
+ ): DevServerMiddleware => {
27
+ let listenerPromise:
28
+ | Promise<
29
+ (req: IncomingMessage, res: ServerResponse) => void | Promise<void>
30
+ >
31
+ | undefined;
32
+
33
+ const getListener = () => {
34
+ listenerPromise ??= (async () => {
35
+ const createRequestHandler =
36
+ dependencies.createRequestHandler ??
37
+ (await import('react-router')).createRequestHandler;
38
+ const createRequestListener =
39
+ dependencies.createRequestListener ??
40
+ (await import('@remix-run/node-fetch-server')).createRequestListener;
41
+ const requestHandler = createRequestHandler(
42
+ dependencies.loadBuild,
43
+ 'development'
44
+ );
45
+ return createRequestListener(requestHandler);
46
+ })();
47
+ return listenerPromise;
48
+ };
49
+
50
+ return async (req, res, next): Promise<void> => {
51
+ try {
52
+ const listener = await getListener();
53
+ await listener(req, res);
54
+ } catch (error) {
55
+ next(error);
56
+ }
57
+ };
58
+ };