/** * 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 * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment */ 'use strict'; let clientExports; let turbopackMap; let turbopackModules; let React; let ReactServer; let ReactDOMServer; let ReactServerDOMServer; let ReactServerDOMClient; let use; let serverAct; describe('ReactFlightTurbopackDOMEdge', () => { beforeEach(() => { jest.resetModules(); serverAct = require('internal-test-utils').serverAct; // Simulate the condition resolution jest.mock('react', () => require('react/react.react-server')); jest.mock('react-server-dom-turbopack/server', () => require('react-server-dom-turbopack/server.edge'), ); const TurbopackMock = require('./utils/TurbopackMock'); clientExports = TurbopackMock.clientExports; turbopackMap = TurbopackMock.turbopackMap; turbopackModules = TurbopackMock.turbopackModules; ReactServer = require('react'); ReactServerDOMServer = require('react-server-dom-turbopack/server.edge'); jest.resetModules(); __unmockReact(); React = require('react'); ReactDOMServer = require('react-dom/server.edge'); ReactServerDOMClient = require('react-server-dom-turbopack/client.edge'); use = React.use; }); async function readResult(stream) { const reader = stream.getReader(); let result = ''; while (true) { const {done, value} = await reader.read(); if (done) { return result; } result += Buffer.from(value).toString('utf8'); } } function normalizeCodeLocInfo(str) { return ( str && str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) { return ' in ' + name + (/\d/.test(m) ? ' (at **)' : ''); }) ); } function createDelayedStream( stream: ReadableStream, ): ReadableStream { return new ReadableStream({ async start(controller) { const reader = stream.getReader(); while (true) { const {done, value} = await reader.read(); if (done) { controller.close(); } else { // Artificially delay between enqueuing chunks. await new Promise(resolve => setTimeout(resolve)); controller.enqueue(value); } } }, }); } it('should allow an alternative module mapping to be used for SSR', async () => { function ClientComponent() { return Client Component; } // The Client build may not have the same IDs as the Server bundles for the same // component. const ClientComponentOnTheClient = clientExports(ClientComponent); const ClientComponentOnTheServer = clientExports(ClientComponent); // In the SSR bundle this module won't exist. We simulate this by deleting it. const clientId = turbopackMap[ClientComponentOnTheClient.$$id].id; delete turbopackModules[clientId]; // Instead, we have to provide a translation from the client meta data to the SSR // meta data. const ssrMetadata = turbopackMap[ClientComponentOnTheServer.$$id]; const translationMap = { [clientId]: { '*': ssrMetadata, }, }; function App() { return ; } const stream = await serverAct(() => ReactServerDOMServer.renderToReadableStream(, turbopackMap), ); const response = ReactServerDOMClient.createFromReadableStream(stream, { serverConsumerManifest: { moduleMap: translationMap, moduleLoading: null, }, }); function ClientRoot() { return use(response); } const ssrStream = await serverAct(() => ReactDOMServer.renderToReadableStream(), ); const result = await readResult(ssrStream); expect(result).toEqual('Client Component'); }); // @gate __DEV__ it('can transport debug info through a separate debug channel', async () => { function Thrower() { throw new Error('ssr-throw'); } const ClientComponentOnTheClient = clientExports( Thrower, 123, 'path/to/chunk.js', ); const ClientComponentOnTheServer = clientExports(Thrower); function App() { return ReactServer.createElement( ReactServer.Suspense, null, ReactServer.createElement(ClientComponentOnTheClient, null), ); } let debugReadableStreamController; const debugReadableStream = new ReadableStream({ start(controller) { debugReadableStreamController = controller; }, }); const rscStream = await serverAct(() => ReactServerDOMServer.renderToReadableStream( ReactServer.createElement(App, null), turbopackMap, { debugChannel: { writable: new WritableStream({ write(chunk) { debugReadableStreamController.enqueue(chunk); }, close() { debugReadableStreamController.close(); }, }), }, }, ), ); function ClientRoot({response}) { return use(response); } const serverConsumerManifest = { moduleMap: { [turbopackMap[ClientComponentOnTheClient.$$id].id]: { '*': turbopackMap[ClientComponentOnTheServer.$$id], }, }, moduleLoading: null, }; const response = ReactServerDOMClient.createFromReadableStream( // Create a delayed stream to simulate that the RSC stream might be // transported slower than the debug channel, which must not lead to a // `Connection closed` error in the Flight client. createDelayedStream(rscStream), { serverConsumerManifest, debugChannel: {readable: debugReadableStream}, }, ); let ownerStack; const ssrStream = await serverAct(() => ReactDOMServer.renderToReadableStream( , { onError(err, errorInfo) { ownerStack = React.captureOwnerStack ? React.captureOwnerStack() : null; }, }, ), ); const result = await readResult(ssrStream); expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)'); expect(result).toContain( 'Switched to client rendering because the server rendering errored:\n\nssr-throw', ); }); // @gate __DEV__ it('can transport debug info through a slow debug channel', async () => { function Thrower() { throw new Error('ssr-throw'); } const ClientComponentOnTheClient = clientExports( Thrower, 123, 'path/to/chunk.js', ); const ClientComponentOnTheServer = clientExports(Thrower); function App() { return ReactServer.createElement( ReactServer.Suspense, null, ReactServer.createElement(ClientComponentOnTheClient, null), ); } let debugReadableStreamController; const debugReadableStream = new ReadableStream({ start(controller) { debugReadableStreamController = controller; }, }); const rscStream = await serverAct(() => ReactServerDOMServer.renderToReadableStream( ReactServer.createElement(App, null), turbopackMap, { debugChannel: { writable: new WritableStream({ write(chunk) { debugReadableStreamController.enqueue(chunk); }, close() { debugReadableStreamController.close(); }, }), }, }, ), ); function ClientRoot({response}) { return use(response); } const serverConsumerManifest = { moduleMap: { [turbopackMap[ClientComponentOnTheClient.$$id].id]: { '*': turbopackMap[ClientComponentOnTheServer.$$id], }, }, moduleLoading: null, }; const response = ReactServerDOMClient.createFromReadableStream(rscStream, { serverConsumerManifest, debugChannel: { readable: // Create a delayed stream to simulate that the debug stream might // be transported slower than the RSC stream, which must not lead to // missing debug info. createDelayedStream(debugReadableStream), }, }); let ownerStack; const ssrStream = await serverAct(() => ReactDOMServer.renderToReadableStream( , { onError(err, errorInfo) { ownerStack = React.captureOwnerStack ? React.captureOwnerStack() : null; }, }, ), ); const result = await readResult(ssrStream); expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)'); expect(result).toContain( 'Switched to client rendering because the server rendering errored:\n\nssr-throw', ); }); });