Skip to content

Commit 94add5a

Browse files
committed
Use undici polyfill for tests in old Node versions
To support old Node we need to adjust how we construct blobs.
1 parent 0a0a3af commit 94add5a

File tree

6 files changed

+135
-116
lines changed

6 files changed

+135
-116
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"through2": "^3.0.1",
9898
"tmp": "^0.1.0",
9999
"typescript": "^3.7.5",
100+
"undici": "^5.28.4",
100101
"web-streams-polyfill": "^3.1.1",
101102
"yargs": "^15.3.1"
102103
},

packages/react-client/src/ReactFlightReplyClient.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,17 @@ export function processReply(
187187

188188
function serializeTypedArray(
189189
tag: string,
190-
typedArray: ArrayBuffer | $ArrayBufferView,
190+
typedArray: $ArrayBufferView,
191191
): string {
192-
const blob = new Blob([typedArray]);
192+
const blob = new Blob([
193+
// We should be able to pass the buffer straight through but Node < 18 treat
194+
// multi-byte array blobs differently so we first convert it to single-byte.
195+
new Uint8Array(
196+
typedArray.buffer,
197+
typedArray.byteOffset,
198+
typedArray.byteLength,
199+
),
200+
]);
193201
const blobId = nextPartId++;
194202
if (formData === null) {
195203
formData = new FormData();
@@ -392,7 +400,13 @@ export function processReply(
392400

393401
if (enableBinaryFlight) {
394402
if (value instanceof ArrayBuffer) {
395-
return serializeTypedArray('A', value);
403+
const blob = new Blob([value]);
404+
const blobId = nextPartId++;
405+
if (formData === null) {
406+
formData = new FormData();
407+
}
408+
formData.append(formFieldPrefix + blobId, blob);
409+
return '$' + 'A' + blobId.toString(16);
396410
}
397411
if (value instanceof Int8Array) {
398412
// char

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010

1111
'use strict';
1212

13+
if (typeof Blob === 'undefined') {
14+
global.Blob = require('buffer').Blob;
15+
}
16+
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
17+
global.File = require('undici').File;
18+
global.FormData = require('undici').FormData;
19+
}
20+
1321
function normalizeCodeLocInfo(str) {
1422
return (
1523
str &&
@@ -513,39 +521,37 @@ describe('ReactFlight', () => {
513521
`);
514522
});
515523

516-
if (typeof FormData !== 'undefined') {
517-
it('can transport FormData (no blobs)', async () => {
518-
function ComponentClient({prop}) {
519-
return `
520-
formData: ${prop instanceof FormData}
521-
hi: ${prop.get('hi')}
522-
multiple: ${prop.getAll('multiple')}
523-
content: ${JSON.stringify(Array.from(prop))}
524-
`;
525-
}
526-
const Component = clientReference(ComponentClient);
527-
528-
const formData = new FormData();
529-
formData.append('hi', 'world');
530-
formData.append('multiple', 1);
531-
formData.append('multiple', 2);
524+
it('can transport FormData (no blobs)', async () => {
525+
function ComponentClient({prop}) {
526+
return `
527+
formData: ${prop instanceof FormData}
528+
hi: ${prop.get('hi')}
529+
multiple: ${prop.getAll('multiple')}
530+
content: ${JSON.stringify(Array.from(prop))}
531+
`;
532+
}
533+
const Component = clientReference(ComponentClient);
532534

533-
const model = <Component prop={formData} />;
535+
const formData = new FormData();
536+
formData.append('hi', 'world');
537+
formData.append('multiple', 1);
538+
formData.append('multiple', 2);
534539

535-
const transport = ReactNoopFlightServer.render(model);
540+
const model = <Component prop={formData} />;
536541

537-
await act(async () => {
538-
ReactNoop.render(await ReactNoopFlightClient.read(transport));
539-
});
542+
const transport = ReactNoopFlightServer.render(model);
540543

541-
expect(ReactNoop).toMatchRenderedOutput(`
542-
formData: true
543-
hi: world
544-
multiple: 1,2
545-
content: [["hi","world"],["multiple","1"],["multiple","2"]]
546-
`);
544+
await act(async () => {
545+
ReactNoop.render(await ReactNoopFlightClient.read(transport));
547546
});
548-
}
547+
548+
expect(ReactNoop).toMatchRenderedOutput(`
549+
formData: true
550+
hi: world
551+
multiple: 1,2
552+
content: [["hi","world"],["multiple","1"],["multiple","2"]]
553+
`);
554+
});
549555

550556
it('can transport cyclic objects', async () => {
551557
function ComponentClient({prop}) {

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@ global.ReadableStream =
1515
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
1616
global.TextEncoder = require('util').TextEncoder;
1717
global.TextDecoder = require('util').TextDecoder;
18-
if (typeof Blob === 'undefined') {
19-
global.Blob = require('buffer').Blob;
20-
}
21-
if (typeof File === 'undefined') {
22-
global.File = require('buffer').File;
18+
global.Blob = require('buffer').Blob;
19+
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
20+
global.File = require('buffer').File || require('undici').File;
21+
global.FormData = require('undici').FormData;
2322
}
2423

2524
// Don't wait before processing work on the server.
@@ -379,45 +378,40 @@ describe('ReactFlightDOMEdge', () => {
379378
expect(await result.arrayBuffer()).toEqual(await blob.arrayBuffer());
380379
});
381380

382-
if (typeof FormData !== 'undefined' && typeof File !== 'undefined') {
383-
// @gate enableBinaryFlight
384-
it('can transport FormData (blobs)', async () => {
385-
const bytes = new Uint8Array([
386-
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
387-
]);
388-
const blob = new Blob([bytes, bytes], {
389-
type: 'application/x-test',
390-
});
391-
392-
const formData = new FormData();
393-
formData.append('hi', 'world');
394-
formData.append('file', blob, 'filename.test');
395-
396-
expect(formData.get('file') instanceof File).toBe(true);
397-
expect(formData.get('file').name).toBe('filename.test');
398-
399-
const stream = passThrough(
400-
ReactServerDOMServer.renderToReadableStream(formData),
401-
);
402-
const result = await ReactServerDOMClient.createFromReadableStream(
403-
stream,
404-
{
405-
ssrManifest: {
406-
moduleMap: null,
407-
moduleLoading: null,
408-
},
409-
},
410-
);
411-
412-
expect(result instanceof FormData).toBe(true);
413-
expect(result.get('hi')).toBe('world');
414-
const resultBlob = result.get('file');
415-
expect(resultBlob instanceof Blob).toBe(true);
416-
expect(resultBlob.name).toBe('blob'); // We should not pass through the file name for security.
417-
expect(resultBlob.size).toBe(bytes.length * 2);
418-
expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
381+
// @gate enableBinaryFlight
382+
it('can transport FormData (blobs)', async () => {
383+
const bytes = new Uint8Array([
384+
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
385+
]);
386+
const blob = new Blob([bytes, bytes], {
387+
type: 'application/x-test',
419388
});
420-
}
389+
390+
const formData = new FormData();
391+
formData.append('hi', 'world');
392+
formData.append('file', blob, 'filename.test');
393+
394+
expect(formData.get('file') instanceof File).toBe(true);
395+
expect(formData.get('file').name).toBe('filename.test');
396+
397+
const stream = passThrough(
398+
ReactServerDOMServer.renderToReadableStream(formData),
399+
);
400+
const result = await ReactServerDOMClient.createFromReadableStream(stream, {
401+
ssrManifest: {
402+
moduleMap: null,
403+
moduleLoading: null,
404+
},
405+
});
406+
407+
expect(result instanceof FormData).toBe(true);
408+
expect(result.get('hi')).toBe('world');
409+
const resultBlob = result.get('file');
410+
expect(resultBlob instanceof Blob).toBe(true);
411+
expect(resultBlob.name).toBe('blob'); // We should not pass through the file name for security.
412+
expect(resultBlob.size).toBe(bytes.length * 2);
413+
expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
414+
});
421415

422416
it('can pass an async import that resolves later to an outline object like a Map', async () => {
423417
let resolve;

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyEdge-test.js

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@ global.ReadableStream =
1616
global.TextEncoder = require('util').TextEncoder;
1717
global.TextDecoder = require('util').TextDecoder;
1818

19-
if (typeof Blob === 'undefined') {
20-
global.Blob = require('buffer').Blob;
21-
}
22-
if (typeof File === 'undefined') {
23-
global.File = require('buffer').File;
19+
global.Blob = require('buffer').Blob;
20+
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
21+
global.File = require('buffer').File || require('undici').File;
22+
global.FormData = require('undici').FormData;
2423
}
2524

2625
// let serverExports;
@@ -44,13 +43,6 @@ describe('ReactFlightDOMReplyEdge', () => {
4443
ReactServerDOMClient = require('react-server-dom-webpack/client.edge');
4544
});
4645

47-
if (typeof FormData === 'undefined') {
48-
// We can't test if we don't have a native FormData implementation because the JSDOM one
49-
// is missing the arrayBuffer() method.
50-
it('cannot test', () => {});
51-
return;
52-
}
53-
5446
it('can encode a reply', async () => {
5547
const body = await ReactServerDOMClient.encodeReply({some: 'object'});
5648
const decoded = await ReactServerDOMServer.decodeReply(
@@ -89,6 +81,8 @@ describe('ReactFlightDOMReplyEdge', () => {
8981
);
9082

9183
expect(result).toEqual(buffers);
84+
// Array buffers can't use the toEqual helper.
85+
expect(new Uint8Array(result[0])).toEqual(new Uint8Array(buffers[0]));
9286
});
9387

9488
// @gate enableBinaryFlight
@@ -109,35 +103,33 @@ describe('ReactFlightDOMReplyEdge', () => {
109103
expect(await result.arrayBuffer()).toEqual(await blob.arrayBuffer());
110104
});
111105

112-
if (typeof FormData !== 'undefined' && typeof File !== 'undefined') {
113-
it('can transport FormData (blobs)', async () => {
114-
const bytes = new Uint8Array([
115-
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
116-
]);
117-
const blob = new Blob([bytes, bytes], {
118-
type: 'application/x-test',
119-
});
120-
121-
const formData = new FormData();
122-
formData.append('hi', 'world');
123-
formData.append('file', blob, 'filename.test');
124-
125-
expect(formData.get('file') instanceof File).toBe(true);
126-
expect(formData.get('file').name).toBe('filename.test');
127-
128-
const body = await ReactServerDOMClient.encodeReply(formData);
129-
const result = await ReactServerDOMServer.decodeReply(
130-
body,
131-
webpackServerMap,
132-
);
133-
134-
expect(result instanceof FormData).toBe(true);
135-
expect(result.get('hi')).toBe('world');
136-
const resultBlob = result.get('file');
137-
expect(resultBlob instanceof Blob).toBe(true);
138-
expect(resultBlob.name).toBe('filename.test'); // In this direction we allow file name to pass through but not other direction.
139-
expect(resultBlob.size).toBe(bytes.length * 2);
140-
expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
106+
it('can transport FormData (blobs)', async () => {
107+
const bytes = new Uint8Array([
108+
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
109+
]);
110+
const blob = new Blob([bytes, bytes], {
111+
type: 'application/x-test',
141112
});
142-
}
113+
114+
const formData = new FormData();
115+
formData.append('hi', 'world');
116+
formData.append('file', blob, 'filename.test');
117+
118+
expect(formData.get('file') instanceof File).toBe(true);
119+
expect(formData.get('file').name).toBe('filename.test');
120+
121+
const body = await ReactServerDOMClient.encodeReply(formData);
122+
const result = await ReactServerDOMServer.decodeReply(
123+
body,
124+
webpackServerMap,
125+
);
126+
127+
expect(result instanceof FormData).toBe(true);
128+
expect(result.get('hi')).toBe('world');
129+
const resultBlob = result.get('file');
130+
expect(resultBlob instanceof Blob).toBe(true);
131+
expect(resultBlob.name).toBe('filename.test'); // In this direction we allow file name to pass through but not other direction.
132+
expect(resultBlob.size).toBe(bytes.length * 2);
133+
expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
134+
});
143135
});

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2182,6 +2182,11 @@
21822182
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.0.0.tgz#1a9e4b4c96d8c7886e0110ed310a0135144a1691"
21832183
integrity sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ==
21842184

2185+
"@fastify/busboy@^2.0.0":
2186+
version "2.1.1"
2187+
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d"
2188+
integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
2189+
21852190
"@gitbeaker/core@^21.7.0":
21862191
version "21.7.0"
21872192
resolved "https://registry.yarnpkg.com/@gitbeaker/core/-/core-21.7.0.tgz#fcf7a12915d39f416e3f316d0a447a814179b8e5"
@@ -15762,6 +15767,13 @@ unc-path-regex@^0.1.0, unc-path-regex@^0.1.2:
1576215767
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
1576315768
integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo=
1576415769

15770+
undici@^5.28.4:
15771+
version "5.28.4"
15772+
resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068"
15773+
integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==
15774+
dependencies:
15775+
"@fastify/busboy" "^2.0.0"
15776+
1576515777
unicode-canonical-property-names-ecmascript@^1.0.4:
1576615778
version "1.0.4"
1576715779
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"

0 commit comments

Comments
 (0)