UNPKG

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