From c91cd03a84d07a0ac99cee67df7d49391a5f450c Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 5 Mar 2025 18:57:48 -0500 Subject: [PATCH] Expose registerServerReference from the client builds This is used to register Server References that exist in the current environment but also exists in the server it might call into. Such as a remote server. If the value comes from the remote server in the first place then this is called automatically to ensure that you can pass a reference back to where it came from - even if the serverModuleMap option is used . --- .../react-client/src/ReactFlightClient.js | 21 +++++++++- .../src/ReactFlightReplyClient.js | 26 ++++++++---- .../src/client/ReactFlightDOMClientBrowser.js | 13 +++--- .../src/client/ReactFlightDOMClientNode.js | 2 + .../src/client/ReactFlightDOMClientBrowser.js | 2 + .../src/client/ReactFlightDOMClientEdge.js | 2 + .../src/client/ReactFlightDOMClientNode.js | 2 + .../src/client/ReactFlightDOMClientBrowser.js | 13 +++--- .../src/client/ReactFlightDOMClientEdge.js | 2 + .../src/client/ReactFlightDOMClientNode.js | 2 + .../src/__tests__/ReactFlightDOMEdge-test.js | 42 +++++++++++++++++++ .../__tests__/ReactFlightDOMReplyEdge-test.js | 29 ++++++++++++- .../src/client/ReactFlightDOMClientBrowser.js | 13 +++--- .../src/client/ReactFlightDOMClientEdge.js | 2 + .../src/client/ReactFlightDOMClientNode.js | 2 + .../src/ReactFlightReplyServer.js | 6 ++- 16 files changed, 141 insertions(+), 38 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 0eaf513a676d..3234814952fa 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -64,7 +64,10 @@ import { rendererPackageName, } from './ReactFlightClientConfig'; -import {createBoundServerReference} from './ReactFlightReplyClient'; +import { + createBoundServerReference, + registerBoundServerReference, +} from './ReactFlightReplyClient'; import {readTemporaryReference} from './ReactFlightTemporaryReferences'; @@ -1096,7 +1099,14 @@ function loadServerReference, T>( let promise: null | Thenable = preloadModule(serverReference); if (!promise) { if (!metaData.bound) { - return (requireModule(serverReference): any); + const resolvedValue = (requireModule(serverReference): any); + registerBoundServerReference( + resolvedValue, + metaData.id, + metaData.bound, + response._encodeFormAction, + ); + return resolvedValue; } else { promise = Promise.resolve(metaData.bound); } @@ -1128,6 +1138,13 @@ function loadServerReference, T>( resolvedValue = resolvedValue.bind.apply(resolvedValue, boundArgs); } + registerBoundServerReference( + resolvedValue, + metaData.id, + metaData.bound, + response._encodeFormAction, + ); + parentObject[key] = resolvedValue; // If this is the root object for a model reference, where `handler.value` diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 65d1129b5383..3fa37cd00c78 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -1125,11 +1125,12 @@ function createFakeServerFunction, T>( } } -function registerServerReference( - proxy: any, - reference: {id: ServerReferenceId, bound: null | Thenable>}, +export function registerBoundServerReference( + reference: T, + id: ServerReferenceId, + bound: null | Thenable>, encodeFormAction: void | EncodeFormActionCallback, -) { +): void { // Expose encoder for use by SSR, as well as a special bind that can be used to // keep server capabilities. if (usedWithSSR) { @@ -1147,13 +1148,22 @@ function registerServerReference( encodeFormAction, ); }; - Object.defineProperties((proxy: any), { + Object.defineProperties((reference: any), { $$FORM_ACTION: {value: $$FORM_ACTION}, $$IS_SIGNATURE_EQUAL: {value: isSignatureEqual}, bind: {value: bind}, }); } - knownServerReferences.set(proxy, reference); + knownServerReferences.set(reference, {id, bound}); +} + +export function registerServerReference( + reference: T, + id: ServerReferenceId, + encodeFormAction?: EncodeFormActionCallback, +): ServerReference { + registerBoundServerReference(reference, id, null, encodeFormAction); + return reference; } // $FlowFixMe[method-unbinding] @@ -1258,7 +1268,7 @@ export function createBoundServerReference, T>( ); } } - registerServerReference(action, {id, bound}, encodeFormAction); + registerBoundServerReference(action, id, bound, encodeFormAction); return action; } @@ -1358,6 +1368,6 @@ export function createServerReference, T>( ); } } - registerServerReference(action, {id, bound: null}, encodeFormAction); + registerBoundServerReference(action, id, null, encodeFormAction); return action; } diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js index afabc291041b..9ae47e3b5512 100644 --- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientBrowser.js @@ -25,9 +25,11 @@ import { injectIntoDevTools, } from 'react-client/src/ReactFlightClient'; -import { - processReply, +import {processReply} from 'react-client/src/ReactFlightReplyClient'; + +export { createServerReference, + registerServerReference, } from 'react-client/src/ReactFlightReplyClient'; import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; @@ -151,12 +153,7 @@ function encodeReply( }); } -export { - createFromFetch, - createFromReadableStream, - encodeReply, - createServerReference, -}; +export {createFromFetch, createFromReadableStream, encodeReply}; if (__DEV__) { injectIntoDevTools(); diff --git a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js index 2e1c5566423c..75c569e5ac4b 100644 --- a/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-esm/src/client/ReactFlightDOMClientNode.js @@ -26,6 +26,8 @@ import { import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + function noServerCall() { throw new Error( 'Server Functions cannot be called during initial render. ' + diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js index 7ea840b14079..3aca4a355dce 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientBrowser.js @@ -26,6 +26,8 @@ import { createServerReference as createServerReferenceImpl, } from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js index 8783cbfc6a7f..b1fbfed08f07 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientEdge.js @@ -25,6 +25,8 @@ import { createServerReference as createServerReferenceImpl, } from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; diff --git a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js index 8be06af9f203..b12a3a3ff49d 100644 --- a/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-parcel/src/client/ReactFlightDOMClientNode.js @@ -21,6 +21,8 @@ import { import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + function findSourceMapURL(filename: string, environmentName: string) { const devServer = parcelRequire.meta.devServer; if (devServer != null) { diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js index b6b55e4586e1..ee319beca18e 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientBrowser.js @@ -25,9 +25,11 @@ import { injectIntoDevTools, } from 'react-client/src/ReactFlightClient'; -import { - processReply, +import {processReply} from 'react-client/src/ReactFlightReplyClient'; + +export { createServerReference, + registerServerReference, } from 'react-client/src/ReactFlightReplyClient'; import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; @@ -150,12 +152,7 @@ function encodeReply( }); } -export { - createFromFetch, - createFromReadableStream, - encodeReply, - createServerReference, -}; +export {createFromFetch, createFromReadableStream, encodeReply}; if (__DEV__) { injectIntoDevTools(); diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js index 509950bc6513..48cb0dd4db13 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientEdge.js @@ -41,6 +41,8 @@ import { createServerReference as createServerReferenceImpl, } from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; diff --git a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js index 2ee76fa3b41c..919be523f882 100644 --- a/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-turbopack/src/client/ReactFlightDOMClientNode.js @@ -38,6 +38,8 @@ import { import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + function noServerCall() { throw new Error( 'Server Functions cannot be called during initial render. ' + diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index ba1ae3b64a85..5d3af9d4116f 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -301,6 +301,48 @@ describe('ReactFlightDOMEdge', () => { expect(result.boundMethod()).toBe('hi, there'); }); + it('should load a server reference on a consuming server and pass it back', async () => { + function greet(name) { + return 'hi, ' + name; + } + const ServerModule = serverExports({ + greet, + }); + + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream( + { + method: ServerModule.greet, + boundMethod: ServerModule.greet.bind(null, 'there'), + }, + webpackMap, + ), + ); + const response = ReactServerDOMClient.createFromReadableStream(stream, { + serverConsumerManifest: { + moduleMap: webpackMap, + serverModuleMap: webpackServerMap, + moduleLoading: webpackModuleLoading, + }, + }); + + const result = await response; + + expect(result.method).toBe(greet); + expect(result.boundMethod()).toBe('hi, there'); + + const body = await ReactServerDOMClient.encodeReply({ + method: result.method, + boundMethod: result.boundMethod, + }); + const replyResult = await ReactServerDOMServer.decodeReply( + body, + webpackServerMap, + ); + expect(replyResult.method).toBe(greet); + expect(replyResult.boundMethod()).toBe('hi, there'); + }); + it('should encode long string in a compact format', async () => { const testString = '"\n\t'.repeat(500) + '🙃'; const testString2 = 'hello'.repeat(400); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js index 2effa9868e99..7315e78c619d 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js @@ -22,7 +22,7 @@ if (typeof File === 'undefined' || typeof FormData === 'undefined') { global.FormData = require('undici').FormData; } -// let serverExports; +let serverExports; let webpackServerMap; let ReactServerDOMServer; let ReactServerDOMClient; @@ -36,7 +36,7 @@ describe('ReactFlightDOMReplyEdge', () => { require('react-server-dom-webpack/server.edge'), ); const WebpackMock = require('./utils/WebpackMock'); - // serverExports = WebpackMock.serverExports; + serverExports = WebpackMock.serverExports; webpackServerMap = WebpackMock.webpackServerMap; ReactServerDOMServer = require('react-server-dom-webpack/server.edge'); jest.resetModules(); @@ -308,4 +308,29 @@ describe('ReactFlightDOMReplyEdge', () => { expect(await decoded.a).toBe('hello'); expect(Array.from(await decoded.b)).toEqual(Array.from(buffer)); }); + + it('can pass a registered server reference', async () => { + function greet(name) { + return 'hi, ' + name; + } + const ServerModule = serverExports({ + greet, + }); + + ReactServerDOMClient.registerServerReference( + ServerModule.greet, + ServerModule.greet.$$id, + ); + + const body = await ReactServerDOMClient.encodeReply({ + method: ServerModule.greet, + boundMethod: ServerModule.greet.bind(null, 'there'), + }); + const replyResult = await ReactServerDOMServer.decodeReply( + body, + webpackServerMap, + ); + expect(replyResult.method).toBe(greet); + expect(replyResult.boundMethod()).toBe('hi, there'); + }); }); diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js index b6b55e4586e1..ee319beca18e 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js @@ -25,9 +25,11 @@ import { injectIntoDevTools, } from 'react-client/src/ReactFlightClient'; -import { - processReply, +import {processReply} from 'react-client/src/ReactFlightReplyClient'; + +export { createServerReference, + registerServerReference, } from 'react-client/src/ReactFlightReplyClient'; import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; @@ -150,12 +152,7 @@ function encodeReply( }); } -export { - createFromFetch, - createFromReadableStream, - encodeReply, - createServerReference, -}; +export {createFromFetch, createFromReadableStream, encodeReply}; if (__DEV__) { injectIntoDevTools(); diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js index 509950bc6513..48cb0dd4db13 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientEdge.js @@ -41,6 +41,8 @@ import { createServerReference as createServerReferenceImpl, } from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences'; diff --git a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js index 22c8928432b7..4118ad046d99 100644 --- a/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-webpack/src/client/ReactFlightDOMClientNode.js @@ -39,6 +39,8 @@ import { import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient'; +export {registerServerReference} from 'react-client/src/ReactFlightReplyClient'; + function noServerCall() { throw new Error( 'Server Functions cannot be called during initial render. ' + diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js index 4db7571bb66a..7c94352f288d 100644 --- a/packages/react-server/src/ReactFlightReplyServer.js +++ b/packages/react-server/src/ReactFlightReplyServer.js @@ -936,8 +936,10 @@ function parseModelString( // Server Reference const ref = value.slice(2); // TODO: Just encode this in the reference inline instead of as a model. - const metaData: {id: ServerReferenceId, bound: Thenable>} = - getOutlinedModel(response, ref, obj, key, createModel); + const metaData: { + id: ServerReferenceId, + bound: null | Thenable>, + } = getOutlinedModel(response, ref, obj, key, createModel); return loadServerReference( response, metaData.id,