rsbuild-plugin-react-router 0.7.2 → 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.
Files changed (53) hide show
  1. package/README.md +14 -2
  2. package/dist/468.js +48 -0
  3. package/dist/511.js +60 -86
  4. package/dist/819.js +64 -0
  5. package/dist/build-output-transforms.d.ts +3 -1
  6. package/dist/constants.d.ts +1 -0
  7. package/dist/dev-hdr-channel.d.ts +9 -0
  8. package/dist/dev-hmr.d.ts +1 -22
  9. package/dist/dev-runtime-controller.d.ts +1 -6
  10. package/dist/dev-server.d.ts +3 -1
  11. package/dist/index.cjs +5417 -5080
  12. package/dist/index.js +1701 -1441
  13. package/dist/manifest-assets.d.ts +34 -0
  14. package/dist/manifest-snapshot.d.ts +17 -0
  15. package/dist/manifest-state.d.ts +12 -0
  16. package/dist/manifest.d.ts +2 -22
  17. package/dist/modify-browser-manifest.d.ts +2 -0
  18. package/dist/node-only-manifest.d.ts +8 -0
  19. package/dist/plugin-utils.d.ts +1 -1
  20. package/dist/rsc-prerender.d.ts +3 -2
  21. package/dist/server-build-worker-client.d.ts +20 -0
  22. package/dist/server-build-worker-protocol.d.ts +84 -0
  23. package/dist/server-build-worker.d.ts +1 -0
  24. package/dist/server-build-worker.js +123 -0
  25. package/dist/server-utils.d.ts +1 -2
  26. package/dist/types.d.ts +6 -0
  27. package/package.json +4 -4
  28. package/src/build-output-transforms.ts +22 -1
  29. package/src/classic-mode.ts +0 -1
  30. package/src/constants.ts +3 -0
  31. package/src/dev-hdr-channel.ts +38 -0
  32. package/src/dev-hmr.ts +57 -100
  33. package/src/dev-runtime-controller.ts +23 -18
  34. package/src/dev-server.ts +24 -4
  35. package/src/index.ts +229 -144
  36. package/src/lazy-compilation.ts +7 -2
  37. package/src/manifest-assets.ts +228 -0
  38. package/src/manifest-snapshot.ts +80 -0
  39. package/src/manifest-state.ts +71 -0
  40. package/src/manifest.ts +23 -161
  41. package/src/mode-plan.ts +12 -4
  42. package/src/modify-browser-manifest.ts +47 -18
  43. package/src/node-only-manifest.ts +52 -0
  44. package/src/plugin-utils.ts +6 -2
  45. package/src/prerender-build.ts +76 -86
  46. package/src/route-chunks.ts +152 -63
  47. package/src/rsc-prerender.ts +7 -35
  48. package/src/server-build-resolution.ts +1 -2
  49. package/src/server-build-worker-client.ts +221 -0
  50. package/src/server-build-worker-protocol.ts +69 -0
  51. package/src/server-build-worker.ts +192 -0
  52. package/src/server-utils.ts +0 -2
  53. package/src/types.ts +7 -0
@@ -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;
@@ -13,8 +13,9 @@ import type { Config } from './react-router-config.js';
13
13
  * inline `__FLIGHT_DATA` scripts, served for client-side navigations
14
14
  *
15
15
  * Instead of an HTTP round-trip through a preview server, the RSC server
16
- * bundle's default-exported `fetch` handler is invoked in-process, matching
17
- * how classic mode prerenders through `createRequestHandler`.
16
+ * bundle's default-exported `fetch` handler is invoked directly (in the
17
+ * server build worker), matching how classic mode prerenders through
18
+ * `createRequestHandler`.
18
19
  */
19
20
  export declare const SPA_FALLBACK_REQUEST_PATH: string;
20
21
  type RscPrerenderBuildApi = Pick<RsbuildPluginAPI, 'logger'>;
@@ -0,0 +1,20 @@
1
+ import { type ServerBuildDescription, type ServerBuildWorkerData } from './server-build-worker-protocol.js';
2
+ export type ServerBuildWorker = {
3
+ /** Plain-data view of the classic server build (routes, assets, prerender). */
4
+ description: ServerBuildDescription | undefined;
5
+ /** Runs the request against the server build in the worker. */
6
+ handler(request: Request): Promise<Response>;
7
+ /** Terminates the worker, and with it any handle the server graph opened. */
8
+ close(): Promise<void>;
9
+ };
10
+ /**
11
+ * Evaluate a built server bundle in a worker thread and proxy requests to it.
12
+ * Build-time rendering used to `import()` the bundle into the build process;
13
+ * a module-scope handle in the app's server graph then kept `rsbuild build`
14
+ * alive forever (#135). The worker is terminated by `close()`.
15
+ *
16
+ * The worker's `exit` is its final event, so any exit (including one between
17
+ * requests, e.g. the app calling `process.exit`) is terminal: outstanding and
18
+ * later requests reject instead of waiting for a reply that cannot come.
19
+ */
20
+ export declare const startServerBuildWorker: (data: ServerBuildWorkerData, workerPath?: string) => Promise<ServerBuildWorker>;
@@ -0,0 +1,84 @@
1
+ /** Headers as a structured-cloneable list (the DOM lib's Headers is not iterable here). */
2
+ export declare const headerEntries: (headers: Headers) => [string, string][];
3
+ export type ServerBuildWorkerData = {
4
+ serverBuildPath: string;
5
+ mode: 'classic' | 'rsc';
6
+ };
7
+ export type ServerBuildWorkerRequest = {
8
+ type: 'close';
9
+ } | {
10
+ id: number;
11
+ type: 'request';
12
+ url: string;
13
+ method: string;
14
+ headers: [string, string][];
15
+ body?: Uint8Array<ArrayBuffer>;
16
+ }
17
+ /** Consume a response body only when the parent asks for it. */
18
+ | {
19
+ id: number;
20
+ type: 'read';
21
+ }
22
+ /** The parent released the request, possibly without reading its body. */
23
+ | {
24
+ id: number;
25
+ type: 'abort';
26
+ };
27
+ export type SerializedResponse = {
28
+ status: number;
29
+ statusText: string;
30
+ headers: [string, string][];
31
+ hasBody: boolean;
32
+ };
33
+ export type SerializedError = {
34
+ message: string;
35
+ stack?: string;
36
+ name?: string;
37
+ };
38
+ export type ServerBuildWorkerResponse = {
39
+ type: 'closed';
40
+ }
41
+ /** Sent once the bundle is evaluated; carries the classic build description. */
42
+ | {
43
+ type: 'ready';
44
+ description?: ServerBuildDescription;
45
+ } | {
46
+ type: 'reply';
47
+ id: number;
48
+ ok: true;
49
+ response: SerializedResponse;
50
+ } | {
51
+ type: 'body';
52
+ id: number;
53
+ ok: true;
54
+ body: Uint8Array<ArrayBuffer>;
55
+ } | {
56
+ type: 'reply';
57
+ id: number;
58
+ ok: false;
59
+ error: SerializedError;
60
+ };
61
+ /**
62
+ * The parts of a classic React Router server build that build-time rendering
63
+ * reads, as plain data: route module exports are reported by presence only.
64
+ */
65
+ export type ServerBuildDescription = {
66
+ prerender?: string[];
67
+ routes: Record<string, {
68
+ id?: string;
69
+ parentId?: string;
70
+ path?: string;
71
+ index?: boolean;
72
+ caseSensitive?: boolean;
73
+ module: {
74
+ default: boolean;
75
+ ErrorBoundary: boolean;
76
+ loader: boolean;
77
+ };
78
+ }>;
79
+ assets: {
80
+ routes: Record<string, {
81
+ hasLoader?: boolean;
82
+ }>;
83
+ };
84
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,123 @@
1
+ let description, handler;
2
+ import { parentPort, workerData } from "node:worker_threads";
3
+ import { pathToFileURL } from "node:url";
4
+ import { createRequestHandler } from "react-router";
5
+ import { PLUGIN_NAME } from "./468.js";
6
+ import { resolveServerBuildModule, headerEntries } from "./819.js";
7
+ if (!parentPort) throw Error('server-build-worker must run as a worker thread');
8
+ let { serverBuildPath: serverBuildPath, mode: mode } = workerData;
9
+ process.env.IS_RR_BUILD_REQUEST = 'yes';
10
+ let post = (message, transfer = [])=>{
11
+ parentPort.postMessage(message, transfer);
12
+ }, serializeError = (error)=>({
13
+ message: String(error?.message ?? error),
14
+ stack: 'string' == typeof error?.stack ? error.stack : void 0,
15
+ name: 'string' == typeof error?.name ? error.name : void 0
16
+ }), server_build_worker_buildModule = await import(pathToFileURL(serverBuildPath).href);
17
+ if ('classic' === mode) {
18
+ let build = await resolveServerBuildModule(server_build_worker_buildModule, `Server build ${JSON.stringify(serverBuildPath)}`);
19
+ description = {
20
+ prerender: build.prerender,
21
+ routes: Object.fromEntries(Object.entries(build.routes).flatMap(([id, route])=>route ? [
22
+ [
23
+ id,
24
+ {
25
+ id: route.id,
26
+ parentId: route.parentId,
27
+ path: route.path,
28
+ index: route.index,
29
+ caseSensitive: route.caseSensitive,
30
+ module: {
31
+ default: void 0 !== route.module.default,
32
+ ErrorBoundary: void 0 !== route.module.ErrorBoundary,
33
+ loader: void 0 !== route.module.loader
34
+ }
35
+ }
36
+ ]
37
+ ] : [])),
38
+ assets: {
39
+ routes: Object.fromEntries(Object.entries(build.assets.routes).flatMap(([id, route])=>route ? [
40
+ [
41
+ id,
42
+ {
43
+ hasLoader: route.hasLoader
44
+ }
45
+ ]
46
+ ] : []))
47
+ }
48
+ }, handler = createRequestHandler(build, 'production');
49
+ } else handler = ((buildModule)=>{
50
+ let fetch = 'function' == typeof buildModule?.default?.fetch ? buildModule.default.fetch : 'function' == typeof buildModule?.default?.default?.fetch ? buildModule.default.default.fetch : null;
51
+ if (!fetch) throw Error(`[${PLUGIN_NAME}] RSC server build ${JSON.stringify(serverBuildPath)} must default-export an object with a fetch function.`);
52
+ return fetch;
53
+ })(server_build_worker_buildModule);
54
+ let controllers = new Map(), responses = new Map(), release = (id)=>{
55
+ controllers.get(id)?.abort(), controllers.delete(id), responses.delete(id);
56
+ };
57
+ parentPort.on('message', async (message)=>{
58
+ if ('close' === message.type) {
59
+ for (let id of controllers.keys())release(id);
60
+ post({
61
+ type: 'closed'
62
+ });
63
+ return;
64
+ }
65
+ if ('abort' === message.type) {
66
+ responses.get(message.id)?.body?.cancel().catch(()=>{}), release(message.id);
67
+ return;
68
+ }
69
+ if ('read' === message.type) {
70
+ try {
71
+ let response = responses.get(message.id);
72
+ if (!response) throw Error('Server build response was released');
73
+ let body = new Uint8Array(await response.arrayBuffer());
74
+ release(message.id), post({
75
+ type: 'body',
76
+ id: message.id,
77
+ ok: !0,
78
+ body
79
+ }, [
80
+ body.buffer
81
+ ]);
82
+ } catch (error) {
83
+ release(message.id), post({
84
+ type: 'reply',
85
+ id: message.id,
86
+ ok: !1,
87
+ error: serializeError(error)
88
+ });
89
+ }
90
+ return;
91
+ }
92
+ let controller = new AbortController();
93
+ controllers.set(message.id, controller);
94
+ try {
95
+ let response = await handler(new Request(message.url, {
96
+ method: message.method,
97
+ headers: message.headers,
98
+ body: message.body,
99
+ signal: controller.signal
100
+ }));
101
+ response.body ? responses.set(message.id, response) : release(message.id), post({
102
+ type: 'reply',
103
+ id: message.id,
104
+ ok: !0,
105
+ response: {
106
+ status: response.status,
107
+ statusText: response.statusText,
108
+ headers: headerEntries(response.headers),
109
+ hasBody: null !== response.body
110
+ }
111
+ });
112
+ } catch (error) {
113
+ release(message.id), post({
114
+ type: 'reply',
115
+ id: message.id,
116
+ ok: !1,
117
+ error: serializeError(error)
118
+ });
119
+ }
120
+ }), post({
121
+ type: 'ready',
122
+ description: description
123
+ });
@@ -1,5 +1,4 @@
1
1
  import type { ServerBuild } from 'react-router';
2
- import { resolveServerBuildModule } from './server-build-resolution.js';
3
2
  import type { Route } from './types.js';
4
3
  /**
5
4
  * Generates the server build template string.
@@ -34,5 +33,5 @@ interface ServerBuildOptions {
34
33
  * @returns The generated module content as a string
35
34
  */
36
35
  export declare function generateServerBuild(routes: Record<string, Route>, options: ServerBuildOptions): string;
37
- export { resolveServerBuildModule };
38
36
  export declare function resolveReactRouterServerBuild(buildModule: unknown): Promise<ServerBuild>;
37
+ export {};
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.2",
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"
@@ -1,3 +1,4 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import type { RsbuildPluginAPI, TransformHandler } from '@rsbuild/core';
2
3
  import jsesc from 'jsesc';
3
4
  import { relative } from 'pathe';
@@ -80,6 +81,8 @@ type RegisterBuildOutputTransformsOptions = {
80
81
  resolvedServerOutput: 'module' | 'commonjs';
81
82
  performanceProfiler: ReactRouterPerformanceProfiler;
82
83
  getLatestServerManifest: () => ReactRouterManifest | null;
84
+ /** File holding the captured manifests; a dependency of the server-manifest module. */
85
+ serverManifestStampPath: string;
83
86
  getLatestServerManifestByBundleId: (
84
87
  bundleId: string
85
88
  ) => ReactRouterManifest | undefined;
@@ -112,6 +115,7 @@ export const registerBuildOutputTransforms = ({
112
115
  resolvedServerOutput,
113
116
  performanceProfiler,
114
117
  getLatestServerManifest,
118
+ serverManifestStampPath,
115
119
  getLatestServerManifestByBundleId,
116
120
  routes,
117
121
  pluginOptions,
@@ -191,7 +195,7 @@ export const registerBuildOutputTransforms = ({
191
195
 
192
196
  api.transform(
193
197
  {
194
- test: /virtual\/react-router\/(browser|server)-manifest/,
198
+ test: /virtual\/react-router\/((browser|server)-manifest|server-build)/,
195
199
  },
196
200
  async args =>
197
201
  performanceProfiler.record(
@@ -205,11 +209,28 @@ export const registerBuildOutputTransforms = ({
205
209
  };
206
210
  }
207
211
 
212
+ // Cache identity for a module whose source never changes (#136);
213
+ // see `serverManifestStampPath` in index.ts.
214
+ if (existsSync(serverManifestStampPath)) {
215
+ args.addDependency(serverManifestStampPath);
216
+ } else {
217
+ args.addMissingDependency(serverManifestStampPath);
218
+ }
219
+ // The virtual server build contains the bundle's route table. A
220
+ // partition change must invalidate it alongside the asset manifest.
221
+ if (args.resource.includes('virtual/react-router/server-build')) {
222
+ return { code: args.code };
223
+ }
208
224
  const bundleMatch = args.resource.match(
209
225
  /virtual\/react-router\/server-manifest(?:-([^?]+))?/
210
226
  );
211
227
  const bundleId = bundleMatch?.[1]?.replace(/\.js$/, '');
212
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
+ }
213
234
  const manifest =
214
235
  (latestServerManifest
215
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
+ };