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
@@ -163,9 +163,10 @@ export function registerModifyBrowserManifestAssets(
163
163
  const browserManifestAsset = assets[BROWSER_MANIFEST_ASSET];
164
164
  if (browserManifestAsset) {
165
165
  const originalSource = browserManifestAsset.source().toString();
166
+ const serializedManifest = jsesc(manifestForBrowser, { es6: true });
166
167
  const newSource = originalSource.replace(
167
168
  /["'`]PLACEHOLDER["'`]/,
168
- jsesc(manifestForBrowser, { es6: true })
169
+ () => serializedManifest
169
170
  );
170
171
  compilation.updateAsset(
171
172
  BROWSER_MANIFEST_ASSET,
@@ -1,6 +1,7 @@
1
1
  import { Worker } from 'node:worker_threads';
2
2
  import { setBoundedCacheEntry } from './bounded-cache.js';
3
- import { getDefaultConcurrency } from './concurrency.js';
3
+ import { getAvailableCpuCount, getDefaultConcurrency } from './concurrency.js';
4
+ import { normalizeEffectError } from './effect-runtime.js';
4
5
  import {
5
6
  executeRouteTransformTask,
6
7
  type RouteTransformResult,
@@ -22,10 +23,12 @@ export type ParallelRouteTransformConfig =
22
23
  export type RouteTransformExecutorOptions = RouteTransformTaskOptions & {
23
24
  parallelRouteTransform?: PluginOptions['parallelRouteTransform'];
24
25
  splitRouteModules?: boolean;
26
+ isBuild?: boolean;
25
27
  };
26
28
 
27
29
  export type RouteTransformExecutor = {
28
30
  run: (task: RouteTransformTask) => Promise<RouteTransformResult>;
31
+ prewarm: () => void;
29
32
  close: () => Promise<void>;
30
33
  };
31
34
 
@@ -34,8 +37,18 @@ type PendingTask = {
34
37
  reject: (error: Error) => void;
35
38
  };
36
39
 
40
+ type RouteTransformWorker = {
41
+ on(event: 'message', handler: (response: WorkerResponse) => void): unknown;
42
+ on(event: 'error', handler: (error: Error) => void): unknown;
43
+ on(event: 'exit', handler: (code: number) => void): unknown;
44
+ postMessage(message: WorkerRequest): void;
45
+ terminate(): Promise<number> | number;
46
+ };
47
+
48
+ type RouteTransformWorkerFactory = () => RouteTransformWorker;
49
+
37
50
  type WorkerState = {
38
- worker: Worker;
51
+ worker: RouteTransformWorker;
39
52
  pending: Map<number, PendingTask>;
40
53
  sourceCache: Map<string, string>;
41
54
  };
@@ -48,19 +61,41 @@ class WorkerStartupError extends Error {
48
61
  }
49
62
 
50
63
  const MAX_WORKER_SOURCE_CACHE_ENTRIES = 2048;
64
+ const MAX_ROUTE_SOURCE_WORKER_ENTRIES = 4096;
51
65
  const AUTO_PARALLEL_ROUTE_THRESHOLD = 256;
52
-
53
- export const getDefaultWorkerCount = (cpuCount?: number): number =>
54
- getDefaultConcurrency(cpuCount);
66
+ const DEFAULT_WORKER_COUNT_LIMIT = 2;
67
+ // Benchmarked on a 4-core cpuset: in dev (large-355 fixture) a single worker
68
+ // recovered no wall time while worker startup and IPC cost ~0.5s of
69
+ // dev-server ready (9.09s -> 8.57s median when disabled), so small machines
70
+ // run dev transforms inline. Production builds are different: at 1024 routes
71
+ // the transform volume is large enough that two workers beat inline by ~8%
72
+ // (synthetic-1024-ssr-esm build, 4.46s inline vs 4.09s with workers), so
73
+ // builds keep the default worker count even on small machines.
74
+ const SMALL_MACHINE_CPU_COUNT = 4;
75
+
76
+ export const getDefaultWorkerCount = (
77
+ cpuCount?: number,
78
+ { isBuild = false }: { isBuild?: boolean } = {}
79
+ ): number => {
80
+ const resolvedCpuCount = cpuCount ?? getAvailableCpuCount();
81
+ if (!isBuild && resolvedCpuCount <= SMALL_MACHINE_CPU_COUNT) {
82
+ return 0;
83
+ }
84
+ return Math.min(
85
+ getDefaultConcurrency(resolvedCpuCount),
86
+ DEFAULT_WORKER_COUNT_LIMIT
87
+ );
88
+ };
55
89
 
56
90
  export const shouldParallelizeRouteTransforms = (routeCount: number): boolean =>
57
91
  routeCount >= AUTO_PARALLEL_ROUTE_THRESHOLD;
58
92
 
59
93
  const getConfiguredWorkerCount = (
60
- parallelRouteTransform: ParallelRouteTransformConfig
94
+ parallelRouteTransform: ParallelRouteTransformConfig,
95
+ isBuild: boolean
61
96
  ): number => {
62
97
  if (parallelRouteTransform === true) {
63
- return getDefaultWorkerCount();
98
+ return getDefaultWorkerCount(undefined, { isBuild });
64
99
  }
65
100
 
66
101
  if (!Number.isInteger(parallelRouteTransform) || parallelRouteTransform < 1) {
@@ -91,37 +126,50 @@ const deserializeWorkerError = (error: WorkerErrorPayload): Error => {
91
126
  const createWorkerUrl = (): URL =>
92
127
  new URL('./parallel-route-transform-worker.js', import.meta.url);
93
128
 
129
+ const createDefaultWorker = (): RouteTransformWorker =>
130
+ new Worker(createWorkerUrl());
131
+
94
132
  const isWorkerStartupError = (error: unknown): error is WorkerStartupError =>
95
133
  error instanceof WorkerStartupError;
96
134
 
135
+ const createWorkerStartupError = (error: unknown): WorkerStartupError => {
136
+ const normalized = normalizeEffectError(error);
137
+ const startupError = new WorkerStartupError(normalized.message);
138
+ startupError.stack = normalized.stack;
139
+ return startupError;
140
+ };
141
+
142
+ const rejectPendingTasks = (state: WorkerState, error: Error): void => {
143
+ for (const pending of state.pending.values()) {
144
+ pending.reject(error);
145
+ }
146
+ state.pending.clear();
147
+ };
148
+
149
+ const createInlineRouteTransformExecutor = (
150
+ options: RouteTransformTaskOptions
151
+ ): RouteTransformExecutor => ({
152
+ run: task => executeRouteTransformTask(task, options),
153
+ prewarm: () => {},
154
+ close: async () => {},
155
+ });
156
+
97
157
  class ParallelRouteTransformExecutor implements RouteTransformExecutor {
98
158
  #closed = false;
99
159
  #closePromise: Promise<void> | undefined;
100
160
  #workersDisabled = false;
101
161
  #nextId = 1;
102
- #nextRouteModuleWorkerIndex = 0;
103
- #nextSplitRouteAnalysisWorkerIndex = 0;
104
- #splitRouteAnalysisWorkers = new Map<string, number>();
105
- #workers: WorkerState[];
162
+ #nextRouteSourceWorkerIndex = 0;
163
+ #routeSourceWorkers = new Map<string, number>();
164
+ #workers: Array<WorkerState | undefined> | undefined;
165
+ #prewarmScheduled = false;
106
166
 
107
167
  constructor(
108
- workerCount: number,
168
+ private readonly workerCount: number,
109
169
  private readonly options: RouteTransformTaskOptions,
110
- private readonly balanceRouteModuleTransforms: boolean
111
- ) {
112
- this.#workers = [];
113
- try {
114
- for (let index = 0; index < workerCount; index += 1) {
115
- this.#workers.push(this.#createWorkerState());
116
- }
117
- } catch (error) {
118
- for (const state of this.#workers) {
119
- void state.worker.terminate();
120
- }
121
- this.#workers = [];
122
- throw error;
123
- }
124
- }
170
+ private readonly balanceRouteModuleTransforms: boolean,
171
+ private readonly createWorker: RouteTransformWorkerFactory
172
+ ) {}
125
173
 
126
174
  async run(task: RouteTransformTask): Promise<RouteTransformResult> {
127
175
  if (this.#closed) {
@@ -141,19 +189,44 @@ class ParallelRouteTransformExecutor implements RouteTransformExecutor {
141
189
  }
142
190
  }
143
191
 
192
+ prewarm(): void {
193
+ if (this.#prewarmScheduled || this.#closed || this.#workersDisabled) {
194
+ return;
195
+ }
196
+ this.#prewarmScheduled = true;
197
+ void this.#prewarmWorkers();
198
+ }
199
+
200
+ async #prewarmWorkers(): Promise<void> {
201
+ try {
202
+ for (let index = 0; index < this.workerCount; index += 1) {
203
+ if (this.#closed || this.#workersDisabled) {
204
+ return;
205
+ }
206
+ try {
207
+ this.#getWorker(index);
208
+ } catch (error) {
209
+ this.#disableWorkers(createWorkerStartupError(error));
210
+ return;
211
+ }
212
+ }
213
+ } finally {
214
+ this.#prewarmScheduled = false;
215
+ }
216
+ }
217
+
144
218
  close(): Promise<void> {
145
219
  if (this.#closePromise) {
146
220
  return this.#closePromise;
147
221
  }
148
222
  this.#closed = true;
149
- const workers = this.#workers;
223
+ const workers = (this.#workers ?? []).filter(
224
+ (state): state is WorkerState => Boolean(state)
225
+ );
150
226
  this.#workers = [];
151
227
  this.#closePromise = Promise.all(
152
228
  workers.map(async state => {
153
- for (const pending of state.pending.values()) {
154
- pending.reject(new Error('Route transform worker closed.'));
155
- }
156
- state.pending.clear();
229
+ rejectPendingTasks(state, new Error('Route transform worker closed.'));
157
230
  await state.worker.terminate();
158
231
  })
159
232
  ).then(() => undefined);
@@ -165,19 +238,18 @@ class ParallelRouteTransformExecutor implements RouteTransformExecutor {
165
238
  return;
166
239
  }
167
240
  this.#workersDisabled = true;
168
- const workers = this.#workers;
241
+ const workers = (this.#workers ?? []).filter(
242
+ (state): state is WorkerState => Boolean(state)
243
+ );
169
244
  this.#workers = [];
170
245
  for (const state of workers) {
171
- for (const pending of state.pending.values()) {
172
- pending.reject(error);
173
- }
174
- state.pending.clear();
246
+ rejectPendingTasks(state, error);
175
247
  void state.worker.terminate();
176
248
  }
177
249
  }
178
250
 
179
251
  #createWorkerState(): WorkerState {
180
- const worker = new Worker(createWorkerUrl());
252
+ const worker = this.createWorker();
181
253
  const state: WorkerState = {
182
254
  worker,
183
255
  pending: new Map(),
@@ -198,9 +270,7 @@ class ParallelRouteTransformExecutor implements RouteTransformExecutor {
198
270
  });
199
271
 
200
272
  worker.on('error', (error: Error) => {
201
- const startupError = new WorkerStartupError(error.message);
202
- startupError.stack = error.stack;
203
- this.#disableWorkers(startupError);
273
+ this.#disableWorkers(createWorkerStartupError(error));
204
274
  });
205
275
 
206
276
  worker.on('exit', code => {
@@ -216,9 +286,24 @@ class ParallelRouteTransformExecutor implements RouteTransformExecutor {
216
286
  return state;
217
287
  }
218
288
 
289
+ #getWorker(index: number): WorkerState | undefined {
290
+ if (this.#closed || this.#workersDisabled) {
291
+ return undefined;
292
+ }
293
+ this.#workers ??= new Array(this.workerCount);
294
+ const existingWorker = this.#workers[index];
295
+ if (existingWorker) {
296
+ return existingWorker;
297
+ }
298
+
299
+ const worker = this.#createWorkerState();
300
+ this.#workers[index] = worker;
301
+ return worker;
302
+ }
303
+
219
304
  #runInWorker(task: RouteTransformTask): Promise<RouteTransformResult> {
220
- const workerIndex = this.#getWorkerIndex(task);
221
- const state = this.#workers[workerIndex];
305
+ const workerIndex = this.#getWorkerIndex(task, this.workerCount);
306
+ const state = this.#getWorker(workerIndex);
222
307
  if (!state) {
223
308
  return executeRouteTransformTask(task, this.options);
224
309
  }
@@ -243,7 +328,7 @@ class ParallelRouteTransformExecutor implements RouteTransformExecutor {
243
328
  // The worker may not have received the source update. Force the next
244
329
  // request for this module to send its full source again.
245
330
  state.sourceCache.delete(sourceCacheKey);
246
- reject(error instanceof Error ? error : new Error(String(error)));
331
+ reject(normalizeEffectError(error));
247
332
  }
248
333
  });
249
334
  }
@@ -268,35 +353,51 @@ class ParallelRouteTransformExecutor implements RouteTransformExecutor {
268
353
  return task;
269
354
  }
270
355
 
271
- #getWorkerIndex(task: RouteTransformTask): number {
272
- const workerCount = Math.max(1, this.#workers.length);
356
+ #getWorkerIndex(task: RouteTransformTask, workerCount: number): number {
357
+ const safeWorkerCount = Math.max(1, workerCount);
273
358
  if (
274
359
  this.balanceRouteModuleTransforms &&
275
360
  (task.kind === 'routeClientEntry' ||
276
361
  task.kind === 'routeChunk' ||
277
362
  task.kind === 'splitRouteExports')
278
363
  ) {
279
- const existingWorkerIndex = this.#splitRouteAnalysisWorkers.get(
280
- task.resourcePath
364
+ return this.#getRouteSourceWorkerIndex(
365
+ task.resourcePath,
366
+ safeWorkerCount
281
367
  );
282
- if (existingWorkerIndex !== undefined) {
283
- return existingWorkerIndex % workerCount;
284
- }
285
- const workerIndex = this.#nextSplitRouteAnalysisWorkerIndex % workerCount;
286
- this.#nextSplitRouteAnalysisWorkerIndex += 1;
287
- this.#splitRouteAnalysisWorkers.set(task.resourcePath, workerIndex);
288
- return workerIndex;
289
368
  }
290
369
  if (
291
370
  this.balanceRouteModuleTransforms &&
292
371
  task.kind === 'routeModule' &&
293
372
  !(task.environmentName === 'web' && !task.ssr && task.isSpaMode)
294
373
  ) {
295
- const workerIndex = this.#nextRouteModuleWorkerIndex % workerCount;
296
- this.#nextRouteModuleWorkerIndex += 1;
297
- return workerIndex;
374
+ return this.#getRouteSourceWorkerIndex(
375
+ task.resourcePath,
376
+ safeWorkerCount
377
+ );
298
378
  }
299
- return hashString(task.resourcePath) % workerCount;
379
+ return hashString(task.resourcePath) % safeWorkerCount;
380
+ }
381
+
382
+ #getRouteSourceWorkerIndex(
383
+ resourcePath: string,
384
+ safeWorkerCount: number
385
+ ): number {
386
+ const existingWorkerIndex = this.#routeSourceWorkers.get(resourcePath);
387
+ if (existingWorkerIndex !== undefined) {
388
+ this.#routeSourceWorkers.delete(resourcePath);
389
+ this.#routeSourceWorkers.set(resourcePath, existingWorkerIndex);
390
+ return existingWorkerIndex % safeWorkerCount;
391
+ }
392
+ const workerIndex = this.#nextRouteSourceWorkerIndex % safeWorkerCount;
393
+ this.#nextRouteSourceWorkerIndex += 1;
394
+ setBoundedCacheEntry(
395
+ this.#routeSourceWorkers,
396
+ resourcePath,
397
+ workerIndex,
398
+ MAX_ROUTE_SOURCE_WORKER_ENTRIES
399
+ );
400
+ return workerIndex;
300
401
  }
301
402
  }
302
403
 
@@ -304,29 +405,54 @@ export const createRouteTransformExecutor = ({
304
405
  parallelRouteTransform,
305
406
  routeChunkCache,
306
407
  splitRouteModules,
408
+ isBuild,
307
409
  }: RouteTransformExecutorOptions = {}): RouteTransformExecutor => {
410
+ return createRouteTransformExecutorWithWorkerFactory(
411
+ {
412
+ parallelRouteTransform,
413
+ routeChunkCache,
414
+ splitRouteModules,
415
+ isBuild,
416
+ },
417
+ createDefaultWorker
418
+ );
419
+ };
420
+
421
+ const createRouteTransformExecutorWithWorkerFactory = (
422
+ {
423
+ parallelRouteTransform,
424
+ routeChunkCache,
425
+ splitRouteModules,
426
+ isBuild,
427
+ }: RouteTransformExecutorOptions = {},
428
+ createWorker: RouteTransformWorkerFactory
429
+ ): RouteTransformExecutor => {
308
430
  const options = { routeChunkCache };
309
431
  if (
310
432
  parallelRouteTransform === undefined ||
311
433
  parallelRouteTransform === false
312
434
  ) {
313
- return {
314
- run: task => executeRouteTransformTask(task, options),
315
- close: async () => {},
316
- };
435
+ return createInlineRouteTransformExecutor(options);
317
436
  }
318
437
 
319
- const workerCount = getConfiguredWorkerCount(parallelRouteTransform);
438
+ const workerCount = getConfiguredWorkerCount(
439
+ parallelRouteTransform,
440
+ Boolean(isBuild)
441
+ );
320
442
  if (workerCount < 1) {
321
- return {
322
- run: task => executeRouteTransformTask(task, options),
323
- close: async () => {},
324
- };
443
+ return createInlineRouteTransformExecutor(options);
325
444
  }
326
445
 
327
446
  return new ParallelRouteTransformExecutor(
328
447
  workerCount,
329
448
  options,
330
- Boolean(splitRouteModules)
449
+ Boolean(splitRouteModules),
450
+ createWorker
331
451
  );
332
452
  };
453
+
454
+ export const createRouteTransformExecutorForTesting = (
455
+ options: RouteTransformExecutorOptions,
456
+ createWorker: RouteTransformWorkerFactory
457
+ ): RouteTransformExecutor =>
458
+ createRouteTransformExecutorWithWorkerFactory(options, createWorker);
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import fsExtra from 'fs-extra';
5
+ import * as Effect from 'effect/Effect';
5
6
  import type { RsbuildPluginAPI } from '@rsbuild/core';
6
7
  import {
7
8
  createRequestHandler,
@@ -22,7 +23,6 @@ import {
22
23
  getPrerenderConcurrency,
23
24
  getSsrFalsePrerenderExportErrors,
24
25
  normalizePrerenderMatchPath,
25
- withBuildRequest,
26
26
  } from './prerender.js';
27
27
  import type {
28
28
  Config,
@@ -30,6 +30,7 @@ import type {
30
30
  } from './react-router-config.js';
31
31
  import { resolveServerBuildModule } from './server-utils.js';
32
32
  import type { PluginOptions, Route } from './types.js';
33
+ import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
33
34
 
34
35
  type ReactRouterManifest = Awaited<
35
36
  ReturnType<typeof getReactRouterManifestForDev>
@@ -131,6 +132,34 @@ const createDataRequestPath = (
131
132
  : `${prerenderPath.replace(/\/$/, '')}.data`;
132
133
  };
133
134
 
135
+ export const createBuildRequestEffect = <T>(
136
+ input: string | URL,
137
+ init: RequestInit | undefined,
138
+ handle: (request: Request) => Promise<T>
139
+ ): Effect.Effect<T, Error, never> =>
140
+ Effect.acquireUseRelease(
141
+ Effect.sync(() => new AbortController()),
142
+ controller =>
143
+ tryPluginPromise(() =>
144
+ handle(
145
+ new Request(input, {
146
+ ...init,
147
+ signal: controller.signal,
148
+ })
149
+ )
150
+ ),
151
+ controller =>
152
+ Effect.sync(() => {
153
+ controller.abort();
154
+ })
155
+ );
156
+
157
+ export const withBuildRequest = <T>(
158
+ input: string | URL,
159
+ init: RequestInit | undefined,
160
+ handle: (request: Request) => Promise<T>
161
+ ): Promise<T> => runPluginEffect(createBuildRequestEffect(input, init, handle));
162
+
134
163
  const prerenderData = async ({
135
164
  handler,
136
165
  prerenderPath,
@@ -401,23 +430,30 @@ const validatePrerenderPathMatches = (
401
430
  }
402
431
  };
403
432
 
404
- const runPrerenderPaths = async ({
433
+ export const createBoundedPrerenderTasksEffect = (
434
+ prerenderPaths: string[],
435
+ concurrency: number,
436
+ renderPath: (path: string) => Effect.Effect<void, Error, never>
437
+ ): Effect.Effect<void, Error, never> =>
438
+ Effect.forEach(prerenderPaths, renderPath, { concurrency, discard: true });
439
+
440
+ const createPrerenderPathEffect = ({
441
+ path,
405
442
  build,
443
+ buildRoutes,
406
444
  requestHandler,
407
445
  clientBuildDir,
408
446
  options,
409
447
  }: {
448
+ path: string;
410
449
  build: PrerenderServerBuild;
450
+ buildRoutes: ReturnType<typeof createPrerenderRoutes>;
411
451
  requestHandler: (request: Request) => Promise<Response>;
412
452
  clientBuildDir: string;
413
453
  options: RunReactRouterPrerenderBuildOptions;
414
- }): Promise<void> => {
415
- const { api, basename, future, prerenderConfig, prerenderPaths } = options;
416
- const buildRoutes = createPrerenderRoutes(build.routes);
417
- const concurrency = getPrerenderConcurrency(prerenderConfig);
418
- const pending = new Set<Promise<void>>();
419
-
420
- const enqueue = async (path: string) => {
454
+ }): Effect.Effect<void, Error, never> =>
455
+ Effect.gen(function* () {
456
+ const { api, basename, future } = options;
421
457
  const matches = matchRoutes(buildRoutes, normalizePrerenderMatchPath(path));
422
458
  if (!matches) {
423
459
  return;
@@ -431,27 +467,33 @@ const runPrerenderPaths = async ({
431
467
 
432
468
  if (isResourceRoute) {
433
469
  if (manifestRoute.loader && routeId) {
434
- await prerenderData({
435
- handler: requestHandler,
436
- prerenderPath: path,
437
- onlyRoutes: [routeId],
438
- clientBuildDir,
439
- basename,
440
- trailingSlashAwareDataRequests:
441
- future.unstable_trailingSlashAwareDataRequests,
442
- api,
443
- });
444
- await prerenderResourceRoute({
445
- handler: requestHandler,
446
- prerenderPath: path,
447
- clientBuildDir,
448
- basename,
449
- api,
450
- });
451
- } else {
452
- api.logger.warn(
453
- `⚠️ Skipping prerendering for resource route without a loader: ${routeId}`
470
+ yield* tryPluginPromise(() =>
471
+ prerenderData({
472
+ handler: requestHandler,
473
+ prerenderPath: path,
474
+ onlyRoutes: [routeId],
475
+ clientBuildDir,
476
+ basename,
477
+ trailingSlashAwareDataRequests:
478
+ future.unstable_trailingSlashAwareDataRequests,
479
+ api,
480
+ })
481
+ );
482
+ yield* tryPluginPromise(() =>
483
+ prerenderResourceRoute({
484
+ handler: requestHandler,
485
+ prerenderPath: path,
486
+ clientBuildDir,
487
+ basename,
488
+ api,
489
+ })
454
490
  );
491
+ } else {
492
+ yield* Effect.sync(() => {
493
+ api.logger.warn(
494
+ `⚠️ Skipping prerendering for resource route without a loader: ${routeId}`
495
+ );
496
+ });
455
497
  }
456
498
  return;
457
499
  }
@@ -463,44 +505,66 @@ const runPrerenderPaths = async ({
463
505
  }
464
506
  return build.assets?.routes?.[matchedRouteId]?.hasLoader;
465
507
  });
466
- let data: string | undefined;
467
- if (hasLoaders) {
468
- data = await prerenderData({
508
+ const data = hasLoaders
509
+ ? yield* tryPluginPromise(() =>
510
+ prerenderData({
511
+ handler: requestHandler,
512
+ prerenderPath: path,
513
+ onlyRoutes: null,
514
+ clientBuildDir,
515
+ basename,
516
+ trailingSlashAwareDataRequests:
517
+ future.unstable_trailingSlashAwareDataRequests,
518
+ api,
519
+ })
520
+ )
521
+ : undefined;
522
+
523
+ yield* tryPluginPromise(() =>
524
+ prerenderRoute({
469
525
  handler: requestHandler,
470
526
  prerenderPath: path,
471
- onlyRoutes: null,
472
527
  clientBuildDir,
473
528
  basename,
474
- trailingSlashAwareDataRequests:
475
- future.unstable_trailingSlashAwareDataRequests,
476
529
  api,
477
- });
478
- }
479
- await prerenderRoute({
480
- handler: requestHandler,
481
- prerenderPath: path,
482
- clientBuildDir,
483
- basename,
484
- api,
485
- requestInit: data
486
- ? {
487
- headers: {
488
- 'X-React-Router-Prerender-Data': encodeURI(data),
489
- },
490
- }
491
- : undefined,
492
- });
493
- };
530
+ requestInit: data
531
+ ? {
532
+ headers: {
533
+ 'X-React-Router-Prerender-Data': encodeURI(data),
534
+ },
535
+ }
536
+ : undefined,
537
+ })
538
+ );
539
+ });
494
540
 
495
- for (const path of prerenderPaths) {
496
- const task = enqueue(path);
497
- pending.add(task);
498
- task.finally(() => pending.delete(task));
499
- if (pending.size >= concurrency) {
500
- await Promise.race(pending);
501
- }
502
- }
503
- await Promise.all(pending);
541
+ const runPrerenderPaths = async ({
542
+ build,
543
+ requestHandler,
544
+ clientBuildDir,
545
+ options,
546
+ }: {
547
+ build: PrerenderServerBuild;
548
+ requestHandler: (request: Request) => Promise<Response>;
549
+ clientBuildDir: string;
550
+ options: RunReactRouterPrerenderBuildOptions;
551
+ }): Promise<void> => {
552
+ const { prerenderConfig, prerenderPaths } = options;
553
+ const buildRoutes = createPrerenderRoutes(build.routes);
554
+ const concurrency = getPrerenderConcurrency(prerenderConfig);
555
+
556
+ await runPluginEffect(
557
+ createBoundedPrerenderTasksEffect(prerenderPaths, concurrency, path =>
558
+ createPrerenderPathEffect({
559
+ path,
560
+ build,
561
+ buildRoutes,
562
+ requestHandler,
563
+ clientBuildDir,
564
+ options,
565
+ })
566
+ )
567
+ );
504
568
  };
505
569
 
506
570
  export const runReactRouterPrerenderBuild = async (
package/src/prerender.ts CHANGED
@@ -92,24 +92,6 @@ export const createPrerenderRoutes = (
92
92
  export const normalizePrerenderMatchPath = (path: string): string =>
93
93
  `/${path}/`.replace(/^\/\/+/, '/');
94
94
 
95
- export const withBuildRequest = async <T>(
96
- input: string | URL,
97
- init: RequestInit | undefined,
98
- handle: (request: Request) => Promise<T>
99
- ): Promise<T> => {
100
- const controller = new AbortController();
101
- try {
102
- return await handle(
103
- new Request(input, {
104
- ...init,
105
- signal: controller.signal,
106
- })
107
- );
108
- } finally {
109
- controller.abort();
110
- }
111
- };
112
-
113
95
  export const getSsrFalsePrerenderExportErrors = ({
114
96
  routes,
115
97
  manifestRoutes,