rsbuild-plugin-react-router 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,458 +0,0 @@
1
- import { Worker } from 'node:worker_threads';
2
- import { setBoundedCacheEntry } from './bounded-cache.js';
3
- import { getAvailableCpuCount, getDefaultConcurrency } from './concurrency.js';
4
- import { normalizeEffectError } from './effect-runtime.js';
5
- import {
6
- executeRouteTransformTask,
7
- type RouteTransformResult,
8
- type RouteTransformTask,
9
- type RouteTransformTaskOptions,
10
- } from './route-transform-tasks.js';
11
- import type { PluginOptions } from './types.js';
12
- import type {
13
- WorkerErrorPayload,
14
- WorkerRequest,
15
- WorkerResponse,
16
- } from './parallel-route-transform-protocol.js';
17
-
18
- export type ParallelRouteTransformConfig =
19
- NonNullable<PluginOptions['parallelRouteTransform']> extends infer Config
20
- ? Exclude<Config, false>
21
- : never;
22
-
23
- export type RouteTransformExecutorOptions = RouteTransformTaskOptions & {
24
- parallelRouteTransform?: PluginOptions['parallelRouteTransform'];
25
- splitRouteModules?: boolean;
26
- isBuild?: boolean;
27
- };
28
-
29
- export type RouteTransformExecutor = {
30
- run: (task: RouteTransformTask) => Promise<RouteTransformResult>;
31
- prewarm: () => void;
32
- close: () => Promise<void>;
33
- };
34
-
35
- type PendingTask = {
36
- resolve: (result: RouteTransformResult) => void;
37
- reject: (error: Error) => void;
38
- };
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
-
50
- type WorkerState = {
51
- worker: RouteTransformWorker;
52
- pending: Map<number, PendingTask>;
53
- sourceCache: Map<string, string>;
54
- };
55
-
56
- class WorkerStartupError extends Error {
57
- constructor(message: string) {
58
- super(message);
59
- this.name = 'WorkerStartupError';
60
- }
61
- }
62
-
63
- const MAX_WORKER_SOURCE_CACHE_ENTRIES = 2048;
64
- const MAX_ROUTE_SOURCE_WORKER_ENTRIES = 4096;
65
- const AUTO_PARALLEL_ROUTE_THRESHOLD = 256;
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
- };
89
-
90
- export const shouldParallelizeRouteTransforms = (routeCount: number): boolean =>
91
- routeCount >= AUTO_PARALLEL_ROUTE_THRESHOLD;
92
-
93
- const getConfiguredWorkerCount = (
94
- parallelRouteTransform: ParallelRouteTransformConfig,
95
- isBuild: boolean
96
- ): number => {
97
- if (parallelRouteTransform === true) {
98
- return getDefaultWorkerCount(undefined, { isBuild });
99
- }
100
-
101
- if (!Number.isInteger(parallelRouteTransform) || parallelRouteTransform < 1) {
102
- throw new Error(
103
- '[react-router] parallelRouteTransform must be true, false, or a positive integer.'
104
- );
105
- }
106
- return parallelRouteTransform;
107
- };
108
-
109
- const hashString = (value: string): number => {
110
- let hash = 0;
111
- for (let index = 0; index < value.length; index += 1) {
112
- hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
113
- }
114
- return hash;
115
- };
116
-
117
- const deserializeWorkerError = (error: WorkerErrorPayload): Error => {
118
- const result = new Error(error.message);
119
- result.name = error.name ?? 'Error';
120
- if (error.stack) {
121
- result.stack = error.stack;
122
- }
123
- return result;
124
- };
125
-
126
- const createWorkerUrl = (): URL =>
127
- new URL('./parallel-route-transform-worker.js', import.meta.url);
128
-
129
- const createDefaultWorker = (): RouteTransformWorker =>
130
- new Worker(createWorkerUrl());
131
-
132
- const isWorkerStartupError = (error: unknown): error is WorkerStartupError =>
133
- error instanceof WorkerStartupError;
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
-
157
- class ParallelRouteTransformExecutor implements RouteTransformExecutor {
158
- #closed = false;
159
- #closePromise: Promise<void> | undefined;
160
- #workersDisabled = false;
161
- #nextId = 1;
162
- #nextRouteSourceWorkerIndex = 0;
163
- #routeSourceWorkers = new Map<string, number>();
164
- #workers: Array<WorkerState | undefined> | undefined;
165
- #prewarmScheduled = false;
166
-
167
- constructor(
168
- private readonly workerCount: number,
169
- private readonly options: RouteTransformTaskOptions,
170
- private readonly balanceRouteModuleTransforms: boolean,
171
- private readonly createWorker: RouteTransformWorkerFactory
172
- ) {}
173
-
174
- async run(task: RouteTransformTask): Promise<RouteTransformResult> {
175
- if (this.#closed) {
176
- return executeRouteTransformTask(task, this.options);
177
- }
178
- if (task.kind === 'clientOnlyStub' && task.resolveExportAllModule) {
179
- return executeRouteTransformTask(task, this.options);
180
- }
181
-
182
- try {
183
- return await this.#runInWorker(task);
184
- } catch (error) {
185
- if (isWorkerStartupError(error)) {
186
- return executeRouteTransformTask(task, this.options);
187
- }
188
- throw error;
189
- }
190
- }
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
-
218
- close(): Promise<void> {
219
- if (this.#closePromise) {
220
- return this.#closePromise;
221
- }
222
- this.#closed = true;
223
- const workers = (this.#workers ?? []).filter(
224
- (state): state is WorkerState => Boolean(state)
225
- );
226
- this.#workers = [];
227
- this.#closePromise = Promise.all(
228
- workers.map(async state => {
229
- rejectPendingTasks(state, new Error('Route transform worker closed.'));
230
- await state.worker.terminate();
231
- })
232
- ).then(() => undefined);
233
- return this.#closePromise;
234
- }
235
-
236
- #disableWorkers(error: WorkerStartupError): void {
237
- if (this.#workersDisabled || this.#closed) {
238
- return;
239
- }
240
- this.#workersDisabled = true;
241
- const workers = (this.#workers ?? []).filter(
242
- (state): state is WorkerState => Boolean(state)
243
- );
244
- this.#workers = [];
245
- for (const state of workers) {
246
- rejectPendingTasks(state, error);
247
- void state.worker.terminate();
248
- }
249
- }
250
-
251
- #createWorkerState(): WorkerState {
252
- const worker = this.createWorker();
253
- const state: WorkerState = {
254
- worker,
255
- pending: new Map(),
256
- sourceCache: new Map(),
257
- };
258
-
259
- worker.on('message', (response: WorkerResponse) => {
260
- const pending = state.pending.get(response.id);
261
- if (!pending) {
262
- return;
263
- }
264
- state.pending.delete(response.id);
265
- if (response.ok) {
266
- pending.resolve(response.result);
267
- } else {
268
- pending.reject(deserializeWorkerError(response.error));
269
- }
270
- });
271
-
272
- worker.on('error', (error: Error) => {
273
- this.#disableWorkers(createWorkerStartupError(error));
274
- });
275
-
276
- worker.on('exit', code => {
277
- if (this.#closed || this.#workersDisabled) {
278
- return;
279
- }
280
- const startupError = new WorkerStartupError(
281
- `Route transform worker exited with code ${code}.`
282
- );
283
- this.#disableWorkers(startupError);
284
- });
285
-
286
- return state;
287
- }
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
-
304
- #runInWorker(task: RouteTransformTask): Promise<RouteTransformResult> {
305
- const workerIndex = this.#getWorkerIndex(task, this.workerCount);
306
- const state = this.#getWorker(workerIndex);
307
- if (!state) {
308
- return executeRouteTransformTask(task, this.options);
309
- }
310
-
311
- const id = this.#nextId++;
312
- const sourceCacheKey = task.resourcePath;
313
- const requestTask = this.#createWorkerRequestTask(
314
- state,
315
- task,
316
- sourceCacheKey
317
- );
318
- return new Promise((resolve, reject) => {
319
- state.pending.set(id, { resolve, reject });
320
- try {
321
- state.worker.postMessage({
322
- id,
323
- task: requestTask,
324
- sourceCacheKey,
325
- } satisfies WorkerRequest);
326
- } catch (error) {
327
- state.pending.delete(id);
328
- // The worker may not have received the source update. Force the next
329
- // request for this module to send its full source again.
330
- state.sourceCache.delete(sourceCacheKey);
331
- reject(normalizeEffectError(error));
332
- }
333
- });
334
- }
335
-
336
- #createWorkerRequestTask(
337
- state: WorkerState,
338
- task: RouteTransformTask,
339
- sourceCacheKey: string
340
- ): WorkerRequest['task'] {
341
- const cachedSource = state.sourceCache.get(sourceCacheKey);
342
- if (cachedSource === task.code) {
343
- const { code: _code, ...cachedTask } = task;
344
- return cachedTask;
345
- }
346
-
347
- setBoundedCacheEntry(
348
- state.sourceCache,
349
- sourceCacheKey,
350
- task.code,
351
- MAX_WORKER_SOURCE_CACHE_ENTRIES
352
- );
353
- return task;
354
- }
355
-
356
- #getWorkerIndex(task: RouteTransformTask, workerCount: number): number {
357
- const safeWorkerCount = Math.max(1, workerCount);
358
- if (
359
- this.balanceRouteModuleTransforms &&
360
- (task.kind === 'routeClientEntry' ||
361
- task.kind === 'routeChunk' ||
362
- task.kind === 'splitRouteExports')
363
- ) {
364
- return this.#getRouteSourceWorkerIndex(
365
- task.resourcePath,
366
- safeWorkerCount
367
- );
368
- }
369
- if (
370
- this.balanceRouteModuleTransforms &&
371
- task.kind === 'routeModule' &&
372
- !(task.environmentName === 'web' && !task.ssr && task.isSpaMode)
373
- ) {
374
- return this.#getRouteSourceWorkerIndex(
375
- task.resourcePath,
376
- safeWorkerCount
377
- );
378
- }
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;
401
- }
402
- }
403
-
404
- export const createRouteTransformExecutor = ({
405
- parallelRouteTransform,
406
- routeChunkCache,
407
- splitRouteModules,
408
- isBuild,
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 => {
430
- const options = { routeChunkCache };
431
- if (
432
- parallelRouteTransform === undefined ||
433
- parallelRouteTransform === false
434
- ) {
435
- return createInlineRouteTransformExecutor(options);
436
- }
437
-
438
- const workerCount = getConfiguredWorkerCount(
439
- parallelRouteTransform,
440
- Boolean(isBuild)
441
- );
442
- if (workerCount < 1) {
443
- return createInlineRouteTransformExecutor(options);
444
- }
445
-
446
- return new ParallelRouteTransformExecutor(
447
- workerCount,
448
- options,
449
- Boolean(splitRouteModules),
450
- createWorker
451
- );
452
- };
453
-
454
- export const createRouteTransformExecutorForTesting = (
455
- options: RouteTransformExecutorOptions,
456
- createWorker: RouteTransformWorkerFactory
457
- ): RouteTransformExecutor =>
458
- createRouteTransformExecutorWithWorkerFactory(options, createWorker);