Skip to content
5 changes: 5 additions & 0 deletions fixtures/fizz/server/render-to-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ module.exports = function render(url, res) {
onError(x) {
didError = true;
console.error(x);
// Redundant with `console.createTask`. Only added for debugging.
console.error(
'The above error occurred during server rendering: %s',
React.captureOwnerStack()
);
},
});
// Abandon and switch to client rendering if enough time passes.
Expand Down
13 changes: 12 additions & 1 deletion fixtures/fizz/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@
*
*/

import {Suspense} from 'react';
import Html from './Html';
import BigComponent from './BigComponent';
import MaybeHaltedComponent from './MaybeHaltedComponent';

export default function App({assets, title}) {
const serverHalt =
typeof window === 'undefined'
? new Promise(() => {})
: Promise.resolve('client');

export default function App({assets, promise, title}) {
const components = [];

for (let i = 0; i <= 250; i++) {
Expand All @@ -21,6 +28,10 @@ export default function App({assets, title}) {
<h1>{title}</h1>
{components}
<h1>all done</h1>
<h2>or maybe not</h2>
<Suspense fallback="loading more...">
<MaybeHaltedComponent promise={serverHalt} />
</Suspense>
</Html>
);
}
6 changes: 6 additions & 0 deletions fixtures/fizz/src/MaybeHaltedComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {use} from 'react';

export default function MaybeHaltedComponent({promise}) {
use(promise);
return <div>Did not halt</div>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,31 @@ describe('ReactFlightDOMNode', () => {
);
}

/**
* Removes all stackframes not pointing into this file
*/
function ignoreListStack(str) {
if (!str) {
return str;
}

let ignoreListedStack = '';
const lines = str.split('\n');

// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const line of lines) {
if (
line.indexOf(__filename) === -1 &&
line.indexOf('<anonymous>') === -1
) {
} else {
ignoreListedStack += '\n' + line.replace(__dirname, '.');
}
}

return ignoreListedStack;
}

function readResult(stream) {
return new Promise((resolve, reject) => {
let buffer = '';
Expand Down Expand Up @@ -762,6 +787,165 @@ describe('ReactFlightDOMNode', () => {
}
});

// @gate enableHalt
it('includes source locations in component and owner stacks for halted Client components', async () => {
function SharedComponent({p1, p2, p3}) {
use(p1);
use(p2);
use(p3);
return <div>Hello, Dave!</div>;
}
const ClientComponentOnTheServer = clientExports(SharedComponent);
const ClientComponentOnTheClient = clientExports(
SharedComponent,
123,
'path/to/chunk.js',
);

let resolvePendingPromise;
function ServerComponent() {
const p1 = Promise.resolve();
const p2 = new Promise(resolve => {
resolvePendingPromise = value => {
p2.status = 'fulfilled';
p2.value = value;
resolve(value);
};
});
const p3 = new Promise(() => {});
return ReactServer.createElement(ClientComponentOnTheClient, {
p1: p1,
p2: p2,
p3: p3,
});
}

function App() {
return ReactServer.createElement(
'html',
null,
ReactServer.createElement(
'body',
null,
ReactServer.createElement(
ReactServer.Suspense,
{fallback: 'Loading...'},
ReactServer.createElement(ServerComponent, null),
),
),
);
}

const errors = [];
const rscStream = await serverAct(() =>
ReactServerDOMServer.renderToPipeableStream(
ReactServer.createElement(App, null),
webpackMap,
),
);

const readable = new Stream.PassThrough(streamOptions);
rscStream.pipe(readable);

function ClientRoot({response}) {
return use(response);
}

const serverConsumerManifest = {
moduleMap: {
[webpackMap[ClientComponentOnTheClient.$$id].id]: {
'*': webpackMap[ClientComponentOnTheServer.$$id],
},
},
moduleLoading: webpackModuleLoading,
};

expect(errors).toEqual([]);

function ClientRoot({response}) {
return use(response);
}

const response = ReactServerDOMClient.createFromNodeStream(
readable,
serverConsumerManifest,
);

let componentStack;
let ownerStack;

const clientAbortController = new AbortController();

const fizzPrerenderStreamResult = ReactDOMFizzStatic.prerender(
React.createElement(ClientRoot, {response}),
{
signal: clientAbortController.signal,
onError(error, errorInfo) {
componentStack = errorInfo.componentStack;
ownerStack = React.captureOwnerStack
? React.captureOwnerStack()
: null;
},
},
);

resolvePendingPromise('custom-instrum-resolve');
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 SharedComponent (at **)\n' +
' in ServerComponent' +
(gate(flags => flags.enableAsyncDebugInfo) ? ' (at **)' : '') +
'\n' +
' in Suspense\n' +
' in body\n' +
' in html\n' +
' in App (at **)\n' +
' in ClientRoot (at **)',
);
} else {
expect(normalizeCodeLocInfo(componentStack)).toBe(
'\n' +
' in SharedComponent (at **)\n' +
' in Suspense\n' +
' in body\n' +
' in html\n' +
' in ClientRoot (at **)',
);
}

if (__DEV__) {
expect(ignoreListStack(ownerStack)).toBe(
// eslint-disable-next-line react-internal/safe-string-coercion
'' +
// The concrete location may change as this test is updated.
// Just make sure they still point at React.use(p2)
(gate(flags => flags.enableAsyncDebugInfo)
? '\n at SharedComponent (./ReactFlightDOMNode-test.js:794:7)'
: '') +
'\n at ServerComponent (file://./ReactFlightDOMNode-test.js:816:26)' +
'\n at App (file://./ReactFlightDOMNode-test.js:833:25)',
);
} else {
expect(ownerStack).toBeNull();
}
});

// @gate enableHalt
it('includes deeper location for aborted stacks', async () => {
async function getData() {
Expand Down Expand Up @@ -1346,12 +1530,12 @@ describe('ReactFlightDOMNode', () => {
'\n' +
' in Dynamic' +
(gate(flags => flags.enableAsyncDebugInfo)
? ' (file://ReactFlightDOMNode-test.js:1216:27)\n'
? ' (file://ReactFlightDOMNode-test.js:1400:27)\n'
: '\n') +
' in body\n' +
' in html\n' +
' in App (file://ReactFlightDOMNode-test.js:1233:25)\n' +
' in ClientRoot (ReactFlightDOMNode-test.js:1308:16)',
' in App (file://ReactFlightDOMNode-test.js:1417:25)\n' +
' in ClientRoot (ReactFlightDOMNode-test.js:1492:16)',
);
} else {
expect(
Expand All @@ -1360,7 +1544,7 @@ describe('ReactFlightDOMNode', () => {
'\n' +
' in body\n' +
' in html\n' +
' in ClientRoot (ReactFlightDOMNode-test.js:1308:16)',
' in ClientRoot (ReactFlightDOMNode-test.js:1492:16)',
);
}

Expand All @@ -1370,16 +1554,16 @@ describe('ReactFlightDOMNode', () => {
normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
).toBe(
'\n' +
' in Dynamic (file://ReactFlightDOMNode-test.js:1216:27)\n' +
' in App (file://ReactFlightDOMNode-test.js:1233:25)',
' in Dynamic (file://ReactFlightDOMNode-test.js:1400:27)\n' +
' in App (file://ReactFlightDOMNode-test.js:1417:25)',
);
} else {
expect(
normalizeCodeLocInfo(ownerStack, {preserveLocation: true}),
).toBe(
'' +
'\n' +
' in App (file://ReactFlightDOMNode-test.js:1233:25)',
' in App (file://ReactFlightDOMNode-test.js:1417:25)',
);
}
} else {
Expand Down
Loading