diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js
index f94fadd42962e..c96f6cef6fd30 100644
--- a/packages/react-client/src/__tests__/ReactFlight-test.js
+++ b/packages/react-client/src/__tests__/ReactFlight-test.js
@@ -10,6 +10,15 @@
'use strict';
+function normalizeCodeLocInfo(str) {
+ return (
+ str &&
+ str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
+ return '\n in ' + name + (/\d/.test(m) ? ' (at **)' : '');
+ })
+ );
+}
+
const heldValues = [];
let finalizationCallback;
function FinalizationRegistryMock(callback) {
@@ -69,6 +78,14 @@ describe('ReactFlight', () => {
error,
};
}
+ componentDidCatch(error, errorInfo) {
+ expect(error).toBe(this.state.error);
+ if (this.props.expectedStack !== undefined) {
+ expect(normalizeCodeLocInfo(errorInfo.componentStack)).toBe(
+ this.props.expectedStack,
+ );
+ }
+ }
componentDidMount() {
expect(this.state.hasError).toBe(true);
expect(this.state.error).toBeTruthy();
@@ -900,6 +917,91 @@ describe('ReactFlight', () => {
});
});
+ it('should include server components in error boundary stacks in dev', async () => {
+ const ClientErrorBoundary = clientReference(ErrorBoundary);
+
+ function Throw({value}) {
+ throw value;
+ }
+
+ const expectedStack = __DEV__
+ ? // TODO: This should include Throw but it doesn't have a Fiber.
+ '\n in div' + '\n in ErrorBoundary (at **)' + '\n in App'
+ : '\n in div' + '\n in ErrorBoundary (at **)';
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const transport = ReactNoopFlightServer.render(, {
+ onError(x) {
+ if (__DEV__) {
+ return 'a dev digest';
+ }
+ if (x instanceof Error) {
+ return `digest("${x.message}")`;
+ } else if (Array.isArray(x)) {
+ return `digest([])`;
+ } else if (typeof x === 'object' && x !== null) {
+ return `digest({})`;
+ }
+ return `digest(${String(x)})`;
+ },
+ });
+
+ await act(() => {
+ startTransition(() => {
+ ReactNoop.render(ReactNoopFlightClient.read(transport));
+ });
+ });
+ });
+
+ it('should include server components in warning stacks', async () => {
+ function Component() {
+ // Trigger key warning
+ return
{[]}
;
+ }
+ const ClientComponent = clientReference(Component);
+
+ function Indirection({children}) {
+ return children;
+ }
+
+ function App() {
+ return (
+
+
+
+ );
+ }
+
+ const transport = ReactNoopFlightServer.render();
+
+ await expect(async () => {
+ await act(() => {
+ startTransition(() => {
+ ReactNoop.render(ReactNoopFlightClient.read(transport));
+ });
+ });
+ }).toErrorDev(
+ 'Each child in a list should have a unique "key" prop.\n' +
+ '\n' +
+ 'Check the render method of `Component`. See https://reactjs.org/link/warning-keys for more information.\n' +
+ ' in span (at **)\n' +
+ ' in Component (at **)\n' +
+ ' in Indirection (at **)\n' +
+ ' in App (at **)',
+ );
+ });
+
it('should trigger the inner most error boundary inside a Client Component', async () => {
function ServerComponent() {
throw new Error('This was thrown in the Server Component.');
diff --git a/packages/react-devtools-shared/src/__tests__/componentStacks-test.js b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js
index 3f3ce3774332e..8771644314984 100644
--- a/packages/react-devtools-shared/src/__tests__/componentStacks-test.js
+++ b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js
@@ -86,4 +86,41 @@ describe('component stack', () => {
'\n in Example (at **)',
);
});
+
+ // @reactVersion >=16.9
+ it('should log the current component stack with debug info from promises', () => {
+ const Child = () => {
+ console.error('Test error.');
+ console.warn('Test warning.');
+ return null;
+ };
+ const ChildPromise = Promise.resolve();
+ ChildPromise.status = 'fulfilled';
+ ChildPromise.value = ;
+ ChildPromise._debugInfo = [
+ {
+ name: 'ServerComponent',
+ env: 'Server',
+ },
+ ];
+ const Parent = () => ChildPromise;
+ const Grandparent = () => ;
+
+ act(() => render());
+
+ expect(mockError).toHaveBeenCalledWith(
+ 'Test error.',
+ '\n in Child (at **)' +
+ '\n in ServerComponent (at **)' +
+ '\n in Parent (at **)' +
+ '\n in Grandparent (at **)',
+ );
+ expect(mockWarn).toHaveBeenCalledWith(
+ 'Test warning.',
+ '\n in Child (at **)' +
+ '\n in ServerComponent (at **)' +
+ '\n in Parent (at **)' +
+ '\n in Grandparent (at **)',
+ );
+ });
});
diff --git a/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js b/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js
index 98d944477d860..4e0e334ce86a6 100644
--- a/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js
+++ b/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js
@@ -50,6 +50,13 @@ export function describeBuiltInComponentFrame(
return '\n' + prefix + name;
}
+export function describeDebugInfoFrame(name: string, env: ?string): string {
+ return describeBuiltInComponentFrame(
+ name + (env ? ' (' + env + ')' : ''),
+ null,
+ );
+}
+
let reentry = false;
let componentFrameCache;
if (__DEV__) {
diff --git a/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js
index 410121c7c96a5..77f0adf37ea4e 100644
--- a/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js
+++ b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js
@@ -19,6 +19,7 @@ import {
describeBuiltInComponentFrame,
describeFunctionComponentFrame,
describeClassComponentFrame,
+ describeDebugInfoFrame,
} from './DevToolsComponentStackFrame';
export function describeFiber(
@@ -87,6 +88,16 @@ export function getStackByFiberInDevAndProd(
let node: Fiber = workInProgress;
do {
info += describeFiber(workTagMap, node, currentDispatcherRef);
+ // Add any Server Component stack frames in reverse order.
+ const debugInfo = node._debugInfo;
+ if (debugInfo) {
+ for (let i = debugInfo.length - 1; i >= 0; i--) {
+ const entry = debugInfo[i];
+ if (typeof entry.name === 'string') {
+ info += describeDebugInfoFrame(entry.name, entry.env);
+ }
+ }
+ }
// $FlowFixMe[incompatible-type] we bail out when we get a null
node = node.return;
} while (node);
diff --git a/packages/react-reconciler/src/ReactFiberComponentStack.js b/packages/react-reconciler/src/ReactFiberComponentStack.js
index 3300dec292c9a..36e22e8a9b1f2 100644
--- a/packages/react-reconciler/src/ReactFiberComponentStack.js
+++ b/packages/react-reconciler/src/ReactFiberComponentStack.js
@@ -26,6 +26,7 @@ import {
describeBuiltInComponentFrame,
describeFunctionComponentFrame,
describeClassComponentFrame,
+ describeDebugInfoFrame,
} from 'shared/ReactComponentStackFrame';
function describeFiber(fiber: Fiber): string {
@@ -64,6 +65,18 @@ export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
let node: Fiber = workInProgress;
do {
info += describeFiber(node);
+ if (__DEV__) {
+ // Add any Server Component stack frames in reverse order.
+ const debugInfo = node._debugInfo;
+ if (debugInfo) {
+ for (let i = debugInfo.length - 1; i >= 0; i--) {
+ const entry = debugInfo[i];
+ if (typeof entry.name === 'string') {
+ info += describeDebugInfoFrame(entry.name, entry.env);
+ }
+ }
+ }
+ }
// $FlowFixMe[incompatible-type] we bail out when we get a null
node = node.return;
} while (node);
diff --git a/packages/shared/ReactComponentStackFrame.js b/packages/shared/ReactComponentStackFrame.js
index 94a6c517d914a..c928f191d7b79 100644
--- a/packages/shared/ReactComponentStackFrame.js
+++ b/packages/shared/ReactComponentStackFrame.js
@@ -51,6 +51,13 @@ export function describeBuiltInComponentFrame(
}
}
+export function describeDebugInfoFrame(name: string, env: ?string): string {
+ return describeBuiltInComponentFrame(
+ name + (env ? ' (' + env + ')' : ''),
+ null,
+ );
+}
+
let reentry = false;
let componentFrameCache;
if (__DEV__) {