diff --git a/packages/shared/__tests__/describeComponentFrame-test.js b/packages/shared/__tests__/describeComponentFrame-test.js new file mode 100644 index 0000000000000..5aabebaaddc5a --- /dev/null +++ b/packages/shared/__tests__/describeComponentFrame-test.js @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +let React; +let ReactDOM; + +describe('Component stack trace displaying', () => { + beforeEach(() => { + React = require('react'); + ReactDOM = require('react-dom'); + }); + + it('should provide filenames in stack traces', () => { + class Component extends React.Component { + render() { + return [a, b]; + } + } + + spyOnDev(console, 'error'); + const container = document.createElement('div'); + const fileNames = { + '': '', + '/': '', + '\\': '', + Foo: 'Foo', + 'Bar/Foo': 'Foo', + 'Bar\\Foo': 'Foo', + 'Baz/Bar/Foo': 'Foo', + 'Baz\\Bar\\Foo': 'Foo', + + 'Foo.js': 'Foo.js', + 'Foo.jsx': 'Foo.jsx', + '/Foo.js': 'Foo.js', + '/Foo.jsx': 'Foo.jsx', + '\\Foo.js': 'Foo.js', + '\\Foo.jsx': 'Foo.jsx', + 'Bar/Foo.js': 'Foo.js', + 'Bar/Foo.jsx': 'Foo.jsx', + 'Bar\\Foo.js': 'Foo.js', + 'Bar\\Foo.jsx': 'Foo.jsx', + '/Bar/Foo.js': 'Foo.js', + '/Bar/Foo.jsx': 'Foo.jsx', + '\\Bar\\Foo.js': 'Foo.js', + '\\Bar\\Foo.jsx': 'Foo.jsx', + 'Bar/Baz/Foo.js': 'Foo.js', + 'Bar/Baz/Foo.jsx': 'Foo.jsx', + 'Bar\\Baz\\Foo.js': 'Foo.js', + 'Bar\\Baz\\Foo.jsx': 'Foo.jsx', + '/Bar/Baz/Foo.js': 'Foo.js', + '/Bar/Baz/Foo.jsx': 'Foo.jsx', + '\\Bar\\Baz\\Foo.js': 'Foo.js', + '\\Bar\\Baz\\Foo.jsx': 'Foo.jsx', + 'C:\\funny long (path)/Foo.js': 'Foo.js', + 'C:\\funny long (path)/Foo.jsx': 'Foo.jsx', + + 'index.js': 'index.js', + 'index.jsx': 'index.jsx', + '/index.js': 'index.js', + '/index.jsx': 'index.jsx', + '\\index.js': 'index.js', + '\\index.jsx': 'index.jsx', + 'Bar/index.js': 'Bar/index.js', + 'Bar/index.jsx': 'Bar/index.jsx', + 'Bar\\index.js': 'Bar/index.js', + 'Bar\\index.jsx': 'Bar/index.jsx', + '/Bar/index.js': 'Bar/index.js', + '/Bar/index.jsx': 'Bar/index.jsx', + '\\Bar\\index.js': 'Bar/index.js', + '\\Bar\\index.jsx': 'Bar/index.jsx', + 'Bar/Baz/index.js': 'Baz/index.js', + 'Bar/Baz/index.jsx': 'Baz/index.jsx', + 'Bar\\Baz\\index.js': 'Baz/index.js', + 'Bar\\Baz\\index.jsx': 'Baz/index.jsx', + '/Bar/Baz/index.js': 'Baz/index.js', + '/Bar/Baz/index.jsx': 'Baz/index.jsx', + '\\Bar\\Baz\\index.js': 'Baz/index.js', + '\\Bar\\Baz\\index.jsx': 'Baz/index.jsx', + 'C:\\funny long (path)/index.js': 'funny long (path)/index.js', + 'C:\\funny long (path)/index.jsx': 'funny long (path)/index.jsx', + }; + Object.keys(fileNames).forEach((fileName, i) => { + ReactDOM.render( + , + container, + ); + }); + if (__DEV__) { + let i = 0; + expect(console.error.calls.count()).toBe(Object.keys(fileNames).length); + for (let fileName in fileNames) { + if (!fileNames.hasOwnProperty(fileName)) { + continue; + } + const args = console.error.calls.argsFor(i); + const stack = args[args.length - 1]; + const expected = fileNames[fileName]; + expect(stack).toContain(`at ${expected}:`); + i++; + } + } + }); +}); diff --git a/packages/shared/describeComponentFrame.js b/packages/shared/describeComponentFrame.js index 5c619e6164e09..23c5a9815f7f8 100644 --- a/packages/shared/describeComponentFrame.js +++ b/packages/shared/describeComponentFrame.js @@ -7,22 +7,31 @@ * @flow */ +const BEFORE_SLASH_RE = /^(.*)[\\\/]/; + export default function( name: null | string, source: any, ownerName: null | string, ) { - return ( - '\n in ' + - (name || 'Unknown') + - (source - ? ' (at ' + - source.fileName.replace(/^.*[\\\/]/, '') + - ':' + - source.lineNumber + - ')' - : ownerName - ? ' (created by ' + ownerName + ')' - : '') - ); + let sourceInfo = ''; + if (source) { + let path = source.fileName; + let fileName = path.replace(BEFORE_SLASH_RE, ''); + if (/^index\./.test(fileName)) { + // Special case: include closest folder name for `index.*` filenames. + const match = path.match(BEFORE_SLASH_RE); + if (match) { + const pathBeforeSlash = match[1]; + if (pathBeforeSlash) { + const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, ''); + fileName = folderName + '/' + fileName; + } + } + } + sourceInfo = ' (at ' + fileName + ':' + source.lineNumber + ')'; + } else if (ownerName) { + sourceInfo = ' (created by ' + ownerName + ')'; + } + return '\n in ' + (name || 'Unknown') + sourceInfo; }