/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import type {ReactFormState} from 'shared/ReactTypes'; import type { ServerManifest, ClientReference as ServerReference, } from 'react-client/src/ReactFlightClientConfig'; import { resolveServerReference, preloadModule, requireModule, } from 'react-client/src/ReactFlightClientConfig'; import { createResponse, close, getRoot, MAX_BOUND_ARGS, } from './ReactFlightReplyServer'; type ServerReferenceId = any; function bindArgs(fn: any, args: any) { if (args.length > MAX_BOUND_ARGS) { throw new Error( 'Server Function has too many bound arguments. Received ' + args.length + ' but the limit is ' + MAX_BOUND_ARGS + '.', ); } return fn.bind.apply(fn, [null].concat(args)); } function loadServerReference( bundlerConfig: ServerManifest, metaData: { id: ServerReferenceId, bound: null | Promise>, }, ): Promise { const id: ServerReferenceId = metaData.id; if (typeof id !== 'string') { return null as any; } const serverReference: ServerReference = resolveServerReference<$FlowFixMe>(bundlerConfig, id); // We expect most servers to not really need this because you'd just have all // the relevant modules already loaded but it allows for lazy loading of code // if needed. const preloadPromise = preloadModule(serverReference); const bound = metaData.bound; if (bound instanceof Promise) { return Promise.all([bound as any, preloadPromise]).then( ([args]: Array) => bindArgs(requireModule(serverReference), args), ); } else if (preloadPromise) { return Promise.resolve(preloadPromise).then(() => requireModule(serverReference), ); } else { // Synchronously available return Promise.resolve(requireModule(serverReference)); } } function decodeBoundActionMetaData( body: FormData, serverManifest: ServerManifest, formFieldPrefix: string, arraySizeLimit: void | number, ): {id: ServerReferenceId, bound: null | Promise>} { // The data for this reference is encoded in multiple fields under this prefix. const actionResponse = createResponse( serverManifest, formFieldPrefix, undefined, body, arraySizeLimit, ); close(actionResponse); const refPromise = getRoot<{ id: ServerReferenceId, bound: null | Promise>, }>(actionResponse); // Force it to initialize // $FlowFixMe[incompatible-type] refPromise.then(() => {}); if (refPromise.status !== 'fulfilled') { // $FlowFixMe[prop-missing] throw refPromise.reason; } return refPromise.value; } export function decodeAction( body: FormData, serverManifest: ServerManifest, ): Promise<() => T> | null { // We're going to create a new formData object that holds all the fields except // the implementation details of the action data. const formData = new FormData(); let maybeActionKey: null | string = null; // $FlowFixMe[prop-missing] body.forEach((value: string | File, key: string) => { if (!key.startsWith('$ACTION_')) { // $FlowFixMe[incompatible-type] formData.append(key, value); } else if (key.startsWith('$ACTION_REF_')) { // Later actions may override earlier actions if a button is used to // override the default form action. However, we don't expect the same // action ref field to be sent multiple times in legitimate form data. maybeActionKey = key; } else if (key.startsWith('$ACTION_ID_')) { // A simple action with no bound arguments may appear twice in the form data // if a button specifies the same action as the default form action. maybeActionKey = key; } }); if (maybeActionKey === null) { return null; } const actionKey = maybeActionKey; let action: Promise<(formData: FormData) => T> | null = null; if (actionKey.startsWith('$ACTION_REF_')) { const formFieldPrefix = '$ACTION_' + actionKey.slice('$ACTION_REF_'.length) + ':'; const metaData = decodeBoundActionMetaData( body, serverManifest, formFieldPrefix, ); action = loadServerReference(serverManifest, metaData); } else if (actionKey.startsWith('$ACTION_ID_')) { const id = actionKey.slice('$ACTION_ID_'.length); action = loadServerReference(serverManifest, { id, bound: null, }); } else { throw new Error('Cannot handle action key. This is a bug in React.'); } // Return the action with the remaining FormData bound to the first argument. return action.then(fn => fn.bind(null, formData)); } export function decodeFormState( actionResult: S, body: FormData, serverManifest: ServerManifest, ): Promise | null> { const keyPath = body.get('$ACTION_KEY'); if (typeof keyPath !== 'string') { // This form submission did not include any form state. return Promise.resolve(null); } // Search through the form data object to get the reference id and the number // of bound arguments. This repeats some of the work done in decodeAction. let actionKey: null | string = null; // $FlowFixMe[prop-missing] body.forEach((value: string | File, key: string) => { if (key.startsWith('$ACTION_REF_')) { actionKey = key; } // We don't check for the simple $ACTION_ID_ case because form state actions // are always bound to the state argument. }); if (actionKey === null) { // Should be unreachable. return Promise.resolve(null); } const formFieldPrefix = '$ACTION_' + actionKey.slice('$ACTION_REF_'.length) + ':'; const metaData = decodeBoundActionMetaData( body, serverManifest, formFieldPrefix, ); const referenceId = metaData.id; return Promise.resolve(metaData.bound).then(bound => { if (bound === null) { // Should be unreachable because form state actions are always bound to the // state argument. return null; } // The form action dispatch method is always bound to the initial state. // But when comparing signatures, we compare to the original unbound action. // Subtract one from the arity to account for this. const boundArity = bound.length - 1; return [actionResult, keyPath, referenceId, boundArity]; }); }