aboutsummaryrefslogtreecommitdiff
path: root/thirdparty/preact/devtools/devtools.js
diff options
context:
space:
mode:
Diffstat (limited to 'thirdparty/preact/devtools/devtools.js')
-rw-r--r--thirdparty/preact/devtools/devtools.js427
1 files changed, 427 insertions, 0 deletions
diff --git a/thirdparty/preact/devtools/devtools.js b/thirdparty/preact/devtools/devtools.js
new file mode 100644
index 000000000..4bcbeae1e
--- /dev/null
+++ b/thirdparty/preact/devtools/devtools.js
@@ -0,0 +1,427 @@
+/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
+
+import { options, Component } from 'preact';
+
+// Internal helpers from preact
+import { ATTR_KEY } from '../src/constants';
+import { isFunctionalComponent } from '../src/vdom/functional-component';
+
+/**
+ * Return a ReactElement-compatible object for the current state of a preact
+ * component.
+ */
+function createReactElement(component) {
+ return {
+ type: component.constructor,
+ key: component.key,
+ ref: null, // Unsupported
+ props: component.props
+ };
+}
+
+/**
+ * Create a ReactDOMComponent-compatible object for a given DOM node rendered
+ * by preact.
+ *
+ * This implements the subset of the ReactDOMComponent interface that
+ * React DevTools requires in order to display DOM nodes in the inspector with
+ * the correct type and properties.
+ *
+ * @param {Node} node
+ */
+function createReactDOMComponent(node) {
+ const childNodes = node.nodeType === Node.ELEMENT_NODE ?
+ Array.from(node.childNodes) : [];
+
+ const isText = node.nodeType === Node.TEXT_NODE;
+
+ return {
+ // --- ReactDOMComponent interface
+ _currentElement: isText ? node.textContent : {
+ type: node.nodeName.toLowerCase(),
+ props: node[ATTR_KEY]
+ },
+ _renderedChildren: childNodes.map(child => {
+ if (child._component) {
+ return updateReactComponent(child._component);
+ }
+ return updateReactComponent(child);
+ }),
+ _stringText: isText ? node.textContent : null,
+
+ // --- Additional properties used by preact devtools
+
+ // A flag indicating whether the devtools have been notified about the
+ // existence of this component instance yet.
+ // This is used to send the appropriate notifications when DOM components
+ // are added or updated between composite component updates.
+ _inDevTools: false,
+ node
+ };
+}
+
+/**
+ * Return the name of a component created by a `ReactElement`-like object.
+ *
+ * @param {ReactElement} element
+ */
+function typeName(element) {
+ if (typeof element.type === 'function') {
+ return element.type.displayName || element.type.name;
+ }
+ return element.type;
+}
+
+/**
+ * Return a ReactCompositeComponent-compatible object for a given preact
+ * component instance.
+ *
+ * This implements the subset of the ReactCompositeComponent interface that
+ * the DevTools requires in order to walk the component tree and inspect the
+ * component's properties.
+ *
+ * See https://github.com/facebook/react-devtools/blob/e31ec5825342eda570acfc9bcb43a44258fceb28/backend/getData.js
+ */
+function createReactCompositeComponent(component) {
+ const _currentElement = createReactElement(component);
+ const node = component.base;
+
+ let instance = {
+ // --- ReactDOMComponent properties
+ getName() {
+ return typeName(_currentElement);
+ },
+ _currentElement: createReactElement(component),
+ props: component.props,
+ state: component.state,
+ forceUpdate: component.forceUpdate.bind(component),
+ setState: component.setState.bind(component),
+
+ // --- Additional properties used by preact devtools
+ node
+ };
+
+ // React DevTools exposes the `_instance` field of the selected item in the
+ // component tree as `$r` in the console. `_instance` must refer to a
+ // React Component (or compatible) class instance with `props` and `state`
+ // fields and `setState()`, `forceUpdate()` methods.
+ instance._instance = component;
+
+ // If the root node returned by this component instance's render function
+ // was itself a composite component, there will be a `_component` property
+ // containing the child component instance.
+ if (component._component) {
+ instance._renderedComponent = updateReactComponent(component._component);
+ } else {
+ // Otherwise, if the render() function returned an HTML/SVG element,
+ // create a ReactDOMComponent-like object for the DOM node itself.
+ instance._renderedComponent = updateReactComponent(node);
+ }
+
+ return instance;
+}
+
+/**
+ * Map of Component|Node to ReactDOMComponent|ReactCompositeComponent-like
+ * object.
+ *
+ * The same React*Component instance must be used when notifying devtools
+ * about the initial mount of a component and subsequent updates.
+ */
+let instanceMap = new Map();
+
+/**
+ * Update (and create if necessary) the ReactDOMComponent|ReactCompositeComponent-like
+ * instance for a given preact component instance or DOM Node.
+ *
+ * @param {Component|Node} componentOrNode
+ */
+function updateReactComponent(componentOrNode) {
+ const newInstance = componentOrNode instanceof Node ?
+ createReactDOMComponent(componentOrNode) :
+ createReactCompositeComponent(componentOrNode);
+ if (instanceMap.has(componentOrNode)) {
+ let inst = instanceMap.get(componentOrNode);
+ Object.assign(inst, newInstance);
+ return inst;
+ }
+ instanceMap.set(componentOrNode, newInstance);
+ return newInstance;
+}
+
+function nextRootKey(roots) {
+ return '.' + Object.keys(roots).length;
+}
+
+/**
+ * Find all root component instances rendered by preact in `node`'s children
+ * and add them to the `roots` map.
+ *
+ * @param {DOMElement} node
+ * @param {[key: string] => ReactDOMComponent|ReactCompositeComponent}
+ */
+function findRoots(node, roots) {
+ Array.from(node.childNodes).forEach(child => {
+ if (child._component) {
+ roots[nextRootKey(roots)] = updateReactComponent(child._component);
+ } else {
+ findRoots(child, roots);
+ }
+ });
+}
+
+/**
+ * Map of functional component name -> wrapper class.
+ */
+let functionalComponentWrappers = new Map();
+
+/**
+ * Wrap a functional component with a stateful component.
+ *
+ * preact does not record any information about the original hierarchy of
+ * functional components in the rendered DOM nodes. Wrapping functional components
+ * with a trivial wrapper allows us to recover information about the original
+ * component structure from the DOM.
+ *
+ * @param {VNode} vnode
+ */
+function wrapFunctionalComponent(vnode) {
+ const originalRender = vnode.nodeName;
+ const name = vnode.nodeName.name || '(Function.name missing)';
+ const wrappers = functionalComponentWrappers;
+ if (!wrappers.has(originalRender)) {
+ let wrapper = class extends Component {
+ render(props, state, context) {
+ return originalRender(props, context);
+ }
+ };
+
+ // Expose the original component name. React Dev Tools will use
+ // this property if it exists or fall back to Function.name
+ // otherwise.
+ wrapper.displayName = name;
+
+ wrappers.set(originalRender, wrapper);
+ }
+ vnode.nodeName = wrappers.get(originalRender);
+}
+
+/**
+ * Create a bridge for exposing preact's component tree to React DevTools.
+ *
+ * It creates implementations of the interfaces that ReactDOM passes to
+ * devtools to enable it to query the component tree and hook into component
+ * updates.
+ *
+ * See https://github.com/facebook/react/blob/59ff7749eda0cd858d5ee568315bcba1be75a1ca/src/renderers/dom/ReactDOM.js
+ * for how ReactDOM exports its internals for use by the devtools and
+ * the `attachRenderer()` function in
+ * https://github.com/facebook/react-devtools/blob/e31ec5825342eda570acfc9bcb43a44258fceb28/backend/attachRenderer.js
+ * for how the devtools consumes the resulting objects.
+ */
+function createDevToolsBridge() {
+ // The devtools has different paths for interacting with the renderers from
+ // React Native, legacy React DOM and current React DOM.
+ //
+ // Here we emulate the interface for the current React DOM (v15+) lib.
+
+ // ReactDOMComponentTree-like object
+ const ComponentTree = {
+ getNodeFromInstance(instance) {
+ return instance.node;
+ },
+ getClosestInstanceFromNode(node) {
+ while (node && !node._component) {
+ node = node.parentNode;
+ }
+ return node ? updateReactComponent(node._component) : null;
+ }
+ };
+
+ // Map of root ID (the ID is unimportant) to component instance.
+ let roots = {};
+ findRoots(document.body, roots);
+
+ // ReactMount-like object
+ //
+ // Used by devtools to discover the list of root component instances and get
+ // notified when new root components are rendered.
+ const Mount = {
+ _instancesByReactRootID: roots,
+
+ // Stub - React DevTools expects to find this method and replace it
+ // with a wrapper in order to observe new root components being added
+ _renderNewRootComponent(/* instance, ... */) { }
+ };
+
+ // ReactReconciler-like object
+ const Reconciler = {
+ // Stubs - React DevTools expects to find these methods and replace them
+ // with wrappers in order to observe components being mounted, updated and
+ // unmounted
+ mountComponent(/* instance, ... */) { },
+ performUpdateIfNecessary(/* instance, ... */) { },
+ receiveComponent(/* instance, ... */) { },
+ unmountComponent(/* instance, ... */) { }
+ };
+
+ /** Notify devtools that a new component instance has been mounted into the DOM. */
+ const componentAdded = component => {
+ const instance = updateReactComponent(component);
+ if (isRootComponent(component)) {
+ instance._rootID = nextRootKey(roots);
+ roots[instance._rootID] = instance;
+ Mount._renderNewRootComponent(instance);
+ }
+ visitNonCompositeChildren(instance, childInst => {
+ childInst._inDevTools = true;
+ Reconciler.mountComponent(childInst);
+ });
+ Reconciler.mountComponent(instance);
+ };
+
+ /** Notify devtools that a component has been updated with new props/state. */
+ const componentUpdated = component => {
+ const prevRenderedChildren = [];
+ visitNonCompositeChildren(instanceMap.get(component), childInst => {
+ prevRenderedChildren.push(childInst);
+ });
+
+ // Notify devtools about updates to this component and any non-composite
+ // children
+ const instance = updateReactComponent(component);
+ Reconciler.receiveComponent(instance);
+ visitNonCompositeChildren(instance, childInst => {
+ if (!childInst._inDevTools) {
+ // New DOM child component
+ childInst._inDevTools = true;
+ Reconciler.mountComponent(childInst);
+ } else {
+ // Updated DOM child component
+ Reconciler.receiveComponent(childInst);
+ }
+ });
+
+ // For any non-composite children that were removed by the latest render,
+ // remove the corresponding ReactDOMComponent-like instances and notify
+ // the devtools
+ prevRenderedChildren.forEach(childInst => {
+ if (!document.body.contains(childInst.node)) {
+ instanceMap.delete(childInst.node);
+ Reconciler.unmountComponent(childInst);
+ }
+ });
+ };
+
+ /** Notify devtools that a component has been unmounted from the DOM. */
+ const componentRemoved = component => {
+ const instance = updateReactComponent(component);
+ visitNonCompositeChildren(childInst => {
+ instanceMap.delete(childInst.node);
+ Reconciler.unmountComponent(childInst);
+ });
+ Reconciler.unmountComponent(instance);
+ instanceMap.delete(component);
+ if (instance._rootID) {
+ delete roots[instance._rootID];
+ }
+ };
+
+ return {
+ componentAdded,
+ componentUpdated,
+ componentRemoved,
+
+ // Interfaces passed to devtools via __REACT_DEVTOOLS_GLOBAL_HOOK__.inject()
+ ComponentTree,
+ Mount,
+ Reconciler
+ };
+}
+
+/**
+ * Return `true` if a preact component is a top level component rendered by
+ * `render()` into a container Element.
+ */
+function isRootComponent(component) {
+ return !component.base.parentElement || !component.base.parentElement[ATTR_KEY];
+}
+
+/**
+ * Visit all child instances of a ReactCompositeComponent-like object that are
+ * not composite components (ie. they represent DOM elements or text)
+ *
+ * @param {Component} component
+ * @param {(Component) => void} visitor
+ */
+function visitNonCompositeChildren(component, visitor) {
+ if (component._renderedComponent) {
+ if (!component._renderedComponent._component) {
+ visitor(component._renderedComponent);
+ visitNonCompositeChildren(component._renderedComponent, visitor);
+ }
+ } else if (component._renderedChildren) {
+ component._renderedChildren.forEach(child => {
+ visitor(child);
+ if (!child._component) visitNonCompositeChildren(child, visitor);
+ });
+ }
+}
+
+/**
+ * Create a bridge between the preact component tree and React's dev tools
+ * and register it.
+ *
+ * After this function is called, the React Dev Tools should be able to detect
+ * "React" on the page and show the component tree.
+ *
+ * This function hooks into preact VNode creation in order to expose functional
+ * components correctly, so it should be called before the root component(s)
+ * are rendered.
+ *
+ * Returns a cleanup function which unregisters the hooks.
+ */
+export function initDevTools() {
+ if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
+ // React DevTools are not installed
+ return;
+ }
+
+ // Hook into preact element creation in order to wrap functional components
+ // with stateful ones in order to make them visible in the devtools
+ const nextVNode = options.vnode;
+ options.vnode = (vnode) => {
+ if (isFunctionalComponent(vnode)) wrapFunctionalComponent(vnode);
+ if (nextVNode) return nextVNode(vnode);
+ };
+
+ // Notify devtools when preact components are mounted, updated or unmounted
+ const bridge = createDevToolsBridge();
+
+ const nextAfterMount = options.afterMount;
+ options.afterMount = component => {
+ bridge.componentAdded(component);
+ if (nextAfterMount) nextAfterMount(component);
+ };
+
+ const nextAfterUpdate = options.afterUpdate;
+ options.afterUpdate = component => {
+ bridge.componentUpdated(component);
+ if (nextAfterUpdate) nextAfterUpdate(component);
+ };
+
+ const nextBeforeUnmount = options.beforeUnmount;
+ options.beforeUnmount = component => {
+ bridge.componentRemoved(component);
+ if (nextBeforeUnmount) nextBeforeUnmount(component);
+ };
+
+ // Notify devtools about this instance of "React"
+ __REACT_DEVTOOLS_GLOBAL_HOOK__.inject(bridge);
+
+ return () => {
+ options.afterMount = nextAfterMount;
+ options.afterUpdate = nextAfterUpdate;
+ options.beforeUnmount = nextBeforeUnmount;
+ };
+}