diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index 0c31177f1db49..9e01c53ea0c45 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -32,6 +32,7 @@ let webpackModuleLoading; let React; let ReactServer; let ReactDOMServer; +let ReactDOMFizzStatic; let ReactServerDOMServer; let ReactServerDOMStaticServer; let ReactServerDOMClient; @@ -102,6 +103,7 @@ describe('ReactFlightDOMEdge', () => { ); React = require('react'); ReactDOMServer = require('react-dom/server.edge'); + ReactDOMFizzStatic = require('react-dom/static.edge'); ReactServerDOMClient = require('react-server-dom-webpack/client'); use = React.use; }); @@ -228,6 +230,30 @@ describe('ReactFlightDOMEdge', () => { } } + async function createBufferedUnclosingStream( + prelude: ReadableStream, + ): ReadableStream { + const chunks: Array = []; + const reader = prelude.getReader(); + while (true) { + const {done, value} = await reader.read(); + if (done) { + break; + } else { + chunks.push(value); + } + } + + let i = 0; + return new ReadableStream({ + async pull(controller) { + if (i < chunks.length) { + controller.enqueue(chunks[i++]); + } + }, + }); + } + it('should allow an alternative module mapping to be used for SSR', async () => { function ClientComponent() { return Client Component; @@ -1777,4 +1803,114 @@ describe('ReactFlightDOMEdge', () => { expect(error).not.toBe(null); expect(error.message).toBe(expectedMessage); }); + + // @gate enableHalt + it('does not include source locations in component stacks for halted components', async () => { + // We only support adding source locations for halted components in the Node.js builds. + + async function Component() { + await new Promise(() => {}); + return null; + } + + function App() { + return ReactServer.createElement( + 'html', + null, + ReactServer.createElement( + 'body', + null, + ReactServer.createElement( + ReactServer.Suspense, + {fallback: 'Loading...'}, + ReactServer.createElement(Component, null), + ), + ), + ); + } + + const serverAbortController = new AbortController(); + const errors = []; + const prerenderResult = ReactServerDOMStaticServer.unstable_prerender( + ReactServer.createElement(App, null), + webpackMap, + { + signal: serverAbortController.signal, + onError(err) { + errors.push(err); + }, + }, + ); + + await new Promise(resolve => { + setImmediate(() => { + serverAbortController.abort(); + resolve(); + }); + }); + + const {prelude} = await prerenderResult; + + expect(errors).toEqual([]); + + function ClientRoot({response}) { + return use(response); + } + + const prerenderResponse = ReactServerDOMClient.createFromReadableStream( + await createBufferedUnclosingStream(prelude), + { + serverConsumerManifest: { + moduleMap: null, + moduleLoading: null, + }, + }, + ); + + let componentStack; + let ownerStack; + + const clientAbortController = new AbortController(); + + const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender( + React.createElement(ClientRoot, {response: prerenderResponse}), + { + signal: clientAbortController.signal, + onError(error, errorInfo) { + componentStack = errorInfo.componentStack; + ownerStack = React.captureOwnerStack + ? React.captureOwnerStack() + : null; + }, + }, + ); + + await new Promise(resolve => { + setImmediate(() => { + clientAbortController.abort(); + resolve(); + }); + }); + + const fizzPrerenderStream = await fizzPrerenderStreamResult; + const prerenderHTML = await readResult(fizzPrerenderStream.prelude); + + expect(prerenderHTML).toContain('Loading...'); + + if (__DEV__) { + expect(normalizeCodeLocInfo(componentStack)).toBe( + '\n in Component\n in Suspense\n in body\n in html\n in ClientRoot (at **)', + ); + } else { + expect(normalizeCodeLocInfo(componentStack)).toBe( + '\n in Suspense\n in body\n in html\n in ClientRoot (at **)', + ); + } + + if (__DEV__) { + expect(normalizeCodeLocInfo(ownerStack)).toBe('\n in App (at **)'); + } else { + expect(ownerStack).toBeNull(); + } + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index 5840762a73c28..aba7430bc2a34 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -21,6 +21,7 @@ let webpackModules; let webpackModuleLoading; let React; let ReactDOMServer; +let ReactDOMFizzStatic; let ReactServer; let ReactServerDOMServer; let ReactServerDOMStaticServer; @@ -70,11 +71,21 @@ describe('ReactFlightDOMNode', () => { React = require('react'); ReactDOMServer = require('react-dom/server.node'); + ReactDOMFizzStatic = require('react-dom/static'); ReactServerDOMClient = require('react-server-dom-webpack/client'); Stream = require('stream'); use = React.use; }); + function normalizeCodeLocInfo(str) { + return ( + str && + str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) { + return ' in ' + name + (/\d/.test(m) ? ' (at **)' : ''); + }) + ); + } + function readResult(stream) { return new Promise((resolve, reject) => { let buffer = ''; @@ -93,6 +104,42 @@ describe('ReactFlightDOMNode', () => { }); } + async function readWebResult(webStream: ReadableStream) { + const reader = webStream.getReader(); + let result = ''; + while (true) { + const {done, value} = await reader.read(); + if (done) { + return result; + } + result += Buffer.from(value).toString('utf8'); + } + } + + async function createBufferedUnclosingStream( + prelude: ReadableStream, + ): ReadableStream { + const chunks: Array = []; + const reader = prelude.getReader(); + while (true) { + const {done, value} = await reader.read(); + if (done) { + break; + } else { + chunks.push(value); + } + } + + let i = 0; + return new ReadableStream({ + async pull(controller) { + if (i < chunks.length) { + controller.enqueue(chunks[i++]); + } + }, + }); + } + it('should support web streams in node', async () => { function Text({children}) { return {children}; @@ -543,4 +590,125 @@ describe('ReactFlightDOMNode', () => { const result = await readResult(ssrStream); expect(result).toContain('loading...'); }); + + // @gate enableHalt && enableAsyncDebugInfo + it('includes source locations in component and owner stacks for halted components', async () => { + async function Component() { + await new Promise(() => {}); + return null; + } + + function App() { + return ReactServer.createElement( + 'html', + null, + ReactServer.createElement( + 'body', + null, + ReactServer.createElement( + ReactServer.Suspense, + {fallback: 'Loading...'}, + ReactServer.createElement(Component, null), + ), + ), + ); + } + + const errors = []; + const serverAbortController = new AbortController(); + const {pendingResult} = await serverAct(async () => { + // destructure trick to avoid the act scope from awaiting the returned value + return { + pendingResult: ReactServerDOMStaticServer.unstable_prerender( + ReactServer.createElement(App, null), + webpackMap, + { + signal: serverAbortController.signal, + onError(error) { + errors.push(error); + }, + }, + ), + }; + }); + + await await serverAct( + async () => + new Promise(resolve => { + setImmediate(() => { + serverAbortController.abort(); + resolve(); + }); + }), + ); + + const {prelude} = await pendingResult; + + expect(errors).toEqual([]); + + function ClientRoot({response}) { + return use(response); + } + + const prerenderResponse = ReactServerDOMClient.createFromReadableStream( + await createBufferedUnclosingStream(prelude), + { + serverConsumerManifest: { + moduleMap: null, + moduleLoading: null, + }, + }, + ); + + let componentStack; + let ownerStack; + + const clientAbortController = new AbortController(); + + const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender( + React.createElement(ClientRoot, {response: prerenderResponse}), + { + signal: clientAbortController.signal, + onError(error, errorInfo) { + componentStack = errorInfo.componentStack; + ownerStack = React.captureOwnerStack + ? React.captureOwnerStack() + : null; + }, + }, + ); + + await await serverAct( + async () => + new Promise(resolve => { + setImmediate(() => { + clientAbortController.abort(); + resolve(); + }); + }), + ); + + const fizzPrerenderStream = await fizzPrerenderStreamResult; + const prerenderHTML = await readWebResult(fizzPrerenderStream.prelude); + + expect(prerenderHTML).toContain('Loading...'); + + if (__DEV__) { + expect(normalizeCodeLocInfo(componentStack)).toBe( + '\n in Component (at **)\n in Suspense\n in body\n in html\n in ClientRoot (at **)', + ); + } else { + expect(normalizeCodeLocInfo(componentStack)).toBe( + '\n in Suspense\n in body\n in html\n in ClientRoot (at **)', + ); + } + + if (__DEV__) { + expect(normalizeCodeLocInfo(ownerStack)).toBe( + '\n in Component (at **)\n in App (at **)', + ); + } else { + expect(ownerStack).toBeNull(); + } + }); });