diff --git a/fixtures/flight/src/Form.js b/fixtures/flight/src/Form.js index 8c2ff2922d..a4b92366d4 100644 --- a/fixtures/flight/src/Form.js +++ b/fixtures/flight/src/Form.js @@ -20,8 +20,14 @@ export default function Form({action, children}) { React.startTransition(() => setIsPending(false)); } }}> - + + + {isPending ? 'Saving...' : null} ); diff --git a/fixtures/flight/src/actions.js b/fixtures/flight/src/actions.js index 7143c31a39..87cba005e0 100644 --- a/fixtures/flight/src/actions.js +++ b/fixtures/flight/src/actions.js @@ -5,5 +5,12 @@ export async function like() { } export async function greet(formData) { - return 'Hi ' + formData.get('name') + '!'; + const name = formData.get('name') || 'you'; + const file = formData.get('file'); + if (file) { + return `Ok, ${name}, here is ${file.name}: + ${(await file.text()).toUpperCase()} + `; + } + return 'Hi ' + name + '!'; } diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js index b1cda7e104..f23959b2f8 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js @@ -88,8 +88,17 @@ function decodeReplyFromBusboy( webpackMap: ServerManifest, ): Thenable { const response = createResponse(webpackMap, ''); + let pendingFiles = 0; + const queuedFields: Array = []; busboyStream.on('field', (name, value) => { - resolveField(response, name, value); + if (pendingFiles > 0) { + // Because the 'end' event fires two microtasks after the next 'field' + // we would resolve files and fields out of order. To handle this properly + // we queue any fields we receive until the previous file is done. + queuedFields.push(name, value); + } else { + resolveField(response, name, value); + } }); busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => { if (encoding.toLowerCase() === 'base64') { @@ -99,12 +108,21 @@ function decodeReplyFromBusboy( 'the wrong assumption, we can easily fix it.', ); } + pendingFiles++; const file = resolveFileInfo(response, name, filename, mimeType); value.on('data', chunk => { resolveFileChunk(response, file, chunk); }); value.on('end', () => { resolveFileComplete(response, name, file); + pendingFiles--; + if (pendingFiles === 0) { + // Release any queued fields + for (let i = 0; i < queuedFields.length; i += 2) { + resolveField(response, queuedFields[i], queuedFields[i + 1]); + } + queuedFields.length = 0; + } }); }); busboyStream.on('finish', () => { diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js index 88b846f283..078f76f11f 100644 --- a/packages/react-server/src/ReactFlightReplyServer.js +++ b/packages/react-server/src/ReactFlightReplyServer.js @@ -564,8 +564,11 @@ export function resolveFileComplete( handle: FileHandle, ): void { // Add this file to the backing store. - const file = new File(handle.chunks, handle.filename, {type: handle.mime}); - response._formData.append(key, file); + // Node.js doesn't expose a global File constructor so we need to use + // the append() form that takes the file name as the third argument, + // to create a File object. + const blob = new Blob(handle.chunks, {type: handle.mime}); + response._formData.append(key, blob, handle.filename); } export function close(response: Response): void {