├── .babelrc ├── .eslintrc ├── .gitignore ├── .noderequirer.json ├── .npmignore ├── LICENSE ├── README.md ├── debug-inspector.css ├── package.json └── src ├── DebugInspector.jsx ├── DebugInspectorPanel.jsx └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "optional": "runtime" 4 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "no-undef": [2], 5 | "no-trailing-spaces": [1], 6 | "space-before-blocks": [2, "always"], 7 | "no-unused-expressions": [0], 8 | "no-underscore-dangle": [0], 9 | "quote-props": [1, "as-needed"], 10 | "no-multi-spaces": [0], 11 | "no-unused-vars": [1], 12 | "no-loop-func": [0], 13 | "key-spacing": [0], 14 | "max-len": [1, 100], 15 | "strict": [0], 16 | "eol-last": [1], 17 | "no-console": [1], 18 | "indent": [1, 2], 19 | "quotes": [2, "single", "avoid-escape"], 20 | "curly": [0], 21 | 22 | "react/jsx-boolean-value": 1, 23 | "react/jsx-quotes": 1, 24 | "react/jsx-no-undef": 2, 25 | "react/jsx-uses-react": 2, 26 | "react/jsx-uses-vars": 2, 27 | "react/no-did-mount-set-state": 1, 28 | "react/no-did-update-set-state": 1, 29 | "react/no-multi-comp": 0, 30 | "react/no-unknown-property": 1, 31 | "react/react-in-jsx-scope": 1, 32 | "react/self-closing-comp": 1, 33 | "react/wrap-multilines": 1, 34 | 35 | "generator-star-spacing": 0, 36 | "new-cap": 0, 37 | "object-curly-spacing": 0, 38 | "object-shorthand": 0, 39 | 40 | "babel/generator-star-spacing": 1, 41 | "babel/new-cap": 1, 42 | "babel/object-curly-spacing": [1, "always"], 43 | "babel/object-shorthand": 1 44 | }, 45 | "plugins": [ 46 | "react", 47 | "babel" 48 | ], 49 | "settings": { 50 | "ecmascript": 6, 51 | "jsx": true 52 | }, 53 | "env": { 54 | "browser": true, 55 | "node": true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.noderequirer.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": true 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | static 2 | src 3 | demo 4 | .* 5 | server.js 6 | webpack.config.js 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alexander Kuznetsov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-transform-debug-inspector 2 | React inspector tranformation function for [babel-plugin-react-transform](https://github.com/gaearon/babel-plugin-react-transform) 3 | 4 | (this feels like more of a demo than a real thing for now, but anyway) 5 | 6 | ## Install 7 | 8 | ``` 9 | $ npm i -D react-transform-debug-inspector 10 | ``` 11 | 12 | Update your `.babelrc`: 13 | ```json 14 | "plugins": ["react-transform"], 15 | "extra": { 16 | "react-transform": [{ 17 | "target": "react-transform-debug-inspector" 18 | }] 19 | } 20 | ``` 21 | 22 | If you need advanced settings, add path to config module: 23 | ```json 24 | "extra": { 25 | "react-transform": [{ 26 | "target": "react-transform-debug-inspector", 27 | "imports": ["./debug/inspectorConfig"] 28 | }] 29 | } 30 | ``` 31 | 32 | Config example: 33 | ```js 34 | // import styles for json tree 35 | import 'style!css!react-transform-debug-inspector/debug-inspector.css'; 36 | 37 | import { DevTools, LogMonitor } from 'redux-devtools/lib/react'; 38 | 39 | function getReduxPanel(component) { 40 | // instead of plain object or literal, you can pass any component - like redux DevTools 41 | if (component.context.store) { 42 | return ( 43 | 44 | ); 45 | } 46 | } 47 | 48 | let _enabled = false; 49 | 50 | export default { 51 | // add your custom panels ('props', 'state', 'context' by default) 52 | getPanels: (defaultPanels, component) => [ 53 | ...( 54 | component.context.store ? [{ 55 | name: 'redux', 56 | data: getReduxPanel(component) 57 | }] : [] 58 | ), 59 | ...defaultPanels 60 | ], 61 | 62 | // enable or disable inspector with key binding or whatever 63 | enabledTrigger: enable => { 64 | window.addEventListener('keydown', e => { 65 | if (e.keyCode === 220) { 66 | _enabled = !_enabled; 67 | enable(_enabled); 68 | } 69 | }); 70 | 71 | // another example: enable(location.search.indexOf('debug') !== -1) 72 | 73 | enable(_enabled); 74 | }, 75 | 76 | // filter components that don't need inspector 77 | showPin: component => true 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /debug-inspector.css: -------------------------------------------------------------------------------- 1 | .RT-debug-inspector { 2 | font-family: Menlo, monospace; 3 | font-size: 11px; 4 | line-height: 14px; 5 | cursor: default; 6 | margin: 0 15px; 7 | } 8 | 9 | .RT-debug-inspector-unselectable { 10 | -webkit-touch-callout: none; 11 | -webkit-user-select: none; 12 | -khtml-user-select: none; 13 | -moz-user-select: none; 14 | -ms-user-select: none; 15 | -o-user-select: none; 16 | user-select: none; 17 | } 18 | 19 | .RT-debug-inspector-expand-control { 20 | color: #6e6e6e; 21 | font-size: 10px; 22 | margin-right: 3px; 23 | white-space: pre; 24 | } 25 | 26 | .RT-debug-inspector-property { 27 | padding-top: 2px; 28 | white-space: nowrap; 29 | overflow: hidden; 30 | text-overflow: ellipsis; 31 | } 32 | 33 | .RT-debug-inspector-property .RT-debug-inspector-object-preview { 34 | opacity: 0.3; 35 | white-space: initial; 36 | } 37 | 38 | .RT-debug-inspector-property:last-child .RT-debug-inspector-object-preview { 39 | opacity: 1; 40 | } 41 | 42 | .RT-debug-inspector-object-name { 43 | color: rgb(136, 19, 145); 44 | } 45 | 46 | .RT-debug-inspector-object-value-null, .RT-debug-inspector-object-value-undefined { 47 | color: rgb(128, 128, 128); 48 | } 49 | 50 | .RT-debug-inspector-object-value-string { 51 | color: rgb(196, 26, 22); 52 | } 53 | 54 | .RT-debug-inspector-object-value-number, .RT-debug-inspector-object-value-boolean { 55 | color: rgb(28, 0, 207); 56 | } 57 | 58 | .RT-debug-inspector-object-value-function-keyword { 59 | color: rgb(170, 13, 145); 60 | font-style: italic; 61 | } 62 | 63 | .RT-debug-inspector-object-value-function-name { 64 | font-style: italic; 65 | } 66 | 67 | .RT-debug-inspector-object-preview { 68 | font-style: italic; 69 | } 70 | 71 | .RT-debug-inspector-property-nodes-container { 72 | padding-left: 12px 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-transform-debug-inspector", 3 | "version": "0.1.3", 4 | "description": "React inspector tranformation function for babel-plugin-wrap-react-components", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "./node_modules/.bin/babel src --out-dir lib", 8 | "lint": "eslint src", 9 | "version": "npm run build && git add -A .", 10 | "postversion": "git push", 11 | "prepublish": "npm run build", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "Alexander (http://kuzya.org/)", 15 | "devDependencies": { 16 | "babel": "^5.8.21", 17 | "babel-core": "^5.4.7", 18 | "babel-eslint": "^4.0.10", 19 | "babel-loader": "^5.1.2", 20 | "eslint": "^1.2.1", 21 | "eslint-loader": "^1.0.0", 22 | "eslint-plugin-babel": "^2.1.1", 23 | "eslint-plugin-react": "^3.3.1" 24 | }, 25 | "license": "MIT", 26 | "dependencies": { 27 | "@alexkuz/react-object-inspector": "^0.1.5-patch", 28 | "react-dock": "^0.1.0" 29 | }, 30 | "peerDependencies": { 31 | "babel-plugin-react-transform": "^1.0.0", 32 | "babel-runtime": "^5.8.20", 33 | "radium": "^0.13.8" 34 | }, 35 | "keywords": [ 36 | "babel-plugin", 37 | "react-transform", 38 | "dx", 39 | "react", 40 | "reactjs", 41 | "devtools" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/DebugInspector.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Dock from 'react-dock'; 3 | import DebugInspectorPanel from './DebugInspectorPanel'; 4 | import radium from 'radium'; 5 | 6 | @radium 7 | export default class DebugInspector extends Component { 8 | constructor(props) { 9 | super(props); 10 | const nullComponent = { 11 | pinRect: { left: -10000, right: -10000, top: -1000 }, 12 | component: null 13 | }; 14 | 15 | this.state = { 16 | components: [nullComponent], 17 | isVisible: false, 18 | shownComponent: nullComponent, 19 | position: 'left' 20 | }; 21 | } 22 | 23 | render() { 24 | const { position, components: [{ pinRect }] } = this.state; 25 | 26 | const pinStyle = { 27 | position: 'absolute', 28 | backgroundColor: '#FFFF33', 29 | width: '16px', 30 | height: '16px', 31 | left: (position === 'left' ? 32 | (pinRect.right + window.scrollX - 24) + 'px' : 33 | (pinRect.left + window.scrollX + 8) + 'px' 34 | ), 35 | top: (pinRect.top + window.scrollY + 8) + 'px', 36 | borderRadius: '100px', 37 | boxShadow: '0 1px 4px rgba(0,0,0,0.3)', 38 | pointerEvents: 'auto', 39 | cursor: 'pointer' 40 | }; 41 | 42 | const wrapperStyle = { 43 | position: 'absolute', 44 | zIndex: '999999999', 45 | top: 0, 46 | left: 0, 47 | width: 0, 48 | height: 0 49 | }; 50 | 51 | const bittonsStyle = { 52 | position: 'absolute', 53 | top: '15px', 54 | right: '15px', 55 | cursor: 'pointer', 56 | lineHeight: '12px', 57 | fontSize: '16px' 58 | }; 59 | 60 | return ( 61 |
62 | this.setState({ isVisible })} 65 | dimMode='none'> 66 |
67 | {this.state.position === 'right' && 68 | 69 | {'\u21E4'} 70 | 71 | } 72 | {this.state.position === 'left' && 73 | 74 | {'\u21E5'} 75 | 76 | } 77 | 78 | × 79 | 80 |
81 | {this.state.isVisible && this.renderPanels()} 82 |
83 |
86 |
87 | ); 88 | } 89 | 90 | renderPanels() { 91 | const { getPanels } = this.props; 92 | const component = this.state.shownComponent.component; 93 | 94 | const panels = getPanels([ 95 | { name: 'props', data: component.props }, 96 | { name: 'state', data: component.state }, 97 | { name: 'context', data: component.context }, 98 | ], component); 99 | 100 | return ( 101 |
102 | {panels.map((panel, idx) => 103 | 106 | )} 107 |
108 | ); 109 | } 110 | 111 | addComponent(component, rect) { 112 | this.setState({ components: [{ 113 | component, 114 | pinRect: rect 115 | }, ...this.state.components] }); 116 | } 117 | 118 | removeComponent(component) { 119 | this.setState({ components: this.state.components.filter(c => c.component !== component) }); 120 | } 121 | 122 | handleCloseClick = () => { 123 | this.setState({ isVisible: false }); 124 | } 125 | 126 | handleLeftClick = () => { 127 | this.setState({ position: 'left' }); 128 | } 129 | 130 | handleRightClick = () => { 131 | this.setState({ position: 'right' }); 132 | } 133 | 134 | handlePinClick = () => { 135 | const { isVisible, components: [topComponent], shownComponent } = this.state; 136 | this.setState({ 137 | isVisible: !isVisible || topComponent !== shownComponent, 138 | shownComponent: topComponent 139 | }); 140 | } 141 | 142 | getPinElement() { 143 | return React.findDOMNode(this.refs.pin); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/DebugInspectorPanel.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import ObjectInspector from '@alexkuz/react-object-inspector'; 3 | import radium from 'radium'; 4 | 5 | @radium 6 | export default class DebugInspectorPanel extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { collapsed: false }; 10 | } 11 | 12 | static propTypes = { 13 | name: PropTypes.string.isRequired, 14 | data: PropTypes.any 15 | } 16 | 17 | render() { 18 | const { name, data } = this.props; 19 | 20 | const panelStyle = { 21 | userSelect: 'none' 22 | } 23 | 24 | const contentStyle = { 25 | transition: 'max-height 0.15s ease-out', 26 | maxHeight: this.state.collapsed ? 0 : '100vh', 27 | userSelect: 'initial', 28 | overflow: 'auto' 29 | }; 30 | 31 | const arrowStyle = [{ 32 | transition: 'transform 0.15s ease-out', 33 | borderTop: '6px solid #333333', 34 | borderLeft: '4px solid transparent', 35 | borderRight: '4px solid transparent', 36 | display: 'inline-block', 37 | marginRight: '5px', 38 | marginBottom: '2px' 39 | }, this.state.collapsed && { 40 | transform: 'rotate(-90deg)' 41 | }]; 42 | 43 | const headerStyle = { 44 | margin: '15px', 45 | cursor: 'pointer' 46 | }; 47 | 48 | return ( 49 |
50 |
52 |
53 | {name} 54 |
55 |
56 | {!React.isValidElement(data) ? 57 | : 59 | data 60 | } 61 |
62 |
63 | ); 64 | } 65 | 66 | toggleCollapsed = () => { 67 | this.setState({ collapsed: !this.state.collapsed }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DebugInspector from './DebugInspector'; 3 | 4 | let _debugInspector; 5 | let _debugPopupWrapper; 6 | 7 | let _enabled = false; 8 | 9 | function init(config) { 10 | if (_debugInspector) { 11 | return; 12 | } 13 | 14 | _debugPopupWrapper = document.createElement('div'); 15 | document.body.appendChild(_debugPopupWrapper); 16 | 17 | _debugInspector = React.render( 18 | , 19 | _debugPopupWrapper 20 | ); 21 | } 22 | 23 | function deinit() { 24 | if (_debugPopupWrapper) { 25 | React.unmountComponentAtNode(_debugPopupWrapper); 26 | document.body.removeChild(_debugPopupWrapper); 27 | _debugPopupWrapper = null; 28 | } 29 | _debugInspector = null; 30 | } 31 | 32 | let triggerCalled = false; 33 | 34 | export default function(options) { 35 | const config = Object.assign({ 36 | enabledTrigger: cb => cb(true), 37 | getPanels: panels => panels, 38 | showPin: () => true 39 | }, options.imports[0]); 40 | 41 | function enter(e) { 42 | if (!_enabled || !config.showPin(this)) return; 43 | var rect = e.target.getBoundingClientRect(); 44 | _debugInspector.addComponent(this, rect); 45 | } 46 | 47 | function leave(e) { 48 | if (!_enabled || !config.showPin(this)) return; 49 | if (e.toElement === _debugInspector.getPinElement()) { 50 | return; 51 | } 52 | _debugInspector.removeComponent(this); 53 | } 54 | 55 | function wrapClass(componentClass) { 56 | function WrappedClass() { 57 | componentClass.apply(this, arguments); 58 | 59 | this.handleMouseEnter = e => { 60 | enter.call(this, e); 61 | } 62 | 63 | this.handleMouseLeave = e => { 64 | leave.call(this, e); 65 | } 66 | } 67 | 68 | WrappedClass.prototype = Object.create(componentClass.prototype); 69 | WrappedClass.prototype.constructor = componentClass; 70 | WrappedClass.displayName = componentClass.displayName || 71 | componentClass.prototype.constructor.name; 72 | 73 | Object.getOwnPropertyNames(componentClass) 74 | .filter(n => ['length', 'name', 'prototype'].indexOf(n) === -1) 75 | .forEach(n => WrappedClass[n] = componentClass[n]); 76 | 77 | WrappedClass.prototype.componentDidMount = function() { 78 | if (componentClass.prototype.componentDidMount) { 79 | componentClass.prototype.componentDidMount.call(this); 80 | } 81 | 82 | var el = React.findDOMNode(this); 83 | el.addEventListener('mouseenter', this.handleMouseEnter); 84 | el.addEventListener('mouseleave', this.handleMouseLeave); 85 | } 86 | 87 | WrappedClass.prototype.componentWillUnmount = function() { 88 | if (componentClass.prototype.componentWillUnmount) { 89 | componentClass.prototype.componentWillUnmount.call(this); 90 | } 91 | 92 | var el = React.findDOMNode(this); 93 | el.removeEventListener('mouseenter', this.handleMouseEnter); 94 | el.removeEventListener('mouseleave', this.handleMouseLeave); 95 | } 96 | 97 | return WrappedClass; 98 | } 99 | 100 | if (!triggerCalled) { 101 | config.enabledTrigger(enabled => { 102 | _enabled = enabled; 103 | if (enabled) { 104 | init(config); 105 | } else { 106 | deinit(); 107 | } 108 | }); 109 | triggerCalled = true; 110 | } 111 | 112 | return componentClass => wrapClass(componentClass); 113 | } 114 | --------------------------------------------------------------------------------