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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"plugins": [
["@babel/plugin-transform-classes", {"loose": true}]
]
}
51 changes: 51 additions & 0 deletions fixtures/stacks/index.html
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 **)',
);
});
});
Loading