From c5129111c9fcca41b0dbb5fef613d1afb3128a7d Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 21 Oct 2023 17:04:13 -0700 Subject: [PATCH] Allow custom encoding of the form action --- .../react-client/src/ReactFlightClient.js | 12 +++- .../src/ReactFlightReplyClient.js | 59 +++++++++++++++++-- .../src/ReactFlightDOMClientBrowser.js | 1 + .../src/ReactFlightDOMClientNode.js | 9 ++- .../src/ReactFlightDOMClientBrowser.js | 1 + .../src/ReactFlightDOMClientEdge.js | 9 ++- .../src/ReactFlightDOMClientNode.js | 16 +++-- .../src/ReactFlightDOMClientBrowser.js | 1 + .../src/ReactFlightDOMClientEdge.js | 9 ++- .../src/ReactFlightDOMClientNode.js | 9 ++- 10 files changed, 110 insertions(+), 16 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 3fb97089f52be..f87a3e7b842a1 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -23,7 +23,10 @@ import type { HintModel, } from 'react-server/src/ReactFlightServerConfig'; -import type {CallServerCallback} from './ReactFlightReplyClient'; +import type { + CallServerCallback, + EncodeFormActionCallback, +} from './ReactFlightReplyClient'; import type {Postpone} from 'react/src/ReactPostpone'; @@ -50,7 +53,7 @@ import { import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry'; -export type {CallServerCallback}; +export type {CallServerCallback, EncodeFormActionCallback}; type UninitializedModel = string; @@ -183,6 +186,7 @@ export type Response = { _bundlerConfig: SSRModuleMap, _moduleLoading: ModuleLoading, _callServer: CallServerCallback, + _encodeFormAction: void | EncodeFormActionCallback, _nonce: ?string, _chunks: Map>, _fromJSON: (key: string, value: JSONValue) => any, @@ -548,7 +552,7 @@ function createServerReferenceProxy, T>( return callServer(metaData.id, bound.concat(args)); }); }; - registerServerReference(proxy, metaData); + registerServerReference(proxy, metaData, response._encodeFormAction); return proxy; } @@ -713,6 +717,7 @@ export function createResponse( bundlerConfig: SSRModuleMap, moduleLoading: ModuleLoading, callServer: void | CallServerCallback, + encodeFormAction: void | EncodeFormActionCallback, nonce: void | string, ): Response { const chunks: Map> = new Map(); @@ -720,6 +725,7 @@ export function createResponse( _bundlerConfig: bundlerConfig, _moduleLoading: moduleLoading, _callServer: callServer !== undefined ? callServer : missingCall, + _encodeFormAction: encodeFormAction, _nonce: nonce, _chunks: chunks, _stringDecoder: createStringDecoder(), diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index dc20eb55d2c1b..f3b308bb524f7 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -47,6 +47,11 @@ export opaque type ServerReference = T; export type CallServerCallback = (id: any, args: A) => Promise; +export type EncodeFormActionCallback = ( + id: any, + args: Promise, +) => ReactCustomFormAction; + export type ServerReferenceId = any; const knownServerReferences: WeakMap< @@ -454,7 +459,7 @@ function encodeFormData(reference: any): Thenable { return thenable; } -export function encodeFormAction( +function defaultEncodeFormAction( this: any => Promise, identifierPrefix: string, ): ReactCustomFormAction { @@ -503,6 +508,25 @@ export function encodeFormAction( }; } +function customEncodeFormAction( + proxy: any => Promise, + identifierPrefix: string, + encodeFormAction: EncodeFormActionCallback, +): ReactCustomFormAction { + const reference = knownServerReferences.get(proxy); + if (!reference) { + throw new Error( + 'Tried to encode a Server Action from a different instance than the encoder is from. ' + + 'This is a bug in React.', + ); + } + let boundPromise: Promise> = (reference.bound: any); + if (boundPromise === null) { + boundPromise = Promise.resolve([]); + } + return encodeFormAction(reference.id, boundPromise); +} + function isSignatureEqual( this: any => Promise, referenceId: ServerReferenceId, @@ -569,13 +593,27 @@ function isSignatureEqual( export function registerServerReference( proxy: any, reference: {id: ServerReferenceId, bound: null | Thenable>}, + encodeFormAction: void | EncodeFormActionCallback, ) { // Expose encoder for use by SSR, as well as a special bind that can be used to // keep server capabilities. if (usedWithSSR) { // Only expose this in builds that would actually use it. Not needed on the client. + const $$FORM_ACTION = + encodeFormAction === undefined + ? defaultEncodeFormAction + : function ( + this: any => Promise, + identifierPrefix: string, + ): ReactCustomFormAction { + return customEncodeFormAction( + this, + identifierPrefix, + encodeFormAction, + ); + }; Object.defineProperties((proxy: any), { - $$FORM_ACTION: {value: encodeFormAction}, + $$FORM_ACTION: {value: $$FORM_ACTION}, $$IS_SIGNATURE_EQUAL: {value: isSignatureEqual}, bind: {value: bind}, }); @@ -587,7 +625,7 @@ export function registerServerReference( const FunctionBind = Function.prototype.bind; // $FlowFixMe[method-unbinding] const ArraySlice = Array.prototype.slice; -function bind(this: Function) { +function bind(this: Function): Function { // $FlowFixMe[unsupported-syntax] const newFn = FunctionBind.apply(this, arguments); const reference = knownServerReferences.get(this); @@ -601,7 +639,17 @@ function bind(this: Function) { } else { boundPromise = Promise.resolve(args); } - registerServerReference(newFn, {id: reference.id, bound: boundPromise}); + // Expose encoder for use by SSR, as well as a special bind that can be used to + // keep server capabilities. + if (usedWithSSR) { + // Only expose this in builds that would actually use it. Not needed on the client. + Object.defineProperties((newFn: any), { + $$FORM_ACTION: {value: this.$$FORM_ACTION}, + $$IS_SIGNATURE_EQUAL: {value: isSignatureEqual}, + bind: {value: bind}, + }); + } + knownServerReferences.set(newFn, {id: reference.id, bound: boundPromise}); } return newFn; } @@ -609,12 +657,13 @@ function bind(this: Function) { export function createServerReference, T>( id: ServerReferenceId, callServer: CallServerCallback, + encodeFormAction?: EncodeFormActionCallback, ): (...A) => Promise { const proxy = function (): Promise { // $FlowFixMe[method-unbinding] const args = Array.prototype.slice.call(arguments); return callServer(id, args); }; - registerServerReference(proxy, {id, bound: null}); + registerServerReference(proxy, {id, bound: null}, encodeFormAction); return proxy; } diff --git a/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js index 181328e93fda5..eabe0546913f0 100644 --- a/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js @@ -38,6 +38,7 @@ function createResponseFromOptions(options: void | Options) { options && options.moduleBaseURL ? options.moduleBaseURL : '', null, options && options.callServer ? options.callServer : undefined, + undefined, // encodeFormAction undefined, // nonce ); } diff --git a/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js index dbc9ed8e3d2a0..71bbcbd577397 100644 --- a/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js @@ -7,7 +7,7 @@ * @flow */ -import type {Thenable} from 'shared/ReactTypes.js'; +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js'; import type {Response} from 'react-client/src/ReactFlightClient'; @@ -38,8 +38,14 @@ export function createServerReference, T>( return createServerReferenceImpl(id, noServerCall); } +type EncodeFormActionCallback = ( + id: any, + args: Promise, +) => ReactCustomFormAction; + export type Options = { nonce?: string, + encodeFormAction?: EncodeFormActionCallback, }; function createFromNodeStream( @@ -52,6 +58,7 @@ function createFromNodeStream( moduleRootPath, moduleBaseURL, noServerCall, + options ? options.encodeFormAction : undefined, options && typeof options.nonce === 'string' ? options.nonce : undefined, ); stream.on('data', chunk => { diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js index 64e6b3886adf7..108040653114d 100644 --- a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js @@ -37,6 +37,7 @@ function createResponseFromOptions(options: void | Options) { null, null, options && options.callServer ? options.callServer : undefined, + undefined, // encodeFormAction undefined, // nonce ); } diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js index 3b2f7aeea044e..78132e887eb70 100644 --- a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js @@ -7,7 +7,7 @@ * @flow */ -import type {Thenable} from 'shared/ReactTypes.js'; +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js'; import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient'; @@ -46,9 +46,15 @@ export function createServerReference, T>( return createServerReferenceImpl(id, noServerCall); } +type EncodeFormActionCallback = ( + id: any, + args: Promise, +) => ReactCustomFormAction; + export type Options = { ssrManifest: SSRManifest, nonce?: string, + encodeFormAction?: EncodeFormActionCallback, }; function createResponseFromOptions(options: Options) { @@ -56,6 +62,7 @@ function createResponseFromOptions(options: Options) { options.ssrManifest.moduleMap, options.ssrManifest.moduleLoading, noServerCall, + options.encodeFormAction, typeof options.nonce === 'string' ? options.nonce : undefined, ); } diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js index 730bc8d61354b..180f595dd9a08 100644 --- a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js @@ -7,7 +7,7 @@ * @flow */ -import type {Thenable} from 'shared/ReactTypes.js'; +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js'; import type {Response} from 'react-client/src/ReactFlightClient'; @@ -40,9 +40,6 @@ function noServerCall() { 'to pass data to Client Components instead.', ); } -export type Options = { - nonce?: string, -}; export function createServerReference, T>( id: any, @@ -51,6 +48,16 @@ export function createServerReference, T>( return createServerReferenceImpl(id, noServerCall); } +type EncodeFormActionCallback = ( + id: any, + args: Promise, +) => ReactCustomFormAction; + +export type Options = { + nonce?: string, + encodeFormAction?: EncodeFormActionCallback, +}; + function createFromNodeStream( stream: Readable, ssrManifest: SSRManifest, @@ -60,6 +67,7 @@ function createFromNodeStream( ssrManifest.moduleMap, ssrManifest.moduleLoading, noServerCall, + options ? options.encodeFormAction : undefined, options && typeof options.nonce === 'string' ? options.nonce : undefined, ); stream.on('data', chunk => { diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js index 64e6b3886adf7..108040653114d 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js @@ -37,6 +37,7 @@ function createResponseFromOptions(options: void | Options) { null, null, options && options.callServer ? options.callServer : undefined, + undefined, // encodeFormAction undefined, // nonce ); } diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js index 3b2f7aeea044e..78132e887eb70 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js @@ -7,7 +7,7 @@ * @flow */ -import type {Thenable} from 'shared/ReactTypes.js'; +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js'; import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient'; @@ -46,9 +46,15 @@ export function createServerReference, T>( return createServerReferenceImpl(id, noServerCall); } +type EncodeFormActionCallback = ( + id: any, + args: Promise, +) => ReactCustomFormAction; + export type Options = { ssrManifest: SSRManifest, nonce?: string, + encodeFormAction?: EncodeFormActionCallback, }; function createResponseFromOptions(options: Options) { @@ -56,6 +62,7 @@ function createResponseFromOptions(options: Options) { options.ssrManifest.moduleMap, options.ssrManifest.moduleLoading, noServerCall, + options.encodeFormAction, typeof options.nonce === 'string' ? options.nonce : undefined, ); } diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js index db6f233d80dc4..180f595dd9a08 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js @@ -7,7 +7,7 @@ * @flow */ -import type {Thenable} from 'shared/ReactTypes.js'; +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js'; import type {Response} from 'react-client/src/ReactFlightClient'; @@ -48,8 +48,14 @@ export function createServerReference, T>( return createServerReferenceImpl(id, noServerCall); } +type EncodeFormActionCallback = ( + id: any, + args: Promise, +) => ReactCustomFormAction; + export type Options = { nonce?: string, + encodeFormAction?: EncodeFormActionCallback, }; function createFromNodeStream( @@ -61,6 +67,7 @@ function createFromNodeStream( ssrManifest.moduleMap, ssrManifest.moduleLoading, noServerCall, + options ? options.encodeFormAction : undefined, options && typeof options.nonce === 'string' ? options.nonce : undefined, ); stream.on('data', chunk => {