rsbuild-plugin-react-router 0.7.3 → 0.8.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.
@@ -0,0 +1,34 @@
1
+ export type ReactRouterManifestStats = {
2
+ assetsByChunkName?: Record<string, string[]>;
3
+ entrypointFilesByName?: Record<string, string[]>;
4
+ assetTypesByName?: Record<string, string>;
5
+ };
6
+ type ManifestStatsChunk = {
7
+ files?: Iterable<string>;
8
+ };
9
+ type ManifestStatsEntrypoint = {
10
+ getFiles?: () => Iterable<string>;
11
+ };
12
+ type ManifestStatsLookup<T> = Iterable<[string, T | null | undefined]> & {
13
+ get?: (name: string) => T | null | undefined;
14
+ };
15
+ type ManifestStatsCompilation = {
16
+ namedChunks: ManifestStatsLookup<ManifestStatsChunk>;
17
+ entrypoints?: ManifestStatsLookup<ManifestStatsEntrypoint>;
18
+ getAsset?: (name: string) => {
19
+ name?: string;
20
+ info?: {
21
+ assetType?: string;
22
+ javascriptModule?: boolean;
23
+ };
24
+ } | void;
25
+ };
26
+ export declare const stripAssetQuery: (name: string) => string;
27
+ export declare const getManifestAssetType: (name: string, assetTypesByName?: Readonly<Record<string, string>>) => string | undefined;
28
+ export declare const createReactRouterManifestStats: (compilation: ManifestStatsCompilation | undefined, chunkNames?: ReadonlySet<string>) => ReactRouterManifestStats | undefined;
29
+ type ChunkAssets = {
30
+ js: string[];
31
+ css: string[];
32
+ };
33
+ export declare const createChunkAssetResolver: (clientStats: ReactRouterManifestStats | undefined, includeEntrypointJavaScript: boolean) => ((chunkName: string) => ChunkAssets);
34
+ export {};
@@ -0,0 +1,17 @@
1
+ import type { ReactRouterManifestForDev, RouteManifestModuleExports } from './manifest.js';
2
+ import type { ReactRouterServerBuildPlan } from './server-build-plan.js';
3
+ import type { Route } from './types.js';
4
+ export type ReactRouterManifestSnapshot = {
5
+ browser: ReactRouterManifestForDev;
6
+ moduleExportsByRouteId: RouteManifestModuleExports;
7
+ server: ReactRouterManifestForDev;
8
+ serverByBundleId: Readonly<Record<string, ReactRouterManifestForDev>>;
9
+ serverByEntryName: Readonly<Record<string, ReactRouterManifestForDev>>;
10
+ };
11
+ export declare const createReactRouterManifestSnapshot: ({ manifest, sri, moduleExportsByRouteId, serverBuildPlan, routesByServerBundleId, }: {
12
+ manifest: ReactRouterManifestForDev;
13
+ sri: ReactRouterManifestForDev["sri"];
14
+ moduleExportsByRouteId: RouteManifestModuleExports;
15
+ serverBuildPlan: Pick<ReactRouterServerBuildPlan, "defaultEntryName" | "serverBundleEntries">;
16
+ routesByServerBundleId: Readonly<Record<string, Readonly<Record<string, Route>>>>;
17
+ }) => ReactRouterManifestSnapshot;
@@ -0,0 +1,12 @@
1
+ import type { RsbuildPluginAPI, Rspack } from '@rsbuild/core';
2
+ import type { ReactRouterManifestSnapshot } from './manifest-snapshot.js';
3
+ type ReactRouterManifestState = {
4
+ stage(compilation: Rspack.Compilation, snapshot: ReactRouterManifestSnapshot): void;
5
+ read(): ReactRouterManifestSnapshot | null;
6
+ };
7
+ export declare const createReactRouterManifestState: ({ api, isBuild, onPublish, }: {
8
+ api: Pick<RsbuildPluginAPI, "onBeforeEnvironmentCompile" | "onAfterEnvironmentCompile">;
9
+ isBuild: boolean;
10
+ onPublish: (compilation: Rspack.Compilation, snapshot: ReactRouterManifestSnapshot) => void;
11
+ }) => ReactRouterManifestState;
12
+ export {};
@@ -1,3 +1,5 @@
1
+ import { type ReactRouterManifestStats } from './manifest-assets.js';
2
+ export { createReactRouterManifestStats, type ReactRouterManifestStats, } from './manifest-assets.js';
1
3
  import type { Route, PluginOptions, RouteManifestItem } from './types.js';
2
4
  import { type RouteChunkCache } from './route-chunks.js';
3
5
  import { type RouteModuleAnalysis } from './export-utils.js';
@@ -44,26 +46,6 @@ export type ReactRouterManifestForDev = {
44
46
  sri?: Record<string, string> | true;
45
47
  routes: Record<string, RouteManifestItem>;
46
48
  };
47
- export type ReactRouterManifestStats = {
48
- assetsByChunkName?: Record<string, string[]>;
49
- entrypointFilesByName?: Record<string, string[]>;
50
- };
51
- type ReactRouterManifestStatsChunk = {
52
- files?: Iterable<string>;
53
- };
54
- type ReactRouterManifestStatsEntrypoint = {
55
- getFiles?: () => Iterable<string>;
56
- };
57
- type ReactRouterManifestStatsLookup<T> = Iterable<[
58
- string,
59
- T | null | undefined
60
- ]> & {
61
- get?: (name: string) => T | null | undefined;
62
- };
63
- type ReactRouterManifestStatsCompilation = {
64
- namedChunks: ReactRouterManifestStatsLookup<ReactRouterManifestStatsChunk>;
65
- entrypoints?: ReactRouterManifestStatsLookup<ReactRouterManifestStatsEntrypoint>;
66
- };
67
49
  export declare const isManifestJsAsset: (asset: string) => boolean;
68
50
  export declare const isManifestCssAsset: (asset: string) => boolean;
69
51
  /**
@@ -95,7 +77,6 @@ export type RscScriptAssetChunk = {
95
77
  * emits it, so function templates and `tools.rspack` overrides are covered.
96
78
  */
97
79
  export declare const collectUnsupportedRscScriptAssets: (compilation: RscScriptAssetCompilation) => string[];
98
- export declare const createReactRouterManifestStats: (compilation: ReactRouterManifestStatsCompilation | undefined, chunkNames?: ReadonlySet<string>) => ReactRouterManifestStats | undefined;
99
80
  export type RouteManifestModuleExports = Record<string, readonly string[]>;
100
81
  export type ReactRouterManifestGenerationResult = {
101
82
  manifest: ReactRouterManifestForDev;
@@ -109,4 +90,3 @@ export declare const getReactRouterManifestPath: ({ version, isBuild, entryModul
109
90
  export declare const getReactRouterManifestChunkNames: (routes: Record<string, Route>, appDirectory: string, splitRouteModules?: boolean | "enforce") => Set<string>;
110
91
  export declare function generateReactRouterManifestForDev(routes: Record<string, Route>, _options: PluginOptions, clientStats: ReactRouterManifestStats | undefined, context: string, assetPrefix?: string, manifestOptions?: ReactRouterManifestOptions): Promise<ReactRouterManifestGenerationResult>;
111
92
  export declare function getReactRouterManifestForDev(...args: Parameters<typeof generateReactRouterManifestForDev>): Promise<ReactRouterManifestForDev>;
112
- export {};
@@ -15,6 +15,8 @@ type CompilationAssetWithIntegrity = {
15
15
  };
16
16
  info?: {
17
17
  integrity?: unknown;
18
+ assetType?: string;
19
+ javascriptModule?: boolean;
18
20
  };
19
21
  };
20
22
  type CompilationWithIntegrityAssets = {
@@ -0,0 +1,8 @@
1
+ import { type RsbuildPluginAPI } from '@rsbuild/core';
2
+ import type { ReactRouterManifestSnapshot } from './manifest-snapshot.js';
3
+ import type { Route } from './types.js';
4
+ export declare const registerNodeOnlyManifestValidation: ({ api, routeByFilePath, getSnapshot, }: {
5
+ api: RsbuildPluginAPI;
6
+ routeByFilePath: ReadonlyMap<string, Route>;
7
+ getSnapshot: () => ReactRouterManifestSnapshot | null;
8
+ }) => void;
@@ -1,4 +1,4 @@
1
- export declare const resolveAppPackagePath: (specifier: string) => string | undefined;
1
+ export declare const resolveAppPackagePath: (specifier: string, rootPath?: string) => string | undefined;
2
2
  export declare const parseVersionMajorMinor: (version: string | undefined) => {
3
3
  major: number;
4
4
  minor: number;
package/dist/types.d.ts CHANGED
@@ -9,6 +9,12 @@ export type Route = {
9
9
  children?: Route[];
10
10
  };
11
11
  export type PluginOptions = {
12
+ /**
13
+ * Generate React Router route types during development and builds.
14
+ * Set to false when type generation is managed separately.
15
+ * @default true
16
+ */
17
+ typegen?: boolean;
12
18
  /**
13
19
  * Whether to disable automatic middleware setup for custom server implementation.
14
20
  * Use this when you want to handle server setup manually.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rsbuild-plugin-react-router",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "React Router plugin for Rsbuild",
5
5
  "repository": {
6
6
  "type": "git",
@@ -86,7 +86,7 @@
86
86
  "@react-router/fs-routes": "^8.3.1",
87
87
  "@react-router/remix-routes-option-adapter": "^8.3.1",
88
88
  "@react-router/serve": "^8.3.1",
89
- "@rsbuild/core": "2.2.0",
89
+ "@rsbuild/core": "2.2.8",
90
90
  "@rsbuild/plugin-less": "1.6.4",
91
91
  "@rsbuild/plugin-mdx": "^1.1.3",
92
92
  "@rsbuild/plugin-react": "2.1.0",
@@ -94,7 +94,7 @@
94
94
  "@rsbuild/plugin-svgr": "2.0.4",
95
95
  "@rsbuild/plugin-tailwindcss": "2.0.3",
96
96
  "@rslib/core": "^0.23.2",
97
- "@rspack/core": "2.2.0",
97
+ "@rspack/core": "2.2.6",
98
98
  "@rstest/core": "^0.11.4",
99
99
  "@rstest/coverage-istanbul": "^0.11.4",
100
100
  "@swc/helpers": "^0.5.23",
@@ -136,7 +136,7 @@
136
136
  },
137
137
  "peerDependencies": {
138
138
  "@react-router/dev": "^7.13.0 || ^8.0.0",
139
- "@rsbuild/core": "^2.2.0",
139
+ "@rsbuild/core": "^2.2.8",
140
140
  "react-router": "^7.13.0 || ^8.0.0",
141
141
  "react-server-dom-rspack": "0.1.0",
142
142
  "rsbuild-plugin-rsc": "^0.1.1"
@@ -226,6 +226,11 @@ export const registerBuildOutputTransforms = ({
226
226
  );
227
227
  const bundleId = bundleMatch?.[1]?.replace(/\.js$/, '');
228
228
  const latestServerManifest = getLatestServerManifest();
229
+ if (isBuild && !latestServerManifest) {
230
+ throw new Error(
231
+ `[${PLUGIN_NAME}] Production server manifest requested before the browser manifest was finalized.`
232
+ );
233
+ }
229
234
  const manifest =
230
235
  (latestServerManifest
231
236
  ? ((bundleId && getLatestServerManifestByBundleId(bundleId)) ??
@@ -125,7 +125,6 @@ export const createClassicBuildArtifacts = async ({
125
125
  isBuild,
126
126
  buildPlan: serverBuildPlan,
127
127
  clientPatchesRouteMetadata: devHmr?.isEnabled,
128
- onNodeRebuildCommitted: devHmr?.onNodeRebuildCommitted,
129
128
  }),
130
129
  prerenderPaths,
131
130
  routesByServerBundleId,
package/src/constants.ts CHANGED
@@ -93,3 +93,6 @@ export const CLIENT_EXPORTS = {
93
93
 
94
94
  // SPA-mode prerender fallback document, served when no prerendered page matches.
95
95
  export const SPA_FALLBACK_HTML_FILE = '__spa-fallback.html';
96
+
97
+ export const BROWSER_MANIFEST_ENTRY_NAME =
98
+ 'virtual/react-router/browser-manifest';
@@ -0,0 +1,38 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { RsbuildDevServer } from '@rsbuild/core';
3
+
4
+ export const DEV_HDR_UPDATE_EVENT = 'react-router:hdr-update';
5
+
6
+ export const createDevHdrChannel = ({
7
+ hot,
8
+ isEnabled,
9
+ }: {
10
+ hot: RsbuildDevServer['environments'][string]['hot'];
11
+ isEnabled: () => boolean;
12
+ }) => {
13
+ const sessionId = randomUUID();
14
+ let revision = 0;
15
+ let closed = false;
16
+ const payload = () => ({
17
+ event: DEV_HDR_UPDATE_EVENT,
18
+ data: { sessionId, revision },
19
+ });
20
+ const unsubscribe = hot.onConnect(client => {
21
+ if (!closed && isEnabled() && revision > 0) {
22
+ client.send('custom', payload());
23
+ }
24
+ });
25
+
26
+ return {
27
+ publish(): void {
28
+ if (closed || !isEnabled()) return;
29
+ revision += 1;
30
+ hot.send('custom', payload());
31
+ },
32
+ close(): void {
33
+ if (closed) return;
34
+ closed = true;
35
+ unsubscribe();
36
+ },
37
+ };
38
+ };
package/src/dev-hmr.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
2
1
  import { createRequire } from 'node:module';
3
2
  import type { Rspack } from '@rsbuild/core';
4
- import { dirname, join } from 'pathe';
3
+ import { join } from 'pathe';
4
+ import { DEV_HDR_UPDATE_EVENT } from './dev-hdr-channel.js';
5
5
 
6
6
  export const DEV_HMR_RUNTIME_MODULE_ID = 'virtual/react-router/hmr-runtime';
7
7
  export const DEV_MANIFEST_UPDATE_EVENT = 'react-router:manifest-update';
@@ -9,7 +9,6 @@ export const DEV_MANIFEST_UPDATE_EVENT = 'react-router:manifest-update';
9
9
  export type DevHmrPlanOptions = {
10
10
  isEnabled: () => boolean;
11
11
  runtimeModule: string;
12
- onNodeRebuildCommitted: () => void;
13
12
  };
14
13
 
15
14
  type SwcLoaderOptions = {
@@ -80,57 +79,6 @@ export const resolveReactRefreshRuntimePath = (
80
79
  }
81
80
  };
82
81
 
83
- const hdrRevisionModuleContent = (revision: number): string =>
84
- `export default ${revision};\n`;
85
-
86
- /**
87
- * The HDR revision module is a real file (not a virtual module) because it
88
- * must wake the web compiler through the regular file watcher: the browser
89
- * HMR runtime imports it, so bumping the revision produces a web hot update
90
- * whenever server code changes, which the client answers by revalidating
91
- * React Router loader data.
92
- */
93
- export const getDevHdrRevisionFilePath = (rootPath: string): string =>
94
- join(rootPath, '.react-router', 'hdr-revision.mjs');
95
-
96
- export type DevHdrRevisionSignal = {
97
- filePath: string;
98
- /** Writes the initial revision module so the first compile can resolve it. */
99
- ensure: () => void;
100
- /** Increments the revision, signaling hot data revalidation to the client. */
101
- bump: () => void;
102
- };
103
-
104
- export const createDevHdrRevisionSignal = ({
105
- filePath,
106
- onError,
107
- }: {
108
- filePath: string;
109
- onError?: (error: Error) => void;
110
- }): DevHdrRevisionSignal => {
111
- let revision = 0;
112
- let dirEnsured = false;
113
- const write = (): void => {
114
- try {
115
- if (!dirEnsured) {
116
- mkdirSync(dirname(filePath), { recursive: true });
117
- dirEnsured = true;
118
- }
119
- writeFileSync(filePath, hdrRevisionModuleContent(revision));
120
- } catch (error) {
121
- onError?.(error instanceof Error ? error : new Error(String(error)));
122
- }
123
- };
124
- return {
125
- filePath,
126
- ensure: write,
127
- bump() {
128
- revision += 1;
129
- write();
130
- },
131
- };
132
- };
133
-
134
82
  /**
135
83
  * Browser-side HMR runtime shared by all route client entries in development.
136
84
  *
@@ -143,20 +91,10 @@ export const createDevHdrRevisionSignal = ({
143
91
  */
144
92
  export const generateDevHmrRuntimeModule = ({
145
93
  reactRefreshRuntimePath,
146
- hdrRevisionFilePath,
147
94
  }: {
148
95
  reactRefreshRuntimePath: string;
149
- hdrRevisionFilePath: string;
150
96
  }): string => `
151
97
  import * as __refreshRuntimeModule from ${JSON.stringify(reactRefreshRuntimePath)};
152
- // Read revision so the import survives sideEffects: false tree-shaking.
153
- import __hdrRevision from ${JSON.stringify(hdrRevisionFilePath)};
154
-
155
- let latestHdrRevision = __hdrRevision;
156
-
157
- export function getReactRouterHdrRevision() {
158
- return latestHdrRevision;
159
- }
160
98
 
161
99
  const RefreshRuntime =
162
100
  __refreshRuntimeModule && __refreshRuntimeModule.performReactRefresh
@@ -166,7 +104,24 @@ const RefreshRuntime =
166
104
  const pendingRouteUpdates = new Map();
167
105
  let flushTimeout;
168
106
  let pendingRevalidation = false;
169
- let pendingComponentRouteRevalidation = false;
107
+ let flushing = false;
108
+ let hdrSession;
109
+ let latestHdrRevision = 0;
110
+ const retiredHdrSessions = new Set();
111
+
112
+ function receiveHdrRevision(message) {
113
+ if (!message || typeof message.sessionId !== 'string' ||
114
+ !Number.isSafeInteger(message.revision) || message.revision < 1 ||
115
+ retiredHdrSessions.has(message.sessionId)) return;
116
+ if (hdrSession !== message.sessionId) {
117
+ if (hdrSession) retiredHdrSessions.add(hdrSession);
118
+ hdrSession = message.sessionId;
119
+ latestHdrRevision = 0;
120
+ }
121
+ if (message.revision <= latestHdrRevision) return;
122
+ latestHdrRevision = message.revision;
123
+ scheduleReactRouterRevalidation();
124
+ }
170
125
 
171
126
  function getCurrentRouterPath(router) {
172
127
  const basename = router.basename || '/';
@@ -207,7 +162,7 @@ export function scheduleReactRouterRouteUpdate(
207
162
  scheduleFlush();
208
163
  }
209
164
 
210
- export function scheduleReactRouterRevalidation() {
165
+ function scheduleReactRouterRevalidation() {
211
166
  pendingRevalidation = true;
212
167
  scheduleFlush();
213
168
  }
@@ -406,45 +361,50 @@ function applyManifestUpdate(nextRoutes) {
406
361
  }
407
362
 
408
363
  async function flush() {
364
+ if (flushing) return;
365
+ if (import.meta.webpackHot && import.meta.webpackHot.status() !== 'idle') {
366
+ scheduleFlush();
367
+ return;
368
+ }
409
369
  const router = window.__reactRouterDataRouter;
410
370
  const routeModules = window.__reactRouterRouteModules;
411
371
  const manifest = window.__reactRouterManifest;
412
372
  const context = window.__reactRouterContext;
413
373
  if (!router || !routeModules || !manifest || !context) {
374
+ scheduleFlush();
414
375
  return;
415
376
  }
416
-
417
- let shouldRevalidate = pendingRevalidation;
418
- pendingRevalidation = false;
419
- const { nextManifest, hmrRoutes, shouldRefreshRouteState } =
420
- applyPendingRouteUpdates(router, routeModules, manifest, context);
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
- }
428
- Object.assign(manifest, nextManifest);
429
- await refreshRouteState(router);
430
- shouldRevalidate = false;
431
- } else if (nextManifest) {
432
- if (hmrRoutes) {
433
- patchCurrentRouteMatches(router, hmrRoutes);
377
+ flushing = true;
378
+ try {
379
+ let shouldRevalidate = pendingRevalidation;
380
+ pendingRevalidation = false;
381
+ const { nextManifest, hmrRoutes, shouldRefreshRouteState } =
382
+ applyPendingRouteUpdates(router, routeModules, manifest, context);
383
+ // Loader updates must be visible during revalidation. Component-only routes
384
+ // stay staged until revalidation completes, matching React Router's HMR flow.
385
+ if (nextManifest && shouldRefreshRouteState) {
386
+ if (hmrRoutes) {
387
+ patchCurrentRouteMatches(router, hmrRoutes);
388
+ }
389
+ Object.assign(manifest, nextManifest);
390
+ await refreshRouteState(router);
391
+ shouldRevalidate = false;
392
+ } else if (nextManifest) {
393
+ if (hmrRoutes) {
394
+ patchCurrentRouteMatches(router, hmrRoutes);
395
+ }
396
+ Object.assign(manifest, nextManifest);
434
397
  }
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 {
398
+ // A replay may represent several server edits, not just the component
399
+ // update in this batch. Only an actual data revalidation can consume it.
400
+ if (shouldRevalidate) {
444
401
  await revalidateRouter(router);
445
402
  }
403
+ performReactRefresh();
404
+ } finally {
405
+ flushing = false;
406
+ if (pendingRevalidation || pendingRouteUpdates.size > 0) scheduleFlush();
446
407
  }
447
- performReactRefresh();
448
408
  }
449
409
 
450
410
  if (typeof window !== 'undefined' && import.meta.webpackHot) {
@@ -452,12 +412,9 @@ if (typeof window !== 'undefined' && import.meta.webpackHot) {
452
412
  ${JSON.stringify(DEV_MANIFEST_UPDATE_EVENT)},
453
413
  applyManifestUpdate
454
414
  );
455
- import.meta.webpackHot.accept(
456
- ${JSON.stringify(hdrRevisionFilePath)},
457
- () => {
458
- latestHdrRevision = __hdrRevision;
459
- scheduleReactRouterRevalidation();
460
- }
415
+ import.meta.webpackHot.on(
416
+ ${JSON.stringify(DEV_HDR_UPDATE_EVENT)},
417
+ receiveHdrRevision
461
418
  );
462
419
  }
463
420
  `;
@@ -20,6 +20,7 @@ import {
20
20
  unregisterReactRouterDevRuntime,
21
21
  } from './dev-generation.js';
22
22
  import { createDevHdrIntentTracker } from './dev-hdr-intent.js';
23
+ import { createDevHdrChannel } from './dev-hdr-channel.js';
23
24
  import { DEV_MANIFEST_UPDATE_EVENT } from './dev-hmr.js';
24
25
  import {
25
26
  getEnvironmentStats,
@@ -57,18 +58,10 @@ type CreateControllerOptions = {
57
58
  * flags) in place, so metadata-only changes no longer need a full reload.
58
59
  */
59
60
  clientPatchesRouteMetadata?: boolean | (() => boolean);
60
- /**
61
- * Invoked after a development attempt commits a re-evaluated node build for
62
- * changed server files. Used to signal hot data revalidation to the client.
63
- */
64
- onNodeRebuildCommitted?: () => void;
65
61
  };
66
62
 
67
63
  const CSS_SOURCE_RELOAD_DELAY_MS = 1000;
68
64
 
69
- const isHdrRevisionFile = (file: string): boolean =>
70
- file.includes('.react-router/hdr-revision.mjs');
71
-
72
65
  const isCssSourceFile = (file: string): boolean =>
73
66
  /\.css(?:\.[cm]?[jt]s)?$/.test(file);
74
67
 
@@ -77,7 +70,6 @@ export const createReactRouterDevRuntimeController = ({
77
70
  isBuild,
78
71
  buildPlan,
79
72
  clientPatchesRouteMetadata,
80
- onNodeRebuildCommitted,
81
73
  }: CreateControllerOptions): ReactRouterDevRuntimeController => {
82
74
  if (isBuild) {
83
75
  return {
@@ -122,7 +114,18 @@ export const createReactRouterDevRuntimeController = ({
122
114
  }, CSS_SOURCE_RELOAD_DELAY_MS);
123
115
  };
124
116
 
117
+ const hdrChannels = new WeakMap<
118
+ RuntimeBinding,
119
+ ReturnType<typeof createDevHdrChannel>
120
+ >();
121
+ const isHmrEnabled = () =>
122
+ typeof clientPatchesRouteMetadata === 'function'
123
+ ? clientPatchesRouteMetadata()
124
+ : clientPatchesRouteMetadata === true;
125
+
125
126
  const closeBinding = (binding: RuntimeBinding, error?: Error): void => {
127
+ hdrChannels.get(binding)?.close();
128
+ hdrChannels.delete(binding);
126
129
  if (scheduledCssAssetOwnershipReload) {
127
130
  clearTimeout(scheduledCssAssetOwnershipReload);
128
131
  scheduledCssAssetOwnershipReload = undefined;
@@ -174,7 +177,7 @@ export const createReactRouterDevRuntimeController = ({
174
177
  identity.node === binding.runtime.getCommittedNodeIdentity()
175
178
  ) {
176
179
  hdrIntentsByPair.get(pair)?.signalCommitted(nodeCompilation, () => {
177
- onNodeRebuildCommitted?.();
180
+ hdrChannels.get(binding)?.publish();
178
181
  });
179
182
  }
180
183
  } catch (cause) {
@@ -278,11 +281,7 @@ export const createReactRouterDevRuntimeController = ({
278
281
  if (sessions.getActiveBinding()?.runtime !== runtime) {
279
282
  return;
280
283
  }
281
- const patchesRouteMetadata =
282
- typeof clientPatchesRouteMetadata === 'function'
283
- ? clientPatchesRouteMetadata()
284
- : clientPatchesRouteMetadata;
285
- if (patchesRouteMetadata) {
284
+ if (isHmrEnabled()) {
286
285
  server.sockWrite('custom', {
287
286
  event: DEV_MANIFEST_UPDATE_EVENT,
288
287
  data: manifest.routes,
@@ -294,6 +293,14 @@ export const createReactRouterDevRuntimeController = ({
294
293
  onWarning: message => api.logger.warn(message),
295
294
  });
296
295
  const binding = sessions.createBinding(server, runtime);
296
+ hdrChannels.set(
297
+ binding,
298
+ createDevHdrChannel({
299
+ hot: server.environments.web.hot,
300
+ isEnabled: () =>
301
+ sessions.getActiveBinding() === binding && isHmrEnabled(),
302
+ })
303
+ );
297
304
  registerReactRouterDevRuntime(server, runtime);
298
305
  sessions.bindCloseObservation(binding);
299
306
  },
@@ -430,9 +437,7 @@ export const createReactRouterDevRuntimeController = ({
430
437
  hdrIntents.capture(
431
438
  compilation,
432
439
  changes.known &&
433
- Array.from(changes.files).some(
434
- file => !isHdrRevisionFile(file) && !isCssSourceFile(file)
435
- )
440
+ Array.from(changes.files).some(file => !isCssSourceFile(file))
436
441
  );
437
442
  pair.latestNodeStart = {
438
443
  status: 'started',
package/src/dev-server.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { pathToFileURL } from 'node:url';
2
3
  import type { RsbuildConfig } from '@rsbuild/core';
3
4
  import type { ServerBuild } from 'react-router';
4
5
  import { installDevServerSourceMapSupport } from './dev-source-maps.js';
6
+ import { resolveAppPackagePath } from './plugin-utils.js';
5
7
 
6
8
  export type ServerSetup = Exclude<
7
9
  NonNullable<NonNullable<RsbuildConfig['server']>['setup']>,
@@ -18,6 +20,7 @@ type RequestHandler = (request: Request) => Response | Promise<Response>;
18
20
  type BuildProvider = () => Promise<ServerBuild>;
19
21
 
20
22
  export type DevServerMiddlewareDependencies = {
23
+ rootPath: string;
21
24
  loadBuild: BuildProvider;
22
25
  createRequestHandler?: (
23
26
  build: BuildProvider,
@@ -39,9 +42,22 @@ export const createDevServerMiddleware = (
39
42
 
40
43
  const getListener = () => {
41
44
  listenerPromise ??= (async () => {
42
- const createRequestHandler =
43
- dependencies.createRequestHandler ??
44
- (await import('react-router')).createRequestHandler;
45
+ let createRequestHandler = dependencies.createRequestHandler;
46
+ if (!createRequestHandler) {
47
+ const reactRouterPath = resolveAppPackagePath(
48
+ 'react-router',
49
+ dependencies.rootPath
50
+ );
51
+ if (!reactRouterPath) {
52
+ throw new Error('Cannot resolve react-router from the application.');
53
+ }
54
+ // Match the application's browser/server bundles, including when the
55
+ // plugin is linked from a workspace with a different Router version.
56
+ const routerModule: typeof import('react-router') = await import(
57
+ pathToFileURL(reactRouterPath).href
58
+ );
59
+ createRequestHandler = routerModule.createRequestHandler;
60
+ }
45
61
  const createRequestListener =
46
62
  dependencies.createRequestListener ??
47
63
  (await import('@remix-run/node-fetch-server')).createRequestListener;
@@ -73,8 +89,10 @@ export const createDevServerMiddleware = (
73
89
 
74
90
  export const createReactRouterDevServerSetup = ({
75
91
  loadBuild,
92
+ rootPath,
76
93
  }: {
77
94
  loadBuild: BuildProvider;
95
+ rootPath: string;
78
96
  }): ServerSetup =>
79
97
  function reactRouterDevServerSetup(context) {
80
98
  if (context.action !== 'dev') {
@@ -85,6 +103,8 @@ export const createReactRouterDevServerSetup = ({
85
103
  // request handler.
86
104
  return () => {
87
105
  installDevServerSourceMapSupport();
88
- context.server.middlewares.use(createDevServerMiddleware({ loadBuild }));
106
+ context.server.middlewares.use(
107
+ createDevServerMiddleware({ loadBuild, rootPath })
108
+ );
89
109
  };
90
110
  };