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,357 @@
|
|
|
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 * as Effect from 'effect/Effect';
|
|
5
|
+
import { dirname, resolve } from 'pathe';
|
|
6
|
+
import { getCappedPluginConcurrency } from './concurrency.js';
|
|
7
|
+
import {
|
|
8
|
+
createDelayedPluginTask,
|
|
9
|
+
runPluginEffect,
|
|
10
|
+
tryPluginPromise,
|
|
11
|
+
} from './effect-runtime.js';
|
|
12
|
+
import type { Route } from './types.js';
|
|
13
|
+
|
|
14
|
+
const ROUTE_RESTART_MARKER_ASSET = '.react-router/route-watch';
|
|
15
|
+
const INITIAL_RESTART_MARKER_CONTENT = 'react-router-route-watch';
|
|
16
|
+
const ROUTE_TOPOLOGY_RESCAN_DEBOUNCE_MS = 100;
|
|
17
|
+
const ROUTE_DIRECTORY_SCAN_CONCURRENCY = getCappedPluginConcurrency();
|
|
18
|
+
|
|
19
|
+
type RouteManifestSnapshotEntry = Pick<
|
|
20
|
+
Route,
|
|
21
|
+
'caseSensitive' | 'file' | 'id' | 'index' | 'parentId' | 'path'
|
|
22
|
+
>;
|
|
23
|
+
type RouteManifestSnapshotEntryPair = readonly [
|
|
24
|
+
string,
|
|
25
|
+
RouteManifestSnapshotEntry,
|
|
26
|
+
];
|
|
27
|
+
type RouteManifestSnapshotEntries =
|
|
28
|
+
| Record<string, RouteManifestSnapshotEntry>
|
|
29
|
+
| ReadonlyArray<RouteManifestSnapshotEntryPair>;
|
|
30
|
+
|
|
31
|
+
type WatchFilesConfig = NonNullable<
|
|
32
|
+
NonNullable<RsbuildConfig['dev']>['watchFiles']
|
|
33
|
+
>;
|
|
34
|
+
export type WatchFileConfig =
|
|
35
|
+
| Exclude<WatchFilesConfig, readonly unknown[]>
|
|
36
|
+
| Extract<WatchFilesConfig, readonly unknown[]>[number];
|
|
37
|
+
|
|
38
|
+
type RouteDirectoryState = {
|
|
39
|
+
directories: Set<string>;
|
|
40
|
+
routeTopology: Set<string>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type DirectoryWatcher = Pick<FSWatcher, 'close'>;
|
|
44
|
+
type WatchDirectoryEntry = (
|
|
45
|
+
directory: string,
|
|
46
|
+
onChange: () => void,
|
|
47
|
+
onError: (error: unknown) => void
|
|
48
|
+
) => DirectoryWatcher;
|
|
49
|
+
|
|
50
|
+
const defaultWatchDirectoryEntry: WatchDirectoryEntry = (
|
|
51
|
+
directory,
|
|
52
|
+
onChange,
|
|
53
|
+
onError
|
|
54
|
+
) => {
|
|
55
|
+
const watcher = watch(directory, onChange);
|
|
56
|
+
watcher.on('error', onError);
|
|
57
|
+
return watcher;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const mergeWatchFiles = (
|
|
61
|
+
existing: WatchFilesConfig | undefined,
|
|
62
|
+
additions: WatchFileConfig[]
|
|
63
|
+
): WatchFilesConfig => {
|
|
64
|
+
if (!existing) {
|
|
65
|
+
return additions as WatchFilesConfig;
|
|
66
|
+
}
|
|
67
|
+
return [
|
|
68
|
+
...(Array.isArray(existing) ? existing : [existing]),
|
|
69
|
+
...additions,
|
|
70
|
+
] as WatchFilesConfig;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const getRouteRestartMarkerPath = (outputClientPath: string): string =>
|
|
74
|
+
resolve(outputClientPath, ROUTE_RESTART_MARKER_ASSET);
|
|
75
|
+
|
|
76
|
+
export const createRouteManifestSnapshot = (
|
|
77
|
+
routes: RouteManifestSnapshotEntries
|
|
78
|
+
): Set<string> =>
|
|
79
|
+
new Set(
|
|
80
|
+
(Array.isArray(routes) ? routes : Object.entries(routes))
|
|
81
|
+
// React Router uses sibling declaration order as a match tiebreaker, so
|
|
82
|
+
// callers that have ordered route config should pass ordered entries
|
|
83
|
+
// instead of a record with numeric-like keys.
|
|
84
|
+
.map(([routeId, route], order) =>
|
|
85
|
+
JSON.stringify([
|
|
86
|
+
order,
|
|
87
|
+
routeId,
|
|
88
|
+
route.id,
|
|
89
|
+
route.parentId ?? null,
|
|
90
|
+
route.path ?? null,
|
|
91
|
+
route.index ?? null,
|
|
92
|
+
route.caseSensitive ?? null,
|
|
93
|
+
route.file,
|
|
94
|
+
])
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
export const ensureDevRestartMarker = async (
|
|
99
|
+
restartMarkerPath: string
|
|
100
|
+
): Promise<void> => {
|
|
101
|
+
// Dev owns this watched file directly so ordinary rebuilds do not rewrite it
|
|
102
|
+
// and trigger reload loops.
|
|
103
|
+
await mkdir(dirname(restartMarkerPath), { recursive: true });
|
|
104
|
+
try {
|
|
105
|
+
await access(restartMarkerPath);
|
|
106
|
+
} catch {
|
|
107
|
+
await writeFile(restartMarkerPath, INITIAL_RESTART_MARKER_CONTENT);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const areSetsEqual = <T>(left: Set<T>, right: Set<T>): boolean => {
|
|
112
|
+
if (left.size !== right.size) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
for (const value of left) {
|
|
116
|
+
if (!right.has(value)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const readRouteDirectories = (watchDirectory: string): Promise<Set<string>> => {
|
|
124
|
+
return runPluginEffect(readRouteDirectoriesEffect(watchDirectory));
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const readRouteDirectoriesEffect = (
|
|
128
|
+
watchDirectory: string
|
|
129
|
+
): Effect.Effect<Set<string>, Error, never> => {
|
|
130
|
+
const directories = new Set<string>();
|
|
131
|
+
const walkDirectory = (directory: string): Effect.Effect<void> =>
|
|
132
|
+
tryPluginPromise(() => readdir(directory, { withFileTypes: true })).pipe(
|
|
133
|
+
Effect.catchAll(() => Effect.succeed([])),
|
|
134
|
+
Effect.map(entries => {
|
|
135
|
+
directories.add(directory);
|
|
136
|
+
return entries
|
|
137
|
+
.filter(entry => entry.isDirectory())
|
|
138
|
+
.map(entry => resolve(directory, entry.name));
|
|
139
|
+
}),
|
|
140
|
+
Effect.flatMap(childDirectories =>
|
|
141
|
+
Effect.forEach(childDirectories, walkDirectory, {
|
|
142
|
+
concurrency: ROUTE_DIRECTORY_SCAN_CONCURRENCY,
|
|
143
|
+
discard: true,
|
|
144
|
+
})
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return walkDirectory(watchDirectory).pipe(Effect.as(directories));
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const createRouteTopologyWatcher = async ({
|
|
152
|
+
watchDirectory,
|
|
153
|
+
getRouteTopology,
|
|
154
|
+
initialRouteTopology,
|
|
155
|
+
restartMarkerPath,
|
|
156
|
+
onError,
|
|
157
|
+
onRouteTopologyChange,
|
|
158
|
+
watchDirectoryEntry: watchDirectoryOverride = defaultWatchDirectoryEntry,
|
|
159
|
+
}: {
|
|
160
|
+
watchDirectory: string;
|
|
161
|
+
getRouteTopology: () => Promise<Set<string>>;
|
|
162
|
+
initialRouteTopology?: Set<string>;
|
|
163
|
+
restartMarkerPath: string;
|
|
164
|
+
onError: (error: unknown) => void;
|
|
165
|
+
onRouteTopologyChange?: () => void | Promise<void>;
|
|
166
|
+
watchDirectoryEntry?: WatchDirectoryEntry;
|
|
167
|
+
}): Promise<() => Promise<void>> => {
|
|
168
|
+
const discoveredDirectories = await readRouteDirectories(watchDirectory);
|
|
169
|
+
let discoveredState: RouteDirectoryState;
|
|
170
|
+
try {
|
|
171
|
+
discoveredState = {
|
|
172
|
+
directories: discoveredDirectories,
|
|
173
|
+
routeTopology: await getRouteTopology(),
|
|
174
|
+
};
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (!initialRouteTopology) {
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
onError(error);
|
|
180
|
+
discoveredState = {
|
|
181
|
+
directories: discoveredDirectories,
|
|
182
|
+
routeTopology: initialRouteTopology,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
let state = {
|
|
186
|
+
...discoveredState,
|
|
187
|
+
routeTopology: initialRouteTopology ?? discoveredState.routeTopology,
|
|
188
|
+
};
|
|
189
|
+
let closed = false;
|
|
190
|
+
let rescanQueue = Promise.resolve();
|
|
191
|
+
const directoryWatchers = new Map<string, DirectoryWatcher>();
|
|
192
|
+
|
|
193
|
+
const touchRestartMarkerEffect = (): Effect.Effect<void, Error, never> =>
|
|
194
|
+
tryPluginPromise(() =>
|
|
195
|
+
mkdir(dirname(restartMarkerPath), { recursive: true })
|
|
196
|
+
).pipe(
|
|
197
|
+
Effect.zipRight(
|
|
198
|
+
tryPluginPromise(() => writeFile(restartMarkerPath, String(Date.now())))
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const closeRemovedDirectoryWatchers = (
|
|
203
|
+
nextDirectories: Set<string>
|
|
204
|
+
): void => {
|
|
205
|
+
for (const [directory, watcher] of directoryWatchers) {
|
|
206
|
+
if (!nextDirectories.has(directory)) {
|
|
207
|
+
watcher.close();
|
|
208
|
+
directoryWatchers.delete(directory);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const watchNewDirectories = (nextDirectories: Set<string>): void => {
|
|
214
|
+
for (const directory of nextDirectories) {
|
|
215
|
+
if (directoryWatchers.has(directory)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
let watcher: DirectoryWatcher;
|
|
220
|
+
watcher = watchDirectoryOverride(
|
|
221
|
+
directory,
|
|
222
|
+
() => rescanTask.reschedule(),
|
|
223
|
+
error => {
|
|
224
|
+
if (directoryWatchers.get(directory) === watcher) {
|
|
225
|
+
watcher.close();
|
|
226
|
+
directoryWatchers.delete(directory);
|
|
227
|
+
}
|
|
228
|
+
onError(error);
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
directoryWatchers.set(directory, watcher);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
onError(error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const syncDirectoryWatchers = (nextDirectories: Set<string>): void => {
|
|
239
|
+
closeRemovedDirectoryWatchers(nextDirectories);
|
|
240
|
+
watchNewDirectories(nextDirectories);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const applyNextStateEffect = (
|
|
244
|
+
nextState: RouteDirectoryState
|
|
245
|
+
): Effect.Effect<void, Error, never> =>
|
|
246
|
+
Effect.suspend(() => {
|
|
247
|
+
if (closed) {
|
|
248
|
+
return Effect.void;
|
|
249
|
+
}
|
|
250
|
+
syncDirectoryWatchers(nextState.directories);
|
|
251
|
+
if (!areSetsEqual(state.routeTopology, nextState.routeTopology)) {
|
|
252
|
+
if (onRouteTopologyChange) {
|
|
253
|
+
// This is a notification boundary, not part of the rescan
|
|
254
|
+
// transaction. A custom-server callback may close this watcher while
|
|
255
|
+
// replacing its compiler, so awaiting it here would deadlock close().
|
|
256
|
+
state = nextState;
|
|
257
|
+
return Effect.sync(() => {
|
|
258
|
+
try {
|
|
259
|
+
void Promise.resolve(onRouteTopologyChange()).catch(onError);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
onError(error);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return touchRestartMarkerEffect().pipe(
|
|
266
|
+
Effect.zipRight(
|
|
267
|
+
Effect.sync(() => {
|
|
268
|
+
if (!closed) {
|
|
269
|
+
state = nextState;
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
)
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
state = nextState;
|
|
276
|
+
return Effect.void;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const runRescanEffect = (): Effect.Effect<void, never, never> => {
|
|
280
|
+
let nextDirectories: Set<string> | undefined;
|
|
281
|
+
return Effect.gen(function* () {
|
|
282
|
+
if (closed) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
nextDirectories = yield* readRouteDirectoriesEffect(watchDirectory);
|
|
286
|
+
if (closed) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const nextState = {
|
|
290
|
+
directories: nextDirectories,
|
|
291
|
+
routeTopology: yield* tryPluginPromise(getRouteTopology),
|
|
292
|
+
};
|
|
293
|
+
if (closed) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
yield* applyNextStateEffect(nextState);
|
|
297
|
+
}).pipe(
|
|
298
|
+
Effect.catchAll(error =>
|
|
299
|
+
Effect.sync(() => {
|
|
300
|
+
if (nextDirectories && !closed) {
|
|
301
|
+
syncDirectoryWatchers(nextDirectories);
|
|
302
|
+
}
|
|
303
|
+
onError(error);
|
|
304
|
+
})
|
|
305
|
+
)
|
|
306
|
+
);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const rescan = (): Promise<void> => {
|
|
310
|
+
rescanQueue = rescanQueue.then(
|
|
311
|
+
() => runPluginEffect(runRescanEffect()),
|
|
312
|
+
() => runPluginEffect(runRescanEffect())
|
|
313
|
+
);
|
|
314
|
+
return rescanQueue;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const rescanTask = createDelayedPluginTask({
|
|
318
|
+
delayMs: ROUTE_TOPOLOGY_RESCAN_DEBOUNCE_MS,
|
|
319
|
+
run: () =>
|
|
320
|
+
Effect.suspend(() =>
|
|
321
|
+
closed ? Effect.void : tryPluginPromise(rescan).pipe(Effect.asVoid)
|
|
322
|
+
),
|
|
323
|
+
onError,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const cancelScheduledRescan = (): Promise<void> =>
|
|
327
|
+
runPluginEffect(rescanTask.cancelEffect());
|
|
328
|
+
|
|
329
|
+
const applyNextState = async (nextState: RouteDirectoryState) => {
|
|
330
|
+
await runPluginEffect(applyNextStateEffect(nextState));
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
await applyNextState(discoveredState);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
onError(error);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return async () => {
|
|
340
|
+
if (closed) {
|
|
341
|
+
await cancelScheduledRescan();
|
|
342
|
+
await rescanQueue;
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
closed = true;
|
|
346
|
+
await cancelScheduledRescan();
|
|
347
|
+
for (const watcher of directoryWatchers.values()) {
|
|
348
|
+
watcher.close();
|
|
349
|
+
}
|
|
350
|
+
directoryWatchers.clear();
|
|
351
|
+
await rescanQueue;
|
|
352
|
+
for (const watcher of directoryWatchers.values()) {
|
|
353
|
+
watcher.close();
|
|
354
|
+
}
|
|
355
|
+
directoryWatchers.clear();
|
|
356
|
+
};
|
|
357
|
+
};
|
|
@@ -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,131 @@
|
|
|
1
|
+
// Internal module: exposes the Effect-based ServerBuild resolution used by
|
|
2
|
+
// dev-runtime code. Not re-exported from the package entry so the public
|
|
3
|
+
// declaration graph stays free of `effect` types; external callers go through
|
|
4
|
+
// the Promise wrappers in server-utils.ts.
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import type { ServerBuild } from 'react-router';
|
|
7
|
+
import { tryPluginPromise, tryPluginSync } from './effect-runtime.js';
|
|
8
|
+
|
|
9
|
+
const RESOLVABLE_BUILD_EXPORTS = new Set([
|
|
10
|
+
'allowedActionOrigins',
|
|
11
|
+
'assets',
|
|
12
|
+
'assetsBuildDirectory',
|
|
13
|
+
'basename',
|
|
14
|
+
'entry',
|
|
15
|
+
'future',
|
|
16
|
+
'isSpaMode',
|
|
17
|
+
'prerender',
|
|
18
|
+
'publicPath',
|
|
19
|
+
'routeDiscovery',
|
|
20
|
+
'routes',
|
|
21
|
+
'ssr',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
29
|
+
return isRecord(value) && typeof value.then === 'function';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isRouteDiscovery(value: unknown): boolean {
|
|
33
|
+
return (
|
|
34
|
+
value === undefined ||
|
|
35
|
+
(isRecord(value) &&
|
|
36
|
+
(value.mode === 'initial' ||
|
|
37
|
+
(value.mode === 'lazy' &&
|
|
38
|
+
(value.manifestPath === undefined ||
|
|
39
|
+
typeof value.manifestPath === 'string'))))
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveBuildExportsEffect(
|
|
44
|
+
build: Record<string, unknown>
|
|
45
|
+
): Effect.Effect<Record<string, unknown>, Error, never> {
|
|
46
|
+
const resolved = { ...build };
|
|
47
|
+
return Effect.forEach(
|
|
48
|
+
Object.keys(build),
|
|
49
|
+
key =>
|
|
50
|
+
Effect.gen(function* () {
|
|
51
|
+
if (!RESOLVABLE_BUILD_EXPORTS.has(key)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const value = build[key];
|
|
55
|
+
if (typeof value === 'function' && value.length === 0) {
|
|
56
|
+
const result = yield* tryPluginSync(() => value());
|
|
57
|
+
resolved[key] = isPromiseLike(result)
|
|
58
|
+
? yield* tryPluginPromise(() => result)
|
|
59
|
+
: result;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (isPromiseLike(value)) {
|
|
63
|
+
resolved[key] = yield* tryPluginPromise(() => value);
|
|
64
|
+
}
|
|
65
|
+
}),
|
|
66
|
+
{ discard: true }
|
|
67
|
+
).pipe(Effect.as(resolved));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isServerBuild(value: unknown): value is ServerBuild {
|
|
71
|
+
return Boolean(
|
|
72
|
+
isRecord(value) &&
|
|
73
|
+
isRecord(value.entry) &&
|
|
74
|
+
isRecord(value.entry.module) &&
|
|
75
|
+
typeof value.entry.module.default === 'function' &&
|
|
76
|
+
isRecord(value.routes) &&
|
|
77
|
+
isRecord(value.assets) &&
|
|
78
|
+
typeof value.assetsBuildDirectory === 'string' &&
|
|
79
|
+
(value.basename === undefined || typeof value.basename === 'string') &&
|
|
80
|
+
isRecord(value.future) &&
|
|
81
|
+
typeof value.isSpaMode === 'boolean' &&
|
|
82
|
+
Array.isArray(value.prerender) &&
|
|
83
|
+
typeof value.publicPath === 'string' &&
|
|
84
|
+
isRouteDiscovery(value.routeDiscovery) &&
|
|
85
|
+
typeof value.ssr === 'boolean'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveServerBuildCandidateEffect(
|
|
90
|
+
candidate: unknown
|
|
91
|
+
): Effect.Effect<ServerBuild | undefined, Error, never> {
|
|
92
|
+
if (!isRecord(candidate)) {
|
|
93
|
+
return Effect.succeed(undefined);
|
|
94
|
+
}
|
|
95
|
+
return resolveBuildExportsEffect(candidate).pipe(
|
|
96
|
+
Effect.map(resolved => (isServerBuild(resolved) ? resolved : undefined))
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function resolveServerBuildModuleEffect(
|
|
101
|
+
buildModule: unknown,
|
|
102
|
+
source: string
|
|
103
|
+
): Effect.Effect<ServerBuild, Error, never> {
|
|
104
|
+
return Effect.gen(function* () {
|
|
105
|
+
const moduleValue = isPromiseLike(buildModule)
|
|
106
|
+
? yield* tryPluginPromise(() => buildModule)
|
|
107
|
+
: buildModule;
|
|
108
|
+
const candidates = [() => moduleValue];
|
|
109
|
+
if (isRecord(moduleValue)) {
|
|
110
|
+
if ('default' in moduleValue) {
|
|
111
|
+
candidates.push(() => moduleValue.default);
|
|
112
|
+
}
|
|
113
|
+
if ('module.exports' in moduleValue) {
|
|
114
|
+
candidates.push(() => moduleValue['module.exports']);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const getCandidate of candidates) {
|
|
119
|
+
const candidate = yield* tryPluginPromise(() => getCandidate());
|
|
120
|
+
const serverBuild = yield* resolveServerBuildCandidateEffect(candidate);
|
|
121
|
+
if (serverBuild) {
|
|
122
|
+
return serverBuild;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return yield* Effect.fail(
|
|
126
|
+
new Error(
|
|
127
|
+
`[rsbuild-plugin-react-router] ${source} did not contain a valid React Router ServerBuild.`
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { resolve } from 'pathe';
|
|
2
|
+
import type { ServerBuild } from 'react-router';
|
|
3
|
+
import { runPluginEffect } from './effect-runtime.js';
|
|
4
|
+
import { resolveServerBuildModuleEffect } from './server-build-resolution.js';
|
|
5
|
+
import type { Route } from './types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generates the server build template string.
|
|
9
|
+
*
|
|
10
|
+
* Note: Federation mode used to require async `import()` wrappers for entrypoints.
|
|
11
|
+
* With Rspack/@module-federation support for `experiments.asyncStartup`, the
|
|
12
|
+
* server build can always use static imports.
|
|
13
|
+
*/
|
|
14
|
+
interface ServerBuildOptions {
|
|
15
|
+
entryServerPath: string;
|
|
16
|
+
assetsBuildDirectory: string;
|
|
17
|
+
basename: string;
|
|
18
|
+
appDirectory: string;
|
|
19
|
+
ssr: boolean;
|
|
20
|
+
future?: unknown;
|
|
21
|
+
allowedActionOrigins?: string[];
|
|
22
|
+
prerender?: string[];
|
|
23
|
+
publicPath?: string;
|
|
24
|
+
serverManifestId?: string;
|
|
25
|
+
routeDiscovery:
|
|
26
|
+
| {
|
|
27
|
+
mode: 'lazy';
|
|
28
|
+
manifestPath?: string;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
mode: 'initial';
|
|
32
|
+
}
|
|
33
|
+
| undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function generateStaticTemplate(
|
|
37
|
+
routes: Record<string, Route>,
|
|
38
|
+
options: ServerBuildOptions
|
|
39
|
+
): string {
|
|
40
|
+
const manifestId =
|
|
41
|
+
options.serverManifestId ?? 'virtual/react-router/server-manifest';
|
|
42
|
+
return `
|
|
43
|
+
import * as entryServer from ${JSON.stringify(options.entryServerPath)};
|
|
44
|
+
${Object.keys(routes)
|
|
45
|
+
.map((key, index) => {
|
|
46
|
+
const route = routes[key];
|
|
47
|
+
return `import * as route${index} from ${JSON.stringify(
|
|
48
|
+
`${resolve(options.appDirectory, route.file)}?react-router-route`
|
|
49
|
+
)};`;
|
|
50
|
+
})
|
|
51
|
+
.join('\n')}
|
|
52
|
+
|
|
53
|
+
export { default as assets } from ${JSON.stringify(manifestId)};
|
|
54
|
+
export const assetsBuildDirectory = ${JSON.stringify(
|
|
55
|
+
options.assetsBuildDirectory
|
|
56
|
+
)};
|
|
57
|
+
export const basename = ${JSON.stringify(options.basename)};
|
|
58
|
+
export const future = ${JSON.stringify(options.future ?? {})};
|
|
59
|
+
export const isSpaMode = ${!options.ssr};
|
|
60
|
+
export const ssr = ${options.ssr};
|
|
61
|
+
export const routeDiscovery = ${JSON.stringify(options.routeDiscovery)};
|
|
62
|
+
export const prerender = ${JSON.stringify(options.prerender ?? [])};
|
|
63
|
+
export const publicPath = ${JSON.stringify(options.publicPath ?? '/')};
|
|
64
|
+
export const entry = { module: entryServer };
|
|
65
|
+
export const allowedActionOrigins = ${JSON.stringify(options.allowedActionOrigins)};
|
|
66
|
+
export var routes = {};
|
|
67
|
+
${Object.keys(routes)
|
|
68
|
+
.map((key, index) => {
|
|
69
|
+
const route = routes[key];
|
|
70
|
+
return `routes[${JSON.stringify(key)}] = {
|
|
71
|
+
id: ${JSON.stringify(route.id)},
|
|
72
|
+
parentId: ${JSON.stringify(route.parentId)},
|
|
73
|
+
path: ${JSON.stringify(route.path)},
|
|
74
|
+
index: ${JSON.stringify(route.index)},
|
|
75
|
+
caseSensitive: ${JSON.stringify(route.caseSensitive)},
|
|
76
|
+
module: route${index}
|
|
77
|
+
};`;
|
|
78
|
+
})
|
|
79
|
+
.join('\n ')}
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generates the server build module content
|
|
85
|
+
* @param routes The route manifest
|
|
86
|
+
* @param options Build options
|
|
87
|
+
* @returns The generated module content as a string
|
|
88
|
+
*/
|
|
89
|
+
function generateServerBuild(
|
|
90
|
+
routes: Record<string, Route>,
|
|
91
|
+
options: ServerBuildOptions & { federation?: boolean }
|
|
92
|
+
): string {
|
|
93
|
+
return generateStaticTemplate(routes, options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function resolveServerBuildModule(
|
|
97
|
+
buildModule: unknown,
|
|
98
|
+
source: string
|
|
99
|
+
): Promise<ServerBuild> {
|
|
100
|
+
return runPluginEffect(resolveServerBuildModuleEffect(buildModule, source));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function resolveReactRouterServerBuild(
|
|
104
|
+
buildModule: unknown
|
|
105
|
+
): Promise<ServerBuild> {
|
|
106
|
+
return runPluginEffect(
|
|
107
|
+
resolveServerBuildModuleEffect(buildModule, 'Imported module')
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export { generateServerBuild };
|