rsbuild-plugin-react-router 0.1.1 → 0.3.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.
Files changed (99) hide show
  1. package/README.md +185 -204
  2. package/dist/451.js +1087 -0
  3. package/dist/bounded-cache.d.ts +1 -0
  4. package/dist/build-manifest.d.ts +1 -1
  5. package/dist/build-output-transforms.d.ts +30 -0
  6. package/dist/concurrency.d.ts +2 -0
  7. package/dist/config-imports.d.ts +3 -0
  8. package/dist/constants.d.ts +3 -0
  9. package/dist/dev-generation.d.ts +25 -0
  10. package/dist/dev-runtime-artifacts.d.ts +39 -0
  11. package/dist/dev-runtime-compilation.d.ts +32 -0
  12. package/dist/dev-runtime-controller.d.ts +14 -0
  13. package/dist/dev-runtime-session.d.ts +34 -0
  14. package/dist/dev-server.d.ts +11 -2
  15. package/dist/export-utils.d.ts +15 -7
  16. package/dist/index.cjs +3085 -1272
  17. package/dist/index.d.ts +4 -1
  18. package/dist/index.js +2039 -1299
  19. package/dist/lazy-compilation.d.ts +5 -0
  20. package/dist/manifest.d.ts +32 -9
  21. package/dist/modify-browser-manifest.d.ts +30 -13
  22. package/dist/parallel-route-transform-protocol.d.ts +25 -0
  23. package/dist/parallel-route-transform-worker.d.ts +1 -0
  24. package/dist/parallel-route-transform-worker.js +38 -0
  25. package/dist/parallel-route-transforms.d.ts +14 -0
  26. package/dist/performance.d.ts +26 -0
  27. package/dist/plugin-utils.d.ts +2 -12
  28. package/dist/prerender-build.d.ts +32 -0
  29. package/dist/prerender.d.ts +28 -2
  30. package/dist/react-router-config.d.ts +8 -1
  31. package/dist/route-artifacts.d.ts +33 -0
  32. package/dist/route-ast.d.ts +21 -0
  33. package/dist/route-chunks.d.ts +9 -1
  34. package/dist/route-component-transform.d.ts +5 -0
  35. package/dist/route-export-pruning.d.ts +6 -0
  36. package/dist/route-export-resolution.d.ts +5 -0
  37. package/dist/route-transform-tasks.d.ts +47 -0
  38. package/dist/route-watch.d.ts +27 -0
  39. package/dist/server-build-plan.d.ts +22 -0
  40. package/dist/server-utils.d.ts +3 -2
  41. package/dist/templates/entry.client.js +3 -3
  42. package/dist/templates/entry.server.cjs +11 -8
  43. package/dist/templates/entry.server.js +5 -5
  44. package/dist/typegen.d.ts +2 -0
  45. package/dist/types.d.ts +30 -12
  46. package/dist/virtual-modules.d.ts +2 -0
  47. package/dist/warnings/warn-on-client-source-maps.d.ts +1 -0
  48. package/dist/yuku.d.ts +15 -0
  49. package/package.json +26 -22
  50. package/src/bounded-cache.ts +18 -0
  51. package/src/build-manifest.ts +168 -0
  52. package/src/build-output-transforms.ts +273 -0
  53. package/src/concurrency.ts +34 -0
  54. package/src/config-imports.ts +45 -0
  55. package/src/constants.ts +91 -0
  56. package/src/dev-generation.ts +700 -0
  57. package/src/dev-runtime-artifacts.ts +207 -0
  58. package/src/dev-runtime-compilation.ts +92 -0
  59. package/src/dev-runtime-controller.ts +462 -0
  60. package/src/dev-runtime-session.ts +184 -0
  61. package/src/dev-server.ts +58 -0
  62. package/src/export-utils.ts +219 -0
  63. package/src/index.ts +1025 -0
  64. package/src/lazy-compilation.ts +94 -0
  65. package/src/manifest.ts +480 -0
  66. package/src/modify-browser-manifest.ts +245 -0
  67. package/src/parallel-route-transform-protocol.ts +35 -0
  68. package/src/parallel-route-transform-worker.ts +82 -0
  69. package/src/parallel-route-transforms.ts +332 -0
  70. package/src/performance.ts +254 -0
  71. package/src/plugin-utils.ts +82 -0
  72. package/src/prerender-build.ts +623 -0
  73. package/src/prerender.ts +367 -0
  74. package/src/react-router-config.ts +220 -0
  75. package/src/route-artifacts.ts +155 -0
  76. package/src/route-ast.ts +163 -0
  77. package/src/route-chunks.ts +857 -0
  78. package/src/route-component-transform.ts +314 -0
  79. package/src/route-config.ts +106 -0
  80. package/src/route-export-pruning.ts +668 -0
  81. package/src/route-export-resolution.ts +329 -0
  82. package/src/route-transform-tasks.ts +249 -0
  83. package/src/route-watch.ts +322 -0
  84. package/src/server-build-plan.ts +91 -0
  85. package/src/server-utils.ts +210 -0
  86. package/src/ssr-externals.ts +59 -0
  87. package/src/templates/context.ts +12 -0
  88. package/src/templates/entry.client.tsx +12 -0
  89. package/src/templates/entry.server.tsx +76 -0
  90. package/src/typegen.ts +49 -0
  91. package/src/types.ts +82 -0
  92. package/src/validation/validate-plugin-order.ts +76 -0
  93. package/src/virtual-modules.ts +30 -0
  94. package/src/warnings/warn-on-client-source-maps.ts +96 -0
  95. package/src/yuku.ts +67 -0
  96. package/dist/906.js +0 -1
  97. package/dist/946.js +0 -1
  98. package/dist/babel.d.ts +0 -8
  99. package/dist/rslib-runtime.js +0 -23
@@ -0,0 +1,245 @@
1
+ import type { Route, PluginOptions } from './types.js';
2
+ import type { RsbuildPluginAPI, Rspack } from '@rsbuild/core';
3
+ import {
4
+ createReactRouterManifestStats,
5
+ generateReactRouterManifestForDev,
6
+ getReactRouterManifestChunkNames,
7
+ getReactRouterManifestForDev,
8
+ getReactRouterManifestPath,
9
+ } from './manifest.js';
10
+ import { combineURLs } from './plugin-utils.js';
11
+ import jsesc from 'jsesc';
12
+
13
+ type StatsAssetWithIntegrity = {
14
+ name?: string;
15
+ integrity?: unknown;
16
+ };
17
+
18
+ type StatsWithIntegrity = {
19
+ assets?: StatsAssetWithIntegrity[];
20
+ };
21
+
22
+ type CompilationAssetWithIntegrity = {
23
+ name: string;
24
+ info?: {
25
+ integrity?: unknown;
26
+ };
27
+ };
28
+
29
+ type CompilationWithIntegrityAssets =
30
+ | {
31
+ getAssets?: () => readonly CompilationAssetWithIntegrity[];
32
+ }
33
+ | Pick<Rspack.Compilation, 'getAssets'>;
34
+
35
+ type ModifyBrowserManifestOptions = {
36
+ future?: { unstable_subResourceIntegrity?: boolean };
37
+ subResourceIntegrity?: boolean;
38
+ manifestChunkNames?: ReadonlySet<string>;
39
+ onManifest?: (
40
+ manifest: Awaited<ReturnType<typeof getReactRouterManifestForDev>>,
41
+ sri: Record<string, string> | true | undefined,
42
+ moduleExportsByRouteId: Awaited<
43
+ ReturnType<typeof generateReactRouterManifestForDev>
44
+ >['moduleExportsByRouteId'],
45
+ context: {
46
+ compilation: Rspack.Compilation;
47
+ manifestStats: ReturnType<typeof createReactRouterManifestStats>;
48
+ }
49
+ ) => void;
50
+ };
51
+
52
+ type ProcessAssetsApi = Pick<RsbuildPluginAPI, 'processAssets'>;
53
+ type AssetPrefixInput = string | (() => string);
54
+ type ReactRouterManifest = Awaited<
55
+ ReturnType<typeof getReactRouterManifestForDev>
56
+ >;
57
+ type RouteManifestModuleExports = Awaited<
58
+ ReturnType<typeof generateReactRouterManifestForDev>
59
+ >['moduleExportsByRouteId'];
60
+ type GeneratedManifest = {
61
+ manifest: ReactRouterManifest;
62
+ moduleExportsByRouteId: RouteManifestModuleExports;
63
+ manifestStats: ReturnType<typeof createReactRouterManifestStats>;
64
+ assetPrefix: string;
65
+ };
66
+
67
+ const BROWSER_MANIFEST_ASSET =
68
+ 'static/js/virtual/react-router/browser-manifest.js';
69
+ const ABSOLUTE_URL_RE = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
70
+
71
+ const toManifestAssetUrl = (assetPrefix: string, assetName: string) => {
72
+ if (
73
+ ABSOLUTE_URL_RE.test(assetName) ||
74
+ assetName.startsWith('//') ||
75
+ assetName.startsWith('/')
76
+ ) {
77
+ return assetName;
78
+ }
79
+ return combineURLs(assetPrefix, assetName);
80
+ };
81
+
82
+ const addIntegrity = (
83
+ sri: Record<string, string>,
84
+ assetPrefix: string,
85
+ assetName: unknown,
86
+ integrity: unknown
87
+ ) => {
88
+ if (typeof assetName !== 'string' || typeof integrity !== 'string') {
89
+ return;
90
+ }
91
+ sri[toManifestAssetUrl(assetPrefix, assetName)] = integrity;
92
+ };
93
+
94
+ export const collectSubresourceIntegrity = (
95
+ stats: StatsWithIntegrity | undefined,
96
+ compilation: CompilationWithIntegrityAssets | undefined,
97
+ assetPrefix = '/'
98
+ ): Record<string, string> | undefined => {
99
+ const sri: Record<string, string> = {};
100
+
101
+ for (const asset of stats?.assets ?? []) {
102
+ addIntegrity(sri, assetPrefix, asset.name, asset.integrity);
103
+ }
104
+
105
+ if (typeof compilation?.getAssets === 'function') {
106
+ const assets =
107
+ compilation.getAssets() as readonly CompilationAssetWithIntegrity[];
108
+ for (const asset of assets) {
109
+ addIntegrity(sri, assetPrefix, asset.name, asset.info?.integrity);
110
+ }
111
+ }
112
+
113
+ return Object.keys(sri).length > 0 ? sri : undefined;
114
+ };
115
+
116
+ export function registerModifyBrowserManifestAssets(
117
+ api: ProcessAssetsApi,
118
+ routes: Record<string, Route>,
119
+ pluginOptions: PluginOptions,
120
+ appDirectory: string,
121
+ assetPrefix: AssetPrefixInput = '/',
122
+ routeChunkOptions?: Parameters<typeof getReactRouterManifestForDev>[5],
123
+ options?: ModifyBrowserManifestOptions
124
+ ): void {
125
+ const getAssetPrefix =
126
+ typeof assetPrefix === 'function' ? assetPrefix : () => assetPrefix;
127
+ const manifestChunkNames =
128
+ options?.manifestChunkNames ??
129
+ getReactRouterManifestChunkNames(
130
+ routes,
131
+ routeChunkOptions?.splitRouteModules
132
+ );
133
+ const finalizeSri = Boolean(
134
+ routeChunkOptions?.isBuild &&
135
+ (options?.subResourceIntegrity ??
136
+ options?.future?.unstable_subResourceIntegrity)
137
+ );
138
+ const generatedManifests = finalizeSri
139
+ ? new WeakMap<Rspack.Compilation, GeneratedManifest>()
140
+ : undefined;
141
+
142
+ api.processAssets(
143
+ { stage: 'additions', environments: ['web'] },
144
+ async ({ assets, sources, compilation }) => {
145
+ const currentAssetPrefix = getAssetPrefix();
146
+ const stats = createReactRouterManifestStats(
147
+ compilation,
148
+ manifestChunkNames
149
+ );
150
+ const { manifest, moduleExportsByRouteId } =
151
+ await generateReactRouterManifestForDev(
152
+ routes,
153
+ pluginOptions,
154
+ stats,
155
+ appDirectory,
156
+ currentAssetPrefix,
157
+ routeChunkOptions
158
+ );
159
+ const manifestForBrowser = finalizeSri
160
+ ? { ...manifest, sri: true as const }
161
+ : manifest;
162
+
163
+ const browserManifestAsset = assets[BROWSER_MANIFEST_ASSET];
164
+ if (browserManifestAsset) {
165
+ const originalSource = browserManifestAsset.source().toString();
166
+ const newSource = originalSource.replace(
167
+ /["'`]PLACEHOLDER["'`]/,
168
+ jsesc(manifestForBrowser, { es6: true })
169
+ );
170
+ compilation.updateAsset(
171
+ BROWSER_MANIFEST_ASSET,
172
+ new sources.RawSource(newSource)
173
+ );
174
+ }
175
+
176
+ if (routeChunkOptions?.isBuild) {
177
+ const entryAssets = stats?.assetsByChunkName?.['entry.client'];
178
+ const entryJsAssets =
179
+ entryAssets?.filter(asset => asset.endsWith('.js')) || [];
180
+ const manifestPath = getReactRouterManifestPath({
181
+ version: manifest.version,
182
+ isBuild: true,
183
+ entryModulePath: entryJsAssets[0],
184
+ });
185
+ const manifestSource = `window.__reactRouterManifest=${jsesc(
186
+ manifestForBrowser,
187
+ { es6: true }
188
+ )};`;
189
+ const source = new sources.RawSource(manifestSource);
190
+ if (compilation.getAsset(manifestPath)) {
191
+ compilation.updateAsset(manifestPath, source);
192
+ } else {
193
+ compilation.emitAsset(manifestPath, source);
194
+ }
195
+ }
196
+
197
+ if (generatedManifests) {
198
+ generatedManifests.set(compilation, {
199
+ manifest,
200
+ moduleExportsByRouteId,
201
+ manifestStats: stats,
202
+ assetPrefix: currentAssetPrefix,
203
+ });
204
+ return;
205
+ }
206
+
207
+ options?.onManifest?.(manifest, undefined, moduleExportsByRouteId, {
208
+ compilation,
209
+ manifestStats: stats,
210
+ });
211
+ }
212
+ );
213
+
214
+ if (generatedManifests) {
215
+ api.processAssets(
216
+ { stage: 'report', environments: ['web'] },
217
+ ({ compilation }) => {
218
+ const generatedManifest = generatedManifests.get(compilation);
219
+ if (!generatedManifest) {
220
+ return;
221
+ }
222
+
223
+ generatedManifests.delete(compilation);
224
+ const sri =
225
+ collectSubresourceIntegrity(
226
+ undefined,
227
+ compilation,
228
+ generatedManifest.assetPrefix
229
+ ) ?? true;
230
+ options?.onManifest?.(
231
+ {
232
+ ...generatedManifest.manifest,
233
+ sri,
234
+ },
235
+ sri,
236
+ generatedManifest.moduleExportsByRouteId,
237
+ {
238
+ compilation,
239
+ manifestStats: generatedManifest.manifestStats,
240
+ }
241
+ );
242
+ }
243
+ );
244
+ }
245
+ }
@@ -0,0 +1,35 @@
1
+ import type {
2
+ RouteTransformResult,
3
+ RouteTransformTask,
4
+ } from './route-transform-tasks.js';
5
+
6
+ type WithoutRequiredSource<Task> = Task extends RouteTransformTask
7
+ ? Omit<Task, 'code'> & { code?: string }
8
+ : never;
9
+
10
+ export type CachedRouteTransformTask =
11
+ WithoutRequiredSource<RouteTransformTask>;
12
+
13
+ export type WorkerRequest = {
14
+ id: number;
15
+ task: RouteTransformTask | CachedRouteTransformTask;
16
+ sourceCacheKey?: string;
17
+ };
18
+
19
+ export type WorkerErrorPayload = {
20
+ name?: string;
21
+ message: string;
22
+ stack?: string;
23
+ };
24
+
25
+ export type WorkerResponse =
26
+ | {
27
+ id: number;
28
+ ok: true;
29
+ result: RouteTransformResult;
30
+ }
31
+ | {
32
+ id: number;
33
+ ok: false;
34
+ error: WorkerErrorPayload;
35
+ };
@@ -0,0 +1,82 @@
1
+ import { parentPort } from 'node:worker_threads';
2
+ import { setBoundedCacheEntry } from './bounded-cache.js';
3
+ import {
4
+ executeRouteTransformTask,
5
+ type RouteTransformTask,
6
+ } from './route-transform-tasks.js';
7
+ import type {
8
+ WorkerErrorPayload,
9
+ WorkerRequest,
10
+ WorkerResponse,
11
+ } from './parallel-route-transform-protocol.js';
12
+
13
+ const serializeError = (error: unknown): WorkerErrorPayload => {
14
+ if (error instanceof Error) {
15
+ return {
16
+ name: error.name,
17
+ message: error.message,
18
+ stack: error.stack,
19
+ };
20
+ }
21
+ return {
22
+ message: String(error),
23
+ };
24
+ };
25
+
26
+ if (!parentPort) {
27
+ throw new Error('parallel route transform worker requires parentPort');
28
+ }
29
+
30
+ const MAX_SOURCE_CACHE_ENTRIES = 2048;
31
+ const sourceCache = new Map<string, string>();
32
+
33
+ const hydrateTaskSource = ({
34
+ task,
35
+ sourceCacheKey,
36
+ }: Pick<WorkerRequest, 'task' | 'sourceCacheKey'>): RouteTransformTask => {
37
+ if (!sourceCacheKey) {
38
+ return task as RouteTransformTask;
39
+ }
40
+
41
+ if (typeof task.code === 'string') {
42
+ setBoundedCacheEntry(
43
+ sourceCache,
44
+ sourceCacheKey,
45
+ task.code,
46
+ MAX_SOURCE_CACHE_ENTRIES
47
+ );
48
+ return task as RouteTransformTask;
49
+ }
50
+
51
+ const code = sourceCache.get(sourceCacheKey);
52
+ if (code === undefined) {
53
+ throw new Error(
54
+ `Missing cached route transform source for ${sourceCacheKey}.`
55
+ );
56
+ }
57
+ return {
58
+ ...task,
59
+ code,
60
+ } as RouteTransformTask;
61
+ };
62
+
63
+ parentPort.on(
64
+ 'message',
65
+ async ({ id, task, sourceCacheKey }: WorkerRequest) => {
66
+ try {
67
+ const hydratedTask = hydrateTaskSource({ task, sourceCacheKey });
68
+ const result = await executeRouteTransformTask(hydratedTask);
69
+ parentPort?.postMessage({
70
+ id,
71
+ ok: true,
72
+ result,
73
+ } satisfies WorkerResponse);
74
+ } catch (error) {
75
+ parentPort?.postMessage({
76
+ id,
77
+ ok: false,
78
+ error: serializeError(error),
79
+ } satisfies WorkerResponse);
80
+ }
81
+ }
82
+ );
@@ -0,0 +1,332 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ import { setBoundedCacheEntry } from './bounded-cache.js';
3
+ import { getDefaultConcurrency } from './concurrency.js';
4
+ import {
5
+ executeRouteTransformTask,
6
+ type RouteTransformResult,
7
+ type RouteTransformTask,
8
+ type RouteTransformTaskOptions,
9
+ } from './route-transform-tasks.js';
10
+ import type { PluginOptions } from './types.js';
11
+ import type {
12
+ WorkerErrorPayload,
13
+ WorkerRequest,
14
+ WorkerResponse,
15
+ } from './parallel-route-transform-protocol.js';
16
+
17
+ export type ParallelRouteTransformConfig =
18
+ NonNullable<PluginOptions['parallelRouteTransform']> extends infer Config
19
+ ? Exclude<Config, false>
20
+ : never;
21
+
22
+ export type RouteTransformExecutorOptions = RouteTransformTaskOptions & {
23
+ parallelRouteTransform?: PluginOptions['parallelRouteTransform'];
24
+ splitRouteModules?: boolean;
25
+ };
26
+
27
+ export type RouteTransformExecutor = {
28
+ run: (task: RouteTransformTask) => Promise<RouteTransformResult>;
29
+ close: () => Promise<void>;
30
+ };
31
+
32
+ type PendingTask = {
33
+ resolve: (result: RouteTransformResult) => void;
34
+ reject: (error: Error) => void;
35
+ };
36
+
37
+ type WorkerState = {
38
+ worker: Worker;
39
+ pending: Map<number, PendingTask>;
40
+ sourceCache: Map<string, string>;
41
+ };
42
+
43
+ class WorkerStartupError extends Error {
44
+ constructor(message: string) {
45
+ super(message);
46
+ this.name = 'WorkerStartupError';
47
+ }
48
+ }
49
+
50
+ const MAX_WORKER_SOURCE_CACHE_ENTRIES = 2048;
51
+ const AUTO_PARALLEL_ROUTE_THRESHOLD = 256;
52
+
53
+ export const getDefaultWorkerCount = (cpuCount?: number): number =>
54
+ getDefaultConcurrency(cpuCount);
55
+
56
+ export const shouldParallelizeRouteTransforms = (routeCount: number): boolean =>
57
+ routeCount >= AUTO_PARALLEL_ROUTE_THRESHOLD;
58
+
59
+ const getConfiguredWorkerCount = (
60
+ parallelRouteTransform: ParallelRouteTransformConfig
61
+ ): number => {
62
+ if (parallelRouteTransform === true) {
63
+ return getDefaultWorkerCount();
64
+ }
65
+
66
+ if (!Number.isInteger(parallelRouteTransform) || parallelRouteTransform < 1) {
67
+ throw new Error(
68
+ '[react-router] parallelRouteTransform must be true, false, or a positive integer.'
69
+ );
70
+ }
71
+ return parallelRouteTransform;
72
+ };
73
+
74
+ const hashString = (value: string): number => {
75
+ let hash = 0;
76
+ for (let index = 0; index < value.length; index += 1) {
77
+ hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
78
+ }
79
+ return hash;
80
+ };
81
+
82
+ const deserializeWorkerError = (error: WorkerErrorPayload): Error => {
83
+ const result = new Error(error.message);
84
+ result.name = error.name ?? 'Error';
85
+ if (error.stack) {
86
+ result.stack = error.stack;
87
+ }
88
+ return result;
89
+ };
90
+
91
+ const createWorkerUrl = (): URL =>
92
+ new URL('./parallel-route-transform-worker.js', import.meta.url);
93
+
94
+ const isWorkerStartupError = (error: unknown): error is WorkerStartupError =>
95
+ error instanceof WorkerStartupError;
96
+
97
+ class ParallelRouteTransformExecutor implements RouteTransformExecutor {
98
+ #closed = false;
99
+ #closePromise: Promise<void> | undefined;
100
+ #workersDisabled = false;
101
+ #nextId = 1;
102
+ #nextRouteModuleWorkerIndex = 0;
103
+ #nextSplitRouteAnalysisWorkerIndex = 0;
104
+ #splitRouteAnalysisWorkers = new Map<string, number>();
105
+ #workers: WorkerState[];
106
+
107
+ constructor(
108
+ workerCount: number,
109
+ 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
+ }
125
+
126
+ async run(task: RouteTransformTask): Promise<RouteTransformResult> {
127
+ if (this.#closed) {
128
+ return executeRouteTransformTask(task, this.options);
129
+ }
130
+ if (task.kind === 'clientOnlyStub' && task.resolveExportAllModule) {
131
+ return executeRouteTransformTask(task, this.options);
132
+ }
133
+
134
+ try {
135
+ return await this.#runInWorker(task);
136
+ } catch (error) {
137
+ if (isWorkerStartupError(error)) {
138
+ return executeRouteTransformTask(task, this.options);
139
+ }
140
+ throw error;
141
+ }
142
+ }
143
+
144
+ close(): Promise<void> {
145
+ if (this.#closePromise) {
146
+ return this.#closePromise;
147
+ }
148
+ this.#closed = true;
149
+ const workers = this.#workers;
150
+ this.#workers = [];
151
+ this.#closePromise = Promise.all(
152
+ 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();
157
+ await state.worker.terminate();
158
+ })
159
+ ).then(() => undefined);
160
+ return this.#closePromise;
161
+ }
162
+
163
+ #disableWorkers(error: WorkerStartupError): void {
164
+ if (this.#workersDisabled || this.#closed) {
165
+ return;
166
+ }
167
+ this.#workersDisabled = true;
168
+ const workers = this.#workers;
169
+ this.#workers = [];
170
+ for (const state of workers) {
171
+ for (const pending of state.pending.values()) {
172
+ pending.reject(error);
173
+ }
174
+ state.pending.clear();
175
+ void state.worker.terminate();
176
+ }
177
+ }
178
+
179
+ #createWorkerState(): WorkerState {
180
+ const worker = new Worker(createWorkerUrl());
181
+ const state: WorkerState = {
182
+ worker,
183
+ pending: new Map(),
184
+ sourceCache: new Map(),
185
+ };
186
+
187
+ worker.on('message', (response: WorkerResponse) => {
188
+ const pending = state.pending.get(response.id);
189
+ if (!pending) {
190
+ return;
191
+ }
192
+ state.pending.delete(response.id);
193
+ if (response.ok) {
194
+ pending.resolve(response.result);
195
+ } else {
196
+ pending.reject(deserializeWorkerError(response.error));
197
+ }
198
+ });
199
+
200
+ worker.on('error', (error: Error) => {
201
+ const startupError = new WorkerStartupError(error.message);
202
+ startupError.stack = error.stack;
203
+ this.#disableWorkers(startupError);
204
+ });
205
+
206
+ worker.on('exit', code => {
207
+ if (this.#closed || this.#workersDisabled) {
208
+ return;
209
+ }
210
+ const startupError = new WorkerStartupError(
211
+ `Route transform worker exited with code ${code}.`
212
+ );
213
+ this.#disableWorkers(startupError);
214
+ });
215
+
216
+ return state;
217
+ }
218
+
219
+ #runInWorker(task: RouteTransformTask): Promise<RouteTransformResult> {
220
+ const workerIndex = this.#getWorkerIndex(task);
221
+ const state = this.#workers[workerIndex];
222
+ if (!state) {
223
+ return executeRouteTransformTask(task, this.options);
224
+ }
225
+
226
+ const id = this.#nextId++;
227
+ const sourceCacheKey = task.resourcePath;
228
+ const requestTask = this.#createWorkerRequestTask(
229
+ state,
230
+ task,
231
+ sourceCacheKey
232
+ );
233
+ return new Promise((resolve, reject) => {
234
+ state.pending.set(id, { resolve, reject });
235
+ try {
236
+ state.worker.postMessage({
237
+ id,
238
+ task: requestTask,
239
+ sourceCacheKey,
240
+ } satisfies WorkerRequest);
241
+ } catch (error) {
242
+ state.pending.delete(id);
243
+ // The worker may not have received the source update. Force the next
244
+ // request for this module to send its full source again.
245
+ state.sourceCache.delete(sourceCacheKey);
246
+ reject(error instanceof Error ? error : new Error(String(error)));
247
+ }
248
+ });
249
+ }
250
+
251
+ #createWorkerRequestTask(
252
+ state: WorkerState,
253
+ task: RouteTransformTask,
254
+ sourceCacheKey: string
255
+ ): WorkerRequest['task'] {
256
+ const cachedSource = state.sourceCache.get(sourceCacheKey);
257
+ if (cachedSource === task.code) {
258
+ const { code: _code, ...cachedTask } = task;
259
+ return cachedTask;
260
+ }
261
+
262
+ setBoundedCacheEntry(
263
+ state.sourceCache,
264
+ sourceCacheKey,
265
+ task.code,
266
+ MAX_WORKER_SOURCE_CACHE_ENTRIES
267
+ );
268
+ return task;
269
+ }
270
+
271
+ #getWorkerIndex(task: RouteTransformTask): number {
272
+ const workerCount = Math.max(1, this.#workers.length);
273
+ if (
274
+ this.balanceRouteModuleTransforms &&
275
+ (task.kind === 'routeClientEntry' ||
276
+ task.kind === 'routeChunk' ||
277
+ task.kind === 'splitRouteExports')
278
+ ) {
279
+ const existingWorkerIndex = this.#splitRouteAnalysisWorkers.get(
280
+ task.resourcePath
281
+ );
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
+ }
290
+ if (
291
+ this.balanceRouteModuleTransforms &&
292
+ task.kind === 'routeModule' &&
293
+ !(task.environmentName === 'web' && !task.ssr && task.isSpaMode)
294
+ ) {
295
+ const workerIndex = this.#nextRouteModuleWorkerIndex % workerCount;
296
+ this.#nextRouteModuleWorkerIndex += 1;
297
+ return workerIndex;
298
+ }
299
+ return hashString(task.resourcePath) % workerCount;
300
+ }
301
+ }
302
+
303
+ export const createRouteTransformExecutor = ({
304
+ parallelRouteTransform,
305
+ routeChunkCache,
306
+ splitRouteModules,
307
+ }: RouteTransformExecutorOptions = {}): RouteTransformExecutor => {
308
+ const options = { routeChunkCache };
309
+ if (
310
+ parallelRouteTransform === undefined ||
311
+ parallelRouteTransform === false
312
+ ) {
313
+ return {
314
+ run: task => executeRouteTransformTask(task, options),
315
+ close: async () => {},
316
+ };
317
+ }
318
+
319
+ const workerCount = getConfiguredWorkerCount(parallelRouteTransform);
320
+ if (workerCount < 1) {
321
+ return {
322
+ run: task => executeRouteTransformTask(task, options),
323
+ close: async () => {},
324
+ };
325
+ }
326
+
327
+ return new ParallelRouteTransformExecutor(
328
+ workerCount,
329
+ options,
330
+ Boolean(splitRouteModules)
331
+ );
332
+ };