rsbuild-plugin-react-router 0.5.0 → 0.6.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 +58 -208
- package/dist/511.js +371 -332
- 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 +3964 -2217
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3474 -1799
- package/dist/lazy-compilation-prewarm.d.ts +8 -5
- package/dist/manifest.d.ts +17 -5
- 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 +8 -2
- 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 +258 -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 +186 -100
- package/src/mode-plan.ts +367 -0
- package/src/modify-browser-manifest.ts +131 -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 +196 -54
- 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
|
@@ -310,6 +310,41 @@ const buildComponentRefreshRegistrations = (names: string[]): string => {
|
|
|
310
310
|
return `\nif (typeof $RefreshReg$ === 'function') {\n${registrations}\n}\n`;
|
|
311
311
|
};
|
|
312
312
|
|
|
313
|
+
export const validateSpaModeRouteExports = ({
|
|
314
|
+
exportNames,
|
|
315
|
+
resourcePath,
|
|
316
|
+
rootRoutePath,
|
|
317
|
+
}: {
|
|
318
|
+
exportNames: readonly string[];
|
|
319
|
+
resourcePath: string;
|
|
320
|
+
rootRoutePath: string | null;
|
|
321
|
+
}): void => {
|
|
322
|
+
const isRootRoute = resourcePath === rootRoutePath;
|
|
323
|
+
const relativePath = relative(process.cwd(), resourcePath);
|
|
324
|
+
const invalidServerOnly = exportNames.filter(exp => {
|
|
325
|
+
if (isRootRoute && exp === 'loader') return false;
|
|
326
|
+
return SERVER_ONLY_ROUTE_EXPORTS_SET.has(exp);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (invalidServerOnly.length > 0) {
|
|
330
|
+
const list = invalidServerOnly.map(exp => `\`${exp}\``).join(', ');
|
|
331
|
+
throw new Error(
|
|
332
|
+
`SPA Mode: ${invalidServerOnly.length} invalid route export(s) in ` +
|
|
333
|
+
`\`${relativePath}\`: ${list}. ` +
|
|
334
|
+
`See https://reactrouter.com/how-to/spa for more information.`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!isRootRoute && exportNames.includes('HydrateFallback')) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`SPA Mode: Invalid \`HydrateFallback\` export found in ` +
|
|
341
|
+
`\`${relativePath}\`. ` +
|
|
342
|
+
`\`HydrateFallback\` is only permitted on the root route in SPA Mode. ` +
|
|
343
|
+
`See https://reactrouter.com/how-to/spa for more information.`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
313
348
|
const transformRouteModule = async (
|
|
314
349
|
task: RouteModuleTransformTask
|
|
315
350
|
): Promise<RouteTransformResult> => {
|
|
@@ -325,32 +360,11 @@ const transformRouteModule = async (
|
|
|
325
360
|
|
|
326
361
|
const ast = parse(code, { sourceType: 'module' });
|
|
327
362
|
if (task.environmentName === 'web' && !task.ssr && task.isSpaMode) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const invalidServerOnly = resolvedExportNames.filter(exp => {
|
|
333
|
-
if (isRootRoute && exp === 'loader') return false;
|
|
334
|
-
return SERVER_ONLY_ROUTE_EXPORTS_SET.has(exp);
|
|
363
|
+
validateSpaModeRouteExports({
|
|
364
|
+
exportNames: collectProgramExportNames(getProgram(ast)),
|
|
365
|
+
resourcePath: task.resourcePath,
|
|
366
|
+
rootRoutePath: task.rootRoutePath,
|
|
335
367
|
});
|
|
336
|
-
|
|
337
|
-
if (invalidServerOnly.length > 0) {
|
|
338
|
-
const list = invalidServerOnly.map(e => `\`${e}\``).join(', ');
|
|
339
|
-
throw new Error(
|
|
340
|
-
`SPA Mode: ${invalidServerOnly.length} invalid route export(s) in ` +
|
|
341
|
-
`\`${relativePath}\`: ${list}. ` +
|
|
342
|
-
`See https://reactrouter.com/how-to/spa for more information.`
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (!isRootRoute && resolvedExportNames.includes('HydrateFallback')) {
|
|
347
|
-
throw new Error(
|
|
348
|
-
`SPA Mode: Invalid \`HydrateFallback\` export found in ` +
|
|
349
|
-
`\`${relativePath}\`. ` +
|
|
350
|
-
`\`HydrateFallback\` is only permitted on the root route in SPA Mode. ` +
|
|
351
|
-
`See https://reactrouter.com/how-to/spa for more information.`
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
368
|
}
|
|
355
369
|
|
|
356
370
|
const removedServerOnlyExports =
|
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
|
+
}
|