rsbuild-plugin-react-router 0.1.1 → 0.3.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 (99) hide show
  1. package/README.md +185 -204
  2. package/dist/451.js +1087 -0
  3. package/dist/bounded-cache.d.ts +1 -0
  4. package/dist/build-manifest.d.ts +1 -1
  5. package/dist/build-output-transforms.d.ts +30 -0
  6. package/dist/concurrency.d.ts +2 -0
  7. package/dist/config-imports.d.ts +3 -0
  8. package/dist/constants.d.ts +3 -0
  9. package/dist/dev-generation.d.ts +25 -0
  10. package/dist/dev-runtime-artifacts.d.ts +39 -0
  11. package/dist/dev-runtime-compilation.d.ts +32 -0
  12. package/dist/dev-runtime-controller.d.ts +14 -0
  13. package/dist/dev-runtime-session.d.ts +34 -0
  14. package/dist/dev-server.d.ts +11 -2
  15. package/dist/export-utils.d.ts +15 -7
  16. package/dist/index.cjs +3085 -1272
  17. package/dist/index.d.ts +4 -1
  18. package/dist/index.js +2039 -1299
  19. package/dist/lazy-compilation.d.ts +5 -0
  20. package/dist/manifest.d.ts +32 -9
  21. package/dist/modify-browser-manifest.d.ts +30 -13
  22. package/dist/parallel-route-transform-protocol.d.ts +25 -0
  23. package/dist/parallel-route-transform-worker.d.ts +1 -0
  24. package/dist/parallel-route-transform-worker.js +38 -0
  25. package/dist/parallel-route-transforms.d.ts +14 -0
  26. package/dist/performance.d.ts +26 -0
  27. package/dist/plugin-utils.d.ts +2 -12
  28. package/dist/prerender-build.d.ts +32 -0
  29. package/dist/prerender.d.ts +28 -2
  30. package/dist/react-router-config.d.ts +8 -1
  31. package/dist/route-artifacts.d.ts +33 -0
  32. package/dist/route-ast.d.ts +21 -0
  33. package/dist/route-chunks.d.ts +9 -1
  34. package/dist/route-component-transform.d.ts +5 -0
  35. package/dist/route-export-pruning.d.ts +6 -0
  36. package/dist/route-export-resolution.d.ts +5 -0
  37. package/dist/route-transform-tasks.d.ts +47 -0
  38. package/dist/route-watch.d.ts +27 -0
  39. package/dist/server-build-plan.d.ts +22 -0
  40. package/dist/server-utils.d.ts +3 -2
  41. package/dist/templates/entry.client.js +3 -3
  42. package/dist/templates/entry.server.cjs +11 -8
  43. package/dist/templates/entry.server.js +5 -5
  44. package/dist/typegen.d.ts +2 -0
  45. package/dist/types.d.ts +30 -12
  46. package/dist/virtual-modules.d.ts +2 -0
  47. package/dist/warnings/warn-on-client-source-maps.d.ts +1 -0
  48. package/dist/yuku.d.ts +15 -0
  49. package/package.json +26 -22
  50. package/src/bounded-cache.ts +18 -0
  51. package/src/build-manifest.ts +168 -0
  52. package/src/build-output-transforms.ts +273 -0
  53. package/src/concurrency.ts +34 -0
  54. package/src/config-imports.ts +45 -0
  55. package/src/constants.ts +91 -0
  56. package/src/dev-generation.ts +700 -0
  57. package/src/dev-runtime-artifacts.ts +207 -0
  58. package/src/dev-runtime-compilation.ts +92 -0
  59. package/src/dev-runtime-controller.ts +462 -0
  60. package/src/dev-runtime-session.ts +184 -0
  61. package/src/dev-server.ts +58 -0
  62. package/src/export-utils.ts +219 -0
  63. package/src/index.ts +1025 -0
  64. package/src/lazy-compilation.ts +94 -0
  65. package/src/manifest.ts +480 -0
  66. package/src/modify-browser-manifest.ts +245 -0
  67. package/src/parallel-route-transform-protocol.ts +35 -0
  68. package/src/parallel-route-transform-worker.ts +82 -0
  69. package/src/parallel-route-transforms.ts +332 -0
  70. package/src/performance.ts +254 -0
  71. package/src/plugin-utils.ts +82 -0
  72. package/src/prerender-build.ts +623 -0
  73. package/src/prerender.ts +367 -0
  74. package/src/react-router-config.ts +220 -0
  75. package/src/route-artifacts.ts +155 -0
  76. package/src/route-ast.ts +163 -0
  77. package/src/route-chunks.ts +857 -0
  78. package/src/route-component-transform.ts +314 -0
  79. package/src/route-config.ts +106 -0
  80. package/src/route-export-pruning.ts +668 -0
  81. package/src/route-export-resolution.ts +329 -0
  82. package/src/route-transform-tasks.ts +249 -0
  83. package/src/route-watch.ts +322 -0
  84. package/src/server-build-plan.ts +91 -0
  85. package/src/server-utils.ts +210 -0
  86. package/src/ssr-externals.ts +59 -0
  87. package/src/templates/context.ts +12 -0
  88. package/src/templates/entry.client.tsx +12 -0
  89. package/src/templates/entry.server.tsx +76 -0
  90. package/src/typegen.ts +49 -0
  91. package/src/types.ts +82 -0
  92. package/src/validation/validate-plugin-order.ts +76 -0
  93. package/src/virtual-modules.ts +30 -0
  94. package/src/warnings/warn-on-client-source-maps.ts +96 -0
  95. package/src/yuku.ts +67 -0
  96. package/dist/906.js +0 -1
  97. package/dist/946.js +0 -1
  98. package/dist/babel.d.ts +0 -8
  99. package/dist/rslib-runtime.js +0 -23
@@ -0,0 +1,322 @@
1
+ import { watch, type FSWatcher } from 'node:fs';
2
+ import { access, mkdir, readdir, writeFile } from 'node:fs/promises';
3
+ import type { RsbuildConfig } from '@rsbuild/core';
4
+ import { dirname, resolve } from 'pathe';
5
+ import type { Route } from './types.js';
6
+
7
+ const ROUTE_RESTART_MARKER_ASSET = '.react-router/route-watch';
8
+ const INITIAL_RESTART_MARKER_CONTENT = 'react-router-route-watch';
9
+
10
+ type RouteManifestSnapshotEntry = Pick<
11
+ Route,
12
+ 'caseSensitive' | 'file' | 'id' | 'index' | 'parentId' | 'path'
13
+ >;
14
+ type RouteManifestSnapshotEntryPair = readonly [
15
+ string,
16
+ RouteManifestSnapshotEntry,
17
+ ];
18
+ type RouteManifestSnapshotEntries =
19
+ | Record<string, RouteManifestSnapshotEntry>
20
+ | ReadonlyArray<RouteManifestSnapshotEntryPair>;
21
+
22
+ type WatchFilesConfig = NonNullable<
23
+ NonNullable<RsbuildConfig['dev']>['watchFiles']
24
+ >;
25
+ export type WatchFileConfig =
26
+ | Exclude<WatchFilesConfig, readonly unknown[]>
27
+ | Extract<WatchFilesConfig, readonly unknown[]>[number];
28
+
29
+ type RouteDirectoryState = {
30
+ directories: Set<string>;
31
+ routeTopology: Set<string>;
32
+ };
33
+
34
+ type DirectoryWatcher = Pick<FSWatcher, 'close'>;
35
+ type WatchDirectoryEntry = (
36
+ directory: string,
37
+ onChange: () => void,
38
+ onError: (error: unknown) => void
39
+ ) => DirectoryWatcher;
40
+
41
+ const defaultWatchDirectoryEntry: WatchDirectoryEntry = (
42
+ directory,
43
+ onChange,
44
+ onError
45
+ ) => {
46
+ const watcher = watch(directory, onChange);
47
+ watcher.on('error', onError);
48
+ return watcher;
49
+ };
50
+
51
+ export const mergeWatchFiles = (
52
+ existing: WatchFilesConfig | undefined,
53
+ additions: WatchFileConfig[]
54
+ ): WatchFilesConfig => {
55
+ if (!existing) {
56
+ return additions as WatchFilesConfig;
57
+ }
58
+ return [
59
+ ...(Array.isArray(existing) ? existing : [existing]),
60
+ ...additions,
61
+ ] as WatchFilesConfig;
62
+ };
63
+
64
+ export const getRouteRestartMarkerPath = (outputClientPath: string): string =>
65
+ resolve(outputClientPath, ROUTE_RESTART_MARKER_ASSET);
66
+
67
+ export const createRouteManifestSnapshot = (
68
+ routes: RouteManifestSnapshotEntries
69
+ ): Set<string> =>
70
+ new Set(
71
+ (Array.isArray(routes) ? routes : Object.entries(routes))
72
+ // React Router uses sibling declaration order as a match tiebreaker, so
73
+ // callers that have ordered route config should pass ordered entries
74
+ // instead of a record with numeric-like keys.
75
+ .map(([routeId, route], order) =>
76
+ JSON.stringify([
77
+ order,
78
+ routeId,
79
+ route.id,
80
+ route.parentId ?? null,
81
+ route.path ?? null,
82
+ route.index ?? null,
83
+ route.caseSensitive ?? null,
84
+ route.file,
85
+ ])
86
+ )
87
+ );
88
+
89
+ export const ensureDevRestartMarker = async (
90
+ restartMarkerPath: string
91
+ ): Promise<void> => {
92
+ // Dev owns this watched file directly so ordinary rebuilds do not rewrite it
93
+ // and trigger reload loops.
94
+ await mkdir(dirname(restartMarkerPath), { recursive: true });
95
+ try {
96
+ await access(restartMarkerPath);
97
+ } catch {
98
+ await writeFile(restartMarkerPath, INITIAL_RESTART_MARKER_CONTENT);
99
+ }
100
+ };
101
+
102
+ const areSetsEqual = <T>(left: Set<T>, right: Set<T>): boolean => {
103
+ if (left.size !== right.size) {
104
+ return false;
105
+ }
106
+ for (const value of left) {
107
+ if (!right.has(value)) {
108
+ return false;
109
+ }
110
+ }
111
+ return true;
112
+ };
113
+
114
+ const readRouteDirectories = async (
115
+ watchDirectory: string
116
+ ): Promise<Set<string>> => {
117
+ const directories = new Set<string>();
118
+
119
+ const walkDirectory = async (directory: string): Promise<void> => {
120
+ let entries;
121
+ try {
122
+ entries = await readdir(directory, { withFileTypes: true });
123
+ } catch {
124
+ return;
125
+ }
126
+
127
+ directories.add(directory);
128
+ await Promise.all(
129
+ entries.map(async entry => {
130
+ const entryPath = resolve(directory, entry.name);
131
+ if (entry.isDirectory()) {
132
+ await walkDirectory(entryPath);
133
+ }
134
+ })
135
+ );
136
+ };
137
+
138
+ await walkDirectory(watchDirectory);
139
+ return directories;
140
+ };
141
+
142
+ export const createRouteTopologyWatcher = async ({
143
+ watchDirectory,
144
+ getRouteTopology,
145
+ initialRouteTopology,
146
+ restartMarkerPath,
147
+ onError,
148
+ onRouteTopologyChange,
149
+ watchDirectoryEntry: watchDirectoryOverride = defaultWatchDirectoryEntry,
150
+ }: {
151
+ watchDirectory: string;
152
+ getRouteTopology: () => Promise<Set<string>>;
153
+ initialRouteTopology?: Set<string>;
154
+ restartMarkerPath: string;
155
+ onError: (error: unknown) => void;
156
+ onRouteTopologyChange?: () => void | Promise<void>;
157
+ watchDirectoryEntry?: WatchDirectoryEntry;
158
+ }): Promise<() => Promise<void>> => {
159
+ const discoveredDirectories = await readRouteDirectories(watchDirectory);
160
+ let discoveredState: RouteDirectoryState;
161
+ try {
162
+ discoveredState = {
163
+ directories: discoveredDirectories,
164
+ routeTopology: await getRouteTopology(),
165
+ };
166
+ } catch (error) {
167
+ if (!initialRouteTopology) {
168
+ throw error;
169
+ }
170
+ onError(error);
171
+ discoveredState = {
172
+ directories: discoveredDirectories,
173
+ routeTopology: initialRouteTopology,
174
+ };
175
+ }
176
+ let state = {
177
+ ...discoveredState,
178
+ routeTopology: initialRouteTopology ?? discoveredState.routeTopology,
179
+ };
180
+ let closed = false;
181
+ let rescanTimer: ReturnType<typeof setTimeout> | undefined;
182
+ let rescanQueue = Promise.resolve();
183
+ const directoryWatchers = new Map<string, DirectoryWatcher>();
184
+
185
+ const touchRestartMarker = async (): Promise<void> => {
186
+ await mkdir(dirname(restartMarkerPath), { recursive: true });
187
+ await writeFile(restartMarkerPath, String(Date.now()));
188
+ };
189
+
190
+ const closeRemovedDirectoryWatchers = (
191
+ nextDirectories: Set<string>
192
+ ): void => {
193
+ for (const [directory, watcher] of directoryWatchers) {
194
+ if (!nextDirectories.has(directory)) {
195
+ watcher.close();
196
+ directoryWatchers.delete(directory);
197
+ }
198
+ }
199
+ };
200
+
201
+ const watchNewDirectories = (nextDirectories: Set<string>): void => {
202
+ for (const directory of nextDirectories) {
203
+ if (directoryWatchers.has(directory)) {
204
+ continue;
205
+ }
206
+ try {
207
+ let watcher: DirectoryWatcher;
208
+ watcher = watchDirectoryOverride(directory, scheduleRescan, error => {
209
+ if (directoryWatchers.get(directory) === watcher) {
210
+ watcher.close();
211
+ directoryWatchers.delete(directory);
212
+ }
213
+ onError(error);
214
+ });
215
+ directoryWatchers.set(directory, watcher);
216
+ } catch (error) {
217
+ onError(error);
218
+ }
219
+ }
220
+ };
221
+
222
+ const syncDirectoryWatchers = (nextDirectories: Set<string>): void => {
223
+ closeRemovedDirectoryWatchers(nextDirectories);
224
+ watchNewDirectories(nextDirectories);
225
+ };
226
+
227
+ const applyNextState = async (nextState: RouteDirectoryState) => {
228
+ if (closed) {
229
+ return;
230
+ }
231
+ syncDirectoryWatchers(nextState.directories);
232
+ if (!areSetsEqual(state.routeTopology, nextState.routeTopology)) {
233
+ if (onRouteTopologyChange) {
234
+ // This is a notification boundary, not part of the rescan
235
+ // transaction. A custom-server callback may close this watcher while
236
+ // replacing its compiler, so awaiting it here would deadlock close().
237
+ state = nextState;
238
+ try {
239
+ void Promise.resolve(onRouteTopologyChange()).catch(onError);
240
+ } catch (error) {
241
+ onError(error);
242
+ }
243
+ return;
244
+ } else {
245
+ await touchRestartMarker();
246
+ }
247
+ if (closed) {
248
+ return;
249
+ }
250
+ state = nextState;
251
+ return;
252
+ }
253
+ state = nextState;
254
+ };
255
+
256
+ const runRescan = async (): Promise<void> => {
257
+ if (closed) {
258
+ return;
259
+ }
260
+ let nextDirectories: Set<string> | undefined;
261
+ try {
262
+ nextDirectories = await readRouteDirectories(watchDirectory);
263
+ if (closed) {
264
+ return;
265
+ }
266
+ const nextState = {
267
+ directories: nextDirectories,
268
+ routeTopology: await getRouteTopology(),
269
+ };
270
+ if (closed) {
271
+ return;
272
+ }
273
+ await applyNextState(nextState);
274
+ } catch (error) {
275
+ if (nextDirectories && !closed) {
276
+ syncDirectoryWatchers(nextDirectories);
277
+ }
278
+ onError(error);
279
+ }
280
+ };
281
+
282
+ const rescan = (): Promise<void> => {
283
+ rescanQueue = rescanQueue.then(runRescan, runRescan);
284
+ return rescanQueue;
285
+ };
286
+
287
+ const scheduleRescan = (): void => {
288
+ if (rescanTimer) {
289
+ clearTimeout(rescanTimer);
290
+ }
291
+ rescanTimer = setTimeout(() => {
292
+ rescanTimer = undefined;
293
+ void rescan();
294
+ }, 100);
295
+ };
296
+
297
+ try {
298
+ await applyNextState(discoveredState);
299
+ } catch (error) {
300
+ onError(error);
301
+ }
302
+
303
+ return async () => {
304
+ if (closed) {
305
+ await rescanQueue;
306
+ return;
307
+ }
308
+ closed = true;
309
+ if (rescanTimer) {
310
+ clearTimeout(rescanTimer);
311
+ }
312
+ for (const watcher of directoryWatchers.values()) {
313
+ watcher.close();
314
+ }
315
+ directoryWatchers.clear();
316
+ await rescanQueue;
317
+ for (const watcher of directoryWatchers.values()) {
318
+ watcher.close();
319
+ }
320
+ directoryWatchers.clear();
321
+ };
322
+ };
@@ -0,0 +1,91 @@
1
+ import { PLUGIN_NAME } from './constants.js';
2
+ import type { ReactRouterDevBuildPlan } from './dev-runtime-artifacts.js';
3
+ import type { Route } from './types.js';
4
+
5
+ export type ReactRouterServerBundleEntry = {
6
+ bundleId: string;
7
+ entryName: string;
8
+ };
9
+
10
+ export type ReactRouterServerBuildPlan = ReactRouterDevBuildPlan & {
11
+ serverBundleEntries: ReactRouterServerBundleEntry[];
12
+ };
13
+
14
+ export const createReactRouterServerBuildPlan = ({
15
+ routesByServerBundleId,
16
+ serverBuildFile,
17
+ defaultEntryName,
18
+ }: {
19
+ routesByServerBundleId: Record<string, Record<string, Route>>;
20
+ serverBuildFile: string | undefined;
21
+ defaultEntryName: string;
22
+ }): ReactRouterServerBuildPlan => {
23
+ const serverBuildFileBase = (serverBuildFile || 'index.js').replace(
24
+ /\.js$/,
25
+ ''
26
+ );
27
+ const serverBundleEntries = Object.entries(routesByServerBundleId)
28
+ .filter(([, bundleRoutes]) =>
29
+ Boolean(bundleRoutes && Object.keys(bundleRoutes).length > 0)
30
+ )
31
+ .map(([bundleId]) => ({
32
+ bundleId,
33
+ entryName: `${bundleId}/${serverBuildFileBase}`,
34
+ }));
35
+
36
+ const reservedNodeEntryNames = new Set([
37
+ 'static/js/app',
38
+ 'static/js/entry.server',
39
+ defaultEntryName,
40
+ ]);
41
+ for (const { entryName } of serverBundleEntries) {
42
+ if (reservedNodeEntryNames.has(entryName)) {
43
+ throw new Error(
44
+ `[${PLUGIN_NAME}] Server bundle entry ${JSON.stringify(entryName)} conflicts with a reserved node entry.`
45
+ );
46
+ }
47
+ reservedNodeEntryNames.add(entryName);
48
+ }
49
+
50
+ return {
51
+ defaultEntryName,
52
+ entryNames: [
53
+ defaultEntryName,
54
+ ...serverBundleEntries.map(({ entryName }) => entryName),
55
+ ],
56
+ serverBundleEntries,
57
+ };
58
+ };
59
+
60
+ export const createReactRouterNodeEntries = ({
61
+ hasServerApp,
62
+ isBuild,
63
+ serverAppPath,
64
+ entryServerPath,
65
+ defaultEntryName,
66
+ serverBundleEntries,
67
+ }: {
68
+ hasServerApp: boolean;
69
+ isBuild: boolean;
70
+ serverAppPath: string;
71
+ entryServerPath: string;
72
+ defaultEntryName: string;
73
+ serverBundleEntries: readonly ReactRouterServerBundleEntry[];
74
+ }): Record<string, string> => {
75
+ const entries: Record<string, string> = {
76
+ 'static/js/app': hasServerApp
77
+ ? serverAppPath
78
+ : 'virtual/react-router/server-build',
79
+ 'static/js/entry.server': entryServerPath,
80
+ };
81
+
82
+ if (hasServerApp && !isBuild) {
83
+ entries[defaultEntryName] = 'virtual/react-router/server-build';
84
+ }
85
+
86
+ for (const { bundleId, entryName } of serverBundleEntries) {
87
+ entries[entryName] = `virtual/react-router/server-build-${bundleId}`;
88
+ }
89
+
90
+ return entries;
91
+ };
@@ -0,0 +1,210 @@
1
+ import { resolve } from 'pathe';
2
+ import type { ServerBuild } from 'react-router';
3
+ import type { Route } from './types.js';
4
+
5
+ /**
6
+ * Generates the server build template string.
7
+ *
8
+ * Note: Federation mode used to require async `import()` wrappers for entrypoints.
9
+ * With Rspack/@module-federation support for `experiments.asyncStartup`, the
10
+ * server build can always use static imports.
11
+ */
12
+ interface ServerBuildOptions {
13
+ entryServerPath: string;
14
+ assetsBuildDirectory: string;
15
+ basename: string;
16
+ appDirectory: string;
17
+ ssr: boolean;
18
+ future?: unknown;
19
+ allowedActionOrigins?: string[];
20
+ prerender?: string[];
21
+ publicPath?: string;
22
+ serverManifestId?: string;
23
+ routeDiscovery:
24
+ | {
25
+ mode: 'lazy';
26
+ manifestPath?: string;
27
+ }
28
+ | {
29
+ mode: 'initial';
30
+ }
31
+ | undefined;
32
+ }
33
+
34
+ function generateStaticTemplate(
35
+ routes: Record<string, Route>,
36
+ options: ServerBuildOptions
37
+ ): string {
38
+ const manifestId =
39
+ options.serverManifestId ?? 'virtual/react-router/server-manifest';
40
+ return `
41
+ import * as entryServer from ${JSON.stringify(options.entryServerPath)};
42
+ ${Object.keys(routes)
43
+ .map((key, index) => {
44
+ const route = routes[key];
45
+ return `import * as route${index} from ${JSON.stringify(
46
+ `${resolve(options.appDirectory, route.file)}?react-router-route`
47
+ )};`;
48
+ })
49
+ .join('\n')}
50
+
51
+ export { default as assets } from ${JSON.stringify(manifestId)};
52
+ export const assetsBuildDirectory = ${JSON.stringify(
53
+ options.assetsBuildDirectory
54
+ )};
55
+ export const basename = ${JSON.stringify(options.basename)};
56
+ export const future = ${JSON.stringify(options.future ?? {})};
57
+ export const isSpaMode = ${!options.ssr};
58
+ export const ssr = ${options.ssr};
59
+ export const routeDiscovery = ${JSON.stringify(options.routeDiscovery)};
60
+ export const prerender = ${JSON.stringify(options.prerender ?? [])};
61
+ export const publicPath = ${JSON.stringify(options.publicPath ?? '/')};
62
+ export const entry = { module: entryServer };
63
+ export const allowedActionOrigins = ${JSON.stringify(options.allowedActionOrigins)};
64
+ export var routes = {};
65
+ ${Object.keys(routes)
66
+ .map((key, index) => {
67
+ const route = routes[key];
68
+ return `routes[${JSON.stringify(key)}] = {
69
+ id: ${JSON.stringify(route.id)},
70
+ parentId: ${JSON.stringify(route.parentId)},
71
+ path: ${JSON.stringify(route.path)},
72
+ index: ${JSON.stringify(route.index)},
73
+ caseSensitive: ${JSON.stringify(route.caseSensitive)},
74
+ module: route${index}
75
+ };`;
76
+ })
77
+ .join('\n ')}
78
+ `;
79
+ }
80
+
81
+ /**
82
+ * Generates the server build module content
83
+ * @param routes The route manifest
84
+ * @param options Build options
85
+ * @returns The generated module content as a string
86
+ */
87
+ function generateServerBuild(
88
+ routes: Record<string, Route>,
89
+ options: ServerBuildOptions & { federation?: boolean }
90
+ ): string {
91
+ return generateStaticTemplate(routes, options);
92
+ }
93
+
94
+ const RESOLVABLE_BUILD_EXPORTS = new Set([
95
+ 'allowedActionOrigins',
96
+ 'assets',
97
+ 'assetsBuildDirectory',
98
+ 'basename',
99
+ 'entry',
100
+ 'future',
101
+ 'isSpaMode',
102
+ 'prerender',
103
+ 'publicPath',
104
+ 'routeDiscovery',
105
+ 'routes',
106
+ 'ssr',
107
+ ]);
108
+
109
+ function isRecord(value: unknown): value is Record<string, unknown> {
110
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
111
+ }
112
+
113
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
114
+ return isRecord(value) && typeof value.then === 'function';
115
+ }
116
+
117
+ function isRouteDiscovery(value: unknown): boolean {
118
+ return (
119
+ isRecord(value) &&
120
+ (value.mode === 'initial' ||
121
+ (value.mode === 'lazy' &&
122
+ (value.manifestPath === undefined ||
123
+ typeof value.manifestPath === 'string')))
124
+ );
125
+ }
126
+
127
+ async function resolveBuildExports(
128
+ build: Record<string, unknown>
129
+ ): Promise<Record<string, unknown>> {
130
+ const resolved = { ...build };
131
+ for (const key of Object.keys(build)) {
132
+ if (!RESOLVABLE_BUILD_EXPORTS.has(key)) {
133
+ continue;
134
+ }
135
+ const value = build[key];
136
+ if (typeof value === 'function' && value.length === 0) {
137
+ const result = value();
138
+ resolved[key] = isPromiseLike(result) ? await result : result;
139
+ continue;
140
+ }
141
+ if (isPromiseLike(value)) {
142
+ resolved[key] = await value;
143
+ }
144
+ }
145
+ return resolved;
146
+ }
147
+
148
+ function isServerBuild(value: unknown): value is ServerBuild {
149
+ return Boolean(
150
+ isRecord(value) &&
151
+ isRecord(value.entry) &&
152
+ isRecord(value.entry.module) &&
153
+ typeof value.entry.module.default === 'function' &&
154
+ isRecord(value.routes) &&
155
+ isRecord(value.assets) &&
156
+ typeof value.assetsBuildDirectory === 'string' &&
157
+ (value.basename === undefined || typeof value.basename === 'string') &&
158
+ isRecord(value.future) &&
159
+ typeof value.isSpaMode === 'boolean' &&
160
+ Array.isArray(value.prerender) &&
161
+ typeof value.publicPath === 'string' &&
162
+ isRouteDiscovery(value.routeDiscovery) &&
163
+ typeof value.ssr === 'boolean'
164
+ );
165
+ }
166
+
167
+ async function resolveServerBuildCandidate(
168
+ candidate: unknown
169
+ ): Promise<ServerBuild | undefined> {
170
+ if (!isRecord(candidate)) {
171
+ return undefined;
172
+ }
173
+ const resolved = await resolveBuildExports(candidate);
174
+ return isServerBuild(resolved) ? resolved : undefined;
175
+ }
176
+
177
+ export async function resolveServerBuildModule(
178
+ buildModule: unknown,
179
+ source: string
180
+ ): Promise<ServerBuild> {
181
+ const moduleValue = await buildModule;
182
+ const candidates = [() => moduleValue];
183
+ if (isRecord(moduleValue)) {
184
+ if ('default' in moduleValue) {
185
+ candidates.push(() => moduleValue.default);
186
+ }
187
+ if ('module.exports' in moduleValue) {
188
+ candidates.push(() => moduleValue['module.exports']);
189
+ }
190
+ }
191
+
192
+ for (const getCandidate of candidates) {
193
+ const candidate = await getCandidate();
194
+ const serverBuild = await resolveServerBuildCandidate(candidate);
195
+ if (serverBuild) {
196
+ return serverBuild;
197
+ }
198
+ }
199
+ throw new Error(
200
+ `[rsbuild-plugin-react-router] ${source} did not contain a valid React Router ServerBuild.`
201
+ );
202
+ }
203
+
204
+ export function resolveReactRouterServerBuild(
205
+ buildModule: unknown
206
+ ): Promise<ServerBuild> {
207
+ return resolveServerBuildModule(buildModule, 'Imported module');
208
+ }
209
+
210
+ export { generateServerBuild };
@@ -0,0 +1,59 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { sep } from 'node:path';
4
+
5
+ const REACT_ROUTER_EXTERNALS = [
6
+ 'react-router',
7
+ 'react-router-dom',
8
+ '@react-router/architect',
9
+ '@react-router/cloudflare',
10
+ '@react-router/dev',
11
+ '@react-router/express',
12
+ '@react-router/node',
13
+ '@react-router/serve',
14
+ ];
15
+
16
+ const requireFromHere = createRequire(import.meta.url);
17
+
18
+ function resolvePackageJson(
19
+ name: string,
20
+ rootDirectory: string
21
+ ): string | null {
22
+ try {
23
+ return requireFromHere.resolve(`${name}/package.json`, {
24
+ paths: [rootDirectory],
25
+ });
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
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
+ export function getSsrExternals(rootDirectory: string): string[] {
44
+ const externals: string[] = [];
45
+
46
+ for (const name of REACT_ROUTER_EXTERNALS) {
47
+ const resolved = resolvePackageJson(name, rootDirectory);
48
+ if (!resolved) {
49
+ continue;
50
+ }
51
+
52
+ const realPath = safeRealpath(resolved);
53
+ if (!isPathInNodeModules(realPath)) {
54
+ externals.push(name);
55
+ }
56
+ }
57
+
58
+ return externals;
59
+ }
@@ -0,0 +1,12 @@
1
+ import { createContext } from 'react';
2
+ import type { Context } from 'react';
3
+
4
+ export interface Assets {
5
+ scriptTags: string[];
6
+ styleTags: string[];
7
+ }
8
+
9
+ export const AssetsContext: Context<Assets> = createContext<Assets>({
10
+ scriptTags: [],
11
+ styleTags: [],
12
+ });
@@ -0,0 +1,12 @@
1
+ import * as React from 'react';
2
+ import { hydrateRoot } from 'react-dom/client';
3
+ import { HydratedRouter } from 'react-router/dom';
4
+
5
+ React.startTransition(() => {
6
+ hydrateRoot(
7
+ document,
8
+ <React.StrictMode>
9
+ <HydratedRouter />
10
+ </React.StrictMode>
11
+ );
12
+ });