| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | * |
| 7 | * @flow |
| 8 | */ |
| 9 | |
| 10 | import type {ReactFormState} from 'shared/ReactTypes'; |
| 11 | |
| 12 | import type { |
| 13 | ServerManifest, |
| 14 | ClientReference as ServerReference, |
| 15 | } from 'react-client/src/ReactFlightClientConfig'; |
| 16 | |
| 17 | import { |
| 18 | resolveServerReference, |
| 19 | preloadModule, |
| 20 | requireModule, |
| 21 | } from 'react-client/src/ReactFlightClientConfig'; |
| 22 | |
| 23 | import { |
| 24 | createResponse, |
| 25 | close, |
| 26 | getRoot, |
| 27 | MAX_BOUND_ARGS, |
| 28 | } from './ReactFlightReplyServer'; |
| 29 | |
| 30 | type ServerReferenceId = any; |
| 31 | |
| 32 | function bindArgs(fn: any, args: any) { |
| 33 | if (args.length > MAX_BOUND_ARGS) { |
| 34 | throw new Error( |
| 35 | 'Server Function has too many bound arguments. Received ' + |
| 36 | args.length + |
| 37 | ' but the limit is ' + |
| 38 | MAX_BOUND_ARGS + |
| 39 | '.', |
| 40 | ); |
| 41 | } |
| 42 | |
| 43 | return fn.bind.apply(fn, [null].concat(args)); |
| 44 | } |
| 45 | |
| 46 | function loadServerReference<T>( |
| 47 | bundlerConfig: ServerManifest, |
| 48 | metaData: { |
| 49 | id: ServerReferenceId, |
| 50 | bound: null | Promise<Array<any>>, |
| 51 | }, |
| 52 | ): Promise<T> { |
| 53 | const id: ServerReferenceId = metaData.id; |
| 54 | if (typeof id !== 'string') { |
| 55 | return null as any; |
| 56 | } |
| 57 | const serverReference: ServerReference<T> = |
| 58 | resolveServerReference<$FlowFixMe>(bundlerConfig, id); |
| 59 | // We expect most servers to not really need this because you'd just have all |
| 60 | // the relevant modules already loaded but it allows for lazy loading of code |
| 61 | // if needed. |
| 62 | const preloadPromise = preloadModule(serverReference); |
| 63 | const bound = metaData.bound; |
| 64 | if (bound instanceof Promise) { |
| 65 | return Promise.all([bound as any, preloadPromise]).then( |
| 66 | ([args]: Array<any>) => bindArgs(requireModule(serverReference), args), |
| 67 | ); |
| 68 | } else if (preloadPromise) { |
| 69 | return Promise.resolve(preloadPromise).then(() => |
| 70 | requireModule(serverReference), |
| 71 | ); |
| 72 | } else { |
| 73 | // Synchronously available |
| 74 | return Promise.resolve(requireModule(serverReference)); |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | function decodeBoundActionMetaData( |
| 79 | body: FormData, |
| 80 | serverManifest: ServerManifest, |
| 81 | formFieldPrefix: string, |
| 82 | arraySizeLimit: void | number, |
| 83 | ): {id: ServerReferenceId, bound: null | Promise<Array<any>>} { |
| 84 | // The data for this reference is encoded in multiple fields under this prefix. |
| 85 | const actionResponse = createResponse( |
| 86 | serverManifest, |
| 87 | formFieldPrefix, |
| 88 | undefined, |
| 89 | body, |
| 90 | arraySizeLimit, |
| 91 | ); |
| 92 | close(actionResponse); |
| 93 | const refPromise = getRoot<{ |
| 94 | id: ServerReferenceId, |
| 95 | bound: null | Promise<Array<any>>, |
| 96 | }>(actionResponse); |
| 97 | // Force it to initialize |
| 98 | // $FlowFixMe[incompatible-type] |
| 99 | refPromise.then(() => {}); |
| 100 | if (refPromise.status !== 'fulfilled') { |
| 101 | // $FlowFixMe[prop-missing] |
| 102 | throw refPromise.reason; |
| 103 | } |
| 104 | return refPromise.value; |
| 105 | } |
| 106 | |
| 107 | export function decodeAction<T>( |
| 108 | body: FormData, |
| 109 | serverManifest: ServerManifest, |
| 110 | ): Promise<() => T> | null { |
| 111 | // We're going to create a new formData object that holds all the fields except |
| 112 | // the implementation details of the action data. |
| 113 | const formData = new FormData(); |
| 114 | |
| 115 | let maybeActionKey: null | string = null; |
| 116 | |
| 117 | // $FlowFixMe[prop-missing] |
| 118 | body.forEach((value: string | File, key: string) => { |
| 119 | if (!key.startsWith('$ACTION_')) { |
| 120 | // $FlowFixMe[incompatible-type] |
| 121 | formData.append(key, value); |
| 122 | } else if (key.startsWith('$ACTION_REF_')) { |
| 123 | // Later actions may override earlier actions if a button is used to |
| 124 | // override the default form action. However, we don't expect the same |
| 125 | // action ref field to be sent multiple times in legitimate form data. |
| 126 | maybeActionKey = key; |
| 127 | } else if (key.startsWith('$ACTION_ID_')) { |
| 128 | // A simple action with no bound arguments may appear twice in the form data |
| 129 | // if a button specifies the same action as the default form action. |
| 130 | maybeActionKey = key; |
| 131 | } |
| 132 | }); |
| 133 | |
| 134 | if (maybeActionKey === null) { |
| 135 | return null; |
| 136 | } |
| 137 | const actionKey = maybeActionKey; |
| 138 | |
| 139 | let action: Promise<(formData: FormData) => T> | null = null; |
| 140 | if (actionKey.startsWith('$ACTION_REF_')) { |
| 141 | const formFieldPrefix = |
| 142 | '$ACTION_' + actionKey.slice('$ACTION_REF_'.length) + ':'; |
| 143 | const metaData = decodeBoundActionMetaData( |
| 144 | body, |
| 145 | serverManifest, |
| 146 | formFieldPrefix, |
| 147 | ); |
| 148 | action = loadServerReference(serverManifest, metaData); |
| 149 | } else if (actionKey.startsWith('$ACTION_ID_')) { |
| 150 | const id = actionKey.slice('$ACTION_ID_'.length); |
| 151 | action = loadServerReference(serverManifest, { |
| 152 | id, |
| 153 | bound: null, |
| 154 | }); |
| 155 | } else { |
| 156 | throw new Error('Cannot handle action key. This is a bug in React.'); |
| 157 | } |
| 158 | |
| 159 | // Return the action with the remaining FormData bound to the first argument. |
| 160 | return action.then(fn => fn.bind(null, formData)); |
| 161 | } |
| 162 | |
| 163 | export function decodeFormState<S>( |
| 164 | actionResult: S, |
| 165 | body: FormData, |
| 166 | serverManifest: ServerManifest, |
| 167 | ): Promise<ReactFormState<S, ServerReferenceId> | null> { |
| 168 | const keyPath = body.get('$ACTION_KEY'); |
| 169 | if (typeof keyPath !== 'string') { |
| 170 | // This form submission did not include any form state. |
| 171 | return Promise.resolve(null); |
| 172 | } |
| 173 | // Search through the form data object to get the reference id and the number |
| 174 | // of bound arguments. This repeats some of the work done in decodeAction. |
| 175 | let actionKey: null | string = null; |
| 176 | // $FlowFixMe[prop-missing] |
| 177 | body.forEach((value: string | File, key: string) => { |
| 178 | if (key.startsWith('$ACTION_REF_')) { |
| 179 | actionKey = key; |
| 180 | } |
| 181 | // We don't check for the simple $ACTION_ID_ case because form state actions |
| 182 | // are always bound to the state argument. |
| 183 | }); |
| 184 | if (actionKey === null) { |
| 185 | // Should be unreachable. |
| 186 | return Promise.resolve(null); |
| 187 | } |
| 188 | |
| 189 | const formFieldPrefix = |
| 190 | '$ACTION_' + actionKey.slice('$ACTION_REF_'.length) + ':'; |
| 191 | const metaData = decodeBoundActionMetaData( |
| 192 | body, |
| 193 | serverManifest, |
| 194 | formFieldPrefix, |
| 195 | ); |
| 196 | |
| 197 | const referenceId = metaData.id; |
| 198 | return Promise.resolve(metaData.bound).then(bound => { |
| 199 | if (bound === null) { |
| 200 | // Should be unreachable because form state actions are always bound to the |
| 201 | // state argument. |
| 202 | return null; |
| 203 | } |
| 204 | // The form action dispatch method is always bound to the initial state. |
| 205 | // But when comparing signatures, we compare to the original unbound action. |
| 206 | // Subtract one from the arity to account for this. |
| 207 | const boundArity = bound.length - 1; |
| 208 | return [actionResult, keyPath, referenceId, boundArity]; |
| 209 | }); |
| 210 | } |