UNPKG

13.5 kBJavaScriptView Raw
1(function (global, factory) {
2 typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('preact')) :
3 typeof define === 'function' && define.amd ? define(['preact'], factory) :
4 (factory(global.preact));
5}(this, (function (preact) { 'use strict';
6
7 // render modes
8
9 var ATTR_KEY = '__preactattr_';
10
11 /* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
12
13 /**
14 * Return a ReactElement-compatible object for the current state of a preact
15 * component.
16 */
17 function createReactElement(component) {
18 return {
19 type: component.constructor,
20 key: component.key,
21 ref: null, // Unsupported
22 props: component.props
23 };
24 }
25
26 /**
27 * Create a ReactDOMComponent-compatible object for a given DOM node rendered
28 * by preact.
29 *
30 * This implements the subset of the ReactDOMComponent interface that
31 * React DevTools requires in order to display DOM nodes in the inspector with
32 * the correct type and properties.
33 *
34 * @param {Node} node
35 */
36 function createReactDOMComponent(node) {
37 var childNodes = node.nodeType === Node.ELEMENT_NODE ? Array.from(node.childNodes) : [];
38
39 var isText = node.nodeType === Node.TEXT_NODE;
40
41 return {
42 // --- ReactDOMComponent interface
43 _currentElement: isText ? node.textContent : {
44 type: node.nodeName.toLowerCase(),
45 props: node[ATTR_KEY]
46 },
47 _renderedChildren: childNodes.map(function (child) {
48 if (child._component) {
49 return updateReactComponent(child._component);
50 }
51 return updateReactComponent(child);
52 }),
53 _stringText: isText ? node.textContent : null,
54
55 // --- Additional properties used by preact devtools
56
57 // A flag indicating whether the devtools have been notified about the
58 // existence of this component instance yet.
59 // This is used to send the appropriate notifications when DOM components
60 // are added or updated between composite component updates.
61 _inDevTools: false,
62 node: node
63 };
64 }
65
66 /**
67 * Return the name of a component created by a `ReactElement`-like object.
68 *
69 * @param {ReactElement} element
70 */
71 function typeName(element) {
72 if (typeof element.type === 'function') {
73 return element.type.displayName || element.type.name;
74 }
75 return element.type;
76 }
77
78 /**
79 * Return a ReactCompositeComponent-compatible object for a given preact
80 * component instance.
81 *
82 * This implements the subset of the ReactCompositeComponent interface that
83 * the DevTools requires in order to walk the component tree and inspect the
84 * component's properties.
85 *
86 * See https://github.com/facebook/react-devtools/blob/e31ec5825342eda570acfc9bcb43a44258fceb28/backend/getData.js
87 */
88 function createReactCompositeComponent(component) {
89 var _currentElement = createReactElement(component);
90 var node = component.base;
91
92 var instance = {
93 // --- ReactDOMComponent properties
94 getName: function getName() {
95 return typeName(_currentElement);
96 },
97
98 _currentElement: createReactElement(component),
99 props: component.props,
100 state: component.state,
101 forceUpdate: component.forceUpdate && component.forceUpdate.bind(component),
102 setState: component.setState && component.setState.bind(component),
103
104 // --- Additional properties used by preact devtools
105 node: node
106 };
107
108 // React DevTools exposes the `_instance` field of the selected item in the
109 // component tree as `$r` in the console. `_instance` must refer to a
110 // React Component (or compatible) class instance with `props` and `state`
111 // fields and `setState()`, `forceUpdate()` methods.
112 instance._instance = component;
113
114 // If the root node returned by this component instance's render function
115 // was itself a composite component, there will be a `_component` property
116 // containing the child component instance.
117 if (component._component) {
118 instance._renderedComponent = updateReactComponent(component._component);
119 } else {
120 // Otherwise, if the render() function returned an HTML/SVG element,
121 // create a ReactDOMComponent-like object for the DOM node itself.
122 instance._renderedComponent = updateReactComponent(node);
123 }
124
125 return instance;
126 }
127
128 /**
129 * Map of Component|Node to ReactDOMComponent|ReactCompositeComponent-like
130 * object.
131 *
132 * The same React*Component instance must be used when notifying devtools
133 * about the initial mount of a component and subsequent updates.
134 */
135 var instanceMap = typeof Map === 'function' && new Map();
136
137 /**
138 * Update (and create if necessary) the ReactDOMComponent|ReactCompositeComponent-like
139 * instance for a given preact component instance or DOM Node.
140 *
141 * @param {Component|Node} componentOrNode
142 */
143 function updateReactComponent(componentOrNode) {
144 var newInstance = componentOrNode instanceof Node ? createReactDOMComponent(componentOrNode) : createReactCompositeComponent(componentOrNode);
145 if (instanceMap.has(componentOrNode)) {
146 var inst = instanceMap.get(componentOrNode);
147 Object.assign(inst, newInstance);
148 return inst;
149 }
150 instanceMap.set(componentOrNode, newInstance);
151 return newInstance;
152 }
153
154 function nextRootKey(roots) {
155 return '.' + Object.keys(roots).length;
156 }
157
158 /**
159 * Find all root component instances rendered by preact in `node`'s children
160 * and add them to the `roots` map.
161 *
162 * @param {DOMElement} node
163 * @param {[key: string] => ReactDOMComponent|ReactCompositeComponent}
164 */
165 function findRoots(node, roots) {
166 Array.from(node.childNodes).forEach(function (child) {
167 if (child._component) {
168 roots[nextRootKey(roots)] = updateReactComponent(child._component);
169 } else {
170 findRoots(child, roots);
171 }
172 });
173 }
174
175 /**
176 * Create a bridge for exposing preact's component tree to React DevTools.
177 *
178 * It creates implementations of the interfaces that ReactDOM passes to
179 * devtools to enable it to query the component tree and hook into component
180 * updates.
181 *
182 * See https://github.com/facebook/react/blob/59ff7749eda0cd858d5ee568315bcba1be75a1ca/src/renderers/dom/ReactDOM.js
183 * for how ReactDOM exports its internals for use by the devtools and
184 * the `attachRenderer()` function in
185 * https://github.com/facebook/react-devtools/blob/e31ec5825342eda570acfc9bcb43a44258fceb28/backend/attachRenderer.js
186 * for how the devtools consumes the resulting objects.
187 */
188 function createDevToolsBridge() {
189 // The devtools has different paths for interacting with the renderers from
190 // React Native, legacy React DOM and current React DOM.
191 //
192 // Here we emulate the interface for the current React DOM (v15+) lib.
193
194 // ReactDOMComponentTree-like object
195 var ComponentTree = {
196 getNodeFromInstance: function getNodeFromInstance(instance) {
197 return instance.node;
198 },
199 getClosestInstanceFromNode: function getClosestInstanceFromNode(node) {
200 while (node && !node._component) {
201 node = node.parentNode;
202 }
203 return node ? updateReactComponent(node._component) : null;
204 }
205 };
206
207 // Map of root ID (the ID is unimportant) to component instance.
208 var roots = {};
209 findRoots(document.body, roots);
210
211 // ReactMount-like object
212 //
213 // Used by devtools to discover the list of root component instances and get
214 // notified when new root components are rendered.
215 var Mount = {
216 _instancesByReactRootID: roots,
217
218 // Stub - React DevTools expects to find this method and replace it
219 // with a wrapper in order to observe new root components being added
220 _renderNewRootComponent: function _renderNewRootComponent() /* instance, ... */{}
221 };
222
223 // ReactReconciler-like object
224 var Reconciler = {
225 // Stubs - React DevTools expects to find these methods and replace them
226 // with wrappers in order to observe components being mounted, updated and
227 // unmounted
228 mountComponent: function mountComponent() /* instance, ... */{},
229 performUpdateIfNecessary: function performUpdateIfNecessary() /* instance, ... */{},
230 receiveComponent: function receiveComponent() /* instance, ... */{},
231 unmountComponent: function unmountComponent() /* instance, ... */{}
232 };
233
234 /** Notify devtools that a new component instance has been mounted into the DOM. */
235 var componentAdded = function componentAdded(component) {
236 var instance = updateReactComponent(component);
237 if (isRootComponent(component)) {
238 instance._rootID = nextRootKey(roots);
239 roots[instance._rootID] = instance;
240 Mount._renderNewRootComponent(instance);
241 }
242 visitNonCompositeChildren(instance, function (childInst) {
243 childInst._inDevTools = true;
244 Reconciler.mountComponent(childInst);
245 });
246 Reconciler.mountComponent(instance);
247 };
248
249 /** Notify devtools that a component has been updated with new props/state. */
250 var componentUpdated = function componentUpdated(component) {
251 var prevRenderedChildren = [];
252 var instance = instanceMap.get(component);
253 if (instance) {
254 visitNonCompositeChildren(instance, function (childInst) {
255 prevRenderedChildren.push(childInst);
256 });
257 }
258 // Notify devtools about updates to this component and any non-composite
259 // children
260 instance = updateReactComponent(component);
261
262 Reconciler.receiveComponent(instance);
263 visitNonCompositeChildren(instance, function (childInst) {
264 if (!childInst._inDevTools) {
265 // New DOM child component
266 childInst._inDevTools = true;
267 Reconciler.mountComponent(childInst);
268 } else {
269 // Updated DOM child component
270 Reconciler.receiveComponent(childInst);
271 }
272 });
273
274 // For any non-composite children that were removed by the latest render,
275 // remove the corresponding ReactDOMComponent-like instances and notify
276 // the devtools
277 prevRenderedChildren.forEach(function (childInst) {
278 if (!document.body.contains(childInst.node)) {
279 instanceMap.delete(childInst.node);
280 Reconciler.unmountComponent(childInst);
281 }
282 });
283 };
284
285 /** Notify devtools that a component has been unmounted from the DOM. */
286 var componentRemoved = function componentRemoved(component) {
287 var instance = updateReactComponent(component);
288 visitNonCompositeChildren(function (childInst) {
289 instanceMap.delete(childInst.node);
290 Reconciler.unmountComponent(childInst);
291 });
292 Reconciler.unmountComponent(instance);
293 instanceMap.delete(component);
294 if (instance._rootID) {
295 delete roots[instance._rootID];
296 }
297 };
298
299 return {
300 componentAdded: componentAdded,
301 componentUpdated: componentUpdated,
302 componentRemoved: componentRemoved,
303
304 // Interfaces passed to devtools via __REACT_DEVTOOLS_GLOBAL_HOOK__.inject()
305 ComponentTree: ComponentTree,
306 Mount: Mount,
307 Reconciler: Reconciler
308 };
309 }
310
311 /**
312 * Return `true` if a preact component is a top level component rendered by
313 * `render()` into a container Element.
314 */
315 function isRootComponent(component) {
316 // `_parentComponent` is actually `__u` after minification
317 if (component._parentComponent || component.__u) {
318 // Component with a composite parent
319 return false;
320 }
321 if (component.base.parentElement && component.base.parentElement[ATTR_KEY]) {
322 // Component with a parent DOM element rendered by Preact
323 return false;
324 }
325 return true;
326 }
327
328 /**
329 * Visit all child instances of a ReactCompositeComponent-like object that are
330 * not composite components (ie. they represent DOM elements or text)
331 *
332 * @param {Component} component
333 * @param {(Component) => void} visitor
334 */
335 function visitNonCompositeChildren(component, visitor) {
336 if (!component) return;
337 if (component._renderedComponent) {
338 if (!component._renderedComponent._component) {
339 visitor(component._renderedComponent);
340 visitNonCompositeChildren(component._renderedComponent, visitor);
341 }
342 } else if (component._renderedChildren) {
343 component._renderedChildren.forEach(function (child) {
344 visitor(child);
345 if (!child._component) visitNonCompositeChildren(child, visitor);
346 });
347 }
348 }
349
350 /**
351 * Create a bridge between the preact component tree and React's dev tools
352 * and register it.
353 *
354 * After this function is called, the React Dev Tools should be able to detect
355 * "React" on the page and show the component tree.
356 *
357 * This function hooks into preact VNode creation in order to expose functional
358 * components correctly, so it should be called before the root component(s)
359 * are rendered.
360 *
361 * Returns a cleanup function which unregisters the hooks.
362 */
363 function initDevTools() {
364 if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
365 // React DevTools are not installed
366 return;
367 }
368
369 // Notify devtools when preact components are mounted, updated or unmounted
370 var bridge = createDevToolsBridge();
371
372 var nextAfterMount = preact.options.afterMount;
373 preact.options.afterMount = function (component) {
374 bridge.componentAdded(component);
375 if (nextAfterMount) nextAfterMount(component);
376 };
377
378 var nextAfterUpdate = preact.options.afterUpdate;
379 preact.options.afterUpdate = function (component) {
380 bridge.componentUpdated(component);
381 if (nextAfterUpdate) nextAfterUpdate(component);
382 };
383
384 var nextBeforeUnmount = preact.options.beforeUnmount;
385 preact.options.beforeUnmount = function (component) {
386 bridge.componentRemoved(component);
387 if (nextBeforeUnmount) nextBeforeUnmount(component);
388 };
389
390 // Notify devtools about this instance of "React"
391 __REACT_DEVTOOLS_GLOBAL_HOOK__.inject(bridge);
392
393 return function () {
394 preact.options.afterMount = nextAfterMount;
395 preact.options.afterUpdate = nextAfterUpdate;
396 preact.options.beforeUnmount = nextBeforeUnmount;
397 };
398 }
399
400 initDevTools();
401
402})));
403//# sourceMappingURL=devtools.js.map