Skip to content

Commit

Permalink
Use undici polyfill for tests in old Node versions
Browse files Browse the repository at this point in the history
To support old Node we need to adjust how we construct blobs.
  • Loading branch information
sebmarkbage committed May 3, 2024
1 parent 0a0a3af commit 94add5a
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 116 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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

0 comments on commit 94add5a

Please sign in to comment.