From 7ab93402a8de30fa7e0e885ff40aae2890b901c7 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 16 Apr 2019 10:22:26 -0700 Subject: [PATCH 01/13] Initial support for v15+ via new legaacy renderer --- .gitignore | 3 +- fixtures/regression/14.9.html | 39 + fixtures/regression/15.0.html | 39 + fixtures/regression/15.1.html | 39 + fixtures/regression/15.2.html | 39 + fixtures/regression/15.3.html | 39 + fixtures/regression/15.4.html | 39 + fixtures/regression/15.5.html | 39 + fixtures/regression/15.6.html | 39 + fixtures/regression/16.0.html | 39 + fixtures/regression/16.1.html | 39 + fixtures/regression/16.2.html | 39 + fixtures/regression/16.3.html | 39 + fixtures/regression/16.4.html | 39 + fixtures/regression/16.5.html | 41 + fixtures/regression/16.6.html | 42 + fixtures/regression/16.7.html | 42 + fixtures/regression/canary.html | 42 + fixtures/regression/index.html | 28 + fixtures/regression/next.html | 42 + fixtures/regression/shared.js | 330 ++++++++ fixtures/regression/styles.css | 37 + src/backend/agent.js | 4 +- src/backend/index.js | 7 +- src/backend/legacy/getChildren.js | 63 ++ src/backend/legacy/getData.js | 46 + src/backend/legacy/getElementType.js | 22 + src/backend/legacy/renderer.js | 787 ++++++++++++++++++ src/backend/legacy/traverseAllChildrenImpl.js | 146 ++++ src/backend/legacy/utils.js | 55 ++ src/backend/renderer.js | 27 +- src/backend/types.js | 19 +- src/utils.js | 105 ++- 33 files changed, 2371 insertions(+), 24 deletions(-) create mode 100644 fixtures/regression/14.9.html create mode 100644 fixtures/regression/15.0.html create mode 100644 fixtures/regression/15.1.html create mode 100644 fixtures/regression/15.2.html create mode 100644 fixtures/regression/15.3.html create mode 100644 fixtures/regression/15.4.html create mode 100644 fixtures/regression/15.5.html create mode 100644 fixtures/regression/15.6.html create mode 100644 fixtures/regression/16.0.html create mode 100644 fixtures/regression/16.1.html create mode 100644 fixtures/regression/16.2.html create mode 100644 fixtures/regression/16.3.html create mode 100644 fixtures/regression/16.4.html create mode 100644 fixtures/regression/16.5.html create mode 100644 fixtures/regression/16.6.html create mode 100644 fixtures/regression/16.7.html create mode 100644 fixtures/regression/canary.html create mode 100644 fixtures/regression/index.html create mode 100644 fixtures/regression/next.html create mode 100644 fixtures/regression/shared.js create mode 100644 fixtures/regression/styles.css create mode 100644 src/backend/legacy/getChildren.js create mode 100644 src/backend/legacy/getData.js create mode 100644 src/backend/legacy/getElementType.js create mode 100644 src/backend/legacy/renderer.js create mode 100644 src/backend/legacy/traverseAllChildrenImpl.js create mode 100644 src/backend/legacy/utils.js 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(); +} From 29811cd24ef304fb089c6d69692b2e2e21f47e1e Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 17 Apr 2019 15:30:20 -0700 Subject: [PATCH 02/13] Added legacy (v15) tests --- package.json | 4 +- .../storeLegacy-v15-test.js.snap | 279 ++++++++++++ src/__tests__/legacy/storeLegacy-v15-test.js | 404 ++++++++++++++++++ yarn.lock | 34 +- 4 files changed, 718 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap create mode 100644 src/__tests__/legacy/storeLegacy-v15-test.js diff --git a/package.json b/package.json index 60850a905b..ac4b71b258 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "/src/__tests__/storeSerializer" ], "testMatch": [ - "**/__tests__/*-test.js" + "**/__tests__/**/*-test.js" ] }, "scripts": { @@ -131,8 +131,10 @@ "prettier": "^1.16.4", "prop-types": "^15.6.2", "react": "0.0.0-4221565e1", + "react-15": "npm:react@^15", "react-color": "^2.11.7", "react-dom": "0.0.0-4221565e1", + "react-dom-15": "npm:react-dom@^15", "react-is": "0.0.0-4221565e1", "react-virtualized-auto-sizer": "^1.0.2", "react-window": "^1.8.0", diff --git a/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap b/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap new file mode 100644 index 0000000000..00e8eff3ab --- /dev/null +++ b/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap @@ -0,0 +1,279 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Store (legacy) collapseNodesByDefault:false should filter DOM nodes from the store tree: 1: mount 1`] = ` +[root] + â–¾ + â–¾ + + â–¾ + +`; + +exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 1: mount 1`] = ` +[root] + â–¾ + â–¾ + + + â–¾ + + +`; + +exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 2: collapse first Parent 1`] = ` +[root] + â–¾ + â–¸ + â–¾ + + +`; + +exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 3: collapse second Parent 1`] = ` +[root] + â–¾ + â–¸ + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 4: expand first Parent 1`] = ` +[root] + â–¾ + â–¾ + + + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 5: collapse Grandparent 1`] = ` +[root] + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 6: expand Grandparent 1`] = ` +[root] + â–¾ + â–¾ + + + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations for multiple roots: 1: mount 1`] = ` +[root] + â–¾ + + + +[root] + â–¾ + + +`; + +exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations for multiple roots: 2: update 1`] = ` +[root] + â–¾ + + + + +[root] + â–¾ + +`; + +exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations for multiple roots: 3: unmount B 1`] = ` +[root] + â–¾ + + + + +`; + +exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations for multiple roots: 4: unmount A 1`] = ``; + +exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations: 1: mount 1`] = ` +[root] + â–¾ + â–¾ + + + + + â–¾ + + + + +`; + +exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations: 2: update 1`] = ` +[root] + â–¾ + â–¾ + + + â–¾ + + +`; + +exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations: 3: unmount 1`] = ``; + +exports[`Store (legacy) collapseNodesByDefault:true should filter DOM nodes from the store tree: 1: mount 1`] = ` +[root] + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should filter DOM nodes from the store tree: 2: expand Grandparent 1`] = ` +[root] + â–¾ + â–¸ + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should filter DOM nodes from the store tree: 3: expand Parent 1`] = ` +[root] + â–¾ + â–¾ + + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding deep parts of the tree: 1: mount 1`] = ` +[root] + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding deep parts of the tree: 2: expand deepest node 1`] = ` +[root] + â–¾ + â–¾ + â–¾ + â–¾ + +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding deep parts of the tree: 3: collapse root 1`] = ` +[root] + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding deep parts of the tree: 4: expand root 1`] = ` +[root] + â–¾ + â–¾ + â–¾ + â–¾ + +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding deep parts of the tree: 5: collapse middle node 1`] = ` +[root] + â–¾ + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding deep parts of the tree: 6: expand middle node 1`] = ` +[root] + â–¾ + â–¾ + â–¾ + â–¾ + +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 1: mount 1`] = ` +[root] + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 2: expand Grandparent 1`] = ` +[root] + â–¾ + â–¸ + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 3: expand first Parent 1`] = ` +[root] + â–¾ + â–¾ + + + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 4: expand second Parent 1`] = ` +[root] + â–¾ + â–¾ + + + â–¾ + + +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 5: collapse first Parent 1`] = ` +[root] + â–¾ + â–¸ + â–¾ + + +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 6: collapse second Parent 1`] = ` +[root] + â–¾ + â–¸ + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 7: collapse Grandparent 1`] = ` +[root] + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support mount and update operations for multiple roots: 1: mount 1`] = ` +[root] + â–¸ +[root] + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support mount and update operations for multiple roots: 2: update 1`] = ` +[root] + â–¸ +[root] + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support mount and update operations for multiple roots: 3: unmount B 1`] = ` +[root] + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support mount and update operations for multiple roots: 4: unmount A 1`] = ``; + +exports[`Store (legacy) collapseNodesByDefault:true should support mount and update operations: 1: mount 1`] = ` +[root] + â–¸ + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support mount and update operations: 2: update 1`] = ` +[root] + â–¸ + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support mount and update operations: 3: unmount 1`] = ``; + +exports[`Store (legacy) should not allow a root node to be collapsed: 1: mount 1`] = ` +[root] + +`; diff --git a/src/__tests__/legacy/storeLegacy-v15-test.js b/src/__tests__/legacy/storeLegacy-v15-test.js new file mode 100644 index 0000000000..748c2666ea --- /dev/null +++ b/src/__tests__/legacy/storeLegacy-v15-test.js @@ -0,0 +1,404 @@ +// @flow + +describe('Store (legacy)', () => { + let React; + let ReactDOM; + let store; + + const act = (callback: Function) => { + callback(); + + jest.runAllTimers(); // Flush Bridge operations + }; + + beforeEach(() => { + store = global.store; + + // Redirect all React/ReactDOM requires to the v15 UMD. + // We use the UMD because Jest doesn't enable us to mock deep imports (e.g. "react/lib/Something"). + jest.mock('react', () => require.requireActual('react-15/dist/react.js')); + jest.mock('react-dom', () => require.requireActual('react-dom-15/dist/react-dom.js')); + + React = require('react'); + ReactDOM = require('react-dom'); + }); + + it('should not allow a root node to be collapsed', () => { + const Component = () =>
Hi
; + + act(() => + ReactDOM.render(, document.createElement('div')) + ); + expect(store).toMatchSnapshot('1: mount'); + + expect(store.roots).toHaveLength(1); + + const rootID = store.roots[0]; + + expect(() => store.toggleIsCollapsed(rootID, true)).toThrow( + 'Root nodes cannot be collapsed' + ); + }); + + describe('collapseNodesByDefault:false', () => { + beforeEach(() => { + store.collapseNodesByDefault = false; + }); + + it('should support mount and update operations', () => { + const Grandparent = ({ count }) => ( +
+ + +
+ ); + const Parent = ({ count }) => +
{new Array(count).fill(true).map((_, index) => )}
; + const Child = () =>
Hi!
; + + const container = document.createElement('div'); + + act(() => ReactDOM.render(, container)); + expect(store).toMatchSnapshot('1: mount'); + + act(() => ReactDOM.render(, container)); + expect(store).toMatchSnapshot('2: update'); + + act(() => ReactDOM.unmountComponentAtNode(container)); + expect(store).toMatchSnapshot('3: unmount'); + }); + + it('should support mount and update operations for multiple roots', () => { + const Parent = ({ count }) => +
{new Array(count).fill(true).map((_, index) => )}
; + const Child = () =>
Hi!
; + + const containerA = document.createElement('div'); + const containerB = document.createElement('div'); + + act(() => { + ReactDOM.render(, containerA); + ReactDOM.render(, containerB); + }); + expect(store).toMatchSnapshot('1: mount'); + + act(() => { + ReactDOM.render(, containerA); + ReactDOM.render(, containerB); + }); + expect(store).toMatchSnapshot('2: update'); + + act(() => ReactDOM.unmountComponentAtNode(containerB)); + expect(store).toMatchSnapshot('3: unmount B'); + + act(() => ReactDOM.unmountComponentAtNode(containerA)); + expect(store).toMatchSnapshot('4: unmount A'); + }); + + it('should filter DOM nodes from the store tree', () => { + const Grandparent = () => ( +
+
+ +
+ +
+ ); + const Parent = () => ( +
+ +
+ ); + const Child = () =>
Hi!
; + + act(() => + ReactDOM.render( + , + document.createElement('div') + ) + ); + expect(store).toMatchSnapshot('1: mount'); + }); + + it('should support collapsing parts of the tree', () => { + const Grandparent = ({ count }) => ( +
+ + +
+ ); + const Parent = ({ count }) => +
{new Array(count).fill(true).map((_, index) => )}
; + const Child = () =>
Hi!
; + + act(() => + ReactDOM.render( + , + document.createElement('div') + ) + ); + expect(store).toMatchSnapshot('1: mount'); + + const grandparentID = store.getElementIDAtIndex(0); + const parentOneID = store.getElementIDAtIndex(1); + const parentTwoID = store.getElementIDAtIndex(4); + + act(() => store.toggleIsCollapsed(parentOneID, true)); + expect(store).toMatchSnapshot('2: collapse first Parent'); + + act(() => store.toggleIsCollapsed(parentTwoID, true)); + expect(store).toMatchSnapshot('3: collapse second Parent'); + + act(() => store.toggleIsCollapsed(parentOneID, false)); + expect(store).toMatchSnapshot('4: expand first Parent'); + + act(() => store.toggleIsCollapsed(grandparentID, true)); + expect(store).toMatchSnapshot('5: collapse Grandparent'); + + act(() => store.toggleIsCollapsed(grandparentID, false)); + expect(store).toMatchSnapshot('6: expand Grandparent'); + }); + + // TODO Re-enable this test once the renderer supports it. + xit('should support reordering of children', () => { + const Root = ({ children }) =>
{children}
; + const Component = () => null; + + const Foo = () =>
{[]}
; + const Bar = () =>
{[, ]}
; + const foo = ; + const bar = ; + + const container = document.createElement('div'); + + act(() => ReactDOM.render({[foo, bar]}, container)); + expect(store).toMatchSnapshot('1: mount'); + + act(() => ReactDOM.render({[bar, foo]}, container)); + expect(store).toMatchSnapshot('3: reorder children'); + + act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true)); + expect(store).toMatchSnapshot('4: collapse root'); + + act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); + expect(store).toMatchSnapshot('5: expand root'); + }); + }); + + describe('collapseNodesByDefault:true', () => { + beforeEach(() => { + store.collapseNodesByDefault = true; + }); + + it('should support mount and update operations', () => { + const Parent = ({ count }) => +
{new Array(count).fill(true).map((_, index) => )}
; + const Child = () =>
Hi!
; + + const container = document.createElement('div'); + + act(() => + ReactDOM.render( +
+ + +
, + container + ) + ); + expect(store).toMatchSnapshot('1: mount'); + + act(() => + ReactDOM.render( +
+ + +
, + container + ) + ); + expect(store).toMatchSnapshot('2: update'); + + act(() => ReactDOM.unmountComponentAtNode(container)); + expect(store).toMatchSnapshot('3: unmount'); + }); + + it('should support mount and update operations for multiple roots', () => { + const Parent = ({ count }) => +
{new Array(count).fill(true).map((_, index) => )}
; + const Child = () =>
Hi!
; + + const containerA = document.createElement('div'); + const containerB = document.createElement('div'); + + act(() => { + ReactDOM.render(, containerA); + ReactDOM.render(, containerB); + }); + expect(store).toMatchSnapshot('1: mount'); + + act(() => { + ReactDOM.render(, containerA); + ReactDOM.render(, containerB); + }); + expect(store).toMatchSnapshot('2: update'); + + act(() => ReactDOM.unmountComponentAtNode(containerB)); + expect(store).toMatchSnapshot('3: unmount B'); + + act(() => ReactDOM.unmountComponentAtNode(containerA)); + expect(store).toMatchSnapshot('4: unmount A'); + }); + + it('should filter DOM nodes from the store tree', () => { + const Grandparent = () => ( +
+
+ +
+ +
+ ); + const Parent = () => ( +
+ +
+ ); + const Child = () =>
Hi!
; + + act(() => + ReactDOM.render( + , + document.createElement('div') + ) + ); + expect(store).toMatchSnapshot('1: mount'); + + act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); + expect(store).toMatchSnapshot('2: expand Grandparent'); + + act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false)); + expect(store).toMatchSnapshot('3: expand Parent'); + }); + + it('should support expanding parts of the tree', () => { + const Grandparent = ({ count }) => ( +
+ + +
+ ); + const Parent = ({ count }) => +
{new Array(count).fill(true).map((_, index) => )}
; + const Child = () =>
Hi!
; + + act(() => + ReactDOM.render( + , + document.createElement('div') + ) + ); + expect(store).toMatchSnapshot('1: mount'); + + const grandparentID = store.getElementIDAtIndex(0); + + act(() => store.toggleIsCollapsed(grandparentID, false)); + expect(store).toMatchSnapshot('2: expand Grandparent'); + + const parentOneID = store.getElementIDAtIndex(1); + const parentTwoID = store.getElementIDAtIndex(2); + + act(() => store.toggleIsCollapsed(parentOneID, false)); + expect(store).toMatchSnapshot('3: expand first Parent'); + + act(() => store.toggleIsCollapsed(parentTwoID, false)); + expect(store).toMatchSnapshot('4: expand second Parent'); + + act(() => store.toggleIsCollapsed(parentOneID, true)); + expect(store).toMatchSnapshot('5: collapse first Parent'); + + act(() => store.toggleIsCollapsed(parentTwoID, true)); + expect(store).toMatchSnapshot('6: collapse second Parent'); + + act(() => store.toggleIsCollapsed(grandparentID, true)); + expect(store).toMatchSnapshot('7: collapse Grandparent'); + }); + + it('should support expanding deep parts of the tree', () => { + const Wrapper = ({ forwardedRef }) => ( + + ); + const Nested = ({ depth, forwardedRef }) => + depth > 0 ? ( + + ) : ( +
+ ); + + let ref = null; + const refSetter = value => { + ref = value; + }; + + act(() => + ReactDOM.render( + , + document.createElement('div') + ) + ); + expect(store).toMatchSnapshot('1: mount'); + + const deepestedNodeID = global.agent.getIDForNode(ref); + + act(() => store.toggleIsCollapsed(deepestedNodeID, false)); + expect(store).toMatchSnapshot('2: expand deepest node'); + + const rootID = store.getElementIDAtIndex(0); + + act(() => store.toggleIsCollapsed(rootID, true)); + expect(store).toMatchSnapshot('3: collapse root'); + + act(() => store.toggleIsCollapsed(rootID, false)); + expect(store).toMatchSnapshot('4: expand root'); + + const id = store.getElementIDAtIndex(1); + + act(() => store.toggleIsCollapsed(id, true)); + expect(store).toMatchSnapshot('5: collapse middle node'); + + act(() => store.toggleIsCollapsed(id, false)); + expect(store).toMatchSnapshot('6: expand middle node'); + }); + + // TODO Re-enable this test once the renderer supports it. + xit('should support reordering of children', () => { + const Root = ({ children }) =>
{children}
; + const Component = () => null; + + const Foo = () =>
{[]}
; + const Bar = () =>
{[, ]}
; + const foo = ; + const bar = ; + + const container = document.createElement('div'); + + act(() => ReactDOM.render({[foo, bar]}, container)); + expect(store).toMatchSnapshot('1: mount'); + + act(() => ReactDOM.render({[bar, foo]}, container)); + expect(store).toMatchSnapshot('3: reorder children'); + + act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); + expect(store).toMatchSnapshot('4: expand root'); + + act(() => { + store.toggleIsCollapsed(store.getElementIDAtIndex(2), false); + store.toggleIsCollapsed(store.getElementIDAtIndex(1), false); + }); + expect(store).toMatchSnapshot('5: expand leaves'); + + act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true)); + expect(store).toMatchSnapshot('6: collapse root'); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index dfd34ac54d..588a7f88d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3462,6 +3462,15 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" +create-react-class@^15.6.0: + version "15.6.3" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036" + integrity sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg== + dependencies: + fbjs "^0.8.9" + loose-envify "^1.3.1" + object-assign "^4.1.1" + create-react-context@^0.2.1: version "0.2.3" resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.3.tgz#9ec140a6914a22ef04b8b09b7771de89567cb6f3" @@ -4930,7 +4939,7 @@ fbjs@0.5.1: ua-parser-js "^0.7.9" whatwg-fetch "^0.9.0" -fbjs@^0.8.0: +fbjs@^0.8.0, fbjs@^0.8.9: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90= @@ -7562,7 +7571,7 @@ log-update@^2.0.0, log-update@^2.3.0: cli-cursor "^2.0.0" wrap-ansi "^3.0.1" -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -9168,6 +9177,17 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +"react-15@npm:react@^15": + version "15.6.2" + resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72" + integrity sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI= + dependencies: + create-react-class "^15.6.0" + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.10" + react-color@^2.11.7: version "2.17.0" resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.17.0.tgz#e14b8a11f4e89163f65a34c8b43faf93f7f02aaa" @@ -9180,6 +9200,16 @@ react-color@^2.11.7: reactcss "^1.2.0" tinycolor2 "^1.4.1" +"react-dom-15@npm:react-dom@^15": + version "15.6.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730" + integrity sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA= + dependencies: + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.10" + react-dom@0.0.0-4221565e1: version "0.0.0-4221565e1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-4221565e1.tgz#d9044312a3294a8828867624a9fea7fd9aa76570" From 4422337562574cb87012d3dc68ea66cab638dda6 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sun, 26 May 2019 08:37:05 -0700 Subject: [PATCH 03/13] Added another (disabled) test; refactored getChildren slightly --- src/__tests__/legacy/storeLegacy-v15-test.js | 53 +++++++++++++++++--- src/backend/legacy/getChildren.js | 48 ++++++++++-------- 2 files changed, 72 insertions(+), 29 deletions(-) diff --git a/src/__tests__/legacy/storeLegacy-v15-test.js b/src/__tests__/legacy/storeLegacy-v15-test.js index 1b8d38753d..baa882e538 100644 --- a/src/__tests__/legacy/storeLegacy-v15-test.js +++ b/src/__tests__/legacy/storeLegacy-v15-test.js @@ -176,6 +176,45 @@ describe('Store (legacy)', () => { expect(store).toMatchSnapshot('6: expand Grandparent'); }); + // TODO Re-enable this test once the renderer supports it. + xit('should support adding and removing children', () => { + const Root = ({ children }) =>
{children}
; + const Component = () => null; + + const container = document.createElement('div'); + + act(() => + ReactDOM.render( + + + , + container + ) + ); + expect(store).toMatchSnapshot('1: mount'); + + act(() => + ReactDOM.render( + + + + , + container + ) + ); + expect(store).toMatchSnapshot('2: add child'); + + act(() => + ReactDOM.render( + + + , + container + ) + ); + expect(store).toMatchSnapshot('3: remove child'); + }); + // TODO Re-enable this test once the renderer supports it. xit('should support reordering of children', () => { const Root = ({ children }) =>
{children}
; @@ -194,13 +233,13 @@ describe('Store (legacy)', () => { expect(store).toMatchSnapshot('1: mount'); act(() => ReactDOM.render({[bar, foo]}, container)); - expect(store).toMatchSnapshot('3: reorder children'); + expect(store).toMatchSnapshot('2: reorder children'); act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true)); - expect(store).toMatchSnapshot('4: collapse root'); + expect(store).toMatchSnapshot('3: collapse root'); act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); - expect(store).toMatchSnapshot('5: expand root'); + expect(store).toMatchSnapshot('4: expand root'); }); }); @@ -422,19 +461,19 @@ describe('Store (legacy)', () => { expect(store).toMatchSnapshot('1: mount'); act(() => ReactDOM.render({[bar, foo]}, container)); - expect(store).toMatchSnapshot('3: reorder children'); + expect(store).toMatchSnapshot('2: reorder children'); act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); - expect(store).toMatchSnapshot('4: expand root'); + expect(store).toMatchSnapshot('3: expand root'); act(() => { store.toggleIsCollapsed(store.getElementIDAtIndex(2), false); store.toggleIsCollapsed(store.getElementIDAtIndex(1), false); }); - expect(store).toMatchSnapshot('5: expand leaves'); + expect(store).toMatchSnapshot('4: expand leaves'); act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true)); - expect(store).toMatchSnapshot('6: collapse root'); + expect(store).toMatchSnapshot('5: collapse root'); }); }); }); diff --git a/src/backend/legacy/getChildren.js b/src/backend/legacy/getChildren.js index 1ed8b52e08..0ce3a39603 100644 --- a/src/backend/legacy/getChildren.js +++ b/src/backend/legacy/getChildren.js @@ -1,22 +1,30 @@ // @flow +import traverseAllChildrenImpl from './traverseAllChildrenImpl'; + +// TODO (legacy) Respect component filters + export default function getChildren(internalInstance: Object): Array { + let children = []; + // 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]; + if (typeof internalInstance !== 'object') { + // No children + } else if ( + internalInstance._currentElement === null || + internalInstance._currentElement === false + ) { + // No children + } else if (internalInstance._renderedComponent) { + children = [internalInstance._renderedComponent]; } else if (internalInstance._renderedChildren) { - return childrenToArray(internalInstance._renderedChildren); + children = renderedChildrenToArray(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 @@ -34,30 +42,26 @@ export default function getChildren(internalInstance: Object): Array { children.push(child); } } - // traverseContext ); - - return children; - */ } - if (internalInstance._instance) { - var inst = internalInstance._instance; + const instance = internalInstance._instance; + if (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); + if (instance._renderedChildren) { + children = renderedChildrenToArray(instance._renderedChildren); } } - return []; + return children; } -function childrenToArray(children) { - const array = []; - for (var name in children) { - array.push(children[name]); +function renderedChildrenToArray(renderedChildren): Array { + const childrenArray = []; + for (let name in renderedChildren) { + childrenArray.push(renderedChildren[name]); } - return array; + return childrenArray; } From 9f44251ebf60af8331aaff5ced5629675e04e4bd Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 31 May 2019 14:27:22 +0100 Subject: [PATCH 04/13] Remove unnecessary code for host text as we skip it anyway --- src/backend/legacy/getChildren.js | 26 +--- src/backend/legacy/renderer.js | 3 + src/backend/legacy/traverseAllChildrenImpl.js | 146 ------------------ 3 files changed, 5 insertions(+), 170 deletions(-) delete mode 100644 src/backend/legacy/traverseAllChildrenImpl.js diff --git a/src/backend/legacy/getChildren.js b/src/backend/legacy/getChildren.js index 0ce3a39603..0f10b5f282 100644 --- a/src/backend/legacy/getChildren.js +++ b/src/backend/legacy/getChildren.js @@ -1,7 +1,5 @@ // @flow -import traverseAllChildrenImpl from './traverseAllChildrenImpl'; - // TODO (legacy) Respect component filters export default function getChildren(internalInstance: Object): Array { @@ -21,29 +19,9 @@ export default function getChildren(internalInstance: Object): Array { children = [internalInstance._renderedComponent]; } else if (internalInstance._renderedChildren) { children = renderedChildrenToArray(internalInstance._renderedChildren); - } else if ( - internalInstance._currentElement && - internalInstance._currentElement.props - ) { - // 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); - } - } - ); } + // Note: we skip the case where children are just strings or numbers + // because the new DevTools skips over host text nodes anyway. const instance = internalInstance._instance; if (instance) { diff --git a/src/backend/legacy/renderer.js b/src/backend/legacy/renderer.js index d8318a9304..1f7edf7d36 100644 --- a/src/backend/legacy/renderer.js +++ b/src/backend/legacy/renderer.js @@ -52,6 +52,9 @@ export function attach( const rootIDs: Set = new Set(); function getID(internalInstance: InternalInstance): number { + if (typeof internalInstance !== 'object') { + throw new Error('Invalid internal instance: ' + internalInstance); + } if (!internalInstanceToIDMap.has(internalInstance)) { const id = getUID(); internalInstanceToIDMap.set(internalInstance, id); diff --git a/src/backend/legacy/traverseAllChildrenImpl.js b/src/backend/legacy/traverseAllChildrenImpl.js deleted file mode 100644 index a7e1cd399c..0000000000 --- a/src/backend/legacy/traverseAllChildrenImpl.js +++ /dev/null @@ -1,146 +0,0 @@ -// @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; -} From 6fd83acb1dd29c0a5102204b48369e5ec7879cde Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 31 May 2019 15:38:58 +0100 Subject: [PATCH 05/13] Default to showing host nodes for legacy renderer --- .../storeLegacy-v15-test.js.snap | 256 +++++++++++------- src/__tests__/legacy/storeLegacy-v15-test.js | 26 +- src/backend/legacy/getData.js | 9 +- src/backend/legacy/getElementType.js | 8 +- src/backend/legacy/renderer.js | 5 - 5 files changed, 192 insertions(+), 112 deletions(-) diff --git a/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap b/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap index 00e8eff3ab..37747be9c6 100644 --- a/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap +++ b/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap @@ -3,46 +3,68 @@ exports[`Store (legacy) collapseNodesByDefault:false should filter DOM nodes from the store tree: 1: mount 1`] = ` [root] â–¾ - â–¾ - - â–¾ - + â–¾
+ â–¾
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ + â–¾
+ â–¾ +
`; exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 1: mount 1`] = ` [root] â–¾ - â–¾ - - - â–¾ - - + â–¾
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ +
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ +
`; exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 2: collapse first Parent 1`] = ` [root] â–¾ - â–¸ - â–¾ - - + â–¾
+ â–¸ + â–¾ + â–¾
+ â–¾ +
+ â–¾ +
`; exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 3: collapse second Parent 1`] = ` [root] â–¾ - â–¸ - â–¸ + â–¾
+ â–¸ + â–¸ `; exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 4: expand first Parent 1`] = ` [root] â–¾ - â–¾ - - - â–¸ + â–¾
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ +
+ â–¸ `; exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 5: collapse Grandparent 1`] = ` @@ -53,43 +75,66 @@ exports[`Store (legacy) collapseNodesByDefault:false should support collapsing p exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 6: expand Grandparent 1`] = ` [root] â–¾ - â–¾ - - - â–¸ + â–¾
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ +
+ â–¸ `; exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations for multiple roots: 1: mount 1`] = ` [root] â–¾ - - - + â–¾
+ â–¾ +
+ â–¾ +
+ â–¾ +
[root] â–¾ - - + â–¾
+ â–¾ +
+ â–¾ +
`; exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations for multiple roots: 2: update 1`] = ` [root] â–¾ - - - - + â–¾
+ â–¾ +
+ â–¾ +
+ â–¾ +
+ â–¾ +
[root] â–¾ - + â–¾
+ â–¾ +
`; exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations for multiple roots: 3: unmount B 1`] = ` [root] â–¾ - - - - + â–¾
+ â–¾ +
+ â–¾ +
+ â–¾ +
+ â–¾ +
`; exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations for multiple roots: 4: unmount A 1`] = ``; @@ -97,49 +142,66 @@ exports[`Store (legacy) collapseNodesByDefault:false should support mount and up exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations: 1: mount 1`] = ` [root] â–¾ - â–¾ - - - - - â–¾ - - - - + â–¾
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ +
+ â–¾ +
+ â–¾ +
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ +
+ â–¾ +
+ â–¾ +
`; exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations: 2: update 1`] = ` [root] â–¾ - â–¾ - - - â–¾ - - + â–¾
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ +
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ +
`; exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations: 3: unmount 1`] = ``; -exports[`Store (legacy) collapseNodesByDefault:true should filter DOM nodes from the store tree: 1: mount 1`] = ` +exports[`Store (legacy) collapseNodesByDefault:true should not filter DOM nodes from the store tree: 1: mount 1`] = ` [root] â–¸ `; -exports[`Store (legacy) collapseNodesByDefault:true should filter DOM nodes from the store tree: 2: expand Grandparent 1`] = ` +exports[`Store (legacy) collapseNodesByDefault:true should not filter DOM nodes from the store tree: 2: expand Grandparent 1`] = ` [root] â–¾ - â–¸ - â–¸ + â–¸
`; -exports[`Store (legacy) collapseNodesByDefault:true should filter DOM nodes from the store tree: 3: expand Parent 1`] = ` +exports[`Store (legacy) collapseNodesByDefault:true should not filter DOM nodes from the store tree: 3: expand div 1`] = ` [root] â–¾ - â–¾ - - â–¸ + â–¾
+ â–¸
+ â–¸ `; exports[`Store (legacy) collapseNodesByDefault:true should support expanding deep parts of the tree: 1: mount 1`] = ` @@ -153,7 +215,8 @@ exports[`Store (legacy) collapseNodesByDefault:true should support expanding dee â–¾ â–¾ â–¾ - + â–¾ +
`; exports[`Store (legacy) collapseNodesByDefault:true should support expanding deep parts of the tree: 3: collapse root 1`] = ` @@ -167,7 +230,8 @@ exports[`Store (legacy) collapseNodesByDefault:true should support expanding dee â–¾ â–¾ â–¾ - + â–¾ +
`; exports[`Store (legacy) collapseNodesByDefault:true should support expanding deep parts of the tree: 5: collapse middle node 1`] = ` @@ -182,7 +246,8 @@ exports[`Store (legacy) collapseNodesByDefault:true should support expanding dee â–¾ â–¾ â–¾ - + â–¾ +
`; exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 1: mount 1`] = ` @@ -193,47 +258,54 @@ exports[`Store (legacy) collapseNodesByDefault:true should support expanding par exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 2: expand Grandparent 1`] = ` [root] â–¾ - â–¸ - â–¸ + â–¸
`; -exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 3: expand first Parent 1`] = ` +exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 3: expand parent div 1`] = ` [root] â–¾ - â–¾ - - - â–¸ + â–¾
+ â–¸ + â–¸ `; -exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 4: expand second Parent 1`] = ` +exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 4: expand first Parent 1`] = ` [root] â–¾ - â–¾ - - - â–¾ - - + â–¾
+ â–¾ + â–¸
+ â–¸ `; -exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 5: collapse first Parent 1`] = ` +exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 5: expand second Parent 1`] = ` [root] â–¾ - â–¸ - â–¾ - - + â–¾
+ â–¾ + â–¸
+ â–¾ + â–¸
`; -exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 6: collapse second Parent 1`] = ` +exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 6: collapse first Parent 1`] = ` [root] â–¾ - â–¸ - â–¸ + â–¾
+ â–¸ + â–¾ + â–¸
`; -exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 7: collapse Grandparent 1`] = ` +exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 7: collapse second Parent 1`] = ` +[root] + â–¾ + â–¾
+ â–¸ + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support expanding parts of the tree: 8: collapse Grandparent 1`] = ` [root] â–¸ `; @@ -261,19 +333,17 @@ exports[`Store (legacy) collapseNodesByDefault:true should support mount and upd exports[`Store (legacy) collapseNodesByDefault:true should support mount and update operations: 1: mount 1`] = ` [root] - â–¸ - â–¸ + â–¸
`; exports[`Store (legacy) collapseNodesByDefault:true should support mount and update operations: 2: update 1`] = ` [root] - â–¸ - â–¸ + â–¸
`; exports[`Store (legacy) collapseNodesByDefault:true should support mount and update operations: 3: unmount 1`] = ``; exports[`Store (legacy) should not allow a root node to be collapsed: 1: mount 1`] = ` [root] - + â–¸ `; diff --git a/src/__tests__/legacy/storeLegacy-v15-test.js b/src/__tests__/legacy/storeLegacy-v15-test.js index baa882e538..bc891b3b75 100644 --- a/src/__tests__/legacy/storeLegacy-v15-test.js +++ b/src/__tests__/legacy/storeLegacy-v15-test.js @@ -157,8 +157,8 @@ describe('Store (legacy)', () => { expect(store).toMatchSnapshot('1: mount'); const grandparentID = store.getElementIDAtIndex(0); - const parentOneID = store.getElementIDAtIndex(1); - const parentTwoID = store.getElementIDAtIndex(4); + const parentOneID = store.getElementIDAtIndex(2); + const parentTwoID = store.getElementIDAtIndex(8); act(() => store.toggleIsCollapsed(parentOneID, true)); expect(store).toMatchSnapshot('2: collapse first Parent'); @@ -318,7 +318,7 @@ describe('Store (legacy)', () => { expect(store).toMatchSnapshot('4: unmount A'); }); - it('should filter DOM nodes from the store tree', () => { + it('should not filter DOM nodes from the store tree', () => { const Grandparent = () => (
@@ -346,7 +346,7 @@ describe('Store (legacy)', () => { expect(store).toMatchSnapshot('2: expand Grandparent'); act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false)); - expect(store).toMatchSnapshot('3: expand Parent'); + expect(store).toMatchSnapshot('3: expand div'); }); it('should support expanding parts of the tree', () => { @@ -378,23 +378,27 @@ describe('Store (legacy)', () => { act(() => store.toggleIsCollapsed(grandparentID, false)); expect(store).toMatchSnapshot('2: expand Grandparent'); - const parentOneID = store.getElementIDAtIndex(1); - const parentTwoID = store.getElementIDAtIndex(2); + const parentDivID = store.getElementIDAtIndex(1); + act(() => store.toggleIsCollapsed(parentDivID, false)); + expect(store).toMatchSnapshot('3: expand parent div'); + + const parentOneID = store.getElementIDAtIndex(2); + const parentTwoID = store.getElementIDAtIndex(3); act(() => store.toggleIsCollapsed(parentOneID, false)); - expect(store).toMatchSnapshot('3: expand first Parent'); + expect(store).toMatchSnapshot('4: expand first Parent'); act(() => store.toggleIsCollapsed(parentTwoID, false)); - expect(store).toMatchSnapshot('4: expand second Parent'); + expect(store).toMatchSnapshot('5: expand second Parent'); act(() => store.toggleIsCollapsed(parentOneID, true)); - expect(store).toMatchSnapshot('5: collapse first Parent'); + expect(store).toMatchSnapshot('6: collapse first Parent'); act(() => store.toggleIsCollapsed(parentTwoID, true)); - expect(store).toMatchSnapshot('6: collapse second Parent'); + expect(store).toMatchSnapshot('7: collapse second Parent'); act(() => store.toggleIsCollapsed(grandparentID, true)); - expect(store).toMatchSnapshot('7: collapse Grandparent'); + expect(store).toMatchSnapshot('8: collapse Grandparent'); }); it('should support expanding deep parts of the tree', () => { diff --git a/src/backend/legacy/getData.js b/src/backend/legacy/getData.js index 0695078517..ffc1d775a8 100644 --- a/src/backend/legacy/getData.js +++ b/src/backend/legacy/getData.js @@ -1,6 +1,10 @@ // @flow -import { ElementTypeClass, ElementTypeOtherOrUnknown } from 'src/types'; +import { + ElementTypeClass, + ElementTypeHostComponent, + ElementTypeOtherOrUnknown, +} from 'src/types'; import { getDisplayName } from 'src/utils'; import type { InternalInstance } from './renderer'; @@ -19,7 +23,8 @@ export default function getData(internalInstance: InternalInstance): FiberData { const elementType = internalInstance._currentElement.type; if (typeof elementType === 'string') { - // ... + type = ElementTypeHostComponent; + displayName = elementType; } 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 diff --git a/src/backend/legacy/getElementType.js b/src/backend/legacy/getElementType.js index 94ba337011..10b8c9f68e 100644 --- a/src/backend/legacy/getElementType.js +++ b/src/backend/legacy/getElementType.js @@ -1,5 +1,9 @@ // @flow -import { ElementTypeClass, ElementTypeOtherOrUnknown } from 'src/types'; +import { + ElementTypeClass, + ElementTypeHostComponent, + ElementTypeOtherOrUnknown, +} from 'src/types'; import type { InternalInstance } from './renderer'; import type { ElementType } from 'src/types'; @@ -12,6 +16,8 @@ export default function getElementType( const elementType = internalInstance._currentElement.type; if (typeof elementType === 'function') { return ElementTypeClass; + } else if (typeof elementType === 'string') { + return ElementTypeHostComponent; } } diff --git a/src/backend/legacy/renderer.js b/src/backend/legacy/renderer.js index 1f7edf7d36..2eb20d759f 100644 --- a/src/backend/legacy/renderer.js +++ b/src/backend/legacy/renderer.js @@ -67,10 +67,6 @@ export function attach( return getChildren(internalInstance).map(getID); } - //function getParentID(internalInstance: InternalInstance): number { - // return getID(internalInstance._hostParent); - //} - function findNearestAncestorInTree( internalInstance: InternalInstance ): number | null { @@ -313,7 +309,6 @@ export function attach( ) { const internalInstance = idToInternalInstanceMap.get(id); - // TODO (legacy) Support component filtering const shouldIncludeInTree = parentID === 0 || getElementType(internalInstance) !== ElementTypeOtherOrUnknown; From c3be0859100f065c3b94b3cf43d764cc8a79e05a Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 31 May 2019 17:40:49 +0100 Subject: [PATCH 06/13] Remove crawling for updates It doesn't seem necessary to crawl because mounts only happen in the context of updates. This fixes updates to not be treated as new mounts. --- src/backend/legacy/renderer.js | 167 +++++++++++++++++---------------- src/backend/legacy/utils.js | 4 +- 2 files changed, 87 insertions(+), 84 deletions(-) diff --git a/src/backend/legacy/renderer.js b/src/backend/legacy/renderer.js index 2eb20d759f..0d9491e5da 100644 --- a/src/backend/legacy/renderer.js +++ b/src/backend/legacy/renderer.js @@ -149,15 +149,17 @@ export function attach( renderer.Mount, '_renderNewRootComponent', internalInstance => { - const id = getID(internalInstance); + // TODO: we might need to reset currentParentID before this runs. + const id = getID(internalInstance); rootIDs.add(id); if (__DEBUG__) { console.log('renderer.Mount._renderNewRootComponent()', id); } - recordPendingMount(internalInstance); + // TODO: maybe we need to record this mount. + // Needs testing. // If we're mounting a root, we've just finished a batch of work, // so it's safe to synchronously flush. @@ -171,15 +173,17 @@ export function attach( renderer.Mount, 'renderComponent', internalInstance => { - const id = getID(internalInstance); + // TODO: we might need to reset currentParentID before this runs. + const id = getID(internalInstance); rootIDs.add(id); if (__DEBUG__) { console.log('renderer.Mount.renderComponent()', id); } - recordPendingMount(internalInstance); + // TODO: maybe we need to record this mount. + // Needs testing. // If we're mounting a root, we've just finished a batch of work, // so it's safe to synchronously flush. @@ -188,24 +192,50 @@ export function attach( ); } + // This is shared mutable state that lets us keep track of where we are. + let currentParentID = 0; + if (renderer.Reconciler) { oldReconcilerMethods = decorateMany(renderer.Reconciler, { - mountComponent(internalInstance, rootID, transaction, context) { + mountComponent(fn, args) { + const [internalInstance] = args; + recordPendingMount(internalInstance); + + let prevParentID = currentParentID; + currentParentID = getID(internalInstance); + const result = fn.apply(this, args); + currentParentID = prevParentID; + + return result; }, - performUpdateIfNecessary( - internalInstance, - nextChild, - transaction, - context - ) { - // TODO Check for change in order of children + performUpdateIfNecessary(fn, args) { + const [internalInstance] = args; + + let prevParentID = currentParentID; + currentParentID = getID(internalInstance); + const result = fn.apply(this, args); + currentParentID = prevParentID; + + return result; }, - receiveComponent(internalInstance, nextChild, transaction, context) { - // TODO Check for change in order of children + receiveComponent(fn, args) { + const [internalInstance] = args; + + let prevParentID = currentParentID; + currentParentID = getID(internalInstance); + const result = fn.apply(this, args); + currentParentID = prevParentID; + + return result; }, - unmountComponent(internalInstance) { + unmountComponent(fn, args) { + const [internalInstance] = args; + + const result = fn.apply(this, args); + recordPendingUnmount(internalInstance); + return result; }, }); } @@ -229,7 +259,6 @@ export function attach( oldRenderComponent = null; } - const mountedIDs: Set = new Set(); const pendingMountIDs: Set = new Set(); const pendingUnmountIDs: Set = new Set(); const pendingOperations: Array = []; @@ -295,57 +324,29 @@ export function attach( rootIDs.add(id); - crawlAndRecordMounts(id, 0, true); + crawlAndRecordInitialMounts(id, 0); // It's safe to synchronously flush for the root we just crawled. flushPendingEvents(id); } } - function crawlAndRecordMounts( - id: number, - parentID: number, - isInitialMount: boolean - ) { + // TODO: this isn't covered by tests. + // Might be broken. + function crawlAndRecordInitialMounts(id: number, parentID: number) { 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); - } + console.group('crawlAndRecordInitialMounts() id:', id); } + recordMount(id, parentID); getChildIDs(internalInstance).forEach(childID => - crawlAndRecordMounts( - childID, - shouldIncludeInTree ? id : parentID, - isInitialMount - ) + crawlAndRecordInitialMounts(childID, id) ); if (__DEBUG__) { @@ -354,38 +355,42 @@ export function attach( } function flushPendingEvents(rootID: number): void { - // Crawl tree and record mounts/updates. - crawlAndRecordMounts(rootID, 0, false); - // Record pending deletions. const unmountIDs = []; pendingUnmountIDs.forEach(id => { - if (mountedIDs.has(id)) { - const internalInstance = idToInternalInstanceMap.get(id); - const isRoot = rootIDs.has(id); + 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) { - pendingUnmountedRootID = id; - - rootIDs.delete(id); - } else { - unmountIDs.push(id); - } - - idToInternalInstanceMap.delete(id); - internalInstanceToIDMap.delete(internalInstance); - - mountedIDs.delete(id); + if (__DEBUG__) { + console.log( + '%crecordUnmount()', + 'color: red; font-weight: bold;', + id, + getData(internalInstance).displayName + ); } + + // TODO: handle the case where it was never mounted. + if (isRoot) { + pendingUnmountedRootID = id; + rootIDs.delete(id); + } else { + unmountIDs.push(id); + } + + idToInternalInstanceMap.delete(id); + internalInstanceToIDMap.delete(internalInstance); + }); + + pendingMountIDs.forEach(id => { + if (pendingUnmountIDs.has(id)) { + return; + } + const parentID = idToParentIDMap.get(id); + if (parentID === undefined) { + return; + } + recordMount(id, parentID); }); const numUnmountIDs = @@ -632,7 +637,9 @@ export function attach( } function recordPendingMount(internalInstance: InternalInstance) { - pendingMountIDs.add(getID(internalInstance)); + const id = getID(internalInstance); + pendingMountIDs.add(id); + idToParentIDMap.set(id, currentParentID); if (__DEBUG__) { console.log( @@ -680,8 +687,6 @@ export function attach( ); } - mountedIDs.add(id); - if (isRoot) { // TODO Is this right? For all versions? const hasOwnerMetadata = diff --git a/src/backend/legacy/utils.js b/src/backend/legacy/utils.js index 49a67888a2..bee9bbae06 100644 --- a/src/backend/legacy/utils.js +++ b/src/backend/legacy/utils.js @@ -19,9 +19,7 @@ export function decorateResult( 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 fn.call(this, old, arguments); }; return old; } From 2794b92164058f38683cad49f04ff801e04ff34b Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 31 May 2019 18:00:56 +0100 Subject: [PATCH 07/13] Add some support for reordering --- .../storeLegacy-v15-test.js.snap | 79 +++++++++++++++++ src/__tests__/legacy/storeLegacy-v15-test.js | 13 ++- src/backend/legacy/renderer.js | 85 ++++++++++++++++++- 3 files changed, 166 insertions(+), 11 deletions(-) diff --git a/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap b/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap index 37747be9c6..05e853c2ee 100644 --- a/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap +++ b/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap @@ -15,6 +15,32 @@ exports[`Store (legacy) collapseNodesByDefault:false should filter DOM nodes fro
`; +exports[`Store (legacy) collapseNodesByDefault:false should support adding and removing children: 1: mount 1`] = ` +[root] + â–¾ + â–¾
+ â–¾ +
+`; + +exports[`Store (legacy) collapseNodesByDefault:false should support adding and removing children: 2: add child 1`] = ` +[root] + â–¾ + â–¾
+ â–¾ +
+ â–¾ +
+`; + +exports[`Store (legacy) collapseNodesByDefault:false should support adding and removing children: 3: remove child 1`] = ` +[root] + â–¾ + â–¾
+ â–¾ +
+`; + exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 1: mount 1`] = ` [root] â–¾ @@ -185,6 +211,59 @@ exports[`Store (legacy) collapseNodesByDefault:false should support mount and up exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations: 3: unmount 1`] = ``; +exports[`Store (legacy) collapseNodesByDefault:false should support reordering of children: 1: mount 1`] = ` +[root] + â–¾ + â–¾
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ +
+`; + +exports[`Store (legacy) collapseNodesByDefault:false should support reordering of children: 2: reorder children 1`] = ` +[root] + â–¾ + â–¾
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ +
+ â–¾ + â–¾
+ â–¾ +
+`; + +exports[`Store (legacy) collapseNodesByDefault:false should support reordering of children: 3: collapse root 1`] = ` +[root] + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:false should support reordering of children: 4: expand root 1`] = ` +[root] + â–¾ + â–¾
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ +
+ â–¾ + â–¾
+ â–¾ +
+`; + exports[`Store (legacy) collapseNodesByDefault:true should not filter DOM nodes from the store tree: 1: mount 1`] = ` [root] â–¸ diff --git a/src/__tests__/legacy/storeLegacy-v15-test.js b/src/__tests__/legacy/storeLegacy-v15-test.js index bc891b3b75..96c8251d77 100644 --- a/src/__tests__/legacy/storeLegacy-v15-test.js +++ b/src/__tests__/legacy/storeLegacy-v15-test.js @@ -176,10 +176,9 @@ describe('Store (legacy)', () => { expect(store).toMatchSnapshot('6: expand Grandparent'); }); - // TODO Re-enable this test once the renderer supports it. - xit('should support adding and removing children', () => { + it('should support adding and removing children', () => { const Root = ({ children }) =>
{children}
; - const Component = () => null; + const Component = () =>
; const container = document.createElement('div'); @@ -215,10 +214,9 @@ describe('Store (legacy)', () => { expect(store).toMatchSnapshot('3: remove child'); }); - // TODO Re-enable this test once the renderer supports it. - xit('should support reordering of children', () => { + it('should support reordering of children', () => { const Root = ({ children }) =>
{children}
; - const Component = () => null; + const Component = () =>
; const Foo = () =>
{[]}
; const Bar = () => ( @@ -447,10 +445,9 @@ describe('Store (legacy)', () => { expect(store).toMatchSnapshot('6: expand middle node'); }); - // TODO Re-enable this test once the renderer supports it. xit('should support reordering of children', () => { const Root = ({ children }) =>
{children}
; - const Component = () => null; + const Component = () =>
; const Foo = () =>
{[]}
; const Bar = () => ( diff --git a/src/backend/legacy/renderer.js b/src/backend/legacy/renderer.js index 0d9491e5da..2e6d65190c 100644 --- a/src/backend/legacy/renderer.js +++ b/src/backend/legacy/renderer.js @@ -12,6 +12,7 @@ import { __DEBUG__, TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, + TREE_OPERATION_REORDER_CHILDREN, } from '../../constants'; import getChildren from './getChildren'; import getData from './getData'; @@ -49,6 +50,10 @@ export function attach( const idToInternalInstanceMap: Map = new Map(); const idToParentIDMap: Map = new Map(); const internalInstanceToIDMap: Map = new Map(); + const internalInstanceToLastKnownChildrenMap: WeakMap< + InternalInstance, + Array + > = new WeakMap(); const rootIDs: Set = new Set(); function getID(internalInstance: InternalInstance): number { @@ -217,6 +222,7 @@ export function attach( const result = fn.apply(this, args); currentParentID = prevParentID; + recordPendingReorder(internalInstance); return result; }, receiveComponent(fn, args) { @@ -227,6 +233,7 @@ export function attach( const result = fn.apply(this, args); currentParentID = prevParentID; + recordPendingReorder(internalInstance); return result; }, unmountComponent(fn, args) { @@ -261,6 +268,7 @@ export function attach( const pendingMountIDs: Set = new Set(); const pendingUnmountIDs: Set = new Set(); + const pendingReorderIDs: Set = new Set(); const pendingOperations: Array = []; const pendingStringTable: Map = new Map(); let pendingStringTableLength: number = 0; @@ -303,7 +311,11 @@ export function attach( // 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) { + if ( + pendingMountIDs.size > 0 || + pendingUnmountIDs.size > 0 || + pendingReorderIDs.size > 0 + ) { rootIDs.forEach(flushPendingEvents); } }, 0); @@ -396,6 +408,7 @@ export function attach( const numUnmountIDs = unmountIDs.length + (pendingUnmountedRootID === null ? 0 : 1); + const reorderOperations = computePendingReorderOperations(); const operations = new Uint32Array( // Identify which renderer this update is coming from. 2 + // [rendererID, rootFiberID] @@ -406,8 +419,12 @@ export function attach( // All unmounts are batched in a single message. // [TREE_OPERATION_REMOVE, removedIDLength, ...ids] (numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) + - // Mount/update/reorder operations - pendingOperations.length + // Mount operations + pendingOperations.length + + // Reorder operation come last because + // bridge expects them to not change children length. + // So both mounts and unmounts need to have happened by now. + reorderOperations.length ); // Identify which renderer this update is coming from. @@ -444,6 +461,9 @@ export function attach( // Fill in the rest of the operations. operations.set(pendingOperations, i); + i += pendingOperations.length; + + operations.set(reorderOperations, i); if (__DEBUG__) { printOperationsArray(operations); @@ -454,12 +474,60 @@ export function attach( pendingOperations.length = 0; pendingMountIDs.clear(); + pendingReorderIDs.clear(); pendingUnmountIDs.clear(); pendingUnmountedRootID = null; pendingStringTable.clear(); pendingStringTableLength = 0; } + function computePendingReorderOperations(): Array { + const ops = []; + pendingReorderIDs.forEach(id => { + const internalInstance = idToInternalInstanceMap.get(id); + if (internalInstance === undefined) { + return; + } + const prevChildIDs = internalInstanceToLastKnownChildrenMap.get( + internalInstance + ); + const nextChildIDs = getChildIDs(internalInstance); + internalInstanceToLastKnownChildrenMap.set( + internalInstance, + nextChildIDs + ); + + let shouldResetChildren = false; + if (prevChildIDs === undefined) { + // We haven't computed children before. + // So we'll have to do it now. + // Next time they'll be cached for comparison + // TODO: this might not make sense. Revisit. + shouldResetChildren = true; + } else if (nextChildIDs.length > 1) { + if (prevChildIDs.length !== nextChildIDs.length) { + shouldResetChildren = true; + } else { + for (let i = 0; i < prevChildIDs.length; i++) { + if (prevChildIDs[i] !== nextChildIDs[i]) { + shouldResetChildren = true; + break; + } + } + } + } + if (shouldResetChildren) { + ops.push(TREE_OPERATION_REORDER_CHILDREN); + ops.push(getID(internalInstance)); + ops.push(nextChildIDs.length); + for (let i = 0; i < nextChildIDs.length; i++) { + ops.push(nextChildIDs[i]); + } + } + }); + return ops; + } + function getStringID(str: string | null): number { if (str === null) { return 0; @@ -652,6 +720,17 @@ export function attach( queueFlushPendingEvents(); } + function recordPendingReorder(internalInstance: InternalInstance) { + const id = getID(internalInstance); + pendingReorderIDs.add(id); + + if (__DEBUG__) { + console.log('%crecordPendingReorder()', 'color: green', id); + } + + queueFlushPendingEvents(); + } + function recordPendingUnmount(internalInstance: InternalInstance) { const id = getID(internalInstance); From 9243bc194c240d3f06ccd5e4333f3488f2628e2f Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Jun 2019 17:21:30 -0700 Subject: [PATCH 08/13] Rewrite to make it sturdier --- .../storeLegacy-v15-test.js.snap | 72 +- src/__tests__/legacy/storeLegacy-v15-test.js | 63 +- src/backend/legacy/getChildren.js | 45 -- src/backend/legacy/getElementType.js | 25 - src/backend/legacy/renderer.js | 722 +++++++----------- src/backend/legacy/utils.js | 14 - 6 files changed, 403 insertions(+), 538 deletions(-) delete mode 100644 src/backend/legacy/getChildren.js delete mode 100644 src/backend/legacy/getElementType.js diff --git a/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap b/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap index 05e853c2ee..d43112d058 100644 --- a/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap +++ b/src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Store (legacy) collapseNodesByDefault:false should filter DOM nodes from the store tree: 1: mount 1`] = ` +exports[`Store (legacy) collapseNodesByDefault:false should not filter DOM nodes from the store tree: 1: mount 1`] = ` [root] â–¾ â–¾
@@ -13,8 +13,27 @@ exports[`Store (legacy) collapseNodesByDefault:false should filter DOM nodes fro â–¾
â–¾
+ `; +exports[`Store (legacy) collapseNodesByDefault:false should not filter DOM nodes from the store tree: 2: update 1`] = ` +[root] + â–¾ + â–¾
+ â–¾
+ â–¾ + â–¾
+ â–¾ +
+ â–¾ + â–¾
+ â–¾ +
+ +`; + +exports[`Store (legacy) collapseNodesByDefault:false should not filter DOM nodes from the store tree: 5: unmount 1`] = ``; + exports[`Store (legacy) collapseNodesByDefault:false should support adding and removing children: 1: mount 1`] = ` [root] â–¾ @@ -281,8 +300,20 @@ exports[`Store (legacy) collapseNodesByDefault:true should not filter DOM nodes â–¾
â–¸
â–¸ + `; +exports[`Store (legacy) collapseNodesByDefault:true should not filter DOM nodes from the store tree: 4: final update 1`] = ` +[root] + â–¾ + â–¾
+ â–¸
+ â–¸ + +`; + +exports[`Store (legacy) collapseNodesByDefault:true should not filter DOM nodes from the store tree: 5: unmount 1`] = ``; + exports[`Store (legacy) collapseNodesByDefault:true should support expanding deep parts of the tree: 1: mount 1`] = ` [root] â–¸ @@ -422,6 +453,45 @@ exports[`Store (legacy) collapseNodesByDefault:true should support mount and upd exports[`Store (legacy) collapseNodesByDefault:true should support mount and update operations: 3: unmount 1`] = ``; +exports[`Store (legacy) collapseNodesByDefault:true should support reordering of children: 1: mount 1`] = ` +[root] + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support reordering of children: 2: reorder children 1`] = ` +[root] + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support reordering of children: 3: expand root 1`] = ` +[root] + â–¾ + â–¸
+`; + +exports[`Store (legacy) collapseNodesByDefault:true should support reordering of children: 4: expand div 1`] = ` +[root] + â–¾ + â–¾
+ â–¸ + â–¸ +`; + +exports[`Store (legacy) collapseNodesByDefault:true should support reordering of children: 4: expand leaves 1`] = ` +[root] + â–¾ + â–¾
+ â–¾ + â–¸
+ â–¾ + â–¸
+`; + +exports[`Store (legacy) collapseNodesByDefault:true should support reordering of children: 5: collapse root 1`] = ` +[root] + â–¸ +`; + exports[`Store (legacy) should not allow a root node to be collapsed: 1: mount 1`] = ` [root] â–¸ diff --git a/src/__tests__/legacy/storeLegacy-v15-test.js b/src/__tests__/legacy/storeLegacy-v15-test.js index 96c8251d77..fc5d373e5b 100644 --- a/src/__tests__/legacy/storeLegacy-v15-test.js +++ b/src/__tests__/legacy/storeLegacy-v15-test.js @@ -107,29 +107,40 @@ describe('Store (legacy)', () => { expect(store).toMatchSnapshot('4: unmount A'); }); - it('should filter DOM nodes from the store tree', () => { - const Grandparent = () => ( + it('should not filter DOM nodes from the store tree', () => { + const Grandparent = ({ flip }) => (
- +
- + +
); - const Parent = () => ( + const Parent = ({ flip }) => (
+ {flip ? 'foo' : null} + {flip && [null, 'hello', 42]} + {flip ? 'bar' : 'baz'}
); const Child = () =>
Hi!
; + const Nothing = () => null; + const container = document.createElement('div'); act(() => - ReactDOM.render( - , - document.createElement('div') - ) + ReactDOM.render(, container) ); expect(store).toMatchSnapshot('1: mount'); + + act(() => + ReactDOM.render(, container) + ); + expect(store).toMatchSnapshot('2: update'); + + act(() => ReactDOM.unmountComponentAtNode(container)); + expect(store).toMatchSnapshot('5: unmount'); }); it('should support collapsing parts of the tree', () => { @@ -317,26 +328,29 @@ describe('Store (legacy)', () => { }); it('should not filter DOM nodes from the store tree', () => { - const Grandparent = () => ( + const Grandparent = ({ flip }) => (
- +
- + +
); - const Parent = () => ( + const Parent = ({ flip }) => (
+ {flip ? 'foo' : null} + {flip && [null, 'hello', 42]} + {flip ? 'bar' : 'baz'}
); const Child = () =>
Hi!
; + const Nothing = () => null; + const container = document.createElement('div'); act(() => - ReactDOM.render( - , - document.createElement('div') - ) + ReactDOM.render(, container) ); expect(store).toMatchSnapshot('1: mount'); @@ -345,6 +359,14 @@ describe('Store (legacy)', () => { act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false)); expect(store).toMatchSnapshot('3: expand div'); + + act(() => + ReactDOM.render(, container) + ); + expect(store).toMatchSnapshot('4: final update'); + + act(() => ReactDOM.unmountComponentAtNode(container)); + expect(store).toMatchSnapshot('5: unmount'); }); it('should support expanding parts of the tree', () => { @@ -445,7 +467,7 @@ describe('Store (legacy)', () => { expect(store).toMatchSnapshot('6: expand middle node'); }); - xit('should support reordering of children', () => { + it('should support reordering of children', () => { const Root = ({ children }) =>
{children}
; const Component = () =>
; @@ -467,9 +489,12 @@ describe('Store (legacy)', () => { act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false)); expect(store).toMatchSnapshot('3: expand root'); + act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false)); + expect(store).toMatchSnapshot('4: expand div'); + act(() => { + store.toggleIsCollapsed(store.getElementIDAtIndex(3), false); store.toggleIsCollapsed(store.getElementIDAtIndex(2), false); - store.toggleIsCollapsed(store.getElementIDAtIndex(1), false); }); expect(store).toMatchSnapshot('4: expand leaves'); diff --git a/src/backend/legacy/getChildren.js b/src/backend/legacy/getChildren.js deleted file mode 100644 index 0f10b5f282..0000000000 --- a/src/backend/legacy/getChildren.js +++ /dev/null @@ -1,45 +0,0 @@ -// @flow - -// TODO (legacy) Respect component filters - -export default function getChildren(internalInstance: Object): Array { - let children = []; - - // 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 internalInstance !== 'object') { - // No children - } else if ( - internalInstance._currentElement === null || - internalInstance._currentElement === false - ) { - // No children - } else if (internalInstance._renderedComponent) { - children = [internalInstance._renderedComponent]; - } else if (internalInstance._renderedChildren) { - children = renderedChildrenToArray(internalInstance._renderedChildren); - } - // Note: we skip the case where children are just strings or numbers - // because the new DevTools skips over host text nodes anyway. - - const instance = internalInstance._instance; - if (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 (instance._renderedChildren) { - children = renderedChildrenToArray(instance._renderedChildren); - } - } - - return children; -} - -function renderedChildrenToArray(renderedChildren): Array { - const childrenArray = []; - for (let name in renderedChildren) { - childrenArray.push(renderedChildren[name]); - } - return childrenArray; -} diff --git a/src/backend/legacy/getElementType.js b/src/backend/legacy/getElementType.js deleted file mode 100644 index 10b8c9f68e..0000000000 --- a/src/backend/legacy/getElementType.js +++ /dev/null @@ -1,25 +0,0 @@ -// @flow -import { - ElementTypeClass, - ElementTypeHostComponent, - ElementTypeOtherOrUnknown, -} from 'src/types'; - -import type { InternalInstance } from './renderer'; -import type { ElementType } from 'src/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; - } else if (typeof elementType === 'string') { - return ElementTypeHostComponent; - } - } - - return ElementTypeOtherOrUnknown; -} diff --git a/src/backend/legacy/renderer.js b/src/backend/legacy/renderer.js index 2e6d65190c..f22f617ed9 100644 --- a/src/backend/legacy/renderer.js +++ b/src/backend/legacy/renderer.js @@ -4,6 +4,7 @@ import { ElementTypeClass, ElementTypeFunction, ElementTypeRoot, + ElementTypeHostComponent, ElementTypeOtherOrUnknown, } from 'src/types'; import { getUID, utfEncodeString, printOperationsArray } from '../../utils'; @@ -14,15 +15,8 @@ import { TREE_OPERATION_REMOVE, TREE_OPERATION_REORDER_CHILDREN, } from '../../constants'; -import getChildren from './getChildren'; import getData from './getData'; -import getElementType from './getElementType'; -import { - decorateResult, - decorateMany, - forceUpdate, - restoreMany, -} from './utils'; +import { decorateMany, forceUpdate, restoreMany } from './utils'; import type { DevToolsHook, @@ -32,7 +26,7 @@ import type { PathMatch, RendererInterface, } from '../types'; -import type { ComponentFilter } from 'src/types'; +import type { ComponentFilter, ElementType } from 'src/types'; import type { InspectedElement, Owner, @@ -41,6 +35,51 @@ import type { export type InternalInstance = Object; type LegacyRenderer = Object; +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; + } else if (typeof elementType === 'string') { + return ElementTypeHostComponent; + } + } + return ElementTypeOtherOrUnknown; +} + +function getChildren(internalInstance: Object): Array { + let children = []; + + // 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 internalInstance !== 'object') { + // No children + } else if ( + internalInstance._currentElement === null || + internalInstance._currentElement === false + ) { + // No children + } else if (internalInstance._renderedComponent) { + const child = internalInstance._renderedComponent; + if (getElementType(child) !== ElementTypeOtherOrUnknown) { + children.push(child); + } + } else if (internalInstance._renderedChildren) { + const renderedChildren = internalInstance._renderedChildren; + for (let name in renderedChildren) { + const child = renderedChildren[name]; + if (getElementType(child) !== ElementTypeOtherOrUnknown) { + children.push(child); + } + } + } + // Note: we skip the case where children are just strings or numbers + // because the new DevTools skips over host text nodes anyway. + return children; +} + export function attach( hook: DevToolsHook, rendererID: number, @@ -48,13 +87,32 @@ export function attach( global: Object ): RendererInterface { const idToInternalInstanceMap: Map = new Map(); - const idToParentIDMap: Map = new Map(); const internalInstanceToIDMap: Map = new Map(); - const internalInstanceToLastKnownChildrenMap: WeakMap< - InternalInstance, - Array - > = new WeakMap(); - const rootIDs: Set = new Set(); + + let getInternalIDForNative: GetFiberIDForNative = ((null: any): GetFiberIDForNative); + let findNativeNodeForInternalID: (id: number) => ?NativeType; + + if (renderer.ComponentTree) { + getInternalIDForNative = (node, findNearestUnfilteredAncestor) => { + const internalInstance = renderer.ComponentTree.getClosestInstanceFromNode( + node + ); + return internalInstanceToIDMap.get(internalInstance) || null; + }; + findNativeNodeForInternalID = (id: number) => { + const internalInstance = idToInternalInstanceMap.get(id); + return renderer.ComponentTree.getNodeFromInstance(internalInstance); + }; + } else if (renderer.Mount.getID && renderer.Mount.getNode) { + getInternalIDForNative = (node, findNearestUnfilteredAncestor) => { + // Not implemented. + return null; + }; + findNativeNodeForInternalID = (id: number) => { + // Not implemented. + return null; + }; + } function getID(internalInstance: InternalInstance): number { if (typeof internalInstance !== 'object') { @@ -68,181 +126,147 @@ export function attach( return ((internalInstanceToIDMap.get(internalInstance): any): number); } - function getChildIDs(internalInstance: InternalInstance): Array { - return getChildren(internalInstance).map(getID); - } - - 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; + function areEqualArrays(a, b) { + if (a.length !== b.length) { + return false; } - return null; - } - - let getInternalIDForNative: GetFiberIDForNative = ((null: any): GetFiberIDForNative); - let findNativeNodeForInternalID: (id: number) => ?NativeType; - - // React Native - if (renderer.Mount.findNodeHandle && renderer.Mount.nativeTagToRootNodeID) { - getInternalIDForNative = (nativeTag, findNearestUnfilteredAncestor) => { - const internalInstance = renderer.Mount.nativeTagToRootNodeID(nativeTag); - return findNearestAncestorInTree(internalInstance); - }; - findNativeNodeForInternalID = (id: number) => { - const internalInstance = idToInternalInstanceMap.get(id); - return renderer.Mount.findNodeHandle(internalInstance); - }; - - // React DOM 15+ - } else if (renderer.ComponentTree) { - getInternalIDForNative = (node, findNearestUnfilteredAncestor) => { - const internalInstance = renderer.ComponentTree.getClosestInstanceFromNode( - node - ); - return findNearestAncestorInTree(internalInstance); - }; - findNativeNodeForInternalID = (id: number) => { - const internalInstance = idToInternalInstanceMap.get(id); - return renderer.ComponentTree.getNodeFromInstance(internalInstance); - }; - - // React DOM - } else if (renderer.Mount.getID && renderer.Mount.getNode) { - getInternalIDForNative = (node, findNearestUnfilteredAncestor) => { - let id = renderer.Mount.getID(node); - while (node && node.parentNode && !id) { - node = node.parentNode; - id = renderer.Mount.getID(node); + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) { + return false; } - return id; - }; - - findNativeNodeForInternalID = (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 => { - // TODO: we might need to reset currentParentID before this runs. - - const id = getID(internalInstance); - rootIDs.add(id); - - if (__DEBUG__) { - console.log('renderer.Mount._renderNewRootComponent()', id); - } - - // TODO: maybe we need to record this mount. - // Needs testing. - - // 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 => { - // TODO: we might need to reset currentParentID before this runs. - - const id = getID(internalInstance); - rootIDs.add(id); - - if (__DEBUG__) { - console.log('renderer.Mount.renderComponent()', id); - } - - // TODO: maybe we need to record this mount. - // Needs testing. - - // If we're mounting a root, we've just finished a batch of work, - // so it's safe to synchronously flush. - flushPendingEvents(id); - } - ); + } + return true; } // This is shared mutable state that lets us keep track of where we are. - let currentParentID = 0; + let parentIDStack = []; + let oldReconcilerMethods = null; if (renderer.Reconciler) { oldReconcilerMethods = decorateMany(renderer.Reconciler, { mountComponent(fn, args) { const [internalInstance] = args; + if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) { + return fn.apply(this, args); + } - recordPendingMount(internalInstance); + const id = getID(internalInstance); - let prevParentID = currentParentID; - currentParentID = getID(internalInstance); - const result = fn.apply(this, args); - currentParentID = prevParentID; + // Push the operation. + const parentID = + parentIDStack.length > 0 + ? parentIDStack[parentIDStack.length - 1] + : 0; + recordMount(internalInstance, id, parentID); + parentIDStack.push(id); - return result; + try { + const result = fn.apply(this, args); + parentIDStack.pop(); + return result; + } catch (err) { + parentIDStack = []; + throw err; + } finally { + if (parentIDStack.length === 0) { + flushPendingEvents(id); + } + } }, performUpdateIfNecessary(fn, args) { const [internalInstance] = args; + if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) { + return fn.apply(this, args); + } - let prevParentID = currentParentID; - currentParentID = getID(internalInstance); - const result = fn.apply(this, args); - currentParentID = prevParentID; + const id = getID(internalInstance); + parentIDStack.push(id); - recordPendingReorder(internalInstance); - return result; + const prevChildren = getChildren(internalInstance); + try { + const result = fn.apply(this, args); + + const nextChildren = getChildren(internalInstance); + if (!areEqualArrays(prevChildren, nextChildren)) { + // Push the operation + recordReorder(internalInstance, id, nextChildren); + } + + parentIDStack.pop(); + return result; + } catch (err) { + parentIDStack = []; + throw err; + } finally { + if (parentIDStack.length === 0) { + // TODO: this is probably wrong! + const rootID = + internalInstance._topLevelWrapper === null + ? id + : getID(internalInstance._topLevelWrapper); + flushPendingEvents(rootID); + } + } }, receiveComponent(fn, args) { const [internalInstance] = args; + if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) { + return fn.apply(this, args); + } - let prevParentID = currentParentID; - currentParentID = getID(internalInstance); - const result = fn.apply(this, args); - currentParentID = prevParentID; + const id = getID(internalInstance); + parentIDStack.push(id); - recordPendingReorder(internalInstance); - return result; + const prevChildren = getChildren(internalInstance); + try { + const result = fn.apply(this, args); + + const nextChildren = getChildren(internalInstance); + if (!areEqualArrays(prevChildren, nextChildren)) { + // Push the operation + recordReorder(internalInstance, id, nextChildren); + } + + parentIDStack.pop(); + return result; + } catch (err) { + parentIDStack = []; + throw err; + } finally { + if (parentIDStack.length === 0) { + // TODO: this is probably wrong! + const rootID = + internalInstance._topLevelWrapper === null + ? id + : getID(internalInstance._topLevelWrapper); + flushPendingEvents(rootID); + } + } }, unmountComponent(fn, args) { const [internalInstance] = args; + if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) { + return fn.apply(this, args); + } - const result = fn.apply(this, args); + const id = getID(internalInstance); + parentIDStack.push(id); + try { + const result = fn.apply(this, args); + parentIDStack.pop(); - recordPendingUnmount(internalInstance); - return result; + // Push the operation. + recordUnmount(internalInstance, id); + + return result; + } catch (err) { + parentIDStack = []; + throw err; + } finally { + if (parentIDStack.length === 0) { + flushPendingEvents(id); + } + } }, }); } @@ -255,75 +279,95 @@ export function attach( restoreMany(renderer.Reconciler, oldReconcilerMethods); } } - if (oldRenderRoot !== null) { - renderer.Mount._renderNewRootComponent = oldRenderRoot; - } - if (oldRenderComponent !== null) { - renderer.Mount.renderComponent = oldRenderComponent; - } oldReconcilerMethods = null; - oldRenderRoot = null; - oldRenderComponent = null; } - const pendingMountIDs: Set = new Set(); - const pendingUnmountIDs: Set = new Set(); - const pendingReorderIDs: Set = new Set(); - const pendingOperations: Array = []; - const pendingStringTable: Map = new Map(); - let pendingStringTableLength: number = 0; - let pendingUnmountedRootID: number | null = null; + function recordMount( + internalInstance: InternalInstance, + id: number, + parentID: number + ) { + const isRoot = parentID === 0; - function pushOperation(op: number): void { - if (__DEV__) { - if (!Number.isInteger(op)) { - console.error( - 'pushOperation() was called but the value is not an integer.', - op - ); - } + if (__DEBUG__) { + console.log( + '%crecordMount()', + 'color: green; font-weight: bold;', + id, + getData(internalInstance).displayName + ); + } + + if (isRoot) { + // TODO Is this right? For all versions? + const hasOwnerMetadata = + internalInstance._currentElement != null && + internalInstance._currentElement._owner != null; + + pushOperation(TREE_OPERATION_ADD); + pushOperation(id); + pushOperation(ElementTypeRoot); + pushOperation(0); // isProfilingSupported? + pushOperation(hasOwnerMetadata ? 1 : 0); + } else { + const { displayName, key, type } = getData(internalInstance); + + const ownerID = + internalInstance._currentElement != null && + internalInstance._currentElement._owner != null + ? getID(internalInstance._currentElement._owner) + : 0; + + let displayNameStringID = getStringID(displayName); + let keyStringID = getStringID(key); + pushOperation(TREE_OPERATION_ADD); + pushOperation(id); + pushOperation(type); + pushOperation(parentID); + pushOperation(ownerID); + pushOperation(displayNameStringID); + pushOperation(keyStringID); } - pendingOperations.push(op); } - // 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. + function recordReorder( + internalInstance: InternalInstance, + id: number, + nextChildren: Array + ) { + pushOperation(TREE_OPERATION_REORDER_CHILDREN); + pushOperation(id); + const nextChildIDs = nextChildren.map(getID); + pushOperation(nextChildIDs.length); + for (let i = 0; i < nextChildIDs.length; i++) { + pushOperation(nextChildIDs[i]); + } + } - // 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; + function recordUnmount(internalInstance: InternalInstance, id: number) { + pendingUnmountedIDs.push(id); + internalInstanceToIDMap.delete(internalInstance); + idToInternalInstanceMap.delete(id); + } - // 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 || - pendingReorderIDs.size > 0 - ) { - rootIDs.forEach(flushPendingEvents); - } - }, 0); + function crawlAndRecordInitialMounts(id: number, parentID: number) { + const internalInstance = idToInternalInstanceMap.get(id); + + if (__DEBUG__) { + console.group('crawlAndRecordInitialMounts() id:', id); + } + + recordMount(internalInstance, id, parentID); + getChildren(internalInstance).forEach(child => + crawlAndRecordInitialMounts(getID(child), id) + ); + + if (__DEBUG__) { + console.groupEnd(); } } 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 = @@ -333,82 +377,29 @@ export function attach( for (let key in roots) { const internalInstance = roots[key]; const id = getID(internalInstance); - - rootIDs.add(id); - crawlAndRecordInitialMounts(id, 0); - - // It's safe to synchronously flush for the root we just crawled. flushPendingEvents(id); } } - // TODO: this isn't covered by tests. - // Might be broken. - function crawlAndRecordInitialMounts(id: number, parentID: number) { - const internalInstance = idToInternalInstanceMap.get(id); + let pendingOperations: Array = []; + let pendingStringTable: Map = new Map(); + let pendingUnmountedIDs: Array = []; + let pendingStringTableLength: number = 0; + let pendingUnmountedRootID: number | null = null; - // 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('crawlAndRecordInitialMounts() id:', id); + function flushPendingEvents(rootID: number) { + if ( + pendingOperations.length === 0 && + pendingUnmountedIDs.length === 0 && + pendingUnmountedRootID === null + ) { + return; } - recordMount(id, parentID); - getChildIDs(internalInstance).forEach(childID => - crawlAndRecordInitialMounts(childID, id) - ); - - if (__DEBUG__) { - console.groupEnd(); - } - } - - function flushPendingEvents(rootID: number): void { - // Record pending deletions. - const unmountIDs = []; - pendingUnmountIDs.forEach(id => { - const internalInstance = idToInternalInstanceMap.get(id); - const isRoot = rootIDs.has(id); - - if (__DEBUG__) { - console.log( - '%crecordUnmount()', - 'color: red; font-weight: bold;', - id, - getData(internalInstance).displayName - ); - } - - // TODO: handle the case where it was never mounted. - if (isRoot) { - pendingUnmountedRootID = id; - rootIDs.delete(id); - } else { - unmountIDs.push(id); - } - - idToInternalInstanceMap.delete(id); - internalInstanceToIDMap.delete(internalInstance); - }); - - pendingMountIDs.forEach(id => { - if (pendingUnmountIDs.has(id)) { - return; - } - const parentID = idToParentIDMap.get(id); - if (parentID === undefined) { - return; - } - recordMount(id, parentID); - }); - const numUnmountIDs = - unmountIDs.length + (pendingUnmountedRootID === null ? 0 : 1); + pendingUnmountedIDs.length + (pendingUnmountedRootID === null ? 0 : 1); - const reorderOperations = computePendingReorderOperations(); const operations = new Uint32Array( // Identify which renderer this update is coming from. 2 + // [rendererID, rootFiberID] @@ -420,11 +411,7 @@ export function attach( // [TREE_OPERATION_REMOVE, removedIDLength, ...ids] (numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) + // Mount operations - pendingOperations.length + - // Reorder operation come last because - // bridge expects them to not change children length. - // So both mounts and unmounts need to have happened by now. - reorderOperations.length + pendingOperations.length ); // Identify which renderer this update is coming from. @@ -449,8 +436,8 @@ export function attach( // The first number is how many unmounted IDs we're gonna send. operations[i++] = numUnmountIDs; // Fill in the unmounts - for (let j = 0; j < unmountIDs.length; j++) { - operations[i++] = unmountIDs[j]; + for (let j = 0; j < pendingUnmountedIDs.length; j++) { + operations[i++] = pendingUnmountedIDs[j]; } // The root ID should always be unmounted last. if (pendingUnmountedRootID !== null) { @@ -463,8 +450,6 @@ export function attach( operations.set(pendingOperations, i); i += pendingOperations.length; - operations.set(reorderOperations, i); - if (__DEBUG__) { printOperationsArray(operations); } @@ -473,59 +458,22 @@ export function attach( hook.emit('operations', operations); pendingOperations.length = 0; - pendingMountIDs.clear(); - pendingReorderIDs.clear(); - pendingUnmountIDs.clear(); + pendingUnmountedIDs = []; pendingUnmountedRootID = null; pendingStringTable.clear(); pendingStringTableLength = 0; } - function computePendingReorderOperations(): Array { - const ops = []; - pendingReorderIDs.forEach(id => { - const internalInstance = idToInternalInstanceMap.get(id); - if (internalInstance === undefined) { - return; + function pushOperation(op: number): void { + if (__DEV__) { + if (!Number.isInteger(op)) { + console.error( + 'pushOperation() was called but the value is not an integer.', + op + ); } - const prevChildIDs = internalInstanceToLastKnownChildrenMap.get( - internalInstance - ); - const nextChildIDs = getChildIDs(internalInstance); - internalInstanceToLastKnownChildrenMap.set( - internalInstance, - nextChildIDs - ); - - let shouldResetChildren = false; - if (prevChildIDs === undefined) { - // We haven't computed children before. - // So we'll have to do it now. - // Next time they'll be cached for comparison - // TODO: this might not make sense. Revisit. - shouldResetChildren = true; - } else if (nextChildIDs.length > 1) { - if (prevChildIDs.length !== nextChildIDs.length) { - shouldResetChildren = true; - } else { - for (let i = 0; i < prevChildIDs.length; i++) { - if (prevChildIDs[i] !== nextChildIDs[i]) { - shouldResetChildren = true; - break; - } - } - } - } - if (shouldResetChildren) { - ops.push(TREE_OPERATION_REORDER_CHILDREN); - ops.push(getID(internalInstance)); - ops.push(nextChildIDs.length); - for (let i = 0; i < nextChildIDs.length; i++) { - ops.push(nextChildIDs[i]); - } - } - }); - return ops; + } + pendingOperations.push(op); } function getStringID(str: string | null): number { @@ -704,100 +652,6 @@ export function attach( } } - function recordPendingMount(internalInstance: InternalInstance) { - const id = getID(internalInstance); - pendingMountIDs.add(id); - idToParentIDMap.set(id, currentParentID); - - if (__DEBUG__) { - console.log( - '%crecordPendingMount()', - 'color: green', - getID(internalInstance) - ); - } - - queueFlushPendingEvents(); - } - - function recordPendingReorder(internalInstance: InternalInstance) { - const id = getID(internalInstance); - pendingReorderIDs.add(id); - - if (__DEBUG__) { - console.log('%crecordPendingReorder()', 'color: green', id); - } - - 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 - ); - } - - if (isRoot) { - // TODO Is this right? For all versions? - const hasOwnerMetadata = - internalInstance._currentElement != null && - internalInstance._currentElement._owner != null; - - pushOperation(TREE_OPERATION_ADD); - pushOperation(id); - pushOperation(ElementTypeRoot); - pushOperation(0); // isProfilingSupported? - pushOperation(hasOwnerMetadata ? 1 : 0); - } else { - const { displayName, key, type } = getData(internalInstance); - - const ownerID = - internalInstance._currentElement != null && - internalInstance._currentElement._owner != null - ? getID(internalInstance._currentElement._owner) - : 0; - - let displayNameStringID = getStringID(displayName); - let keyStringID = getStringID(key); - pushOperation(TREE_OPERATION_ADD); - pushOperation(id); - pushOperation(type); - pushOperation(parentID); - pushOperation(ownerID); - pushOperation(displayNameStringID); - pushOperation(keyStringID); - } - } - function setInProps(id: number, path: Array, value: any) { const internalInstance = idToInternalInstanceMap.get(id); if (internalInstance != null) { diff --git a/src/backend/legacy/utils.js b/src/backend/legacy/utils.js index bee9bbae06..9fbe307935 100644 --- a/src/backend/legacy/utils.js +++ b/src/backend/legacy/utils.js @@ -2,20 +2,6 @@ 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) { From 65ac53bba41031c313e38ca55509b5e436f668dc Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Jun 2019 18:33:03 -0700 Subject: [PATCH 09/13] Fixes --- src/backend/legacy/renderer.js | 78 +++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/src/backend/legacy/renderer.js b/src/backend/legacy/renderer.js index f22f617ed9..7969335cd8 100644 --- a/src/backend/legacy/renderer.js +++ b/src/backend/legacy/renderer.js @@ -27,10 +27,7 @@ import type { RendererInterface, } from '../types'; import type { ComponentFilter, ElementType } from 'src/types'; -import type { - InspectedElement, - Owner, -} from 'src/devtools/views/Components/types'; +import type { Owner, InspectedElement } from '../types'; export type InternalInstance = Object; type LegacyRenderer = Object; @@ -87,7 +84,14 @@ export function attach( global: Object ): RendererInterface { const idToInternalInstanceMap: Map = new Map(); - const internalInstanceToIDMap: Map = new Map(); + const internalInstanceToIDMap: WeakMap< + InternalInstance, + number + > = new WeakMap(); + const internalInstanceToRootIDMap: WeakMap< + InternalInstance, + number + > = new WeakMap(); let getInternalIDForNative: GetFiberIDForNative = ((null: any): GetFiberIDForNative); let findNativeNodeForInternalID: (id: number) => ?NativeType; @@ -143,9 +147,15 @@ export function attach( let oldReconcilerMethods = null; if (renderer.Reconciler) { + // React 15 oldReconcilerMethods = decorateMany(renderer.Reconciler, { mountComponent(fn, args) { - const [internalInstance] = args; + const [ + internalInstance, + transaction, + hostParent, + hostContainerInfo, + ] = args; if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) { return fn.apply(this, args); } @@ -160,6 +170,12 @@ export function attach( recordMount(internalInstance, id, parentID); parentIDStack.push(id); + // Remember the root. + internalInstanceToRootIDMap.set( + internalInstance, + getID(hostContainerInfo._topLevelWrapper) + ); + try { const result = fn.apply(this, args); parentIDStack.pop(); @@ -169,7 +185,11 @@ export function attach( throw err; } finally { if (parentIDStack.length === 0) { - flushPendingEvents(id); + const rootID = internalInstanceToRootIDMap.get(internalInstance); + if (rootID === undefined) { + throw new Error('Expected to find root ID.'); + } + flushPendingEvents(rootID); } } }, @@ -199,11 +219,10 @@ export function attach( throw err; } finally { if (parentIDStack.length === 0) { - // TODO: this is probably wrong! - const rootID = - internalInstance._topLevelWrapper === null - ? id - : getID(internalInstance._topLevelWrapper); + const rootID = internalInstanceToRootIDMap.get(internalInstance); + if (rootID === undefined) { + throw new Error('Expected to find root ID.'); + } flushPendingEvents(rootID); } } @@ -234,11 +253,10 @@ export function attach( throw err; } finally { if (parentIDStack.length === 0) { - // TODO: this is probably wrong! - const rootID = - internalInstance._topLevelWrapper === null - ? id - : getID(internalInstance._topLevelWrapper); + const rootID = internalInstanceToRootIDMap.get(internalInstance); + if (rootID === undefined) { + throw new Error('Expected to find root ID.'); + } flushPendingEvents(rootID); } } @@ -264,7 +282,11 @@ export function attach( throw err; } finally { if (parentIDStack.length === 0) { - flushPendingEvents(id); + const rootID = internalInstanceToRootIDMap.get(internalInstance); + if (rootID === undefined) { + throw new Error('Expected to find root ID.'); + } + flushPendingEvents(rootID); } } }, @@ -346,7 +368,6 @@ export function attach( function recordUnmount(internalInstance: InternalInstance, id: number) { pendingUnmountedIDs.push(id); - internalInstanceToIDMap.delete(internalInstance); idToInternalInstanceMap.delete(id); } @@ -525,9 +546,11 @@ export function attach( if (owner) { owners = []; while (owner != null) { + const ownerData = getData(owner); owners.push({ - displayName: getData(owner).displayName || 'Unknown', + displayName: ownerData.displayName || 'Unknown', id: getID(owner), + type: ownerData.type, }); owner = owner.owner; } @@ -555,6 +578,8 @@ export function attach( displayName: data.displayName, + type: data.type, + // New events system did not exist in legacy versions events: null, @@ -717,23 +742,26 @@ export function attach( }; function getBestMatchForTrackedPath(): PathMatch | null { - return null; // TODO (legacy) + // Not implemented. + return null; } function getPathForElement(id: number): Array | null { - return null; // TODO (legacy) + // Not implemented. + return null; } function updateComponentFilters(componentFilters: Array) { - // TODO (legacy) + // Not implemented. } function setTrackedPath(path: Array | null) { - // TODO (legacy) + // Not implemented. } function getOwnersList(id: number): Array | null { - return null; // TODO (legacy) + // Not implemented. + return null; } return { From 90fa0adc0a2d41188f9ddd6db7663a072cad9670 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Jun 2019 18:37:24 -0700 Subject: [PATCH 10/13] Fix lint --- src/backend/legacy/renderer.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/backend/legacy/renderer.js b/src/backend/legacy/renderer.js index 7969335cd8..5ea0a9fb95 100644 --- a/src/backend/legacy/renderer.js +++ b/src/backend/legacy/renderer.js @@ -150,12 +150,7 @@ export function attach( // React 15 oldReconcilerMethods = decorateMany(renderer.Reconciler, { mountComponent(fn, args) { - const [ - internalInstance, - transaction, - hostParent, - hostContainerInfo, - ] = args; + const [internalInstance, , , hostContainerInfo] = args; if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) { return fn.apply(this, args); } From fdc5ca4ba6227d35df89c404542be4ea3f6504d2 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Jun 2019 18:42:12 -0700 Subject: [PATCH 11/13] Remove dead code --- src/backend/legacy/getData.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/backend/legacy/getData.js b/src/backend/legacy/getData.js index ffc1d775a8..d2fce307d3 100644 --- a/src/backend/legacy/getData.js +++ b/src/backend/legacy/getData.js @@ -26,17 +26,9 @@ export default function getData(internalInstance: InternalInstance): FiberData { type = ElementTypeHostComponent; displayName = elementType; } 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 + // TODO: detect 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); } } From 8f532350175dbdac8a53fbdb92b90ac62529d4c3 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sun, 9 Jun 2019 16:16:06 -0700 Subject: [PATCH 12/13] Fixed regression fixtures --- fixtures/regression/14.9.html | 7 ++-- fixtures/regression/15.0.html | 7 ++-- fixtures/regression/15.1.html | 7 ++-- fixtures/regression/15.2.html | 7 ++-- fixtures/regression/15.3.html | 7 ++-- fixtures/regression/15.4.html | 7 ++-- fixtures/regression/15.5.html | 7 ++-- fixtures/regression/15.6.html | 7 ++-- fixtures/regression/16.0.html | 7 ++-- fixtures/regression/16.1.html | 7 ++-- fixtures/regression/16.2.html | 7 ++-- fixtures/regression/16.3.html | 7 ++-- fixtures/regression/16.4.html | 7 ++-- fixtures/regression/16.5.html | 7 ++-- fixtures/regression/16.6.html | 7 ++-- fixtures/regression/16.7.html | 7 ++-- fixtures/regression/canary.html | 7 ++-- fixtures/regression/next.html | 7 ++-- fixtures/regression/server.js | 16 ++++++++ fixtures/regression/shared.js | 2 +- package.json | 1 + yarn.lock | 66 +++++++++++++++++++++++++++++++-- 22 files changed, 135 insertions(+), 76 deletions(-) create mode 100755 fixtures/regression/server.js diff --git a/fixtures/regression/14.9.html b/fixtures/regression/14.9.html index e92a308301..efa28f49cd 100644 --- a/fixtures/regression/14.9.html +++ b/fixtures/regression/14.9.html @@ -21,11 +21,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/14.9.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/14.9.html
diff --git a/fixtures/regression/15.0.html b/fixtures/regression/15.0.html index b3f05333c2..464ea1f548 100644 --- a/fixtures/regression/15.0.html +++ b/fixtures/regression/15.0.html @@ -21,11 +21,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/15.0.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/15.0.html
diff --git a/fixtures/regression/15.1.html b/fixtures/regression/15.1.html index 8712b56a9e..7e02e81bf9 100644 --- a/fixtures/regression/15.1.html +++ b/fixtures/regression/15.1.html @@ -21,11 +21,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/15.1.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/15.1.html
diff --git a/fixtures/regression/15.2.html b/fixtures/regression/15.2.html index acde32fe21..ff1e6b8746 100644 --- a/fixtures/regression/15.2.html +++ b/fixtures/regression/15.2.html @@ -21,11 +21,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/15.2.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/15.2.html
diff --git a/fixtures/regression/15.3.html b/fixtures/regression/15.3.html index ced3559ea6..8d1795962e 100644 --- a/fixtures/regression/15.3.html +++ b/fixtures/regression/15.3.html @@ -21,11 +21,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/15.3.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/15.3.html
diff --git a/fixtures/regression/15.4.html b/fixtures/regression/15.4.html index 0f23cdea7d..ee1bfc0723 100644 --- a/fixtures/regression/15.4.html +++ b/fixtures/regression/15.4.html @@ -21,11 +21,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/15.4.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/15.4.html
diff --git a/fixtures/regression/15.5.html b/fixtures/regression/15.5.html index 361512d9bd..4097bdf708 100644 --- a/fixtures/regression/15.5.html +++ b/fixtures/regression/15.5.html @@ -21,11 +21,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/15.5.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/15.5.html
diff --git a/fixtures/regression/15.6.html b/fixtures/regression/15.6.html index 97ff99ccf5..ab10ba8716 100644 --- a/fixtures/regression/15.6.html +++ b/fixtures/regression/15.6.html @@ -21,11 +21,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/15.6.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/15.6.html
diff --git a/fixtures/regression/16.0.html b/fixtures/regression/16.0.html index ea5cf812c9..7c55d75114 100644 --- a/fixtures/regression/16.0.html +++ b/fixtures/regression/16.0.html @@ -21,11 +21,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/16.0.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/16.0.html
diff --git a/fixtures/regression/16.1.html b/fixtures/regression/16.1.html index 494cc97900..d93addf46e 100644 --- a/fixtures/regression/16.1.html +++ b/fixtures/regression/16.1.html @@ -21,11 +21,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/16.1.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/16.1.html
diff --git a/fixtures/regression/16.2.html b/fixtures/regression/16.2.html index b1c3186720..10d8e150ed 100644 --- a/fixtures/regression/16.2.html +++ b/fixtures/regression/16.2.html @@ -21,11 +21,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/16.2.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/16.2.html
diff --git a/fixtures/regression/16.3.html b/fixtures/regression/16.3.html index 76799903d8..2e42e2813a 100644 --- a/fixtures/regression/16.3.html +++ b/fixtures/regression/16.3.html @@ -21,11 +21,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/16.3.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/16.3.html
diff --git a/fixtures/regression/16.4.html b/fixtures/regression/16.4.html index ca6c30dec1..f59f4e9e6f 100644 --- a/fixtures/regression/16.4.html +++ b/fixtures/regression/16.4.html @@ -21,11 +21,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/16.4.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/16.4.html
diff --git a/fixtures/regression/16.5.html b/fixtures/regression/16.5.html index 48e14a457c..455e06f219 100644 --- a/fixtures/regression/16.5.html +++ b/fixtures/regression/16.5.html @@ -23,11 +23,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/16.5.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/16.5.html
diff --git a/fixtures/regression/16.6.html b/fixtures/regression/16.6.html index ca32dc5b9c..1f61c47f85 100644 --- a/fixtures/regression/16.6.html +++ b/fixtures/regression/16.6.html @@ -24,11 +24,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/16.6.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/16.6.html
diff --git a/fixtures/regression/16.7.html b/fixtures/regression/16.7.html index 2b7ec759fa..d839d7e030 100644 --- a/fixtures/regression/16.7.html +++ b/fixtures/regression/16.7.html @@ -24,11 +24,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/16.7.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/16.7.html
diff --git a/fixtures/regression/canary.html b/fixtures/regression/canary.html index cd25e6162a..ab59d7029e 100644 --- a/fixtures/regression/canary.html +++ b/fixtures/regression/canary.html @@ -24,11 +24,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/canary.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/canary.html
diff --git a/fixtures/regression/next.html b/fixtures/regression/next.html index c457f03acc..77bb9bbed2 100644 --- a/fixtures/regression/next.html +++ b/fixtures/regression/next.html @@ -24,11 +24,10 @@
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: + Use the server script instead:

- npm install -g pushstate-server
- pushstate-server .
- open http://localhost:9000/fixtures/regression/next.html + node ./fixtures/regression/server.js
+ open http://localhost:3000/fixtures/regression/next.html
diff --git a/fixtures/regression/server.js b/fixtures/regression/server.js new file mode 100755 index 0000000000..ec544ab006 --- /dev/null +++ b/fixtures/regression/server.js @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +const finalhandler = require('finalhandler'); +const http = require('http'); +const serveStatic = require('serve-static'); + +// Serve regression folder +const serve = serveStatic(__dirname, { index: 'index.html' }); + +// Create server +const server = http.createServer(function onRequest(req, res) { + serve(req, res, finalhandler(req, res)); +}); + +// Listen +server.listen(3000); diff --git a/fixtures/regression/shared.js b/fixtures/regression/shared.js index 362e02e911..2afd063b6b 100644 --- a/fixtures/regression/shared.js +++ b/fixtures/regression/shared.js @@ -153,7 +153,7 @@ switch (major) { ); } } - const onRender = (...args) => console.log('onRender()', ...args); + const onRender = (...args) => {}; const Profiler = React.unstable_Profiler || React.Profiler; apps.push( = 1.4.0 < 2" +http-errors@~1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-parser-js@>=0.4.0: version "0.5.0" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.0.tgz#d65edbede84349d0dc30320815a15d39cc3cbbd8" @@ -8292,6 +8303,11 @@ mime@1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + mime@^2.3.1: version "2.4.2" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.2.tgz#ce5229a5e99ffc313abac806b482c10e7ba6ac78" @@ -8425,7 +8441,7 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@^2.1.1: +ms@2.1.1, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== @@ -9193,7 +9209,7 @@ parse5@^3.0.1: dependencies: "@types/node" "*" -parseurl@~1.3.2: +parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -9728,6 +9744,11 @@ range-parser@^1.0.3, range-parser@~1.2.0: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + raw-body@2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" @@ -10563,6 +10584,25 @@ send@0.16.2: range-parser "~1.2.0" statuses "~1.4.0" +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + serialize-javascript@^1.4.0: version "1.6.1" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.6.1.tgz#4d1f697ec49429a847ca6f442a2a755126c4d879" @@ -10591,6 +10631,16 @@ serve-static@1.13.2: parseurl "~1.3.2" send "0.16.2" +serve-static@^1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -10626,6 +10676,11 @@ setprototypeof@1.1.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + sha.js@2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba" @@ -11028,7 +11083,7 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.4.0 < 2": +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= @@ -11578,6 +11633,11 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + tosource@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/tosource/-/tosource-1.0.0.tgz#42d88dd116618bcf00d6106dd5446f3427902ff1" From e66fcaf17d48adbda479251c86ede7cf1e253fdf Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sun, 9 Jun 2019 16:22:23 -0700 Subject: [PATCH 13/13] Nit --- .gitignore | 3 +-- fixtures/regression/server.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 8d57dd31ee..e2524be341 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,4 @@ yarn-error.log yarn-error.log .vscode .idea -.watchmanconfig -key.pem \ No newline at end of file +.watchmanconfig \ No newline at end of file diff --git a/fixtures/regression/server.js b/fixtures/regression/server.js index ec544ab006..f7e03f075d 100755 --- a/fixtures/regression/server.js +++ b/fixtures/regression/server.js @@ -4,7 +4,7 @@ const finalhandler = require('finalhandler'); const http = require('http'); const serveStatic = require('serve-static'); -// Serve regression folder +// Serve fixtures folder const serve = serveStatic(__dirname, { index: 'index.html' }); // Create server