rsbuild-plugin-react-router 0.4.1 → 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 +72 -240
- package/dist/{451.js → 511.js} +461 -331
- package/dist/build-output-transforms.d.ts +32 -7
- 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 -3
- 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/federation.d.ts +2 -0
- package/dist/index.cjs +3675 -2107
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3614 -2214
- 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/performance.d.ts +4 -1
- 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-module-transform-loader.d.ts +14 -0
- package/dist/route-module-transform-loader.js +44 -0
- package/dist/route-module-transform-rules.d.ts +14 -0
- package/dist/route-transform-tasks.d.ts +6 -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 +34 -6
- package/package.json +76 -22
- package/src/build-output-transforms.ts +181 -43
- 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 -100
- 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/federation.ts +38 -0
- package/src/index.ts +666 -519
- 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/performance.ts +11 -2
- 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-module-transform-loader.ts +149 -0
- package/src/route-module-transform-rules.ts +115 -0
- package/src/route-transform-tasks.ts +44 -29
- 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 +43 -6
- package/src/warnings/warn-on-client-source-maps.ts +6 -10
- package/dist/parallel-route-transform-protocol.d.ts +0 -25
- package/dist/parallel-route-transform-worker.d.ts +0 -1
- package/dist/parallel-route-transform-worker.js +0 -38
- package/dist/parallel-route-transforms.d.ts +0 -29
- package/src/parallel-route-transform-protocol.ts +0 -35
- package/src/parallel-route-transform-worker.ts +0 -82
- package/src/parallel-route-transforms.ts +0 -458
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import type { RsbuildPluginAPI } from '@rsbuild/core';
|
|
5
|
+
import { dirname, relative, resolve } from 'pathe';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import { PLUGIN_NAME, SPA_FALLBACK_HTML_FILE } from './constants.js';
|
|
8
|
+
import {
|
|
9
|
+
createBuildRequestEffect,
|
|
10
|
+
createBoundedPrerenderTasksEffect,
|
|
11
|
+
} from './prerender-build.js';
|
|
12
|
+
import { normalizeAssetPrefix } from './plugin-utils.js';
|
|
13
|
+
import { getPrerenderConcurrency } from './prerender.js';
|
|
14
|
+
import type { Config } from './react-router-config.js';
|
|
15
|
+
import { runPluginEffect } from './effect-runtime.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* RSC-mode prerendering.
|
|
19
|
+
*
|
|
20
|
+
* Mirrors React Router's upstream RSC behavior, feeding the shared
|
|
21
|
+
* `prerender` plugin with one request per prerender path (plus
|
|
22
|
+
* `/__spa-fallback.html` when `ssr: false`) and post-processes each HTML
|
|
23
|
+
* response into two artifacts in the client build directory:
|
|
24
|
+
*
|
|
25
|
+
* - `<path>/index.html` — the prerendered document
|
|
26
|
+
* - `<path>.rsc` (`_.rsc` for `/`) — the RSC payload reassembled from the
|
|
27
|
+
* inline `__FLIGHT_DATA` scripts, served for client-side navigations
|
|
28
|
+
*
|
|
29
|
+
* Instead of an HTTP round-trip through a preview server, the RSC server
|
|
30
|
+
* bundle's default-exported `fetch` handler is invoked in-process, matching
|
|
31
|
+
* how classic mode prerenders through `createRequestHandler`.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export const SPA_FALLBACK_REQUEST_PATH: string = `/${SPA_FALLBACK_HTML_FILE}`;
|
|
35
|
+
|
|
36
|
+
const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
|
|
37
|
+
|
|
38
|
+
const FLIGHT_DATA_SCRIPT_REGEX =
|
|
39
|
+
/<script>\(self\.__FLIGHT_DATA\|\|=\[\]\)\.push\(("(?:[^"\\]|\\.)*")\)<\/script>/gim;
|
|
40
|
+
|
|
41
|
+
type RscRequestHandler = (request: Request) => Promise<Response>;
|
|
42
|
+
|
|
43
|
+
type RscPrerenderBuildApi = Pick<RsbuildPluginAPI, 'logger'>;
|
|
44
|
+
|
|
45
|
+
type RscPrerenderRequest = {
|
|
46
|
+
requestPath: string;
|
|
47
|
+
artifactPath: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type RunReactRouterRscPrerenderBuildOptions = {
|
|
51
|
+
api: RscPrerenderBuildApi;
|
|
52
|
+
hasWebEnvironment: boolean;
|
|
53
|
+
buildDirectory: string;
|
|
54
|
+
serverBuildFile?: string;
|
|
55
|
+
ssr: boolean;
|
|
56
|
+
prerenderConfig: Config['prerender'];
|
|
57
|
+
prerenderPaths: string[];
|
|
58
|
+
basename: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Same empty/'auto'/trailing-slash semantics as asset prefixes.
|
|
62
|
+
export const normalizeRscPrerenderBasename: (basename: string) => string =
|
|
63
|
+
normalizeAssetPrefix;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The request paths to prerender: the resolved prerender paths joined with
|
|
67
|
+
* the basename, plus the SPA fallback document when `ssr: false`.
|
|
68
|
+
*/
|
|
69
|
+
export const getRscPrerenderRequests = ({
|
|
70
|
+
prerenderPaths,
|
|
71
|
+
ssr,
|
|
72
|
+
basename,
|
|
73
|
+
}: {
|
|
74
|
+
prerenderPaths: string[];
|
|
75
|
+
ssr: boolean;
|
|
76
|
+
basename: string;
|
|
77
|
+
}): RscPrerenderRequest[] => {
|
|
78
|
+
const paths = new Set(prerenderPaths);
|
|
79
|
+
if (!ssr) {
|
|
80
|
+
paths.add(SPA_FALLBACK_REQUEST_PATH);
|
|
81
|
+
}
|
|
82
|
+
const normalizedBasename = normalizeRscPrerenderBasename(basename);
|
|
83
|
+
return Array.from(paths).map(path => ({
|
|
84
|
+
artifactPath: path,
|
|
85
|
+
requestPath: `${normalizedBasename}${path.startsWith('/') ? path.slice(1) : path}`,
|
|
86
|
+
}));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Reassembles the RSC payload from the inline flight data scripts that
|
|
91
|
+
* `react-router`'s HTML stream injects into server-rendered documents.
|
|
92
|
+
* Returns `null` when the document carries no flight data.
|
|
93
|
+
*/
|
|
94
|
+
export const extractRscFlightData = (html: string): string | null => {
|
|
95
|
+
const matches = Array.from(html.matchAll(FLIGHT_DATA_SCRIPT_REGEX));
|
|
96
|
+
if (matches.length === 0) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
let rscData = '';
|
|
100
|
+
for (const match of matches) {
|
|
101
|
+
rscData += JSON.parse(match[1]) as string;
|
|
102
|
+
}
|
|
103
|
+
return rscData;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const getRscHtmlFilePath = (pathname: string): string => {
|
|
107
|
+
if (pathname === SPA_FALLBACK_REQUEST_PATH) {
|
|
108
|
+
return SPA_FALLBACK_HTML_FILE;
|
|
109
|
+
}
|
|
110
|
+
return `${pathname.endsWith('/') ? pathname : `${pathname}/`}index.html`;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const getRscPayloadFilePath = (pathname: string): string => {
|
|
114
|
+
if (pathname === '/') {
|
|
115
|
+
return '_.rsc';
|
|
116
|
+
}
|
|
117
|
+
if (pathname === SPA_FALLBACK_REQUEST_PATH) {
|
|
118
|
+
return '__spa-fallback.rsc';
|
|
119
|
+
}
|
|
120
|
+
return `${pathname}.rsc`;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const createRedirectHtml = ({
|
|
124
|
+
pathname,
|
|
125
|
+
location,
|
|
126
|
+
status,
|
|
127
|
+
}: {
|
|
128
|
+
pathname: string;
|
|
129
|
+
location: string;
|
|
130
|
+
status: number;
|
|
131
|
+
}): string => {
|
|
132
|
+
// A short delay causes Google to interpret the redirect as temporary.
|
|
133
|
+
const delay = status === 302 ? 2 : 0;
|
|
134
|
+
return `<!doctype html>
|
|
135
|
+
<head>
|
|
136
|
+
<title>Redirecting to: ${location}</title>
|
|
137
|
+
<meta http-equiv="refresh" content="${delay};url=${location}">
|
|
138
|
+
<meta name="robots" content="noindex">
|
|
139
|
+
</head>
|
|
140
|
+
<body>
|
|
141
|
+
\t<a href="${location}">
|
|
142
|
+
Redirecting from <code>${pathname}</code> to <code>${location}</code>
|
|
143
|
+
</a>
|
|
144
|
+
</body>
|
|
145
|
+
</html>`;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const resolveRscRequestHandler = (
|
|
149
|
+
buildModule: unknown,
|
|
150
|
+
serverBuildPath: string
|
|
151
|
+
): RscRequestHandler => {
|
|
152
|
+
const moduleRecord = buildModule as
|
|
153
|
+
| { default?: { fetch?: unknown; default?: { fetch?: unknown } } }
|
|
154
|
+
| undefined;
|
|
155
|
+
const handler =
|
|
156
|
+
typeof moduleRecord?.default?.fetch === 'function'
|
|
157
|
+
? moduleRecord.default.fetch
|
|
158
|
+
: typeof moduleRecord?.default?.default?.fetch === 'function'
|
|
159
|
+
? moduleRecord.default.default.fetch
|
|
160
|
+
: null;
|
|
161
|
+
if (!handler) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`[${PLUGIN_NAME}] RSC server build ${JSON.stringify(
|
|
164
|
+
serverBuildPath
|
|
165
|
+
)} must default-export an object with a fetch function.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return handler as RscRequestHandler;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const writePrerenderedFile = async ({
|
|
172
|
+
api,
|
|
173
|
+
clientBuildDir,
|
|
174
|
+
filePath,
|
|
175
|
+
contents,
|
|
176
|
+
}: {
|
|
177
|
+
api: RscPrerenderBuildApi;
|
|
178
|
+
clientBuildDir: string;
|
|
179
|
+
filePath: string;
|
|
180
|
+
contents: string | Uint8Array;
|
|
181
|
+
}): Promise<void> => {
|
|
182
|
+
const normalizedPath = filePath.startsWith('/')
|
|
183
|
+
? filePath.slice(1)
|
|
184
|
+
: filePath;
|
|
185
|
+
const outputPath = resolve(clientBuildDir, ...normalizedPath.split('/'));
|
|
186
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
187
|
+
await writeFile(outputPath, contents);
|
|
188
|
+
api.logger.info(`Prerendered ${relative(process.cwd(), outputPath)}`);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const assertPrerenderableResponse = (
|
|
192
|
+
pathname: string,
|
|
193
|
+
response: Response
|
|
194
|
+
): void => {
|
|
195
|
+
if (
|
|
196
|
+
response.status === 200 ||
|
|
197
|
+
response.status === 202 ||
|
|
198
|
+
redirectStatusCodes.has(response.status) ||
|
|
199
|
+
(pathname === SPA_FALLBACK_REQUEST_PATH && response.status === 404)
|
|
200
|
+
) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Prerender: Received a ${response.status} status code from ` +
|
|
205
|
+
`the RSC server while prerendering the \`${pathname}\` path.`
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const prerenderRscUrlEffect = ({
|
|
210
|
+
api,
|
|
211
|
+
artifactPath,
|
|
212
|
+
clientBuildDir,
|
|
213
|
+
handler,
|
|
214
|
+
url,
|
|
215
|
+
}: {
|
|
216
|
+
api: RscPrerenderBuildApi;
|
|
217
|
+
artifactPath?: string;
|
|
218
|
+
clientBuildDir: string;
|
|
219
|
+
handler: RscRequestHandler;
|
|
220
|
+
url: URL;
|
|
221
|
+
}): Effect.Effect<void, Error, never> =>
|
|
222
|
+
createBuildRequestEffect(url, undefined, async request => {
|
|
223
|
+
const response = await handler(request);
|
|
224
|
+
const pathname = url.pathname;
|
|
225
|
+
const outputPathname = artifactPath ?? pathname;
|
|
226
|
+
assertPrerenderableResponse(pathname, response);
|
|
227
|
+
|
|
228
|
+
if (redirectStatusCodes.has(response.status)) {
|
|
229
|
+
const location = response.headers.get('Location') ?? '';
|
|
230
|
+
await writePrerenderedFile({
|
|
231
|
+
api,
|
|
232
|
+
clientBuildDir,
|
|
233
|
+
filePath: getRscHtmlFilePath(outputPathname),
|
|
234
|
+
contents: createRedirectHtml({
|
|
235
|
+
pathname,
|
|
236
|
+
location,
|
|
237
|
+
status: response.status,
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
return Effect.void;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const isHtml = Boolean(
|
|
244
|
+
response.headers.get('content-type')?.includes('text/html')
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (isHtml) {
|
|
248
|
+
const html = await response.text();
|
|
249
|
+
await writePrerenderedFile({
|
|
250
|
+
api,
|
|
251
|
+
clientBuildDir,
|
|
252
|
+
filePath: getRscHtmlFilePath(outputPathname),
|
|
253
|
+
contents: html,
|
|
254
|
+
});
|
|
255
|
+
const flightData = extractRscFlightData(html);
|
|
256
|
+
if (flightData !== null) {
|
|
257
|
+
await writePrerenderedFile({
|
|
258
|
+
api,
|
|
259
|
+
clientBuildDir,
|
|
260
|
+
filePath: getRscPayloadFilePath(outputPathname),
|
|
261
|
+
contents: flightData,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return Effect.void;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Non-HTML response (e.g. a resource route): emit the raw payload at the
|
|
268
|
+
// request path and also prerender its `.rsc` variant for client
|
|
269
|
+
// navigations, matching upstream RSC behavior.
|
|
270
|
+
await writePrerenderedFile({
|
|
271
|
+
api,
|
|
272
|
+
clientBuildDir,
|
|
273
|
+
filePath: outputPathname,
|
|
274
|
+
contents: new Uint8Array(await response.arrayBuffer()),
|
|
275
|
+
});
|
|
276
|
+
if (!pathname.endsWith('.rsc')) {
|
|
277
|
+
const dataUrl = new URL(url);
|
|
278
|
+
dataUrl.pathname += '.rsc';
|
|
279
|
+
return prerenderRscUrlEffect({
|
|
280
|
+
api,
|
|
281
|
+
artifactPath: `${outputPathname}.rsc`,
|
|
282
|
+
clientBuildDir,
|
|
283
|
+
handler,
|
|
284
|
+
url: dataUrl,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return Effect.void;
|
|
288
|
+
}).pipe(Effect.flatten);
|
|
289
|
+
|
|
290
|
+
export const runReactRouterRscPrerenderBuild = async (
|
|
291
|
+
options: RunReactRouterRscPrerenderBuildOptions
|
|
292
|
+
): Promise<void> => {
|
|
293
|
+
const {
|
|
294
|
+
api,
|
|
295
|
+
hasWebEnvironment,
|
|
296
|
+
buildDirectory,
|
|
297
|
+
serverBuildFile,
|
|
298
|
+
ssr,
|
|
299
|
+
prerenderConfig,
|
|
300
|
+
prerenderPaths,
|
|
301
|
+
basename,
|
|
302
|
+
} = options;
|
|
303
|
+
|
|
304
|
+
if (!hasWebEnvironment) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const prerenderRequests = getRscPrerenderRequests({
|
|
309
|
+
prerenderPaths,
|
|
310
|
+
ssr,
|
|
311
|
+
basename,
|
|
312
|
+
});
|
|
313
|
+
if (prerenderRequests.length === 0) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const serverBuildPath = resolve(
|
|
318
|
+
buildDirectory,
|
|
319
|
+
'server',
|
|
320
|
+
serverBuildFile || 'index.js'
|
|
321
|
+
);
|
|
322
|
+
if (!existsSync(serverBuildPath)) {
|
|
323
|
+
api.logger.warn(
|
|
324
|
+
`[${PLUGIN_NAME}] RSC server build not found at ${serverBuildPath}. ` +
|
|
325
|
+
'Skipping prerendering.'
|
|
326
|
+
);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const clientBuildDir = resolve(buildDirectory, 'client');
|
|
331
|
+
await mkdir(clientBuildDir, { recursive: true });
|
|
332
|
+
|
|
333
|
+
const previousBuildRequestFlag = process.env.IS_RR_BUILD_REQUEST;
|
|
334
|
+
process.env.IS_RR_BUILD_REQUEST = 'yes';
|
|
335
|
+
try {
|
|
336
|
+
const buildModule = await import(pathToFileURL(serverBuildPath).toString());
|
|
337
|
+
const handler = resolveRscRequestHandler(buildModule, serverBuildPath);
|
|
338
|
+
|
|
339
|
+
api.logger.info(`Prerender: ${prerenderRequests.length} path(s)...`);
|
|
340
|
+
|
|
341
|
+
await runPluginEffect(
|
|
342
|
+
createBoundedPrerenderTasksEffect(
|
|
343
|
+
prerenderRequests,
|
|
344
|
+
getPrerenderConcurrency(prerenderConfig),
|
|
345
|
+
prerenderRequest =>
|
|
346
|
+
prerenderRscUrlEffect({
|
|
347
|
+
api,
|
|
348
|
+
artifactPath: prerenderRequest.artifactPath,
|
|
349
|
+
clientBuildDir,
|
|
350
|
+
handler,
|
|
351
|
+
url: new URL(`http://localhost${prerenderRequest.requestPath}`),
|
|
352
|
+
})
|
|
353
|
+
)
|
|
354
|
+
);
|
|
355
|
+
} finally {
|
|
356
|
+
if (previousBuildRequestFlag === undefined) {
|
|
357
|
+
delete process.env.IS_RR_BUILD_REQUEST;
|
|
358
|
+
} else {
|
|
359
|
+
process.env.IS_RR_BUILD_REQUEST = previousBuildRequestFlag;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { resolve } from 'pathe';
|
|
2
|
+
import type { Route } from './types.js';
|
|
3
|
+
import {
|
|
4
|
+
describeRscRouteExportConflict,
|
|
5
|
+
RSC_ROUTE_COMPONENT_EXPORTS,
|
|
6
|
+
} from './rsc-route-exports.js';
|
|
7
|
+
|
|
8
|
+
const js = String.raw;
|
|
9
|
+
|
|
10
|
+
type RouteNode = Route & {
|
|
11
|
+
children?: RouteNode[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const createRouteTree = (routes: Record<string, Route>): RouteNode[] => {
|
|
15
|
+
const nodes = new Map<string, RouteNode>();
|
|
16
|
+
for (const route of Object.values(routes)) {
|
|
17
|
+
nodes.set(route.id, { ...route, children: [] });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const roots: RouteNode[] = [];
|
|
21
|
+
for (const route of nodes.values()) {
|
|
22
|
+
if (route.parentId) {
|
|
23
|
+
const parent = nodes.get(route.parentId);
|
|
24
|
+
if (parent) {
|
|
25
|
+
parent.children?.push(route);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
roots.push(route);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const normalizeChildren = (route: RouteNode): RouteNode => {
|
|
33
|
+
if (route.children?.length) {
|
|
34
|
+
route.children = route.children.map(normalizeChildren);
|
|
35
|
+
} else {
|
|
36
|
+
delete route.children;
|
|
37
|
+
}
|
|
38
|
+
return route;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return roots.map(normalizeChildren);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const appendRoute = (
|
|
45
|
+
code: string,
|
|
46
|
+
route: RouteNode,
|
|
47
|
+
appDirectory: string
|
|
48
|
+
): string => {
|
|
49
|
+
const routeFile = resolve(appDirectory, route.file);
|
|
50
|
+
code += '{';
|
|
51
|
+
code += `lazy: frameworkRoute(() => import(${JSON.stringify(routeFile)})),`;
|
|
52
|
+
code += `id: ${JSON.stringify(route.id)},`;
|
|
53
|
+
if (typeof route.path === 'string') {
|
|
54
|
+
code += `path: ${JSON.stringify(route.path)},`;
|
|
55
|
+
}
|
|
56
|
+
if (route.index) {
|
|
57
|
+
code += 'index: true,';
|
|
58
|
+
}
|
|
59
|
+
if (route.caseSensitive) {
|
|
60
|
+
code += 'caseSensitive: true,';
|
|
61
|
+
}
|
|
62
|
+
if (route.children?.length) {
|
|
63
|
+
code += 'children:[';
|
|
64
|
+
for (const child of route.children) {
|
|
65
|
+
code = appendRoute(code, child, appDirectory);
|
|
66
|
+
}
|
|
67
|
+
code += ']';
|
|
68
|
+
}
|
|
69
|
+
code += '},';
|
|
70
|
+
return code;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const createComponentResolutionCode = (): string =>
|
|
74
|
+
RSC_ROUTE_COMPONENT_EXPORTS.map(
|
|
75
|
+
({ routeProperty, clientExport, serverExport }) => {
|
|
76
|
+
const clientKey = JSON.stringify(clientExport);
|
|
77
|
+
const serverKey = JSON.stringify(serverExport);
|
|
78
|
+
const error = describeRscRouteExportConflict(clientExport, serverExport);
|
|
79
|
+
return js`
|
|
80
|
+
let ${routeProperty};
|
|
81
|
+
if (${clientKey} in mod && mod[${clientKey}]) {
|
|
82
|
+
if (${serverKey} in mod && mod[${serverKey}]) {
|
|
83
|
+
throw new Error(${JSON.stringify(error)});
|
|
84
|
+
}
|
|
85
|
+
${routeProperty} = mod[${clientKey}];
|
|
86
|
+
} else if (${serverKey} in mod && mod[${serverKey}]) {
|
|
87
|
+
${routeProperty} = mod[${serverKey}];
|
|
88
|
+
}`;
|
|
89
|
+
}
|
|
90
|
+
).join('\n');
|
|
91
|
+
|
|
92
|
+
export const createRscRouteConfig = ({
|
|
93
|
+
appDirectory,
|
|
94
|
+
routes,
|
|
95
|
+
}: {
|
|
96
|
+
appDirectory: string;
|
|
97
|
+
routes: Record<string, Route>;
|
|
98
|
+
}): string => {
|
|
99
|
+
const componentResolutionCode = createComponentResolutionCode();
|
|
100
|
+
let code = js`
|
|
101
|
+
function frameworkRoute(lazy) {
|
|
102
|
+
return async () => {
|
|
103
|
+
const mod = await lazy();
|
|
104
|
+
${componentResolutionCode}
|
|
105
|
+
|
|
106
|
+
const {
|
|
107
|
+
action,
|
|
108
|
+
clientAction,
|
|
109
|
+
clientLoader,
|
|
110
|
+
clientMiddleware,
|
|
111
|
+
handle,
|
|
112
|
+
headers,
|
|
113
|
+
links,
|
|
114
|
+
loader,
|
|
115
|
+
meta,
|
|
116
|
+
middleware,
|
|
117
|
+
shouldRevalidate,
|
|
118
|
+
} = mod;
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
Component,
|
|
122
|
+
ErrorBoundary,
|
|
123
|
+
HydrateFallback,
|
|
124
|
+
Layout,
|
|
125
|
+
action,
|
|
126
|
+
clientAction,
|
|
127
|
+
clientLoader,
|
|
128
|
+
clientMiddleware,
|
|
129
|
+
handle,
|
|
130
|
+
headers,
|
|
131
|
+
links,
|
|
132
|
+
loader,
|
|
133
|
+
meta,
|
|
134
|
+
middleware,
|
|
135
|
+
shouldRevalidate,
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export default [`;
|
|
140
|
+
|
|
141
|
+
for (const route of createRouteTree(routes)) {
|
|
142
|
+
code = appendRoute(code, route, appDirectory);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return `${code}];\n`;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const createRscInternalClientModule = (): string => js`
|
|
149
|
+
"use client";
|
|
150
|
+
|
|
151
|
+
export {
|
|
152
|
+
BrowserRouter,
|
|
153
|
+
Form,
|
|
154
|
+
HashRouter,
|
|
155
|
+
Link,
|
|
156
|
+
Links,
|
|
157
|
+
MemoryRouter,
|
|
158
|
+
Meta,
|
|
159
|
+
Navigate,
|
|
160
|
+
NavLink,
|
|
161
|
+
Outlet,
|
|
162
|
+
Route,
|
|
163
|
+
Router,
|
|
164
|
+
RouterProvider,
|
|
165
|
+
Routes,
|
|
166
|
+
ScrollRestoration,
|
|
167
|
+
StaticRouter,
|
|
168
|
+
StaticRouterProvider,
|
|
169
|
+
UNSAFE_AwaitContextProvider,
|
|
170
|
+
UNSAFE_WithComponentProps,
|
|
171
|
+
UNSAFE_WithErrorBoundaryProps,
|
|
172
|
+
UNSAFE_WithHydrateFallbackProps,
|
|
173
|
+
unstable_HistoryRouter,
|
|
174
|
+
} from "react-router";
|
|
175
|
+
`;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type RscRouteComponentExport = {
|
|
2
|
+
readonly routeProperty:
|
|
3
|
+
| 'Component'
|
|
4
|
+
| 'Layout'
|
|
5
|
+
| 'ErrorBoundary'
|
|
6
|
+
| 'HydrateFallback';
|
|
7
|
+
readonly clientExport:
|
|
8
|
+
| 'default'
|
|
9
|
+
| 'Layout'
|
|
10
|
+
| 'ErrorBoundary'
|
|
11
|
+
| 'HydrateFallback';
|
|
12
|
+
readonly serverExport:
|
|
13
|
+
| 'ServerComponent'
|
|
14
|
+
| 'ServerLayout'
|
|
15
|
+
| 'ServerErrorBoundary'
|
|
16
|
+
| 'ServerHydrateFallback';
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type RscClientComponentExport = RscRouteComponentExport['clientExport'];
|
|
20
|
+
export type RscServerComponentExport = RscRouteComponentExport['serverExport'];
|
|
21
|
+
|
|
22
|
+
export const RSC_ROUTE_COMPONENT_EXPORTS: readonly RscRouteComponentExport[] = [
|
|
23
|
+
{
|
|
24
|
+
routeProperty: 'Component',
|
|
25
|
+
clientExport: 'default',
|
|
26
|
+
serverExport: 'ServerComponent',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
routeProperty: 'Layout',
|
|
30
|
+
clientExport: 'Layout',
|
|
31
|
+
serverExport: 'ServerLayout',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
routeProperty: 'ErrorBoundary',
|
|
35
|
+
clientExport: 'ErrorBoundary',
|
|
36
|
+
serverExport: 'ServerErrorBoundary',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
routeProperty: 'HydrateFallback',
|
|
40
|
+
clientExport: 'HydrateFallback',
|
|
41
|
+
serverExport: 'ServerHydrateFallback',
|
|
42
|
+
},
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
export const RSC_MUTUALLY_EXCLUSIVE_ROUTE_EXPORTS: readonly (readonly [
|
|
46
|
+
RscClientComponentExport,
|
|
47
|
+
RscServerComponentExport,
|
|
48
|
+
])[] = RSC_ROUTE_COMPONENT_EXPORTS.map(
|
|
49
|
+
({ clientExport, serverExport }) => [clientExport, serverExport] as const
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export const RSC_CLIENT_COMPONENT_EXPORTS: readonly RscClientComponentExport[] =
|
|
53
|
+
RSC_ROUTE_COMPONENT_EXPORTS.map(({ clientExport }) => clientExport);
|
|
54
|
+
|
|
55
|
+
export const RSC_SERVER_COMPONENT_EXPORTS: readonly RscServerComponentExport[] =
|
|
56
|
+
RSC_ROUTE_COMPONENT_EXPORTS.map(({ serverExport }) => serverExport);
|
|
57
|
+
|
|
58
|
+
export const describeRscRouteExportConflict = (
|
|
59
|
+
clientExport: string,
|
|
60
|
+
serverExport: string
|
|
61
|
+
): string => {
|
|
62
|
+
const clientDescription =
|
|
63
|
+
clientExport === 'default'
|
|
64
|
+
? 'a default export'
|
|
65
|
+
: `a ${clientExport} export`;
|
|
66
|
+
return `Module cannot have both ${clientDescription} and a ${serverExport} export`;
|
|
67
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
type LoaderCallback = (
|
|
2
|
+
error: Error | null,
|
|
3
|
+
code?: string | Buffer,
|
|
4
|
+
map?: unknown
|
|
5
|
+
) => void;
|
|
6
|
+
|
|
7
|
+
type TransformArgs = {
|
|
8
|
+
code: string;
|
|
9
|
+
resource: string;
|
|
10
|
+
resourcePath: string;
|
|
11
|
+
resourceQuery?: string;
|
|
12
|
+
environment: { name: string };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type TransformResult = {
|
|
16
|
+
code: string;
|
|
17
|
+
map?: unknown;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type ReactRouterRscTransform = (
|
|
21
|
+
args: TransformArgs
|
|
22
|
+
) => Promise<TransformResult>;
|
|
23
|
+
|
|
24
|
+
type LoaderCompiler = {
|
|
25
|
+
__reactRouterRscRouteTransform?: ReactRouterRscTransform;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type RscRouteTransformLoaderContext = {
|
|
29
|
+
_compiler?: LoaderCompiler;
|
|
30
|
+
async(): LoaderCallback;
|
|
31
|
+
getOptions(): {
|
|
32
|
+
environmentName: string;
|
|
33
|
+
};
|
|
34
|
+
resource: string;
|
|
35
|
+
resourcePath: string;
|
|
36
|
+
resourceQuery?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default function rscRouteTransformLoader(
|
|
40
|
+
this: RscRouteTransformLoaderContext,
|
|
41
|
+
source: string | Buffer,
|
|
42
|
+
map: unknown
|
|
43
|
+
): void {
|
|
44
|
+
const callback = this.async();
|
|
45
|
+
const transform = this._compiler?.__reactRouterRscRouteTransform;
|
|
46
|
+
|
|
47
|
+
if (!transform) {
|
|
48
|
+
callback(null, source, map);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
transform({
|
|
53
|
+
code: source.toString(),
|
|
54
|
+
resource: this.resource,
|
|
55
|
+
resourcePath: this.resourcePath,
|
|
56
|
+
resourceQuery: this.resourceQuery,
|
|
57
|
+
environment: { name: this.getOptions().environmentName },
|
|
58
|
+
})
|
|
59
|
+
.then(result => {
|
|
60
|
+
callback(null, result.code, result.map ?? map);
|
|
61
|
+
})
|
|
62
|
+
.catch(error => {
|
|
63
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
64
|
+
});
|
|
65
|
+
}
|