rsbuild-plugin-react-router 0.5.0 → 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.
- package/README.md +58 -208
- package/dist/511.js +370 -331
- package/dist/build-output-transforms.d.ts +30 -6
- package/dist/classic-mode.d.ts +56 -0
- package/dist/config-imports.d.ts +8 -2
- package/dist/constants.d.ts +3 -0
- package/dist/dev-background-resources.d.ts +3 -1
- package/dist/dev-generation.d.ts +2 -3
- package/dist/dev-hmr.d.ts +8 -2
- package/dist/dev-runtime-controller.d.ts +6 -1
- package/dist/dev-source-maps.d.ts +4 -0
- package/dist/effect-runtime.d.ts +17 -5
- package/dist/entry-paths.d.ts +16 -0
- package/dist/environment-output.d.ts +6 -0
- package/dist/export-utils.d.ts +2 -1
- package/dist/index.cjs +3957 -2208
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3470 -1793
- package/dist/lazy-compilation-prewarm.d.ts +8 -5
- package/dist/manifest.d.ts +16 -4
- package/dist/mode-plan.d.ts +82 -0
- package/dist/modify-browser-manifest.d.ts +8 -4
- package/dist/plugin-utils.d.ts +27 -1
- package/dist/prerender-build.d.ts +5 -5
- package/dist/prerender.d.ts +1 -5
- package/dist/react-router-config.d.ts +11 -3
- package/dist/route-artifacts.d.ts +4 -2
- package/dist/route-chunks.d.ts +2 -1
- package/dist/route-component-transform.d.ts +0 -2
- package/dist/route-export-pruning.d.ts +4 -1
- package/dist/route-imports.d.ts +18 -0
- package/dist/route-transform-tasks.d.ts +5 -0
- package/dist/route-watch.d.ts +11 -6
- package/dist/rsc-dev-server.d.ts +26 -0
- package/dist/rsc-prerender.d.ts +54 -0
- package/dist/rsc-route-config.d.ts +6 -0
- package/dist/rsc-route-exports.d.ts +15 -0
- package/dist/rsc-route-transform-loader.cjs +43 -0
- package/dist/rsc-route-transform-loader.d.ts +30 -0
- package/dist/rsc-route-transform-loader.js +16 -0
- package/dist/rsc-route-transform-registration.d.ts +12 -0
- package/dist/rsc-route-transforms.d.ts +21 -0
- package/dist/rsc-support.d.ts +23 -0
- package/dist/rsc-virtual-modules.d.ts +19 -0
- package/dist/server-build-plan.d.ts +2 -1
- package/dist/server-build-resolution.d.ts +1 -2
- package/dist/server-utils.d.ts +4 -5
- package/dist/ssr-asset-relocation.d.ts +98 -0
- package/dist/templates/entry.rsc.client.d.ts +1 -0
- package/dist/templates/entry.rsc.client.js +61 -0
- package/dist/templates/entry.rsc.d.ts +9 -0
- package/dist/templates/entry.rsc.js +38 -0
- package/dist/templates/entry.rsc.ssr.d.ts +4 -0
- package/dist/templates/entry.rsc.ssr.js +24 -0
- package/dist/typegen.d.ts +4 -2
- package/dist/types.d.ts +30 -1
- package/package.json +69 -14
- package/src/build-output-transforms.ts +155 -21
- package/src/classic-mode.ts +253 -0
- package/src/config-imports.ts +153 -6
- package/src/constants.ts +6 -2
- package/src/dev-background-resources.ts +46 -85
- package/src/dev-generation.ts +52 -33
- package/src/dev-hmr.ts +112 -80
- package/src/dev-runtime-artifacts.ts +11 -12
- package/src/dev-runtime-controller.ts +75 -85
- package/src/dev-runtime-session.ts +14 -18
- package/src/dev-server.ts +2 -0
- package/src/dev-source-maps.ts +257 -0
- package/src/effect-runtime.ts +105 -57
- package/src/entry-paths.ts +80 -0
- package/src/environment-output.ts +55 -0
- package/src/export-utils.ts +15 -11
- package/src/index.ts +661 -496
- package/src/lazy-compilation-prewarm.ts +20 -4
- package/src/manifest.ts +157 -82
- package/src/mode-plan.ts +367 -0
- package/src/modify-browser-manifest.ts +130 -124
- package/src/plugin-utils.ts +103 -39
- package/src/prerender-build.ts +82 -104
- package/src/prerender.ts +23 -24
- package/src/react-router-config.ts +72 -26
- package/src/route-artifacts.ts +96 -66
- package/src/route-chunks.ts +167 -51
- package/src/route-component-transform.ts +14 -21
- package/src/route-export-pruning.ts +4 -3
- package/src/route-imports.ts +100 -0
- package/src/route-transform-tasks.ts +39 -25
- package/src/route-watch.ts +166 -188
- package/src/rsc-dev-server.ts +112 -0
- package/src/rsc-prerender.ts +362 -0
- package/src/rsc-route-config.ts +175 -0
- package/src/rsc-route-exports.ts +67 -0
- package/src/rsc-route-transform-loader.ts +65 -0
- package/src/rsc-route-transform-registration.ts +145 -0
- package/src/rsc-route-transforms.ts +995 -0
- package/src/rsc-runtime.d.ts +143 -0
- package/src/rsc-support.ts +116 -0
- package/src/rsc-virtual-modules.ts +113 -0
- package/src/server-build-plan.ts +14 -3
- package/src/server-build-resolution.ts +37 -47
- package/src/server-utils.ts +21 -35
- package/src/ssr-asset-relocation.ts +183 -0
- package/src/ssr-externals.ts +8 -26
- package/src/templates/entry.rsc.client.tsx +168 -0
- package/src/templates/entry.rsc.ssr.tsx +45 -0
- package/src/templates/entry.rsc.tsx +80 -0
- package/src/typegen.ts +40 -23
- package/src/types.ts +39 -1
- package/src/warnings/warn-on-client-source-maps.ts +6 -10
package/src/route-watch.ts
CHANGED
|
@@ -2,11 +2,12 @@ import { watch, type FSWatcher } from 'node:fs';
|
|
|
2
2
|
import { access, mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
3
3
|
import type { RsbuildConfig } from '@rsbuild/core';
|
|
4
4
|
import * as Effect from 'effect/Effect';
|
|
5
|
+
import type * as Scope from 'effect/Scope';
|
|
5
6
|
import { dirname, resolve } from 'pathe';
|
|
6
7
|
import { getCappedPluginConcurrency } from './concurrency.js';
|
|
7
8
|
import {
|
|
8
9
|
createDelayedPluginTask,
|
|
9
|
-
|
|
10
|
+
type PluginEffectRuntime,
|
|
10
11
|
tryPluginPromise,
|
|
11
12
|
} from './effect-runtime.js';
|
|
12
13
|
import type { Route } from './types.js';
|
|
@@ -47,6 +48,17 @@ type WatchDirectoryEntry = (
|
|
|
47
48
|
onError: (error: unknown) => void
|
|
48
49
|
) => DirectoryWatcher;
|
|
49
50
|
|
|
51
|
+
export type CreateRouteTopologyWatcherOptions = {
|
|
52
|
+
runtime: PluginEffectRuntime;
|
|
53
|
+
watchDirectory: string;
|
|
54
|
+
getRouteTopology: () => Promise<Set<string>>;
|
|
55
|
+
initialRouteTopology?: Set<string>;
|
|
56
|
+
restartMarkerPath: string;
|
|
57
|
+
onError: (error: unknown) => void;
|
|
58
|
+
onRouteTopologyChange?: () => void | Promise<void>;
|
|
59
|
+
watchDirectoryEntry?: WatchDirectoryEntry;
|
|
60
|
+
};
|
|
61
|
+
|
|
50
62
|
const defaultWatchDirectoryEntry: WatchDirectoryEntry = (
|
|
51
63
|
directory,
|
|
52
64
|
onChange,
|
|
@@ -70,8 +82,8 @@ export const mergeWatchFiles = (
|
|
|
70
82
|
] as WatchFilesConfig;
|
|
71
83
|
};
|
|
72
84
|
|
|
73
|
-
export const getRouteRestartMarkerPath = (
|
|
74
|
-
resolve(
|
|
85
|
+
export const getRouteRestartMarkerPath = (appDirectory: string): string =>
|
|
86
|
+
resolve(appDirectory, '..', ROUTE_RESTART_MARKER_ASSET);
|
|
75
87
|
|
|
76
88
|
export const createRouteManifestSnapshot = (
|
|
77
89
|
routes: RouteManifestSnapshotEntries
|
|
@@ -120,10 +132,6 @@ const areSetsEqual = <T>(left: Set<T>, right: Set<T>): boolean => {
|
|
|
120
132
|
return true;
|
|
121
133
|
};
|
|
122
134
|
|
|
123
|
-
const readRouteDirectories = (watchDirectory: string): Promise<Set<string>> => {
|
|
124
|
-
return runPluginEffect(readRouteDirectoriesEffect(watchDirectory));
|
|
125
|
-
};
|
|
126
|
-
|
|
127
135
|
const readRouteDirectoriesEffect = (
|
|
128
136
|
watchDirectory: string
|
|
129
137
|
): Effect.Effect<Set<string>, Error, never> => {
|
|
@@ -148,7 +156,8 @@ const readRouteDirectoriesEffect = (
|
|
|
148
156
|
return walkDirectory(watchDirectory).pipe(Effect.as(directories));
|
|
149
157
|
};
|
|
150
158
|
|
|
151
|
-
|
|
159
|
+
const createRouteTopologyWatcherEffect = ({
|
|
160
|
+
runtime,
|
|
152
161
|
watchDirectory,
|
|
153
162
|
getRouteTopology,
|
|
154
163
|
initialRouteTopology,
|
|
@@ -156,202 +165,171 @@ export const createRouteTopologyWatcher = async ({
|
|
|
156
165
|
onError,
|
|
157
166
|
onRouteTopologyChange,
|
|
158
167
|
watchDirectoryEntry: watchDirectoryOverride = defaultWatchDirectoryEntry,
|
|
159
|
-
}:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
discoveredState = {
|
|
168
|
+
}: CreateRouteTopologyWatcherOptions) =>
|
|
169
|
+
Effect.gen(function* () {
|
|
170
|
+
const discoveredDirectories =
|
|
171
|
+
yield* readRouteDirectoriesEffect(watchDirectory);
|
|
172
|
+
const routeTopology = yield* tryPluginPromise(getRouteTopology).pipe(
|
|
173
|
+
Effect.catchAll(error => {
|
|
174
|
+
if (!initialRouteTopology) return Effect.fail(error);
|
|
175
|
+
onError(error);
|
|
176
|
+
return Effect.succeed(initialRouteTopology);
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
const discoveredState: RouteDirectoryState = {
|
|
172
180
|
directories: discoveredDirectories,
|
|
173
|
-
routeTopology
|
|
181
|
+
routeTopology,
|
|
174
182
|
};
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
onError(error);
|
|
180
|
-
discoveredState = {
|
|
181
|
-
directories: discoveredDirectories,
|
|
182
|
-
routeTopology: initialRouteTopology,
|
|
183
|
+
let state = {
|
|
184
|
+
...discoveredState,
|
|
185
|
+
routeTopology: initialRouteTopology ?? discoveredState.routeTopology,
|
|
183
186
|
};
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
...discoveredState,
|
|
187
|
-
routeTopology: initialRouteTopology ?? discoveredState.routeTopology,
|
|
188
|
-
};
|
|
189
|
-
let closed = false;
|
|
190
|
-
let rescanQueue = Promise.resolve();
|
|
191
|
-
const directoryWatchers = new Map<string, DirectoryWatcher>();
|
|
187
|
+
let closed = false;
|
|
188
|
+
const directoryWatchers = new Map<string, DirectoryWatcher>();
|
|
192
189
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
};
|
|
190
|
+
const touchRestartMarkerEffect = (): Effect.Effect<void, Error, never> =>
|
|
191
|
+
tryPluginPromise(() =>
|
|
192
|
+
mkdir(dirname(restartMarkerPath), { recursive: true })
|
|
193
|
+
).pipe(
|
|
194
|
+
Effect.zipRight(
|
|
195
|
+
tryPluginPromise(() =>
|
|
196
|
+
writeFile(restartMarkerPath, String(Date.now()))
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
);
|
|
212
200
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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);
|
|
201
|
+
const closeRemovedDirectoryWatchers = (
|
|
202
|
+
nextDirectories: Set<string>
|
|
203
|
+
): void => {
|
|
204
|
+
for (const [directory, watcher] of directoryWatchers) {
|
|
205
|
+
if (!nextDirectories.has(directory)) {
|
|
206
|
+
watcher.close();
|
|
207
|
+
directoryWatchers.delete(directory);
|
|
208
|
+
}
|
|
234
209
|
}
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
const syncDirectoryWatchers = (nextDirectories: Set<string>): void => {
|
|
239
|
-
closeRemovedDirectoryWatchers(nextDirectories);
|
|
240
|
-
watchNewDirectories(nextDirectories);
|
|
241
|
-
};
|
|
210
|
+
};
|
|
242
211
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
212
|
+
const watchNewDirectories = (nextDirectories: Set<string>): void => {
|
|
213
|
+
for (const directory of nextDirectories) {
|
|
214
|
+
if (directoryWatchers.has(directory)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
let watcher: DirectoryWatcher;
|
|
219
|
+
watcher = watchDirectoryOverride(
|
|
220
|
+
directory,
|
|
221
|
+
() => {
|
|
222
|
+
if (!closed) rescanTask.reschedule();
|
|
223
|
+
},
|
|
224
|
+
error => {
|
|
225
|
+
if (closed) return;
|
|
226
|
+
if (directoryWatchers.get(directory) === watcher) {
|
|
227
|
+
watcher.close();
|
|
228
|
+
directoryWatchers.delete(directory);
|
|
229
|
+
}
|
|
261
230
|
onError(error);
|
|
262
231
|
}
|
|
263
|
-
|
|
232
|
+
);
|
|
233
|
+
directoryWatchers.set(directory, watcher);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
onError(error);
|
|
264
236
|
}
|
|
265
|
-
return touchRestartMarkerEffect().pipe(
|
|
266
|
-
Effect.zipRight(
|
|
267
|
-
Effect.sync(() => {
|
|
268
|
-
if (!closed) {
|
|
269
|
-
state = nextState;
|
|
270
|
-
}
|
|
271
|
-
})
|
|
272
|
-
)
|
|
273
|
-
);
|
|
274
237
|
}
|
|
275
|
-
|
|
276
|
-
return Effect.void;
|
|
277
|
-
});
|
|
238
|
+
};
|
|
278
239
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
240
|
+
const syncDirectoryWatchers = (nextDirectories: Set<string>): void => {
|
|
241
|
+
closeRemovedDirectoryWatchers(nextDirectories);
|
|
242
|
+
watchNewDirectories(nextDirectories);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const applyNextStateEffect = (
|
|
246
|
+
nextState: RouteDirectoryState
|
|
247
|
+
): Effect.Effect<void, Error, never> =>
|
|
248
|
+
Effect.suspend(() => {
|
|
249
|
+
if (closed) return Effect.void;
|
|
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
|
+
});
|
|
302
264
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
});
|
|
308
278
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
279
|
+
const runRescanEffect = (): Effect.Effect<void, never, never> => {
|
|
280
|
+
let nextDirectories: Set<string> | undefined;
|
|
281
|
+
return Effect.gen(function* () {
|
|
282
|
+
if (closed) return;
|
|
283
|
+
nextDirectories = yield* readRouteDirectoriesEffect(watchDirectory);
|
|
284
|
+
if (closed) return;
|
|
285
|
+
const nextState = {
|
|
286
|
+
directories: nextDirectories,
|
|
287
|
+
routeTopology: yield* tryPluginPromise(getRouteTopology),
|
|
288
|
+
};
|
|
289
|
+
if (closed) return;
|
|
290
|
+
yield* applyNextStateEffect(nextState);
|
|
291
|
+
}).pipe(
|
|
292
|
+
Effect.catchAll(error =>
|
|
293
|
+
Effect.sync(() => {
|
|
294
|
+
if (nextDirectories && !closed)
|
|
295
|
+
syncDirectoryWatchers(nextDirectories);
|
|
296
|
+
onError(error);
|
|
297
|
+
})
|
|
298
|
+
)
|
|
299
|
+
);
|
|
300
|
+
};
|
|
316
301
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
closed ? Effect.void :
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
});
|
|
302
|
+
const rescanTask = createDelayedPluginTask({
|
|
303
|
+
runtime,
|
|
304
|
+
delayMs: ROUTE_TOPOLOGY_RESCAN_DEBOUNCE_MS,
|
|
305
|
+
run: () =>
|
|
306
|
+
Effect.suspend(() => (closed ? Effect.void : runRescanEffect())),
|
|
307
|
+
onError,
|
|
308
|
+
});
|
|
325
309
|
|
|
326
|
-
|
|
327
|
-
|
|
310
|
+
const closeEffect = (): Effect.Effect<void, Error> =>
|
|
311
|
+
tryPluginPromise(() => {
|
|
312
|
+
if (closed) return;
|
|
313
|
+
closed = true;
|
|
314
|
+
for (const watcher of directoryWatchers.values()) {
|
|
315
|
+
watcher.close();
|
|
316
|
+
}
|
|
317
|
+
directoryWatchers.clear();
|
|
318
|
+
}).pipe(Effect.ensuring(rescanTask.cancelEffect()));
|
|
328
319
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
320
|
+
yield* applyNextStateEffect(discoveredState).pipe(
|
|
321
|
+
Effect.catchAll(error => Effect.sync(() => onError(error))),
|
|
322
|
+
Effect.onInterrupt(() => Effect.ignore(closeEffect()))
|
|
323
|
+
);
|
|
332
324
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
} catch (error) {
|
|
336
|
-
onError(error);
|
|
337
|
-
}
|
|
325
|
+
return closeEffect;
|
|
326
|
+
});
|
|
338
327
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
};
|
|
328
|
+
export const acquireRouteTopologyWatcher = (
|
|
329
|
+
options: CreateRouteTopologyWatcherOptions
|
|
330
|
+
): Effect.Effect<() => Effect.Effect<void, Error>, Error, Scope.Scope> =>
|
|
331
|
+
Effect.acquireRelease(createRouteTopologyWatcherEffect(options), close =>
|
|
332
|
+
close().pipe(
|
|
333
|
+
Effect.catchAll(error => Effect.sync(() => options.onError(error)))
|
|
334
|
+
)
|
|
335
|
+
);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createRequestListener } from '@remix-run/node-fetch-server';
|
|
2
|
+
import type { RsbuildConfig } from '@rsbuild/core';
|
|
3
|
+
import { installDevServerSourceMapSupport } from './dev-source-maps.js';
|
|
4
|
+
import { escapeHtml } from './plugin-utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Turns an error thrown by the RSC server build into a source-mapped 500 HTML
|
|
8
|
+
* document. `@remix-run/node-fetch-server`'s request listener catches handler
|
|
9
|
+
* throws internally and, without an `onError`, emits a bare 500 with no body
|
|
10
|
+
* markup. React Router's classic dev path instead returns an ErrorBoundary
|
|
11
|
+
* document whose `<main>` carries the stack, and the integration test asserts
|
|
12
|
+
* on that `<main>`. Reading `error.stack` here triggers the installed
|
|
13
|
+
* `prepareStackTrace`, remapping generated `.js` frames back to the original
|
|
14
|
+
* `.tsx` source.
|
|
15
|
+
*/
|
|
16
|
+
const renderDevServerError = (error: unknown): Response => {
|
|
17
|
+
const stack =
|
|
18
|
+
error instanceof Error ? (error.stack ?? String(error)) : String(error);
|
|
19
|
+
return new Response(
|
|
20
|
+
`<!doctype html><html><body><main><pre>${escapeHtml(
|
|
21
|
+
stack
|
|
22
|
+
)}</pre></main></body></html>`,
|
|
23
|
+
{ status: 500, headers: { 'content-type': 'text/html' } }
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type RscServerBuild = {
|
|
28
|
+
default?: {
|
|
29
|
+
fetch?: (request: Request) => Promise<Response>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type RscDevServerSetup = NonNullable<
|
|
34
|
+
NonNullable<RsbuildConfig['server']>['setup']
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
type RscDevServerSetupOptions = {
|
|
38
|
+
entryName: string;
|
|
39
|
+
pluginName: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Decides whether a dev-server request should skip the RSC request handler
|
|
44
|
+
* and fall through to the rest of the middleware chain.
|
|
45
|
+
*
|
|
46
|
+
* Only Rsbuild-internal endpoints (`/__rsbuild_*`, e.g. HMR web socket
|
|
47
|
+
* fallbacks) are bypassed. Everything else — any method, any pathname,
|
|
48
|
+
* with or without a file extension — is handled by the RSC server build,
|
|
49
|
+
* matching production where the RSC server handles every request.
|
|
50
|
+
*
|
|
51
|
+
* Static assets need no bypass here: the RSC middleware is registered via
|
|
52
|
+
* the callback returned from `server.setup`, which Rsbuild invokes only
|
|
53
|
+
* after its built-in middlewares (compiled assets, public dir) are
|
|
54
|
+
* registered. A request that reaches this middleware was already declined
|
|
55
|
+
* by static asset serving.
|
|
56
|
+
*/
|
|
57
|
+
export const shouldBypassRscDevRequest = (request: {
|
|
58
|
+
url?: string;
|
|
59
|
+
}): boolean => {
|
|
60
|
+
if (!request.url) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
const { pathname } = new URL(request.url, 'http://localhost');
|
|
64
|
+
return pathname.startsWith('/__rsbuild_');
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function createReactRouterRscDevServerSetup({
|
|
68
|
+
entryName,
|
|
69
|
+
pluginName,
|
|
70
|
+
}: RscDevServerSetupOptions): RscDevServerSetup {
|
|
71
|
+
return context => {
|
|
72
|
+
if (context.action === 'preview') {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const { server } = context;
|
|
76
|
+
// Remap generated `.js` stack frames back to original `.tsx` sources so a
|
|
77
|
+
// loader/render error surfaced below carries source-mapped locations, the
|
|
78
|
+
// same support the classic dev path installs.
|
|
79
|
+
installDevServerSourceMapSupport();
|
|
80
|
+
const listener = createRequestListener(
|
|
81
|
+
async request => {
|
|
82
|
+
const build =
|
|
83
|
+
await server.environments.node.loadBundle<RscServerBuild>(entryName);
|
|
84
|
+
const handler = build.default?.fetch;
|
|
85
|
+
if (typeof handler !== 'function') {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`[${pluginName}] RSC server build must default-export an object with a fetch function.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return handler(request);
|
|
91
|
+
},
|
|
92
|
+
{ onError: renderDevServerError }
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Register the RSC middleware in the callback returned from
|
|
96
|
+
// `server.setup`. Rsbuild runs middlewares registered synchronously in
|
|
97
|
+
// `setup` BEFORE its built-ins, but runs returned callbacks AFTER
|
|
98
|
+
// built-in middlewares (compiled assets, public dir) are registered.
|
|
99
|
+
// Registering here makes the RSC handler the fallback for every request
|
|
100
|
+
// that static asset serving did not handle — the same routing semantics
|
|
101
|
+
// as production, where the RSC server handles everything.
|
|
102
|
+
return () => {
|
|
103
|
+
server.middlewares.use((req, res, next) => {
|
|
104
|
+
if (shouldBypassRscDevRequest(req)) {
|
|
105
|
+
next();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
Promise.resolve(listener(req, res)).catch(next);
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
}
|