Skip to content

Commit 76d74ed

Browse files
committed
Push comment in ReactServerDOMEsbuild
1 parent e1f8734 commit 76d74ed

3 files changed

Lines changed: 215 additions & 9 deletions

File tree

packages/react-server-dom-esbuild/ReactServerDOMEsbuild.js

Lines changed: 212 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
11
/*
22
* This file is a bundler integration between react (react-client/flight), esbuild and server-reason-react.
33
*
4+
* React's Flight client (`react-client/flight`) is a factory function that accepts a `$$$config`
5+
* object with bundler-specific implementations. Each official React integration (webpack, parcel, etc.)
6+
* provides its own config. This file is the esbuild-specific config for server-reason-react.
7+
*
8+
* The `$$$config` object is composed from three groups of options plus renderer metadata:
9+
* 1. **Stream config** — how to decode binary chunks from the RSC stream into strings.
10+
* 2. **Console config** — how to replay server-side console logs on the client (dev only).
11+
* 3. **Bundler config** — how to resolve and load client/server modules at runtime.
12+
* 4. **Renderer metadata** — version and package name for React DevTools integration.
13+
*
414
* Similar resources:
515
* - **react-server-dom-webpack**: https://github.com/facebook/react/blob/5c56b873efb300b4d1afc4ba6f16acf17e4e5800/packages/react-server-dom-webpack/src/ReactFlightWebpackPlugin.js#L156-L194
616
* - **react-server-dom-parcel**: https://github.com/facebook/react/pull/31725
7-
*/
17+
*
18+
* ## Why `@pedrobslisboa/react-client`?
19+
*
20+
* React's `react-client` package (which provides the Flight protocol deserializer)
21+
* is an internal package that is NOT published to npm by the React team.
22+
* It is only consumed internally by React's own bundler integrations (webpack, parcel, esm).
23+
*
24+
* Since server-reason-react needs a custom esbuild integration, and `react-client`
25+
* is the intended extension point (via the `$$$config` injection pattern), Pedro
26+
* (a core contributor to server-reason-react) republished the package under
27+
* `@pedrobslisboa/react-client` so this project can use the Flight client factory directly.
28+
*/
829

930
import ReactClientFlight from "@pedrobslisboa/react-client/flight";
1031

@@ -16,15 +37,36 @@ const debug = (...args) => {
1637
}
1738
};
1839

40+
/*
41+
* Stream config — tells the Flight client how to decode binary chunks into strings.
42+
*
43+
* These three functions are called during `processBinaryChunk` to turn raw
44+
* `Uint8Array` buffers from the ReadableStream into string content that the
45+
* Flight protocol parser can process.
46+
*/
1947
const ReactFlightClientStreamConfigWeb = {
48+
/*
49+
* Creates a TextDecoder instance used for all subsequent string decoding.
50+
* Stored as `response._stringDecoder` on the Flight response object.
51+
*/
2052
createStringDecoder() {
2153
return new TextDecoder();
2254
},
2355

56+
/*
57+
* Decodes a partial binary chunk in streaming mode (`{ stream: true }`).
58+
* Called for every buffer segment except the last one in a row.
59+
* The `stream: true` option prevents the decoder from flushing incomplete
60+
* multi-byte characters, allowing them to be completed by subsequent chunks.
61+
*/
2462
readPartialStringChunk(decoder, buffer) {
2563
return decoder.decode(buffer, { stream: true });
2664
},
2765

66+
/*
67+
* Decodes the final binary chunk of a row (without `stream: true`).
68+
* This flushes any remaining bytes in the decoder's internal buffer.
69+
*/
2870
readFinalStringChunk(decoder, buffer) {
2971
return decoder.decode(buffer);
3072
},
@@ -47,7 +89,25 @@ const pad = " ";
4789

4890
const bind = Function.prototype.bind;
4991

92+
/*
93+
* Console config — tells the Flight client how to replay server-side console
94+
* logs on the client with a badge indicating the server environment.
95+
*
96+
* In production builds, `bindToConsole` is extracted from the config but
97+
* never actually called (dead code). In development, it is called for each
98+
* replayed server console message with the method name, args, and environment
99+
* badge name.
100+
*/
50101
const ReactClientConsoleConfigBrowser = {
102+
/*
103+
* Wraps a console method call with badge formatting so that replayed
104+
* server logs appear with a visual tag (e.g., "[Server]") in the browser console.
105+
*
106+
* @param methodName - The console method (e.g., "log", "warn", "error", "assert")
107+
* @param args - The original arguments passed to the console method on the server
108+
* @param badgeName - The environment name to display as a badge (e.g., "Server")
109+
* @returns A bound console function ready to be called
110+
*/
51111
bindToConsole(methodName, args, badgeName) {
52112
let offset = 0;
53113
switch (methodName) {
@@ -92,16 +152,49 @@ const ReactClientConsoleConfigBrowser = {
92152
},
93153
};
94154

155+
/* Indices into the metadata tuple returned by the RSC stream for client component references. */
95156
const ID = 0;
96157
const NAME = 1;
97158
const BUNDLES = 2;
98159

160+
/*
161+
* Bundler config — tells the Flight client how to resolve and load modules.
162+
*
163+
* These functions bridge between the abstract module references in the RSC
164+
* stream and the actual runtime modules available in the browser. In the
165+
* esbuild integration, client components and server functions are registered
166+
* in global manifest maps (`window.__client_manifest_map` and
167+
* `window.__server_functions_manifest_map`) by the esbuild build plugin.
168+
*/
99169
const ReactFlightClientConfigBundlerEsbuild = {
170+
/*
171+
* Called when the Flight client encounters a client module reference in the stream.
172+
* Allows the integration to initiate loading of scripts/stylesheets needed by the module.
173+
*
174+
* In the esbuild integration this is a no-op because all client bundles are
175+
* already loaded via script tags — there's no dynamic chunk loading.
176+
*
177+
* @param moduleLoading - The `moduleLoading` config passed to `createResponse` (null for esbuild)
178+
* @param nonce - CSP nonce for script injection (undefined for esbuild)
179+
* @param metadata - The parsed module metadata from the RSC stream
180+
*/
100181
prepareDestinationForModule(moduleLoading, nonce, metadata) {
101182
debug("prepareDestinationForModule", moduleLoading, nonce, metadata);
102183
return;
103184
},
104185

186+
/*
187+
* Called to resolve a client component reference from the RSC stream into
188+
* a bundler-specific reference object. The returned object is later passed
189+
* to `preloadModule` and `requireModule`.
190+
*
191+
* In the esbuild integration, metadata comes as a tuple [id, name, bundles]
192+
* and we restructure it into a typed object.
193+
*
194+
* @param bundlerConfig - The `bundlerConfig` passed to `createResponse` (null for esbuild)
195+
* @param metadata - The serialized module reference from the RSC stream [id, name, bundles]
196+
* @returns An object with { type, id, name, bundles } used by `requireModule`
197+
*/
105198
resolveClientReference(bundlerConfig, metadata) {
106199
debug("resolveClientReference", bundlerConfig, metadata);
107200
// Reference is already resolved during the build
@@ -113,6 +206,16 @@ const ReactFlightClientConfigBundlerEsbuild = {
113206
};
114207
},
115208

209+
/*
210+
* Called to resolve a server function reference from the RSC stream.
211+
* Only called when `serverReferenceConfig` (second arg to `createResponse`)
212+
* is truthy. When falsy, server references fall back to `createBoundServerReference`
213+
* which uses `callServer` directly.
214+
*
215+
* @param bundlerConfig - The `serverReferenceConfig` passed to `createResponse` ({} for esbuild)
216+
* @param ref - The server reference ID string from the RSC stream
217+
* @returns An object with { type, id } used by `requireModule`
218+
*/
116219
resolveServerReference(bundlerConfig, ref) {
117220
debug("resolveServerReference", bundlerConfig, ref);
118221

@@ -122,12 +225,35 @@ const ReactFlightClientConfigBundlerEsbuild = {
122225
};
123226
},
124227

228+
/*
229+
* Called to optionally preload a module before it's required. Should return
230+
* a thenable/promise if async loading is needed, or a falsy value if the
231+
* module is already available synchronously.
232+
*
233+
* In the esbuild integration this always returns undefined because all modules
234+
* are pre-loaded via the global manifest maps.
235+
*
236+
* @param metadata - The resolved reference from `resolveClientReference` or `resolveServerReference`
237+
* @returns undefined (no preloading needed)
238+
*/
125239
preloadModule(metadata) {
126240
debug("preloadModule", metadata);
127241
/* TODO: Does it make sense to preload a module in esbuild? */
128242
return undefined;
129243
},
130244

245+
/*
246+
* Called to synchronously obtain the actual module export (component or function).
247+
* This is the final step — the returned value is what React will render or invoke.
248+
*
249+
* Looks up modules from two global manifest maps populated by the esbuild build plugin:
250+
* - `window.__client_manifest_map` — maps client component IDs to their React components
251+
* - `window.__server_functions_manifest_map` — maps server function IDs to their callable functions
252+
*
253+
* @param metadata - The resolved reference with { type, id } from resolve*Reference
254+
* @returns The actual React component or server function
255+
* @throws If the module is not found in the manifest
256+
*/
131257
requireModule(metadata) {
132258
const getModule = (type, id) => {
133259
switch (type) {
@@ -151,27 +277,60 @@ const ReactFlightClientConfigBundlerEsbuild = {
151277
},
152278
};
153279

154-
/* TODO: Can we use the real thing, instead of mocks/vendored code here? */
280+
/*
281+
* The assembled config object passed to `ReactClientFlight($$$config)`.
282+
*
283+
* Combines all three config groups plus renderer metadata. The Flight client
284+
* destructures this to extract each function/value at module initialization time.
285+
*
286+
* TODO: Can we use the real thing, instead of mocks/vendored code here?
287+
*/
155288
const ReactServerDOMEsbuildConfig = {
156289
...ReactFlightClientStreamConfigWeb,
157290
...ReactClientConsoleConfigBrowser,
158291
...ReactFlightClientConfigBundlerEsbuild,
159-
rendererVersion: "19.0.0",
292+
293+
/* Reported to React DevTools via `__REACT_DEVTOOLS_GLOBAL_HOOK__` for identification. */
294+
rendererVersion: "19.1.0",
295+
296+
/* Reported to React DevTools via `__REACT_DEVTOOLS_GLOBAL_HOOK__` for identification. */
160297
rendererPackageName: "react-server-dom-esbuild",
298+
299+
/*
300+
* Indicates this Flight client is used with SSR. Currently extracted from the config
301+
* but NOT read by the `react-client/flight` internals — it has no runtime effect.
302+
* May be a forward-looking property for future React versions.
303+
*/
161304
usedWithSSR: true,
162305
};
163306

307+
/*
308+
* Initialize the Flight client with our esbuild-specific config.
309+
* This returns an object with the core Flight protocol functions.
310+
*/
164311
const {
312+
/* Creates a new Flight response object that accumulates streamed RSC data. */
165313
createResponse,
314+
/* Creates a reference to a server function that can be called from the client. */
166315
createServerReference: createServerReferenceImpl,
316+
/* Serializes a value (e.g., server action arguments) into a format suitable for sending to the server. */
167317
processReply,
318+
/* Returns the root promise of a Flight response — resolves to the React element tree. */
168319
getRoot,
320+
/* Reports a top-level error to all pending chunks in the response. */
169321
reportGlobalError,
322+
/* Processes a binary chunk from the ReadableStream into the Flight response. */
170323
processBinaryChunk,
324+
/* Creates a stream state object used to track binary chunk processing. */
171325
createStreamState,
326+
/* Signals that the stream is complete and no more chunks will arrive. */
172327
close,
173328
} = ReactClientFlight(ReactServerDOMEsbuildConfig);
174329

330+
/*
331+
* Reads from a ReadableStream and feeds binary chunks into the Flight response.
332+
* Continues reading until the stream is done, then closes the response.
333+
*/
175334
function startReadingFromStream(response, stream) {
176335
const streamState = createStreamState();
177336
const reader = stream.getReader();
@@ -193,6 +352,10 @@ function startReadingFromStream(response, stream) {
193352
reader.read().then(progress).catch(error);
194353
}
195354

355+
/*
356+
* Wraps `callServer` to provide a helpful error if no callback was registered.
357+
* The returned function is passed as the `callServer` parameter to `createResponse`.
358+
*/
196359
function callCurrentServerCallback(callServer) {
197360
return function (id, args) {
198361
if (!callServer) {
@@ -204,16 +367,41 @@ function callCurrentServerCallback(callServer) {
204367
};
205368
}
206369

370+
/*
371+
* Public API: Creates a Flight response from a ReadableStream.
372+
* Returns a thenable that resolves to the deserialized React element tree.
373+
*
374+
* @param stream - A ReadableStream containing the RSC payload
375+
* @param options - Optional config: { callServer, temporaryReferences }
376+
*/
207377
export function createFromReadableStream(stream, options) {
208378
const response = createResponseFromOptions(options);
209379
startReadingFromStream(response, stream);
210380
return getRoot(response);
211381
}
212382

383+
/*
384+
* Internal helper to create a Flight response object from user-provided options.
385+
*
386+
* Maps the public API options to the internal `createResponse` parameters.
387+
*
388+
* Parameters to `createResponse`:
389+
* 1. bundlerConfig — null: client references are pre-resolved at build time by esbuild
390+
* 2. serverReferenceConfig — {}: truthy but empty, forces the `resolveServerReference` code path
391+
* (when null/falsy, server refs fall back to `createBoundServerReference` using only `callServer`)
392+
* 3. moduleLoading — null: no dynamic module loading config needed (scripts are pre-loaded)
393+
* 4. callServer — callback invoked when a server action is called from the client
394+
* 5. encodeFormAction — undefined: no custom form action encoding (uses default)
395+
* 6. nonce — undefined: no CSP nonce needed for script injection
396+
* 7. temporaryReferences — allows objects to be passed by reference between server/client
397+
* 8. findSourceMapURL — undefined: no source map resolution (DEV only)
398+
* 9. replayConsoleLogs — false: server console log replay is disabled
399+
* 10. environmentName — undefined: no custom environment badge name (DEV only, defaults to "Server")
400+
*/
213401
function createResponseFromOptions(options) {
214402
let response = createResponse(
215403
null, // bundlerConfig
216-
{}, // serverFunctionsConfig, this is the manifest that can contain configs related to server functions. It requires it to not be null, to run resolveServerReference
404+
{}, // serverReferenceConfig, this is the manifest that can contain configs related to server functions. It requires it to not be null, to run resolveServerReference
217405
null, // moduleLoading
218406
callCurrentServerCallback(options ? options.callServer : undefined),
219407
undefined, // encodeFormAction
@@ -231,6 +419,13 @@ function createResponseFromOptions(options) {
231419
return response;
232420
}
233421

422+
/*
423+
* Public API: Creates a Flight response from a fetch() promise.
424+
* Waits for the fetch to resolve, then reads the response body as a stream.
425+
*
426+
* @param promise - A Promise<Response> (e.g., from `fetch("/rsc")`)
427+
* @param options - Optional config: { callServer, temporaryReferences }
428+
*/
234429
export function createFromFetch(promise, options) {
235430
const response = createResponseFromOptions(options);
236431
promise.then(
@@ -244,8 +439,21 @@ export function createFromFetch(promise, options) {
244439
return getRoot(response);
245440
}
246441

442+
/*
443+
* Public API: Re-export of `createServerReference` from the Flight client.
444+
* Creates a callable reference to a server function identified by its ID.
445+
*/
247446
export const createServerReference = createServerReferenceImpl;
248447

448+
/*
449+
* Public API: Serializes a value into a format the server can decode.
450+
* Used to encode arguments when calling server actions.
451+
*
452+
* @param value - The value to serialize (can include React elements, FormData, etc.)
453+
* @param options.temporaryReferences - Optional map for temporary references
454+
* @param options.signal - Optional AbortSignal to cancel the encoding
455+
* @returns A Promise that resolves to the serialized form (string or FormData)
456+
*/
249457
export const encodeReply = (
250458
value,
251459
options = { temporaryReferences: undefined, signal: undefined }

packages/reactDom/src/ReactDOM.ml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,10 +295,8 @@ type stream_context = {
295295
let complete_boundary_script =
296296
{|function $RC(a,b){a=document.getElementById(a);b=document.getElementById(b);b.parentNode.removeChild(b);if(a){a=a.previousSibling;var f=a.parentNode,c=a.nextSibling,e=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d)if(0===e)break;else e--;else"$"!==d&&"$?"!==d&&"$!"!==d||e++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;b.firstChild;)f.insertBefore(b.firstChild,c);a.data="$";a._reactRetry&&a._reactRetry()}}|}
297297

298-
let replacement b s = Printf.sprintf "$RC('B:%i','S:%i')" b s
299-
300298
let write_inline_complete_boundary_script buf has_rc_script_been_injected boundary_id suspense_id =
301-
let rc_call = replacement boundary_id suspense_id in
299+
let rc_call = Printf.sprintf "$RC('B:%i','S:%i')" boundary_id suspense_id in
302300
if not has_rc_script_been_injected then (
303301
Buffer.add_string buf "<script>";
304302
Buffer.add_string buf complete_boundary_script;

packages/server-reason-react-ppx/server_reason_react_ppx.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,7 +1510,7 @@ let rewrite_structure_item ~nested_module_names structure_item =
15101510
in
15111511
match make_props_bindings with
15121512
| [] -> pstr_value ~loc:structure_item.pstr_loc rec_flag internal_bindings
1513-
| _ ->
1513+
| _ -> (
15141514
let loc = structure_item.pstr_loc in
15151515
let is_react_attr attr =
15161516
let name = attr.attr_name.txt in
@@ -1528,7 +1528,7 @@ let rewrite_structure_item ~nested_module_names structure_item =
15281528
[%%i pstr_value ~loc:structure_item.pstr_loc Nonrecursive public_bindings]
15291529
end]
15301530
in
1531-
(match (propagated_attrs, include_stri.pstr_desc) with
1531+
match (propagated_attrs, include_stri.pstr_desc) with
15321532
| _ :: _, Pstr_include incl ->
15331533
{ include_stri with pstr_desc = Pstr_include { incl with pincl_attributes = propagated_attrs } }
15341534
| _ -> include_stri)

0 commit comments

Comments
 (0)