rsbuild-plugin-react-router 0.7.2 → 0.8.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 (53) hide show
  1. package/README.md +14 -2
  2. package/dist/468.js +48 -0
  3. package/dist/511.js +60 -86
  4. package/dist/819.js +64 -0
  5. package/dist/build-output-transforms.d.ts +3 -1
  6. package/dist/constants.d.ts +1 -0
  7. package/dist/dev-hdr-channel.d.ts +9 -0
  8. package/dist/dev-hmr.d.ts +1 -22
  9. package/dist/dev-runtime-controller.d.ts +1 -6
  10. package/dist/dev-server.d.ts +3 -1
  11. package/dist/index.cjs +5417 -5080
  12. package/dist/index.js +1701 -1441
  13. package/dist/manifest-assets.d.ts +34 -0
  14. package/dist/manifest-snapshot.d.ts +17 -0
  15. package/dist/manifest-state.d.ts +12 -0
  16. package/dist/manifest.d.ts +2 -22
  17. package/dist/modify-browser-manifest.d.ts +2 -0
  18. package/dist/node-only-manifest.d.ts +8 -0
  19. package/dist/plugin-utils.d.ts +1 -1
  20. package/dist/rsc-prerender.d.ts +3 -2
  21. package/dist/server-build-worker-client.d.ts +20 -0
  22. package/dist/server-build-worker-protocol.d.ts +84 -0
  23. package/dist/server-build-worker.d.ts +1 -0
  24. package/dist/server-build-worker.js +123 -0
  25. package/dist/server-utils.d.ts +1 -2
  26. package/dist/types.d.ts +6 -0
  27. package/package.json +4 -4
  28. package/src/build-output-transforms.ts +22 -1
  29. package/src/classic-mode.ts +0 -1
  30. package/src/constants.ts +3 -0
  31. package/src/dev-hdr-channel.ts +38 -0
  32. package/src/dev-hmr.ts +57 -100
  33. package/src/dev-runtime-controller.ts +23 -18
  34. package/src/dev-server.ts +24 -4
  35. package/src/index.ts +229 -144
  36. package/src/lazy-compilation.ts +7 -2
  37. package/src/manifest-assets.ts +228 -0
  38. package/src/manifest-snapshot.ts +80 -0
  39. package/src/manifest-state.ts +71 -0
  40. package/src/manifest.ts +23 -161
  41. package/src/mode-plan.ts +12 -4
  42. package/src/modify-browser-manifest.ts +47 -18
  43. package/src/node-only-manifest.ts +52 -0
  44. package/src/plugin-utils.ts +6 -2
  45. package/src/prerender-build.ts +76 -86
  46. package/src/route-chunks.ts +152 -63
  47. package/src/rsc-prerender.ts +7 -35
  48. package/src/server-build-resolution.ts +1 -2
  49. package/src/server-build-worker-client.ts +221 -0
  50. package/src/server-build-worker-protocol.ts +69 -0
  51. package/src/server-build-worker.ts +192 -0
  52. package/src/server-utils.ts +0 -2
  53. package/src/types.ts +7 -0
@@ -1,10 +1,13 @@
1
1
  import {
2
2
  Analyzer,
3
+ SymbolFlags,
4
+ type Export as YukuExport,
3
5
  type Module,
6
+ type Reference as YukuReference,
4
7
  type Symbol as YukuSymbol,
5
8
  } from 'yuku-analyzer';
6
9
  import { print } from 'yuku-codegen';
7
- import { walk } from 'yuku-parser';
10
+ import { walk, type Node } from 'yuku-parser';
8
11
  import { dirname, normalize, relative, resolve } from 'pathe';
9
12
  import { SERVER_ONLY_ROUTE_EXPORTS_SET } from './constants.js';
10
13
  import { createRouteId } from './plugin-utils.js';
@@ -146,6 +149,7 @@ type ExportDependencies = {
146
149
  importedIdentifierNames: Set<string>;
147
150
  importSources: Set<string>;
148
151
  exportedVariableDeclarators: Set<AnyNode>;
152
+ exportedLocalSymbols: Set<YukuSymbol>;
149
153
  };
150
154
 
151
155
  const getTopLevelStatementForNode = (
@@ -162,30 +166,30 @@ const getTopLevelStatementForNode = (
162
166
  return current;
163
167
  };
164
168
 
165
- const getVariableDeclaratorForNode = (
169
+ const getExportedVariableDeclaratorForNode = (
166
170
  module: Module,
167
171
  node: AnyNode
168
172
  ): AnyNode | null => {
169
- let current: AnyNode | null = node;
170
- while (current) {
171
- if (current.type === 'VariableDeclarator') {
172
- return current;
173
+ let current = node as Node;
174
+ while (true) {
175
+ const parent = module.parentOf(current);
176
+ if (!parent || parent.type === 'Program') {
177
+ return null;
173
178
  }
174
- current = module.parentOf(current as never) as AnyNode | null;
175
- }
176
- return null;
177
- };
178
-
179
- const isTopLevelExportedVariableDeclarator = (
180
- module: Module,
181
- node: AnyNode
182
- ): boolean => {
183
- const declaration = module.parentOf(node as never) as AnyNode | null;
184
- if (declaration?.type !== 'VariableDeclaration') {
185
- return false;
179
+ if (
180
+ current.type === 'VariableDeclarator' &&
181
+ parent.type === 'VariableDeclaration'
182
+ ) {
183
+ const exported = module.parentOf(parent);
184
+ if (
185
+ exported?.type === 'ExportNamedDeclaration' &&
186
+ module.parentOf(exported)?.type === 'Program'
187
+ ) {
188
+ return current;
189
+ }
190
+ }
191
+ current = parent;
186
192
  }
187
- const statement = module.parentOf(declaration as never) as AnyNode | null;
188
- return statement?.type === 'ExportNamedDeclaration';
189
193
  };
190
194
 
191
195
  const getExportedName = (exported: AnyNode): string => {
@@ -241,9 +245,29 @@ const getExportDependencies = (
241
245
  code,
242
246
  () => {
243
247
  const { module } = analyzeCode(code, cache, cacheKey);
248
+ const namedExports = module.exports.filter(
249
+ (exp): exp is YukuExport & { name: string } =>
250
+ exp.name !== null &&
251
+ !exp.typeOnly &&
252
+ !exp.isStar &&
253
+ !exp.isExportEquals
254
+ );
255
+ // Removing type declarations can change legacy decorator metadata and
256
+ // name hygiene. Preserve the original dependency graph for decorated
257
+ // modules because the downstream compiler options are not known here.
258
+ let hasDecorators = false;
259
+ walk(module.ast, {
260
+ Decorator(_node, context) {
261
+ hasDecorators = true;
262
+ context.stop();
263
+ },
264
+ });
244
265
  const exportDependencies = new Map<string, ExportDependencies>();
245
266
  const topLevelStatementCache = new Map<AnyNode, AnyNode>();
246
- const variableDeclaratorCache = new Map<AnyNode, AnyNode | null>();
267
+ const exportedVariableDeclaratorCache = new Map<
268
+ AnyNode,
269
+ AnyNode | null
270
+ >();
247
271
  const getCachedTopLevelStatementForNode = (node: AnyNode): AnyNode => {
248
272
  const cached = topLevelStatementCache.get(node);
249
273
  if (cached) {
@@ -254,14 +278,67 @@ const getExportDependencies = (
254
278
  return statement;
255
279
  };
256
280
 
257
- const getCachedVariableDeclaratorForNode = (
281
+ // Ordinary imports can be repeated in multiple chunks. Exported local
282
+ // bindings must keep a single owner, including functions and classes.
283
+ const nonShareableExportedSymbols = new Set<YukuSymbol>();
284
+ for (const { local } of namedExports) {
285
+ if (!local?.has(SymbolFlags.ValueSpace | SymbolFlags.ValueImport)) {
286
+ continue;
287
+ }
288
+ const isImport = local.declarations.every(
289
+ declaration =>
290
+ getCachedTopLevelStatementForNode(declaration).type ===
291
+ 'ImportDeclaration'
292
+ );
293
+ // Setup belongs to the imported value too. If another export consumes
294
+ // that value, moving its setup into a separate chunk changes behavior.
295
+ const hasSetup =
296
+ isImport &&
297
+ local.references.some(reference => {
298
+ const statement = getCachedTopLevelStatementForNode(reference.node);
299
+ return (
300
+ reference.kind === 'value' &&
301
+ statement.type !== 'ImportDeclaration' &&
302
+ !statement.type.startsWith('Export')
303
+ );
304
+ });
305
+ if (isImport && !hasSetup) continue;
306
+ nonShareableExportedSymbols.add(local);
307
+ }
308
+
309
+ const isValueImportEqualsReference = (
310
+ reference: YukuReference
311
+ ): boolean => {
312
+ let node: Node = reference.node;
313
+ let parent = module.parentOf(node);
314
+ while (parent?.type === 'TSQualifiedName') {
315
+ node = parent;
316
+ parent = module.parentOf(node);
317
+ }
318
+ // Yuku also marks the runtime RHS of `import x = Namespace.value`
319
+ // as a type reference.
320
+ return (
321
+ parent?.type === 'TSImportEqualsDeclaration' &&
322
+ parent.moduleReference === node &&
323
+ parent.importKind !== 'type'
324
+ );
325
+ };
326
+
327
+ const isRuntimeRelevantReference = (reference: YukuReference): boolean =>
328
+ hasDecorators ||
329
+ reference.kind === 'value' ||
330
+ isValueImportEqualsReference(reference);
331
+
332
+ const getCachedExportedVariableDeclaratorForNode = (
258
333
  node: AnyNode
259
334
  ): AnyNode | null => {
260
- if (variableDeclaratorCache.has(node)) {
261
- return variableDeclaratorCache.get(node) ?? null;
335
+ if (exportedVariableDeclaratorCache.has(node)) {
336
+ return exportedVariableDeclaratorCache.get(node) ?? null;
262
337
  }
263
- const declarator = getVariableDeclaratorForNode(module, node);
264
- variableDeclaratorCache.set(node, declarator);
338
+ // Only direct exported declarators can be emitted independently.
339
+ // Every other top-level statement is moved as a whole.
340
+ const declarator = getExportedVariableDeclaratorForNode(module, node);
341
+ exportedVariableDeclaratorCache.set(node, declarator);
265
342
  return declarator;
266
343
  };
267
344
 
@@ -291,22 +368,41 @@ const getExportDependencies = (
291
368
  importedIdentifierNames: new Set(),
292
369
  importSources: new Set(),
293
370
  exportedVariableDeclarators: new Set(),
371
+ exportedLocalSymbols: new Set(),
294
372
  };
295
373
  const visitedSymbols = new Set<YukuSymbol>();
296
374
  const scannedNodes = new Set<AnyNode>();
297
375
 
376
+ const visitIdentifier = (node: YukuReference['node']) => {
377
+ const reference = module.referenceOf(node);
378
+ if (reference) {
379
+ if (reference.symbol && isRuntimeRelevantReference(reference)) {
380
+ visitSymbol(reference.symbol);
381
+ }
382
+ return;
383
+ }
384
+ const symbol = module.symbolOf(node);
385
+ if (
386
+ symbol?.scope === module.rootScope &&
387
+ symbol.has(SymbolFlags.ValueSpace | SymbolFlags.ValueImport) &&
388
+ dependencies.topLevelNonModuleStatements.has(
389
+ getCachedTopLevelStatementForNode(node)
390
+ )
391
+ ) {
392
+ // Moving a statement also moves the bindings it declares. Follow
393
+ // their consumers so no references remain in another chunk.
394
+ visitSymbol(symbol);
395
+ }
396
+ };
397
+
298
398
  const scanNode = (node: AnyNode) => {
299
399
  if (scannedNodes.has(node)) {
300
400
  return;
301
401
  }
302
402
  scannedNodes.add(node);
303
403
  walk(node as any, {
304
- Identifier(node: AnyNode) {
305
- const reference = module.referenceOf(node as never);
306
- if (reference?.symbol) {
307
- visitSymbol(reference.symbol);
308
- }
309
- },
404
+ Identifier: visitIdentifier,
405
+ JSXIdentifier: visitIdentifier,
310
406
  });
311
407
  };
312
408
 
@@ -318,6 +414,9 @@ const getExportDependencies = (
318
414
  if (symbol.declarations.length === 0) {
319
415
  return;
320
416
  }
417
+ if (nonShareableExportedSymbols.has(symbol)) {
418
+ dependencies.exportedLocalSymbols.add(symbol);
419
+ }
321
420
 
322
421
  for (const declaration of symbol.declarations as AnyNode[]) {
323
422
  const statement = addCachedTopLevelStatement(
@@ -329,24 +428,27 @@ const getExportDependencies = (
329
428
  if (typeof statement.source?.value === 'string') {
330
429
  dependencies.importSources.add(statement.source.value);
331
430
  }
332
- return;
431
+ // Ordinary imports are shareable; a directly exported import
432
+ // also owns setup statements such as `load.hydrate = true`.
433
+ if (symbol !== localSymbol) return;
333
434
  }
334
- const declarator = getCachedVariableDeclaratorForNode(declaration);
335
- if (
336
- declarator &&
337
- isTopLevelExportedVariableDeclarator(module, declarator)
338
- ) {
435
+ const declarator =
436
+ getCachedExportedVariableDeclaratorForNode(declaration);
437
+ if (declarator) {
339
438
  dependencies.exportedVariableDeclarators.add(declarator);
340
439
  }
341
440
  scanNode(declarator ?? statement);
342
441
  }
343
442
 
344
- for (const reference of symbol.references as any[]) {
443
+ for (const reference of symbol.references) {
444
+ if (!isRuntimeRelevantReference(reference)) {
445
+ continue;
446
+ }
345
447
  const statement = addCachedTopLevelStatement(
346
448
  dependencies,
347
449
  reference.node
348
450
  );
349
- const declarator = getCachedVariableDeclaratorForNode(
451
+ const declarator = getCachedExportedVariableDeclaratorForNode(
350
452
  reference.node
351
453
  );
352
454
  scanNode(declarator ?? statement);
@@ -365,10 +467,7 @@ const getExportDependencies = (
365
467
  exportDependencies.set(exportName, dependencies);
366
468
  };
367
469
 
368
- for (const exp of module.exports as any[]) {
369
- if (exp.typeOnly || exp.isStar || exp.isExportEquals) {
370
- continue;
371
- }
470
+ for (const exp of namedExports) {
372
471
  handleExport(exp.name, exp.node as AnyNode, exp.local ?? null);
373
472
  }
374
473
 
@@ -383,7 +482,7 @@ const isExportChunkable = (
383
482
  importer: string
384
483
  ) => {
385
484
  const dependencies = exportDependencies.get(exportName);
386
- if (!dependencies) {
485
+ if (!dependencies || dependencies.exportedVariableDeclarators.size > 1) {
387
486
  return false;
388
487
  }
389
488
  if (exportName === 'clientLoader' && hasHydrateAssignment(dependencies)) {
@@ -403,29 +502,19 @@ const isExportChunkable = (
403
502
  setsIntersect(
404
503
  currentDependencies.topLevelNonModuleStatements,
405
504
  dependencies.topLevelNonModuleStatements
505
+ ) ||
506
+ setsIntersect(
507
+ currentDependencies.exportedVariableDeclarators,
508
+ dependencies.exportedVariableDeclarators
509
+ ) ||
510
+ setsIntersect(
511
+ currentDependencies.exportedLocalSymbols,
512
+ dependencies.exportedLocalSymbols
406
513
  )
407
514
  ) {
408
515
  return false;
409
516
  }
410
517
  }
411
- if (dependencies.exportedVariableDeclarators.size > 1) {
412
- return false;
413
- }
414
- if (dependencies.exportedVariableDeclarators.size > 0) {
415
- for (const [currentExportName, currentDependencies] of exportDependencies) {
416
- if (currentExportName === exportName) {
417
- continue;
418
- }
419
- if (
420
- setsIntersect(
421
- currentDependencies.exportedVariableDeclarators,
422
- dependencies.exportedVariableDeclarators
423
- )
424
- ) {
425
- return false;
426
- }
427
- }
428
- }
429
518
  return true;
430
519
  };
431
520
 
@@ -1,10 +1,10 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
- import { pathToFileURL } from 'node:url';
4
3
  import type { RsbuildPluginAPI } from '@rsbuild/core';
5
4
  import { dirname, relative, resolve } from 'pathe';
6
5
  import * as Effect from 'effect/Effect';
7
6
  import { PLUGIN_NAME, SPA_FALLBACK_HTML_FILE } from './constants.js';
7
+ import { startServerBuildWorker } from './server-build-worker-client.js';
8
8
  import {
9
9
  createBuildRequestEffect,
10
10
  createBoundedPrerenderTasksEffect,
@@ -27,8 +27,9 @@ import { runPluginEffect } from './effect-runtime.js';
27
27
  * inline `__FLIGHT_DATA` scripts, served for client-side navigations
28
28
  *
29
29
  * Instead of an HTTP round-trip through a preview server, the RSC server
30
- * bundle's default-exported `fetch` handler is invoked in-process, matching
31
- * how classic mode prerenders through `createRequestHandler`.
30
+ * bundle's default-exported `fetch` handler is invoked directly (in the
31
+ * server build worker), matching how classic mode prerenders through
32
+ * `createRequestHandler`.
32
33
  */
33
34
 
34
35
  export const SPA_FALLBACK_REQUEST_PATH: string = `/${SPA_FALLBACK_HTML_FILE}`;
@@ -145,29 +146,6 @@ const createRedirectHtml = ({
145
146
  </html>`;
146
147
  };
147
148
 
148
- const resolveRscRequestHandler = (
149
- buildModule: unknown,
150
- serverBuildPath: string
151
- ): RscRequestHandler => {
152
- const moduleRecord = buildModule as
153
- | { default?: { fetch?: unknown; default?: { fetch?: unknown } } }
154
- | undefined;
155
- const handler =
156
- typeof moduleRecord?.default?.fetch === 'function'
157
- ? moduleRecord.default.fetch
158
- : typeof moduleRecord?.default?.default?.fetch === 'function'
159
- ? moduleRecord.default.default.fetch
160
- : null;
161
- if (!handler) {
162
- throw new Error(
163
- `[${PLUGIN_NAME}] RSC server build ${JSON.stringify(
164
- serverBuildPath
165
- )} must default-export an object with a fetch function.`
166
- );
167
- }
168
- return handler as RscRequestHandler;
169
- };
170
-
171
149
  const writePrerenderedFile = async ({
172
150
  api,
173
151
  clientBuildDir,
@@ -330,11 +308,9 @@ export const runReactRouterRscPrerenderBuild = async (
330
308
  const clientBuildDir = resolve(buildDirectory, 'client');
331
309
  await mkdir(clientBuildDir, { recursive: true });
332
310
 
333
- const previousBuildRequestFlag = process.env.IS_RR_BUILD_REQUEST;
334
- process.env.IS_RR_BUILD_REQUEST = 'yes';
311
+ const worker = await startServerBuildWorker({ serverBuildPath, mode: 'rsc' });
335
312
  try {
336
- const buildModule = await import(pathToFileURL(serverBuildPath).toString());
337
- const handler = resolveRscRequestHandler(buildModule, serverBuildPath);
313
+ const handler: RscRequestHandler = worker.handler;
338
314
 
339
315
  api.logger.info(`Prerender: ${prerenderRequests.length} path(s)...`);
340
316
 
@@ -353,10 +329,6 @@ export const runReactRouterRscPrerenderBuild = async (
353
329
  )
354
330
  );
355
331
  } finally {
356
- if (previousBuildRequestFlag === undefined) {
357
- delete process.env.IS_RR_BUILD_REQUEST;
358
- } else {
359
- process.env.IS_RR_BUILD_REQUEST = previousBuildRequestFlag;
360
- }
332
+ await worker.close();
361
333
  }
362
334
  };
@@ -1,7 +1,6 @@
1
1
  // Internal module: exposes ServerBuild resolution used by dev-runtime code.
2
2
  // External callers go through the Promise wrappers in server-utils.ts.
3
3
  import type { ServerBuild } from 'react-router';
4
- import { normalizeEffectError } from './effect-runtime.js';
5
4
 
6
5
  const RESOLVABLE_BUILD_EXPORTS = new Set([
7
6
  'allowedActionOrigins',
@@ -116,6 +115,6 @@ export async function resolveServerBuildModule(
116
115
  `[rsbuild-plugin-react-router] ${source} did not contain a valid React Router ServerBuild.`
117
116
  );
118
117
  } catch (cause) {
119
- throw normalizeEffectError(cause);
118
+ throw cause instanceof Error ? cause : new Error(String(cause));
120
119
  }
121
120
  }
@@ -0,0 +1,221 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { Worker } from 'node:worker_threads';
3
+ import { normalizeEffectError } from './effect-runtime.js';
4
+ import {
5
+ headerEntries,
6
+ type ServerBuildDescription,
7
+ type ServerBuildWorkerData,
8
+ type ServerBuildWorkerRequest,
9
+ type ServerBuildWorkerResponse,
10
+ } from './server-build-worker-protocol.js';
11
+
12
+ const defaultWorkerPath = fileURLToPath(
13
+ new URL('./server-build-worker.js', import.meta.url)
14
+ );
15
+
16
+ export type ServerBuildWorker = {
17
+ /** Plain-data view of the classic server build (routes, assets, prerender). */
18
+ description: ServerBuildDescription | undefined;
19
+ /** Runs the request against the server build in the worker. */
20
+ handler(request: Request): Promise<Response>;
21
+ /** Terminates the worker, and with it any handle the server graph opened. */
22
+ close(): Promise<void>;
23
+ };
24
+
25
+ type Reply = Exclude<ServerBuildWorkerResponse, { type: 'ready' | 'closed' }>;
26
+
27
+ type Pending = {
28
+ resolve: (reply: Reply) => void;
29
+ reject: (error: Error) => void;
30
+ };
31
+
32
+ const replyError = (reply: Extract<Reply, { ok: false }>): Error => {
33
+ const error = new Error(reply.error.message);
34
+ error.name = reply.error.name ?? error.name;
35
+ if (reply.error.stack) {
36
+ error.stack = reply.error.stack;
37
+ }
38
+ return error;
39
+ };
40
+
41
+ /**
42
+ * Evaluate a built server bundle in a worker thread and proxy requests to it.
43
+ * Build-time rendering used to `import()` the bundle into the build process;
44
+ * a module-scope handle in the app's server graph then kept `rsbuild build`
45
+ * alive forever (#135). The worker is terminated by `close()`.
46
+ *
47
+ * The worker's `exit` is its final event, so any exit (including one between
48
+ * requests, e.g. the app calling `process.exit`) is terminal: outstanding and
49
+ * later requests reject instead of waiting for a reply that cannot come.
50
+ */
51
+ export const startServerBuildWorker = async (
52
+ data: ServerBuildWorkerData,
53
+ // Tests run from `src/` and point this at the built worker.
54
+ workerPath: string = defaultWorkerPath
55
+ ): Promise<ServerBuildWorker> => {
56
+ const worker = new Worker(workerPath, { workerData: data });
57
+ const pending = new Map<number, Pending>();
58
+ const active = new Map<number, (error: Error) => void>();
59
+ let nextId = 0;
60
+ let failure: Error | undefined;
61
+ let acknowledgeClose: () => void = () => {};
62
+ const closed = new Promise<void>(resolve => {
63
+ acknowledgeClose = resolve;
64
+ });
65
+
66
+ const fail = (error: Error): void => {
67
+ failure ??= error;
68
+ for (const { reject } of pending.values()) {
69
+ reject(failure);
70
+ }
71
+ pending.clear();
72
+ for (const stop of active.values()) stop(failure);
73
+ active.clear();
74
+ };
75
+
76
+ const ready = new Promise<ServerBuildDescription | undefined>(
77
+ (resolve, reject) => {
78
+ worker.on('message', (message: ServerBuildWorkerResponse) => {
79
+ if (message.type === 'closed') {
80
+ acknowledgeClose();
81
+ return;
82
+ }
83
+ if (message.type === 'ready') {
84
+ resolve(message.description);
85
+ return;
86
+ }
87
+ const entry = pending.get(message.id);
88
+ pending.delete(message.id);
89
+ entry?.resolve(message);
90
+ });
91
+ worker.on('error', error => {
92
+ acknowledgeClose();
93
+ fail(normalizeEffectError(error));
94
+ reject(failure);
95
+ });
96
+ worker.on('exit', code => {
97
+ acknowledgeClose();
98
+ fail(new Error(`Server build worker exited with code ${code}`));
99
+ reject(failure);
100
+ });
101
+ }
102
+ );
103
+
104
+ const send = (
105
+ request: ServerBuildWorkerRequest,
106
+ transfer: ArrayBuffer[] = []
107
+ ): void => {
108
+ worker.postMessage(request, transfer);
109
+ };
110
+
111
+ // Import errors surface as worker 'error' events, an early exit as 'exit'.
112
+ const description = await ready;
113
+
114
+ return {
115
+ description,
116
+ async handler(request) {
117
+ const id = nextId++;
118
+ const body = request.body
119
+ ? new Uint8Array(await request.arrayBuffer())
120
+ : undefined;
121
+ let responseController:
122
+ | ReadableStreamDefaultController<Uint8Array>
123
+ | undefined;
124
+ const cleanup = (): void => {
125
+ active.delete(id);
126
+ request.signal.removeEventListener('abort', onAbort);
127
+ };
128
+ const stop = (error: Error): void => {
129
+ responseController?.error(error);
130
+ pending.get(id)?.reject(error);
131
+ pending.delete(id);
132
+ cleanup();
133
+ };
134
+ // Keep the relay alive after headers arrive: RSC may release a redirect
135
+ // or rejected status without ever consuming its response body.
136
+ const onAbort = (): void => {
137
+ send({ type: 'abort', id });
138
+ if (responseController)
139
+ stop(new Error('Server build request was aborted'));
140
+ };
141
+ const reply = await new Promise<Reply>((resolve, reject) => {
142
+ if (failure) {
143
+ reject(failure);
144
+ return;
145
+ }
146
+ pending.set(id, { resolve, reject });
147
+ active.set(id, stop);
148
+ request.signal.addEventListener('abort', onAbort, { once: true });
149
+ send(
150
+ {
151
+ type: 'request',
152
+ id,
153
+ url: request.url,
154
+ method: request.method,
155
+ headers: headerEntries(request.headers),
156
+ body,
157
+ },
158
+ body ? [body.buffer] : []
159
+ );
160
+ if (request.signal.aborted) onAbort();
161
+ }).catch(error => {
162
+ cleanup();
163
+ throw error;
164
+ });
165
+ if (!reply.ok) {
166
+ cleanup();
167
+ throw replyError(reply);
168
+ }
169
+ if (reply.type !== 'reply')
170
+ throw new Error('Unexpected server build response');
171
+ const { status, statusText, headers, hasBody } = reply.response;
172
+ const responseBody = hasBody
173
+ ? new ReadableStream<Uint8Array>(
174
+ {
175
+ start(controller) {
176
+ responseController = controller;
177
+ },
178
+ async pull(controller) {
179
+ try {
180
+ const result = await new Promise<Reply>((resolve, reject) => {
181
+ if (failure) {
182
+ reject(failure);
183
+ return;
184
+ }
185
+ pending.set(id, { resolve, reject });
186
+ send({ type: 'read', id });
187
+ });
188
+ if (!result.ok) throw replyError(result);
189
+ if (result.type !== 'body')
190
+ throw new Error('Unexpected server build body');
191
+ controller.enqueue(result.body);
192
+ controller.close();
193
+ } catch (error) {
194
+ controller.error(error);
195
+ } finally {
196
+ cleanup();
197
+ }
198
+ },
199
+ cancel() {
200
+ send({ type: 'abort', id });
201
+ stop(new Error('Server build response was canceled'));
202
+ },
203
+ },
204
+ { highWaterMark: 0 }
205
+ )
206
+ : null;
207
+ if (!hasBody) cleanup();
208
+ return new Response(responseBody, {
209
+ status,
210
+ statusText,
211
+ headers,
212
+ });
213
+ },
214
+ async close() {
215
+ fail(new Error('Server build worker was closed'));
216
+ send({ type: 'close' });
217
+ await closed;
218
+ await worker.terminate();
219
+ },
220
+ };
221
+ };