diff --git a/.gitignore b/.gitignore index 85e420fd9f..b3eab07625 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,4 @@ yarn-error.log .vscode .idea .watchmanconfig -key.pem - +key.pem \ No newline at end of file diff --git a/fixtures/regression/14.9.html b/fixtures/regression/14.9.html new file mode 100644 index 0000000000..e92a308301 --- /dev/null +++ b/fixtures/regression/14.9.html @@ -0,0 +1,39 @@ + + + + + React 14.9 + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/14.9.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/15.0.html b/fixtures/regression/15.0.html new file mode 100644 index 0000000000..b3f05333c2 --- /dev/null +++ b/fixtures/regression/15.0.html @@ -0,0 +1,39 @@ + + + + + React 15.0 + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/15.0.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/15.1.html b/fixtures/regression/15.1.html new file mode 100644 index 0000000000..8712b56a9e --- /dev/null +++ b/fixtures/regression/15.1.html @@ -0,0 +1,39 @@ + + + + + React 15.1 + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/15.1.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/15.2.html b/fixtures/regression/15.2.html new file mode 100644 index 0000000000..acde32fe21 --- /dev/null +++ b/fixtures/regression/15.2.html @@ -0,0 +1,39 @@ + + + + + React 15.2 + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/15.2.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/15.3.html b/fixtures/regression/15.3.html new file mode 100644 index 0000000000..ced3559ea6 --- /dev/null +++ b/fixtures/regression/15.3.html @@ -0,0 +1,39 @@ + + + + + React 15.3 + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/15.3.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/15.4.html b/fixtures/regression/15.4.html new file mode 100644 index 0000000000..0f23cdea7d --- /dev/null +++ b/fixtures/regression/15.4.html @@ -0,0 +1,39 @@ + + + + + React 15.4 + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/15.4.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/15.5.html b/fixtures/regression/15.5.html new file mode 100644 index 0000000000..361512d9bd --- /dev/null +++ b/fixtures/regression/15.5.html @@ -0,0 +1,39 @@ + + + + + React 15.5 + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/15.5.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/15.6.html b/fixtures/regression/15.6.html new file mode 100644 index 0000000000..97ff99ccf5 --- /dev/null +++ b/fixtures/regression/15.6.html @@ -0,0 +1,39 @@ + + + + + React 15.6 + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/15.6.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/16.0.html b/fixtures/regression/16.0.html new file mode 100644 index 0000000000..ea5cf812c9 --- /dev/null +++ b/fixtures/regression/16.0.html @@ -0,0 +1,39 @@ + + + + + React 16.0 + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/16.0.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/16.1.html b/fixtures/regression/16.1.html new file mode 100644 index 0000000000..494cc97900 --- /dev/null +++ b/fixtures/regression/16.1.html @@ -0,0 +1,39 @@ + + + + + React 16.1 + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/16.1.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/16.2.html b/fixtures/regression/16.2.html new file mode 100644 index 0000000000..b1c3186720 --- /dev/null +++ b/fixtures/regression/16.2.html @@ -0,0 +1,39 @@ + + + + + React 16.2 + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/16.2.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/16.3.html b/fixtures/regression/16.3.html new file mode 100644 index 0000000000..76799903d8 --- /dev/null +++ b/fixtures/regression/16.3.html @@ -0,0 +1,39 @@ + + + + + React 16.3 + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/16.3.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/16.4.html b/fixtures/regression/16.4.html new file mode 100644 index 0000000000..ca6c30dec1 --- /dev/null +++ b/fixtures/regression/16.4.html @@ -0,0 +1,39 @@ + + + + + React 16.4 + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/16.4.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/16.5.html b/fixtures/regression/16.5.html new file mode 100644 index 0000000000..48e14a457c --- /dev/null +++ b/fixtures/regression/16.5.html @@ -0,0 +1,41 @@ + + + + + React 16.5 + + + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/16.5.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/16.6.html b/fixtures/regression/16.6.html new file mode 100644 index 0000000000..ca32dc5b9c --- /dev/null +++ b/fixtures/regression/16.6.html @@ -0,0 +1,42 @@ + + + + + React 16.6 + + + + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/16.6.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/16.7.html b/fixtures/regression/16.7.html new file mode 100644 index 0000000000..2b7ec759fa --- /dev/null +++ b/fixtures/regression/16.7.html @@ -0,0 +1,42 @@ + + + + + React 16.7 + + + + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/16.7.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/canary.html b/fixtures/regression/canary.html new file mode 100644 index 0000000000..cd25e6162a --- /dev/null +++ b/fixtures/regression/canary.html @@ -0,0 +1,42 @@ + + + + + React canary + + + + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/canary.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/index.html b/fixtures/regression/index.html new file mode 100644 index 0000000000..125a8e8baf --- /dev/null +++ b/fixtures/regression/index.html @@ -0,0 +1,28 @@ + + + + + React DevTools regression test + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/regression/next.html b/fixtures/regression/next.html new file mode 100644 index 0000000000..c457f03acc --- /dev/null +++ b/fixtures/regression/next.html @@ -0,0 +1,42 @@ + + + + + React next + + + + + + + + + + + + + + +
+ If you are seeing this message, you are likely viewing this file using the file protocol which does not support cross origin requests. +

+ Use a web server like serve instead: +

+ npm install -g pushstate-server
+ pushstate-server .
+ open http://localhost:9000/fixtures/regression/next.html +
+ + + + + + \ No newline at end of file diff --git a/fixtures/regression/shared.js b/fixtures/regression/shared.js new file mode 100644 index 0000000000..362e02e911 --- /dev/null +++ b/fixtures/regression/shared.js @@ -0,0 +1,330 @@ +/* eslint-disable no-fallthrough, react/react-in-jsx-scope, react/jsx-no-undef */ +/* global React ReactCache ReactDOM SchedulerTracing ScheduleTracing */ + +const apps = []; + +const pieces = React.version.split('.'); +const major = + pieces[0] === '0' ? parseInt(pieces[1], 10) : parseInt(pieces[0], 10); +const minor = + pieces[0] === '0' ? parseInt(pieces[2], 10) : parseInt(pieces[1], 10); + +// Convenience wrapper to organize API features in DevTools. +function Feature({ children, label, version }) { + return ( +
+
+ {label} + {version} +
+ {children} +
+ ); +} + +// Simplify interaction tracing for tests below. +let trace = null; +if (typeof SchedulerTracing !== 'undefined') { + trace = SchedulerTracing.unstable_trace; +} else if (typeof ScheduleTracing !== 'undefined') { + trace = ScheduleTracing.unstable_trace; +} else { + trace = (_, __, callback) => callback(); +} + +// https://github.com/facebook/react/blob/master/CHANGELOG.md +switch (major) { + case 16: + switch (minor) { + case 7: + if (typeof React.useState === 'function') { + // Hooks + function Hooks() { + const [count, setCount] = React.useState(0); + const incrementCount = React.useCallback( + () => setCount(count + 1), + [count] + ); + return ( +
+ count: {count}{' '} + +
+ ); + } + apps.push( + + + + ); + } + case 6: + // memo + function LabelComponent({ label }) { + return ; + } + const AnonymousMemoized = React.memo(({ label }) => ( + + )); + const Memoized = React.memo(LabelComponent); + const CustomMemoized = React.memo(LabelComponent); + CustomMemoized.displayName = 'MemoizedLabelFunction'; + apps.push( + + + + + + ); + + // Suspense + const loadResource = ([text, ms]) => { + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve(text); + }, ms); + }); + }; + const getResourceKey = ([text, ms]) => text; + const Resource = ReactCache.unstable_createResource( + loadResource, + getResourceKey + ); + class Suspending extends React.Component { + state = { useSuspense: false }; + useSuspense = () => this.setState({ useSuspense: true }); + render() { + if (this.state.useSuspense) { + const text = Resource.read(['loaded', 2000]); + return text; + } else { + return ; + } + } + } + apps.push( + + loading...}> + + + + ); + + // lazy + const LazyWithDefaultProps = React.lazy( + () => + new Promise(resolve => { + function FooWithDefaultProps(props) { + return ( +

+ {props.greeting}, {props.name} +

+ ); + } + FooWithDefaultProps.defaultProps = { + name: 'World', + greeting: 'Bonjour', + }; + resolve({ + default: FooWithDefaultProps, + }); + }) + ); + apps.push( + + loading...}> + + + + ); + case 5: + case 4: + // unstable_Profiler + class ProfilerChild extends React.Component { + state = { count: 0 }; + incrementCount = () => + this.setState(prevState => ({ count: prevState.count + 1 })); + render() { + return ( +
+ count: {this.state.count}{' '} + +
+ ); + } + } + const onRender = (...args) => console.log('onRender()', ...args); + const Profiler = React.unstable_Profiler || React.Profiler; + apps.push( + + +
+ +
+
+
+ ); + case 3: + // createContext() + const LocaleContext = React.createContext(); + LocaleContext.displayName = 'LocaleContext'; + const ThemeContext = React.createContext(); + apps.push( + + + + {theme =>
theme: {theme}
} +
+
+ + + {locale =>
locale: {locale}
} +
+
+
+ ); + + // forwardRef() + const AnonymousFunction = React.forwardRef((props, ref) => ( +
{props.children}
+ )); + const NamedFunction = React.forwardRef(function named(props, ref) { + return
{props.children}
; + }); + const CustomName = React.forwardRef((props, ref) => ( +
{props.children}
+ )); + CustomName.displayName = 'CustomNameForwardRef'; + apps.push( + + AnonymousFunction + NamedFunction + CustomName + + ); + + // StrictMode + class StrictModeChild extends React.Component { + render() { + return 'StrictModeChild'; + } + } + apps.push( + + + + + + ); + + // unstable_AsyncMode (later renamed to unstable_ConcurrentMode, then ConcurrentMode) + const ConcurrentMode = + React.ConcurrentMode || + React.unstable_ConcurrentMode || + React.unstable_AsyncMode; + apps.push( + + +
+ unstable_AsyncMode was added in 16.3, renamed to + unstable_ConcurrentMode in 16.5, and then renamed to + ConcurrentMode in 16.7 +
+
+
+ ); + case 2: + // Fragment + apps.push( + + +
one
+
two
+
+
+ ); + case 1: + case 0: + default: + break; + } + break; + case 15: + break; + case 14: + break; + default: + break; +} + +function Even() { + return (even); +} + +// Simple stateful app shared by all React versions +class SimpleApp extends React.Component { + state = { count: 0 }; + incrementCount = () => { + const updaterFn = prevState => ({ count: prevState.count + 1 }); + trace('Updating count', performance.now(), () => this.setState(updaterFn)); + }; + render() { + const { count } = this.state; + return ( +
+ {count % 2 === 0 ? ( + + count: {count} + + ) : ( + count: {count} + )}{' '} + +
+ ); + } +} +apps.push( + + + +); + +// This component, with the version prop, helps organize DevTools at a glance. +function TopLevelWrapperForDevTools({ version }) { + let header =

React {version}

; + if (version.includes('canary')) { + const commitSha = version.match(/.+canary-(.+)/)[1]; + header = ( +

+ React canary{' '} + + {commitSha} + +

+ ); + } else if (version.includes('alpha')) { + header =

React next

; + } + + return ( +
+ {header} + {apps} +
+ ); +} +TopLevelWrapperForDevTools.displayName = 'React'; + +ReactDOM.render( + , + document.getElementById('root') +); diff --git a/fixtures/regression/styles.css b/fixtures/regression/styles.css new file mode 100644 index 0000000000..6cbaaa5c01 --- /dev/null +++ b/fixtures/regression/styles.css @@ -0,0 +1,37 @@ +body { + font-family: sans-serif; + font-size: 12px; +} + +h1 { + margin: 0; + font-size: 20px; +} + +h2 { + margin: 1rem 0 0; +} + +iframe { + border: 1px solid #ddd; + border-radius: 0.5rem; +} + +code { + white-space: nowrap; +} + +.Feature { + margin: 1rem 0; + border-bottom: 1px solid #eee; + padding-bottom: 1rem; +} +.FeatureHeader { + font-size: 16px; + margin-bottom: 0.5rem; +} +.FeatureCode { + background-color: #eee; + padding: 0.25rem; + border-radius: 0.25rem; +} diff --git a/src/backend/agent.js b/src/backend/agent.js index f1f7532e1f..5ca5696646 100644 --- a/src/backend/agent.js +++ b/src/backend/agent.js @@ -113,7 +113,7 @@ export default class Agent extends EventEmitter { const renderer = ((this._rendererInterfaces[ (rendererID: any) ]: any): RendererInterface); - return renderer.getFiberIDFromNative(node, true); + return renderer.getInternalIDFromNative(node, true); } catch (e) {} } return null; @@ -253,7 +253,7 @@ export default class Agent extends EventEmitter { let node: HTMLElement | null = null; if (renderer !== null) { - node = ((renderer.findNativeByFiberID(id): any): HTMLElement); + node = ((renderer.getNativeFromInternal(id): any): HTMLElement); } if (node != null) { diff --git a/src/backend/index.js b/src/backend/index.js index 298d06a121..2df5c21154 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -4,6 +4,7 @@ import type { DevToolsHook, ReactRenderer, RendererInterface } from './types'; import Agent from './agent'; import { attach } from './renderer'; +import { attach as attachLegacy } from './legacy/renderer'; export function initBackend( hook: DevToolsHook, @@ -40,7 +41,11 @@ export function initBackend( // Inject any not-yet-injected renderers (if we didn't reload-and-profile) if (!rendererInterface) { - rendererInterface = attach(hook, id, renderer, global); + if (typeof renderer.findFiberByHostInstance === 'function') { + rendererInterface = attach(hook, id, renderer, global); + } else { + rendererInterface = attachLegacy(hook, id, renderer, global); + } hook.rendererInterfaces.set(id, rendererInterface); } diff --git a/src/backend/legacy/getChildren.js b/src/backend/legacy/getChildren.js new file mode 100644 index 0000000000..1ed8b52e08 --- /dev/null +++ b/src/backend/legacy/getChildren.js @@ -0,0 +1,63 @@ +// @flow + +export default function getChildren(internalInstance: Object): Array { + // 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 (internalInstance._renderedComponent) { + return [internalInstance._renderedComponent]; + } else if (internalInstance._renderedChildren) { + return childrenToArray(internalInstance._renderedChildren); + } else if ( + internalInstance._currentElement && + internalInstance._currentElement.props + ) { + // DevTools doesn't need to display primative child types, + // So we can filter them out early. + /* + const children = []; + + // This is a native node without rendered children -- meaning the children + // prop is the unfiltered list of children. + // This may include 'null' or even other invalid values, so we need to + // filter it the same way that ReactDOM does. + // Instead of pulling in the whole React library, we just copied over the + // 'traverseAllChildrenImpl' method. + // https://github.com/facebook/react/blob/240b84ed8e1db715d759afaae85033718a0b24e1/src/isomorphic/children/ReactChildren.js#L112-L158 + const unfilteredChildren = internalInstance._currentElement.props.children; + traverseAllChildrenImpl( + unfilteredChildren, + '', // nameSoFar + (_traverseContext, child) => { + const childType = typeof child; + if (childType === 'string' || childType === 'number') { + children.push(child); + } + } + // traverseContext + ); + + return children; + */ + } + + if (internalInstance._instance) { + var inst = internalInstance._instance; + // 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) { + return childrenToArray(inst._renderedChildren); + } + } + + return []; +} + +function childrenToArray(children) { + const array = []; + for (var name in children) { + array.push(children[name]); + } + return array; +} diff --git a/src/backend/legacy/getData.js b/src/backend/legacy/getData.js new file mode 100644 index 0000000000..200fd767a4 --- /dev/null +++ b/src/backend/legacy/getData.js @@ -0,0 +1,46 @@ +// @flow + +import { + ElementTypeClass, + ElementTypeOtherOrUnknown, +} from 'src/devtools/types'; +import { getDisplayName } from 'src/utils'; + +import type { InternalInstance } from './renderer'; +import type { FiberData } from '../types'; + +export default function getData(internalInstance: InternalInstance): FiberData { + let displayName = null; + let key = null; + let type = ElementTypeOtherOrUnknown; + + // != used deliberately here to catch undefined and null + if (internalInstance._currentElement != null) { + if (internalInstance._currentElement.key) { + key = String(internalInstance._currentElement.key); + } + + const elementType = internalInstance._currentElement.type; + if (typeof elementType === 'string') { + // ... + } else if (typeof elementType === 'function') { + // TODO Can we differentiate between function and class component types? + // Dan said _compositeType tells you PureClass, ImpureClass, StatelessFunctional but it was only added in v14 + // getPublicInstance() returns null for function components + type = ElementTypeClass; + displayName = getDisplayName(elementType); + } else if (typeof internalInstance._stringText === 'string') { + // ... + } else { + // TODO What kind of case does this cover? + console.log('what is this type?'); + displayName = getDisplayName(elementType); + } + } + + return { + displayName, + key, + type, + }; +} diff --git a/src/backend/legacy/getElementType.js b/src/backend/legacy/getElementType.js new file mode 100644 index 0000000000..1740cfe9ad --- /dev/null +++ b/src/backend/legacy/getElementType.js @@ -0,0 +1,22 @@ +// @flow +import { + ElementTypeClass, + ElementTypeOtherOrUnknown, +} from 'src/devtools/types'; + +import type { InternalInstance } from './renderer'; +import type { ElementType } from 'src/devtools/types'; + +export default function getElementType( + internalInstance: InternalInstance +): ElementType { + // != used deliberately here to catch undefined and null + if (internalInstance._currentElement != null) { + const elementType = internalInstance._currentElement.type; + if (typeof elementType === 'function') { + return ElementTypeClass; + } + } + + return ElementTypeOtherOrUnknown; +} diff --git a/src/backend/legacy/renderer.js b/src/backend/legacy/renderer.js new file mode 100644 index 0000000000..5d4afa1eb5 --- /dev/null +++ b/src/backend/legacy/renderer.js @@ -0,0 +1,787 @@ +// @flow + +import { + ElementTypeClass, + ElementTypeFunction, + ElementTypeRoot, + ElementTypeOtherOrUnknown, +} from 'src/devtools/types'; +import { getUID, utfEncodeString, operationsArrayToString } from '../../utils'; +import { cleanForBridge, copyWithSet } from '../utils'; +import { + __DEBUG__, + TREE_OPERATION_ADD, + TREE_OPERATION_REMOVE, +} from '../../constants'; +import getChildren from './getChildren'; +import getData from './getData'; +import getElementType from './getElementType'; +import { + decorateResult, + decorateMany, + forceUpdate, + restoreMany, +} from './utils'; + +import type { + DevToolsHook, + GetInternalIDFromNative, + GetNativeFromInternal, + NativeType, + RendererInterface, +} from '../types'; +import type { InspectedElement } from 'src/devtools/views/Components/types'; + +export type InternalInstance = Object; +type LegacyRenderer = Object; + +export function attach( + hook: DevToolsHook, + rendererID: number, + renderer: LegacyRenderer, + global: Object +): RendererInterface { + const idToInternalInstanceMap: Map = new Map(); + const idToParentIDMap: Map = new Map(); + const internalInstanceToIDMap: Map = new Map(); + const mountedIDs: Set = new Set(); + const pendingMountIDs: Set = new Set(); + const pendingUnmountIDs: Set = new Set(); + const rootIDs: Set = new Set(); + + function getID(internalInstance: InternalInstance): number { + if (!internalInstanceToIDMap.has(internalInstance)) { + const id = getUID(); + internalInstanceToIDMap.set(internalInstance, id); + idToInternalInstanceMap.set(id, internalInstance); + } + return ((internalInstanceToIDMap.get(internalInstance): any): number); + } + + function getChildIDs(internalInstance: InternalInstance): Array { + return getChildren(internalInstance).map(getID); + } + + //function getParentID(internalInstance: InternalInstance): number { + // return getID(internalInstance._hostParent); + //} + + function findNearestAncestorInTree( + internalInstance: InternalInstance + ): number | null { + let current = internalInstance; + while (current != null) { + const id = getID(current); + if ( + rootIDs.has(id) || + getElementType(current) !== ElementTypeOtherOrUnknown + ) { + return id; + } + const parentID = idToParentIDMap.get(id); + current = parentID != null ? idToInternalInstanceMap.get(parentID) : null; + } + return null; + } + + let getInternalIDFromNative: GetInternalIDFromNative = ((null: any): GetInternalIDFromNative); + let getNativeFromInternal: ( + id: number + ) => ?NativeType = ((null: any): GetNativeFromInternal); + + // React Native + if (renderer.Mount.findNodeHandle && renderer.Mount.nativeTagToRootNodeID) { + getInternalIDFromNative = (nativeTag, findNearestUnfilteredAncestor) => { + const internalInstance = renderer.Mount.nativeTagToRootNodeID(nativeTag); + return findNearestAncestorInTree(internalInstance); + }; + getNativeFromInternal = (id: number) => { + const internalInstance = idToInternalInstanceMap.get(id); + return renderer.Mount.findNodeHandle(internalInstance); + }; + + // React DOM 15+ + } else if (renderer.ComponentTree) { + getInternalIDFromNative = (node, findNearestUnfilteredAncestor) => { + const internalInstance = renderer.ComponentTree.getClosestInstanceFromNode( + node + ); + return findNearestAncestorInTree(internalInstance); + }; + getNativeFromInternal = (id: number) => { + const internalInstance = idToInternalInstanceMap.get(id); + return renderer.ComponentTree.getNodeFromInstance(internalInstance); + }; + + // React DOM + } else if (renderer.Mount.getID && renderer.Mount.getNode) { + getInternalIDFromNative = (node, findNearestUnfilteredAncestor) => { + let id = renderer.Mount.getID(node); + while (node && node.parentNode && !id) { + node = node.parentNode; + id = renderer.Mount.getID(node); + } + return id; + }; + + getNativeFromInternal = (id: number) => { + try { + const internalInstance = idToInternalInstanceMap.get(id); + if (internalInstance != null) { + return renderer.Mount.getNode(internalInstance._rootNodeID); + } + } catch (e) {} + + return null; + }; + } else { + console.warn( + 'Unknown React version (does not have getID), probably an unshimmed React Native' + ); + } + + let oldReconcilerMethods = null; + let oldRenderComponent = null; + let oldRenderRoot = null; + + // React DOM + if (renderer.Mount._renderNewRootComponent) { + oldRenderRoot = decorateResult( + renderer.Mount, + '_renderNewRootComponent', + internalInstance => { + const id = getID(internalInstance); + + rootIDs.add(id); + + if (__DEBUG__) { + console.log('renderer.Mount._renderNewRootComponent()', id); + } + + recordPendingMount(internalInstance); + + // If we're mounting a root, we've just finished a batch of work, + // so it's safe to synchronously flush. + flushPendingEvents(id); + } + ); + + // React Native + } else if (renderer.Mount.renderComponent) { + oldRenderComponent = decorateResult( + renderer.Mount, + 'renderComponent', + internalInstance => { + const id = getID(internalInstance); + + rootIDs.add(id); + + if (__DEBUG__) { + console.log('renderer.Mount.renderComponent()', id); + } + + recordPendingMount(internalInstance); + + // If we're mounting a root, we've just finished a batch of work, + // so it's safe to synchronously flush. + flushPendingEvents(id); + } + ); + } + + if (renderer.Reconciler) { + oldReconcilerMethods = decorateMany(renderer.Reconciler, { + mountComponent(internalInstance, rootID, transaction, context) { + recordPendingMount(internalInstance); + }, + performUpdateIfNecessary( + internalInstance, + nextChild, + transaction, + context + ) { + // TODO Check for change in order of children + }, + receiveComponent(internalInstance, nextChild, transaction, context) { + // TODO Check for change in order of children + }, + unmountComponent(internalInstance) { + recordPendingUnmount(internalInstance); + }, + }); + } + + function cleanup() { + if (oldReconcilerMethods !== null) { + if (renderer.Component) { + restoreMany(renderer.Component.Mixin, oldReconcilerMethods); + } else { + restoreMany(renderer.Reconciler, oldReconcilerMethods); + } + } + if (oldRenderRoot !== null) { + renderer.Mount._renderNewRootComponent = oldRenderRoot; + } + if (oldRenderComponent !== null) { + renderer.Mount.renderComponent = oldRenderComponent; + } + oldReconcilerMethods = null; + oldRenderRoot = null; + oldRenderComponent = null; + } + + let pendingOperations: Uint32Array = new Uint32Array(0); + + function addOperation( + newAction: Uint32Array, + addToStartOfQueue: boolean = false + ): void { + const oldActions = pendingOperations; + pendingOperations = new Uint32Array(oldActions.length + newAction.length); + if (addToStartOfQueue) { + pendingOperations.set(newAction); + pendingOperations.set(oldActions, newAction.length); + } else { + pendingOperations.set(oldActions); + pendingOperations.set(newAction, oldActions.length); + } + } + + // TODO Rethink the below queueing mechanism. + // Every mount is some parent's update (except for the root mount which we can explicitly handle) + // So maybe we only need to call queueFlushPendingEvents() for updates, + // and maybe we can rely on an id-to-root Map for this case, to limit the scope of what we crawl. + + // Older React renderers did not have the concept of a commit. + // The data structure was just ad-hoc mutated in place. + // So except for the case of the root mounting the first time, + // there is no event we can observe to signal that a render is finished. + // However since older renderers were always synchronous, + // we can use setTimeout to batch operations together. + // In the case of a cascading update, we might batch multiple "commits"- + // but that should be okay, since the batching is not strictly necessary. + let flushPendingEventsTimeoutID: TimeoutID | null = null; + function queueFlushPendingEvents() { + if (flushPendingEventsTimeoutID === null) { + flushPendingEventsTimeoutID = setTimeout(() => { + flushPendingEventsTimeoutID = null; + + // If there are pending operations, walk the tree and find them. + // Ideally we wouldjust pluck the pending operations out of the sets directly, + // but without doing a full traversal, it would be hard for us to determine the filtered parent. + // It should be possible to improve this though, by maintaining a map of id-to-parent, + // and crawling upward to the first non-filtered node. + // TODO Revisit this and think about it more... + if (pendingMountIDs.size > 0 || pendingUnmountIDs.size > 0) { + rootIDs.forEach(flushPendingEvents); + } + }, 0); + } + } + + function flushInitialOperations() { + // Older versions of React do not support profiling mode, so there's nothing to flush. + // Crawl roots though and register any nodes that mounted before we were injected. + + const roots = + renderer.Mount._instancesByReactRootID || + renderer.Mount._instancesByContainerID; + + for (let key in roots) { + const internalInstance = roots[key]; + const id = getID(internalInstance); + + rootIDs.add(id); + + crawlAndRecordMounts(id, 0, true); + + // It's safe to synchronously flush for the root we just crawled. + flushPendingEvents(id); + } + } + + function crawlAndRecordMounts( + id: number, + parentID: number, + isInitialMount: boolean + ) { + const internalInstance = idToInternalInstanceMap.get(id); + const shouldIncludeInTree = + parentID === 0 || + getElementType(internalInstance) !== ElementTypeOtherOrUnknown; + + // Not all nodes are mounted in the frontend DevTools tree, + // but it's important to track parent info even for the unmounted ones. + idToParentIDMap.set(id, parentID); + + if (__DEBUG__) { + console.group( + 'crawlAndRecordMounts() id:', + id, + 'shouldIncludeInTree?', + shouldIncludeInTree + ); + } + + if (shouldIncludeInTree) { + const didMount = isInitialMount || pendingMountIDs.has(id); + const didUnmount = pendingUnmountIDs.has(id); + + // If this node was both mounted and unmounted in the same batch, + // just skip it and don't send any update. + if (didMount && didUnmount) { + pendingUnmountIDs.delete(id); + return; + } else if (didMount) { + recordMount(id, parentID); + } + } + + getChildIDs(internalInstance).forEach(childID => + crawlAndRecordMounts( + childID, + shouldIncludeInTree ? id : parentID, + isInitialMount + ) + ); + + if (__DEBUG__) { + console.groupEnd(); + } + } + + function flushPendingEvents(rootID: number): void { + // Crawl tree and queue mounts/updates. + crawlAndRecordMounts(rootID, 0, false); + + // Send pending deletions. + pendingUnmountIDs.forEach(id => { + if (mountedIDs.has(id)) { + recordUnmount(id); + } + }); + + // Identify which renderer this update is coming from. + // This enables roots to be mapped to renderers, + // Which in turn enables fiber props, states, and hooks to be inspected. + const idArray = new Uint32Array(2); + idArray[0] = rendererID; + idArray[1] = rootID; + addOperation(idArray, true); + + if (__DEBUG__) { + operationsArrayToString(pendingOperations); + } + + // If we've already connected to the frontend, just pass the operations through. + hook.emit('operations', pendingOperations); + + pendingMountIDs.clear(); + pendingUnmountIDs.clear(); + pendingOperations = new Uint32Array(0); + } + + function inspectElement(id: number): InspectedElement | null { + let result = inspectElementRaw(id); + if (result === null) { + return null; + } + // TODO Review sanitization approach for the below inspectable values. + result.context = cleanForBridge(result.context); + result.props = cleanForBridge(result.props); + result.state = cleanForBridge(result.state); + return result; + } + + function inspectElementRaw(id: number): InspectedElement | null { + const internalInstance = idToInternalInstanceMap.get(id); + const data = getData(internalInstance); + + let context = null; + let owners = null; + let props = null; + let state = null; + let source = null; + + if (internalInstance != null) { + const element = internalInstance._currentElement; + if (element !== null) { + props = element.props; + source = element._source != null ? element._source : null; + + let owner = element._owner; + if (owner) { + owners = []; + while (owner != null) { + owners.push({ + displayName: getData(owner).displayName || 'Unknown', + id: getID(owner), + }); + owner = owner.owner; + } + } + } + context = internalInstance.context || null; + state = internalInstance.state || null; + } + + return { + id, + + // Hooks did not exist in legacy versions + canEditHooks: false, + + // Does the current renderer support editable function props? + canEditFunctionProps: true, + + // Suspense did not exist in legacy versions + canToggleSuspense: false, + + // Can view component source location. + canViewSource: + data.type === ElementTypeClass || data.type === ElementTypeFunction, + + displayName: data.displayName, + + // Inspectable properties. + context, + hooks: null, + props, + state, + + // List of owners + owners, + + // Location of component in source coude. + source, + }; + } + + function logElementToConsole(id: number): void { + const result = inspectElementRaw(id); + if (result === null) { + console.warn(`Could not find element with id "${id}"`); + return; + } + + const supportsGroup = typeof console.groupCollapsed === 'function'; + if (supportsGroup) { + console.groupCollapsed( + `[Click to expand] %c<${result.displayName || 'Component'} />`, + // --dom-tag-name-color is the CSS variable Chrome styles HTML elements with in the console. + 'color: var(--dom-tag-name-color); font-weight: normal;' + ); + } + if (result.props !== null) { + console.log('Props:', result.props); + } + if (result.state !== null) { + console.log('State:', result.state); + } + if (result.context !== null) { + console.log('State:', result.context); + } + const nativeNode = getNativeFromInternal(id); + if (nativeNode !== null) { + console.log('Node:', nativeNode); + } + if (window.chrome || /firefox/i.test(navigator.userAgent)) { + console.log( + 'Right-click any value to save it as a global variable for further inspection.' + ); + } + if (supportsGroup) { + console.groupEnd(); + } + } + + function prepareViewElementSource(id: number): void { + const internalInstance = idToInternalInstanceMap.get(id); + if (internalInstance == null) { + console.warn(`Could not find instance with id "${id}"`); + return; + } + + const element = internalInstance._currentElement; + if (element == null) { + console.warn(`Could not find element with id "${id}"`); + return; + } + + global.$type = element.type; + } + + function selectElement(id: number): void { + const internalInstance = idToInternalInstanceMap.get(id); + if (internalInstance == null) { + console.warn(`Could not find instance with id "${id}"`); + return; + } + + switch (getElementType(internalInstance)) { + case ElementTypeClass: + case ElementTypeFunction: + const element = internalInstance._currentElement; + if (element == null) { + console.warn(`Could not find element with id "${id}"`); + return; + } + + global.$r = { + props: element.props, + type: element.type, + }; + break; + default: + break; + } + } + + function recordPendingMount(internalInstance: InternalInstance) { + pendingMountIDs.add(getID(internalInstance)); + + if (__DEBUG__) { + console.log( + '%crecordPendingMount()', + 'color: green', + getID(internalInstance) + ); + } + + queueFlushPendingEvents(); + } + + function recordPendingUnmount(internalInstance: InternalInstance) { + const id = getID(internalInstance); + + pendingUnmountIDs.add(id); + + // Not all nodes are mounted (or unmounted) in the frontend DevTools tree, + // so it's important to remove entries from this map on pending unmount. + idToParentIDMap.delete(id); + + if (__DEBUG__) { + console.log( + '%crecordPendingUnmount()', + 'color: red', + getID(internalInstance) + ); + } + + queueFlushPendingEvents(); + } + + function recordMount(id: number, parentID: number) { + const internalInstance = ((idToInternalInstanceMap.get( + id + ): any): InternalInstance); + const isRoot = rootIDs.has(id); + + if (__DEBUG__) { + console.log( + '%crecordMount()', + 'color: green; font-weight: bold;', + id, + getData(internalInstance).displayName + ); + } + + mountedIDs.add(id); + + if (isRoot) { + // TODO Is this right? For all versions? + const hasOwnerMetadata = + internalInstance._currentElement != null && + internalInstance._currentElement._owner != null; + + const operation = new Uint32Array(5); + operation[0] = TREE_OPERATION_ADD; + operation[1] = id; + operation[2] = ElementTypeRoot; + operation[3] = 0; // isProfilingSupported? + operation[4] = hasOwnerMetadata ? 1 : 0; + addOperation(operation); + } else { + const { displayName, key, type } = getData(internalInstance); + + const ownerID = + internalInstance._currentElement != null && + internalInstance._currentElement._owner != null + ? getID(internalInstance._currentElement._owner) + : 0; + + let encodedDisplayName = ((null: any): Uint8Array); + let encodedKey = ((null: any): Uint8Array); + + if (displayName !== null) { + encodedDisplayName = utfEncodeString(displayName); + } + + if (key !== null) { + // React$Key supports string and number types as inputs, + // But React converts numeric keys to strings, so we only have to handle that type here. + // https://github.com/facebook/react/blob/0e67969cb1ad8c27a72294662e68fa5d7c2c9783/packages/react/src/ReactElement.js#L187 + encodedKey = utfEncodeString(((key: any): string)); + } + + const encodedDisplayNameSize = + displayName === null ? 0 : encodedDisplayName.length; + const encodedKeySize = key === null ? 0 : encodedKey.length; + + const operation = new Uint32Array( + 7 + encodedDisplayNameSize + encodedKeySize + ); + operation[0] = TREE_OPERATION_ADD; + operation[1] = id; + operation[2] = type; + operation[3] = parentID; + operation[4] = ownerID; + operation[5] = encodedDisplayNameSize; + if (displayName !== null) { + operation.set(encodedDisplayName, 6); + } + operation[6 + encodedDisplayNameSize] = encodedKeySize; + if (key !== null) { + operation.set(encodedKey, 6 + encodedDisplayNameSize + 1); + } + addOperation(operation); + } + } + + function recordUnmount(id: number) { + const internalInstance = idToInternalInstanceMap.get(id); + const isRoot = rootIDs.has(id); + + if (__DEBUG__) { + console.log( + '%crecordUnmount()', + 'color: red; font-weight: bold;', + id, + getData(internalInstance).displayName + ); + } + + if (isRoot) { + const operation = new Uint32Array(2); + operation[0] = TREE_OPERATION_REMOVE; + operation[1] = id; + addOperation(operation); + + rootIDs.delete(id); + } else { + const operation = new Uint32Array(2); + operation[0] = TREE_OPERATION_REMOVE; + operation[1] = id; + addOperation(operation); + } + + idToInternalInstanceMap.delete(id); + internalInstanceToIDMap.delete(internalInstance); + + mountedIDs.delete(id); + } + + function setInProps(id: number, path: Array, value: any) { + const internalInstance = idToInternalInstanceMap.get(id); + if (internalInstance != null) { + const element = internalInstance._currentElement; + internalInstance._currentElement = { + ...element, + props: copyWithSet(element.props, path, value), + }; + forceUpdate(internalInstance._instance); + } + } + + function setInState(id: number, path: Array, value: any) { + const internalInstance = idToInternalInstanceMap.get(id); + if (internalInstance != null) { + setIn(internalInstance.state, path, value); + internalInstance.forceUpdate(); + } + } + + function setInContext(id: number, path: Array, value: any) { + const internalInstance = idToInternalInstanceMap.get(id); + if (internalInstance != null) { + setIn(internalInstance.context, path, value); + forceUpdate(internalInstance); + } + } + + function setIn(obj: Object, path: Array, value: any) { + const last = path.pop(); + const parent = path.reduce( + // $FlowFixMe + (reduced, attr) => (reduced ? reduced[attr] : null), + obj + ); + if (parent) { + // $FlowFixMe + parent[last] = value; + } + } + + // v16+ only features + const getCommitDetails = () => { + throw new Error('getCommitDetails not supported by this renderer'); + }; + const getFiberCommits = () => { + throw new Error('getFiberCommits not supported by this renderer'); + }; + const getInteractions = () => { + throw new Error('getInteractions not supported by this renderer'); + }; + const getProfilingDataForDownload = () => { + throw new Error( + 'getProfilingDataForDownload not supported by this renderer' + ); + }; + const getProfilingSummary = () => { + throw new Error('getProfilingSummary not supported by this renderer'); + }; + const handleCommitFiberRoot = () => { + throw new Error('handleCommitFiberRoot not supported by this renderer'); + }; + const handleCommitFiberUnmount = () => { + throw new Error('handleCommitFiberUnmount not supported by this renderer'); + }; + const overrideSuspense = () => { + throw new Error('overrideSuspense not supported by this renderer'); + }; + const setInHook = () => { + throw new Error('setInHook not supported by this renderer'); + }; + const startProfiling = () => { + throw new Error('startProfiling not supported by this renderer'); + }; + const stopProfiling = () => { + throw new Error('stopProfiling not supported by this renderer'); + }; + + return { + cleanup, + flushInitialOperations, + getCommitDetails, + getFiberCommits, + getInteractions, + getInternalIDFromNative, + getNativeFromInternal, + getProfilingDataForDownload, + getProfilingSummary, + handleCommitFiberRoot, + handleCommitFiberUnmount, + inspectElement, + logElementToConsole, + overrideSuspense, + prepareViewElementSource, + renderer, + selectElement, + setInContext, + setInHook, + setInProps, + setInState, + startProfiling, + stopProfiling, + }; +} diff --git a/src/backend/legacy/traverseAllChildrenImpl.js b/src/backend/legacy/traverseAllChildrenImpl.js new file mode 100644 index 0000000000..a7e1cd399c --- /dev/null +++ b/src/backend/legacy/traverseAllChildrenImpl.js @@ -0,0 +1,146 @@ +// @flow + +const SEPARATOR = '.'; +const SUBSEPARATOR = ':'; + +const FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec. +// The Symbol used to tag the ReactElement type. If there is no native Symbol +// nor polyfill, then a plain number is used for performance. +const ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; +const REACT_ELEMENT_TYPE = + (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) || + 0xeac7; + +/** + * Escape and wrap key so it is safe to use as a reactid + * + * @param {string} key to be escaped. + * @return {string} the escaped key. + */ +function escape(key: string): string { + const escapeRegex = /[=:]/g; + const escaperLookup = { + '=': '=0', + ':': '=2', + }; + const escapedString = ('' + key).replace(escapeRegex, function(match) { + return escaperLookup[match]; + }); + + return '$' + escapedString; +} + +/** + * Generate a key string that identifies a component within a set. + * + * @param {*} component A component that could contain a manual key. + * @param {number} index Index that is used if a manual key is not provided. + * @return {string} + */ +function getComponentKey(component, index) { + // Do some typechecking here since we call this blindly. We want to ensure + // that we don't block potential future ES APIs. + if ( + typeof component === 'object' && + component !== null && + component.key != null + ) { + // Explicit key + return escape(component.key); + } + // Implicit key determined by the index in the set + return index.toString(36); +} + +/** + * We do a copied the 'traverseAllChildrenImpl' method from + * `React.Children` so that we don't pull in the whole React library. + * @param {?*} children Children tree container. + * @param {!string} nameSoFar Name of the key path so far. + * @param {!function} callback Callback to invoke with each child found. + * @param {?*} traverseContext Used to pass information throughout the traversal + * process. + * @return {!number} The number of children in this subtree. + */ +export default function traverseAllChildrenImpl( + children: any, + nameSoFar: string, + callback: Function, + traverseContext: any +): number { + const type = typeof children; + + if (type === 'undefined' || type === 'boolean') { + // All of the above are perceived as null. + children = null; + } + + if ( + children === null || + type === 'string' || + type === 'number' || + // The following is inlined from ReactElement. This means we can optimize + // some checks. React Fiber also inlines this logic for similar purposes. + (type === 'object' && children.$$typeof === REACT_ELEMENT_TYPE) + ) { + callback( + traverseContext, + children, + // If it's the only child, treat the name as if it was wrapped in an array + // so that it's consistent if the number of children grows. + nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar + ); + return 1; + } + + let child; + let nextName; + let subtreeCount = 0; // Count of children found in the current subtree. + let nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR; + + if (Array.isArray(children)) { + for (let i = 0; i < children.length; i++) { + child = children[i]; + nextName = nextNamePrefix + getComponentKey(child, i); + subtreeCount += traverseAllChildrenImpl( + child, + nextName, + callback, + traverseContext + ); + } + } else { + let iteratorFn = + (ITERATOR_SYMBOL && children[ITERATOR_SYMBOL]) || + children[FAUX_ITERATOR_SYMBOL]; + if (typeof iteratorFn === 'function') { + let iterator = iteratorFn.call(children); + let step; + let ii = 0; + while (!(step = iterator.next()).done) { + child = step.value; + nextName = nextNamePrefix + getComponentKey(child, ii++); + subtreeCount += traverseAllChildrenImpl( + child, + nextName, + callback, + traverseContext + ); + } + } else if (type === 'object') { + let addendum = + ' If you meant to render a collection of children, use an array ' + + 'instead.'; + let childrenString = '' + children; + throw Error( + `The React Devtools cannot render an object as a child. (found: ${ + childrenString === '[object Object]' + ? 'object with keys {' + Object.keys(children).join(', ') + '}' + : childrenString + }). ${addendum}` + ); + } + } + + return subtreeCount; +} diff --git a/src/backend/legacy/utils.js b/src/backend/legacy/utils.js new file mode 100644 index 0000000000..49a67888a2 --- /dev/null +++ b/src/backend/legacy/utils.js @@ -0,0 +1,55 @@ +// @flow + +import type { InternalInstance } from './renderer'; + +export function decorateResult( + object: Object, + attr: string, + fn: Function +): Function { + const old = object[attr]; + object[attr] = function(instance: InternalInstance) { + const res = old.apply(this, arguments); + fn(res); + return res; + }; + return old; +} + +export function decorate(object: Object, attr: string, fn: Function): Function { + const old = object[attr]; + object[attr] = function(instance: InternalInstance) { + const res = old.apply(this, arguments); + fn.apply(this, arguments); + return res; + }; + return old; +} + +export function decorateMany( + source: Object, + fns: { [attr: string]: Function } +): Object { + const olds = {}; + for (const name in fns) { + olds[name] = decorate(source, name, fns[name]); + } + return olds; +} + +export function restoreMany(source: Object, olds: Object): void { + for (let name in olds) { + source[name] = olds[name]; + } +} + +export function forceUpdate(instance: InternalInstance): void { + if (typeof instance.forceUpdate === 'function') { + instance.forceUpdate(); + } else if ( + instance.updater != null && + typeof instance.updater.enqueueForceUpdate === 'function' + ) { + instance.updater.enqueueForceUpdate(this, () => {}, 'forceUpdate'); + } +} diff --git a/src/backend/renderer.js b/src/backend/renderer.js index 7aa8b1555a..d0acd59fa5 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -14,7 +14,7 @@ import { ElementTypeRoot, ElementTypeSuspense, } from 'src/devtools/types'; -import { getDisplayName, utfEncodeString } from '../utils'; +import { getDisplayName, getUID, utfEncodeString } from '../utils'; import { cleanForBridge, copyWithSet, setInObject } from './utils'; import { __DEBUG__, @@ -25,7 +25,6 @@ import { TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, } from '../constants'; -import { getUID } from '../utils'; import { inspectHooksOfFiber } from './ReactDebugHooks'; import type { @@ -312,8 +311,6 @@ export function attach( : symbolOrNumber; } - // TODO: we might want to change the data structure once we no longer suppport Stack versions of `getData`. - // TODO: Keep in sync with getElementType() function getDataForFiber(fiber: Fiber): FiberData { const { elementType, type, key, tag } = fiber; @@ -1166,7 +1163,7 @@ export function attach( currentRootID = -1; } - function findNativeByFiberID(id: number) { + function getNativeFromInternal(id: number) { try { const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id)); if (fiber === null) { @@ -1190,7 +1187,7 @@ export function attach( } } - function getFiberIDFromNative( + function getInternalIDFromNative( hostInstance, findNearestUnfilteredAncestor = false ) { @@ -1441,6 +1438,14 @@ export function attach( case ForwardRef: global.$type = fiber.type.render; break; + case MemoComponent: + case SimpleMemoComponent: + const { elementType, type } = fiber; + global.$type = + elementType != null && elementType.type != null + ? elementType.type + : type; + break; default: global.$type = null; break; @@ -1483,7 +1488,9 @@ export function attach( tag === FunctionComponent || tag === IncompleteClassComponent || tag === IndeterminateComponent || - tag === ForwardRef + tag === MemoComponent || + tag === ForwardRef || + tag === SimpleMemoComponent ) { canViewSource = true; if (stateNode && stateNode.context != null) { @@ -1623,7 +1630,7 @@ export function attach( if (result.hooks !== null) { console.log('Hooks:', result.hooks); } - const nativeNode = findNativeByFiberID(id); + const nativeNode = getNativeFromInternal(id); if (nativeNode !== null) { console.log('Node:', nativeNode); } @@ -1938,10 +1945,10 @@ export function attach( cleanup, flushInitialOperations, getCommitDetails, - getFiberIDFromNative, + getInternalIDFromNative, getFiberCommits, getInteractions, - findNativeByFiberID, + getNativeFromInternal, getProfilingDataForDownload, getProfilingSummary, handleCommitFiberRoot, diff --git a/src/backend/types.js b/src/backend/types.js index bd4562eef7..afd060bb48 100644 --- a/src/backend/types.js +++ b/src/backend/types.js @@ -19,7 +19,7 @@ export type FiberData = {| type: ElementType, |}; -export type NativeType = {}; +export type NativeType = Object; export type RendererID = number; type Dispatcher = any; @@ -90,17 +90,20 @@ export type ProfilingSummary = {| rootID: number, |}; +export type GetInternalIDFromNative = ( + component: NativeType, + findNearestUnfilteredAncestor?: boolean +) => number | null; +export type GetNativeFromInternal = (id: number) => ?NativeType; + export type RendererInterface = { cleanup: () => void, - findNativeByFiberID: (id: number) => ?NativeType, flushInitialOperations: () => void, getCommitDetails: (rootID: number, commitIndex: number) => CommitDetails, - getFiberIDFromNative: ( - component: NativeType, - findNearestUnfilteredAncestor?: boolean - ) => number | null, getFiberCommits: (rootID: number, fiberID: number) => FiberCommits, getInteractions: (rootID: number) => Interactions, + getInternalIDFromNative: GetInternalIDFromNative, + getNativeFromInternal: GetNativeFromInternal, getProfilingDataForDownload: (rootID: number) => Object, getProfilingSummary: (rootID: number) => ProfilingSummary, handleCommitFiberRoot: (fiber: Object) => void, @@ -112,14 +115,14 @@ export type RendererInterface = { renderer: ReactRenderer | null, selectElement: (id: number) => void, setInContext: (id: number, path: Array, value: any) => void, + setInProps: (id: number, path: Array, value: any) => void, + setInState: (id: number, path: Array, value: any) => void, setInHook: ( id: number, index: number, path: Array, value: any ) => void, - setInProps: (id: number, path: Array, value: any) => void, - setInState: (id: number, path: Array, value: any) => void, startProfiling: () => void, stopProfiling: () => void, }; diff --git a/src/utils.js b/src/utils.js index f085f550d5..7065a6fe3c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,16 @@ // @flow -const LRU = require('lru-cache'); +import LRU from 'lru-cache'; +import { + TREE_OPERATION_ADD, + TREE_OPERATION_REMOVE, + TREE_OPERATION_RESET_CHILDREN, + TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN, + TREE_OPERATION_UPDATE_TREE_BASE_DURATION, +} from './constants'; +import { ElementTypeRoot } from 'src/devtools/types'; + +import type { ElementType } from 'src/devtools/types'; const FB_MODULE_RE = /^(.*) \[from (.*)\]$/; const cachedDisplayNames: WeakMap = new WeakMap(); @@ -76,3 +86,96 @@ export function utfEncodeString(string: string): Uint32Array { function toCodePoint(string: string) { return string.codePointAt(0); } + +export function operationsArrayToString(operations: Uint32Array) { + const rendererID = operations[0]; + const rootID = operations[1]; + + console.group('rendererID:', rendererID, 'rootID:', rootID); + + let i = 2; + while (i < operations.length) { + let id: number = ((null: any): number); + let parentID: number = ((null: any): number); + let type: ElementType = ((null: any): ElementType); + + const operation = operations[i]; + + switch (operation) { + case TREE_OPERATION_ADD: + id = ((operations[i + 1]: any): number); + type = ((operations[i + 2]: any): ElementType); + + i = i + 3; + + if (type === ElementTypeRoot) { + console.log(`Add root fiber ${id}`); + + i++; // supportsProfiling + i++; // hasOwnerMetadata + } else { + parentID = ((operations[i]: any): number); + i++; + + i++; // ownerID + + const displayNameLength = operations[i]; + i++; + const displayName = + displayNameLength === 0 + ? null + : utfDecodeString( + (operations.slice(i, i + displayNameLength): any) + ); + i += displayNameLength; + + const keyLength = operations[i]; + i++; + i += +keyLength; + + console.log( + `Add fiber ${id} (${displayName || 'null'}) as child of ${parentID}` + ); + } + break; + case TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN: { + id = ((operations[i + 1]: any): number); + + i = i + 2; + + console.log(`Recursively remove children from fiber ${id}`); + break; + } + case TREE_OPERATION_REMOVE: { + id = ((operations[i + 1]: any): number); + + i = i + 2; + + console.log(`Remove fiber ${id}`); + break; + } + case TREE_OPERATION_RESET_CHILDREN: + id = ((operations[i + 1]: any): number); + const numChildren = ((operations[i + 2]: any): number); + const children = ((operations.slice( + i + 3, + i + 3 + numChildren + ): any): Array); + + i = i + 3 + numChildren; + + console.log(`Re-order fiber ${id} children ${children.join(',')}`); + break; + case TREE_OPERATION_UPDATE_TREE_BASE_DURATION: + // Base duration updates are only sent while profiling is in progress. + // We can ignore them at this point. + // The profiler UI uses them lazily in order to generate the tree. + i = i + 3; + break; + default: + throw Error(`Unsupported Bridge operation ${operation}`); + } + } + + console.groupEnd(); +}