rsbuild-plugin-react-router 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +58 -208
  2. package/dist/511.js +371 -332
  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 +3964 -2217
  17. package/dist/index.d.ts +3 -1
  18. package/dist/index.js +3474 -1799
  19. package/dist/lazy-compilation-prewarm.d.ts +8 -5
  20. package/dist/manifest.d.ts +17 -5
  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 +8 -2
  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 +258 -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 +186 -100
  77. package/src/mode-plan.ts +367 -0
  78. package/src/modify-browser-manifest.ts +131 -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 +196 -54
  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
package/src/dev-hmr.ts CHANGED
@@ -3,9 +3,14 @@ import { createRequire } from 'node:module';
3
3
  import type { Rspack } from '@rsbuild/core';
4
4
  import { dirname, join } from 'pathe';
5
5
 
6
- import { HMR_PATCHABLE_ROUTE_FLAGS } from './route-artifacts.js';
7
-
8
6
  export const DEV_HMR_RUNTIME_MODULE_ID = 'virtual/react-router/hmr-runtime';
7
+ export const DEV_MANIFEST_UPDATE_EVENT = 'react-router:manifest-update';
8
+
9
+ export type DevHmrPlanOptions = {
10
+ isEnabled: () => boolean;
11
+ runtimeModule: string;
12
+ onNodeRebuildCommitted: () => void;
13
+ };
9
14
 
10
15
  type SwcLoaderOptions = {
11
16
  jsc?: { transform?: { react?: { refresh?: boolean } } };
@@ -17,30 +22,21 @@ const isObject = (value: unknown): value is Record<string, unknown> =>
17
22
  const isSwcLoader = (loader: unknown): boolean =>
18
23
  typeof loader === 'string' && loader.includes('builtin:swc-loader');
19
24
 
20
- const hasReactRefresh = (options: unknown): boolean =>
21
- (options as SwcLoaderOptions | undefined)?.jsc?.transform?.react?.refresh ===
22
- true;
23
-
24
25
  const readSwcLoaderRefresh = (value: unknown): boolean => {
25
- if (Array.isArray(value)) {
26
- return value.some(readSwcLoaderRefresh);
27
- }
28
- if (!isObject(value)) {
29
- return false;
30
- }
26
+ if (Array.isArray(value)) return value.some(readSwcLoaderRefresh);
27
+ if (!isObject(value)) return false;
31
28
  if (isSwcLoader(value.loader)) {
32
- return hasReactRefresh(value.options);
29
+ return (
30
+ (value.options as SwcLoaderOptions | undefined)?.jsc?.transform?.react
31
+ ?.refresh === true
32
+ );
33
33
  }
34
34
  return Object.values(value).some(readSwcLoaderRefresh);
35
35
  };
36
36
 
37
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
- }
38
+ if (!isObject(rule)) return false;
39
+ if (isSwcLoader(rule.loader)) return readSwcLoaderRefresh(rule);
44
40
  return (
45
41
  readSwcLoaderRefresh(rule.use) ||
46
42
  readRuleSetSwcRefresh(rule.oneOf) ||
@@ -94,12 +90,11 @@ const hdrRevisionModuleContent = (revision: number): string =>
94
90
  * whenever server code changes, which the client answers by revalidating
95
91
  * React Router loader data.
96
92
  */
97
- export const DEV_HDR_REVISION_RELATIVE_PATH = '.react-router/hdr-revision.mjs';
98
-
99
93
  export const getDevHdrRevisionFilePath = (rootPath: string): string =>
100
- join(rootPath, DEV_HDR_REVISION_RELATIVE_PATH);
94
+ join(rootPath, '.react-router', 'hdr-revision.mjs');
101
95
 
102
96
  export type DevHdrRevisionSignal = {
97
+ filePath: string;
103
98
  /** Writes the initial revision module so the first compile can resolve it. */
104
99
  ensure: () => void;
105
100
  /** Increments the revision, signaling hot data revalidation to the client. */
@@ -127,6 +122,7 @@ export const createDevHdrRevisionSignal = ({
127
122
  }
128
123
  };
129
124
  return {
125
+ filePath,
130
126
  ensure: write,
131
127
  bump() {
132
128
  revision += 1;
@@ -138,7 +134,7 @@ export const createDevHdrRevisionSignal = ({
138
134
  /**
139
135
  * Browser-side HMR runtime shared by all route client entries in development.
140
136
  *
141
- * This mirrors React Router's Vite HMR contract (see `refresh-utils.mjs` in
137
+ * This mirrors React Router framework HMR contract (see `refresh-utils.mjs` in
142
138
  * `@react-router/dev`): route module updates are applied by patching
143
139
  * `window.__reactRouterRouteModules` while preserving the previous component
144
140
  * identities (React Fast Refresh swaps their implementations in place),
@@ -156,7 +152,11 @@ import * as __refreshRuntimeModule from ${JSON.stringify(reactRefreshRuntimePath
156
152
  // Read revision so the import survives sideEffects: false tree-shaking.
157
153
  import __hdrRevision from ${JSON.stringify(hdrRevisionFilePath)};
158
154
 
159
- void __hdrRevision;
155
+ let latestHdrRevision = __hdrRevision;
156
+
157
+ export function getReactRouterHdrRevision() {
158
+ return latestHdrRevision;
159
+ }
160
160
 
161
161
  const RefreshRuntime =
162
162
  __refreshRuntimeModule && __refreshRuntimeModule.performReactRefresh
@@ -166,6 +166,7 @@ const RefreshRuntime =
166
166
  const pendingRouteUpdates = new Map();
167
167
  let flushTimeout;
168
168
  let pendingRevalidation = false;
169
+ let pendingComponentRouteRevalidation = false;
169
170
 
170
171
  function getCurrentRouterPath(router) {
171
172
  const basename = router.basename || '/';
@@ -199,10 +200,10 @@ export function registerReactRouterRouteExports(routeId, moduleExports) {
199
200
 
200
201
  export function scheduleReactRouterRouteUpdate(
201
202
  routeId,
202
- routeFlags,
203
+ routeMetadata,
203
204
  getRouteModuleExports
204
205
  ) {
205
- pendingRouteUpdates.set(routeId, { routeFlags, getRouteModuleExports });
206
+ pendingRouteUpdates.set(routeId, { routeMetadata, getRouteModuleExports });
206
207
  scheduleFlush();
207
208
  }
208
209
 
@@ -228,16 +229,8 @@ function takePendingRouteUpdates() {
228
229
  return updates;
229
230
  }
230
231
 
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
232
  function applyRouteModuleUpdate(routeId, update, routeEntry, routeModules) {
240
- Object.assign(routeEntry, getRouteMetadata(update.routeFlags));
233
+ Object.assign(routeEntry, update.routeMetadata);
241
234
  const imported = update.getRouteModuleExports();
242
235
  registerReactRouterRouteExports(routeId, imported);
243
236
  const current = routeModules[routeId];
@@ -294,11 +287,7 @@ function patchCurrentRouteMatches(router, routes) {
294
287
 
295
288
  function applyPendingRouteUpdates(router, routeModules, manifest, context) {
296
289
  if (pendingRouteUpdates.size === 0) {
297
- return {
298
- nextManifest: undefined,
299
- shouldRefreshRouteState: false,
300
- routesToRevalidate: new Set(),
301
- };
290
+ return { nextManifest: undefined, hmrRoutes: undefined };
302
291
  }
303
292
 
304
293
  // Clone only entries mutated before the manifest is committed in flush().
@@ -309,8 +298,7 @@ function applyPendingRouteUpdates(router, routeModules, manifest, context) {
309
298
  const existingEntry = nextManifest.routes[routeId];
310
299
  if (!existingEntry) continue;
311
300
 
312
- // Shallow clone is enough: only top-level flags are mutated below.
313
- const routeEntry = { ...existingEntry };
301
+ const routeEntry = JSON.parse(JSON.stringify(existingEntry));
314
302
  nextManifest.routes[routeId] = routeEntry;
315
303
  applyRouteModuleUpdate(routeId, update, routeEntry, routeModules);
316
304
  if (
@@ -332,54 +320,52 @@ function applyPendingRouteUpdates(router, routeModules, manifest, context) {
332
320
  }
333
321
  }
334
322
 
323
+ let hmrRoutes;
335
324
  if (
336
325
  typeof router.createRoutesForHMR === 'function' &&
337
326
  typeof router._internalSetRoutes === 'function'
338
327
  ) {
339
- const routes = router.createRoutesForHMR(
328
+ hmrRoutes = router.createRoutesForHMR(
340
329
  routesToRevalidate,
341
330
  nextManifest.routes,
342
331
  routeModules,
343
332
  context.ssr,
344
333
  context.isSpaMode
345
334
  );
346
- router._internalSetRoutes(routes);
347
- patchCurrentRouteMatches(router, routes);
335
+ router._internalSetRoutes(hmrRoutes);
348
336
  }
349
337
 
350
- return { nextManifest, shouldRefreshRouteState, routesToRevalidate };
338
+ return { nextManifest, hmrRoutes, shouldRefreshRouteState };
351
339
  }
352
340
 
353
- async function withHdrActive(fn) {
341
+ async function revalidateRouter(router) {
354
342
  try {
355
343
  window.__reactRouterHdrActive = true;
356
- await fn();
344
+ if (typeof router.revalidate === 'function') {
345
+ await router.revalidate();
346
+ return;
347
+ }
348
+ if (typeof router.navigate === 'function') {
349
+ await router.navigate(getCurrentRouterPath(router), {
350
+ replace: true,
351
+ preventScrollReset: true,
352
+ });
353
+ }
357
354
  } finally {
358
355
  window.__reactRouterHdrActive = false;
359
356
  }
360
357
  }
361
358
 
362
- async function revalidateRouter(router) {
363
- if (typeof router.revalidate === 'function') {
364
- await withHdrActive(() => router.revalidate());
359
+ async function refreshRouteState(router) {
360
+ if (typeof router.revalidate !== 'function') {
365
361
  return;
366
362
  }
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;
363
+ try {
364
+ window.__reactRouterHdrActive = true;
365
+ await router.revalidate();
366
+ } finally {
367
+ window.__reactRouterHdrActive = false;
381
368
  }
382
- return false;
383
369
  }
384
370
 
385
371
  function performReactRefresh() {
@@ -391,6 +377,34 @@ function performReactRefresh() {
391
377
  }
392
378
  }
393
379
 
380
+ function applyManifestUpdate(nextRoutes) {
381
+ const router = window.__reactRouterDataRouter;
382
+ const routeModules = window.__reactRouterRouteModules;
383
+ const manifest = window.__reactRouterManifest;
384
+ const context = window.__reactRouterContext;
385
+ if (
386
+ !router ||
387
+ !routeModules ||
388
+ !manifest ||
389
+ !context ||
390
+ !nextRoutes ||
391
+ typeof router.createRoutesForHMR !== 'function' ||
392
+ typeof router._internalSetRoutes !== 'function'
393
+ ) {
394
+ return;
395
+ }
396
+ const routes = router.createRoutesForHMR(
397
+ new Set(Object.keys(nextRoutes)),
398
+ nextRoutes,
399
+ routeModules,
400
+ context.ssr,
401
+ context.isSpaMode
402
+ );
403
+ manifest.routes = nextRoutes;
404
+ router._internalSetRoutes(routes);
405
+ patchCurrentRouteMatches(router, routes);
406
+ }
407
+
394
408
  async function flush() {
395
409
  const router = window.__reactRouterDataRouter;
396
410
  const routeModules = window.__reactRouterRouteModules;
@@ -402,30 +416,48 @@ async function flush() {
402
416
 
403
417
  let shouldRevalidate = pendingRevalidation;
404
418
  pendingRevalidation = false;
405
- const { nextManifest, shouldRefreshRouteState, routesToRevalidate } =
419
+ const { nextManifest, hmrRoutes, shouldRefreshRouteState } =
406
420
  applyPendingRouteUpdates(router, routeModules, manifest, context);
407
- if (nextManifest) {
421
+ // Loader updates must be visible during revalidation. Component-only routes
422
+ // stay staged until revalidation completes, matching React Router's HMR flow.
423
+ if (nextManifest && shouldRefreshRouteState) {
424
+ pendingComponentRouteRevalidation = false;
425
+ if (hmrRoutes) {
426
+ patchCurrentRouteMatches(router, hmrRoutes);
427
+ }
408
428
  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;
429
+ await refreshRouteState(router);
430
+ shouldRevalidate = false;
431
+ } else if (nextManifest) {
432
+ if (hmrRoutes) {
433
+ patchCurrentRouteMatches(router, hmrRoutes);
434
+ }
435
+ Object.assign(manifest, nextManifest);
436
+ // The node compiler also emits an HDR revision for this route edit. If it
437
+ // did not arrive in this flush, consume that redundant revalidation later.
438
+ pendingComponentRouteRevalidation = !shouldRevalidate;
439
+ shouldRevalidate = false;
440
+ } else if (shouldRevalidate) {
441
+ if (pendingComponentRouteRevalidation) {
442
+ pendingComponentRouteRevalidation = false;
443
+ } else {
444
+ await revalidateRouter(router);
417
445
  }
418
- }
419
- if (shouldRevalidate) {
420
- await revalidateRouter(router);
421
446
  }
422
447
  performReactRefresh();
423
448
  }
424
449
 
425
450
  if (typeof window !== 'undefined' && import.meta.webpackHot) {
451
+ import.meta.webpackHot.on(
452
+ ${JSON.stringify(DEV_MANIFEST_UPDATE_EVENT)},
453
+ applyManifestUpdate
454
+ );
426
455
  import.meta.webpackHot.accept(
427
456
  ${JSON.stringify(hdrRevisionFilePath)},
428
- scheduleReactRouterRevalidation
457
+ () => {
458
+ latestHdrRevision = __hdrRevision;
459
+ scheduleReactRouterRevalidation();
460
+ }
429
461
  );
430
462
  }
431
463
  `;
@@ -5,7 +5,7 @@ import type { ServerBuild } from 'react-router';
5
5
  import type { ReactRouterManifestForDev } from './manifest.js';
6
6
  import { getCappedPluginConcurrency } from './concurrency.js';
7
7
  import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
8
- import { resolveServerBuildModuleEffect } from './server-build-resolution.js';
8
+ import { resolveServerBuildModule } from './server-build-resolution.js';
9
9
 
10
10
  export type ReactRouterDevManifest = ReactRouterManifestForDev;
11
11
 
@@ -137,14 +137,17 @@ export const getEnvironmentStats = (
137
137
  const startServerBuildEvaluationEffect = (
138
138
  server: RsbuildDevServer,
139
139
  entryName: string
140
- ): Effect.Effect<ServerBuild, Error, never> =>
140
+ ): Effect.Effect<readonly [string, ServerBuild], Error, never> =>
141
141
  tryPluginPromise(() => server.environments.node.loadBundle(entryName)).pipe(
142
142
  Effect.flatMap(buildModule =>
143
- resolveServerBuildModuleEffect(
144
- buildModule,
145
- `Server entry ${JSON.stringify(entryName)}`
143
+ tryPluginPromise(() =>
144
+ resolveServerBuildModule(
145
+ buildModule,
146
+ `Server entry ${JSON.stringify(entryName)}`
147
+ )
146
148
  )
147
- )
149
+ ),
150
+ Effect.map(build => [entryName, build] as const)
148
151
  );
149
152
 
150
153
  const evaluateServerBuildsEffect = (
@@ -152,12 +155,8 @@ const evaluateServerBuildsEffect = (
152
155
  entryNames: readonly string[]
153
156
  ): Effect.Effect<ReactRouterServerBuilds, Error, never> =>
154
157
  Effect.forEach(
155
- entryNames.map(entryName =>
156
- startServerBuildEvaluationEffect(server, entryName).pipe(
157
- Effect.map(build => [entryName, build] as const)
158
- )
159
- ),
160
- evaluation => evaluation,
158
+ entryNames,
159
+ entryName => startServerBuildEvaluationEffect(server, entryName),
161
160
  { concurrency: getCappedPluginConcurrency() }
162
161
  ).pipe(
163
162
  Effect.map(
@@ -1,7 +1,7 @@
1
1
  import type { RsbuildConfig, RsbuildPluginAPI, Rspack } from '@rsbuild/core';
2
- import * as Effect from 'effect/Effect';
3
2
  import type { ServerBuild } from 'react-router';
4
3
  import { PLUGIN_NAME } from './constants.js';
4
+ import { escapeHtml } from './plugin-utils.js';
5
5
  import {
6
6
  beginDevCompilerAttempt,
7
7
  clearDevCompilerStart,
@@ -19,6 +19,7 @@ import {
19
19
  registerReactRouterDevRuntime,
20
20
  unregisterReactRouterDevRuntime,
21
21
  } from './dev-generation.js';
22
+ import { DEV_MANIFEST_UPDATE_EVENT } from './dev-hmr.js';
22
23
  import {
23
24
  getEnvironmentStats,
24
25
  snapshotDevChangedFiles,
@@ -27,17 +28,11 @@ import {
27
28
  type ReactRouterDevBuildPlan,
28
29
  type ReactRouterDevManifestSet,
29
30
  } from './dev-runtime-artifacts.js';
30
- import { DEV_HDR_REVISION_RELATIVE_PATH } from './dev-hmr.js';
31
31
  import {
32
32
  createDevRuntimeSessionManager,
33
33
  type RuntimeBinding,
34
34
  } from './dev-runtime-session.js';
35
- import {
36
- normalizeEffectError,
37
- runPluginEffect,
38
- tryPluginPromise,
39
- tryPluginSync,
40
- } from './effect-runtime.js';
35
+ import { normalizeEffectError } from './effect-runtime.js';
41
36
 
42
37
  type ServerSetup = Exclude<
43
38
  NonNullable<NonNullable<RsbuildConfig['server']>['setup']>,
@@ -56,6 +51,11 @@ type CreateControllerOptions = {
56
51
  api: RsbuildPluginAPI;
57
52
  isBuild: boolean;
58
53
  buildPlan: ReactRouterDevBuildPlan;
54
+ /**
55
+ * The browser HMR runtime patches route manifest metadata (loader/action
56
+ * flags) in place, so metadata-only changes no longer need a full reload.
57
+ */
58
+ clientPatchesRouteMetadata?: boolean | (() => boolean);
59
59
  /**
60
60
  * Invoked after a development attempt commits a re-evaluated node build for
61
61
  * changed server files. Used to signal hot data revalidation to the client.
@@ -63,36 +63,19 @@ type CreateControllerOptions = {
63
63
  onNodeRebuildCommitted?: () => void;
64
64
  };
65
65
 
66
- const escapeHtml = (value: string): string =>
67
- value
68
- .replaceAll('&', '&amp;')
69
- .replaceAll('<', '&lt;')
70
- .replaceAll('>', '&gt;');
71
-
72
66
  const CSS_SOURCE_RELOAD_DELAY_MS = 1000;
73
67
 
74
68
  const isHdrRevisionFile = (file: string): boolean =>
75
- file.includes(DEV_HDR_REVISION_RELATIVE_PATH);
69
+ file.includes('.react-router/hdr-revision.mjs');
76
70
 
77
71
  const isCssSourceFile = (file: string): boolean =>
78
72
  /\.css(?:\.[cm]?[jt]s)?$/.test(file);
79
73
 
80
- // A change that should bump the HDR revision: anything except the revision
81
- // file itself (the bump's own echo) and CSS sources (styling never changes
82
- // loader data).
83
- const hasHdrTriggeringChange = (files: Iterable<string>): boolean => {
84
- for (const file of files) {
85
- if (!isHdrRevisionFile(file) && !isCssSourceFile(file)) {
86
- return true;
87
- }
88
- }
89
- return false;
90
- };
91
-
92
74
  export const createReactRouterDevRuntimeController = ({
93
75
  api,
94
76
  isBuild,
95
77
  buildPlan,
78
+ clientPatchesRouteMetadata,
96
79
  onNodeRebuildCommitted,
97
80
  }: CreateControllerOptions): ReactRouterDevRuntimeController => {
98
81
  if (isBuild) {
@@ -165,56 +148,44 @@ export const createReactRouterDevRuntimeController = ({
165
148
  NonNullable<DevGraphIdentity['node']>
166
149
  >();
167
150
 
168
- const finishRuntimeAttemptEffect = (
151
+ const finishRuntimeAttempt = async (
169
152
  binding: RuntimeBinding,
170
153
  pair: DevCompilerPair,
171
154
  stats: DevRuntimeStats,
172
155
  changes: Parameters<RuntimeBinding['runtime']['finishAttempt']>[1],
173
156
  identity: Parameters<RuntimeBinding['runtime']['finishAttempt']>[2]
174
- ): Effect.Effect<void, Error, never> =>
175
- tryPluginPromise(() =>
176
- binding.runtime.finishAttempt(stats, changes, identity)
177
- ).pipe(
178
- Effect.flatMap(result =>
179
- tryPluginSync(() => {
180
- if (sessions.getActiveBinding()?.id !== binding.id) {
181
- return;
182
- }
183
- if (result === 'retry-node') {
184
- pair.node.watching?.invalidate();
185
- return;
186
- }
187
- if (
188
- result === 'committed' &&
189
- changes.node.known &&
190
- identity.node !== undefined &&
191
- hdrSignaledNodeIdentity.get(pair) !== identity.node &&
192
- hasHdrTriggeringChange(changes.node.files)
193
- ) {
194
- hdrSignaledNodeIdentity.set(pair, identity.node);
195
- onNodeRebuildCommitted?.();
196
- }
197
- })
198
- ),
199
- Effect.catchAll(cause =>
200
- tryPluginSync(() => {
201
- if (sessions.getActiveBinding()?.id === binding.id) {
202
- binding.runtime.failAttempt(normalizeEffectError(cause));
203
- }
204
- })
205
- )
206
- );
207
-
208
- const finishRuntimeAttempt = (
209
- binding: RuntimeBinding,
210
- pair: DevCompilerPair,
211
- stats: DevRuntimeStats,
212
- changes: Parameters<RuntimeBinding['runtime']['finishAttempt']>[1],
213
- identity: Parameters<RuntimeBinding['runtime']['finishAttempt']>[2]
214
- ): Promise<void> =>
215
- runPluginEffect(
216
- finishRuntimeAttemptEffect(binding, pair, stats, changes, identity)
217
- );
157
+ ): Promise<void> => {
158
+ try {
159
+ const result = await binding.runtime.finishAttempt(
160
+ stats,
161
+ changes,
162
+ identity
163
+ );
164
+ if (sessions.getActiveBinding()?.id !== binding.id) {
165
+ return;
166
+ }
167
+ if (result === 'retry-node') {
168
+ pair.node.watching?.invalidate();
169
+ return;
170
+ }
171
+ if (
172
+ result === 'committed' &&
173
+ changes.node.known &&
174
+ identity.node !== undefined &&
175
+ hdrSignaledNodeIdentity.get(pair) !== identity.node &&
176
+ Array.from(changes.node.files).some(
177
+ file => !isHdrRevisionFile(file) && !isCssSourceFile(file)
178
+ )
179
+ ) {
180
+ hdrSignaledNodeIdentity.set(pair, identity.node);
181
+ onNodeRebuildCommitted?.();
182
+ }
183
+ } catch (cause) {
184
+ if (sessions.getActiveBinding()?.id === binding.id) {
185
+ binding.runtime.failAttempt(normalizeEffectError(cause));
186
+ }
187
+ }
188
+ };
218
189
 
219
190
  const flushSettledAttempt = (
220
191
  binding: RuntimeBinding,
@@ -306,11 +277,22 @@ export const createReactRouterDevRuntimeController = ({
306
277
  reloadAfterCssAssetOwnershipRemoval = change === 'removed';
307
278
  sendCssAssetOwnershipReload();
308
279
  },
309
- onRouteManifestChanged() {
280
+ onRouteManifestChanged(manifest) {
310
281
  if (sessions.getActiveBinding()?.runtime !== runtime) {
311
282
  return;
312
283
  }
313
- server.sockWrite('full-reload', { path: '*' });
284
+ const patchesRouteMetadata =
285
+ typeof clientPatchesRouteMetadata === 'function'
286
+ ? clientPatchesRouteMetadata()
287
+ : clientPatchesRouteMetadata;
288
+ if (patchesRouteMetadata) {
289
+ server.sockWrite('custom', {
290
+ event: DEV_MANIFEST_UPDATE_EVENT,
291
+ data: manifest.routes,
292
+ });
293
+ } else {
294
+ server.sockWrite('full-reload', { path: '*' });
295
+ }
314
296
  },
315
297
  onWarning: message => api.logger.warn(message),
316
298
  });
@@ -566,21 +548,29 @@ export const createReactRouterDevRuntimeController = ({
566
548
  },
567
549
 
568
550
  createBuildLoader(entryName?: string): () => Promise<ServerBuild> {
569
- const server = sessions.getActiveBinding()?.server;
570
- if (server) {
571
- return () => loadReactRouterServerBuild(server, entryName);
572
- }
573
- const state = sessions.getState();
574
- if (state.status === 'terminal') {
575
- const { error } = state;
576
- return () => Promise.reject(error);
577
- }
578
- return () =>
579
- Promise.reject(
551
+ // Pin the loader to the dev-server session active at creation time. Once a
552
+ // loader is handed to React Router for session N it must keep serving N (or
553
+ // fail loudly with 'not registered' once N closes) and never silently migrate
554
+ // to a replacement session — this preserves SSR generation/session coherency.
555
+ // The live fallback below applies ONLY when no session exists yet at creation
556
+ // (boundServer === undefined), i.e. a loader built during config setup before
557
+ // the dev server has started; there is no session to stay coherent with yet.
558
+ const boundServer = sessions.getActiveBinding()?.server;
559
+ return () => {
560
+ const server = boundServer ?? sessions.getActiveBinding()?.server;
561
+ if (server) {
562
+ return loadReactRouterServerBuild(server, entryName);
563
+ }
564
+ const state = sessions.getState();
565
+ if (state.status === 'terminal') {
566
+ return Promise.reject(state.error);
567
+ }
568
+ return Promise.reject(
580
569
  new Error(
581
570
  `[${PLUGIN_NAME}] The development server runtime is not ready.`
582
571
  )
583
572
  );
573
+ };
584
574
  },
585
575
  };
586
576
  };
@@ -1,13 +1,8 @@
1
1
  import type { RsbuildDevServer } from '@rsbuild/core';
2
- import * as Effect from 'effect/Effect';
3
2
  import { PLUGIN_NAME } from './constants.js';
4
3
  import type { ReactRouterDevRuntime } from './dev-generation.js';
5
4
  import type { DevCompilerPair } from './dev-runtime-compilation.js';
6
- import {
7
- runPluginEffect,
8
- tryPluginPromise,
9
- tryPluginSync,
10
- } from './effect-runtime.js';
5
+ import { normalizeEffectError } from './effect-runtime.js';
11
6
 
12
7
  export type RuntimeBinding = {
13
8
  id: number;
@@ -115,18 +110,19 @@ export const createDevRuntimeSessionManager = (
115
110
  if (observation.promise) {
116
111
  return observation.promise;
117
112
  }
118
- observation.promise = runPluginEffect(
119
- tryPluginPromise(close).pipe(
120
- Effect.tap(() =>
121
- tryPluginSync(() => applyCloseOutcome(observation, { ok: true }))
122
- ),
123
- Effect.catchAll(cause =>
124
- tryPluginSync(() =>
125
- applyCloseOutcome(observation, { ok: false, cause })
126
- ).pipe(Effect.zipRight(Effect.fail(cause)))
127
- )
128
- )
129
- );
113
+ let closePromise: Promise<void>;
114
+ try {
115
+ closePromise = Promise.resolve(close());
116
+ } catch (cause) {
117
+ closePromise = Promise.reject(normalizeEffectError(cause));
118
+ }
119
+ observation.promise = closePromise
120
+ .then(() => applyCloseOutcome(observation, { ok: true }))
121
+ .catch(cause => {
122
+ const error = normalizeEffectError(cause);
123
+ applyCloseOutcome(observation, { ok: false, cause: error });
124
+ throw error;
125
+ });
130
126
  return observation.promise;
131
127
  };
132
128
  closeObservationByServer.set(server, observation);
package/src/dev-server.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import type { RsbuildConfig } from '@rsbuild/core';
3
3
  import type { ServerBuild } from 'react-router';
4
+ import { installDevServerSourceMapSupport } from './dev-source-maps.js';
4
5
 
5
6
  export type ServerSetup = Exclude<
6
7
  NonNullable<NonNullable<RsbuildConfig['server']>['setup']>,
@@ -83,6 +84,7 @@ export const createReactRouterDevServerSetup = ({
83
84
  // middlewares, so static assets are served before the React Router
84
85
  // request handler.
85
86
  return () => {
87
+ installDevServerSourceMapSupport();
86
88
  context.server.middlewares.use(createDevServerMiddleware({ loadBuild }));
87
89
  };
88
90
  };