mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
While most builds of Flight and Fizz schedule work in new tasks some do execute work synchronously. While this is necessary for legacy APIs like renderToString for modern APIs there really isn't a great reason to do this synchronously. We could schedule works as microtasks but we actually want to yield so the runtime can run events and other things that will unblock additional work before starting the next work loop. This change updates all non-legacy uses to be async using the best availalble macrotask scheduler. Browser now uses postMessage Bun uses setTimeout because while it also supports setImmediate the scheduling is not as eager as the same API in node the FB build also uses setTimeout This change required a number of changes to tests which were utilizing the sync nature of work in the Browser builds to avoid having to manage timers and tasks. I added a patch to install MessageChannel which is required by the browser builds and made this patched version integrate with the Scheduler mock. This way we can effectively use `act` to flush flight and fizz work similar to how we do this on the client.
377 lines
10 KiB
JavaScript
377 lines
10 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @emails react-core
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
|
|
|
|
global.ReadableStream =
|
|
require('web-streams-polyfill/ponyfill/es6').ReadableStream;
|
|
|
|
let clientExports;
|
|
let webpackMap;
|
|
let webpackModules;
|
|
let webpackModuleLoading;
|
|
let React;
|
|
let ReactDOMServer;
|
|
let ReactServerDOMServer;
|
|
let ReactServerDOMClient;
|
|
let Stream;
|
|
let use;
|
|
let ReactServerScheduler;
|
|
let reactServerAct;
|
|
|
|
describe('ReactFlightDOMNode', () => {
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
|
|
ReactServerScheduler = require('scheduler');
|
|
patchSetImmediate(ReactServerScheduler);
|
|
reactServerAct = require('internal-test-utils').act;
|
|
|
|
// Simulate the condition resolution
|
|
jest.mock('react', () => require('react/react.react-server'));
|
|
jest.mock('react-server-dom-webpack/server', () =>
|
|
require('react-server-dom-webpack/server.node'),
|
|
);
|
|
ReactServerDOMServer = require('react-server-dom-webpack/server');
|
|
|
|
const WebpackMock = require('./utils/WebpackMock');
|
|
clientExports = WebpackMock.clientExports;
|
|
webpackMap = WebpackMock.webpackMap;
|
|
webpackModules = WebpackMock.webpackModules;
|
|
webpackModuleLoading = WebpackMock.moduleLoading;
|
|
|
|
jest.resetModules();
|
|
__unmockReact();
|
|
jest.unmock('react-server-dom-webpack/server');
|
|
jest.mock('react-server-dom-webpack/client', () =>
|
|
require('react-server-dom-webpack/client.node'),
|
|
);
|
|
|
|
React = require('react');
|
|
ReactDOMServer = require('react-dom/server.node');
|
|
ReactServerDOMClient = require('react-server-dom-webpack/client');
|
|
Stream = require('stream');
|
|
use = React.use;
|
|
});
|
|
|
|
async function serverAct(callback) {
|
|
let maybePromise;
|
|
await reactServerAct(() => {
|
|
maybePromise = callback();
|
|
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
maybePromise.catch(() => {});
|
|
}
|
|
});
|
|
return maybePromise;
|
|
}
|
|
|
|
function readResult(stream) {
|
|
return new Promise((resolve, reject) => {
|
|
let buffer = '';
|
|
const writable = new Stream.PassThrough();
|
|
writable.setEncoding('utf8');
|
|
writable.on('data', chunk => {
|
|
buffer += chunk;
|
|
});
|
|
writable.on('error', error => {
|
|
reject(error);
|
|
});
|
|
writable.on('end', () => {
|
|
resolve(buffer);
|
|
});
|
|
stream.pipe(writable);
|
|
});
|
|
}
|
|
|
|
it('should allow an alternative module mapping to be used for SSR', async () => {
|
|
function ClientComponent() {
|
|
return <span>Client Component</span>;
|
|
}
|
|
// The Client build may not have the same IDs as the Server bundles for the same
|
|
// component.
|
|
const ClientComponentOnTheClient = clientExports(
|
|
ClientComponent,
|
|
123,
|
|
'path/to/chunk.js',
|
|
);
|
|
const ClientComponentOnTheServer = clientExports(ClientComponent);
|
|
|
|
// In the SSR bundle this module won't exist. We simulate this by deleting it.
|
|
const clientId = webpackMap[ClientComponentOnTheClient.$$id].id;
|
|
delete webpackModules[clientId];
|
|
|
|
// Instead, we have to provide a translation from the client meta data to the SSR
|
|
// meta data.
|
|
const ssrMetadata = webpackMap[ClientComponentOnTheServer.$$id];
|
|
const translationMap = {
|
|
[clientId]: {
|
|
'*': ssrMetadata,
|
|
},
|
|
};
|
|
const ssrManifest = {
|
|
moduleMap: translationMap,
|
|
moduleLoading: webpackModuleLoading,
|
|
};
|
|
|
|
function App() {
|
|
return <ClientComponentOnTheClient />;
|
|
}
|
|
|
|
const stream = await serverAct(() =>
|
|
ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
|
|
);
|
|
const readable = new Stream.PassThrough();
|
|
let response;
|
|
|
|
stream.pipe(readable);
|
|
|
|
function ClientRoot() {
|
|
if (response) return use(response);
|
|
response = ReactServerDOMClient.createFromNodeStream(
|
|
readable,
|
|
ssrManifest,
|
|
);
|
|
return use(response);
|
|
}
|
|
|
|
const ssrStream = await serverAct(() =>
|
|
ReactDOMServer.renderToPipeableStream(<ClientRoot />),
|
|
);
|
|
const result = await readResult(ssrStream);
|
|
expect(result).toEqual(
|
|
'<script src="/path/to/chunk.js" async=""></script><span>Client Component</span>',
|
|
);
|
|
});
|
|
|
|
it('should encode long string in a compact format', async () => {
|
|
const testString = '"\n\t'.repeat(500) + '🙃';
|
|
|
|
const stream = await serverAct(() =>
|
|
ReactServerDOMServer.renderToPipeableStream({
|
|
text: testString,
|
|
}),
|
|
);
|
|
|
|
const readable = new Stream.PassThrough();
|
|
|
|
const stringResult = readResult(readable);
|
|
const parsedResult = ReactServerDOMClient.createFromNodeStream(readable, {
|
|
moduleMap: {},
|
|
moduleLoading: webpackModuleLoading,
|
|
});
|
|
|
|
stream.pipe(readable);
|
|
|
|
const serializedContent = await stringResult;
|
|
// The content should be compact an unescaped
|
|
expect(serializedContent.length).toBeLessThan(2000);
|
|
expect(serializedContent).not.toContain('\\n');
|
|
expect(serializedContent).not.toContain('\\t');
|
|
expect(serializedContent).not.toContain('\\"');
|
|
expect(serializedContent).toContain('\t');
|
|
|
|
const result = await parsedResult;
|
|
// Should still match the result when parsed
|
|
expect(result.text).toBe(testString);
|
|
});
|
|
|
|
// @gate enableBinaryFlight
|
|
it('should be able to serialize any kind of typed array', async () => {
|
|
const buffer = new Uint8Array([
|
|
123, 4, 10, 5, 100, 255, 244, 45, 56, 67, 43, 124, 67, 89, 100, 20,
|
|
]).buffer;
|
|
const buffers = [
|
|
buffer,
|
|
new Int8Array(buffer, 1),
|
|
new Uint8Array(buffer, 2),
|
|
new Uint8ClampedArray(buffer, 2),
|
|
new Int16Array(buffer, 2),
|
|
new Uint16Array(buffer, 2),
|
|
new Int32Array(buffer, 4),
|
|
new Uint32Array(buffer, 4),
|
|
new Float32Array(buffer, 4),
|
|
new Float64Array(buffer, 0),
|
|
new BigInt64Array(buffer, 0),
|
|
new BigUint64Array(buffer, 0),
|
|
new DataView(buffer, 3),
|
|
];
|
|
const stream = await serverAct(() =>
|
|
ReactServerDOMServer.renderToPipeableStream(buffers),
|
|
);
|
|
const readable = new Stream.PassThrough();
|
|
const promise = ReactServerDOMClient.createFromNodeStream(readable, {
|
|
moduleMap: {},
|
|
moduleLoading: webpackModuleLoading,
|
|
});
|
|
stream.pipe(readable);
|
|
const result = await promise;
|
|
expect(result).toEqual(buffers);
|
|
});
|
|
|
|
it('should allow accept a nonce option for Flight preinitialized scripts', async () => {
|
|
function ClientComponent() {
|
|
return <span>Client Component</span>;
|
|
}
|
|
// The Client build may not have the same IDs as the Server bundles for the same
|
|
// component.
|
|
const ClientComponentOnTheClient = clientExports(
|
|
ClientComponent,
|
|
123,
|
|
'path/to/chunk.js',
|
|
);
|
|
const ClientComponentOnTheServer = clientExports(ClientComponent);
|
|
|
|
// In the SSR bundle this module won't exist. We simulate this by deleting it.
|
|
const clientId = webpackMap[ClientComponentOnTheClient.$$id].id;
|
|
delete webpackModules[clientId];
|
|
|
|
// Instead, we have to provide a translation from the client meta data to the SSR
|
|
// meta data.
|
|
const ssrMetadata = webpackMap[ClientComponentOnTheServer.$$id];
|
|
const translationMap = {
|
|
[clientId]: {
|
|
'*': ssrMetadata,
|
|
},
|
|
};
|
|
const ssrManifest = {
|
|
moduleMap: translationMap,
|
|
moduleLoading: webpackModuleLoading,
|
|
};
|
|
|
|
function App() {
|
|
return <ClientComponentOnTheClient />;
|
|
}
|
|
|
|
const stream = await serverAct(() =>
|
|
ReactServerDOMServer.renderToPipeableStream(<App />, webpackMap),
|
|
);
|
|
const readable = new Stream.PassThrough();
|
|
let response;
|
|
|
|
stream.pipe(readable);
|
|
|
|
function ClientRoot() {
|
|
if (response) return use(response);
|
|
response = ReactServerDOMClient.createFromNodeStream(
|
|
readable,
|
|
ssrManifest,
|
|
{
|
|
nonce: 'r4nd0m',
|
|
},
|
|
);
|
|
return use(response);
|
|
}
|
|
|
|
const ssrStream = await serverAct(() =>
|
|
ReactDOMServer.renderToPipeableStream(<ClientRoot />),
|
|
);
|
|
const result = await readResult(ssrStream);
|
|
expect(result).toEqual(
|
|
'<script src="/path/to/chunk.js" async="" nonce="r4nd0m"></script><span>Client Component</span>',
|
|
);
|
|
});
|
|
|
|
// @gate enableFlightReadableStream
|
|
it('should cancels the underlying ReadableStream when we are cancelled', async () => {
|
|
let controller;
|
|
let cancelReason;
|
|
const s = new ReadableStream({
|
|
start(c) {
|
|
controller = c;
|
|
},
|
|
cancel(r) {
|
|
cancelReason = r;
|
|
},
|
|
});
|
|
|
|
const rscStream = await serverAct(() =>
|
|
ReactServerDOMServer.renderToPipeableStream(
|
|
s,
|
|
{},
|
|
{
|
|
onError(error) {
|
|
return error.message;
|
|
},
|
|
},
|
|
),
|
|
);
|
|
|
|
const writable = new Stream.PassThrough();
|
|
rscStream.pipe(writable);
|
|
|
|
controller.enqueue('hi');
|
|
|
|
const reason = new Error('aborted');
|
|
writable.destroy(reason);
|
|
|
|
await new Promise(resolve => {
|
|
writable.on('error', () => {
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
expect(cancelReason.message).toBe(
|
|
'The destination stream errored while writing data.',
|
|
);
|
|
});
|
|
|
|
// @gate enableFlightReadableStream
|
|
it('should cancels the underlying ReadableStream when we abort', async () => {
|
|
const errors = [];
|
|
let controller;
|
|
let cancelReason;
|
|
const s = new ReadableStream({
|
|
start(c) {
|
|
controller = c;
|
|
},
|
|
cancel(r) {
|
|
cancelReason = r;
|
|
},
|
|
});
|
|
const rscStream = await serverAct(() =>
|
|
ReactServerDOMServer.renderToPipeableStream(
|
|
s,
|
|
{},
|
|
{
|
|
onError(x) {
|
|
errors.push(x);
|
|
return x.message;
|
|
},
|
|
},
|
|
),
|
|
);
|
|
|
|
const readable = new Stream.PassThrough();
|
|
rscStream.pipe(readable);
|
|
|
|
const result = await ReactServerDOMClient.createFromNodeStream(readable, {
|
|
moduleMap: {},
|
|
moduleLoading: webpackModuleLoading,
|
|
});
|
|
const reader = result.getReader();
|
|
controller.enqueue('hi');
|
|
|
|
const reason = new Error('aborted');
|
|
rscStream.abort(reason);
|
|
|
|
expect(cancelReason).toBe(reason);
|
|
|
|
let error = null;
|
|
try {
|
|
await reader.read();
|
|
} catch (x) {
|
|
error = x;
|
|
}
|
|
expect(error.digest).toBe('aborted');
|
|
expect(errors).toEqual([reason]);
|
|
});
|
|
});
|