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.
- package/README.md +58 -208
- package/dist/511.js +370 -331
- package/dist/build-output-transforms.d.ts +30 -6
- package/dist/classic-mode.d.ts +56 -0
- package/dist/config-imports.d.ts +8 -2
- package/dist/constants.d.ts +3 -0
- package/dist/dev-background-resources.d.ts +3 -1
- package/dist/dev-generation.d.ts +2 -3
- package/dist/dev-hmr.d.ts +8 -2
- package/dist/dev-runtime-controller.d.ts +6 -1
- package/dist/dev-source-maps.d.ts +4 -0
- package/dist/effect-runtime.d.ts +17 -5
- package/dist/entry-paths.d.ts +16 -0
- package/dist/environment-output.d.ts +6 -0
- package/dist/export-utils.d.ts +2 -1
- package/dist/index.cjs +3957 -2208
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3470 -1793
- package/dist/lazy-compilation-prewarm.d.ts +8 -5
- package/dist/manifest.d.ts +16 -4
- package/dist/mode-plan.d.ts +82 -0
- package/dist/modify-browser-manifest.d.ts +8 -4
- package/dist/plugin-utils.d.ts +27 -1
- package/dist/prerender-build.d.ts +5 -5
- package/dist/prerender.d.ts +1 -5
- package/dist/react-router-config.d.ts +11 -3
- package/dist/route-artifacts.d.ts +4 -2
- package/dist/route-chunks.d.ts +2 -1
- package/dist/route-component-transform.d.ts +0 -2
- package/dist/route-export-pruning.d.ts +4 -1
- package/dist/route-imports.d.ts +18 -0
- package/dist/route-transform-tasks.d.ts +5 -0
- package/dist/route-watch.d.ts +11 -6
- package/dist/rsc-dev-server.d.ts +26 -0
- package/dist/rsc-prerender.d.ts +54 -0
- package/dist/rsc-route-config.d.ts +6 -0
- package/dist/rsc-route-exports.d.ts +15 -0
- package/dist/rsc-route-transform-loader.cjs +43 -0
- package/dist/rsc-route-transform-loader.d.ts +30 -0
- package/dist/rsc-route-transform-loader.js +16 -0
- package/dist/rsc-route-transform-registration.d.ts +12 -0
- package/dist/rsc-route-transforms.d.ts +21 -0
- package/dist/rsc-support.d.ts +23 -0
- package/dist/rsc-virtual-modules.d.ts +19 -0
- package/dist/server-build-plan.d.ts +2 -1
- package/dist/server-build-resolution.d.ts +1 -2
- package/dist/server-utils.d.ts +4 -5
- package/dist/ssr-asset-relocation.d.ts +98 -0
- package/dist/templates/entry.rsc.client.d.ts +1 -0
- package/dist/templates/entry.rsc.client.js +61 -0
- package/dist/templates/entry.rsc.d.ts +9 -0
- package/dist/templates/entry.rsc.js +38 -0
- package/dist/templates/entry.rsc.ssr.d.ts +4 -0
- package/dist/templates/entry.rsc.ssr.js +24 -0
- package/dist/typegen.d.ts +4 -2
- package/dist/types.d.ts +30 -1
- package/package.json +69 -14
- package/src/build-output-transforms.ts +155 -21
- package/src/classic-mode.ts +253 -0
- package/src/config-imports.ts +153 -6
- package/src/constants.ts +6 -2
- package/src/dev-background-resources.ts +46 -85
- package/src/dev-generation.ts +52 -33
- package/src/dev-hmr.ts +112 -80
- package/src/dev-runtime-artifacts.ts +11 -12
- package/src/dev-runtime-controller.ts +75 -85
- package/src/dev-runtime-session.ts +14 -18
- package/src/dev-server.ts +2 -0
- package/src/dev-source-maps.ts +257 -0
- package/src/effect-runtime.ts +105 -57
- package/src/entry-paths.ts +80 -0
- package/src/environment-output.ts +55 -0
- package/src/export-utils.ts +15 -11
- package/src/index.ts +661 -496
- package/src/lazy-compilation-prewarm.ts +20 -4
- package/src/manifest.ts +157 -82
- package/src/mode-plan.ts +367 -0
- package/src/modify-browser-manifest.ts +130 -124
- package/src/plugin-utils.ts +103 -39
- package/src/prerender-build.ts +82 -104
- package/src/prerender.ts +23 -24
- package/src/react-router-config.ts +72 -26
- package/src/route-artifacts.ts +96 -66
- package/src/route-chunks.ts +167 -51
- package/src/route-component-transform.ts +14 -21
- package/src/route-export-pruning.ts +4 -3
- package/src/route-imports.ts +100 -0
- package/src/route-transform-tasks.ts +39 -25
- package/src/route-watch.ts +166 -188
- package/src/rsc-dev-server.ts +112 -0
- package/src/rsc-prerender.ts +362 -0
- package/src/rsc-route-config.ts +175 -0
- package/src/rsc-route-exports.ts +67 -0
- package/src/rsc-route-transform-loader.ts +65 -0
- package/src/rsc-route-transform-registration.ts +145 -0
- package/src/rsc-route-transforms.ts +995 -0
- package/src/rsc-runtime.d.ts +143 -0
- package/src/rsc-support.ts +116 -0
- package/src/rsc-virtual-modules.ts +113 -0
- package/src/server-build-plan.ts +14 -3
- package/src/server-build-resolution.ts +37 -47
- package/src/server-utils.ts +21 -35
- package/src/ssr-asset-relocation.ts +183 -0
- package/src/ssr-externals.ts +8 -26
- package/src/templates/entry.rsc.client.tsx +168 -0
- package/src/templates/entry.rsc.ssr.tsx +45 -0
- package/src/templates/entry.rsc.tsx +80 -0
- package/src/typegen.ts +40 -23
- package/src/types.ts +39 -1
- 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
|
+
};
|
package/src/ssr-externals.ts
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 {
|
|
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;
|