Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use undici polyfill for tests in old Node versions #28887

Merged
merged 1 commit into from May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -97,6 +97,7 @@
"through2": "^3.0.1",
"tmp": "^0.1.0",
"typescript": "^3.7.5",
"undici": "^5.28.4",
"web-streams-polyfill": "^3.1.1",
"yargs": "^15.3.1"
},
Expand Down
20 changes: 17 additions & 3 deletions packages/react-client/src/ReactFlightReplyClient.js
Expand Up @@ -187,9 +187,17 @@ export function processReply(

function serializeTypedArray(
tag: string,
typedArray: ArrayBuffer | $ArrayBufferView,
typedArray: $ArrayBufferView,
): string {
const blob = new Blob([typedArray]);
const blob = new Blob([
// We should be able to pass the buffer straight through but Node < 18 treat
// multi-byte array blobs differently so we first convert it to single-byte.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are only relevant in Server-to-Server calls and maybe SSR serialization of forms. The question is do we support old Node without polyfilling?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should only support supported versions of Node so we can probably assume FormData

new Uint8Array(
typedArray.buffer,
typedArray.byteOffset,
typedArray.byteLength,
),
]);
const blobId = nextPartId++;
if (formData === null) {
formData = new FormData();
Expand Down Expand Up @@ -392,7 +400,13 @@ export function processReply(

if (enableBinaryFlight) {
if (value instanceof ArrayBuffer) {
return serializeTypedArray('A', value);
const blob = new Blob([value]);
const blobId = nextPartId++;
if (formData === null) {
formData = new FormData();
}
formData.append(formFieldPrefix + blobId, blob);
return '$' + 'A' + blobId.toString(16);
}
if (value instanceof Int8Array) {
// char
Expand Down
62 changes: 34 additions & 28 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Expand Up @@ -10,6 +10,14 @@

'use strict';

if (typeof Blob === 'undefined') {
global.Blob = require('buffer').Blob;
}
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
global.File = require('undici').File;
global.FormData = require('undici').FormData;
}

function normalizeCodeLocInfo(str) {
return (
str &&
Expand Down Expand Up @@ -513,39 +521,37 @@ describe('ReactFlight', () => {
`);
});

if (typeof FormData !== 'undefined') {
it('can transport FormData (no blobs)', async () => {
function ComponentClient({prop}) {
return `
formData: ${prop instanceof FormData}
hi: ${prop.get('hi')}
multiple: ${prop.getAll('multiple')}
content: ${JSON.stringify(Array.from(prop))}
`;
}
const Component = clientReference(ComponentClient);

const formData = new FormData();
formData.append('hi', 'world');
formData.append('multiple', 1);
formData.append('multiple', 2);
it('can transport FormData (no blobs)', async () => {
function ComponentClient({prop}) {
return `
formData: ${prop instanceof FormData}
hi: ${prop.get('hi')}
multiple: ${prop.getAll('multiple')}
content: ${JSON.stringify(Array.from(prop))}
`;
}
const Component = clientReference(ComponentClient);

const model = <Component prop={formData} />;
const formData = new FormData();
formData.append('hi', 'world');
formData.append('multiple', 1);
formData.append('multiple', 2);

const transport = ReactNoopFlightServer.render(model);
const model = <Component prop={formData} />;

await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
const transport = ReactNoopFlightServer.render(model);

expect(ReactNoop).toMatchRenderedOutput(`
formData: true
hi: world
multiple: 1,2
content: [["hi","world"],["multiple","1"],["multiple","2"]]
`);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
}

expect(ReactNoop).toMatchRenderedOutput(`
formData: true
hi: world
multiple: 1,2
content: [["hi","world"],["multiple","1"],["multiple","2"]]
`);
});

it('can transport cyclic objects', async () => {
function ComponentClient({prop}) {
Expand Down
Expand Up @@ -15,11 +15,10 @@ global.ReadableStream =
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
if (typeof Blob === 'undefined') {
global.Blob = require('buffer').Blob;
}
if (typeof File === 'undefined') {
global.File = require('buffer').File;
global.Blob = require('buffer').Blob;
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
global.File = require('buffer').File || require('undici').File;
global.FormData = require('undici').FormData;
}

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

if (typeof FormData !== 'undefined' && typeof File !== 'undefined') {
// @gate enableBinaryFlight
it('can transport FormData (blobs)', async () => {
const bytes = new Uint8Array([
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
]);
const blob = new Blob([bytes, bytes], {
type: 'application/x-test',
});

const formData = new FormData();
formData.append('hi', 'world');
formData.append('file', blob, 'filename.test');

expect(formData.get('file') instanceof File).toBe(true);
expect(formData.get('file').name).toBe('filename.test');

const stream = passThrough(
ReactServerDOMServer.renderToReadableStream(formData),
);
const result = await ReactServerDOMClient.createFromReadableStream(
stream,
{
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
},
);

expect(result instanceof FormData).toBe(true);
expect(result.get('hi')).toBe('world');
const resultBlob = result.get('file');
expect(resultBlob instanceof Blob).toBe(true);
expect(resultBlob.name).toBe('blob'); // We should not pass through the file name for security.
expect(resultBlob.size).toBe(bytes.length * 2);
expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
// @gate enableBinaryFlight
it('can transport FormData (blobs)', async () => {
const bytes = new Uint8Array([
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
]);
const blob = new Blob([bytes, bytes], {
type: 'application/x-test',
});
}

const formData = new FormData();
formData.append('hi', 'world');
formData.append('file', blob, 'filename.test');

expect(formData.get('file') instanceof File).toBe(true);
expect(formData.get('file').name).toBe('filename.test');

const stream = passThrough(
ReactServerDOMServer.renderToReadableStream(formData),
);
const result = await ReactServerDOMClient.createFromReadableStream(stream, {
ssrManifest: {
moduleMap: null,
moduleLoading: null,
},
});

expect(result instanceof FormData).toBe(true);
expect(result.get('hi')).toBe('world');
const resultBlob = result.get('file');
expect(resultBlob instanceof Blob).toBe(true);
expect(resultBlob.name).toBe('blob'); // We should not pass through the file name for security.
expect(resultBlob.size).toBe(bytes.length * 2);
expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
});

it('can pass an async import that resolves later to an outline object like a Map', async () => {
let resolve;
Expand Down
Expand Up @@ -16,11 +16,10 @@ global.ReadableStream =
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;

if (typeof Blob === 'undefined') {
global.Blob = require('buffer').Blob;
}
if (typeof File === 'undefined') {
global.File = require('buffer').File;
global.Blob = require('buffer').Blob;
if (typeof File === 'undefined' || typeof FormData === 'undefined') {
global.File = require('buffer').File || require('undici').File;
global.FormData = require('undici').FormData;
}

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

if (typeof FormData === 'undefined') {
// We can't test if we don't have a native FormData implementation because the JSDOM one
// is missing the arrayBuffer() method.
it('cannot test', () => {});
return;
}

it('can encode a reply', async () => {
const body = await ReactServerDOMClient.encodeReply({some: 'object'});
const decoded = await ReactServerDOMServer.decodeReply(
Expand Down Expand Up @@ -89,6 +81,8 @@ describe('ReactFlightDOMReplyEdge', () => {
);

expect(result).toEqual(buffers);
// Array buffers can't use the toEqual helper.
expect(new Uint8Array(result[0])).toEqual(new Uint8Array(buffers[0]));
});

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

if (typeof FormData !== 'undefined' && typeof File !== 'undefined') {
it('can transport FormData (blobs)', async () => {
const bytes = new Uint8Array([
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
]);
const blob = new Blob([bytes, bytes], {
type: 'application/x-test',
});

const formData = new FormData();
formData.append('hi', 'world');
formData.append('file', blob, 'filename.test');

expect(formData.get('file') instanceof File).toBe(true);
expect(formData.get('file').name).toBe('filename.test');

const body = await ReactServerDOMClient.encodeReply(formData);
const result = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);

expect(result instanceof FormData).toBe(true);
expect(result.get('hi')).toBe('world');
const resultBlob = result.get('file');
expect(resultBlob instanceof Blob).toBe(true);
expect(resultBlob.name).toBe('filename.test'); // In this direction we allow file name to pass through but not other direction.
expect(resultBlob.size).toBe(bytes.length * 2);
expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
it('can transport FormData (blobs)', async () => {
const bytes = new Uint8Array([
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
]);
const blob = new Blob([bytes, bytes], {
type: 'application/x-test',
});
}

const formData = new FormData();
formData.append('hi', 'world');
formData.append('file', blob, 'filename.test');

expect(formData.get('file') instanceof File).toBe(true);
expect(formData.get('file').name).toBe('filename.test');

const body = await ReactServerDOMClient.encodeReply(formData);
const result = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
);

expect(result instanceof FormData).toBe(true);
expect(result.get('hi')).toBe('world');
const resultBlob = result.get('file');
expect(resultBlob instanceof Blob).toBe(true);
expect(resultBlob.name).toBe('filename.test'); // In this direction we allow file name to pass through but not other direction.
expect(resultBlob.size).toBe(bytes.length * 2);
expect(await resultBlob.arrayBuffer()).toEqual(await blob.arrayBuffer());
});
});
12 changes: 12 additions & 0 deletions yarn.lock
Expand Up @@ -2182,6 +2182,11 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.0.0.tgz#1a9e4b4c96d8c7886e0110ed310a0135144a1691"
integrity sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ==

"@fastify/busboy@^2.0.0":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d"
integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==

"@gitbeaker/core@^21.7.0":
version "21.7.0"
resolved "https://registry.yarnpkg.com/@gitbeaker/core/-/core-21.7.0.tgz#fcf7a12915d39f416e3f316d0a447a814179b8e5"
Expand Down Expand Up @@ -15762,6 +15767,13 @@ unc-path-regex@^0.1.0, unc-path-regex@^0.1.2:
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo=

undici@^5.28.4:
version "5.28.4"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068"
integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==
dependencies:
"@fastify/busboy" "^2.0.0"

unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
Expand Down