UNPKG

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