Add support for dumping current React hierarchy
Reviewed By: sebmarkbage Differential Revision: D3287391 fbshipit-source-id: 0ef1cd25a910dcc0a50ed52a2cc0ae702a9654f4
This commit is contained in:
parent
610cfdc819
commit
f13322c820
|
@ -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 ' +
|
||||
|
|
|
@ -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) + `</${data.name}>\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;
|
|
@ -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 <option>) a list of
|
||||
// strings & numbers.
|
||||
children = element._currentElement.props.children;
|
||||
}
|
||||
|
||||
if (!props && element._currentElement && element._currentElement.props) {
|
||||
props = element._currentElement.props;
|
||||
}
|
||||
|
||||
// != used deliberately here to catch undefined and null
|
||||
if (element._currentElement != null) {
|
||||
type = element._currentElement.type;
|
||||
if (typeof type === 'string') {
|
||||
name = type;
|
||||
} else if (element.getName) {
|
||||
nodeType = 'Composite';
|
||||
name = element.getName();
|
||||
// 0.14 top-level wrapper
|
||||
// TODO(jared): The backend should just act as if these don't exist.
|
||||
if (element._renderedComponent && element._currentElement.props === element._renderedComponent._currentElement) {
|
||||
nodeType = 'Wrapper';
|
||||
}
|
||||
if (name === null) {
|
||||
name = 'No display name';
|
||||
}
|
||||
} else if (element._stringText) {
|
||||
nodeType = 'Text';
|
||||
text = element._stringText;
|
||||
} else {
|
||||
name = type.displayName || type.name || 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
if (element._instance) {
|
||||
var inst = element._instance;
|
||||
updater = {
|
||||
setState: inst.setState && inst.setState.bind(inst),
|
||||
forceUpdate: inst.forceUpdate && inst.forceUpdate.bind(inst),
|
||||
setInProps: inst.forceUpdate && setInProps.bind(null, element),
|
||||
setInState: inst.forceUpdate && setInState.bind(null, inst),
|
||||
setInContext: inst.forceUpdate && setInContext.bind(null, inst),
|
||||
};
|
||||
publicInstance = inst;
|
||||
|
||||
// TODO: React ART currently falls in this bucket, but this doesn't
|
||||
// actually make sense and we should clean this up after stabilizing our
|
||||
// API for backends
|
||||
if (inst._renderedChildren) {
|
||||
children = childrenList(inst._renderedChildren);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodeType,
|
||||
type,
|
||||
name,
|
||||
props,
|
||||
state,
|
||||
context,
|
||||
children,
|
||||
text,
|
||||
updater,
|
||||
publicInstance,
|
||||
};
|
||||
}
|
||||
|
||||
function setInProps(internalInst, path: Array<string | number>, value: any) {
|
||||
var element = internalInst._currentElement;
|
||||
internalInst._currentElement = {
|
||||
...element,
|
||||
props: copyWithSet(element.props, path, value),
|
||||
};
|
||||
internalInst._instance.forceUpdate();
|
||||
}
|
||||
|
||||
function setInState(inst, path: Array<string | number>, value: any) {
|
||||
setIn(inst.state, path, value);
|
||||
inst.forceUpdate();
|
||||
}
|
||||
|
||||
function setInContext(inst, path: Array<string | number>, value: any) {
|
||||
setIn(inst.context, path, value);
|
||||
inst.forceUpdate();
|
||||
}
|
||||
|
||||
function setIn(obj: Object, path: Array<string | number>, value: any) {
|
||||
var last = path.pop();
|
||||
var parent = path.reduce((obj_, attr) => obj_ ? obj_[attr] : null, obj);
|
||||
if (parent) {
|
||||
parent[last] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function childrenList(children) {
|
||||
var res = [];
|
||||
for (var name in children) {
|
||||
res.push(children[name]);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function copyWithSetImpl(obj, path, idx, value) {
|
||||
if (idx >= path.length) {
|
||||
return value;
|
||||
}
|
||||
var key = path[idx];
|
||||
var updated = Array.isArray(obj) ? obj.slice() : {...obj};
|
||||
// $FlowFixMe number or string is fine here
|
||||
updated[key] = copyWithSetImpl(obj[key], path, idx + 1, value);
|
||||
return updated;
|
||||
}
|
||||
|
||||
function copyWithSet(obj: Object | Array<any>, path: Array<string | number>, value: any): Object | Array<any> {
|
||||
return copyWithSetImpl(obj, path, 0, value);
|
||||
}
|
||||
|
||||
module.exports = getData;
|
Loading…
Reference in New Issue