UNPKG

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