diff options
Diffstat (limited to 'thirdparty/preact/devtools/devtools.js')
-rw-r--r-- | thirdparty/preact/devtools/devtools.js | 427 |
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; + }; +} |