rsbuild-plugin-react-router 0.3.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 +55 -24
- package/dist/451.js +36 -20
- package/dist/build-manifest.d.ts +6 -3
- package/dist/concurrency.d.ts +2 -1
- package/dist/config-imports.d.ts +7 -0
- package/dist/dev-background-resources.d.ts +38 -0
- package/dist/dev-generation.d.ts +2 -2
- package/dist/dev-runtime-artifacts.d.ts +9 -1
- package/dist/dev-runtime-compilation.d.ts +17 -2
- package/dist/dev-server.d.ts +5 -0
- package/dist/effect-runtime.d.ts +18 -0
- package/dist/export-utils.d.ts +2 -2
- package/dist/index.cjs +11448 -891
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7950 -758
- package/dist/lazy-compilation-prewarm.d.ts +25 -0
- package/dist/lazy-compilation.d.ts +2 -1
- package/dist/manifest.d.ts +19 -4
- package/dist/parallel-route-transforms.d.ts +17 -2
- package/dist/prerender-build.d.ts +4 -0
- package/dist/prerender.d.ts +0 -1
- package/dist/react-router-config.d.ts +8 -5
- package/dist/server-build-resolution.d.ts +3 -0
- package/dist/ssr-externals.d.ts +1 -0
- package/dist/typegen.d.ts +14 -1
- package/dist/types.d.ts +15 -6
- package/package.json +11 -9
- package/src/build-manifest.ts +110 -73
- package/src/concurrency.ts +3 -22
- package/src/config-imports.ts +38 -0
- package/src/dev-background-resources.ts +255 -0
- package/src/dev-generation.ts +43 -53
- package/src/dev-runtime-artifacts.ts +50 -18
- package/src/dev-runtime-compilation.ts +80 -1
- package/src/dev-runtime-controller.ts +111 -31
- package/src/dev-runtime-session.ts +18 -11
- package/src/dev-server.ts +31 -1
- package/src/effect-runtime.ts +130 -0
- package/src/export-utils.ts +82 -23
- package/src/index.ts +118 -153
- package/src/lazy-compilation-prewarm.ts +279 -0
- package/src/lazy-compilation.ts +12 -5
- package/src/manifest.ts +366 -255
- package/src/modify-browser-manifest.ts +2 -1
- package/src/parallel-route-transforms.ts +195 -69
- package/src/prerender-build.ts +125 -61
- package/src/prerender.ts +0 -18
- package/src/react-router-config.ts +98 -73
- package/src/route-artifacts.ts +2 -2
- package/src/route-export-resolution.ts +3 -3
- package/src/route-watch.ts +119 -84
- package/src/server-build-resolution.ts +131 -0
- package/src/server-utils.ts +7 -106
- package/src/ssr-externals.ts +1 -1
- package/src/typegen.ts +162 -33
- package/src/types.ts +16 -6
|
@@ -4,6 +4,9 @@ import type {
|
|
|
4
4
|
} from '@react-router/dev/config';
|
|
5
5
|
import type { NormalizedConfig } from '@rsbuild/core';
|
|
6
6
|
import type { RouteConfigEntry } from '@react-router/dev/routes';
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
8
|
+
import { getCappedPluginConcurrency } from './concurrency.js';
|
|
9
|
+
import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
|
|
7
10
|
|
|
8
11
|
export type BuildEndHook = {
|
|
9
12
|
bivarianceHack(args: {
|
|
@@ -45,6 +48,12 @@ type RouteManifestEntry = {
|
|
|
45
48
|
|
|
46
49
|
type RouteManifest = Record<string, RouteManifestEntry>;
|
|
47
50
|
|
|
51
|
+
type ResolveReactRouterConfigResult = {
|
|
52
|
+
resolved: ResolvedReactRouterConfig;
|
|
53
|
+
presets: NonNullable<Config['presets']>;
|
|
54
|
+
hasConfiguredServerModuleFormat: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
48
57
|
export type ResolvedReactRouterConfig = Readonly<{
|
|
49
58
|
appDirectory: string;
|
|
50
59
|
basename: string;
|
|
@@ -102,10 +111,15 @@ const mergeReactRouterConfig = (...configs: Config[]): Config => {
|
|
|
102
111
|
buildEnd: async (
|
|
103
112
|
...args: Parameters<NonNullable<Config['buildEnd']>>
|
|
104
113
|
) => {
|
|
105
|
-
await
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
await runPluginEffect(
|
|
115
|
+
Effect.all(
|
|
116
|
+
[
|
|
117
|
+
tryPluginPromise(() => configA.buildEnd?.(...args)),
|
|
118
|
+
tryPluginPromise(() => configB.buildEnd?.(...args)),
|
|
119
|
+
],
|
|
120
|
+
{ discard: true }
|
|
121
|
+
)
|
|
122
|
+
);
|
|
109
123
|
},
|
|
110
124
|
}
|
|
111
125
|
: {}),
|
|
@@ -145,76 +159,87 @@ const normalizeSubResourceIntegrity = (config: Config): Config => {
|
|
|
145
159
|
};
|
|
146
160
|
};
|
|
147
161
|
|
|
148
|
-
export const
|
|
162
|
+
export const resolveReactRouterConfigEffect = (
|
|
149
163
|
reactRouterUserConfig: Config
|
|
150
|
-
):
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
164
|
+
): Effect.Effect<ResolveReactRouterConfigResult, Error, never> =>
|
|
165
|
+
Effect.gen(function* () {
|
|
166
|
+
const presets = yield* Effect.forEach(
|
|
167
|
+
reactRouterUserConfig.presets ?? [],
|
|
168
|
+
preset =>
|
|
169
|
+
Effect.gen(function* () {
|
|
170
|
+
if (!preset.name) {
|
|
171
|
+
return yield* Effect.fail(
|
|
172
|
+
new Error(
|
|
173
|
+
'React Router presets must have a `name` property defined.'
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (!preset.reactRouterConfig) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const { buildEnd: _buildEnd, ...reactRouterUserConfigForPreset } =
|
|
181
|
+
reactRouterUserConfig;
|
|
182
|
+
const presetConfig = yield* tryPluginPromise(() =>
|
|
183
|
+
preset.reactRouterConfig?.({
|
|
184
|
+
reactRouterUserConfig: reactRouterUserConfigForPreset,
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
if (!presetConfig) return null;
|
|
188
|
+
const { presets: _presets, ...rest } = presetConfig as Config;
|
|
189
|
+
return rest;
|
|
190
|
+
}),
|
|
191
|
+
{ concurrency: getCappedPluginConcurrency() }
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const userAndPresetConfigs = mergeReactRouterConfig(
|
|
195
|
+
...(presets.filter(Boolean) as Config[]).map(
|
|
196
|
+
normalizeSubResourceIntegrity
|
|
197
|
+
),
|
|
198
|
+
normalizeSubResourceIntegrity(reactRouterUserConfig)
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const subResourceIntegrity =
|
|
202
|
+
userAndPresetConfigs.subResourceIntegrity ??
|
|
203
|
+
userAndPresetConfigs.future?.unstable_subResourceIntegrity ??
|
|
204
|
+
DEFAULT_CONFIG.subResourceIntegrity;
|
|
205
|
+
const resolvedFuture: FutureConfig = {
|
|
206
|
+
...DEFAULT_CONFIG.future,
|
|
207
|
+
...(userAndPresetConfigs.future ?? {}),
|
|
208
|
+
unstable_subResourceIntegrity: subResourceIntegrity,
|
|
209
|
+
};
|
|
210
|
+
const splitRouteModules =
|
|
211
|
+
userAndPresetConfigs.splitRouteModules ??
|
|
212
|
+
userAndPresetConfigs.future?.v8_splitRouteModules ??
|
|
213
|
+
DEFAULT_CONFIG.splitRouteModules;
|
|
214
|
+
|
|
215
|
+
let resolved: ResolvedReactRouterConfig = {
|
|
216
|
+
...DEFAULT_CONFIG,
|
|
217
|
+
...userAndPresetConfigs,
|
|
218
|
+
future: resolvedFuture,
|
|
219
|
+
splitRouteModules,
|
|
220
|
+
subResourceIntegrity,
|
|
221
|
+
allowedActionOrigins:
|
|
222
|
+
userAndPresetConfigs.allowedActionOrigins ??
|
|
223
|
+
DEFAULT_CONFIG.allowedActionOrigins,
|
|
224
|
+
routes: DEFAULT_CONFIG.routes,
|
|
225
|
+
unstable_routeConfig: DEFAULT_CONFIG.unstable_routeConfig,
|
|
226
|
+
};
|
|
227
|
+
if (!resolved.ssr) {
|
|
228
|
+
resolved = {
|
|
229
|
+
...resolved,
|
|
230
|
+
serverBundles: undefined,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
180
233
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
...DEFAULT_CONFIG.future,
|
|
187
|
-
...(userAndPresetConfigs.future ?? {}),
|
|
188
|
-
unstable_subResourceIntegrity: subResourceIntegrity,
|
|
189
|
-
};
|
|
190
|
-
const splitRouteModules =
|
|
191
|
-
userAndPresetConfigs.splitRouteModules ??
|
|
192
|
-
userAndPresetConfigs.future?.v8_splitRouteModules ??
|
|
193
|
-
DEFAULT_CONFIG.splitRouteModules;
|
|
194
|
-
|
|
195
|
-
let resolved: ResolvedReactRouterConfig = {
|
|
196
|
-
...DEFAULT_CONFIG,
|
|
197
|
-
...userAndPresetConfigs,
|
|
198
|
-
future: resolvedFuture,
|
|
199
|
-
splitRouteModules,
|
|
200
|
-
subResourceIntegrity,
|
|
201
|
-
allowedActionOrigins:
|
|
202
|
-
userAndPresetConfigs.allowedActionOrigins ??
|
|
203
|
-
DEFAULT_CONFIG.allowedActionOrigins,
|
|
204
|
-
routes: DEFAULT_CONFIG.routes,
|
|
205
|
-
unstable_routeConfig: DEFAULT_CONFIG.unstable_routeConfig,
|
|
206
|
-
};
|
|
207
|
-
if (!resolved.ssr) {
|
|
208
|
-
resolved = {
|
|
209
|
-
...resolved,
|
|
210
|
-
serverBundles: undefined,
|
|
234
|
+
return {
|
|
235
|
+
resolved,
|
|
236
|
+
presets: reactRouterUserConfig.presets ?? [],
|
|
237
|
+
hasConfiguredServerModuleFormat:
|
|
238
|
+
userAndPresetConfigs.serverModuleFormat !== undefined,
|
|
211
239
|
};
|
|
212
|
-
}
|
|
240
|
+
});
|
|
213
241
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
userAndPresetConfigs.serverModuleFormat !== undefined,
|
|
219
|
-
};
|
|
220
|
-
};
|
|
242
|
+
export const resolveReactRouterConfig = (
|
|
243
|
+
reactRouterUserConfig: Config
|
|
244
|
+
): Promise<ResolveReactRouterConfigResult> =>
|
|
245
|
+
runPluginEffect(resolveReactRouterConfigEffect(reactRouterUserConfig));
|
package/src/route-artifacts.ts
CHANGED
|
@@ -92,7 +92,7 @@ export const createRouteClientEntryArtifact = async ({
|
|
|
92
92
|
)
|
|
93
93
|
: null;
|
|
94
94
|
const exportNames =
|
|
95
|
-
routeChunkInfo?.exportNames ?? (await getExportNames(code));
|
|
95
|
+
routeChunkInfo?.exportNames ?? (await getExportNames(code, resourcePath));
|
|
96
96
|
const chunkedExports = routeChunkInfo?.chunkedExports ?? [];
|
|
97
97
|
return {
|
|
98
98
|
code: buildRouteClientEntryCode({
|
|
@@ -140,7 +140,7 @@ export const createRouteChunkArtifact = async ({
|
|
|
140
140
|
);
|
|
141
141
|
|
|
142
142
|
if (splitRouteModules === 'enforce' && chunkName === 'main' && chunk) {
|
|
143
|
-
const exportNames = await getExportNames(chunk);
|
|
143
|
+
const exportNames = await getExportNames(chunk, resourcePath);
|
|
144
144
|
validateRouteChunks({
|
|
145
145
|
config: routeChunkConfig,
|
|
146
146
|
id: resourcePath,
|
|
@@ -264,9 +264,9 @@ export type RouteModuleResolver = (
|
|
|
264
264
|
export const createBundlerRouteExportResolver =
|
|
265
265
|
(resolveModule: RouteModuleResolver): RouteExportResolver =>
|
|
266
266
|
(specifier, importerPath) =>
|
|
267
|
-
new Promise<string | null>(
|
|
267
|
+
new Promise<string | null>(resolvePromise => {
|
|
268
268
|
resolveModule(dirname(importerPath), specifier, (error, resolved) => {
|
|
269
|
-
|
|
269
|
+
resolvePromise(error || !resolved ? null : resolved);
|
|
270
270
|
});
|
|
271
271
|
});
|
|
272
272
|
|
|
@@ -276,7 +276,7 @@ export const collectClientOnlyStubExportNames = async (
|
|
|
276
276
|
resolveModule: RouteExportResolver = resolveExportAllModule
|
|
277
277
|
): Promise<Set<string>> => {
|
|
278
278
|
const { exportNames: directExportNames, exportAllModules } =
|
|
279
|
-
await getExportNamesAndExportAll(code);
|
|
279
|
+
await getExportNamesAndExportAll(code, resourcePath);
|
|
280
280
|
const exportNames = new Set(directExportNames);
|
|
281
281
|
const unresolvedExportAll = new Set<string>();
|
|
282
282
|
const visitedModules = new Set<string>();
|
package/src/route-watch.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
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
|
+
import * as Effect from 'effect/Effect';
|
|
4
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';
|
|
5
12
|
import type { Route } from './types.js';
|
|
6
13
|
|
|
7
14
|
const ROUTE_RESTART_MARKER_ASSET = '.react-router/route-watch';
|
|
8
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();
|
|
9
18
|
|
|
10
19
|
type RouteManifestSnapshotEntry = Pick<
|
|
11
20
|
Route,
|
|
@@ -111,32 +120,32 @@ const areSetsEqual = <T>(left: Set<T>, right: Set<T>): boolean => {
|
|
|
111
120
|
return true;
|
|
112
121
|
};
|
|
113
122
|
|
|
114
|
-
const readRouteDirectories =
|
|
123
|
+
const readRouteDirectories = (watchDirectory: string): Promise<Set<string>> => {
|
|
124
|
+
return runPluginEffect(readRouteDirectoriesEffect(watchDirectory));
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const readRouteDirectoriesEffect = (
|
|
115
128
|
watchDirectory: string
|
|
116
|
-
):
|
|
129
|
+
): Effect.Effect<Set<string>, Error, never> => {
|
|
117
130
|
const directories = new Set<string>();
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
})
|
|
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
|
+
)
|
|
135
146
|
);
|
|
136
|
-
};
|
|
137
147
|
|
|
138
|
-
|
|
139
|
-
return directories;
|
|
148
|
+
return walkDirectory(watchDirectory).pipe(Effect.as(directories));
|
|
140
149
|
};
|
|
141
150
|
|
|
142
151
|
export const createRouteTopologyWatcher = async ({
|
|
@@ -178,14 +187,17 @@ export const createRouteTopologyWatcher = async ({
|
|
|
178
187
|
routeTopology: initialRouteTopology ?? discoveredState.routeTopology,
|
|
179
188
|
};
|
|
180
189
|
let closed = false;
|
|
181
|
-
let rescanTimer: ReturnType<typeof setTimeout> | undefined;
|
|
182
190
|
let rescanQueue = Promise.resolve();
|
|
183
191
|
const directoryWatchers = new Map<string, DirectoryWatcher>();
|
|
184
192
|
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
);
|
|
189
201
|
|
|
190
202
|
const closeRemovedDirectoryWatchers = (
|
|
191
203
|
nextDirectories: Set<string>
|
|
@@ -205,13 +217,17 @@ export const createRouteTopologyWatcher = async ({
|
|
|
205
217
|
}
|
|
206
218
|
try {
|
|
207
219
|
let watcher: DirectoryWatcher;
|
|
208
|
-
watcher = watchDirectoryOverride(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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);
|
|
212
229
|
}
|
|
213
|
-
|
|
214
|
-
});
|
|
230
|
+
);
|
|
215
231
|
directoryWatchers.set(directory, watcher);
|
|
216
232
|
} catch (error) {
|
|
217
233
|
onError(error);
|
|
@@ -224,74 +240,94 @@ export const createRouteTopologyWatcher = async ({
|
|
|
224
240
|
watchNewDirectories(nextDirectories);
|
|
225
241
|
};
|
|
226
242
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
}
|
|
243
|
+
const applyNextStateEffect = (
|
|
244
|
+
nextState: RouteDirectoryState
|
|
245
|
+
): Effect.Effect<void, Error, never> =>
|
|
246
|
+
Effect.suspend(() => {
|
|
247
247
|
if (closed) {
|
|
248
|
-
return;
|
|
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
|
+
);
|
|
249
274
|
}
|
|
250
275
|
state = nextState;
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
state = nextState;
|
|
254
|
-
};
|
|
276
|
+
return Effect.void;
|
|
277
|
+
});
|
|
255
278
|
|
|
256
|
-
const
|
|
257
|
-
if (closed) {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
279
|
+
const runRescanEffect = (): Effect.Effect<void, never, never> => {
|
|
260
280
|
let nextDirectories: Set<string> | undefined;
|
|
261
|
-
|
|
262
|
-
|
|
281
|
+
return Effect.gen(function* () {
|
|
282
|
+
if (closed) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
nextDirectories = yield* readRouteDirectoriesEffect(watchDirectory);
|
|
263
286
|
if (closed) {
|
|
264
287
|
return;
|
|
265
288
|
}
|
|
266
289
|
const nextState = {
|
|
267
290
|
directories: nextDirectories,
|
|
268
|
-
routeTopology:
|
|
291
|
+
routeTopology: yield* tryPluginPromise(getRouteTopology),
|
|
269
292
|
};
|
|
270
293
|
if (closed) {
|
|
271
294
|
return;
|
|
272
295
|
}
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
+
);
|
|
280
307
|
};
|
|
281
308
|
|
|
282
309
|
const rescan = (): Promise<void> => {
|
|
283
|
-
rescanQueue = rescanQueue.then(
|
|
310
|
+
rescanQueue = rescanQueue.then(
|
|
311
|
+
() => runPluginEffect(runRescanEffect()),
|
|
312
|
+
() => runPluginEffect(runRescanEffect())
|
|
313
|
+
);
|
|
284
314
|
return rescanQueue;
|
|
285
315
|
};
|
|
286
316
|
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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));
|
|
295
331
|
};
|
|
296
332
|
|
|
297
333
|
try {
|
|
@@ -302,13 +338,12 @@ export const createRouteTopologyWatcher = async ({
|
|
|
302
338
|
|
|
303
339
|
return async () => {
|
|
304
340
|
if (closed) {
|
|
341
|
+
await cancelScheduledRescan();
|
|
305
342
|
await rescanQueue;
|
|
306
343
|
return;
|
|
307
344
|
}
|
|
308
345
|
closed = true;
|
|
309
|
-
|
|
310
|
-
clearTimeout(rescanTimer);
|
|
311
|
-
}
|
|
346
|
+
await cancelScheduledRescan();
|
|
312
347
|
for (const watcher of directoryWatchers.values()) {
|
|
313
348
|
watcher.close();
|
|
314
349
|
}
|
|
@@ -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
|
+
}
|