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.
Files changed (56) hide show
  1. package/README.md +55 -24
  2. package/dist/451.js +36 -20
  3. package/dist/build-manifest.d.ts +6 -3
  4. package/dist/concurrency.d.ts +2 -1
  5. package/dist/config-imports.d.ts +7 -0
  6. package/dist/dev-background-resources.d.ts +38 -0
  7. package/dist/dev-generation.d.ts +2 -2
  8. package/dist/dev-runtime-artifacts.d.ts +9 -1
  9. package/dist/dev-runtime-compilation.d.ts +17 -2
  10. package/dist/dev-server.d.ts +5 -0
  11. package/dist/effect-runtime.d.ts +18 -0
  12. package/dist/export-utils.d.ts +2 -2
  13. package/dist/index.cjs +11448 -891
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.js +7950 -758
  16. package/dist/lazy-compilation-prewarm.d.ts +25 -0
  17. package/dist/lazy-compilation.d.ts +2 -1
  18. package/dist/manifest.d.ts +19 -4
  19. package/dist/parallel-route-transforms.d.ts +17 -2
  20. package/dist/prerender-build.d.ts +4 -0
  21. package/dist/prerender.d.ts +0 -1
  22. package/dist/react-router-config.d.ts +8 -5
  23. package/dist/server-build-resolution.d.ts +3 -0
  24. package/dist/ssr-externals.d.ts +1 -0
  25. package/dist/typegen.d.ts +14 -1
  26. package/dist/types.d.ts +15 -6
  27. package/package.json +11 -9
  28. package/src/build-manifest.ts +110 -73
  29. package/src/concurrency.ts +3 -22
  30. package/src/config-imports.ts +38 -0
  31. package/src/dev-background-resources.ts +255 -0
  32. package/src/dev-generation.ts +43 -53
  33. package/src/dev-runtime-artifacts.ts +50 -18
  34. package/src/dev-runtime-compilation.ts +80 -1
  35. package/src/dev-runtime-controller.ts +111 -31
  36. package/src/dev-runtime-session.ts +18 -11
  37. package/src/dev-server.ts +31 -1
  38. package/src/effect-runtime.ts +130 -0
  39. package/src/export-utils.ts +82 -23
  40. package/src/index.ts +118 -153
  41. package/src/lazy-compilation-prewarm.ts +279 -0
  42. package/src/lazy-compilation.ts +12 -5
  43. package/src/manifest.ts +366 -255
  44. package/src/modify-browser-manifest.ts +2 -1
  45. package/src/parallel-route-transforms.ts +195 -69
  46. package/src/prerender-build.ts +125 -61
  47. package/src/prerender.ts +0 -18
  48. package/src/react-router-config.ts +98 -73
  49. package/src/route-artifacts.ts +2 -2
  50. package/src/route-export-resolution.ts +3 -3
  51. package/src/route-watch.ts +119 -84
  52. package/src/server-build-resolution.ts +131 -0
  53. package/src/server-utils.ts +7 -106
  54. package/src/ssr-externals.ts +1 -1
  55. package/src/typegen.ts +162 -33
  56. 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 Promise.all([
106
- configA.buildEnd?.(...args),
107
- configB.buildEnd?.(...args),
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 resolveReactRouterConfig = async (
162
+ export const resolveReactRouterConfigEffect = (
149
163
  reactRouterUserConfig: Config
150
- ): Promise<{
151
- resolved: ResolvedReactRouterConfig;
152
- presets: NonNullable<Config['presets']>;
153
- hasConfiguredServerModuleFormat: boolean;
154
- }> => {
155
- const presets = await Promise.all(
156
- (reactRouterUserConfig.presets ?? []).map(async preset => {
157
- if (!preset.name) {
158
- throw new Error(
159
- 'React Router presets must have a `name` property defined.'
160
- );
161
- }
162
- if (!preset.reactRouterConfig) {
163
- return null;
164
- }
165
- const { buildEnd: _buildEnd, ...reactRouterUserConfigForPreset } =
166
- reactRouterUserConfig;
167
- const presetConfig = await preset.reactRouterConfig({
168
- reactRouterUserConfig: reactRouterUserConfigForPreset,
169
- });
170
- if (!presetConfig) return null;
171
- const { presets: _presets, ...rest } = presetConfig as Config;
172
- return rest;
173
- })
174
- );
175
-
176
- const userAndPresetConfigs = mergeReactRouterConfig(
177
- ...(presets.filter(Boolean) as Config[]).map(normalizeSubResourceIntegrity),
178
- normalizeSubResourceIntegrity(reactRouterUserConfig)
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
- const subResourceIntegrity =
182
- userAndPresetConfigs.subResourceIntegrity ??
183
- userAndPresetConfigs.future?.unstable_subResourceIntegrity ??
184
- DEFAULT_CONFIG.subResourceIntegrity;
185
- const resolvedFuture: FutureConfig = {
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
- return {
215
- resolved,
216
- presets: reactRouterUserConfig.presets ?? [],
217
- hasConfiguredServerModuleFormat:
218
- userAndPresetConfigs.serverModuleFormat !== undefined,
219
- };
220
- };
242
+ export const resolveReactRouterConfig = (
243
+ reactRouterUserConfig: Config
244
+ ): Promise<ResolveReactRouterConfigResult> =>
245
+ runPluginEffect(resolveReactRouterConfigEffect(reactRouterUserConfig));
@@ -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>(resolveResolvedPath => {
267
+ new Promise<string | null>(resolvePromise => {
268
268
  resolveModule(dirname(importerPath), specifier, (error, resolved) => {
269
- resolveResolvedPath(error || !resolved ? null : resolved);
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>();
@@ -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 = async (
123
+ const readRouteDirectories = (watchDirectory: string): Promise<Set<string>> => {
124
+ return runPluginEffect(readRouteDirectoriesEffect(watchDirectory));
125
+ };
126
+
127
+ const readRouteDirectoriesEffect = (
115
128
  watchDirectory: string
116
- ): Promise<Set<string>> => {
129
+ ): Effect.Effect<Set<string>, Error, never> => {
117
130
  const directories = new Set<string>();
118
-
119
- const walkDirectory = async (directory: string): Promise<void> => {
120
- let entries;
121
- try {
122
- entries = await readdir(directory, { withFileTypes: true });
123
- } catch {
124
- return;
125
- }
126
-
127
- directories.add(directory);
128
- await Promise.all(
129
- entries.map(async entry => {
130
- const entryPath = resolve(directory, entry.name);
131
- if (entry.isDirectory()) {
132
- await walkDirectory(entryPath);
133
- }
134
- })
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
- await walkDirectory(watchDirectory);
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 touchRestartMarker = async (): Promise<void> => {
186
- await mkdir(dirname(restartMarkerPath), { recursive: true });
187
- await writeFile(restartMarkerPath, String(Date.now()));
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(directory, scheduleRescan, error => {
209
- if (directoryWatchers.get(directory) === watcher) {
210
- watcher.close();
211
- directoryWatchers.delete(directory);
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
- onError(error);
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 applyNextState = async (nextState: RouteDirectoryState) => {
228
- if (closed) {
229
- return;
230
- }
231
- syncDirectoryWatchers(nextState.directories);
232
- if (!areSetsEqual(state.routeTopology, nextState.routeTopology)) {
233
- if (onRouteTopologyChange) {
234
- // This is a notification boundary, not part of the rescan
235
- // transaction. A custom-server callback may close this watcher while
236
- // replacing its compiler, so awaiting it here would deadlock close().
237
- state = nextState;
238
- try {
239
- void Promise.resolve(onRouteTopologyChange()).catch(onError);
240
- } catch (error) {
241
- onError(error);
242
- }
243
- return;
244
- } else {
245
- await touchRestartMarker();
246
- }
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 runRescan = async (): Promise<void> => {
257
- if (closed) {
258
- return;
259
- }
279
+ const runRescanEffect = (): Effect.Effect<void, never, never> => {
260
280
  let nextDirectories: Set<string> | undefined;
261
- try {
262
- nextDirectories = await readRouteDirectories(watchDirectory);
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: await getRouteTopology(),
291
+ routeTopology: yield* tryPluginPromise(getRouteTopology),
269
292
  };
270
293
  if (closed) {
271
294
  return;
272
295
  }
273
- await applyNextState(nextState);
274
- } catch (error) {
275
- if (nextDirectories && !closed) {
276
- syncDirectoryWatchers(nextDirectories);
277
- }
278
- onError(error);
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(runRescan, runRescan);
310
+ rescanQueue = rescanQueue.then(
311
+ () => runPluginEffect(runRescanEffect()),
312
+ () => runPluginEffect(runRescanEffect())
313
+ );
284
314
  return rescanQueue;
285
315
  };
286
316
 
287
- const scheduleRescan = (): void => {
288
- if (rescanTimer) {
289
- clearTimeout(rescanTimer);
290
- }
291
- rescanTimer = setTimeout(() => {
292
- rescanTimer = undefined;
293
- void rescan();
294
- }, 100);
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
- if (rescanTimer) {
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
+ }