rsbuild-plugin-react-router 0.2.0 → 0.3.1
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 +213 -201
- package/dist/451.js +1103 -0
- package/dist/bounded-cache.d.ts +1 -0
- package/dist/build-manifest.d.ts +7 -4
- package/dist/build-output-transforms.d.ts +30 -0
- package/dist/concurrency.d.ts +3 -0
- package/dist/config-imports.d.ts +10 -0
- package/dist/constants.d.ts +3 -0
- package/dist/dev-background-resources.d.ts +38 -0
- package/dist/dev-generation.d.ts +25 -0
- package/dist/dev-runtime-artifacts.d.ts +47 -0
- package/dist/dev-runtime-compilation.d.ts +47 -0
- package/dist/dev-runtime-controller.d.ts +14 -0
- package/dist/dev-runtime-session.d.ts +34 -0
- package/dist/dev-server.d.ts +16 -2
- package/dist/effect-runtime.d.ts +18 -0
- package/dist/export-utils.d.ts +15 -7
- package/dist/index.cjs +13775 -1412
- package/dist/index.d.ts +8 -1
- package/dist/index.js +9429 -1501
- package/dist/lazy-compilation-prewarm.d.ts +25 -0
- package/dist/lazy-compilation.d.ts +6 -0
- package/dist/manifest.d.ts +48 -10
- package/dist/modify-browser-manifest.d.ts +30 -13
- package/dist/parallel-route-transform-protocol.d.ts +25 -0
- package/dist/parallel-route-transform-worker.d.ts +1 -0
- package/dist/parallel-route-transform-worker.js +38 -0
- package/dist/parallel-route-transforms.d.ts +29 -0
- package/dist/performance.d.ts +26 -0
- package/dist/plugin-utils.d.ts +2 -12
- package/dist/prerender-build.d.ts +36 -0
- package/dist/prerender.d.ts +27 -2
- package/dist/react-router-config.d.ts +15 -5
- package/dist/route-artifacts.d.ts +33 -0
- package/dist/route-ast.d.ts +21 -0
- package/dist/route-chunks.d.ts +9 -1
- package/dist/route-component-transform.d.ts +5 -0
- package/dist/route-export-pruning.d.ts +6 -0
- package/dist/route-export-resolution.d.ts +5 -0
- package/dist/route-transform-tasks.d.ts +47 -0
- package/dist/route-watch.d.ts +27 -0
- package/dist/server-build-plan.d.ts +22 -0
- package/dist/server-build-resolution.d.ts +3 -0
- package/dist/server-utils.d.ts +3 -2
- package/dist/ssr-externals.d.ts +1 -0
- package/dist/templates/entry.server.cjs +3 -3
- package/dist/templates/entry.server.js +3 -3
- package/dist/typegen.d.ts +15 -0
- package/dist/types.d.ts +41 -14
- package/dist/virtual-modules.d.ts +2 -0
- package/dist/warnings/warn-on-client-source-maps.d.ts +1 -0
- package/dist/yuku.d.ts +15 -0
- package/package.json +24 -19
- package/src/bounded-cache.ts +18 -0
- package/src/build-manifest.ts +205 -0
- package/src/build-output-transforms.ts +273 -0
- package/src/concurrency.ts +15 -0
- package/src/config-imports.ts +83 -0
- package/src/constants.ts +91 -0
- package/src/dev-background-resources.ts +255 -0
- package/src/dev-generation.ts +690 -0
- package/src/dev-runtime-artifacts.ts +239 -0
- package/src/dev-runtime-compilation.ts +171 -0
- package/src/dev-runtime-controller.ts +542 -0
- package/src/dev-runtime-session.ts +191 -0
- package/src/dev-server.ts +88 -0
- package/src/effect-runtime.ts +130 -0
- package/src/export-utils.ts +278 -0
- package/src/index.ts +990 -0
- package/src/lazy-compilation-prewarm.ts +279 -0
- package/src/lazy-compilation.ts +101 -0
- package/src/manifest.ts +591 -0
- package/src/modify-browser-manifest.ts +246 -0
- package/src/parallel-route-transform-protocol.ts +35 -0
- package/src/parallel-route-transform-worker.ts +82 -0
- package/src/parallel-route-transforms.ts +458 -0
- package/src/performance.ts +254 -0
- package/src/plugin-utils.ts +82 -0
- package/src/prerender-build.ts +687 -0
- package/src/prerender.ts +349 -0
- package/src/react-router-config.ts +245 -0
- package/src/route-artifacts.ts +155 -0
- package/src/route-ast.ts +163 -0
- package/src/route-chunks.ts +857 -0
- package/src/route-component-transform.ts +314 -0
- package/src/route-config.ts +106 -0
- package/src/route-export-pruning.ts +668 -0
- package/src/route-export-resolution.ts +329 -0
- package/src/route-transform-tasks.ts +249 -0
- package/src/route-watch.ts +357 -0
- package/src/server-build-plan.ts +91 -0
- package/src/server-build-resolution.ts +131 -0
- package/src/server-utils.ts +111 -0
- package/src/ssr-externals.ts +59 -0
- package/src/templates/context.ts +12 -0
- package/src/templates/entry.client.tsx +12 -0
- package/src/templates/entry.server.tsx +76 -0
- package/src/typegen.ts +178 -0
- package/src/types.ts +92 -0
- package/src/validation/validate-plugin-order.ts +76 -0
- package/src/virtual-modules.ts +30 -0
- package/src/warnings/warn-on-client-source-maps.ts +96 -0
- package/src/yuku.ts +67 -0
- package/dist/0~rslib-runtime.js +0 -16
- package/dist/babel.d.ts +0 -8
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import fsExtra from 'fs-extra';
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import type { RsbuildPluginAPI } from '@rsbuild/core';
|
|
7
|
+
import {
|
|
8
|
+
createRequestHandler,
|
|
9
|
+
matchRoutes,
|
|
10
|
+
type ServerBuild,
|
|
11
|
+
} from 'react-router';
|
|
12
|
+
import { dirname, relative, resolve } from 'pathe';
|
|
13
|
+
import { PLUGIN_NAME } from './constants.js';
|
|
14
|
+
import { getBuildManifest } from './build-manifest.js';
|
|
15
|
+
import {
|
|
16
|
+
generateReactRouterManifestForDev,
|
|
17
|
+
getReactRouterManifestForDev,
|
|
18
|
+
type ReactRouterManifestStats,
|
|
19
|
+
type RouteManifestModuleExports,
|
|
20
|
+
} from './manifest.js';
|
|
21
|
+
import {
|
|
22
|
+
createPrerenderRoutes,
|
|
23
|
+
getPrerenderConcurrency,
|
|
24
|
+
getSsrFalsePrerenderExportErrors,
|
|
25
|
+
normalizePrerenderMatchPath,
|
|
26
|
+
} from './prerender.js';
|
|
27
|
+
import type {
|
|
28
|
+
Config,
|
|
29
|
+
ResolvedReactRouterConfig,
|
|
30
|
+
} from './react-router-config.js';
|
|
31
|
+
import { resolveServerBuildModule } from './server-utils.js';
|
|
32
|
+
import type { PluginOptions, Route } from './types.js';
|
|
33
|
+
import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
|
|
34
|
+
|
|
35
|
+
type ReactRouterManifest = Awaited<
|
|
36
|
+
ReturnType<typeof getReactRouterManifestForDev>
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
type BuildRouteModule = {
|
|
40
|
+
loader?: unknown;
|
|
41
|
+
default?: unknown;
|
|
42
|
+
ErrorBoundary?: unknown;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type PrerenderServerBuild = ServerBuild & {
|
|
46
|
+
routes: Record<string, { id?: string; module?: BuildRouteModule }>;
|
|
47
|
+
assets?: {
|
|
48
|
+
routes?: Record<string, { hasLoader?: boolean }>;
|
|
49
|
+
};
|
|
50
|
+
prerender?: string[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type PrerenderBuildApi = Pick<
|
|
54
|
+
RsbuildPluginAPI,
|
|
55
|
+
'logger' | 'getNormalizedConfig'
|
|
56
|
+
>;
|
|
57
|
+
|
|
58
|
+
type RunReactRouterPrerenderBuildOptions = {
|
|
59
|
+
api: PrerenderBuildApi;
|
|
60
|
+
hasWebEnvironment: boolean;
|
|
61
|
+
buildDirectory: string;
|
|
62
|
+
serverBuildFile?: string;
|
|
63
|
+
ssr: boolean;
|
|
64
|
+
isPrerenderEnabled: boolean;
|
|
65
|
+
prerenderConfig: Config['prerender'];
|
|
66
|
+
prerenderPaths: string[];
|
|
67
|
+
basename: string;
|
|
68
|
+
future: ResolvedReactRouterConfig['future'];
|
|
69
|
+
routes: Record<string, Route>;
|
|
70
|
+
latestBrowserManifest: ReactRouterManifest | null;
|
|
71
|
+
latestBrowserManifestModuleExports: RouteManifestModuleExports;
|
|
72
|
+
clientStats: ReactRouterManifestStats | undefined;
|
|
73
|
+
pluginOptions: PluginOptions;
|
|
74
|
+
appDirectory: string;
|
|
75
|
+
assetPrefix: string;
|
|
76
|
+
routeChunkOptions: Parameters<typeof getReactRouterManifestForDev>[5];
|
|
77
|
+
buildManifest: Awaited<ReturnType<typeof getBuildManifest>>;
|
|
78
|
+
resolvedConfigWithRoutes: ResolvedReactRouterConfig;
|
|
79
|
+
buildEnd: Config['buildEnd'];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
|
|
83
|
+
|
|
84
|
+
const getServerBuildPath = async ({
|
|
85
|
+
buildDirectory,
|
|
86
|
+
serverBuildFile,
|
|
87
|
+
}: {
|
|
88
|
+
buildDirectory: string;
|
|
89
|
+
serverBuildFile?: string;
|
|
90
|
+
}): Promise<{ serverBuildDir: string; serverBuildPath: string }> => {
|
|
91
|
+
const serverBuildDir = resolve(buildDirectory, 'server');
|
|
92
|
+
const defaultServerBuildFile = 'static/js/app.js';
|
|
93
|
+
const configuredServerBuildFile = serverBuildFile || 'index.js';
|
|
94
|
+
const configuredServerBuildPath = resolve(
|
|
95
|
+
serverBuildDir,
|
|
96
|
+
configuredServerBuildFile
|
|
97
|
+
);
|
|
98
|
+
const defaultServerBuildPath = resolve(
|
|
99
|
+
serverBuildDir,
|
|
100
|
+
defaultServerBuildFile
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
configuredServerBuildFile !== defaultServerBuildFile &&
|
|
105
|
+
existsSync(defaultServerBuildPath) &&
|
|
106
|
+
!existsSync(configuredServerBuildPath)
|
|
107
|
+
) {
|
|
108
|
+
await mkdir(dirname(configuredServerBuildPath), { recursive: true });
|
|
109
|
+
await fsExtra.copy(defaultServerBuildPath, configuredServerBuildPath);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
serverBuildDir,
|
|
114
|
+
serverBuildPath: existsSync(configuredServerBuildPath)
|
|
115
|
+
? configuredServerBuildPath
|
|
116
|
+
: defaultServerBuildPath,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const createDataRequestPath = (
|
|
121
|
+
prerenderPath: string,
|
|
122
|
+
trailingSlashAwareDataRequests: boolean
|
|
123
|
+
): string => {
|
|
124
|
+
if (trailingSlashAwareDataRequests) {
|
|
125
|
+
return prerenderPath.endsWith('/')
|
|
126
|
+
? `${prerenderPath}_.data`
|
|
127
|
+
: `${prerenderPath}.data`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return prerenderPath === '/'
|
|
131
|
+
? '/_root.data'
|
|
132
|
+
: `${prerenderPath.replace(/\/$/, '')}.data`;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const createBuildRequestEffect = <T>(
|
|
136
|
+
input: string | URL,
|
|
137
|
+
init: RequestInit | undefined,
|
|
138
|
+
handle: (request: Request) => Promise<T>
|
|
139
|
+
): Effect.Effect<T, Error, never> =>
|
|
140
|
+
Effect.acquireUseRelease(
|
|
141
|
+
Effect.sync(() => new AbortController()),
|
|
142
|
+
controller =>
|
|
143
|
+
tryPluginPromise(() =>
|
|
144
|
+
handle(
|
|
145
|
+
new Request(input, {
|
|
146
|
+
...init,
|
|
147
|
+
signal: controller.signal,
|
|
148
|
+
})
|
|
149
|
+
)
|
|
150
|
+
),
|
|
151
|
+
controller =>
|
|
152
|
+
Effect.sync(() => {
|
|
153
|
+
controller.abort();
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
export const withBuildRequest = <T>(
|
|
158
|
+
input: string | URL,
|
|
159
|
+
init: RequestInit | undefined,
|
|
160
|
+
handle: (request: Request) => Promise<T>
|
|
161
|
+
): Promise<T> => runPluginEffect(createBuildRequestEffect(input, init, handle));
|
|
162
|
+
|
|
163
|
+
const prerenderData = async ({
|
|
164
|
+
handler,
|
|
165
|
+
prerenderPath,
|
|
166
|
+
onlyRoutes,
|
|
167
|
+
clientBuildDir,
|
|
168
|
+
basename,
|
|
169
|
+
trailingSlashAwareDataRequests,
|
|
170
|
+
api,
|
|
171
|
+
requestInit,
|
|
172
|
+
}: {
|
|
173
|
+
handler: (request: Request) => Promise<Response>;
|
|
174
|
+
prerenderPath: string;
|
|
175
|
+
onlyRoutes: string[] | null;
|
|
176
|
+
clientBuildDir: string;
|
|
177
|
+
basename: string;
|
|
178
|
+
trailingSlashAwareDataRequests: boolean;
|
|
179
|
+
api: PrerenderBuildApi;
|
|
180
|
+
requestInit?: RequestInit;
|
|
181
|
+
}): Promise<string> => {
|
|
182
|
+
const dataRequestPath = createDataRequestPath(
|
|
183
|
+
prerenderPath,
|
|
184
|
+
trailingSlashAwareDataRequests
|
|
185
|
+
);
|
|
186
|
+
const normalizedPath = `${basename}${dataRequestPath}`.replace(/\/\/+/g, '/');
|
|
187
|
+
const url = new URL(`http://localhost${normalizedPath}`);
|
|
188
|
+
if (onlyRoutes?.length) {
|
|
189
|
+
url.searchParams.set('_routes', onlyRoutes.join(','));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return withBuildRequest(url, requestInit, async request => {
|
|
193
|
+
const response = await handler(request);
|
|
194
|
+
const data = await response.text();
|
|
195
|
+
|
|
196
|
+
if (response.status !== 200 && response.status !== 202) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Prerender (data): Received a ${response.status} status code from ` +
|
|
199
|
+
`\`entry.server.tsx\` while prerendering the \`${prerenderPath}\` path.\n` +
|
|
200
|
+
`${normalizedPath}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const outputPath = resolve(clientBuildDir, ...normalizedPath.split('/'));
|
|
205
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
206
|
+
await writeFile(outputPath, data);
|
|
207
|
+
api.logger.info(
|
|
208
|
+
`Prerender (data): ${prerenderPath} -> ${relative(
|
|
209
|
+
process.cwd(),
|
|
210
|
+
outputPath
|
|
211
|
+
)}`
|
|
212
|
+
);
|
|
213
|
+
return data;
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const prerenderRoute = async ({
|
|
218
|
+
handler,
|
|
219
|
+
prerenderPath,
|
|
220
|
+
clientBuildDir,
|
|
221
|
+
basename,
|
|
222
|
+
api,
|
|
223
|
+
requestInit,
|
|
224
|
+
}: {
|
|
225
|
+
handler: (request: Request) => Promise<Response>;
|
|
226
|
+
prerenderPath: string;
|
|
227
|
+
clientBuildDir: string;
|
|
228
|
+
basename: string;
|
|
229
|
+
api: PrerenderBuildApi;
|
|
230
|
+
requestInit?: RequestInit;
|
|
231
|
+
}): Promise<void> => {
|
|
232
|
+
const normalizedPath = `${basename}${prerenderPath}/`.replace(/\/\/+/g, '/');
|
|
233
|
+
await withBuildRequest(
|
|
234
|
+
`http://localhost${normalizedPath}`,
|
|
235
|
+
requestInit,
|
|
236
|
+
async request => {
|
|
237
|
+
const response = await handler(request);
|
|
238
|
+
let html = await response.text();
|
|
239
|
+
|
|
240
|
+
if (redirectStatusCodes.has(response.status)) {
|
|
241
|
+
const location = response.headers.get('Location');
|
|
242
|
+
const delay = response.status === 302 ? 2 : 0;
|
|
243
|
+
html = `<!doctype html>
|
|
244
|
+
<head>
|
|
245
|
+
<title>Redirecting to: ${location}</title>
|
|
246
|
+
<meta http-equiv="refresh" content="${delay};url=${location}">
|
|
247
|
+
<meta name="robots" content="noindex">
|
|
248
|
+
</head>
|
|
249
|
+
<body>
|
|
250
|
+
\t<a href="${location}">
|
|
251
|
+
Redirecting from <code>${normalizedPath}</code> to <code>${location}</code>
|
|
252
|
+
</a>
|
|
253
|
+
</body>
|
|
254
|
+
</html>`;
|
|
255
|
+
} else if (response.status !== 200) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Prerender (html): Received a ${response.status} status code from ` +
|
|
258
|
+
`\`entry.server.tsx\` while prerendering the \`${normalizedPath}\` path.\n` +
|
|
259
|
+
html
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const outputPath = resolve(
|
|
264
|
+
clientBuildDir,
|
|
265
|
+
...normalizedPath.split('/'),
|
|
266
|
+
'index.html'
|
|
267
|
+
);
|
|
268
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
269
|
+
await writeFile(outputPath, html);
|
|
270
|
+
api.logger.info(
|
|
271
|
+
`Prerender (html): ${prerenderPath} -> ${relative(
|
|
272
|
+
process.cwd(),
|
|
273
|
+
outputPath
|
|
274
|
+
)}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const prerenderResourceRoute = async ({
|
|
281
|
+
handler,
|
|
282
|
+
prerenderPath,
|
|
283
|
+
clientBuildDir,
|
|
284
|
+
basename,
|
|
285
|
+
api,
|
|
286
|
+
requestInit,
|
|
287
|
+
}: {
|
|
288
|
+
handler: (request: Request) => Promise<Response>;
|
|
289
|
+
prerenderPath: string;
|
|
290
|
+
clientBuildDir: string;
|
|
291
|
+
basename: string;
|
|
292
|
+
api: PrerenderBuildApi;
|
|
293
|
+
requestInit?: RequestInit;
|
|
294
|
+
}): Promise<void> => {
|
|
295
|
+
const normalizedPath = `${basename}${prerenderPath}/`
|
|
296
|
+
.replace(/\/\/+/g, '/')
|
|
297
|
+
.replace(/\/$/g, '');
|
|
298
|
+
await withBuildRequest(
|
|
299
|
+
`http://localhost${normalizedPath}`,
|
|
300
|
+
requestInit,
|
|
301
|
+
async request => {
|
|
302
|
+
const response = await handler(request);
|
|
303
|
+
const content = Buffer.from(await response.arrayBuffer());
|
|
304
|
+
|
|
305
|
+
if (response.status !== 200) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Prerender (resource): Received a ${response.status} status code from ` +
|
|
308
|
+
`\`entry.server.tsx\` while prerendering the \`${normalizedPath}\` path.\n` +
|
|
309
|
+
content.toString('utf8')
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const outputPath = resolve(clientBuildDir, ...normalizedPath.split('/'));
|
|
314
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
315
|
+
await writeFile(outputPath, content);
|
|
316
|
+
api.logger.info(
|
|
317
|
+
`Prerender (resource): ${prerenderPath} -> ${relative(
|
|
318
|
+
process.cwd(),
|
|
319
|
+
outputPath
|
|
320
|
+
)}`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const handleSpaMode = async ({
|
|
327
|
+
handler,
|
|
328
|
+
build,
|
|
329
|
+
clientBuildDir,
|
|
330
|
+
basename,
|
|
331
|
+
api,
|
|
332
|
+
}: {
|
|
333
|
+
handler: (request: Request) => Promise<Response>;
|
|
334
|
+
build: PrerenderServerBuild;
|
|
335
|
+
clientBuildDir: string;
|
|
336
|
+
basename: string;
|
|
337
|
+
api: PrerenderBuildApi;
|
|
338
|
+
}): Promise<void> => {
|
|
339
|
+
await withBuildRequest(
|
|
340
|
+
`http://localhost${basename}`,
|
|
341
|
+
{
|
|
342
|
+
headers: {
|
|
343
|
+
'X-React-Router-SPA-Mode': 'yes',
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
async request => {
|
|
347
|
+
const response = await handler(request);
|
|
348
|
+
const html = await response.text();
|
|
349
|
+
const isPrerenderSpaFallback = build.prerender?.includes('/');
|
|
350
|
+
const filename = isPrerenderSpaFallback
|
|
351
|
+
? '__spa-fallback.html'
|
|
352
|
+
: 'index.html';
|
|
353
|
+
|
|
354
|
+
if (response.status !== 200) {
|
|
355
|
+
if (isPrerenderSpaFallback) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
`Prerender: Received a ${response.status} status code from ` +
|
|
358
|
+
`\`entry.server.tsx\` while prerendering your \`${filename}\` file.\n` +
|
|
359
|
+
html
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
throw new Error(
|
|
363
|
+
`SPA Mode: Received a ${response.status} status code from ` +
|
|
364
|
+
`\`entry.server.tsx\` while prerendering your \`${filename}\` file.\n` +
|
|
365
|
+
html
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (
|
|
370
|
+
!html.includes('window.__reactRouterContext =') ||
|
|
371
|
+
!html.includes('window.__reactRouterRouteModules =')
|
|
372
|
+
) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
'SPA Mode: Did you forget to include `<Scripts/>` in your root route? ' +
|
|
375
|
+
'Your pre-rendered HTML cannot hydrate without `<Scripts />`.'
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const outputPath = resolve(clientBuildDir, filename);
|
|
380
|
+
await writeFile(outputPath, html);
|
|
381
|
+
const prettyPath = relative(process.cwd(), outputPath);
|
|
382
|
+
if (build.prerender?.length) {
|
|
383
|
+
api.logger.info(`Prerender (html): SPA Fallback -> ${prettyPath}`);
|
|
384
|
+
} else {
|
|
385
|
+
api.logger.info(`SPA Mode: Generated ${prettyPath}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const assertValidSsrFalsePrerenderExports = ({
|
|
392
|
+
routes,
|
|
393
|
+
manifestRoutes,
|
|
394
|
+
routeExports,
|
|
395
|
+
prerenderPaths,
|
|
396
|
+
api,
|
|
397
|
+
}: {
|
|
398
|
+
routes: Record<string, Route>;
|
|
399
|
+
manifestRoutes: ReactRouterManifest['routes'];
|
|
400
|
+
routeExports: RouteManifestModuleExports;
|
|
401
|
+
prerenderPaths: string[];
|
|
402
|
+
api: PrerenderBuildApi;
|
|
403
|
+
}) => {
|
|
404
|
+
const errors = getSsrFalsePrerenderExportErrors({
|
|
405
|
+
routes,
|
|
406
|
+
manifestRoutes,
|
|
407
|
+
routeExports,
|
|
408
|
+
prerenderPaths,
|
|
409
|
+
});
|
|
410
|
+
if (errors.length > 0) {
|
|
411
|
+
api.logger.error(errors.join('\n'));
|
|
412
|
+
throw new Error(
|
|
413
|
+
'Invalid route exports found when prerendering with `ssr:false`'
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const validatePrerenderPathMatches = (
|
|
419
|
+
routes: Record<string, Route>,
|
|
420
|
+
prerenderPaths: string[]
|
|
421
|
+
): void => {
|
|
422
|
+
const routeTree = createPrerenderRoutes(routes);
|
|
423
|
+
for (const path of prerenderPaths) {
|
|
424
|
+
const matches = matchRoutes(routeTree, normalizePrerenderMatchPath(path));
|
|
425
|
+
if (!matches) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
`Unable to prerender path because it does not match any routes: ${path}`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
export const createBoundedPrerenderTasksEffect = (
|
|
434
|
+
prerenderPaths: string[],
|
|
435
|
+
concurrency: number,
|
|
436
|
+
renderPath: (path: string) => Effect.Effect<void, Error, never>
|
|
437
|
+
): Effect.Effect<void, Error, never> =>
|
|
438
|
+
Effect.forEach(prerenderPaths, renderPath, { concurrency, discard: true });
|
|
439
|
+
|
|
440
|
+
const createPrerenderPathEffect = ({
|
|
441
|
+
path,
|
|
442
|
+
build,
|
|
443
|
+
buildRoutes,
|
|
444
|
+
requestHandler,
|
|
445
|
+
clientBuildDir,
|
|
446
|
+
options,
|
|
447
|
+
}: {
|
|
448
|
+
path: string;
|
|
449
|
+
build: PrerenderServerBuild;
|
|
450
|
+
buildRoutes: ReturnType<typeof createPrerenderRoutes>;
|
|
451
|
+
requestHandler: (request: Request) => Promise<Response>;
|
|
452
|
+
clientBuildDir: string;
|
|
453
|
+
options: RunReactRouterPrerenderBuildOptions;
|
|
454
|
+
}): Effect.Effect<void, Error, never> =>
|
|
455
|
+
Effect.gen(function* () {
|
|
456
|
+
const { api, basename, future } = options;
|
|
457
|
+
const matches = matchRoutes(buildRoutes, normalizePrerenderMatchPath(path));
|
|
458
|
+
if (!matches) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const leafRoute = matches[matches.length - 1]?.route;
|
|
463
|
+
const routeId = leafRoute?.id;
|
|
464
|
+
const manifestRoute = routeId ? build.routes?.[routeId]?.module : null;
|
|
465
|
+
const isResourceRoute =
|
|
466
|
+
manifestRoute && !manifestRoute.default && !manifestRoute.ErrorBoundary;
|
|
467
|
+
|
|
468
|
+
if (isResourceRoute) {
|
|
469
|
+
if (manifestRoute.loader && routeId) {
|
|
470
|
+
yield* tryPluginPromise(() =>
|
|
471
|
+
prerenderData({
|
|
472
|
+
handler: requestHandler,
|
|
473
|
+
prerenderPath: path,
|
|
474
|
+
onlyRoutes: [routeId],
|
|
475
|
+
clientBuildDir,
|
|
476
|
+
basename,
|
|
477
|
+
trailingSlashAwareDataRequests:
|
|
478
|
+
future.unstable_trailingSlashAwareDataRequests,
|
|
479
|
+
api,
|
|
480
|
+
})
|
|
481
|
+
);
|
|
482
|
+
yield* tryPluginPromise(() =>
|
|
483
|
+
prerenderResourceRoute({
|
|
484
|
+
handler: requestHandler,
|
|
485
|
+
prerenderPath: path,
|
|
486
|
+
clientBuildDir,
|
|
487
|
+
basename,
|
|
488
|
+
api,
|
|
489
|
+
})
|
|
490
|
+
);
|
|
491
|
+
} else {
|
|
492
|
+
yield* Effect.sync(() => {
|
|
493
|
+
api.logger.warn(
|
|
494
|
+
`⚠️ Skipping prerendering for resource route without a loader: ${routeId}`
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const hasLoaders = matches.some(match => {
|
|
502
|
+
const matchedRouteId = match.route.id;
|
|
503
|
+
if (!matchedRouteId) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
return build.assets?.routes?.[matchedRouteId]?.hasLoader;
|
|
507
|
+
});
|
|
508
|
+
const data = hasLoaders
|
|
509
|
+
? yield* tryPluginPromise(() =>
|
|
510
|
+
prerenderData({
|
|
511
|
+
handler: requestHandler,
|
|
512
|
+
prerenderPath: path,
|
|
513
|
+
onlyRoutes: null,
|
|
514
|
+
clientBuildDir,
|
|
515
|
+
basename,
|
|
516
|
+
trailingSlashAwareDataRequests:
|
|
517
|
+
future.unstable_trailingSlashAwareDataRequests,
|
|
518
|
+
api,
|
|
519
|
+
})
|
|
520
|
+
)
|
|
521
|
+
: undefined;
|
|
522
|
+
|
|
523
|
+
yield* tryPluginPromise(() =>
|
|
524
|
+
prerenderRoute({
|
|
525
|
+
handler: requestHandler,
|
|
526
|
+
prerenderPath: path,
|
|
527
|
+
clientBuildDir,
|
|
528
|
+
basename,
|
|
529
|
+
api,
|
|
530
|
+
requestInit: data
|
|
531
|
+
? {
|
|
532
|
+
headers: {
|
|
533
|
+
'X-React-Router-Prerender-Data': encodeURI(data),
|
|
534
|
+
},
|
|
535
|
+
}
|
|
536
|
+
: undefined,
|
|
537
|
+
})
|
|
538
|
+
);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const runPrerenderPaths = async ({
|
|
542
|
+
build,
|
|
543
|
+
requestHandler,
|
|
544
|
+
clientBuildDir,
|
|
545
|
+
options,
|
|
546
|
+
}: {
|
|
547
|
+
build: PrerenderServerBuild;
|
|
548
|
+
requestHandler: (request: Request) => Promise<Response>;
|
|
549
|
+
clientBuildDir: string;
|
|
550
|
+
options: RunReactRouterPrerenderBuildOptions;
|
|
551
|
+
}): Promise<void> => {
|
|
552
|
+
const { prerenderConfig, prerenderPaths } = options;
|
|
553
|
+
const buildRoutes = createPrerenderRoutes(build.routes);
|
|
554
|
+
const concurrency = getPrerenderConcurrency(prerenderConfig);
|
|
555
|
+
|
|
556
|
+
await runPluginEffect(
|
|
557
|
+
createBoundedPrerenderTasksEffect(prerenderPaths, concurrency, path =>
|
|
558
|
+
createPrerenderPathEffect({
|
|
559
|
+
path,
|
|
560
|
+
build,
|
|
561
|
+
buildRoutes,
|
|
562
|
+
requestHandler,
|
|
563
|
+
clientBuildDir,
|
|
564
|
+
options,
|
|
565
|
+
})
|
|
566
|
+
)
|
|
567
|
+
);
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
export const runReactRouterPrerenderBuild = async (
|
|
571
|
+
options: RunReactRouterPrerenderBuildOptions
|
|
572
|
+
): Promise<void> => {
|
|
573
|
+
if (!options.hasWebEnvironment) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const {
|
|
578
|
+
api,
|
|
579
|
+
buildDirectory,
|
|
580
|
+
serverBuildFile,
|
|
581
|
+
ssr,
|
|
582
|
+
isPrerenderEnabled,
|
|
583
|
+
prerenderPaths,
|
|
584
|
+
routes,
|
|
585
|
+
latestBrowserManifest,
|
|
586
|
+
latestBrowserManifestModuleExports,
|
|
587
|
+
clientStats,
|
|
588
|
+
pluginOptions,
|
|
589
|
+
appDirectory,
|
|
590
|
+
assetPrefix,
|
|
591
|
+
routeChunkOptions,
|
|
592
|
+
buildManifest,
|
|
593
|
+
resolvedConfigWithRoutes,
|
|
594
|
+
buildEnd,
|
|
595
|
+
basename,
|
|
596
|
+
} = options;
|
|
597
|
+
const { serverBuildDir, serverBuildPath } = await getServerBuildPath({
|
|
598
|
+
buildDirectory,
|
|
599
|
+
serverBuildFile,
|
|
600
|
+
});
|
|
601
|
+
const clientBuildDir = resolve(buildDirectory, 'client');
|
|
602
|
+
|
|
603
|
+
if (!existsSync(serverBuildPath)) {
|
|
604
|
+
console.warn(
|
|
605
|
+
`[${PLUGIN_NAME}] Server build not found at ${serverBuildPath}. ` +
|
|
606
|
+
'Skipping prerendering.'
|
|
607
|
+
);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
await mkdir(clientBuildDir, { recursive: true });
|
|
612
|
+
|
|
613
|
+
if (!ssr || isPrerenderEnabled) {
|
|
614
|
+
process.env.IS_RR_BUILD_REQUEST = 'yes';
|
|
615
|
+
const buildModule = await import(pathToFileURL(serverBuildPath).toString());
|
|
616
|
+
const build = (await resolveServerBuildModule(
|
|
617
|
+
buildModule,
|
|
618
|
+
`Server build ${JSON.stringify(serverBuildPath)}`
|
|
619
|
+
)) as PrerenderServerBuild;
|
|
620
|
+
const requestHandler = createRequestHandler(build, 'production');
|
|
621
|
+
|
|
622
|
+
if (isPrerenderEnabled) {
|
|
623
|
+
if (!ssr) {
|
|
624
|
+
const generated = latestBrowserManifest
|
|
625
|
+
? {
|
|
626
|
+
manifest: latestBrowserManifest,
|
|
627
|
+
moduleExportsByRouteId: latestBrowserManifestModuleExports,
|
|
628
|
+
}
|
|
629
|
+
: await generateReactRouterManifestForDev(
|
|
630
|
+
routes,
|
|
631
|
+
pluginOptions,
|
|
632
|
+
clientStats,
|
|
633
|
+
appDirectory,
|
|
634
|
+
assetPrefix,
|
|
635
|
+
routeChunkOptions
|
|
636
|
+
);
|
|
637
|
+
assertValidSsrFalsePrerenderExports({
|
|
638
|
+
routes,
|
|
639
|
+
manifestRoutes: generated.manifest.routes,
|
|
640
|
+
routeExports: generated.moduleExportsByRouteId,
|
|
641
|
+
prerenderPaths,
|
|
642
|
+
api,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
validatePrerenderPathMatches(routes, prerenderPaths);
|
|
647
|
+
|
|
648
|
+
if (prerenderPaths.length > 0) {
|
|
649
|
+
api.logger.info(
|
|
650
|
+
`Prerender (html): ${prerenderPaths.length} path(s)...`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
await runPrerenderPaths({
|
|
655
|
+
build,
|
|
656
|
+
requestHandler,
|
|
657
|
+
clientBuildDir,
|
|
658
|
+
options,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (!ssr) {
|
|
663
|
+
await handleSpaMode({
|
|
664
|
+
handler: requestHandler,
|
|
665
|
+
build,
|
|
666
|
+
clientBuildDir,
|
|
667
|
+
basename,
|
|
668
|
+
api,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (!ssr) {
|
|
674
|
+
await fsExtra.remove(serverBuildDir);
|
|
675
|
+
api.logger.info(
|
|
676
|
+
`[${PLUGIN_NAME}] Removed server build (static deployment)`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (buildEnd) {
|
|
681
|
+
await buildEnd({
|
|
682
|
+
buildManifest,
|
|
683
|
+
reactRouterConfig: resolvedConfigWithRoutes,
|
|
684
|
+
viteConfig: api.getNormalizedConfig(),
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
};
|