mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Show component code frame, if available (#48785)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/48785 ## Overview This change adds code frames for component stacks if it differs from the call stack frame. ## Background After adding native stack based stack frames, which we already symbolicate, we had the capability to show a code frame for component stacks as well as the stack frame (if they differ). However, this usually wasn't that useful, because the native component stack was unlikely to be more useful than the component stack (see the comparisons below for key errors). ## Owner stacks With owner stacks the component frame is a lot more useful and in many cases are better at showing the location than the call stack frame. ## Example Screens Before: {F1974436645} After (with owner stacks): {F1974436723} Changelog: [General][Added] - Add owner stack code frames to LogBox Reviewed By: hoxyq Differential Revision: D68285627 fbshipit-source-id: 541cecbd4786fffd1970d2e85e659757e3f81604
This commit is contained in:
committed by
Facebook GitHub Bot
parent
967ef32154
commit
0affa544c3
+42
-20
@@ -79,6 +79,7 @@ class LogBoxLog {
|
||||
count: number;
|
||||
level: LogLevel;
|
||||
codeFrame: ?CodeFrame;
|
||||
componentCodeFrame: ?CodeFrame;
|
||||
isComponentError: boolean;
|
||||
extraData: mixed | void;
|
||||
symbolicated:
|
||||
@@ -140,8 +141,18 @@ class LogBoxLog {
|
||||
}
|
||||
|
||||
retrySymbolicate(callback?: (status: SymbolicationStatus) => void): void {
|
||||
let retry = false;
|
||||
if (this.symbolicated.status !== 'COMPLETE') {
|
||||
LogBoxSymbolication.deleteStack(this.stack);
|
||||
retry = true;
|
||||
}
|
||||
if (this.symbolicatedComponentStack.status !== 'COMPLETE') {
|
||||
LogBoxSymbolication.deleteStack(
|
||||
convertComponentStateToStack(this.componentStack),
|
||||
);
|
||||
retry = true;
|
||||
}
|
||||
if (retry) {
|
||||
this.handleSymbolicate(callback);
|
||||
}
|
||||
}
|
||||
@@ -153,7 +164,10 @@ class LogBoxLog {
|
||||
}
|
||||
|
||||
handleSymbolicate(callback?: (status: SymbolicationStatus) => void): void {
|
||||
if (this.symbolicated.status !== 'PENDING') {
|
||||
if (
|
||||
this.symbolicated.status !== 'PENDING' &&
|
||||
this.symbolicated.status !== 'COMPLETE'
|
||||
) {
|
||||
this.updateStatus(null, null, null, callback);
|
||||
LogBoxSymbolication.symbolicate(this.stack, this.extraData).then(
|
||||
data => {
|
||||
@@ -163,25 +177,30 @@ class LogBoxLog {
|
||||
this.updateStatus(error, null, null, callback);
|
||||
},
|
||||
);
|
||||
if (this.componentStack != null && this.componentStackType === 'stack') {
|
||||
this.updateComponentStackStatus(null, null, null, callback);
|
||||
const componentStackFrames = convertComponentStateToStack(
|
||||
this.componentStack,
|
||||
);
|
||||
LogBoxSymbolication.symbolicate(componentStackFrames, []).then(
|
||||
data => {
|
||||
this.updateComponentStackStatus(
|
||||
null,
|
||||
convertStackToComponentStack(data.stack),
|
||||
null,
|
||||
callback,
|
||||
);
|
||||
},
|
||||
error => {
|
||||
this.updateComponentStackStatus(error, null, null, callback);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
this.componentStack != null &&
|
||||
this.componentStackType === 'stack' &&
|
||||
this.symbolicatedComponentStack.status !== 'PENDING' &&
|
||||
this.symbolicatedComponentStack.status !== 'COMPLETE'
|
||||
) {
|
||||
this.updateComponentStackStatus(null, null, null, callback);
|
||||
const componentStackFrames = convertComponentStateToStack(
|
||||
this.componentStack,
|
||||
);
|
||||
LogBoxSymbolication.symbolicate(componentStackFrames, []).then(
|
||||
data => {
|
||||
this.updateComponentStackStatus(
|
||||
null,
|
||||
convertStackToComponentStack(data.stack),
|
||||
data?.codeFrame,
|
||||
callback,
|
||||
);
|
||||
},
|
||||
error => {
|
||||
this.updateComponentStackStatus(error, null, null, callback);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,6 +254,9 @@ class LogBoxLog {
|
||||
status: 'FAILED',
|
||||
};
|
||||
} else if (componentStack != null) {
|
||||
if (codeFrame) {
|
||||
this.componentCodeFrame = codeFrame;
|
||||
}
|
||||
this.symbolicatedComponentStack = {
|
||||
error: null,
|
||||
componentStack,
|
||||
|
||||
+757
-189
File diff suppressed because it is too large
Load Diff
@@ -52,7 +52,10 @@ export default function LogBoxInspectorBody(props: {
|
||||
title={headerTitle}
|
||||
/>
|
||||
<ScrollView style={styles.scrollBody}>
|
||||
<LogBoxInspectorCodeFrame codeFrame={props.log.codeFrame} />
|
||||
<LogBoxInspectorCodeFrame
|
||||
codeFrame={props.log.codeFrame}
|
||||
componentCodeFrame={props.log.componentCodeFrame}
|
||||
/>
|
||||
<LogBoxInspectorReactFrames log={props.log} />
|
||||
<LogBoxInspectorStackFrames log={props.log} onRetry={props.onRetry} />
|
||||
</ScrollView>
|
||||
@@ -68,7 +71,10 @@ export default function LogBoxInspectorBody(props: {
|
||||
level={props.log.level}
|
||||
title={headerTitle}
|
||||
/>
|
||||
<LogBoxInspectorCodeFrame codeFrame={props.log.codeFrame} />
|
||||
<LogBoxInspectorCodeFrame
|
||||
codeFrame={props.log.codeFrame}
|
||||
componentCodeFrame={props.log.componentCodeFrame}
|
||||
/>
|
||||
<LogBoxInspectorReactFrames log={props.log} />
|
||||
<LogBoxInspectorStackFrames log={props.log} onRetry={props.onRetry} />
|
||||
</ScrollView>
|
||||
|
||||
@@ -22,16 +22,13 @@ import LogBoxButton from './LogBoxButton';
|
||||
import LogBoxInspectorSection from './LogBoxInspectorSection';
|
||||
import * as LogBoxStyle from './LogBoxStyle';
|
||||
import * as React from 'react';
|
||||
|
||||
type Props = $ReadOnly<{
|
||||
componentCodeFrame: ?CodeFrame,
|
||||
codeFrame: ?CodeFrame,
|
||||
}>;
|
||||
|
||||
function LogBoxInspectorCodeFrame(props: Props): React.Node {
|
||||
const codeFrame = props.codeFrame;
|
||||
if (codeFrame == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function CodeFrameDisplay({codeFrame}: {codeFrame: CodeFrame}): React.Node {
|
||||
function getFileName() {
|
||||
// $FlowFixMe[incompatible-use]
|
||||
const matches = /[^/]*$/.exec(codeFrame.fileName);
|
||||
@@ -56,30 +53,52 @@ function LogBoxInspectorCodeFrame(props: Props): React.Node {
|
||||
}
|
||||
|
||||
return (
|
||||
<LogBoxInspectorSection heading="Source" action={<AppInfo />}>
|
||||
<View style={styles.box}>
|
||||
<View style={styles.frame}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
contentContainerStyle={styles.contentContainer}>
|
||||
<AnsiHighlight style={styles.content} text={codeFrame.content} />
|
||||
</ScrollView>
|
||||
</View>
|
||||
<LogBoxButton
|
||||
backgroundColor={{
|
||||
default: 'transparent',
|
||||
pressed: LogBoxStyle.getBackgroundDarkColor(1),
|
||||
}}
|
||||
style={styles.button}
|
||||
onPress={() => {
|
||||
openFileInEditor(codeFrame.fileName, codeFrame.location?.row ?? 0);
|
||||
}}>
|
||||
<Text style={styles.fileText}>
|
||||
{getFileName()}
|
||||
{getLocation()}
|
||||
</Text>
|
||||
</LogBoxButton>
|
||||
<View style={styles.box}>
|
||||
<View style={styles.frame}>
|
||||
<ScrollView horizontal contentContainerStyle={styles.contentContainer}>
|
||||
<AnsiHighlight style={styles.content} text={codeFrame.content} />
|
||||
</ScrollView>
|
||||
</View>
|
||||
<LogBoxButton
|
||||
backgroundColor={{
|
||||
default: 'transparent',
|
||||
pressed: LogBoxStyle.getBackgroundDarkColor(1),
|
||||
}}
|
||||
style={styles.button}
|
||||
onPress={() => {
|
||||
openFileInEditor(codeFrame.fileName, codeFrame.location?.row ?? 0);
|
||||
}}>
|
||||
<Text style={styles.fileText}>
|
||||
{getFileName()}
|
||||
{getLocation()}
|
||||
</Text>
|
||||
</LogBoxButton>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function LogBoxInspectorCodeFrame(props: Props): React.Node {
|
||||
const {codeFrame, componentCodeFrame} = props;
|
||||
let sources = [];
|
||||
if (codeFrame != null) {
|
||||
sources.push(codeFrame);
|
||||
}
|
||||
if (
|
||||
componentCodeFrame != null &&
|
||||
componentCodeFrame?.content !== codeFrame?.content
|
||||
) {
|
||||
sources.push(componentCodeFrame);
|
||||
}
|
||||
if (sources.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<LogBoxInspectorSection
|
||||
heading={sources.length > 1 ? 'Sources' : 'Source'}
|
||||
action={<AppInfo />}>
|
||||
{sources.map((frame, index) => (
|
||||
<CodeFrameDisplay key={index} codeFrame={frame} />
|
||||
))}
|
||||
</LogBoxInspectorSection>
|
||||
);
|
||||
}
|
||||
|
||||
+65
-1
@@ -37,7 +37,7 @@ jest.mock('../LogBoxInspectorSection', () => ({
|
||||
describe('LogBoxInspectorCodeFrame', () => {
|
||||
it('should render null for no code frame', async () => {
|
||||
const output = await render.create(
|
||||
<LogBoxInspectorCodeFrame codeFrame={null} />,
|
||||
<LogBoxInspectorCodeFrame componentCodeFrame={null} codeFrame={null} />,
|
||||
);
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
@@ -46,6 +46,7 @@ describe('LogBoxInspectorCodeFrame', () => {
|
||||
it('should render a code frame', async () => {
|
||||
const output = await render.create(
|
||||
<LogBoxInspectorCodeFrame
|
||||
componentCodeFrame={null}
|
||||
codeFrame={{
|
||||
fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js',
|
||||
location: {row: 199, column: 0},
|
||||
@@ -61,9 +62,72 @@ describe('LogBoxInspectorCodeFrame', () => {
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render both a code frame and a component frame', async () => {
|
||||
const output = await render.create(
|
||||
<LogBoxInspectorCodeFrame
|
||||
componentCodeFrame={{
|
||||
content: ` 89 |
|
||||
90 | function Child() {
|
||||
> 91 | return <ConsoleWithThrow />;
|
||||
| ^
|
||||
92 | }
|
||||
93 |
|
||||
94 |`,
|
||||
location: {row: 90, column: 10},
|
||||
fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js',
|
||||
}}
|
||||
codeFrame={{
|
||||
fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js',
|
||||
location: {row: 64, column: 16},
|
||||
content: ` 62 |
|
||||
63 | function ConsoleWithThrow() {
|
||||
> 64 | console.error('hit');
|
||||
| ^
|
||||
65 | throw new Error('test');
|
||||
66 | }
|
||||
67 |`,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should dedupe if code frames are the same', async () => {
|
||||
const output = await render.create(
|
||||
<LogBoxInspectorCodeFrame
|
||||
componentCodeFrame={{
|
||||
content: ` 63 | function ConsoleWithThrow() {
|
||||
64 | console.error('hit');
|
||||
> 65 | throw new Error('test');
|
||||
| ^
|
||||
66 | }
|
||||
67 |
|
||||
68 |`,
|
||||
location: {row: 65, column: 18},
|
||||
fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js',
|
||||
}}
|
||||
codeFrame={{
|
||||
content: ` 63 | function ConsoleWithThrow() {
|
||||
64 | console.error('hit');
|
||||
> 65 | throw new Error('test');
|
||||
| ^
|
||||
66 | }
|
||||
67 |
|
||||
68 |`,
|
||||
location: {row: 65, column: 18},
|
||||
fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render a code frame without a location', async () => {
|
||||
const output = await render.create(
|
||||
<LogBoxInspectorCodeFrame
|
||||
componentCodeFrame={null}
|
||||
codeFrame={{
|
||||
fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js',
|
||||
location: null,
|
||||
|
||||
+259
@@ -1,5 +1,94 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LogBoxInspectorCodeFrame should dedupe if code frames are the same 1`] = `
|
||||
<LogBoxInspectorSection
|
||||
action={<AppInfo />}
|
||||
heading="Source"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(51, 51, 51, 1)",
|
||||
"borderRadius": 3,
|
||||
"marginLeft": 10,
|
||||
"marginRight": 10,
|
||||
"marginTop": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"borderBottomColor": "rgba(255, 255, 255, 0.1)",
|
||||
"borderBottomWidth": 1,
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"minWidth": "100%",
|
||||
}
|
||||
}
|
||||
horizontal={true}
|
||||
>
|
||||
<Ansi
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255, 255, 255, 1)",
|
||||
"fontFamily": "Menlo",
|
||||
"fontSize": 12,
|
||||
"includeFontPadding": false,
|
||||
"lineHeight": 20,
|
||||
}
|
||||
}
|
||||
text=" 63 | function ConsoleWithThrow() {
|
||||
64 | console.error('hit');
|
||||
> 65 | throw new Error('test');
|
||||
| ^
|
||||
66 | }
|
||||
67 |
|
||||
68 |"
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<LogBoxButton
|
||||
backgroundColor={
|
||||
Object {
|
||||
"default": "transparent",
|
||||
"pressed": "rgba(34, 34, 34, 1)",
|
||||
}
|
||||
}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"paddingBottom": 10,
|
||||
"paddingTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255, 255, 255, 0.5)",
|
||||
"flex": 1,
|
||||
"fontFamily": "Menlo",
|
||||
"fontSize": 12,
|
||||
"includeFontPadding": false,
|
||||
"lineHeight": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
CrashReactApp.js
|
||||
(65:19)
|
||||
</Text>
|
||||
</LogBoxButton>
|
||||
</View>
|
||||
</LogBoxInspectorSection>
|
||||
`;
|
||||
|
||||
exports[`LogBoxInspectorCodeFrame should render a code frame 1`] = `
|
||||
<LogBoxInspectorSection
|
||||
action={<AppInfo />}
|
||||
@@ -173,4 +262,174 @@ exports[`LogBoxInspectorCodeFrame should render a code frame without a location
|
||||
</LogBoxInspectorSection>
|
||||
`;
|
||||
|
||||
exports[`LogBoxInspectorCodeFrame should render both a code frame and a component frame 1`] = `
|
||||
<LogBoxInspectorSection
|
||||
action={<AppInfo />}
|
||||
heading="Sources"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(51, 51, 51, 1)",
|
||||
"borderRadius": 3,
|
||||
"marginLeft": 10,
|
||||
"marginRight": 10,
|
||||
"marginTop": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"borderBottomColor": "rgba(255, 255, 255, 0.1)",
|
||||
"borderBottomWidth": 1,
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"minWidth": "100%",
|
||||
}
|
||||
}
|
||||
horizontal={true}
|
||||
>
|
||||
<Ansi
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255, 255, 255, 1)",
|
||||
"fontFamily": "Menlo",
|
||||
"fontSize": 12,
|
||||
"includeFontPadding": false,
|
||||
"lineHeight": 20,
|
||||
}
|
||||
}
|
||||
text=" 62 |
|
||||
63 | function ConsoleWithThrow() {
|
||||
> 64 | console.error('hit');
|
||||
| ^
|
||||
65 | throw new Error('test');
|
||||
66 | }
|
||||
67 |"
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<LogBoxButton
|
||||
backgroundColor={
|
||||
Object {
|
||||
"default": "transparent",
|
||||
"pressed": "rgba(34, 34, 34, 1)",
|
||||
}
|
||||
}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"paddingBottom": 10,
|
||||
"paddingTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255, 255, 255, 0.5)",
|
||||
"flex": 1,
|
||||
"fontFamily": "Menlo",
|
||||
"fontSize": 12,
|
||||
"includeFontPadding": false,
|
||||
"lineHeight": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
CrashReactApp.js
|
||||
(64:17)
|
||||
</Text>
|
||||
</LogBoxButton>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(51, 51, 51, 1)",
|
||||
"borderRadius": 3,
|
||||
"marginLeft": 10,
|
||||
"marginRight": 10,
|
||||
"marginTop": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"borderBottomColor": "rgba(255, 255, 255, 0.1)",
|
||||
"borderBottomWidth": 1,
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"minWidth": "100%",
|
||||
}
|
||||
}
|
||||
horizontal={true}
|
||||
>
|
||||
<Ansi
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255, 255, 255, 1)",
|
||||
"fontFamily": "Menlo",
|
||||
"fontSize": 12,
|
||||
"includeFontPadding": false,
|
||||
"lineHeight": 20,
|
||||
}
|
||||
}
|
||||
text=" 89 |
|
||||
90 | function Child() {
|
||||
> 91 | return <ConsoleWithThrow />;
|
||||
| ^
|
||||
92 | }
|
||||
93 |
|
||||
94 |"
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<LogBoxButton
|
||||
backgroundColor={
|
||||
Object {
|
||||
"default": "transparent",
|
||||
"pressed": "rgba(34, 34, 34, 1)",
|
||||
}
|
||||
}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"paddingBottom": 10,
|
||||
"paddingTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255, 255, 255, 0.5)",
|
||||
"flex": 1,
|
||||
"fontFamily": "Menlo",
|
||||
"fontSize": 12,
|
||||
"includeFontPadding": false,
|
||||
"lineHeight": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
CrashReactApp.js
|
||||
(90:11)
|
||||
</Text>
|
||||
</LogBoxButton>
|
||||
</View>
|
||||
</LogBoxInspectorSection>
|
||||
`;
|
||||
|
||||
exports[`LogBoxInspectorCodeFrame should render null for no code frame 1`] = `null`;
|
||||
|
||||
@@ -6064,6 +6064,7 @@ declare class LogBoxLog {
|
||||
count: number;
|
||||
level: LogLevel;
|
||||
codeFrame: ?CodeFrame;
|
||||
componentCodeFrame: ?CodeFrame;
|
||||
isComponentError: boolean;
|
||||
extraData: mixed | void;
|
||||
symbolicated:
|
||||
@@ -6257,6 +6258,7 @@ exports[`public API should not change unintentionally Libraries/LogBox/UI/LogBox
|
||||
|
||||
exports[`public API should not change unintentionally Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js 1`] = `
|
||||
"type Props = $ReadOnly<{
|
||||
componentCodeFrame: ?CodeFrame,
|
||||
codeFrame: ?CodeFrame,
|
||||
}>;
|
||||
declare function LogBoxInspectorCodeFrame(props: Props): React.Node;
|
||||
|
||||
Reference in New Issue
Block a user