├── .babelrc
├── .gitignore
├── .markdownlint.json
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── devtools.gif
├── index.d.ts
├── jsconfig.json
├── package-lock.json
├── package.json
├── publish.js
├── src
├── Controls
│ ├── GraphControl.jsx
│ ├── LogControl.jsx
│ └── UpdatesControl.jsx
├── DevTool.js
├── Graph
│ ├── index.js
│ └── styles.js
├── Highlighter
│ ├── index.js
│ └── styles.js
├── ModalContainer
│ ├── index.jsx
│ └── styles.js
├── Panel
│ ├── PanelButton.jsx
│ ├── index.jsx
│ └── styles
│ │ ├── graph-active.svg
│ │ ├── graph.svg
│ │ ├── index.js
│ │ ├── inspect.svg
│ │ ├── log-active.svg
│ │ ├── log.svg
│ │ ├── updates-active.svg
│ │ └── updates.svg
├── RenderingMonitor.js
├── consoleLogChange.js
├── deduplicateDependencies.js
├── globalStore.js
└── index.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "react"],
3 | "plugins": ["transform-class-properties"],
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /index.js
3 | /npm-debug.log
4 | *.iml
5 | *.ipr
6 | *.iws
7 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "blanks-around-lists": false,
3 | "line-length": false
4 | }
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /src
2 | /node_modules/
3 | webpack.config.js
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 6.1.1
2 |
3 | * Fixed `Uncaught ReferenceError: propTypes is not defined` [#111](https://github.com/mobxjs/mobx-react-devtools/issues/111) through [#110](https://github.com/mobxjs/mobx-react-devtools/pull/110) by [mpedersen15](https://github.com/mpedersen15)
4 |
5 | # 6.1.0
6 |
7 | * Added more styling options to the panels, see [#100](https://github.com/mobxjs/mobx-react-devtools/pull/100) by [@janaagaard75](https://github.com/janaagaard75) and [#103](https://github.com/mobxjs/mobx-react-devtools/pull/103) by [@rokoroku](https://github.com/rokoroku)
8 | * Firefox now supports nested spy logging as well. [#105](https://github.com/mobxjs/mobx-react-devtools/pull/105) by [@wkillerud](https://github.com/wkillerud)
9 |
10 | # 6.0.3
11 |
12 | * Fixed #101: `window` not defined on node environments
13 | * Made border uniform, PR [#99](https://github.com/mobxjs/mobx-react-devtools/pull/99) by [@janaagaard75](https://github.com/janaagaard75)
14 |
15 | # 6.0.2
16 |
17 | * Fixed issue where an exception was thrown when an observer component returns a text node. Fixes [#80](https://github.com/mobxjs/mobx-react-devtools/issue/80)
18 | * Fixed issue where `isObservableMap` was undefined when logging map transtions. Through [#98](https://github.com/mobxjs/mobx-react-devtools/pull/98) by [@AMilassin](https://github.com/AMilassin)
19 |
20 | # 6.0.1
21 |
22 | * Corrected peer dependency
23 |
24 | # 6.0.0
25 |
26 | * Added compatibility with MobX 5. See [#96](https://github.com/mobxjs/mobx-react-devtools/pull/96) by [max9599](https://github.com/max9599)
27 | * Added support for tree-shaking / dead code elimination when the package is required but not rendered. [#95](https://github.com/mobxjs/mobx-react-devtools/pull/95) by [rifler](https://github.com/rifler)
28 | * Stack traces are now automatically collapsed if the browser supports it. [#79](https://github.com/mobxjs/mobx-react-devtools/pull/78) by [will-stone](https://github.com/will-stone)
29 | * Fixed several console output issues where `undefined` was printed incorrectly [#94](https://github.com/mobxjs/mobx-react-devtools/pull/94) by [srg-kostyrko](https://github.com/srg-kostyrko)
30 | * Webpack 4 is now used to build the package. See [#87](https://github.com/mobxjs/mobx-react-devtools/pull/87) by [hiroppy](https://github.com/hiroppy)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Michel Weststrate
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mobx-react-devtools
2 |
3 | _:warning: Note: This package is deprecated. Use the [browser plugin](https://github.com/mobxjs/mobx-devtools) instead. Also note that with mobx-react@6 and higher the package should no longer be needed, see [changelog](https://github.com/mobxjs/mobx-react/blob/master/CHANGELOG.md#600) :warning:_
4 |
5 | DevTools for MobX to track the rendering behavior and data dependencies of your app.
6 |
7 | 
8 |
9 | *The default position of the panel has been changed to bottom right. If you prefer top right like in the gif above, add `position="topRight"` to ` `.*
10 |
11 | ## Installation
12 |
13 | `npm install --save-dev mobx-react-devtools`
14 |
15 | or
16 |
17 | ``
18 |
19 | ## Usage
20 |
21 | Somewhere in your application, create a DevTools component:
22 |
23 | ```js
24 | import DevTools from 'mobx-react-devtools';
25 |
26 | class MyApp extends React.Component {
27 | render() {
28 | return (
29 |
30 | ...
31 |
32 |
33 | );
34 | }
35 | }
36 | ```
37 |
38 | or
39 |
40 | `React.createElement(mobxDevtools.default)`
41 |
42 | Supported props:
43 | * `highlightTimeout` — number, default: 1500
44 | * `noPanel` — boolean, if set, do not render control panel, default: false
45 | * `position` — string (or object), `topRight`, `bottomRight`, `bottomLeft` or `topLeft`, default: `bottomRight`
46 | * `className` — string, className of control panel, default: not defined
47 | * `style` — object, inline style object of control panel, default: not defined
48 |
49 | In order to be compatible with earlier versions of `mobx-react-devtools` it is also possible to assign `position` to an object containing inline styles. Using the dedicated `style` property is however recommended.
50 |
51 | From there on, after each rendering a reactive components logs the following three metrics:
52 | 1. Number of times the component did render so far
53 | 2. The time spend in the `render()` method of a component
54 | 3. The time spend from the start of the `render()` method until the changes are flushed to the DOM
55 |
56 | For each component the color indicates roughly how long the coloring took. Rendering times are cumulative; they include time spend in the children
57 | * Green: less then 25 ms
58 | * Orange: less then 100 ms
59 | * Red: rendering for this component took more than 100ms
60 |
61 | ### About log groups
62 |
63 | Note that if logging is enabled, MobX actions and reactions will appear as collapsible groups inside the browsers console.
64 | Mind that any log statements that are printed during these (re)actions will appear inside those groups as well, so that you can exactly trace when they are triggered.
65 |
66 | ### Configuration
67 |
68 | ```js
69 | import { configureDevtool } from 'mobx-react-devtools';
70 |
71 | // Any configurations are optional
72 | configureDevtool({
73 | // Turn on logging changes button programmatically:
74 | logEnabled: true,
75 | // Turn off displaying components updates button programmatically:
76 | updatesEnabled: false,
77 | // Log only changes of type `reaction`
78 | // (only affects top-level messages in console, not inside groups)
79 | logFilter: change => change.type === 'reaction',
80 | });
81 |
82 | ```
83 |
84 | There are also aliases for turning on/off devtools buttons:
85 |
86 | ```js
87 | import { setLogEnabled, setUpdatesEnabled, setGraphEnabled } from 'mobx-react-devtools';
88 |
89 | setLogEnabled(true); // same as configureDevtool({ logEnabled: true });
90 | setUpdatesEnabled(false); // same as configureDevtool({ updatesEnabled: false });
91 | setGraphEnabled(false); // same as configureDevtool({ graphEnabled: false });
92 | ```
93 |
94 | ### Custom panel design
95 |
96 | ```js
97 | import DevTools, { GraphControl, LogControl, UpdatesControl } from 'mobx-react-devtools';
98 |
99 | class MyNiceButton extends React.Component {
100 | render() {
101 | const { active, onToggle, children } = this.props;
102 | return (
103 |
104 | {children}
105 | {active ? ' on' : ' off'}
106 |
107 | );
108 | }
109 | }
110 |
111 | class MyApp extends React.Component {
112 | render() {
113 | return (
114 |
115 |
116 | {/* Include somewhere with `noPanel` prop. Is needed to display updates and modals */}
117 |
118 |
119 |
120 |
121 | {/* Must have only one child that takes props: `active` (bool), `onToggle` (func) */}
122 | Graph
123 |
124 |
125 | {/* Must have only one child that takes props: `active` (bool), `onToggle` (func) */}
126 | Log
127 |
128 |
129 | {/* Must have only one child that takes props: `active` (bool), `onToggle` (func) */}
130 | Updates
131 |
132 |
133 |
134 | );
135 | }
136 | }
137 | ```
138 |
139 | ## Roadmap
140 |
141 | * ~~Be able to turn dev-tools on and off at runtime~~
142 | * ~~Select and log dependency tree of components~~
143 | * Visualize observer tree values
144 | * ~~Be able to enable state change tracking from the extras module~~
145 |
146 | ## Changelog
147 |
148 | 5.0.1
149 |
150 | * Updated peer dependencies for mobx-react@5.0.0
151 |
152 | 5.0.0
153 |
154 | * Upgraded to MobX 4.0.0
155 |
156 | 4.2.15
157 |
158 | * Fixed error on logging & expr
159 |
160 | 4.2.14
161 |
162 | * Stopped using mobx default export (#1043)
163 |
164 | 4.2.13
165 |
166 | * Fixed warning about calling PropTypes validators directly (#62)
167 |
168 | 4.2.12
169 |
170 | * Added react 15.5/16 support
171 |
172 | 4.2.11
173 |
174 | * Added MobX 3 support
175 |
176 | 4.2.9
177 | * Fixed typescript typings (#42)
178 |
179 | 4.2.8
180 | * Fixed typescript typings (#36)
181 |
182 | 4.2.7
183 | * Fixed passing highlightTimeout from DevTools (#41)
184 |
185 | 4.2.6
186 | * Fixed “max event listeners” warning when rendering in node.js ()
187 |
188 | 4.2.5
189 | * Added ability to filter displaying changes in console
190 | * Fixed submitting forms by DevTools panel buttons (#29)
191 |
192 | 4.2.4
193 | * Added ability to change buttons state programmatically(#27)
194 |
195 | 4.2.3
196 | * Made console colors lighter (#25)
197 |
198 | 4.2.2
199 | * Added modular devtools controls (#21)
200 |
201 | 4.0.5
202 | * Added Object.assign polyfill to avoid issues with server side rendering on old node vesions
203 |
204 | 4.0.2
205 | * Make sure AMD / root imports work (#12)
206 | * DevTools should now 'work' (not do anything) when used in Isomorphic rendering (#11)
207 | * Highlighting boxes now show up at the proper coordinates when using complex stacking contexts
208 |
209 | 4.0.1
210 | * Added typescript typings (see #6)
211 | * Use (fix) uglify, by @evoyy
212 | * Added option to customize the position of the toolbar (by @evoyy)
213 |
214 | 4.0.0
215 | * Upgraded to MobX 2.0 / MobX React 3.0
216 |
--------------------------------------------------------------------------------
/devtools.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mobxjs/mobx-react-devtools/bcd0298108655c5d271e9300cf008b9f53eba07a/devtools.gif
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Turns a React component or stateless render function into a reactive component.
3 | */
4 | import React = require("react")
5 |
6 | export interface IDevToolProps {
7 | highlightTimeout?: number
8 | position?: "topRight" | "bottomRight" | "bottomLeft" | "topLeft" |
9 | {
10 | top?: number | string
11 | right?: number | string
12 | bottom?: number | string
13 | left?: number | string
14 | }
15 | noPanel?: boolean;
16 | className?: string;
17 | style?: React.CSSProperties;
18 | }
19 |
20 | export default class DevTools extends React.Component {}
21 | export class GraphControl extends React.Component<{}, {}> {}
22 | export class LogControl extends React.Component<{}, {}> {}
23 | export class UpdatesControl extends React.Component<{ highlightTimeout?: number }, {}> {}
24 |
25 | export function configureDevtool(options: {
26 | logEnabled?: boolean
27 | updatesEnabled?: boolean
28 | graphEnabled?: boolean
29 | logFilter?: (p: any) => boolean
30 | }): void
31 |
32 | export function setUpdatesEnabled(enabled: boolean): void
33 | export function setGraphEnabled(enabled: boolean): void
34 | export function setLogEnabled(enabled: boolean): void
35 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "module": "commonjs"
5 | },
6 | "exclude": [
7 | "node_modules"
8 | ]
9 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mobx-react-devtools",
3 | "version": "6.1.1",
4 | "description": "Dev-tools for MobX and React",
5 | "main": "index.js",
6 | "typings": "index",
7 | "sideEffects": false,
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/mobxjs/mobx-react-devtools.git"
11 | },
12 | "scripts": {
13 | "prettier": "prettier --write --print-width 100 --tab-width 4 --no-semi \"**/*.js\" \"**/*.ts\"",
14 | "build:dev": "cross-env NODE_ENV=development webpack",
15 | "build:prod": "cross-env NODE_ENV=production webpack",
16 | "prepublish": "npm run build:prod",
17 | "precommit": "lint-staged"
18 | },
19 | "author": "Michel Weststrate",
20 | "license": "MIT",
21 | "bugs": {
22 | "url": "https://github.com/mobxjs/mobx/issues"
23 | },
24 | "homepage": "https://mobxjs.github.io/mobx",
25 | "peerDependencies": {
26 | "mobx": "^4.3.1 || ^5.0.0",
27 | "mobx-react": "^4.0.0 || ^5.0.0"
28 | },
29 | "devDependencies": {
30 | "babel-core": "^6.5.1",
31 | "babel-loader": "^7.1.4",
32 | "babel-plugin-transform-class-properties": "^6.5.0",
33 | "babel-preset-env": "^1.7.0",
34 | "babel-preset-react": "^6.5.0",
35 | "cross-env": "^5.1.4",
36 | "file-loader": "^0.8.5",
37 | "lint-staged": "^4.1.3",
38 | "prettier": "^1.6.1",
39 | "prop-types": "^15.5.10",
40 | "url-loader": "^1.0.1",
41 | "webpack": "^4.1.1",
42 | "webpack-cli": "^2.0.12"
43 | },
44 | "keywords": [
45 | "mobx",
46 | "mobservable",
47 | "react-component",
48 | "react",
49 | "reactjs",
50 | "reactive",
51 | "devtools"
52 | ],
53 | "lint-staged": {
54 | "*.{ts,tsx,js,jsx}": [
55 | "prettier --write --print-width 100 --tab-width 4 --no-semi",
56 | "git add"
57 | ]
58 | }
59 | }
--------------------------------------------------------------------------------
/publish.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /* Publish.js, publish a new version of the npm package as found in the current directory */
3 | /* Run this file from the root of the repository */
4 |
5 | const shell = require("shelljs")
6 | const fs = require("fs")
7 | const readline = require("readline")
8 |
9 | const rl = readline.createInterface({
10 | input: process.stdin,
11 | output: process.stdout
12 | })
13 |
14 | function run(command, options) {
15 | const continueOnErrors = options && options.continueOnErrors
16 | const ret = shell.exec(command, options)
17 | if (!continueOnErrors && ret.code !== 0) {
18 | shell.exit(1)
19 | }
20 | return ret
21 | }
22 |
23 | function exit(code, msg) {
24 | console.error(msg)
25 | shell.exit(code)
26 | }
27 |
28 | async function prompt(question, defaultValue) {
29 | return new Promise(resolve => {
30 | rl.question(`${question} [${defaultValue}]: `, answer => {
31 | answer = answer && answer.trim()
32 | resolve(answer ? answer : defaultValue)
33 | })
34 | })
35 | }
36 |
37 | async function main() {
38 | const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"))
39 |
40 | // Bump version number
41 | let nrs = pkg.version.split(".")
42 | nrs[2] = 1 + parseInt(nrs[2], 10)
43 | const version = (pkg.version = await prompt(
44 | "Please specify the new package version of '" + pkg.name + "' (Ctrl^C to abort)",
45 | nrs.join(".")
46 | ))
47 | if (!version.match(/^\d+\.\d+\.\d+$/)) {
48 | exit(1, "Invalid semantic version: " + version)
49 | }
50 |
51 | // Check registry data
52 | const npmInfoRet = run(`npm info ${pkg.name} --json`, {
53 | continueOnErrors: true,
54 | silent: true
55 | })
56 | if (npmInfoRet.code === 0) {
57 | //package is registered in npm?
58 | var publishedPackageInfo = JSON.parse(npmInfoRet.stdout)
59 | if (
60 | publishedPackageInfo.versions == version ||
61 | publishedPackageInfo.versions.includes(version)
62 | ) {
63 | exit(2, "Version " + pkg.version + " is already published to npm")
64 | }
65 |
66 | fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2), "utf8")
67 |
68 | // Finally, commit and publish!
69 | run("npm publish")
70 | run(`git commit -am "Published version ${version}"`)
71 | run(`git tag ${version}`)
72 |
73 | run("git push")
74 | run("git push --tags")
75 | console.log("Published!")
76 | exit(0)
77 | } else {
78 | exit(1, pkg.name + " is not an existing npm package")
79 | }
80 | }
81 |
82 | main().catch(e => {
83 | throw e
84 | })
85 |
--------------------------------------------------------------------------------
/src/Controls/GraphControl.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { trackComponents } from 'mobx-react';
3 | import { getGlobalState, setGlobalState, eventEmitter, _handleMouseMove, _handleClick } from '../globalStore';
4 |
5 | export default class GraphControl extends Component {
6 |
7 | componentWillMount() {
8 | this.setState({});
9 | }
10 |
11 | componentDidMount() {
12 | trackComponents();
13 |
14 | eventEmitter.on('update', this.handleUpdate);
15 |
16 | if (typeof window !== 'undefined') {
17 | if (typeof document !== 'undefined') {
18 | document.body.addEventListener('mousemove', _handleMouseMove, true);
19 | document.body.addEventListener('click', _handleClick, true);
20 | }
21 | }
22 | }
23 |
24 | componentWillUnmount() {
25 | eventEmitter.removeListener('update', this.handleUpdate)
26 | if (typeof document !== 'undefined') {
27 | document.body.removeEventListener('mousemove', _handleMouseMove, true);
28 | document.body.removeEventListener('click', _handleMouseMove, true);
29 | }
30 | }
31 |
32 | handleUpdate = () => this.setState({});
33 |
34 | handleToggleGraph = () => {
35 | const { graphEnabled } = getGlobalState();
36 | setGlobalState({
37 | hoverBoxes: [],
38 | graphEnabled: !graphEnabled,
39 | });
40 | };
41 |
42 | render() {
43 | const { graphEnabled } = getGlobalState();
44 | const { children } = this.props;
45 | return React.cloneElement(children, {
46 | onToggle: this.handleToggleGraph,
47 | active: graphEnabled,
48 | });
49 | }
50 | }
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/Controls/LogControl.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { getGlobalState, setGlobalState, eventEmitter, restoreLogFromLocalstorage } from '../globalStore';
3 |
4 | export default class LogControl extends Component {
5 |
6 | componentDidMount() {
7 | eventEmitter.on('update', this.handleUpdate);
8 | restoreLogFromLocalstorage();
9 | }
10 |
11 | componentWillUnmount() {
12 | eventEmitter.removeListener('update', this.handleUpdate)
13 | }
14 |
15 | handleUpdate = () => {
16 | this.setState({});
17 | };
18 |
19 | handleToggleLog = () => {
20 | const { logEnabled } = getGlobalState();
21 | setGlobalState({ logEnabled: !logEnabled })
22 | };
23 |
24 | render() {
25 | const { logEnabled } = getGlobalState();
26 | const { children } = this.props;
27 | return React.cloneElement(children, {
28 | onToggle: this.handleToggleLog,
29 | active: logEnabled,
30 | });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Controls/UpdatesControl.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import RenderingMonitor from '../RenderingMonitor';
4 | import { getGlobalState, setGlobalState, eventEmitter, restoreUpdatesFromLocalstorage } from '../globalStore';
5 |
6 | export default class UpdatesControl extends Component {
7 |
8 | static propTypes = {
9 | highlightTimeout: PropTypes.number,
10 | };
11 |
12 | static defaultProps = {
13 | highlightTimeout: 1500,
14 | };
15 |
16 | componentDidMount() {
17 | eventEmitter.on('update', this.handleUpdate);
18 | const { highlightTimeout } = this.props;
19 | this.renderingMonitor = new RenderingMonitor({ highlightTimeout });
20 | restoreUpdatesFromLocalstorage();
21 | }
22 |
23 | componentWillUnmount() {
24 | eventEmitter.removeListener('update', this.handleUpdate)
25 | this.renderingMonitor.dispose();
26 | }
27 |
28 | handleUpdate = () => this.setState({});
29 |
30 | handleToggleUpdates = () => {
31 | const { updatesEnabled } = getGlobalState();
32 | setGlobalState({ updatesEnabled: !updatesEnabled });
33 | };
34 |
35 | render() {
36 | const { updatesEnabled } = getGlobalState();
37 | const { children } = this.props;
38 | return React.cloneElement(children, {
39 | onToggle: this.handleToggleUpdates,
40 | active: updatesEnabled,
41 | });
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/DevTool.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import PropTypes from "prop-types"
3 | import { getGlobalState, restoreState, eventEmitter } from "./globalStore"
4 | import Panel from "./Panel"
5 | import Highlighter from "./Highlighter"
6 | import Graph from "./Graph"
7 |
8 | export default class DevTool extends Component {
9 | static propTypes = {
10 | highlightTimeout: PropTypes.number,
11 | noPanel: PropTypes.bool,
12 | className: PropTypes.string,
13 | style: PropTypes.object,
14 | position: PropTypes.oneOfType(
15 | PropTypes.oneOf(['topRight', 'bottomRight', 'bottomLeft', 'topLeft']),
16 | PropTypes.shape({
17 | top: PropTypes.string,
18 | right: PropTypes.string,
19 | bottom: PropTypes.string,
20 | left: PropTypes.string,
21 | })
22 | )
23 | }
24 |
25 | static defaultProps = {
26 | noPanel: false,
27 | className: ''
28 | }
29 |
30 | componentWillMount() {
31 | this.setState(getGlobalState())
32 | }
33 |
34 | componentDidMount() {
35 | eventEmitter.on("update", this.handleUpdate)
36 | }
37 |
38 | componentWillUnmount() {
39 | eventEmitter.removeListener("update", this.handleUpdate)
40 | }
41 |
42 | handleUpdate = () => this.setState(getGlobalState())
43 |
44 | handleToggleGraph = () => {
45 | this.setState({
46 | hoverBoxes: [],
47 | graphEnabled: !this.state.graphEnabled
48 | })
49 | }
50 |
51 | render() {
52 | const { noPanel, highlightTimeout, className, style } = this.props
53 | const { renderingBoxes, hoverBoxes } = this.state
54 | return (
55 |
56 | {noPanel !== true && (
57 |
63 | )}
64 |
65 |
66 |
67 | )
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Graph/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import ModalContainer from "../ModalContainer"
3 | import { getGlobalState, setGlobalState, eventEmitter } from "../globalStore"
4 | import * as styles from "./styles.js"
5 |
6 | export default class Graph extends Component {
7 | componentDidMount() {
8 | eventEmitter.on("update", this.handleUpdate)
9 | }
10 |
11 | componentWillUnmount() {
12 | eventEmitter.removeListener("update", this.handleUpdate)
13 | }
14 |
15 | handleUpdate = () => this.setState({})
16 |
17 | handleClose = () => setGlobalState({ dependencyTree: undefined })
18 |
19 | renderTreeItem({ name, dependencies }, isLast, isRoot) {
20 | return (
21 |
22 |
{name}
23 | {dependencies && (
24 |
25 | {dependencies.map((dependency, i) =>
26 | this.renderTreeItem(
27 | dependency,
28 | /*isLast:*/ i == dependencies.length - 1
29 | )
30 | )}
31 |
32 | )}
33 | {!isRoot &&
}
34 | {!isRoot && (
35 |
42 | )}
43 |
44 | )
45 | }
46 |
47 | render() {
48 | const { dependencyTree } = getGlobalState()
49 | return (
50 |
51 | {dependencyTree && (
52 |
53 |
54 | ×
55 |
56 |
57 | {this.renderTreeItem(
58 | dependencyTree,
59 | /*isLast:*/ true,
60 | /*isRoot:*/ true
61 | )}
62 |
63 |
64 | )}
65 |
66 | )
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Graph/styles.js:
--------------------------------------------------------------------------------
1 | export const graph = {
2 | background: "white",
3 | padding: "40px"
4 | }
5 |
6 | export const close = {
7 | color: "rgba(0, 0, 0, 0.2)",
8 | fontSize: "36px",
9 | position: "absolute",
10 | top: "5px",
11 | right: "5px",
12 | width: "40px",
13 | height: "40px",
14 | lineHeight: "34px",
15 | textAlign: "center",
16 | cursor: "pointer",
17 | ":hover": {
18 | color: "rgba(0, 0, 0, 0.5)"
19 | }
20 | }
21 |
22 | /* TREE */
23 |
24 | export const tree = {
25 | position: "relative",
26 | paddingLeft: "25px"
27 | }
28 |
29 | export const item = {
30 | position: "relative"
31 | }
32 |
33 | export const box = {
34 | padding: "4px 10px",
35 | background: "rgba(0, 0, 0, 0.05)",
36 | display: "inline-block",
37 | marginBottom: "8px",
38 | color: "#000",
39 | root: {
40 | fontSize: "15px",
41 | fontWeight: "bold",
42 | padding: "6px 13px"
43 | }
44 | }
45 |
46 | export const itemHorisontalDash = {
47 | position: "absolute",
48 | left: "-12px",
49 | borderTop: "1px solid rgba(0, 0, 0, 0.2)",
50 | top: "14px",
51 | width: "12px",
52 | height: "0"
53 | }
54 |
55 | export const itemVericalStick = {
56 | position: "absolute",
57 | left: "-12px",
58 | borderLeft: "1px solid rgba(0, 0, 0, 0.2)",
59 | height: "100%",
60 | width: 0,
61 | top: "-8px",
62 | short: {
63 | height: "23px"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Highlighter/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import PropTypes from "prop-types"
3 | import * as styles from "./styles"
4 |
5 | export default class Highlighter extends Component {
6 | static propTypes = {
7 | boxes: PropTypes.arrayOf(
8 | PropTypes.shape({
9 | type: PropTypes.oneOf(["rendering", "hover"]).isRequired,
10 | x: PropTypes.number.isRequired,
11 | y: PropTypes.number.isRequired,
12 | width: PropTypes.number.isRequired,
13 | height: PropTypes.number.isRequired,
14 | renderInfo: PropTypes.shape({
15 | count: PropTypes.number.isRequired,
16 | renderTime: PropTypes.number.isRequired,
17 | totalTime: PropTypes.number.isRequired,
18 | cost: PropTypes.oneOf(["cheap", "acceptable", "expensive"]).isRequired
19 | }),
20 | lifeTime: PropTypes.number.isRequired
21 | })
22 | ).isRequired
23 | }
24 |
25 | renderBox(box) {
26 | switch (box.type) {
27 | case "rendering":
28 | let renderingCostStyle = styles.rendering[box.renderInfo.cost] || {}
29 | return (
30 |
34 | setTimeout(() => {
35 | if (el) el.style.opacity = 0
36 | }, box.lifeTime - 500)}
37 | style={Object.assign({}, styles.box, styles.rendering, renderingCostStyle, {
38 | left: box.x,
39 | top: box.y,
40 | width: box.width,
41 | height: box.height
42 | })}
43 | >
44 |
45 | {box.renderInfo.count}x | {box.renderInfo.renderTime} /{" "}
46 | {box.renderInfo.totalTime} ms
47 |
48 |
49 | )
50 |
51 | case "hover":
52 | return (
53 |
62 | )
63 |
64 | default:
65 | throw new Error()
66 | }
67 | }
68 |
69 | render() {
70 | const { boxes } = this.props
71 |
72 | return {boxes.map(box => this.renderBox(box))}
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Highlighter/styles.js:
--------------------------------------------------------------------------------
1 | export const box = {
2 | display: "block",
3 | position: "fixed",
4 | zIndex: "64998",
5 | minWidth: "60px",
6 | outline: "3px solid",
7 | pointerEvents: "none",
8 | transition: "opacity 500ms ease-in"
9 | }
10 |
11 | export const text = {
12 | fontFamily: "verdana, sans-serif",
13 | padding: "0 4px 2px",
14 | color: "rgba(0, 0, 0, 0.6)",
15 | fontSize: "10px",
16 | lineHeight: "12px",
17 | pointerEvents: "none",
18 | float: "right",
19 | borderBottomRightRadius: "2px",
20 | maxWidth: "100%",
21 | maxHeight: "100%",
22 | overflow: "hidden",
23 | whiteSpace: "nowrap",
24 | textOverflow: "ellipsis"
25 | }
26 |
27 | export const rendering = {
28 | cheap: {
29 | outlineColor: "rgba(182, 218, 146, 0.75)",
30 | text: {
31 | backgroundColor: "rgba(182, 218, 146, 0.75)"
32 | }
33 | },
34 | acceptable: {
35 | outlineColor: "rgba(228, 195, 66, 0.85)",
36 | text: {
37 | backgroundColor: "rgba(228, 195, 66, 0.85)"
38 | }
39 | },
40 | expensive: {
41 | outlineColor: "rgba(228, 171, 171, 0.95)",
42 | text: {
43 | backgroundColor: "rgba(228, 171, 171, 0.95)"
44 | }
45 | }
46 | }
47 |
48 | export const hover = {
49 | outlineColor: "rgba(128, 128, 255, 0.5)"
50 | }
51 |
--------------------------------------------------------------------------------
/src/ModalContainer/index.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, {Component} from 'react';
3 | import * as styles from './styles';
4 |
5 | export default class ModalContainer extends Component {
6 |
7 | static propTypes = {
8 | children: PropTypes.node,
9 | onOverlayClick: PropTypes.func.isRequired,
10 | };
11 |
12 | componentDidUpdate(prevProps) {
13 | const html = document.body.parentNode;
14 | if (prevProps.children && !this.props.children) {
15 | // Disappeared
16 | html.style.borderRight = null;
17 | html.style.overflow = null;
18 | } else if (!prevProps.children && this.props.children) {
19 | // Appeared
20 | const prevTotalWidth = html.offsetWidth;
21 | html.style.overflow = 'hidden';
22 | const nextTotalWidth = html.offsetWidth;
23 | const rightOffset = Math.max(0, nextTotalWidth - prevTotalWidth);
24 | html.style.borderRight = `${rightOffset}px solid transparent`
25 | }
26 | }
27 |
28 | stopPropagation = e => e.stopPropagation();
29 |
30 | render() {
31 | const { children, onOverlayClick } = this.props;
32 | if (!children) return null;
33 | return (
34 |
38 |
43 | {children}
44 |
45 |
46 | );
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/src/ModalContainer/styles.js:
--------------------------------------------------------------------------------
1 | export const overlay = {
2 | position: "fixed",
3 | top: 0,
4 | right: 0,
5 | bottom: 0,
6 | left: 0,
7 | zIndex: 66000,
8 | overflow: "auto",
9 | WebkitOverflowScrolling: "touch",
10 | outline: 0,
11 | backgroundColor: "rgba(40, 40, 50, 0.5)",
12 | transformOrigin: "50% 25%"
13 | }
14 |
15 | export const modal = {
16 | position: "relative",
17 | width: "auto",
18 | margin: "5% 10%",
19 | zIndex: 1060
20 | }
21 |
--------------------------------------------------------------------------------
/src/Panel/PanelButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import * as styles from './styles';
4 |
5 | export default class PanelButton extends Component {
6 |
7 | static props = {
8 | onToggle: PropTypes.bool.isRequired,
9 | active: PropTypes.bool.isRequired,
10 | name: PropTypes.oneOf(['buttonUpdates', 'buttonGraph', 'buttonLog']).isRequired,
11 | };
12 |
13 | state = {
14 | hovered: false,
15 | };
16 |
17 | handleMouseOver = () => this.setState({ hovered: true });
18 | handleMouseOut = () => this.setState({ hovered: false });
19 |
20 | render() {
21 | const { active, id, onToggle } = this.props;
22 | const { hovered } = this.state;
23 |
24 | const additionalStyles = (() => {
25 | switch (id) {
26 | case 'buttonUpdates': return active ? styles.buttonUpdatesActive : styles.buttonUpdates;
27 | case 'buttonGraph': return active ? styles.buttonGraphActive : styles.buttonGraph;
28 | case 'buttonLog': return active ? styles.buttonLogActive : styles.buttonLog;
29 | }
30 | })();
31 |
32 | const title = (() => {
33 | switch (id) {
34 | case 'buttonUpdates': return 'Visualize component re-renders';
35 | case 'buttonGraph': return 'Select a component and show its dependency tree';
36 | case 'buttonLog': return 'Log all MobX state changes and reactions to the browser console (use F12 to show / hide the console). Use Chrome / Chromium for an optimal experience';
37 | }
38 | })();
39 |
40 | const finalSyles = Object.assign(
41 | {},
42 | styles.button,
43 | additionalStyles,
44 | active && styles.button.active,
45 | hovered && styles.button.hover
46 | );
47 |
48 | return (
49 |
57 | );
58 | }
59 | };
60 |
--------------------------------------------------------------------------------
/src/Panel/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { eventEmitter } from '../globalStore';
4 | import GraphControl from '../Controls/GraphControl';
5 | import LogControl from '../Controls/LogControl';
6 | import UpdatesControl from '../Controls/UpdatesControl';
7 | import PanelButton from './PanelButton';
8 | import * as styles from './styles';
9 |
10 | export default class Panel extends Component {
11 |
12 | static propTypes = {
13 | highlightTimeout: PropTypes.number,
14 | className: PropTypes.string,
15 | style: PropTypes.object
16 | };
17 |
18 | static defaultProps = {
19 | className: '',
20 | position: 'bottomRight'
21 | }
22 |
23 | componentDidMount() {
24 | eventEmitter.on('update', this.handleUpdate);
25 | }
26 |
27 | componentWillUnmount() {
28 | eventEmitter.removeListener('update', this.handleUpdate)
29 | }
30 |
31 | handleUpdate = () => this.setState({});
32 |
33 | render() {
34 | const { position, highlightTimeout, className, style } = this.props;
35 |
36 | const additionalPanelStyles = {};
37 |
38 | if (typeof position === 'string') {
39 | switch (position) {
40 | case 'topRight':
41 | additionalPanelStyles.top = '-2px';
42 | additionalPanelStyles.right = '20px';
43 | break;
44 | case 'bottomRight':
45 | additionalPanelStyles.bottom = '-2px';
46 | additionalPanelStyles.right = '20px';
47 | break;
48 | case 'bottomLeft':
49 | additionalPanelStyles.bottom = '-2px';
50 | additionalPanelStyles.left = '20px';
51 | break;
52 | case 'topLeft':
53 | additionalPanelStyles.top = '-2px';
54 | additionalPanelStyles.left = '20px';
55 | break;
56 | }
57 | } else {
58 | Object.assign(additionalPanelStyles, position);
59 | }
60 |
61 | return (
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | );
76 | }
77 | };
78 |
--------------------------------------------------------------------------------
/src/Panel/styles/graph-active.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/Panel/styles/graph.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/Panel/styles/index.js:
--------------------------------------------------------------------------------
1 | export const panel = {
2 | position: "fixed",
3 | height: "26px",
4 | backgroundColor: "#fff",
5 | color: "rgba(0, 0, 0, 0.8)",
6 | borderRadius: "2px",
7 | borderStyle: "solid",
8 | borderWidth: "1px",
9 | borderColor: "rgba(0, 0, 0, 0.1)",
10 | zIndex: "65000",
11 | fontFamily: "Helvetica, sans-serif",
12 | display: "flex",
13 | padding: "0 5px"
14 | }
15 |
16 | export const button = {
17 | opacity: 0.45,
18 | background: "transparent none center / 16px 16px no-repeat",
19 | width: "26px",
20 | margin: "0 10px",
21 | cursor: "pointer",
22 | border: "none",
23 | hover: {
24 | opacity: 0.7
25 | },
26 | active: {
27 | opacity: 1,
28 | ":hover": {
29 | opacity: 1
30 | }
31 | }
32 | }
33 |
34 | export const buttonLog = {
35 | backgroundImage: `url(${require("./log.svg")})`
36 | }
37 |
38 | export const buttonLogActive = {
39 | backgroundImage: `url(${require("./log-active.svg")})`
40 | }
41 |
42 | export const buttonUpdates = {
43 | backgroundImage: `url(${require("./updates.svg")})`
44 | }
45 |
46 | export const buttonUpdatesActive = {
47 | backgroundImage: `url(${require("./updates-active.svg")})`
48 | }
49 |
50 | export const buttonGraph = {
51 | backgroundImage: `url(${require("./graph.svg")})`
52 | }
53 |
54 | export const buttonGraphActive = {
55 | backgroundImage: `url(${require("./graph-active.svg")})`
56 | }
57 |
--------------------------------------------------------------------------------
/src/Panel/styles/inspect.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/Panel/styles/log-active.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Panel/styles/log.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Panel/styles/updates-active.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/Panel/styles/updates.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/RenderingMonitor.js:
--------------------------------------------------------------------------------
1 | import { renderReporter } from "mobx-react"
2 | import { getGlobalState, setGlobalState } from "./globalStore"
3 |
4 | const getCost = renderTime => {
5 | switch (true) {
6 | case renderTime < 25:
7 | return "cheap"
8 | case renderTime < 100:
9 | return "acceptable"
10 | default:
11 | return "expensive"
12 | }
13 | }
14 |
15 | export default class RenderingMonitor {
16 | _boxesRegistry = typeof WeakMap !== "undefined" ? new WeakMap() : new Map()
17 |
18 | constructor({ highlightTimeout }) {
19 | this._renderReporterDisposer = renderReporter.on(report => {
20 | if (getGlobalState().updatesEnabled !== true) return
21 | switch (report.event) {
22 | case "render":
23 | if (!report.node || !report.node.getBoundingClientRect || isNaN(report.renderTime)) return
24 | const offset = report.node.getBoundingClientRect()
25 | const box = this.getBoxForNode(report.node)
26 | box.type = "rendering"
27 | box.y = offset.top
28 | box.x = offset.left
29 | box.width = offset.width
30 | box.height = offset.height
31 | box.renderInfo = {
32 | count: (box.renderInfo && ++box.renderInfo.count) || 1,
33 | renderTime: report.renderTime,
34 | totalTime: report.totalTime,
35 | cost: getCost(report.renderTime)
36 | }
37 | box.lifeTime = highlightTimeout
38 |
39 | let renderingBoxes = getGlobalState().renderingBoxes
40 | if (renderingBoxes.indexOf(box) === -1)
41 | renderingBoxes = renderingBoxes.concat([box])
42 | setGlobalState({ renderingBoxes })
43 | if (box._timeout) clearTimeout(box._timeout)
44 | box._timeout = setTimeout(
45 | () => this.removeBox(report.node, true),
46 | highlightTimeout
47 | )
48 | return
49 |
50 | case "destroy":
51 | this.removeBox(report.node)
52 | this._boxesRegistry.delete(report.node)
53 | return
54 |
55 | default:
56 | return
57 | }
58 | })
59 | }
60 |
61 | getBoxForNode(node) {
62 | if (this._boxesRegistry.has(node)) return this._boxesRegistry.get(node)
63 | const box = {
64 | id: Math.random()
65 | .toString(32)
66 | .substr(2)
67 | }
68 | this._boxesRegistry.set(node, box)
69 | return box
70 | }
71 |
72 | dispose() {
73 | this._renderReporterDisposer()
74 | }
75 |
76 | removeBox(node) {
77 | if (this._boxesRegistry.has(node) === false) return
78 | let renderingBoxes = getGlobalState().renderingBoxes
79 | const index = renderingBoxes.indexOf(this._boxesRegistry.get(node))
80 | if (index !== -1) {
81 | renderingBoxes = renderingBoxes.slice(0, index).concat(renderingBoxes.slice(index + 1))
82 | setGlobalState({ renderingBoxes })
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/consoleLogChange.js:
--------------------------------------------------------------------------------
1 | import { $mobx, isObservableArray, isObservableObject, isObservableMap, getDebugName } from "mobx"
2 |
3 | let advisedToUseModernBrowser = false
4 |
5 | let currentDepth = 0
6 | let isInsideSkippedGroup = false
7 |
8 | export default function consoleLogChange(change, filter) {
9 | if (
10 | advisedToUseModernBrowser === false &&
11 | typeof navigator !== "undefined" &&
12 | navigator.userAgent.indexOf("Chrome") === -1 &&
13 | navigator.userAgent.indexOf("Firefox") === -1
14 | ) {
15 | console.warn("The output of the MobX logger is optimized for browsers with a modern console API like Chrome and Firefox")
16 | advisedToUseModernBrowser = true
17 | }
18 |
19 | const isGroupStart = change.spyReportStart === true
20 | const isGroupEnd = change.spyReportEnd === true
21 |
22 | let show
23 | if (currentDepth === 0) {
24 | show = filter(change)
25 | if (isGroupStart && !show) {
26 | isInsideSkippedGroup = true
27 | }
28 | } else if (isGroupEnd && isInsideSkippedGroup && currentDepth === 1) {
29 | show = false
30 | isInsideSkippedGroup = false
31 | } else {
32 | show = isInsideSkippedGroup !== true
33 | }
34 |
35 | if (show && isGroupEnd) {
36 | groupEnd(change.time)
37 | } else if (show) {
38 | const logNext = isGroupStart ? group : log
39 | switch (change.type) {
40 | case "action":
41 | // name, target, arguments, fn
42 | logNext(
43 | `%caction '%s' %s`,
44 | "color:dodgerblue",
45 | change.name,
46 | autoWrap("(", getNameForThis(change.target))
47 | )
48 | log(change.arguments)
49 | trace()
50 | break
51 | case "transaction":
52 | // name, target
53 | logNext(
54 | `%ctransaction '%s' %s`,
55 | "color:gray",
56 | change.name,
57 | autoWrap("(", getNameForThis(change.target))
58 | )
59 | break
60 | case "scheduled-reaction":
61 | // object
62 | logNext(`%cscheduled async reaction '%s'`, "color:#10a210", change.name)
63 | break
64 | case "reaction":
65 | // object, fn
66 | logNext(`%creaction '%s'`, "color:#10a210", change.name)
67 | // dir({
68 | // fn: change.fn
69 | // });
70 | trace()
71 | break
72 | case "compute":
73 | // object, target, fn
74 | group(
75 | `%ccomputed '%s' %s`,
76 | "color:#10a210",
77 | change.name,
78 | autoWrap("(", getNameForThis(change.target))
79 | )
80 | // dir({
81 | // fn: change.fn,
82 | // target: change.target
83 | // });
84 | groupEnd()
85 | break
86 | case "error":
87 | // message
88 | logNext("%cerror: %s", "color:tomato", change.message)
89 | trace()
90 | closeGroupsOnError()
91 | break
92 | case "update":
93 | // (array) object, index, newValue, oldValue
94 | // (map, obbject) object, name, newValue, oldValue
95 | // (value) object, newValue, oldValue
96 | if (isObservableArray(change.object)) {
97 | logNext(
98 | "updated '%s[%s]': %s (was: %s)",
99 | observableName(change.object),
100 | change.index,
101 | formatValue(change.newValue),
102 | formatValue(change.oldValue)
103 | )
104 | } else if (isObservableObject(change.object) || isObservableMap(change.object)) {
105 | logNext(
106 | "updated '%s.%s': %s (was: %s)",
107 | change.name,
108 | change.key,
109 | formatValue(change.newValue),
110 | formatValue(change.oldValue)
111 | )
112 | } else {
113 | logNext(
114 | "updated '%s': %s (was: %s)",
115 | change.name,
116 | formatValue(change.newValue),
117 | formatValue(change.oldValue)
118 | )
119 | }
120 | dir({
121 | newValue: change.newValue,
122 | oldValue: change.oldValue
123 | })
124 | trace()
125 | break
126 | case "splice":
127 | // (array) object, index, added, removed, addedCount, removedCount
128 | logNext(
129 | "spliced '%s': index %d, added %d, removed %d",
130 | observableName(change.object),
131 | change.index,
132 | change.addedCount,
133 | change.removedCount
134 | )
135 | dir({
136 | added: change.added,
137 | removed: change.removed
138 | })
139 | trace()
140 | break
141 | case "add":
142 | // (map, object) object, name, newValue
143 | logNext("set '%s.%s': %s", change.name, change.key, formatValue(change.newValue))
144 | dir({
145 | newValue: change.newValue
146 | })
147 | trace()
148 | break
149 | case "delete":
150 | case "remove":
151 | // (map) object, name, oldValue
152 | logNext(
153 | "removed '%s.%s' (was %s)",
154 | change.name,
155 | change.key,
156 | formatValue(change.oldValue)
157 | )
158 | dir({
159 | oldValue: change.oldValue
160 | })
161 | trace()
162 | break
163 | case "create":
164 | // (value) object, newValue
165 | logNext("set '%s': %s", change.name, formatValue(change.newValue))
166 | dir({
167 | newValue: change.newValue
168 | })
169 | trace()
170 | break
171 | default:
172 | // generic fallback for future events
173 | logNext(change.type)
174 | dir(change)
175 | break
176 | }
177 | }
178 |
179 | if (isGroupStart) currentDepth++
180 | if (isGroupEnd) currentDepth--
181 | }
182 |
183 | const consoleSupportsGroups = typeof console.groupCollapsed === "function"
184 | let currentlyLoggedDepth = 0
185 |
186 | function group() {
187 | // TODO: firefox does not support formatting in groupStart methods..
188 | console[consoleSupportsGroups ? "groupCollapsed" : "log"].apply(console, arguments)
189 | currentlyLoggedDepth++
190 | }
191 |
192 | function groupEnd(time) {
193 | currentlyLoggedDepth--
194 | if (typeof time === "number") {
195 | log("%ctotal time: %sms", "color:gray", time)
196 | }
197 | if (consoleSupportsGroups) console.groupEnd()
198 | }
199 |
200 | function log() {
201 | console.log.apply(console, arguments)
202 | }
203 |
204 | function dir() {
205 | console.dir.apply(console, arguments)
206 | }
207 |
208 | function trace() {
209 | consoleSupportsGroups && console.groupCollapsed("stack")
210 | // TODO: needs wrapping in firefox?
211 | console.trace("stack") // TODO: use stacktrace.js or similar and strip off unrelevant stuff?
212 | consoleSupportsGroups && console.groupEnd()
213 | }
214 |
215 | function closeGroupsOnError() {
216 | for (let i = 0, m = currentlyLoggedDepth; i < m; i++) groupEnd()
217 | }
218 |
219 | const closeToken = {
220 | '"': '"',
221 | "'": "'",
222 | "(": ")",
223 | "[": "]",
224 | "<": "]",
225 | "#": ""
226 | }
227 |
228 | function autoWrap(token, value) {
229 | if (!value) return ""
230 | return (token || "") + value + (closeToken[token] || "")
231 | }
232 |
233 | function observableName(object) {
234 | if (!object) return String(object)
235 | return getDebugName(object)
236 | }
237 |
238 | function formatValue(value) {
239 | if (isPrimitive(value)) {
240 | if (typeof value === "string" && value.length > 100) return value.substr(0, 97) + "..."
241 | return value
242 | } else return autoWrap("(", getNameForThis(value))
243 | }
244 |
245 | function getNameForThis(who) {
246 | if (who === null || who === undefined) {
247 | return ""
248 | } else if (who && typeof who === "object") {
249 | if (who && who[$mobx]) {
250 | return who[$mobx].name
251 | } else if (who.constructor) {
252 | return who.constructor.name || "object"
253 | }
254 | }
255 | return `${typeof who}`
256 | }
257 |
258 | function isPrimitive(value) {
259 | return (
260 | value === null ||
261 | value === undefined ||
262 | typeof value === "string" ||
263 | typeof value === "number" ||
264 | typeof value === "boolean"
265 | )
266 | }
267 |
--------------------------------------------------------------------------------
/src/deduplicateDependencies.js:
--------------------------------------------------------------------------------
1 | const deduplicateDependencies = depTree => {
2 | if (!depTree.dependencies) return undefined
3 |
4 | for (var i = depTree.dependencies.length - 1; i >= 0; i--) {
5 | var name = depTree.dependencies[i].name
6 | for (var i2 = i - 1; i2 >= 0; i2--) {
7 | if (depTree.dependencies[i2].name === name) {
8 | depTree.dependencies[i2].dependencies = [].concat(
9 | depTree.dependencies[i2].dependencies || [],
10 | depTree.dependencies[i].dependencies || []
11 | )
12 | depTree.dependencies.splice(i, 1)
13 | break
14 | }
15 | }
16 | }
17 | depTree.dependencies.forEach(deduplicateDependencies)
18 | }
19 |
20 | export default deduplicateDependencies
21 |
--------------------------------------------------------------------------------
/src/globalStore.js:
--------------------------------------------------------------------------------
1 | import { $mobx, spy, getDependencyTree } from "mobx"
2 | import { componentByNodeRegistery } from "mobx-react"
3 | import EventEmmiter from "events"
4 | import deduplicateDependencies from "./deduplicateDependencies"
5 | import consoleLogChange from "./consoleLogChange"
6 |
7 | const LS_UPDATES_KEY = "mobx-react-devtool__updatesEnabled"
8 | const LS_LOG_KEY = "mobx-react-devtool__logEnabled"
9 |
10 | let state = {
11 | updatesEnabled: false,
12 | graphEnabled: false,
13 | logEnabled: false,
14 | hoverBoxes: [],
15 | renderingBoxes: [],
16 | logFilter: () => true
17 | }
18 |
19 | export const eventEmitter = new EventEmmiter()
20 |
21 | eventEmitter.setMaxListeners(Infinity)
22 |
23 | let loggerDisposer
24 |
25 | export const setGlobalState = newState => {
26 | if (state.logEnabled !== newState.logEnabled) {
27 | if (newState.logEnabled === true) {
28 | if (loggerDisposer) loggerDisposer()
29 | loggerDisposer = spy(change => consoleLogChange(change, state.logFilter))
30 | } else if (newState.logEnabled === false && loggerDisposer) {
31 | loggerDisposer()
32 | }
33 | }
34 |
35 | if (typeof window !== "undefined" && window.localStorage) {
36 | if (newState.updatesEnabled === true) {
37 | window.localStorage.setItem(LS_UPDATES_KEY, "YES")
38 | } else if (newState.updatesEnabled === false) {
39 | window.localStorage.removeItem(LS_UPDATES_KEY)
40 | }
41 | if (newState.logEnabled === true) {
42 | window.localStorage.setItem(LS_LOG_KEY, "YES")
43 | } else if (newState.logEnabled === false) {
44 | window.localStorage.removeItem(LS_LOG_KEY)
45 | }
46 | }
47 |
48 | if (newState.graphEnabled === false) {
49 | newState.hoverBoxes = []
50 | }
51 |
52 | state = Object.assign({}, state, newState)
53 |
54 | eventEmitter.emit("update")
55 | }
56 |
57 | export const getGlobalState = () => state
58 |
59 | export const restoreUpdatesFromLocalstorage = () => {
60 | if (typeof window !== "undefined" && window.localStorage) {
61 | const updatesEnabled = window.localStorage.getItem(LS_UPDATES_KEY) === "YES"
62 | setGlobalState({ updatesEnabled })
63 | }
64 | }
65 | export const restoreLogFromLocalstorage = () => {
66 | if (typeof window !== "undefined" && window.localStorage) {
67 | const logEnabled = window.localStorage.getItem(LS_LOG_KEY) === "YES"
68 | setGlobalState({ logEnabled })
69 | }
70 | }
71 |
72 | const findComponentAndNode = target => {
73 | let node = target
74 | let component
75 | while (node) {
76 | component = componentByNodeRegistery.get(node)
77 | if (component) return { component, node }
78 | node = node.parentNode
79 | }
80 | return { component: undefined, node: undefined }
81 | }
82 |
83 | export const _handleMouseMove = e => {
84 | if (state.graphEnabled) {
85 | const target = e.target
86 | const node = findComponentAndNode(target).node
87 | if (node && node.getBoundingClientRect) {
88 | const coordinates = node.getBoundingClientRect()
89 | setGlobalState({
90 | hoverBoxes: [
91 | {
92 | id: "the hovered node",
93 | type: "hover",
94 | x: coordinates.left,
95 | y: coordinates.top,
96 | width: coordinates.width,
97 | height: coordinates.height,
98 | lifeTime: Infinity
99 | }
100 | ]
101 | })
102 | }
103 | }
104 | }
105 |
106 | export const _handleClick = e => {
107 | if (state.graphEnabled) {
108 | const target = e.target
109 | const component = findComponentAndNode(target).component
110 | if (component) {
111 | e.stopPropagation()
112 | e.preventDefault()
113 | const dependencyTree = getDependencyTree(component.render[$mobx])
114 | deduplicateDependencies(dependencyTree)
115 | setGlobalState({
116 | dependencyTree,
117 | hoverBoxes: [],
118 | graphEnabled: false
119 | })
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { setGlobalState } from "./globalStore"
2 |
3 | export { default } from "./DevTool"
4 | export { default as GraphControl } from "./Controls/GraphControl"
5 | export { default as LogControl } from "./Controls/LogControl"
6 | export { default as UpdatesControl } from "./Controls/UpdatesControl"
7 |
8 | export const configureDevtool = ({ logEnabled, updatesEnabled, graphEnabled, logFilter }) => {
9 | const config = {}
10 | if (logEnabled !== undefined) {
11 | config.logEnabled = Boolean(logEnabled)
12 | }
13 | if (updatesEnabled !== undefined) {
14 | config.updatesEnabled = Boolean(updatesEnabled)
15 | }
16 | if (graphEnabled !== undefined) {
17 | config.graphEnabled = Boolean(graphEnabled)
18 | }
19 | if (typeof logFilter === "function") {
20 | config.logFilter = logFilter
21 | }
22 | setGlobalState(config)
23 | }
24 |
25 | export const setUpdatesEnabled = updatesEnabled => configureDevtool({ updatesEnabled })
26 | export const setGraphEnabled = graphEnabled => configureDevtool({ graphEnabled })
27 | export const setLogEnabled = logEnabled => configureDevtool({ logEnabled })
28 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const { join } = require("path")
2 | const webpack = require("webpack")
3 |
4 | module.exports = {
5 | mode: process.env.NODE_ENV,
6 | entry: join(__dirname, "src", "index.js"),
7 | output: {
8 | libraryTarget: "umd",
9 | library: "mobxDevtools",
10 | path: __dirname,
11 | filename: "index.js",
12 | globalObject: "this"
13 | },
14 | resolve: {
15 | extensions: [".js", ".jsx"]
16 | },
17 | module: {
18 | rules: [
19 | {
20 | test: /\.jsx?$/,
21 | exclude: /node_modules/,
22 | use: "babel-loader"
23 | },
24 | {
25 | test: /\.svg$/,
26 | use: "url-loader"
27 | }
28 | ]
29 | },
30 | externals: {
31 | "mobx-react": {
32 | root: "mobxReact",
33 | commonjs: "mobx-react",
34 | commonjs2: "mobx-react",
35 | amd: "mobx-react"
36 | },
37 | react: {
38 | root: "React",
39 | commonjs: "react",
40 | commonjs2: "react",
41 | amd: "react"
42 | },
43 | mobx: "mobx"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------