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.
Files changed (126) hide show
  1. package/README.md +72 -240
  2. package/dist/{451.js → 511.js} +461 -331
  3. package/dist/build-output-transforms.d.ts +32 -7
  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 -3
  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/federation.d.ts +2 -0
  17. package/dist/index.cjs +3675 -2107
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.js +3614 -2214
  20. package/dist/lazy-compilation-prewarm.d.ts +8 -5
  21. package/dist/manifest.d.ts +16 -4
  22. package/dist/mode-plan.d.ts +82 -0
  23. package/dist/modify-browser-manifest.d.ts +8 -4
  24. package/dist/performance.d.ts +4 -1
  25. package/dist/plugin-utils.d.ts +27 -1
  26. package/dist/prerender-build.d.ts +5 -5
  27. package/dist/prerender.d.ts +1 -5
  28. package/dist/react-router-config.d.ts +11 -3
  29. package/dist/route-artifacts.d.ts +4 -2
  30. package/dist/route-chunks.d.ts +2 -1
  31. package/dist/route-component-transform.d.ts +0 -2
  32. package/dist/route-export-pruning.d.ts +4 -1
  33. package/dist/route-imports.d.ts +18 -0
  34. package/dist/route-module-transform-loader.d.ts +14 -0
  35. package/dist/route-module-transform-loader.js +44 -0
  36. package/dist/route-module-transform-rules.d.ts +14 -0
  37. package/dist/route-transform-tasks.d.ts +6 -0
  38. package/dist/route-watch.d.ts +11 -6
  39. package/dist/rsc-dev-server.d.ts +26 -0
  40. package/dist/rsc-prerender.d.ts +54 -0
  41. package/dist/rsc-route-config.d.ts +6 -0
  42. package/dist/rsc-route-exports.d.ts +15 -0
  43. package/dist/rsc-route-transform-loader.cjs +43 -0
  44. package/dist/rsc-route-transform-loader.d.ts +30 -0
  45. package/dist/rsc-route-transform-loader.js +16 -0
  46. package/dist/rsc-route-transform-registration.d.ts +12 -0
  47. package/dist/rsc-route-transforms.d.ts +21 -0
  48. package/dist/rsc-support.d.ts +23 -0
  49. package/dist/rsc-virtual-modules.d.ts +19 -0
  50. package/dist/server-build-plan.d.ts +2 -1
  51. package/dist/server-build-resolution.d.ts +1 -2
  52. package/dist/server-utils.d.ts +4 -5
  53. package/dist/ssr-asset-relocation.d.ts +98 -0
  54. package/dist/templates/entry.rsc.client.d.ts +1 -0
  55. package/dist/templates/entry.rsc.client.js +61 -0
  56. package/dist/templates/entry.rsc.d.ts +9 -0
  57. package/dist/templates/entry.rsc.js +38 -0
  58. package/dist/templates/entry.rsc.ssr.d.ts +4 -0
  59. package/dist/templates/entry.rsc.ssr.js +24 -0
  60. package/dist/typegen.d.ts +4 -2
  61. package/dist/types.d.ts +34 -6
  62. package/package.json +76 -22
  63. package/src/build-output-transforms.ts +181 -43
  64. package/src/classic-mode.ts +253 -0
  65. package/src/config-imports.ts +153 -6
  66. package/src/constants.ts +6 -2
  67. package/src/dev-background-resources.ts +46 -100
  68. package/src/dev-generation.ts +52 -33
  69. package/src/dev-hmr.ts +112 -80
  70. package/src/dev-runtime-artifacts.ts +11 -12
  71. package/src/dev-runtime-controller.ts +75 -85
  72. package/src/dev-runtime-session.ts +14 -18
  73. package/src/dev-server.ts +2 -0
  74. package/src/dev-source-maps.ts +257 -0
  75. package/src/effect-runtime.ts +105 -57
  76. package/src/entry-paths.ts +80 -0
  77. package/src/environment-output.ts +55 -0
  78. package/src/export-utils.ts +15 -11
  79. package/src/federation.ts +38 -0
  80. package/src/index.ts +666 -519
  81. package/src/lazy-compilation-prewarm.ts +20 -4
  82. package/src/manifest.ts +157 -82
  83. package/src/mode-plan.ts +367 -0
  84. package/src/modify-browser-manifest.ts +130 -124
  85. package/src/performance.ts +11 -2
  86. package/src/plugin-utils.ts +103 -39
  87. package/src/prerender-build.ts +82 -104
  88. package/src/prerender.ts +23 -24
  89. package/src/react-router-config.ts +72 -26
  90. package/src/route-artifacts.ts +96 -66
  91. package/src/route-chunks.ts +167 -51
  92. package/src/route-component-transform.ts +14 -21
  93. package/src/route-export-pruning.ts +4 -3
  94. package/src/route-imports.ts +100 -0
  95. package/src/route-module-transform-loader.ts +149 -0
  96. package/src/route-module-transform-rules.ts +115 -0
  97. package/src/route-transform-tasks.ts +44 -29
  98. package/src/route-watch.ts +166 -188
  99. package/src/rsc-dev-server.ts +112 -0
  100. package/src/rsc-prerender.ts +362 -0
  101. package/src/rsc-route-config.ts +175 -0
  102. package/src/rsc-route-exports.ts +67 -0
  103. package/src/rsc-route-transform-loader.ts +65 -0
  104. package/src/rsc-route-transform-registration.ts +145 -0
  105. package/src/rsc-route-transforms.ts +995 -0
  106. package/src/rsc-runtime.d.ts +143 -0
  107. package/src/rsc-support.ts +116 -0
  108. package/src/rsc-virtual-modules.ts +113 -0
  109. package/src/server-build-plan.ts +14 -3
  110. package/src/server-build-resolution.ts +37 -47
  111. package/src/server-utils.ts +21 -35
  112. package/src/ssr-asset-relocation.ts +183 -0
  113. package/src/ssr-externals.ts +8 -26
  114. package/src/templates/entry.rsc.client.tsx +168 -0
  115. package/src/templates/entry.rsc.ssr.tsx +45 -0
  116. package/src/templates/entry.rsc.tsx +80 -0
  117. package/src/typegen.ts +40 -23
  118. package/src/types.ts +43 -6
  119. package/src/warnings/warn-on-client-source-maps.ts +6 -10
  120. package/dist/parallel-route-transform-protocol.d.ts +0 -25
  121. package/dist/parallel-route-transform-worker.d.ts +0 -1
  122. package/dist/parallel-route-transform-worker.js +0 -38
  123. package/dist/parallel-route-transforms.d.ts +0 -29
  124. package/src/parallel-route-transform-protocol.ts +0 -35
  125. package/src/parallel-route-transform-worker.ts +0 -82
  126. 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
+ }