rsbuild-plugin-react-router 0.2.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 +213 -201
- package/dist/451.js +1103 -0
- package/dist/bounded-cache.d.ts +1 -0
- package/dist/build-manifest.d.ts +7 -4
- package/dist/build-output-transforms.d.ts +30 -0
- package/dist/concurrency.d.ts +3 -0
- package/dist/config-imports.d.ts +10 -0
- package/dist/constants.d.ts +3 -0
- package/dist/dev-background-resources.d.ts +38 -0
- package/dist/dev-generation.d.ts +25 -0
- package/dist/dev-runtime-artifacts.d.ts +47 -0
- package/dist/dev-runtime-compilation.d.ts +47 -0
- package/dist/dev-runtime-controller.d.ts +14 -0
- package/dist/dev-runtime-session.d.ts +34 -0
- package/dist/dev-server.d.ts +16 -2
- package/dist/effect-runtime.d.ts +18 -0
- package/dist/export-utils.d.ts +15 -7
- package/dist/index.cjs +13775 -1412
- package/dist/index.d.ts +8 -1
- package/dist/index.js +9429 -1501
- package/dist/lazy-compilation-prewarm.d.ts +25 -0
- package/dist/lazy-compilation.d.ts +6 -0
- package/dist/manifest.d.ts +48 -10
- package/dist/modify-browser-manifest.d.ts +30 -13
- package/dist/parallel-route-transform-protocol.d.ts +25 -0
- package/dist/parallel-route-transform-worker.d.ts +1 -0
- package/dist/parallel-route-transform-worker.js +38 -0
- package/dist/parallel-route-transforms.d.ts +29 -0
- package/dist/performance.d.ts +26 -0
- package/dist/plugin-utils.d.ts +2 -12
- package/dist/prerender-build.d.ts +36 -0
- package/dist/prerender.d.ts +27 -2
- package/dist/react-router-config.d.ts +15 -5
- package/dist/route-artifacts.d.ts +33 -0
- package/dist/route-ast.d.ts +21 -0
- package/dist/route-chunks.d.ts +9 -1
- package/dist/route-component-transform.d.ts +5 -0
- package/dist/route-export-pruning.d.ts +6 -0
- package/dist/route-export-resolution.d.ts +5 -0
- package/dist/route-transform-tasks.d.ts +47 -0
- package/dist/route-watch.d.ts +27 -0
- package/dist/server-build-plan.d.ts +22 -0
- package/dist/server-build-resolution.d.ts +3 -0
- package/dist/server-utils.d.ts +3 -2
- package/dist/ssr-externals.d.ts +1 -0
- package/dist/templates/entry.server.cjs +3 -3
- package/dist/templates/entry.server.js +3 -3
- package/dist/typegen.d.ts +15 -0
- package/dist/types.d.ts +41 -14
- package/dist/virtual-modules.d.ts +2 -0
- package/dist/warnings/warn-on-client-source-maps.d.ts +1 -0
- package/dist/yuku.d.ts +15 -0
- package/package.json +24 -19
- package/src/bounded-cache.ts +18 -0
- package/src/build-manifest.ts +205 -0
- package/src/build-output-transforms.ts +273 -0
- package/src/concurrency.ts +15 -0
- package/src/config-imports.ts +83 -0
- package/src/constants.ts +91 -0
- package/src/dev-background-resources.ts +255 -0
- package/src/dev-generation.ts +690 -0
- package/src/dev-runtime-artifacts.ts +239 -0
- package/src/dev-runtime-compilation.ts +171 -0
- package/src/dev-runtime-controller.ts +542 -0
- package/src/dev-runtime-session.ts +191 -0
- package/src/dev-server.ts +88 -0
- package/src/effect-runtime.ts +130 -0
- package/src/export-utils.ts +278 -0
- package/src/index.ts +990 -0
- package/src/lazy-compilation-prewarm.ts +279 -0
- package/src/lazy-compilation.ts +101 -0
- package/src/manifest.ts +591 -0
- package/src/modify-browser-manifest.ts +246 -0
- package/src/parallel-route-transform-protocol.ts +35 -0
- package/src/parallel-route-transform-worker.ts +82 -0
- package/src/parallel-route-transforms.ts +458 -0
- package/src/performance.ts +254 -0
- package/src/plugin-utils.ts +82 -0
- package/src/prerender-build.ts +687 -0
- package/src/prerender.ts +349 -0
- package/src/react-router-config.ts +245 -0
- package/src/route-artifacts.ts +155 -0
- package/src/route-ast.ts +163 -0
- package/src/route-chunks.ts +857 -0
- package/src/route-component-transform.ts +314 -0
- package/src/route-config.ts +106 -0
- package/src/route-export-pruning.ts +668 -0
- package/src/route-export-resolution.ts +329 -0
- package/src/route-transform-tasks.ts +249 -0
- package/src/route-watch.ts +357 -0
- package/src/server-build-plan.ts +91 -0
- package/src/server-build-resolution.ts +131 -0
- package/src/server-utils.ts +111 -0
- package/src/ssr-externals.ts +59 -0
- package/src/templates/context.ts +12 -0
- package/src/templates/entry.client.tsx +12 -0
- package/src/templates/entry.server.tsx +76 -0
- package/src/typegen.ts +178 -0
- package/src/types.ts +92 -0
- package/src/validation/validate-plugin-order.ts +76 -0
- package/src/virtual-modules.ts +30 -0
- package/src/warnings/warn-on-client-source-maps.ts +96 -0
- package/src/yuku.ts +67 -0
- package/dist/0~rslib-runtime.js +0 -16
- package/dist/babel.d.ts +0 -8
|
@@ -0,0 +1,458 @@
|
|
|
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);
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
type OperationTiming = {
|
|
2
|
+
count: number;
|
|
3
|
+
// Total sums every recorded duration, so parallel work can make it larger
|
|
4
|
+
// than elapsed wall-clock time. Use wallMs for non-overlapping elapsed time.
|
|
5
|
+
totalMs: number;
|
|
6
|
+
wallMs: number;
|
|
7
|
+
maxMs: number;
|
|
8
|
+
slowest: Array<{
|
|
9
|
+
durationMs: number;
|
|
10
|
+
resource: string;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type OperationInterval = { startMs: number; endMs: number };
|
|
15
|
+
|
|
16
|
+
type MutableOperationTiming = Omit<OperationTiming, 'wallMs' | 'slowest'> & {
|
|
17
|
+
slowest: Array<{
|
|
18
|
+
durationMs: number;
|
|
19
|
+
resource: string;
|
|
20
|
+
}>;
|
|
21
|
+
intervals: OperationInterval[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type EnvironmentTimings = Map<string, MutableOperationTiming>;
|
|
25
|
+
|
|
26
|
+
const MAX_SLOWEST_ENTRIES = 5;
|
|
27
|
+
|
|
28
|
+
const insertSlowestEntry = (
|
|
29
|
+
slowest: MutableOperationTiming['slowest'],
|
|
30
|
+
entry: MutableOperationTiming['slowest'][number]
|
|
31
|
+
) => {
|
|
32
|
+
if (
|
|
33
|
+
slowest.length === MAX_SLOWEST_ENTRIES &&
|
|
34
|
+
entry.durationMs <= slowest[slowest.length - 1].durationMs
|
|
35
|
+
) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let insertIndex = slowest.length;
|
|
40
|
+
while (
|
|
41
|
+
insertIndex > 0 &&
|
|
42
|
+
entry.durationMs > slowest[insertIndex - 1].durationMs
|
|
43
|
+
) {
|
|
44
|
+
insertIndex -= 1;
|
|
45
|
+
}
|
|
46
|
+
slowest.splice(insertIndex, 0, entry);
|
|
47
|
+
if (slowest.length > MAX_SLOWEST_ENTRIES) {
|
|
48
|
+
slowest.pop();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const roundMs = (value: number): number => Math.round(value * 10) / 10;
|
|
53
|
+
|
|
54
|
+
export type ReactRouterPerformanceReport = {
|
|
55
|
+
environment: string;
|
|
56
|
+
compilerLifecycleMs?: number;
|
|
57
|
+
operations: Record<string, OperationTiming>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type ReactRouterPerformanceProfiler = {
|
|
61
|
+
record<T>(
|
|
62
|
+
environment: string | undefined,
|
|
63
|
+
operation: string,
|
|
64
|
+
resource: string,
|
|
65
|
+
callback: () => Promise<T>
|
|
66
|
+
): Promise<T>;
|
|
67
|
+
recordSync<T>(
|
|
68
|
+
environment: string | undefined,
|
|
69
|
+
operation: string,
|
|
70
|
+
resource: string,
|
|
71
|
+
callback: () => T
|
|
72
|
+
): T;
|
|
73
|
+
flush(
|
|
74
|
+
environment: string | undefined,
|
|
75
|
+
details?: Pick<ReactRouterPerformanceReport, 'compilerLifecycleMs'>
|
|
76
|
+
): void;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const createReactRouterPerformanceProfiler = ({
|
|
80
|
+
enabled,
|
|
81
|
+
log,
|
|
82
|
+
}: {
|
|
83
|
+
enabled: boolean;
|
|
84
|
+
log: (message: string) => void;
|
|
85
|
+
}): ReactRouterPerformanceProfiler => {
|
|
86
|
+
const timingsByEnvironment = new Map<string, EnvironmentTimings>();
|
|
87
|
+
|
|
88
|
+
const getOperationTiming = (
|
|
89
|
+
environment: string,
|
|
90
|
+
operation: string
|
|
91
|
+
): MutableOperationTiming => {
|
|
92
|
+
let timings = timingsByEnvironment.get(environment);
|
|
93
|
+
if (!timings) {
|
|
94
|
+
timings = new Map();
|
|
95
|
+
timingsByEnvironment.set(environment, timings);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let timing = timings.get(operation);
|
|
99
|
+
if (!timing) {
|
|
100
|
+
timing = {
|
|
101
|
+
count: 0,
|
|
102
|
+
totalMs: 0,
|
|
103
|
+
maxMs: 0,
|
|
104
|
+
slowest: [],
|
|
105
|
+
intervals: [],
|
|
106
|
+
};
|
|
107
|
+
timings.set(operation, timing);
|
|
108
|
+
}
|
|
109
|
+
return timing;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const computeWallMs = (intervals: OperationInterval[]) => {
|
|
113
|
+
if (intervals.length === 0) {
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const sortedIntervals = [...intervals].sort(
|
|
118
|
+
(a, b) => a.startMs - b.startMs || a.endMs - b.endMs
|
|
119
|
+
);
|
|
120
|
+
let mergedStart = sortedIntervals[0].startMs;
|
|
121
|
+
let mergedEnd = sortedIntervals[0].endMs;
|
|
122
|
+
let wallMs = 0;
|
|
123
|
+
|
|
124
|
+
for (const interval of sortedIntervals.slice(1)) {
|
|
125
|
+
if (interval.startMs <= mergedEnd) {
|
|
126
|
+
mergedEnd = Math.max(mergedEnd, interval.endMs);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
wallMs += mergedEnd - mergedStart;
|
|
131
|
+
mergedStart = interval.startMs;
|
|
132
|
+
mergedEnd = interval.endMs;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
wallMs += mergedEnd - mergedStart;
|
|
136
|
+
return roundMs(wallMs);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const toOperationTiming = (
|
|
140
|
+
timing: MutableOperationTiming
|
|
141
|
+
): OperationTiming => ({
|
|
142
|
+
count: timing.count,
|
|
143
|
+
totalMs: roundMs(timing.totalMs),
|
|
144
|
+
wallMs: computeWallMs(timing.intervals),
|
|
145
|
+
maxMs: roundMs(timing.maxMs),
|
|
146
|
+
slowest: timing.slowest.map(entry => ({
|
|
147
|
+
durationMs: roundMs(entry.durationMs),
|
|
148
|
+
resource: entry.resource,
|
|
149
|
+
})),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const recordDuration = (
|
|
153
|
+
environment: string,
|
|
154
|
+
operation: string,
|
|
155
|
+
resource: string,
|
|
156
|
+
startMs: number,
|
|
157
|
+
endMs: number
|
|
158
|
+
) => {
|
|
159
|
+
const duration = endMs - startMs;
|
|
160
|
+
const timing = getOperationTiming(environment, operation);
|
|
161
|
+
timing.count += 1;
|
|
162
|
+
timing.totalMs += duration;
|
|
163
|
+
timing.maxMs = Math.max(timing.maxMs, duration);
|
|
164
|
+
timing.intervals.push({ startMs, endMs });
|
|
165
|
+
insertSlowestEntry(timing.slowest, {
|
|
166
|
+
durationMs: duration,
|
|
167
|
+
resource,
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
record(environment, operation, resource, callback) {
|
|
173
|
+
if (!enabled) {
|
|
174
|
+
try {
|
|
175
|
+
return Promise.resolve(callback());
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return Promise.reject(error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const resolvedEnvironment = environment ?? 'unknown';
|
|
182
|
+
const start = performance.now();
|
|
183
|
+
try {
|
|
184
|
+
return callback().then(
|
|
185
|
+
result => {
|
|
186
|
+
const end = performance.now();
|
|
187
|
+
recordDuration(
|
|
188
|
+
resolvedEnvironment,
|
|
189
|
+
operation,
|
|
190
|
+
resource,
|
|
191
|
+
start,
|
|
192
|
+
end
|
|
193
|
+
);
|
|
194
|
+
return result;
|
|
195
|
+
},
|
|
196
|
+
error => {
|
|
197
|
+
const end = performance.now();
|
|
198
|
+
recordDuration(
|
|
199
|
+
resolvedEnvironment,
|
|
200
|
+
operation,
|
|
201
|
+
resource,
|
|
202
|
+
start,
|
|
203
|
+
end
|
|
204
|
+
);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const end = performance.now();
|
|
210
|
+
recordDuration(resolvedEnvironment, operation, resource, start, end);
|
|
211
|
+
return Promise.reject(error);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
recordSync(environment, operation, resource, callback) {
|
|
215
|
+
if (!enabled) {
|
|
216
|
+
return callback();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const resolvedEnvironment = environment ?? 'unknown';
|
|
220
|
+
const start = performance.now();
|
|
221
|
+
try {
|
|
222
|
+
return callback();
|
|
223
|
+
} finally {
|
|
224
|
+
const end = performance.now();
|
|
225
|
+
recordDuration(resolvedEnvironment, operation, resource, start, end);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
flush(environment, details = {}) {
|
|
229
|
+
if (!enabled) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const resolvedEnvironment = environment ?? 'unknown';
|
|
234
|
+
const timings = timingsByEnvironment.get(resolvedEnvironment);
|
|
235
|
+
if (!timings || timings.size === 0) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const operations = Object.fromEntries(
|
|
240
|
+
[...timings.entries()].map(([operation, timing]) => [
|
|
241
|
+
operation,
|
|
242
|
+
toOperationTiming(timing),
|
|
243
|
+
])
|
|
244
|
+
);
|
|
245
|
+
const report: ReactRouterPerformanceReport = {
|
|
246
|
+
environment: resolvedEnvironment,
|
|
247
|
+
...details,
|
|
248
|
+
operations,
|
|
249
|
+
};
|
|
250
|
+
log(`[react-router:performance] ${JSON.stringify(report)}`);
|
|
251
|
+
timingsByEnvironment.delete(resolvedEnvironment);
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
};
|