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.
- package/README.md +185 -204
- package/dist/451.js +1087 -0
- package/dist/bounded-cache.d.ts +1 -0
- package/dist/build-manifest.d.ts +1 -1
- package/dist/build-output-transforms.d.ts +30 -0
- package/dist/concurrency.d.ts +2 -0
- package/dist/config-imports.d.ts +3 -0
- package/dist/constants.d.ts +3 -0
- package/dist/dev-generation.d.ts +25 -0
- package/dist/dev-runtime-artifacts.d.ts +39 -0
- package/dist/dev-runtime-compilation.d.ts +32 -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 +11 -2
- package/dist/export-utils.d.ts +15 -7
- package/dist/index.cjs +3085 -1272
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2039 -1299
- package/dist/lazy-compilation.d.ts +5 -0
- package/dist/manifest.d.ts +32 -9
- 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 +14 -0
- package/dist/performance.d.ts +26 -0
- package/dist/plugin-utils.d.ts +2 -12
- package/dist/prerender-build.d.ts +32 -0
- package/dist/prerender.d.ts +28 -2
- package/dist/react-router-config.d.ts +8 -1
- 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-utils.d.ts +3 -2
- package/dist/templates/entry.client.js +3 -3
- package/dist/templates/entry.server.cjs +11 -8
- package/dist/templates/entry.server.js +5 -5
- package/dist/typegen.d.ts +2 -0
- package/dist/types.d.ts +30 -12
- 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 +26 -22
- package/src/bounded-cache.ts +18 -0
- package/src/build-manifest.ts +168 -0
- package/src/build-output-transforms.ts +273 -0
- package/src/concurrency.ts +34 -0
- package/src/config-imports.ts +45 -0
- package/src/constants.ts +91 -0
- package/src/dev-generation.ts +700 -0
- package/src/dev-runtime-artifacts.ts +207 -0
- package/src/dev-runtime-compilation.ts +92 -0
- package/src/dev-runtime-controller.ts +462 -0
- package/src/dev-runtime-session.ts +184 -0
- package/src/dev-server.ts +58 -0
- package/src/export-utils.ts +219 -0
- package/src/index.ts +1025 -0
- package/src/lazy-compilation.ts +94 -0
- package/src/manifest.ts +480 -0
- package/src/modify-browser-manifest.ts +245 -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 +332 -0
- package/src/performance.ts +254 -0
- package/src/plugin-utils.ts +82 -0
- package/src/prerender-build.ts +623 -0
- package/src/prerender.ts +367 -0
- package/src/react-router-config.ts +220 -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 +322 -0
- package/src/server-build-plan.ts +91 -0
- package/src/server-utils.ts +210 -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 +49 -0
- package/src/types.ts +82 -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/906.js +0 -1
- package/dist/946.js +0 -1
- package/dist/babel.d.ts +0 -8
- 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
|
+
});
|