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}{' '}
+ increment
+
+ );
+ }
+ apps.push(
+
+
+
+ );
+ }
+ case 6:
+ // memo
+ function LabelComponent({ label }) {
+ return {label} ;
+ }
+ const AnonymousMemoized = React.memo(({ label }) => (
+ {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 load data ;
+ }
+ }
+ }
+ 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}{' '}
+ increment
+
+ );
+ }
+ }
+ 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}
+ )}{' '}
+ increment
+
+ );
+ }
+}
+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 = (
+
+ );
+ } 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();
+}