diff --git a/agent/Bridge.js b/agent/Bridge.js index db41f48288..9c21b0ef8e 100644 --- a/agent/Bridge.js +++ b/agent/Bridge.js @@ -403,27 +403,37 @@ class Bridge { var protoclean = []; if (inspectable) { var val = getIn(inspectable, path); + var protod = false; var isFn = typeof val === 'function'; - Object.getOwnPropertyNames(val).forEach(name => { - if (name === '__proto__') { + + // Extract inner state of the third-party frameworks data objects... + var source = ( val && val.__inner_state__ ) || val; + + Object.getOwnPropertyNames( source ).forEach(name => { + if (name === '__proto__' ) { protod = true; } + if (isFn && (name === 'arguments' || name === 'callee' || name === 'caller')) { return; } - result[name] = dehydrate(val[name], cleaned, [name]); + result[name] = dehydrate( source[name], cleaned, [name]); }); /* eslint-disable no-proto */ - if (!protod && val.__proto__ && val.constructor.name !== 'Object') { + if (!protod && val.__proto__ && val.constructor.name !== 'Object' ) { var newProto = {}; var pIsFn = typeof val.__proto__ === 'function'; Object.getOwnPropertyNames(val.__proto__).forEach(name => { - if (pIsFn && (name === 'arguments' || name === 'callee' || name === 'caller')) { + if ( name === '__inner_state__' || ( pIsFn && (name === 'arguments' || name === 'callee' || name === 'caller') ) ) { return; } - newProto[name] = dehydrate(val.__proto__[name], protoclean, [name]); + + // Calculated properties should not be evaluated on prototype. + var prop = Object.getOwnPropertyDescriptor( val.__proto__, name ); + + newProto[name] = dehydrate( prop.get ? prop.get : val.__proto__[name], protoclean, [name]); }); proto = newProto; } @@ -439,8 +449,27 @@ class Bridge { } function getIn(base, path) { + let isPrototypeChain = false; + return path.reduce((obj, attr) => { - return obj ? obj[attr] : null; + if (!obj) { + return null; + } + + // Mark the beginning of the prototype chain... + if (attr === '__proto__') { + isPrototypeChain = true; + return obj[attr]; + } + + if (isPrototypeChain) { + // Avoid calling calculated properties on prototype. + const property = Object.getOwnPropertyDescriptor( obj, attr ); + return property.get || property.value; + } + + // Traverse inner state of the third-party data frameworks objects... + return ( obj.__inner_state__ || obj )[attr]; }, base); } diff --git a/agent/dehydrate.js b/agent/dehydrate.js index 0fddfac5a1..b68e784207 100644 --- a/agent/dehydrate.js +++ b/agent/dehydrate.js @@ -31,6 +31,11 @@ * and cleaned = [["some", "attr"], ["other"]] */ function dehydrate(data: Object, cleaned: Array>, path?: Array, level?: number): string | Object { + // Support third-party frameworks data objects in react component state. + if (data && data.__inner_state__ && path && path[path.length - 1] === 'state') { + data = data.__inner_state__; + } + level = level || 0; path = path || []; if (typeof data === 'function') { diff --git a/backend/backend.js b/backend/backend.js index ff64a5ccd9..92f555f692 100644 --- a/backend/backend.js +++ b/backend/backend.js @@ -24,10 +24,13 @@ 'use strict'; import type {Hook} from './types'; +import attachInnerStateInspectors from './innerStateInspectors'; var attachRenderer = require('./attachRenderer'); module.exports = function setupBackend(hook: Hook): boolean { + attachInnerStateInspectors( hook ); + var oldReact = window.React && window.React.__internals; if (oldReact && Object.keys(hook._renderers).length === 0) { hook.inject(oldReact); diff --git a/backend/innerStateInspectors.js b/backend/innerStateInspectors.js new file mode 100644 index 0000000000..cfbfc1ce34 --- /dev/null +++ b/backend/innerStateInspectors.js @@ -0,0 +1,18 @@ +/** + * @flow + * + * Inner state inspectors for the built in JS data types. + */ + +'use strict'; + +import type {Hook} from './types'; + +export default +function attachInnerStateInspectors({ addInnerStateInspector } : Hook ) { + addInnerStateInspector( Date, ( x : Date ) => ({ + local : `${ x.toDateString() } ${ x.toTimeString() }`, + iso : x.toISOString(), + timestamp : x.getTime(), + }), true ); +} diff --git a/backend/installGlobalHook.js b/backend/installGlobalHook.js index a9f0192178..fd37130f13 100644 --- a/backend/installGlobalHook.js +++ b/backend/installGlobalHook.js @@ -57,6 +57,17 @@ function installGlobalHook(window: Object) { this._listeners[evt].map(fn => fn(data)); } }, + addInnerStateInspector: function(Ctor, getInnerState, skipIfDefined) { + if ( !( skipIfDefined && Ctor.prototype.hasOwnProperty( '__inner_state__' ) ) ) { + Object.defineProperty(Ctor.prototype, '__inner_state__', ({ + get: function() { + return getInnerState(this); + }, + enumerable: false, + configurable: true, + } : Object )); + } + }, }: Hook), }); } diff --git a/backend/types.js b/backend/types.js index 6d0dd5f733..cb497db6bd 100644 --- a/backend/types.js +++ b/backend/types.js @@ -80,6 +80,7 @@ export type Helpers = { }; export type Handler = (data: any) => void; +export type InnerStateInspector = (data: any) => any; export type Hook = { _renderers: {[key: string]: ReactRenderer}, @@ -91,4 +92,5 @@ export type Hook = { on: (evt: string, handler: Handler) => void, off: (evt: string, handler: Handler) => void, reactDevtoolsAgent?: ?Object, + addInnerStateInspector: ( Ctor : Function, handler : InnerStateInspector, skipIfDefined? : boolean ) => void; };