diff --git a/Libraries/AppRegistry/AppRegistry.js b/Libraries/AppRegistry/AppRegistry.js index 035c69c49..b76afb379 100644 --- a/Libraries/AppRegistry/AppRegistry.js +++ b/Libraries/AppRegistry/AppRegistry.js @@ -90,6 +90,7 @@ var AppRegistry = { console.log(msg); BugReporting.init(); BugReporting.addSource('AppRegistry.runApplication' + runCount++, () => msg); + BugReporting.addFileSource('react_hierarchy.txt', () => require('dumpReactTree')()); invariant( runnables[appKey] && runnables[appKey].run, 'Application ' + appKey + ' has not been registered. This ' + diff --git a/Libraries/BugReporting/dumpReactTree.js b/Libraries/BugReporting/dumpReactTree.js new file mode 100644 index 000000000..4fc369083 --- /dev/null +++ b/Libraries/BugReporting/dumpReactTree.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule dumpReactTree + * @flow + */ +'use strict'; + +const ReactNativeMount = require('ReactNativeMount'); +const getReactData = require('getReactData'); + +const INDENTATION_SIZE = 2; +const MAX_DEPTH = 2; +const MAX_STRING_LENGTH = 50; + +/** + * Dump all React Native root views and their content. This function tries + * it best to get the content but ultimately relies on implementation details + * of React and will fail in future versions. + */ +function dumpReactTree() { + try { + return getReactTree(); + } catch (e) { + return 'Failed to dump react tree: ' + e; + } +} + +function getReactTree() { + let output = ''; + const rootIds = Object.getOwnPropertyNames(ReactNativeMount._instancesByContainerID); + for (const rootId of rootIds) { + const instance = ReactNativeMount._instancesByContainerID[rootId]; + output += `============ Root ID: ${rootId} ============\n`; + output += dumpNode(instance, 0); + output += `============ End root ID: ${rootId} ============\n`; + } + return output; +} + +function dumpNode(node: Object, identation: number) { + const data = getReactData(node); + if (data.nodeType == 'Text') { + return indent(identation) + data.text + '\n'; + } else if (data.nodeType == 'Empty') { + return ''; + } + let output = indent(identation) + `<${data.name}`; + if (data.nodeType == 'Composite') { + for (const propName of Object.getOwnPropertyNames(data.props || {})) { + if (isNormalProp(propName)) { + const value = convertValue(data.props[propName]); + if (value) { + output += ` ${propName}=${value}`; + } + } + } + } + let childOutput = ''; + for (const child of data.children || []) { + childOutput += dumpNode(child, identation + 1); + } + + if (childOutput) { + output += '>\n' + childOutput + indent(identation) + `\n`;; + } else { + output += ' />\n'; + } + + return output; +} + +function isNormalProp(name: string): boolean { + switch (name) { + case 'children': + case 'key': + case 'ref': + return false; + default: + return true; + } +} + +function convertObject(object: Object, depth: number) { + if (depth >= MAX_DEPTH) { + return '[...omitted]'; + } + let output = '{' + let first = true; + for (const key of Object.getOwnPropertyNames(object)) { + if (!first) { + output += ', ' + } + output += `${key}: ${convertValue(object[key], depth + 1)}`; + first = false; + } + return output + '}'; +} + +function convertValue(value, depth = 0): ?string { + if (!value) { + return null; + } + + switch (typeof value) { + case 'string': + return JSON.stringify(possiblyEllipsis(value).replace('\n', '\\n')); + case 'boolean': + case 'number': + return JSON.stringify(value); + case 'function': + return '[function]'; + case 'object': + return convertObject(value, depth); + default: + return null; + } +} + +function possiblyEllipsis(value: string) { + if (value.length > MAX_STRING_LENGTH) { + return value.slice(0, MAX_STRING_LENGTH) + '...'; + } else { + return value; + } +} + +function indent(size: number) { + return ' '.repeat(size * INDENTATION_SIZE); +} + +module.exports = dumpReactTree; diff --git a/Libraries/BugReporting/getReactData.js b/Libraries/BugReporting/getReactData.js new file mode 100644 index 000000000..18f4b0888 --- /dev/null +++ b/Libraries/BugReporting/getReactData.js @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule getReactData + * @flow + */ +'use strict'; + +/** + * Convert a react internal instance to a sanitized data object. + * + * This is shamelessly stolen from react-devtools: + * https://github.com/facebook/react-devtools/blob/master/backend/getData.js + */ +function getData(element: Object): Object { + var children = null; + var props = null; + var state = null; + var context = null; + var updater = null; + var name = null; + var type = null; + var text = null; + var publicInstance = null; + var nodeType = 'Native'; + // If the parent is a native node without rendered children, but with + // multiple string children, then the `element` that gets passed in here is + // a plain value -- a string or number. + if (typeof element !== 'object') { + nodeType = 'Text'; + text = element + ''; + } else if (element._currentElement === null || element._currentElement === false) { + nodeType = 'Empty'; + } else if (element._renderedComponent) { + nodeType = 'NativeWrapper'; + children = [element._renderedComponent]; + props = element._instance.props; + state = element._instance.state; + context = element._instance.context; + if (context && Object.keys(context).length === 0) { + context = null; + } + } else if (element._renderedChildren) { + children = childrenList(element._renderedChildren); + } else if (element._currentElement && element._currentElement.props) { + // This is a native node without rendered children -- meaning the children + // prop is just a string or (in the case of the