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
930import 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+ */
1947const 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
4890const 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+ */
50101const 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. */
95156const ID = 0 ;
96157const NAME = 1 ;
97158const 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+ */
99169const 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+ */
155288const 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+ */
164311const {
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+ */
175334function 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+ */
196359function 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+ */
207377export 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+ */
213401function 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+ */
234429export 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+ */
247446export 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+ */
249457export const encodeReply = (
250458 value ,
251459 options = { temporaryReferences : undefined , signal : undefined }
0 commit comments