diff --git a/Libraries/LogBox/Data/__tests__/LogBoxLog-test.js b/Libraries/LogBox/Data/__tests__/LogBoxLog-test.js index 0226dc37c55..c08d16786d1 100644 --- a/Libraries/LogBox/Data/__tests__/LogBoxLog-test.js +++ b/Libraries/LogBox/Data/__tests__/LogBoxLog-test.js @@ -25,7 +25,13 @@ function getLogBoxLog() { message: {content: '...', substitutions: []}, stack: createStack(['A', 'B', 'C']), category: 'Message category...', - componentStack: [{component: 'LogBoxLog', location: 'LogBoxLog.js:1'}], + componentStack: [ + { + content: 'LogBoxLog', + fileName: 'LogBoxLog.js', + location: {column: -1, row: 1}, + }, + ], codeFrame: { fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', location: {row: 199, column: 0}, @@ -69,7 +75,11 @@ describe('LogBoxLog', () => { expect(log.stack).toEqual(createStack(['A', 'B', 'C'])); expect(log.category).toEqual('Message category...'); expect(log.componentStack).toEqual([ - {component: 'LogBoxLog', location: 'LogBoxLog.js:1'}, + { + content: 'LogBoxLog', + fileName: 'LogBoxLog.js', + location: {column: -1, row: 1}, + }, ]); expect(log.codeFrame).toEqual({ fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', diff --git a/Libraries/LogBox/Data/__tests__/parseLogBoxLog-test.js b/Libraries/LogBox/Data/__tests__/parseLogBoxLog-test.js index cc114c222b2..f4025dcc176 100644 --- a/Libraries/LogBox/Data/__tests__/parseLogBoxLog-test.js +++ b/Libraries/LogBox/Data/__tests__/parseLogBoxLog-test.js @@ -117,8 +117,16 @@ describe('parseLogBoxLog', () => { ]), ).toEqual({ componentStack: [ - {component: 'MyComponent', location: 'filename.js:1'}, - {component: 'MyOtherComponent', location: 'filename2.js:1'}, + { + content: 'MyComponent', + fileName: 'filename.js', + location: {column: -1, row: 1}, + }, + { + content: 'MyOtherComponent', + fileName: 'filename2.js', + location: {column: -1, row: 1}, + }, ], category: 'Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?%s', @@ -143,8 +151,16 @@ describe('parseLogBoxLog', () => { ]), ).toEqual({ componentStack: [ - {component: 'MyComponent', location: 'filename.js:1'}, - {component: 'MyOtherComponent', location: 'filename2.js:1'}, + { + content: 'MyComponent', + fileName: 'filename.js', + location: {column: -1, row: 1}, + }, + { + content: 'MyOtherComponent', + fileName: 'filename2.js', + location: {column: -1, row: 1}, + }, ], category: 'Some kind of message', message: { @@ -164,8 +180,16 @@ describe('parseLogBoxLog', () => { ]), ).toEqual({ componentStack: [ - {component: 'MyComponent', location: 'filename.js:1'}, - {component: 'MyOtherComponent', location: 'filename2.js:1'}, + { + content: 'MyComponent', + fileName: 'filename.js', + location: {column: -1, row: 1}, + }, + { + content: 'MyOtherComponent', + fileName: 'filename2.js', + location: {column: -1, row: 1}, + }, ], category: 'Some kind of message Some other kind of message Some third kind of message', @@ -418,12 +442,14 @@ Please follow the instructions at: fburl.com/rn-remote-assets`, }, componentStack: [ { - component: 'MyComponent', - location: 'filename.js:1', + content: 'MyComponent', + fileName: 'filename.js', + location: {column: -1, row: 1}, }, { - component: 'MyOtherComponent', - location: 'filename2.js:1', + content: 'MyOtherComponent', + fileName: 'filename2.js', + location: {column: -1, row: 1}, }, ], stack: [ diff --git a/Libraries/LogBox/Data/parseLogBoxLog.js b/Libraries/LogBox/Data/parseLogBoxLog.js index dd15f4e356d..a65bd5d531e 100644 --- a/Libraries/LogBox/Data/parseLogBoxLog.js +++ b/Libraries/LogBox/Data/parseLogBoxLog.js @@ -42,12 +42,7 @@ export type Message = $ReadOnly<{| >, |}>; -export type ComponentStack = $ReadOnlyArray< - $ReadOnly<{| - component: string, - location: string, - |}>, ->; +export type ComponentStack = $ReadOnlyArray; const SUBSTITUTION = UTFSequence.BOM + '%s'; @@ -127,6 +122,7 @@ export function parseCategory( }, }; } + export function parseComponentStack(message: string): ComponentStack { return message .split(/\n {4}in /g) @@ -134,11 +130,17 @@ export function parseComponentStack(message: string): ComponentStack { if (!s) { return null; } - let [component, location] = s.split(/ \(at /); - if (!location) { - [component, location] = s.split(/ \(/); + const match = s.match(/(.*) \(at (.*\.js):([\d]+)\)/); + if (!match) { + return null; } - return {component, location: location && location.replace(')', '')}; + + let [content, fileName, row] = match.slice(1); + return { + content, + fileName, + location: {column: -1, row: parseInt(row, 10)}, + }; }) .filter(Boolean); } diff --git a/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js b/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js index d03c663791e..45f9574c554 100644 --- a/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js +++ b/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js @@ -18,12 +18,37 @@ import View from '../../Components/View/View'; import LogBoxButton from './LogBoxButton'; import * as LogBoxStyle from './LogBoxStyle'; import LogBoxInspectorSection from './LogBoxInspectorSection'; +import openFileInEditor from '../../Core/Devtools/openFileInEditor'; import type LogBoxLog from '../Data/LogBoxLog'; type Props = $ReadOnly<{| log: LogBoxLog, |}>; +const BEFORE_SLASH_RE = /^(.*)[\\/]/; + +// Taken from React https://github.com/facebook/react/blob/206d61f72214e8ae5b935f0bf8628491cb7f0797/packages/react-devtools-shared/src/backend/describeComponentFrame.js#L27-L41 +function getPrettyFileName(path) { + let fileName = path.replace(BEFORE_SLASH_RE, ''); + + // In DEV, include code for a common special case: + // prefer "folder/index.js" instead of just "index.js". + if (/^index\./.test(fileName)) { + const match = path.match(BEFORE_SLASH_RE); + if (match) { + const pathBeforeSlash = match[1]; + if (pathBeforeSlash) { + const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, ''); + // Note the below string contains a zero width space after the "/" character. + // This is to prevent browsers like Chrome from formatting the file name as a link. + // (Since this is a source link, it would not work to open the source file anyway.) + fileName = folderName + '/​' + fileName; + } + } + } + + return fileName; +} function LogBoxInspectorReactFrames(props: Props): React.Node { const [collapsed, setCollapsed] = React.useState(true); if (props.log.componentStack == null || props.log.componentStack.length < 1) { @@ -39,6 +64,10 @@ function LogBoxInspectorReactFrames(props: Props): React.Node { } function getCollapseMessage() { + if (props.log.componentStack.length <= 3) { + return; + } + const count = props.log.componentStack.length - 3; if (collapsed) { return `See ${count} more components`; @@ -53,15 +82,34 @@ function LogBoxInspectorReactFrames(props: Props): React.Node { - - - {'<'} - {frame.component} - {' />'} + style={componentStyles.frameContainer}> + + openFileInEditor(frame.fileName, frame.location?.row ?? 1) + : null + } + style={componentStyles.frame}> + + + {'<'} + {frame.content} + {' />'} + + + + {getPrettyFileName(frame.fileName)} + {frame.location ? `:${frame.location.row}` : ''} - - {frame.location} + ))} @@ -96,9 +144,15 @@ const componentStyles = StyleSheet.create({ paddingVertical: 5, paddingHorizontal: 10, }, + frameContainer: { + flexDirection: 'row', + paddingHorizontal: 15, + }, frame: { - paddingHorizontal: 25, + flex: 1, paddingVertical: 4, + paddingHorizontal: 10, + borderRadius: 5, }, component: { flexDirection: 'row', diff --git a/Libraries/LogBox/UI/__tests__/LogBoxInspectorReactFrames-test.js b/Libraries/LogBox/UI/__tests__/LogBoxInspectorReactFrames-test.js index ebd82f63460..117fbafde0e 100644 --- a/Libraries/LogBox/UI/__tests__/LogBoxInspectorReactFrames-test.js +++ b/Libraries/LogBox/UI/__tests__/LogBoxInspectorReactFrames-test.js @@ -40,7 +40,7 @@ describe('LogBoxInspectorReactFrames', () => { expect(output).toMatchSnapshot(); }); - it('should render componentStack frames', () => { + it('should render componentStack frames without full path pressable', () => { const output = render.shallowRender( { category: 'Some kind of message', componentStack: [ { - component: 'MyComponent', - location: 'MyComponentFile.js:1', + content: 'MyComponent', + fileName: 'MyComponentFile.js', + location: { + row: 1, + column: -1, + }, + }, + ], + }) + } + />, + ); + + expect(output).toMatchSnapshot(); + }); + + it('should render componentStack frames with full path pressable', () => { + const output = render.shallowRender( + , + ); + + expect(output).toMatchSnapshot(); + }); + + it('should render componentStack frames with parent folder of index.js', () => { + const output = render.shallowRender( + , + ); + + expect(output).toMatchSnapshot(); + }); + + it('should render componentStack frames with more than 3 stacks', () => { + const output = render.shallowRender( + - - - < - - MyComponent - - /> + > + < + + MyComponent + + /> + + + + MyComponentFile.js + :1 - - + + + - MyComponentFile.js:1 - + + + + +`; + +exports[`LogBoxInspectorReactFrames should render componentStack frames with more than 3 stacks 1`] = ` + + + + + + + < + + MyComponent + + /> + + + + + to/​index.js + :1 + + + + + + + + + < + + MyComponent2 + + /> + + + + + index2.js + :1 + + + + + + + + + < + + MyComponent3 + + /> + + + + + index3.js + :1 + + - See -2 more components + See 1 more components `; +exports[`LogBoxInspectorReactFrames should render componentStack frames with parent folder of index.js 1`] = ` + + + + + + + < + + MyComponent + + /> + + + + + to/​index.js + :1 + + + + + + + + + +`; + +exports[`LogBoxInspectorReactFrames should render componentStack frames without full path pressable 1`] = ` + + + + + + + < + + MyComponent + + /> + + + + + MyComponentFile.js + :1 + + + + + + + + + +`; + exports[`LogBoxInspectorReactFrames should render null for no componentStack frames 1`] = `null`;