rsbuild-plugin-react-router 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +58 -208
  2. package/dist/511.js +370 -331
  3. package/dist/build-output-transforms.d.ts +30 -6
  4. package/dist/classic-mode.d.ts +56 -0
  5. package/dist/config-imports.d.ts +8 -2
  6. package/dist/constants.d.ts +3 -0
  7. package/dist/dev-background-resources.d.ts +3 -1
  8. package/dist/dev-generation.d.ts +2 -3
  9. package/dist/dev-hmr.d.ts +8 -2
  10. package/dist/dev-runtime-controller.d.ts +6 -1
  11. package/dist/dev-source-maps.d.ts +4 -0
  12. package/dist/effect-runtime.d.ts +17 -5
  13. package/dist/entry-paths.d.ts +16 -0
  14. package/dist/environment-output.d.ts +6 -0
  15. package/dist/export-utils.d.ts +2 -1
  16. package/dist/index.cjs +3957 -2208
  17. package/dist/index.d.ts +3 -1
  18. package/dist/index.js +3470 -1793
  19. package/dist/lazy-compilation-prewarm.d.ts +8 -5
  20. package/dist/manifest.d.ts +16 -4
  21. package/dist/mode-plan.d.ts +82 -0
  22. package/dist/modify-browser-manifest.d.ts +8 -4
  23. package/dist/plugin-utils.d.ts +27 -1
  24. package/dist/prerender-build.d.ts +5 -5
  25. package/dist/prerender.d.ts +1 -5
  26. package/dist/react-router-config.d.ts +11 -3
  27. package/dist/route-artifacts.d.ts +4 -2
  28. package/dist/route-chunks.d.ts +2 -1
  29. package/dist/route-component-transform.d.ts +0 -2
  30. package/dist/route-export-pruning.d.ts +4 -1
  31. package/dist/route-imports.d.ts +18 -0
  32. package/dist/route-transform-tasks.d.ts +5 -0
  33. package/dist/route-watch.d.ts +11 -6
  34. package/dist/rsc-dev-server.d.ts +26 -0
  35. package/dist/rsc-prerender.d.ts +54 -0
  36. package/dist/rsc-route-config.d.ts +6 -0
  37. package/dist/rsc-route-exports.d.ts +15 -0
  38. package/dist/rsc-route-transform-loader.cjs +43 -0
  39. package/dist/rsc-route-transform-loader.d.ts +30 -0
  40. package/dist/rsc-route-transform-loader.js +16 -0
  41. package/dist/rsc-route-transform-registration.d.ts +12 -0
  42. package/dist/rsc-route-transforms.d.ts +21 -0
  43. package/dist/rsc-support.d.ts +23 -0
  44. package/dist/rsc-virtual-modules.d.ts +19 -0
  45. package/dist/server-build-plan.d.ts +2 -1
  46. package/dist/server-build-resolution.d.ts +1 -2
  47. package/dist/server-utils.d.ts +4 -5
  48. package/dist/ssr-asset-relocation.d.ts +98 -0
  49. package/dist/templates/entry.rsc.client.d.ts +1 -0
  50. package/dist/templates/entry.rsc.client.js +61 -0
  51. package/dist/templates/entry.rsc.d.ts +9 -0
  52. package/dist/templates/entry.rsc.js +38 -0
  53. package/dist/templates/entry.rsc.ssr.d.ts +4 -0
  54. package/dist/templates/entry.rsc.ssr.js +24 -0
  55. package/dist/typegen.d.ts +4 -2
  56. package/dist/types.d.ts +30 -1
  57. package/package.json +69 -14
  58. package/src/build-output-transforms.ts +155 -21
  59. package/src/classic-mode.ts +253 -0
  60. package/src/config-imports.ts +153 -6
  61. package/src/constants.ts +6 -2
  62. package/src/dev-background-resources.ts +46 -85
  63. package/src/dev-generation.ts +52 -33
  64. package/src/dev-hmr.ts +112 -80
  65. package/src/dev-runtime-artifacts.ts +11 -12
  66. package/src/dev-runtime-controller.ts +75 -85
  67. package/src/dev-runtime-session.ts +14 -18
  68. package/src/dev-server.ts +2 -0
  69. package/src/dev-source-maps.ts +257 -0
  70. package/src/effect-runtime.ts +105 -57
  71. package/src/entry-paths.ts +80 -0
  72. package/src/environment-output.ts +55 -0
  73. package/src/export-utils.ts +15 -11
  74. package/src/index.ts +661 -496
  75. package/src/lazy-compilation-prewarm.ts +20 -4
  76. package/src/manifest.ts +157 -82
  77. package/src/mode-plan.ts +367 -0
  78. package/src/modify-browser-manifest.ts +130 -124
  79. package/src/plugin-utils.ts +103 -39
  80. package/src/prerender-build.ts +82 -104
  81. package/src/prerender.ts +23 -24
  82. package/src/react-router-config.ts +72 -26
  83. package/src/route-artifacts.ts +96 -66
  84. package/src/route-chunks.ts +167 -51
  85. package/src/route-component-transform.ts +14 -21
  86. package/src/route-export-pruning.ts +4 -3
  87. package/src/route-imports.ts +100 -0
  88. package/src/route-transform-tasks.ts +39 -25
  89. package/src/route-watch.ts +166 -188
  90. package/src/rsc-dev-server.ts +112 -0
  91. package/src/rsc-prerender.ts +362 -0
  92. package/src/rsc-route-config.ts +175 -0
  93. package/src/rsc-route-exports.ts +67 -0
  94. package/src/rsc-route-transform-loader.ts +65 -0
  95. package/src/rsc-route-transform-registration.ts +145 -0
  96. package/src/rsc-route-transforms.ts +995 -0
  97. package/src/rsc-runtime.d.ts +143 -0
  98. package/src/rsc-support.ts +116 -0
  99. package/src/rsc-virtual-modules.ts +113 -0
  100. package/src/server-build-plan.ts +14 -3
  101. package/src/server-build-resolution.ts +37 -47
  102. package/src/server-utils.ts +21 -35
  103. package/src/ssr-asset-relocation.ts +183 -0
  104. package/src/ssr-externals.ts +8 -26
  105. package/src/templates/entry.rsc.client.tsx +168 -0
  106. package/src/templates/entry.rsc.ssr.tsx +45 -0
  107. package/src/templates/entry.rsc.tsx +80 -0
  108. package/src/typegen.ts +40 -23
  109. package/src/types.ts +39 -1
  110. package/src/warnings/warn-on-client-source-maps.ts +6 -10
@@ -0,0 +1,183 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { dirname, join } from 'pathe';
5
+
6
+ /**
7
+ * Relocation of server-only static assets to the client build.
8
+ *
9
+ * In React Router framework mode a route's server-only code (a loader or a
10
+ * `.server` module) can import an asset — for example
11
+ * `import txtUrl from "./file.txt?url"` or a `.css?url` file. The imported URL
12
+ * is returned to the client (e.g. as loader data) and fetched from the client
13
+ * build directory at runtime, so the asset file must exist under
14
+ * `build/client` even though only the node/server compilation referenced it.
15
+ *
16
+ * Rspack emits these assets into the server output (`build/server/static/...`)
17
+ * and never into the client output. This module mirrors upstream React
18
+ * Router's Vite plugin, which internally enables `ssrEmitAssets` and then, in
19
+ * the SSR `writeBundle` hook, moves server-emitted static assets into the
20
+ * client assets directory and strips them from the server build
21
+ * (`react-router-dev/vite/plugin.ts`). Here the equivalent runs inside the
22
+ * node compilation's `processAssets` hook: static assets are written into the
23
+ * client output at the same public path and deleted from the server
24
+ * compilation so the server build never ships duplicate static files.
25
+ */
26
+
27
+ /**
28
+ * The subset of an Rspack asset info that identifies static assets emitted by
29
+ * asset modules (`asset/resource`) and the CSS pipeline.
30
+ */
31
+ export interface RelocatableAssetInfo {
32
+ /**
33
+ * Set by Rspack for assets produced from a source module (asset modules and
34
+ * extracted CSS). JavaScript chunks and generated files such as
35
+ * `package.json` do not carry a `sourceFilename`.
36
+ */
37
+ sourceFilename?: string;
38
+ /**
39
+ * Set for JavaScript chunk assets. Static assets never set this, so it is
40
+ * used to keep code-split JS in the server build.
41
+ */
42
+ javascriptModule?: boolean;
43
+ }
44
+
45
+ /**
46
+ * The minimal compilation surface needed to collect and relocate assets. Kept
47
+ * narrow so the logic can be unit-tested without a real Rspack compilation.
48
+ */
49
+ export interface RelocatableAssetCompilation {
50
+ assets: Record<string, unknown>;
51
+ getAsset(name: string):
52
+ | {
53
+ name: string;
54
+ source: { buffer(): Buffer };
55
+ info: RelocatableAssetInfo;
56
+ }
57
+ | undefined
58
+ | void;
59
+ deleteAsset(name: string): void;
60
+ }
61
+
62
+ export interface CollectedServerAsset {
63
+ /** The public path of the asset, relative to the compilation output root. */
64
+ name: string;
65
+ source: { buffer(): Buffer };
66
+ }
67
+
68
+ /**
69
+ * Determine whether an emitted asset is a static asset that must live in the
70
+ * client build. Static assets (asset modules and CSS) carry a `sourceFilename`
71
+ * and are not JavaScript chunks. Code-split JavaScript is intentionally
72
+ * excluded so it remains in the server build.
73
+ */
74
+ export const isRelocatableServerAsset = (
75
+ info: RelocatableAssetInfo | undefined
76
+ ): boolean => Boolean(info?.sourceFilename) && !info?.javascriptModule;
77
+
78
+ /**
79
+ * Collect the server-only static assets from a node compilation. Returns them
80
+ * in stable (sorted) order so callers and tests see deterministic output.
81
+ */
82
+ export const collectRelocatableServerAssets = (
83
+ compilation: RelocatableAssetCompilation
84
+ ): CollectedServerAsset[] => {
85
+ const collected: CollectedServerAsset[] = [];
86
+ for (const name of Object.keys(compilation.assets)) {
87
+ const asset = compilation.getAsset(name);
88
+ if (!asset) {
89
+ continue;
90
+ }
91
+ if (!isRelocatableServerAsset(asset.info)) {
92
+ continue;
93
+ }
94
+ collected.push({ name, source: asset.source });
95
+ }
96
+ collected.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
97
+ return collected;
98
+ };
99
+
100
+ export interface RelocateServerAssetsResult {
101
+ /** Assets written into the client output because they were missing or stale. */
102
+ written: string[];
103
+ /**
104
+ * Assets that already existed in the client output. They are still removed
105
+ * from the server build but not rewritten.
106
+ */
107
+ skipped: string[];
108
+ }
109
+
110
+ /**
111
+ * Move server-only static assets into the client output and strip them from
112
+ * the server compilation.
113
+ *
114
+ * For each static asset: if it is missing or stale in the client output at the
115
+ * same public path, write it there; then delete it from the server compilation
116
+ * regardless. This mirrors upstream's move/remove behavior — the server build
117
+ * never ships static assets, and the client build gains fresh server-only
118
+ * assets.
119
+ */
120
+ const digestBuffer = (buffer: Buffer): string =>
121
+ createHash('sha256').update(buffer).digest('hex');
122
+
123
+ export const relocateServerAssetsToClient = async ({
124
+ compilation,
125
+ outputClientPath,
126
+ relocatedDestinations,
127
+ existsSyncFn = existsSync,
128
+ mkdirFn = (dir: string) => mkdir(dir, { recursive: true }),
129
+ readFileFn = readFile,
130
+ renameFn = rename,
131
+ rmFn = rm,
132
+ writeFileFn = writeFile,
133
+ }: {
134
+ compilation: RelocatableAssetCompilation;
135
+ outputClientPath: string;
136
+ /**
137
+ * Destination content digests already handled by a previous compilation
138
+ * (dev rebuilds, serverBundles). Stable dev filenames can carry changed
139
+ * bytes, so the cache is keyed by path and digest rather than path alone.
140
+ */
141
+ relocatedDestinations?: Map<string, string>;
142
+ existsSyncFn?: (path: string) => boolean;
143
+ mkdirFn?: (dir: string) => Promise<unknown>;
144
+ readFileFn?: (path: string) => Promise<Buffer>;
145
+ renameFn?: (oldPath: string, newPath: string) => Promise<void>;
146
+ rmFn?: (path: string, options: { force: true }) => Promise<void>;
147
+ writeFileFn?: (path: string, data: Buffer) => Promise<void>;
148
+ }): Promise<RelocateServerAssetsResult> => {
149
+ const assets = collectRelocatableServerAssets(compilation);
150
+ const written: string[] = [];
151
+ const skipped: string[] = [];
152
+
153
+ for (const asset of assets) {
154
+ const destination = join(outputClientPath, asset.name);
155
+ const sourceBuffer = asset.source.buffer();
156
+ const sourceDigest = digestBuffer(sourceBuffer);
157
+ const cachedDigest = relocatedDestinations?.get(destination);
158
+ const alreadyRelocated =
159
+ cachedDigest === sourceDigest ||
160
+ (existsSyncFn(destination) &&
161
+ digestBuffer(await readFileFn(destination)) === sourceDigest);
162
+ if (alreadyRelocated) {
163
+ skipped.push(asset.name);
164
+ } else {
165
+ await mkdirFn(dirname(destination));
166
+ const temporaryDestination = `${destination}.tmp-${process.pid}-${Date.now()}`;
167
+ await writeFileFn(temporaryDestination, sourceBuffer);
168
+ try {
169
+ await renameFn(temporaryDestination, destination);
170
+ } catch (error) {
171
+ await rmFn(temporaryDestination, { force: true });
172
+ throw error;
173
+ }
174
+ written.push(asset.name);
175
+ }
176
+ relocatedDestinations?.set(destination, sourceDigest);
177
+ // Remove the static asset from the server build so it is not shipped
178
+ // twice. Code-split JavaScript is not collected, so it stays.
179
+ compilation.deleteAsset(asset.name);
180
+ }
181
+
182
+ return { written, skipped };
183
+ };
@@ -28,32 +28,14 @@ export function resolvePackageJson(
28
28
  }
29
29
  }
30
30
 
31
- function safeRealpath(pathname: string): string {
32
- try {
33
- return realpathSync(pathname);
34
- } catch {
35
- return pathname;
36
- }
37
- }
38
-
39
- function isPathInNodeModules(pathname: string): boolean {
40
- return pathname.split(sep).includes('node_modules');
41
- }
42
-
43
31
  export function getSsrExternals(rootDirectory: string): string[] {
44
- const externals: string[] = [];
45
-
46
- for (const name of REACT_ROUTER_EXTERNALS) {
32
+ return REACT_ROUTER_EXTERNALS.filter(name => {
47
33
  const resolved = resolvePackageJson(name, rootDirectory);
48
- if (!resolved) {
49
- continue;
50
- }
51
-
52
- const realPath = safeRealpath(resolved);
53
- if (!isPathInNodeModules(realPath)) {
54
- externals.push(name);
55
- }
56
- }
57
-
58
- return externals;
34
+ if (resolved === null) return false;
35
+ let packageJsonPath = resolved;
36
+ try {
37
+ packageJsonPath = realpathSync(packageJsonPath);
38
+ } catch {}
39
+ return !packageJsonPath.split(sep).includes('node_modules');
40
+ });
59
41
  }
@@ -0,0 +1,168 @@
1
+ import 'virtual/react-router/unstable_rsc/inject-hmr-runtime';
2
+
3
+ import * as React from 'react';
4
+ import { startTransition } from 'react';
5
+ import { hydrateRoot } from 'react-dom/client';
6
+ import type { DataRouter } from 'react-router';
7
+ import {
8
+ unstable_createCallServer as createCallServer,
9
+ unstable_getRSCStream as getRSCStream,
10
+ unstable_RSCHydratedRouter as RSCHydratedRouter,
11
+ type unstable_RSCPayload as RSCPayload,
12
+ } from 'react-router/dom';
13
+ import {
14
+ createFromReadableStream,
15
+ createTemporaryReferenceSet,
16
+ encodeReply,
17
+ setServerCallback,
18
+ } from 'react-server-dom-rspack/client.browser';
19
+
20
+ type RscHydrationWindow = Window & {
21
+ __reactRouterDataRouter?: DataRouter;
22
+ __reactRouterHdrActive?: boolean;
23
+ };
24
+
25
+ const hydrationWindow = window as RscHydrationWindow;
26
+
27
+ const InitialRscDataLoadGuard = ({
28
+ active,
29
+ children,
30
+ }: {
31
+ active: boolean;
32
+ children?: React.ReactNode;
33
+ }): React.ReactNode => {
34
+ React.useEffect(() => {
35
+ if (!active) {
36
+ return;
37
+ }
38
+
39
+ let frame: number;
40
+ const clearGuardOnceDataLoadStarts = () => {
41
+ const router = hydrationWindow.__reactRouterDataRouter;
42
+ if (
43
+ router &&
44
+ (router.state.initialized || router.state.navigation.state !== 'idle')
45
+ ) {
46
+ hydrationWindow.__reactRouterHdrActive = false;
47
+ return;
48
+ }
49
+ frame = requestAnimationFrame(clearGuardOnceDataLoadStarts);
50
+ };
51
+ frame = requestAnimationFrame(clearGuardOnceDataLoadStarts);
52
+ return () => cancelAnimationFrame(frame);
53
+ }, [active]);
54
+ return children;
55
+ };
56
+
57
+ setServerCallback(
58
+ createCallServer({
59
+ createFromReadableStream,
60
+ createTemporaryReferenceSet,
61
+ encodeReply,
62
+ })
63
+ );
64
+
65
+ const hydrate = () => {
66
+ createFromReadableStream<RSCPayload>(getRSCStream()).then(
67
+ payload => {
68
+ startTransition(async () => {
69
+ const formState =
70
+ payload.type === 'render' ? await payload.formState : undefined;
71
+ const needsInitialDataLoadGuard =
72
+ payload.type === 'render' &&
73
+ payload.matches.some(
74
+ match =>
75
+ match.clientLoader != null &&
76
+ !match.hasLoader &&
77
+ match.hydrateFallbackElement != null
78
+ );
79
+
80
+ // React Router's RSC single-fetch hydration skips its request while the
81
+ // router is both idle and uninitialized. An implicitly hydrating
82
+ // clientLoader with a fallback and no server loader can race the layout
83
+ // effect that starts initialization, leaving it with an empty result
84
+ // for parent routes. Its HDR guard already means “the initial data
85
+ // request is required”; keep that guard active until router
86
+ // initialization starts, then clear it in the effect above.
87
+ if (needsInitialDataLoadGuard) {
88
+ hydrationWindow.__reactRouterHdrActive = true;
89
+ }
90
+ hydrateRoot(
91
+ document,
92
+ React.createElement(
93
+ React.StrictMode,
94
+ null,
95
+ React.createElement(
96
+ InitialRscDataLoadGuard,
97
+ { active: needsInitialDataLoadGuard },
98
+ React.createElement(RSCHydratedRouter, {
99
+ createFromReadableStream,
100
+ payload,
101
+ })
102
+ )
103
+ ),
104
+ {
105
+ // @ts-expect-error React Router RSC formState is not typed yet.
106
+ formState,
107
+ }
108
+ );
109
+ });
110
+ },
111
+ error => {
112
+ setTimeout(() => {
113
+ throw error;
114
+ });
115
+ }
116
+ );
117
+ };
118
+
119
+ if (document.readyState === 'loading') {
120
+ document.addEventListener('DOMContentLoaded', hydrate, { once: true });
121
+ } else {
122
+ hydrate();
123
+ }
124
+
125
+ // The RSC plugin emits an unqualified update for server-component changes; in
126
+ // that case navigate to refresh the route tree. The framework integration emits
127
+ // `{ revalidate: true }` for server-only data dependency changes, which can
128
+ // refresh loader data without remounting client or browser state.
129
+ //
130
+ // The RSC plugin's message has no payload; there is no `reload` flag (full
131
+ // reloads travel through the HMR runtime's own `full-reload` message).
132
+ const hot = (
133
+ import.meta as unknown as {
134
+ webpackHot?: {
135
+ on(
136
+ event: string,
137
+ handler: (data?: { revalidate?: boolean }) => void
138
+ ): void;
139
+ };
140
+ }
141
+ ).webpackHot;
142
+
143
+ hot?.on('rsc:update', data => {
144
+ requestAnimationFrame(() => {
145
+ const router = (
146
+ window as typeof window & { __reactRouterDataRouter?: DataRouter }
147
+ ).__reactRouterDataRouter;
148
+ if (data?.revalidate) {
149
+ router?.revalidate();
150
+ return;
151
+ }
152
+ if (router?.navigate) {
153
+ const basename = router.basename || '/';
154
+ let pathname = window.location.pathname;
155
+ if (basename !== '/' && pathname.startsWith(basename)) {
156
+ pathname = pathname.slice(basename.length) || '/';
157
+ if (pathname[0] !== '/') pathname = '/' + pathname;
158
+ }
159
+ void router.navigate(
160
+ pathname + window.location.search + window.location.hash,
161
+ {
162
+ replace: true,
163
+ preventScrollReset: true,
164
+ }
165
+ );
166
+ }
167
+ });
168
+ });
@@ -0,0 +1,45 @@
1
+ import * as React from 'react';
2
+ import { renderToReadableStream as renderHTMLToReadableStream } from 'react-dom/server';
3
+ import {
4
+ unstable_routeRSCServerRequest as routeRSCServerRequest,
5
+ unstable_RSCStaticRouter as RSCStaticRouter,
6
+ } from 'react-router';
7
+ import type { unstable_RSCPayload as RSCPayload } from 'react-router/dom';
8
+ import { createFromReadableStream } from 'react-server-dom-rspack/client.node';
9
+
10
+ type PayloadPromise = Promise<RSCPayload> & {
11
+ _deepestRenderedBoundaryId?: string | null;
12
+ formState?: Promise<unknown>;
13
+ };
14
+
15
+ export async function generateHTML(
16
+ request: Request,
17
+ serverResponse: Response,
18
+ options: {
19
+ bootstrapScripts?: string[];
20
+ bootstrapModules?: string[];
21
+ } = {}
22
+ ): Promise<Response> {
23
+ return routeRSCServerRequest({
24
+ request,
25
+ serverResponse,
26
+ createFromReadableStream,
27
+ async renderHTML(getPayload, renderOptions) {
28
+ const payloadPromise = getPayload() as PayloadPromise;
29
+ payloadPromise.formState ??= payloadPromise.then(payload =>
30
+ payload.type === 'render' ? payload.formState : undefined
31
+ );
32
+
33
+ return renderHTMLToReadableStream(
34
+ <RSCStaticRouter getPayload={getPayload} />,
35
+ {
36
+ ...renderOptions,
37
+ bootstrapModules: options.bootstrapModules,
38
+ bootstrapScripts: options.bootstrapScripts,
39
+ formState: (await payloadPromise.formState) as never,
40
+ signal: request.signal,
41
+ }
42
+ );
43
+ },
44
+ });
45
+ }
@@ -0,0 +1,80 @@
1
+ import {
2
+ createTemporaryReferenceSet,
3
+ decodeAction,
4
+ decodeFormState,
5
+ decodeReply,
6
+ loadServerAction as loadServerActionSync,
7
+ renderToReadableStream,
8
+ } from 'react-server-dom-rspack/server.node';
9
+ import {
10
+ RouterContextProvider,
11
+ unstable_matchRSCServerRequest as matchRSCServerRequest,
12
+ } from 'react-router';
13
+
14
+ import routes from 'virtual/react-router/unstable_rsc/routes';
15
+ import routeDiscovery from 'virtual/react-router/unstable_rsc/route-discovery';
16
+ import basename from 'virtual/react-router/unstable_rsc/basename';
17
+ import allowedActionOrigins from 'virtual/react-router/unstable_rsc/allowed-action-origins';
18
+ import clientVersion from 'virtual/react-router/unstable_rsc/client-version';
19
+ import unstable_reactRouterServeConfig from 'virtual/react-router/unstable_rsc/react-router-serve-config';
20
+ import bootstrapScripts from 'virtual/react-router/unstable_rsc/bootstrap-scripts';
21
+ import getServerManifest from 'virtual/react-router/unstable_rsc/server-manifest';
22
+ import { generateHTML } from './entry.rsc.ssr.js';
23
+
24
+ export { unstable_reactRouterServeConfig };
25
+
26
+ type RscRequestHandler = {
27
+ fetch(
28
+ request: Request,
29
+ requestContext?: RouterContextProvider
30
+ ): Promise<Response>;
31
+ };
32
+
33
+ export function fetchServer(
34
+ request: Request,
35
+ requestContext?: RouterContextProvider
36
+ ): Promise<Response> {
37
+ return matchRSCServerRequest({
38
+ allowedActionOrigins,
39
+ basename,
40
+ // @ts-expect-error Supported React Router 7.x types predate the 8.3 RSC
41
+ // client-version option; those runtimes safely ignore the extra field.
42
+ clientVersion,
43
+ createTemporaryReferenceSet,
44
+ decodeAction: body => decodeAction(body, getServerManifest()),
45
+ decodeFormState: async (actionResult, body) =>
46
+ (await decodeFormState(actionResult, body, getServerManifest())) ??
47
+ undefined,
48
+ decodeReply,
49
+ loadServerAction: (id: string) => Promise.resolve(loadServerActionSync(id)),
50
+ request,
51
+ requestContext,
52
+ routes,
53
+ routeDiscovery,
54
+ generateResponse(match, options) {
55
+ return new Response(renderToReadableStream(match.payload, options), {
56
+ status: match.statusCode,
57
+ headers: match.headers,
58
+ });
59
+ },
60
+ });
61
+ }
62
+
63
+ const handler: RscRequestHandler = {
64
+ async fetch(
65
+ request: Request,
66
+ requestContext?: RouterContextProvider
67
+ ): Promise<Response> {
68
+ if (requestContext && !(requestContext instanceof RouterContextProvider)) {
69
+ requestContext = undefined;
70
+ }
71
+
72
+ return generateHTML(request, await fetchServer(request, requestContext), {
73
+ bootstrapScripts,
74
+ });
75
+ },
76
+ };
77
+
78
+ export default handler;
79
+
80
+ import.meta.webpackHot?.accept();
package/src/typegen.ts CHANGED
@@ -3,7 +3,11 @@ import { dirname, resolve } from 'node:path';
3
3
  import type { RsbuildPluginAPI } from '@rsbuild/core';
4
4
  import type { ResultPromise } from 'execa';
5
5
  import * as Effect from 'effect/Effect';
6
- import { createDelayedPluginTask, tryPluginPromise } from './effect-runtime.js';
6
+ import {
7
+ createDelayedPluginTask,
8
+ type PluginEffectRuntime,
9
+ tryPluginPromise,
10
+ } from './effect-runtime.js';
7
11
  import { resolvePackageJson } from './ssr-externals.js';
8
12
 
9
13
  // Quiet period with no dev compiles before the typegen watch starts. Long
@@ -69,6 +73,7 @@ export const createReactRouterTypegenRunner = (
69
73
  ): ReactRouterTypegenRunner => {
70
74
  let typegenProcess: ResultPromise | undefined;
71
75
  let typegenCommand: TypegenCommand | undefined;
76
+ let watchGeneration = 0;
72
77
 
73
78
  const getTypegenCommand = (): TypegenCommand => {
74
79
  typegenCommand ??= (appDirectory
@@ -96,7 +101,11 @@ export const createReactRouterTypegenRunner = (
96
101
  return;
97
102
  }
98
103
 
104
+ const generation = watchGeneration;
99
105
  const execa = await loadExeca();
106
+ if (typegenProcess || generation !== watchGeneration) {
107
+ return;
108
+ }
100
109
  const { command, args } = getTypegenCommand();
101
110
  const process = execa(command, [...args, 'typegen', '--watch'], {
102
111
  stdio: 'inherit',
@@ -108,6 +117,7 @@ export const createReactRouterTypegenRunner = (
108
117
  },
109
118
 
110
119
  async closeWatch(): Promise<void> {
120
+ watchGeneration += 1;
111
121
  const process = typegenProcess;
112
122
  typegenProcess = undefined;
113
123
  if (!process) {
@@ -128,36 +138,48 @@ export const createReactRouterTypegenRunner = (
128
138
  };
129
139
  };
130
140
 
131
- export const registerReactRouterTypegen = (
141
+ export const registerReactRouterTypegen = async (
132
142
  api: RsbuildPluginAPI,
133
143
  {
144
+ runtime,
134
145
  runner,
135
146
  devWatchDelayMs = TYPEGEN_IDLE_DELAY_MS,
136
147
  appDirectory,
137
148
  }: {
149
+ runtime: PluginEffectRuntime;
138
150
  runner?: ReactRouterTypegenRunner;
139
151
  devWatchDelayMs?: number;
140
152
  appDirectory?: string;
141
- } = {}
142
- ): void => {
153
+ }
154
+ ): Promise<void> => {
143
155
  const resolvedRunner =
144
156
  runner ?? createReactRouterTypegenRunner(loadDefaultExeca, appDirectory);
145
- let devWatchStarted = false;
146
- const devWatchTask = createDelayedPluginTask({
147
- delayMs: devWatchDelayMs,
148
- run: () =>
149
- tryPluginPromise(() => {
150
- devWatchStarted = true;
151
- return resolvedRunner.startWatch();
152
- }).pipe(Effect.asVoid),
153
- onError(error) {
154
- api.logger.warn(
155
- `[react-router] Failed to start React Router typegen watch: ${error}`
156
- );
157
- },
158
- });
159
157
 
160
158
  if (api.context.action !== 'build') {
159
+ let devWatchStarted = false;
160
+ const devWatchTask = createDelayedPluginTask({
161
+ runtime,
162
+ delayMs: devWatchDelayMs,
163
+ run: () =>
164
+ tryPluginPromise(() => {
165
+ devWatchStarted = true;
166
+ return resolvedRunner.startWatch();
167
+ }).pipe(Effect.asVoid),
168
+ onError(error) {
169
+ api.logger.warn(
170
+ `[react-router] Failed to start React Router typegen watch: ${error}`
171
+ );
172
+ },
173
+ });
174
+ const closeWatchEffect = () =>
175
+ devWatchTask
176
+ .cancelEffect()
177
+ .pipe(
178
+ Effect.zipRight(
179
+ Effect.orDie(tryPluginPromise(() => resolvedRunner.closeWatch()))
180
+ )
181
+ );
182
+ await runtime.runPromise(Effect.addFinalizer(closeWatchEffect));
161
183
  // Reschedule on every compile so the typegen watch only starts after a
162
184
  // quiet period with no compiles. Starting it during the initial compile
163
185
  // burst competes with HMR rebuilds for CPU on small machines.
@@ -169,10 +191,5 @@ export const registerReactRouterTypegen = (
169
191
  });
170
192
  }
171
193
 
172
- api.onCloseDevServer(async () => {
173
- await devWatchTask.cancel();
174
- await resolvedRunner.closeWatch();
175
- });
176
-
177
194
  api.onBeforeBuild(() => resolvedRunner.runBuild());
178
195
  };
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { RsbuildConfig } from '@rsbuild/core';
1
+ import type { RsbuildConfig, Rspack } from '@rsbuild/core';
2
2
 
3
3
  export type Route = {
4
4
  id: string;
@@ -30,6 +30,23 @@ export type PluginOptions = {
30
30
  */
31
31
  federation?: boolean;
32
32
 
33
+ /**
34
+ * Enable experimental React Router RSC framework mode.
35
+ * This composes `rsbuild-plugin-rsc` with React Router's Rsbuild
36
+ * environments. Environment names are managed by this plugin.
37
+ * Requires `react-router >=7.18.0 || >=8.0.0`, `rsbuild-plugin-rsc`,
38
+ * and `react-server-dom-rspack`.
39
+ * @default false
40
+ */
41
+ rsc?:
42
+ | boolean
43
+ | {
44
+ layers?: {
45
+ rsc?: Rspack.RuleSetCondition;
46
+ ssr?: Rspack.RuleSetCondition;
47
+ };
48
+ };
49
+
33
50
  /**
34
51
  * Rsbuild dev-only lazy compilation behavior.
35
52
  *
@@ -73,6 +90,27 @@ export type PluginOptions = {
73
90
  onRouteTopologyChange?: () => void | Promise<void>;
74
91
  };
75
92
 
93
+ export type ReactRouterRSCPluginOptions = Omit<PluginOptions, 'rsc'> & {
94
+ /**
95
+ * Optional overrides forwarded to `rsbuild-plugin-rsc`.
96
+ * Environment names are managed by this plugin.
97
+ */
98
+ rsc?: Exclude<NonNullable<PluginOptions['rsc']>, boolean>;
99
+ };
100
+
101
+ export type PrerenderPathsConfig =
102
+ | boolean
103
+ | string[]
104
+ | ((args: {
105
+ getStaticPaths: () => string[];
106
+ }) => boolean | string[] | Promise<boolean | string[]>);
107
+
108
+ export type PrerenderConfigObject = {
109
+ paths: PrerenderPathsConfig;
110
+ concurrency?: number;
111
+ unstable_concurrency?: number;
112
+ };
113
+
76
114
  export type RouteManifestItem = Omit<Route, 'file' | 'children'> & {
77
115
  module: string;
78
116
  clientActionModule?: string;