Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build Component Stacks from Native Stack Frames #18561

Merged
merged 9 commits into from Apr 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions fixtures/stacks/BabelClass-compiled.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions fixtures/stacks/BabelClass-compiled.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions fixtures/stacks/BabelClass.js
@@ -0,0 +1,8 @@
// Compile this with Babel.
// babel --config-file ./babel.config.json BabelClass.js --out-file BabelClass-compiled.js --source-maps

class BabelClass extends React.Component {
render() {
return this.props.children;
}
}
20 changes: 20 additions & 0 deletions fixtures/stacks/Component.js
@@ -0,0 +1,20 @@
// Example

const Throw = React.lazy(() => {
throw new Error('Example');
});

const Component = React.memo(function Component({children}) {
return children;
});

function DisplayName({children}) {
return children;
}
DisplayName.displayName = 'Custom Name';

class NativeClass extends React.Component {
render() {
return this.props.children;
}
}
59 changes: 59 additions & 0 deletions fixtures/stacks/Example.js
@@ -0,0 +1,59 @@
// Example

const x = React.createElement;

class ErrorBoundary extends React.Component {
static getDerivedStateFromError(error) {
return {
error: error,
};
}

componentDidCatch(error, errorInfo) {
console.log(error.message, errorInfo.componentStack);
this.setState({
componentStack: errorInfo.componentStack,
});
}

render() {
if (this.state && this.state.error) {
return x(
'div',
null,
x('h3', null, this.state.error.message),
x('pre', null, this.state.componentStack)
);
}
return this.props.children;
}
}

function Example() {
let state = React.useState(false);
return x(
ErrorBoundary,
null,
x(
DisplayName,
null,
x(
React.SuspenseList,
null,
x(
NativeClass,
null,
x(
BabelClass,
null,
x(
React.Suspense,
null,
x('div', null, x(Component, null, x(Throw)))
)
)
)
)
)
);
}
5 changes: 5 additions & 0 deletions fixtures/stacks/babel.config.json
@@ -0,0 +1,5 @@
{
"plugins": [
["@babel/plugin-transform-classes", {"loose": true}]
]
}
51 changes: 51 additions & 0 deletions fixtures/stacks/index.html
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Component Stacks</title>
<style>
html, body {
margin: 20px;
}
pre {
background: #eee;
border: 1px solid #ccc;
padding: 2px;
}
</style>
</head>
<body>
<div id="container">
<p>
To install React, follow the instructions on
<a href="https://github.com/facebook/react/">GitHub</a>.
</p>
<p>
If you can see this, React is <strong>not</strong> working right.
If you checked out the source from GitHub make sure to run <code>npm run build</code>.
</p>
</div>
<script src="../../build/node_modules/react/umd/react.production.min.js"></script>
<script src="../../build/node_modules/react-dom/umd/react-dom.production.min.js"></script>
<script src="./Component.js"></script>
<script src="./BabelClass-compiled.js"></script>
<script src="./Example.js"></script>
<script>
const container = document.getElementById("container");
ReactDOM.render(React.createElement(Example), container);
</script>
<h3>The above stack should look something like this:</h3>
<pre>

at Lazy
at Component (/stacks/Component.js:7:50)
at div
at Suspense
at BabelClass (/stacks/BabelClass-compiled.js:13:29)
at NativeClass (/stacks/Component.js:16:1)
at SuspenseList
at Custom Name (/stacks/Component.js:11:23)
at ErrorBoundary (/stacks/Example.js:5:1)
at Example (/stacks/Example.js:33:21)</pre>
</body>
</html>
7 changes: 6 additions & 1 deletion packages/react-devtools-shared/src/__tests__/console-test.js
Expand Up @@ -61,7 +61,12 @@ describe('console', () => {
});

function normalizeCodeLocInfo(str) {
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
return (
str &&
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
return '\n in ' + name + ' (at **)';
})
);
}

it('should not patch console methods that do not receive component stacks', () => {
Expand Down
7 changes: 6 additions & 1 deletion packages/react-dom/src/__tests__/ReactComponent-test.js
Expand Up @@ -16,7 +16,12 @@ let ReactTestUtils;

describe('ReactComponent', () => {
function normalizeCodeLocInfo(str) {
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
return (
str &&
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
return '\n in ' + name + ' (at **)';
})
);
}

beforeEach(() => {
Expand Down
37 changes: 26 additions & 11 deletions packages/react-dom/src/__tests__/ReactDOMComponent-test.js
Expand Up @@ -17,7 +17,12 @@ describe('ReactDOMComponent', () => {
const ReactFeatureFlags = require('shared/ReactFeatureFlags');

function normalizeCodeLocInfo(str) {
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
return (
str &&
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
return '\n in ' + name + ' (at **)';
})
);
}

beforeEach(() => {
Expand Down Expand Up @@ -1719,16 +1724,26 @@ describe('ReactDOMComponent', () => {
<tr />
</div>,
);
}).toErrorDev([
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
'<div>.' +
'\n in tr (at **)' +
'\n in div (at **)',
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
'<div>.' +
'\n in tr (at **)' +
'\n in div (at **)',
]);
}).toErrorDev(
ReactFeatureFlags.enableComponentStackLocations
? [
// This warning dedupes since they're in the same component.
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
'<div>.' +
'\n in tr (at **)' +
'\n in div (at **)',
]
: [
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
'<div>.' +
'\n in tr (at **)' +
'\n in div (at **)',
'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
'<div>.' +
'\n in tr (at **)' +
'\n in div (at **)',
],
);
});

it('warns on invalid nesting at root', () => {
Expand Down
Expand Up @@ -1097,7 +1097,7 @@ describe('ReactDOMServerHooks', () => {

it('useOpaqueIdentifier: ID is not used during hydration but is used in an update', async () => {
let _setShow;
function App() {
function App({unused}) {
Scheduler.unstable_yieldValue('App');
const id = useOpaqueIdentifier();
const [show, setShow] = useState(false);
Expand Down Expand Up @@ -1129,7 +1129,7 @@ describe('ReactDOMServerHooks', () => {

it('useOpaqueIdentifier: ID is not used during hydration but is used in an update in legacy', async () => {
let _setShow;
function App() {
function App({unused}) {
Scheduler.unstable_yieldValue('App');
const id = useOpaqueIdentifier();
const [show, setShow] = useState(false);
Expand Down
Expand Up @@ -497,18 +497,18 @@ describe('ReactErrorBoundaries', () => {
}
};

BrokenUseEffect = props => {
BrokenUseEffect = ({children}) => {
Scheduler.unstable_yieldValue('BrokenUseEffect render');

React.useEffect(() => {
Scheduler.unstable_yieldValue('BrokenUseEffect useEffect [!]');
throw new Error('Hello');
});

return props.children;
return children;
};

BrokenUseLayoutEffect = props => {
BrokenUseLayoutEffect = ({children}) => {
Scheduler.unstable_yieldValue('BrokenUseLayoutEffect render');

React.useLayoutEffect(() => {
Expand All @@ -518,7 +518,7 @@ describe('ReactErrorBoundaries', () => {
throw new Error('Hello');
});

return props.children;
return children;
};

NoopErrorBoundary = class extends React.Component {
Expand Down
Expand Up @@ -18,7 +18,12 @@ const enableSuspenseServerRenderer = require('shared/ReactFeatureFlags')
.enableSuspenseServerRenderer;

function normalizeCodeLocInfo(str) {
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
return (
str &&
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
return '\n in ' + name + ' (at **)';
})
);
}

describe('ReactDOMServer', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-dom/src/__tests__/ReactUpdates-test.js
Expand Up @@ -1615,7 +1615,7 @@ describe('ReactUpdates', () => {
Scheduler.unstable_clearYields();
}
expect(error).toContain('Warning: Maximum update depth exceeded.');
expect(stack).toContain('in NonTerminating');
expect(stack).toContain(' NonTerminating');
// rethrow error to prevent going into an infinite loop when act() exits
throw error;
});
Expand Down
Expand Up @@ -16,7 +16,12 @@ let createReactNativeComponentClass;
let computeComponentStackForErrorReporting;

function normalizeCodeLocInfo(str) {
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
return (
str &&
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
return '\n in ' + name + ' (at **)';
})
);
}

describe('ReactNativeError', () => {
Expand Down Expand Up @@ -74,20 +79,11 @@ describe('ReactNativeError', () => {
computeComponentStackForErrorReporting(reactTag),
);

if (__DEV__) {
expect(componentStack).toBe(
'\n' +
' in View (at **)\n' +
' in FunctionComponent (at **)\n' +
' in ClassComponent (at **)',
);
} else {
expect(componentStack).toBe(
'\n' +
' in View\n' +
' in FunctionComponent\n' +
' in ClassComponent',
);
}
expect(componentStack).toBe(
'\n' +
' in View (at **)\n' +
' in FunctionComponent (at **)\n' +
' in ClassComponent (at **)',
);
});
});