Standalone NPM packages and React Native support (#335)
* Add version 4 react-devtools and react-devtools-core packages which support both React Native and e.g. Safari or iframe DOM usage. * Replaces typed operations arrays with regular arrays in order to support Hermes. This is unfortunate, since in theory a typed array buffer could be more efficiently transferred between frontend and backend for the web extension, but this never actually worked properly in v8, only Spidermonkey, and it fails entirely in Hermes so for the time being- it's been removed. * Adds support for React Native (paper renderer) * Adds a style editor for react-native and react-native-web
Brian Vaughn committed
Jul 13, 2019 at 10:05 UTC
0f2fb5badf5908ee271e0b9e7ec7f3022e31a648
77 files changed
+3911
-387
.gitignore
+1
@@ -3,6 +3,7 @@
3
/shells/browser/firefox/*.xpi
4
/shells/browser/firefox/*.pem
5
/shells/browser/shared/build
6
+/packages/react-devtools-core/dist
7
/shells/dev/dist
8
build
9
node_modules
babel.config.js
+3
@@ -24,6 +24,9 @@ module.exports = api => {
24
} else {
25
targets.chrome = minChromeVersion.toString();
26
targets.firefox = minFirefoxVersion.toString();
27
+
28
+ // This targets RN/Hermes.
29
+ targets.IE = '11';
30
}
31
const plugins = [
32
['@babel/plugin-transform-flow-strip-types'],
fixtures/standalone/index.html
new
+284
@@ -0,0 +1,284 @@
1
+<!DOCTYPE html>
2
+<html>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>TODO List</title>
6
+
7
+ <!-- DevTools -->
8
+ <script src="http://localhost:8097"></script>
9
+
10
+ <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
11
+ <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
12
+
13
+ <!-- Don't use this in production: -->
14
+ <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
15
+
16
+ <style type="text/css">
17
+ .Input {
18
+ font-size: 1rem;
19
+ padding: 0.25rem;
20
+ }
21
+
22
+ .IconButton {
23
+ padding: 0.25rem;
24
+ border: none;
25
+ background: none;
26
+ cursor: pointer;
27
+ }
28
+
29
+ .List {
30
+ margin: 0.5rem 0 0;
31
+ padding: 0;
32
+ }
33
+
34
+ .ListItem {
35
+ list-style-type: none;
36
+ }
37
+
38
+ .Label {
39
+ cursor: pointer;
40
+ padding: 0.25rem;
41
+ color: #555;
42
+ }
43
+ .Label:hover {
44
+ color: #000;
45
+ }
46
+
47
+ .IconButton {
48
+ padding: 0.25rem;
49
+ border: none;
50
+ background: none;
51
+ cursor: pointer;
52
+ }
53
+ </style>
54
+ </head>
55
+ <body>
56
+ <div id="root"></div>
57
+ <script type="text/babel">
58
+ const { Fragment, useCallback, useState } = React;
59
+
60
+ function List(props) {
61
+ const [newItemText, setNewItemText] = useState("");
62
+ const [items, setItems] = useState([
63
+ { id: 1, isComplete: true, text: "First" },
64
+ { id: 2, isComplete: true, text: "Second" },
65
+ { id: 3, isComplete: false, text: "Third" }
66
+ ]);
67
+ const [uid, setUID] = useState(4);
68
+
69
+ const handleClick = useCallback(() => {
70
+ if (newItemText !== "") {
71
+ setItems([
72
+ ...items,
73
+ {
74
+ id: uid,
75
+ isComplete: false,
76
+ text: newItemText
77
+ }
78
+ ]);
79
+ setUID(uid + 1);
80
+ setNewItemText("");
81
+ }
82
+ }, [newItemText, items, uid]);
83
+
84
+ const handleKeyPress = useCallback(
85
+ event => {
86
+ if (event.key === "Enter") {
87
+ handleClick();
88
+ }
89
+ },
90
+ [handleClick]
91
+ );
92
+
93
+ const handleChange = useCallback(
94
+ event => {
95
+ setNewItemText(event.currentTarget.value);
96
+ },
97
+ [setNewItemText]
98
+ );
99
+
100
+ const removeItem = useCallback(
101
+ itemToRemove => setItems(items.filter(item => item !== itemToRemove)),
102
+ [items]
103
+ );
104
+
105
+ const toggleItem = useCallback(
106
+ itemToToggle => {
107
+ const index = items.indexOf(itemToToggle);
108
+
109
+ setItems(
110
+ items
111
+ .slice(0, index)
112
+ .concat({
113
+ ...itemToToggle,
114
+ isComplete: !itemToToggle.isComplete
115
+ })
116
+ .concat(items.slice(index + 1))
117
+ );
118
+ },
119
+ [items]
120
+ );
121
+
122
+ return (
123
+ <Fragment>
124
+ <h1>List</h1>
125
+ <input
126
+ type="text"
127
+ placeholder="New list item..."
128
+ className="Input"
129
+ value={newItemText}
130
+ onChange={handleChange}
131
+ onKeyPress={handleKeyPress}
132
+ />
133
+ <button
134
+ className="IconButton"
135
+ disabled={newItemText === ""}
136
+ onClick={handleClick}
137
+ >
138
+ <span role="img" aria-label="Add item">
139
+ ➕
140
+ </span>
141
+ </button>
142
+ <ul className="List">
143
+ {items.map(item => (
144
+ <ListItem
145
+ key={item.id}
146
+ item={item}
147
+ removeItem={removeItem}
148
+ toggleItem={toggleItem}
149
+ />
150
+ ))}
151
+ </ul>
152
+ </Fragment>
153
+ );
154
+ }
155
+
156
+ function ListItem({ item, removeItem, toggleItem }) {
157
+ const handleDelete = useCallback(() => {
158
+ removeItem(item);
159
+ }, [item, removeItem]);
160
+
161
+ const handleToggle = useCallback(() => {
162
+ toggleItem(item);
163
+ }, [item, toggleItem]);
164
+
165
+ return (
166
+ <li className="ListItem">
167
+ <button className="IconButton" onClick={handleDelete}>
168
+ 🗑
169
+ </button>
170
+ <label className="Label">
171
+ <input
172
+ className="Input"
173
+ checked={item.isComplete}
174
+ onChange={handleToggle}
175
+ type="checkbox"
176
+ />{" "}
177
+ {item.text}
178
+ </label>
179
+ </li>
180
+ );
181
+ }
182
+
183
+ function SimpleValues() {
184
+ return (
185
+ <ChildComponent
186
+ string="abc"
187
+ emptyString=""
188
+ number={123}
189
+ undefined={undefined}
190
+ null={null}
191
+ nan={NaN}
192
+ infinity={Infinity}
193
+ true={true}
194
+ false={false}
195
+ />
196
+ );
197
+ }
198
+
199
+ class Custom {
200
+ _number = 42;
201
+ get number() {
202
+ return this._number;
203
+ }
204
+ }
205
+
206
+ function CustomObject() {
207
+ return <ChildComponent customObject={new Custom()} />;
208
+ }
209
+
210
+ const object = {
211
+ string: "abc",
212
+ longString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKJLMNOPQRSTUVWXYZ1234567890",
213
+ emptyString: "",
214
+ number: 123,
215
+ boolean: true,
216
+ undefined: undefined,
217
+ null: null
218
+ };
219
+
220
+ function ObjectProps() {
221
+ return (
222
+ <ChildComponent
223
+ object={{
224
+ outer: {
225
+ inner: object
226
+ }
227
+ }}
228
+ array={["first", "second", "third"]}
229
+ objectInArray={[object]}
230
+ arrayInObject={{ array: ["first", "second", "third"] }}
231
+ deepObject={{
232
+ // Known limitation: we won't go deeper than several levels.
233
+ // In the future, we might offer a way to request deeper access on demand.
234
+ a: {
235
+ b: {
236
+ c: {
237
+ d: {
238
+ e: {
239
+ f: {
240
+ g: {
241
+ h: {
242
+ i: {
243
+ j: 10
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }}
254
+ />
255
+ );
256
+ }
257
+
258
+ function ChildComponent(props: any) {
259
+ return null;
260
+ }
261
+
262
+ function InspectableElements() {
263
+ return (
264
+ <Fragment>
265
+ <SimpleValues />
266
+ <ObjectProps />
267
+ <CustomObject />
268
+ </Fragment>
269
+ );
270
+ }
271
+
272
+ function App() {
273
+ return (
274
+ <Fragment>
275
+ <List />
276
+ <InspectableElements />
277
+ </Fragment>
278
+ );
279
+ }
280
+
281
+ ReactDOM.render(<App />, document.getElementById("root"));
282
+ </script>
283
+ </body>
284
+</html>
flow-typed/chrome.js
new
+94
@@ -0,0 +1,94 @@
1
+// @flow
2
+
3
+declare var chrome: {
4
+ devtools: {
5
+ network: {
6
+ onNavigated: {
7
+ addListener: (cb: (url: string) => void) => void,
8
+ removeListener: (cb: () => void) => void,
9
+ },
10
+ },
11
+ inspectedWindow: {
12
+ eval: (code: string, cb?: (res: any, err: ?Object) => any) => void,
13
+ tabId: number,
14
+ },
15
+ panels: {
16
+ create: (
17
+ title: string,
18
+ icon: string,
19
+ filename: string,
20
+ cb: (panel: {
21
+ onHidden: {
22
+ addListener: (cb: (window: Object) => void) => void,
23
+ },
24
+ onShown: {
25
+ addListener: (cb: (window: Object) => void) => void,
26
+ },
27
+ }) => void
28
+ ) => void,
29
+ themeName: ?string,
30
+ },
31
+ },
32
+ tabs: {
33
+ create: (options: Object) => void,
34
+ executeScript: (tabId: number, options: Object, fn: () => void) => void,
35
+ onUpdated: {
36
+ addListener: (
37
+ fn: (tabId: number, changeInfo: Object, tab: Object) => void
38
+ ) => void,
39
+ },
40
+ query: (options: Object, fn: (tabArray: Array<Object>) => void) => void,
41
+ },
42
+ browserAction: {
43
+ setIcon: (options: {
44
+ tabId: number,
45
+ path: { [key: string]: string },
46
+ }) => void,
47
+ setPopup: (options: {
48
+ tabId: number,
49
+ popup: string,
50
+ }) => void,
51
+ },
52
+ runtime: {
53
+ getURL: (path: string) => string,
54
+ sendMessage: (config: Object) => void,
55
+ connect: (
56
+ config: Object
57
+ ) => {
58
+ disconnect: () => void,
59
+ onMessage: {
60
+ addListener: (fn: (message: Object) => void) => void,
61
+ },
62
+ onDisconnect: {
63
+ addListener: (fn: (message: Object) => void) => void,
64
+ },
65
+ postMessage: (data: Object) => void,
66
+ },
67
+ onConnect: {
68
+ addListener: (
69
+ fn: (port: {
70
+ name: string,
71
+ sender: {
72
+ tab: {
73
+ id: number,
74
+ url: string,
75
+ },
76
+ },
77
+ }) => void
78
+ ) => void,
79
+ },
80
+ onMessage: {
81
+ addListener: (
82
+ fn: (
83
+ req: Object,
84
+ sender: {
85
+ url: string,
86
+ tab: {
87
+ id: number,
88
+ },
89
+ }
90
+ ) => void
91
+ ) => void,
92
+ },
93
+ },
94
+};
package.json
+6
-1
@@ -1,7 +1,7 @@
1
{
2
"version": "4.0.0",
3
"repository": "bvaughn/react-devtools-experimental",
4
- "license": "BSD-3-Clause",
4
+ "license": "MIT",
5
"private": true,
6
"workspaces": [
7
"packages/*"
@@ -32,6 +32,8 @@
32
]
33
},
34
"scripts": {
35
+ "build:core:backend": "cd ./packages/react-devtools-core && yarn build:backend",
36
+ "build:core:standalone": "cd ./packages/react-devtools-core && yarn build:standalone",
37
"build:demo": "cd ./shells/dev && cross-env NODE_ENV=development cross-env TARGET=remote webpack --config webpack.config.js",
38
"build:extension": "cross-env NODE_ENV=production yarn run build:extension:chrome && yarn run build:extension:firefox",
39
"build:extension:dev": "cross-env NODE_ENV=development yarn run build:extension:chrome && yarn run build:extension:firefox",
@@ -52,6 +54,9 @@
54
"prettier": "prettier --write '**/*.{js,json,css}'",
55
"prettier:ci": "prettier --check '**/*.{js,json,css}'",
56
"start": "cd ./shells/dev && cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open",
57
+ "start:core:backend": "cd ./packages/react-devtools-core && yarn start:backend",
58
+ "start:core:standalone": "cd ./packages/react-devtools-core && yarn start:standalone",
59
+ "start:electron": "cd ./packages/react-devtools && node bin.js",
60
"start:prod": "cd ./shells/dev && cross-env NODE_ENV=production cross-env TARGET=local webpack-dev-server --open",
61
"test": "jest",
62
"test-debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
packages/react-devtools-core/README.md
new
+44
@@ -0,0 +1,44 @@
1
+# `react-devtools-core`
2
+
3
+A standalone React DevTools implementation.
4
+
5
+This is a low-level package. If you're looking for the Electron app you can run, **use `react-devtools` package instead.**
6
+
7
+## API
8
+
9
+### `react-devtools-core`
10
+
11
+This is similar requiring the `react-devtools` package, but provides several configurable options. Unlike `react-devtools`, requiring `react-devtools-core` doesn't connect immediately but instead exports a function:
12
+
13
+```js
14
+const { connectToDevTools } = require("react-devtools-core");
15
+connectToDevTools({
16
+ // Config options
17
+});
18
+
19
+```
20
+
21
+Run `connectToDevTools()` in the same context as React to set up a connection to DevTools.
22
+Be sure to run this function *before* importing e.g. `react`, `react-dom`, `react-native`.
23
+
24
+The `options` object may contain:
25
+* `host: string` (defaults to "localhost") - Websocket will connect to this host.
26
+* `port: number` (defaults to `8097`) - Websocket will connect to this port.
27
+* `websocket: Websocket` - Custom websocked to use. Overrides `host` and `port` settings if provided.
28
+* `resolveNativeStyle: (style: number) => ?Object` - Used by the React Native style plug-in.
29
+* `isAppActive: () => boolean` - If provided, DevTools will poll this method and wait until it returns true before connecting to React.
30
+
31
+## `react-devtools-core/standalone`
32
+
33
+Renders the DevTools interface into a DOM node.
34
+
35
+```js
36
+require("react-devtools-core/standalone")
37
+ .setContentDOMNode(document.getElementById("container"))
38
+ .setStatusListener(status => {
39
+ // This callback is optional...
40
+ })
41
+ .startServer(port);
42
+```
43
+
44
+Reference the `react-devtools` package for a complete integration example.
packages/react-devtools-core/backend.js
new
+1
@@ -0,0 +1 @@
1
+module.exports = require('./dist/backend');
packages/react-devtools-core/package.json
new
+31
@@ -0,0 +1,31 @@
1
+{
2
+ "name": "react-devtools-core",
3
+ "description": "Use react-devtools outside of the browser",
4
+ "version": "4.0.0",
5
+ "license": "MIT",
6
+ "main": "./dist/backend.js",
7
+ "repository": {
8
+ "url": "https://github.com/bvaughn/react-devtools-experimental.git",
9
+ "type": "git"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "backend.js",
14
+ "standalone.js"
15
+ ],
16
+ "scripts": {
17
+ "build": "yarn build:backend && yarn build:standalone",
18
+ "build:backend": "cross-env NODE_ENV=production webpack --config webpack.backend.js",
19
+ "build:standalone": "cross-env NODE_ENV=production webpack --config webpack.standalone.js",
20
+ "prepublish": "yarn run build",
21
+ "start:backend": "cross-env NODE_ENV=development webpack --config webpack.backend.js --watch",
22
+ "start:standalone": "cross-env NODE_ENV=development webpack --config webpack.standalone.js --watch"
23
+ },
24
+ "dependencies": {
25
+ "shell-quote": "^1.6.1",
26
+ "ws": "^7"
27
+ },
28
+ "devDependencies": {
29
+ "cross-env": "^3.1.4"
30
+ }
31
+}
packages/react-devtools-core/src/backend.js
new
+269
@@ -0,0 +1,269 @@
1
+// @flow
2
+
3
+import Agent from 'src/backend/agent';
4
+import Bridge from 'src/bridge';
5
+import { installHook } from 'src/hook';
6
+import { initBackend } from 'src/backend';
7
+import { __DEBUG__ } from 'src/constants';
8
+import setupNativeStyleEditor from 'src/backend/NativeStyleEditor/setupNativeStyleEditor';
9
+import { getDefaultComponentFilters } from 'src/utils';
10
+
11
+import type { ComponentFilter } from 'src/types';
12
+import type { DevToolsHook } from 'src/backend/types';
13
+import type { ResolveNativeStyle } from 'src/backend/NativeStyleEditor/setupNativeStyleEditor';
14
+
15
+type ConnectOptions = {
16
+ host?: string,
17
+ nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
18
+ port?: number,
19
+ resolveRNStyle?: ResolveNativeStyle,
20
+ isAppActive?: () => boolean,
21
+ websocket?: ?WebSocket,
22
+};
23
+
24
+installHook(window);
25
+
26
+const hook: DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
27
+
28
+let savedComponentFilters: Array<ComponentFilter> = getDefaultComponentFilters();
29
+
30
+function debug(methodName: string, ...args) {
31
+ if (__DEBUG__) {
32
+ console.log(
33
+ `%c[core/backend] %c${methodName}`,
34
+ 'color: teal; font-weight: bold;',
35
+ 'font-weight: bold;',
36
+ ...args
37
+ );
38
+ }
39
+}
40
+
41
+export function connectToDevTools(options: ?ConnectOptions) {
42
+ const {
43
+ host = 'localhost',
44
+ nativeStyleEditorValidAttributes,
45
+ port = 8097,
46
+ websocket,
47
+ resolveRNStyle = null,
48
+ isAppActive = () => true,
49
+ } = options || {};
50
+
51
+ let retryTimeoutID: TimeoutID | null = null;
52
+
53
+ function scheduleRetry() {
54
+ if (retryTimeoutID === null) {
55
+ // Two seconds because RN had issues with quick retries.
56
+ retryTimeoutID = setTimeout(() => connectToDevTools(options), 2000);
57
+ }
58
+ }
59
+
60
+ if (!isAppActive()) {
61
+ // If the app is in background, maybe retry later.
62
+ // Don't actually attempt to connect until we're in foreground.
63
+ scheduleRetry();
64
+ return;
65
+ }
66
+
67
+ let bridge: Bridge | null = null;
68
+
69
+ const messageListeners = [];
70
+ const uri = 'ws://' + host + ':' + port;
71
+
72
+ // If existing websocket is passed, use it.
73
+ // This is necessary to support our custom integrations.
74
+ // See D6251744.
75
+ const ws = websocket ? websocket : new window.WebSocket(uri);
76
+ ws.onclose = handleClose;
77
+ ws.onerror = handleFailed;
78
+ ws.onmessage = handleMessage;
79
+ ws.onopen = function() {
80
+ bridge = new Bridge({
81
+ listen(fn) {
82
+ messageListeners.push(fn);
83
+ return () => {
84
+ const index = messageListeners.indexOf(fn);
85
+ if (index >= 0) {
86
+ messageListeners.splice(index, 1);
87
+ }
88
+ };
89
+ },
90
+ send(event: string, payload: any, transferable?: Array<any>) {
91
+ if (ws.readyState === ws.OPEN) {
92
+ if (__DEBUG__) {
93
+ debug('wall.send()', event, payload);
94
+ }
95
+
96
+ ws.send(JSON.stringify({ event, payload }));
97
+ } else {
98
+ if (__DEBUG__) {
99
+ debug(
100
+ 'wall.send()',
101
+ 'Shutting down bridge because of closed WebSocket connection'
102
+ );
103
+ }
104
+
105
+ if (bridge !== null) {
106
+ bridge.emit('shutdown');
107
+ }
108
+
109
+ scheduleRetry();
110
+ }
111
+ },
112
+ });
113
+ bridge.addListener(
114
+ 'selectElement',
115
+ ({ id, rendererID }: {| id: number, rendererID: number |}) => {
116
+ const renderer = agent.rendererInterfaces[rendererID];
117
+ if (renderer != null) {
118
+ // Send event for RN to highlight.
119
+ const nodes: ?Array<HTMLElement> = renderer.findNativeNodesForFiberID(
120
+ id
121
+ );
122
+ if (nodes != null && nodes[0] != null) {
123
+ agent.emit('showNativeHighlight', nodes[0]);
124
+ }
125
+ }
126
+ }
127
+ );
128
+ bridge.addListener(
129
+ 'updateComponentFilters',
130
+ (componentFilters: Array<ComponentFilter>) => {
131
+ // Save filter changes in memory, in case DevTools is reloaded.
132
+ // In that case, the renderer will already be using the updated values.
133
+ // We'll lose these in between backend reloads but that can't be helped.
134
+ savedComponentFilters = componentFilters;
135
+ }
136
+ );
137
+
138
+ // The renderer interface doesn't read saved component filters directly,
139
+ // because they are generally stored in localStorage within the context of the extension.
140
+ // Because of this it relies on the extension to pass filters.
141
+ // In the case of the standalone DevTools being used with a website,
142
+ // saved filters are injected along with the backend script tag so we shouldn't override them here.
143
+ // This injection strategy doesn't work for React Native though.
144
+ // Ideally the backend would save the filters itself, but RN doesn't provide a sync storage solution.
145
+ // So for now we just fall back to using the default filters...
146
+ if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) {
147
+ bridge.send('overrideComponentFilters', savedComponentFilters);
148
+ }
149
+
150
+ // TODO (npm-packages) Warn if "isBackendStorageAPISupported"
151
+ const agent = new Agent(bridge);
152
+ agent.addListener('shutdown', () => {
153
+ // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
154
+ // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
155
+ hook.emit('shutdown');
156
+ });
157
+
158
+ initBackend(hook, agent, window);
159
+
160
+ // Setup React Native style editor if the environment supports it.
161
+ if (resolveRNStyle != null || hook.resolveRNStyle != null) {
162
+ setupNativeStyleEditor(
163
+ bridge,
164
+ agent,
165
+ ((resolveRNStyle || hook.resolveRNStyle: any): ResolveNativeStyle),
166
+ nativeStyleEditorValidAttributes ||
167
+ hook.nativeStyleEditorValidAttributes ||
168
+ null
169
+ );
170
+ } else {
171
+ // Otherwise listen to detect if the environment later supports it.
172
+ // For example, Flipper does not eagerly inject these values.
173
+ // Instead it relies on the React Native Inspector to lazily inject them.
174
+ let lazyResolveRNStyle;
175
+ let lazyNativeStyleEditorValidAttributes;
176
+
177
+ const initAfterTick = () => {
178
+ if (bridge !== null) {
179
+ setupNativeStyleEditor(
180
+ bridge,
181
+ agent,
182
+ lazyResolveRNStyle,
183
+ lazyNativeStyleEditorValidAttributes
184
+ );
185
+ }
186
+ };
187
+
188
+ Object.defineProperty(
189
+ hook,
190
+ 'resolveRNStyle',
191
+ ({
192
+ enumerable: false,
193
+ get() {
194
+ return lazyResolveRNStyle;
195
+ },
196
+ set(value) {
197
+ lazyResolveRNStyle = value;
198
+ initAfterTick();
199
+ },
200
+ }: Object)
201
+ );
202
+ Object.defineProperty(
203
+ hook,
204
+ 'nativeStyleEditorValidAttributes',
205
+ ({
206
+ enumerable: false,
207
+ get() {
208
+ return lazyNativeStyleEditorValidAttributes;
209
+ },
210
+ set(value) {
211
+ lazyNativeStyleEditorValidAttributes = value;
212
+ initAfterTick();
213
+ },
214
+ }: Object)
215
+ );
216
+ }
217
+ };
218
+
219
+ function handleClose() {
220
+ if (__DEBUG__) {
221
+ debug('WebSocket.onclose');
222
+ }
223
+
224
+ if (bridge !== null) {
225
+ bridge.emit('shutdown');
226
+ }
227
+
228
+ scheduleRetry();
229
+ }
230
+
231
+ function handleFailed() {
232
+ if (__DEBUG__) {
233
+ debug('WebSocket.onerror');
234
+ }
235
+
236
+ scheduleRetry();
237
+ }
238
+
239
+ function handleMessage(event) {
240
+ let data;
241
+ try {
242
+ if (typeof event.data === 'string') {
243
+ data = JSON.parse(event.data);
244
+ if (__DEBUG__) {
245
+ debug('WebSocket.onmessage', data);
246
+ }
247
+ } else {
248
+ throw Error();
249
+ }
250
+ } catch (e) {
251
+ console.error(
252
+ '[React DevTools] Failed to parse JSON: ' + String(event.data)
253
+ );
254
+ return;
255
+ }
256
+ messageListeners.forEach(fn => {
257
+ try {
258
+ fn(data);
259
+ } catch (error) {
260
+ // jsc doesn't play so well with tracebacks that go into eval'd code,
261
+ // so the stack trace here will stop at the `eval()` call. Getting the
262
+ // message that caused the error is the best we can do for now.
263
+ console.log('[React DevTools] Error calling listener', data);
264
+ console.log('error:', error);
265
+ throw error;
266
+ }
267
+ });
268
+ }
269
+}
packages/react-devtools-core/src/launchEditor.js
new
+170
@@ -0,0 +1,170 @@
1
+// @flow
2
+
3
+import { existsSync } from 'fs';
4
+import { basename, join, isAbsolute } from 'path';
5
+import { execSync, spawn } from 'child_process';
6
+import { parse } from 'shell-quote';
7
+
8
+function isTerminalEditor(editor: string): boolean {
9
+ switch (editor) {
10
+ case 'vim':
11
+ case 'emacs':
12
+ case 'nano':
13
+ return true;
14
+ default:
15
+ return false;
16
+ }
17
+}
18
+
19
+// Map from full process name to binary that starts the process
20
+// We can't just re-use full process name, because it will spawn a new instance
21
+// of the app every time
22
+const COMMON_EDITORS = {
23
+ '/Applications/Atom.app/Contents/MacOS/Atom': 'atom',
24
+ '/Applications/Atom Beta.app/Contents/MacOS/Atom Beta':
25
+ '/Applications/Atom Beta.app/Contents/MacOS/Atom Beta',
26
+ '/Applications/Sublime Text.app/Contents/MacOS/Sublime Text':
27
+ '/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl',
28
+ '/Applications/Sublime Text 2.app/Contents/MacOS/Sublime Text 2':
29
+ '/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl',
30
+ '/Applications/Visual Studio Code.app/Contents/MacOS/Electron': 'code',
31
+};
32
+
33
+function getArgumentsForLineNumber(
34
+ editor: string,
35
+ filePath: string,
36
+ lineNumber: number
37
+): Array<string> {
38
+ switch (basename(editor)) {
39
+ case 'vim':
40
+ case 'mvim':
41
+ return [filePath, '+' + lineNumber];
42
+ case 'atom':
43
+ case 'Atom':
44
+ case 'Atom Beta':
45
+ case 'subl':
46
+ case 'sublime':
47
+ case 'wstorm':
48
+ case 'appcode':
49
+ case 'charm':
50
+ case 'idea':
51
+ return [filePath + ':' + lineNumber];
52
+ case 'joe':
53
+ case 'emacs':
54
+ case 'emacsclient':
55
+ return ['+' + lineNumber, filePath];
56
+ case 'rmate':
57
+ case 'mate':
58
+ case 'mine':
59
+ return ['--line', lineNumber + '', filePath];
60
+ case 'code':
61
+ return ['-g', filePath + ':' + lineNumber];
62
+ default:
63
+ // For all others, drop the lineNumber until we have
64
+ // a mapping above, since providing the lineNumber incorrectly
65
+ // can result in errors or confusing behavior.
66
+ return [filePath];
67
+ }
68
+}
69
+
70
+function guessEditor(): Array<string> {
71
+ // Explicit config always wins
72
+ if (process.env.REACT_EDITOR) {
73
+ return parse(process.env.REACT_EDITOR);
74
+ }
75
+
76
+ // Using `ps x` on OSX we can find out which editor is currently running.
77
+ // Potentially we could use similar technique for Windows and Linux
78
+ if (process.platform === 'darwin') {
79
+ try {
80
+ const output = execSync('ps x').toString();
81
+ const processNames = Object.keys(COMMON_EDITORS);
82
+ for (let i = 0; i < processNames.length; i++) {
83
+ const processName = processNames[i];
84
+ if (output.indexOf(processName) !== -1) {
85
+ return [COMMON_EDITORS[processName]];
86
+ }
87
+ }
88
+ } catch (error) {
89
+ // Ignore...
90
+ }
91
+ }
92
+
93
+ // Last resort, use old skool env vars
94
+ if (process.env.VISUAL) {
95
+ return [process.env.VISUAL];
96
+ } else if (process.env.EDITOR) {
97
+ return [process.env.EDITOR];
98
+ }
99
+
100
+ return [];
101
+}
102
+
103
+let childProcess = null;
104
+
105
+export default function launchEditor(
106
+ maybeRelativePath: string,
107
+ lineNumber: number,
108
+ absoluteProjectRoots: Array<string>
109
+) {
110
+ // We use relative paths at Facebook with deterministic builds.
111
+ // This is why our internal tooling calls React DevTools with absoluteProjectRoots.
112
+ // If the filename is absolute then we don't need to care about this.
113
+ let filePath;
114
+ if (isAbsolute(maybeRelativePath)) {
115
+ if (existsSync(maybeRelativePath)) {
116
+ filePath = maybeRelativePath;
117
+ }
118
+ } else {
119
+ for (let i = 0; i < absoluteProjectRoots.length; i++) {
120
+ const projectRoot = absoluteProjectRoots[i];
121
+ const joinedPath = join(projectRoot, maybeRelativePath);
122
+ if (existsSync(joinedPath)) {
123
+ filePath = joinedPath;
124
+ break;
125
+ }
126
+ }
127
+ }
128
+
129
+ if (!filePath) {
130
+ return;
131
+ }
132
+
133
+ // Sanitize lineNumber to prevent malicious use on win32
134
+ // via: https://github.com/nodejs/node/blob/c3bb4b1aa5e907d489619fb43d233c3336bfc03d/lib/child_process.js#L333
135
+ if (lineNumber && isNaN(lineNumber)) {
136
+ return;
137
+ }
138
+
139
+ let [editor, ...args] = guessEditor();
140
+ if (!editor) {
141
+ return;
142
+ }
143
+
144
+ if (lineNumber) {
145
+ args = args.concat(getArgumentsForLineNumber(editor, filePath, lineNumber));
146
+ } else {
147
+ args.push(filePath);
148
+ }
149
+
150
+ if (childProcess && isTerminalEditor(editor)) {
151
+ // There's an existing editor process already and it's attached
152
+ // to the terminal, so go kill it. Otherwise two separate editor
153
+ // instances attach to the stdin/stdout which gets confusing.
154
+ childProcess.kill('SIGKILL');
155
+ }
156
+
157
+ if (process.platform === 'win32') {
158
+ // On Windows, launch the editor in a shell because spawn can only
159
+ // launch .exe files.
160
+ childProcess = spawn('cmd.exe', ['/C', editor].concat(args), {
161
+ stdio: 'inherit',
162
+ });
163
+ } else {
164
+ childProcess = spawn(editor, args, { stdio: 'inherit' });
165
+ }
166
+ childProcess.on('error', function() {});
167
+ childProcess.on('exit', function(errorCode) {
168
+ childProcess = null;
169
+ });
170
+}
packages/react-devtools-core/src/standalone.js
new
+286
@@ -0,0 +1,286 @@
1
+// @flow
2
+
3
+import { createElement } from 'react';
4
+import {
5
+ // $FlowFixMe Flow does not yet know about flushSync()
6
+ flushSync,
7
+ // $FlowFixMe Flow does not yet know about createRoot()
8
+ unstable_createRoot as createRoot,
9
+} from 'react-dom';
10
+import Bridge from 'src/bridge';
11
+import Store from 'src/devtools/store';
12
+import { getSavedComponentFilters } from 'src/utils';
13
+import { Server } from 'ws';
14
+import { existsSync, readFileSync } from 'fs';
15
+import { installHook } from 'src/hook';
16
+import DevTools from 'src/devtools/views/DevTools';
17
+import launchEditor from './launchEditor';
18
+import { __DEBUG__ } from 'src/constants';
19
+
20
+import type { InspectedElement } from 'src/devtools/views/Components/types';
21
+
22
+installHook(window);
23
+
24
+export type StatusListener = (message: string) => void;
25
+
26
+let node: HTMLElement = ((null: any): HTMLElement);
27
+let nodeWaitingToConnectHTML: string = '';
28
+let projectRoots: Array<string> = [];
29
+let statusListener: StatusListener = (message: string) => {};
30
+
31
+function setContentDOMNode(value: HTMLElement) {
32
+ node = value;
33
+
34
+ // Save so we can restore the exact waiting message between sessions.
35
+ nodeWaitingToConnectHTML = node.innerHTML;
36
+
37
+ return DevtoolsUI;
38
+}
39
+
40
+function setProjectRoots(value: Array<string>) {
41
+ projectRoots = value;
42
+}
43
+
44
+function setStatusListener(value: StatusListener) {
45
+ statusListener = value;
46
+ return DevtoolsUI;
47
+}
48
+
49
+let bridge: Bridge | null = null;
50
+let store: Store | null = null;
51
+let root = null;
52
+
53
+const log = (...args) => console.log('[React DevTools]', ...args);
54
+log.warn = (...args) => console.warn('[React DevTools]', ...args);
55
+log.error = (...args) => console.error('[React DevTools]', ...args);
56
+
57
+function debug(methodName: string, ...args) {
58
+ if (__DEBUG__) {
59
+ console.log(
60
+ `%c[core/standalone] %c${methodName}`,
61
+ 'color: teal; font-weight: bold;',
62
+ 'font-weight: bold;',
63
+ ...args
64
+ );
65
+ }
66
+}
67
+
68
+function safeUnmount() {
69
+ flushSync(() => {
70
+ if (root !== null) {
71
+ root.unmount();
72
+ }
73
+ });
74
+ root = null;
75
+}
76
+
77
+function reload() {
78
+ safeUnmount();
79
+
80
+ node.innerHTML = '';
81
+
82
+ setTimeout(() => {
83
+ root = createRoot(node);
84
+ root.render(
85
+ createElement(DevTools, {
86
+ bridge: ((bridge: any): Bridge),
87
+ showTabBar: true,
88
+ store: ((store: any): Store),
89
+ viewElementSourceFunction,
90
+ viewElementSourceRequiresFileLocation: true,
91
+ })
92
+ );
93
+ }, 100);
94
+}
95
+
96
+function viewElementSourceFunction(
97
+ id: number,
98
+ inspectedElement: InspectedElement
99
+): void {
100
+ const { source } = inspectedElement;
101
+ if (source !== null) {
102
+ launchEditor(source.fileName, source.lineNumber, projectRoots);
103
+ } else {
104
+ log.error('Cannot inspect element', id);
105
+ }
106
+}
107
+
108
+function onDisconnected() {
109
+ safeUnmount();
110
+
111
+ node.innerHTML = nodeWaitingToConnectHTML;
112
+}
113
+
114
+function onError({ code, message }) {
115
+ safeUnmount();
116
+
117
+ if (code === 'EADDRINUSE') {
118
+ node.innerHTML = `<div id="waiting"><h2>Another instance of DevTools is running</h2></div>`;
119
+ } else {
120
+ node.innerHTML = `<div id="waiting"><h2>Unknown error (${message})</h2></div>`;
121
+ }
122
+}
123
+
124
+function initialize(socket: WebSocket) {
125
+ const listeners = [];
126
+ socket.onmessage = event => {
127
+ let data;
128
+ try {
129
+ if (typeof event.data === 'string') {
130
+ data = JSON.parse(event.data);
131
+
132
+ if (__DEBUG__) {
133
+ debug('WebSocket.onmessage', data);
134
+ }
135
+ } else {
136
+ throw Error();
137
+ }
138
+ } catch (e) {
139
+ log.error('Failed to parse JSON', event.data);
140
+ return;
141
+ }
142
+ listeners.forEach(fn => {
143
+ try {
144
+ fn(data);
145
+ } catch (error) {
146
+ log.error('Error calling listener', data);
147
+ throw error;
148
+ }
149
+ });
150
+ };
151
+
152
+ bridge = new Bridge({
153
+ listen(fn) {
154
+ listeners.push(fn);
155
+ return () => {
156
+ const index = listeners.indexOf(fn);
157
+ if (index >= 0) {
158
+ listeners.splice(index, 1);
159
+ }
160
+ };
161
+ },
162
+ send(event: string, payload: any, transferable?: Array<any>) {
163
+ if (socket.readyState === socket.OPEN) {
164
+ socket.send(JSON.stringify({ event, payload }));
165
+ }
166
+ },
167
+ });
168
+ ((bridge: any): Bridge).addListener('shutdown', () => {
169
+ socket.close();
170
+ });
171
+
172
+ store = new Store(bridge, { supportsNativeInspection: false });
173
+
174
+ log('Connected');
175
+ reload();
176
+}
177
+
178
+let startServerTimeoutID: TimeoutID | null = null;
179
+
180
+function connectToSocket(socket: WebSocket) {
181
+ socket.onerror = err => {
182
+ onDisconnected();
183
+ log.error('Error with websocket connection', err);
184
+ };
185
+ socket.onclose = () => {
186
+ onDisconnected();
187
+ log('Connection to RN closed');
188
+ };
189
+ initialize(socket);
190
+
191
+ return {
192
+ close: function() {
193
+ onDisconnected();
194
+ },
195
+ };
196
+}
197
+
198
+function startServer(port?: number = 8097) {
199
+ const httpServer = require('http').createServer();
200
+ const server = new Server({ server: httpServer });
201
+ let connected: WebSocket | null = null;
202
+ server.on('connection', (socket: WebSocket) => {
203
+ if (connected !== null) {
204
+ connected.close();
205
+ log.warn(
206
+ 'Only one connection allowed at a time.',
207
+ 'Closing the previous connection'
208
+ );
209
+ }
210
+ connected = socket;
211
+ socket.onerror = error => {
212
+ connected = null;
213
+ onDisconnected();
214
+ log.error('Error with websocket connection', error);
215
+ };
216
+ socket.onclose = () => {
217
+ connected = null;
218
+ onDisconnected();
219
+ log('Connection to RN closed');
220
+ };
221
+ initialize(socket);
222
+ });
223
+
224
+ server.on('error', event => {
225
+ onError(event);
226
+ log.error('Failed to start the DevTools server', event);
227
+ startServerTimeoutID = setTimeout(() => startServer(port), 1000);
228
+ });
229
+
230
+ httpServer.on('request', (request, response) => {
231
+ // NPM installs should read from node_modules,
232
+ // But local dev mode needs to use a relative path.
233
+ const basePath = existsSync('./node_modules/react-devtools-core')
234
+ ? 'node_modules/react-devtools-core'
235
+ : '../react-devtools-core';
236
+
237
+ // Serve a file that immediately sets up the connection.
238
+ const backendFile = readFileSync(`${basePath}/dist/backend.js`);
239
+
240
+ // The renderer interface doesn't read saved component filters directly,
241
+ // because they are generally stored in localStorage within the context of the extension.
242
+ // Because of this it relies on the extension to pass filters, so include them wth the response here.
243
+ // This will ensure that saved filters are shared across different web pages.
244
+ const savedFiltersString = `window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
245
+ getSavedComponentFilters()
246
+ )};`;
247
+
248
+ response.end(
249
+ savedFiltersString +
250
+ '\n;' +
251
+ backendFile.toString() +
252
+ '\n;' +
253
+ 'ReactDevToolsBackend.connectToDevTools();'
254
+ );
255
+ });
256
+
257
+ httpServer.on('error', event => {
258
+ onError(event);
259
+ statusListener('Failed to start the server.');
260
+ startServerTimeoutID = setTimeout(() => startServer(port), 1000);
261
+ });
262
+
263
+ httpServer.listen(port, () => {
264
+ statusListener('The server is listening on the port ' + port + '.');
265
+ });
266
+
267
+ return {
268
+ close: function() {
269
+ connected = null;
270
+ onDisconnected();
271
+ clearTimeout(startServerTimeoutID);
272
+ server.close();
273
+ httpServer.close();
274
+ },
275
+ };
276
+}
277
+
278
+const DevtoolsUI = {
279
+ connectToSocket,
280
+ setContentDOMNode,
281
+ setProjectRoots,
282
+ setStatusListener,
283
+ startServer,
284
+};
285
+
286
+export default DevtoolsUI;
packages/react-devtools-core/standalone.js
new
+1
@@ -0,0 +1 @@
1
+module.exports = require('./dist/standalone');
packages/react-devtools-core/webpack.backend.js
new
+53
@@ -0,0 +1,53 @@
1
+const { resolve } = require('path');
2
+const { DefinePlugin } = require('webpack');
3
+const { getGitHubURL, getVersionString } = require('../../shells/utils');
4
+
5
+const NODE_ENV = process.env.NODE_ENV;
6
+if (!NODE_ENV) {
7
+ console.error('NODE_ENV not set');
8
+ process.exit(1);
9
+}
10
+
11
+const __DEV__ = NODE_ENV === 'development';
12
+
13
+const GITHUB_URL = getGitHubURL();
14
+const DEVTOOLS_VERSION = getVersionString();
15
+
16
+module.exports = {
17
+ mode: 'development', // TODO TESTING __DEV__ ? 'development' : 'production',
18
+ devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
19
+ entry: {
20
+ backend: './src/backend.js',
21
+ },
22
+ output: {
23
+ path: __dirname + '/dist',
24
+ filename: '[name].js',
25
+
26
+ // This name is important; standalone references it in order to connect.
27
+ library: 'ReactDevToolsBackend',
28
+ libraryTarget: 'umd',
29
+ },
30
+ resolve: {
31
+ alias: {
32
+ src: resolve(__dirname, '../../src'),
33
+ },
34
+ },
35
+ plugins: [
36
+ new DefinePlugin({
37
+ __DEV__: true,
38
+ 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
39
+ 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
40
+ }),
41
+ ],
42
+ module: {
43
+ rules: [
44
+ {
45
+ test: /\.js$/,
46
+ loader: 'babel-loader',
47
+ options: {
48
+ configFile: resolve(__dirname, '../../babel.config.js'),
49
+ },
50
+ },
51
+ ],
52
+ },
53
+};
packages/react-devtools-core/webpack.standalone.js
new
+69
@@ -0,0 +1,69 @@
1
+const { resolve } = require('path');
2
+const { DefinePlugin } = require('webpack');
3
+const { getGitHubURL, getVersionString } = require('../../shells/utils');
4
+
5
+const NODE_ENV = process.env.NODE_ENV;
6
+if (!NODE_ENV) {
7
+ console.error('NODE_ENV not set');
8
+ process.exit(1);
9
+}
10
+
11
+const __DEV__ = NODE_ENV === 'development';
12
+
13
+const GITHUB_URL = getGitHubURL();
14
+const DEVTOOLS_VERSION = getVersionString();
15
+
16
+module.exports = {
17
+ mode: __DEV__ ? 'development' : 'production',
18
+ devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
19
+ target: 'electron-main',
20
+ entry: {
21
+ standalone: './src/standalone.js',
22
+ },
23
+ output: {
24
+ path: __dirname + '/dist',
25
+ filename: '[name].js',
26
+ library: '[name]',
27
+ libraryTarget: 'commonjs2',
28
+ },
29
+ resolve: {
30
+ alias: {
31
+ src: resolve(__dirname, '../../src'),
32
+ },
33
+ },
34
+ plugins: [
35
+ new DefinePlugin({
36
+ __DEV__: false,
37
+ 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
38
+ 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
39
+ 'process.env.NODE_ENV': `"${NODE_ENV}"`,
40
+ }),
41
+ ],
42
+ module: {
43
+ rules: [
44
+ {
45
+ test: /\.js$/,
46
+ loader: 'babel-loader',
47
+ options: {
48
+ configFile: resolve(__dirname, '../../babel.config.js'),
49
+ },
50
+ },
51
+ {
52
+ test: /\.css$/,
53
+ use: [
54
+ {
55
+ loader: 'style-loader',
56
+ },
57
+ {
58
+ loader: 'css-loader',
59
+ options: {
60
+ sourceMap: true,
61
+ modules: true,
62
+ localIdentName: '[local]___[hash:base64:5]',
63
+ },
64
+ },
65
+ ],
66
+ },
67
+ ],
68
+ },
69
+};
packages/react-devtools/README.md
new
+98
@@ -0,0 +1,98 @@
1
+# `react-devtools`
2
+
3
+React DevTools is available as a built-in extension for Chrome and Firefox browsers. This package enables you to debug a React app elsewhere (e.g. a mobile browser, an embedded webview, Safari, inside an iframe).
4
+
5
+It works both with React DOM and React Native.
6
+
7
+<img src="http://i.imgur.com/IXeHiZD.png" width="600" alt="Screenshot of React DevTools running with React Native">
8
+
9
+## Installation
10
+Install the `react-devtools` package. Because this is a development tool, a global install is often the most convenient:
11
+```sh
12
+# Yarn
13
+yarn global add react-devtools
14
+
15
+# NPM
16
+npm install -g react-devtools
17
+```
18
+
19
+If you prefer to avoid global installations, you can add `react-devtools` as a project dependency. With Yarn, you can do this by running:
20
+```sh
21
+yarn add --dev react-devtools
22
+```
23
+
24
+With NPM you can just use [NPX](https://www.npmjs.com/package/npx):
25
+```sh
26
+npx react-devtools
27
+```
28
+
29
+## Usage with React Native
30
+Run `react-devtools` from the terminal to launch the standalone DevTools app:
31
+```sh
32
+react-devtools
33
+```
34
+
35
+If you're not in a simulator then you also need to run the following in a command prompt:
36
+```sh
37
+adb reverse tcp:8097 tcp:8097
38
+```
39
+
40
+If you're using React Native 0.43 or higher, it should connect to your simulator within a few seconds.
41
+
42
+### Integration with React Native Inspector
43
+
44
+You can open the [in-app developer menu](https://facebook.github.io/react-native/docs/debugging.html#accessing-the-in-app-developer-menu) and choose "Show Inspector". It will bring up an overlay that lets you tap on any UI element and see information about it:
45
+
46
+
47
+
48
+However, when `react-devtools` is running, Inspector will enter a special collapsed mode, and instead use the DevTools as primary UI. In this mode, clicking on something in the simulator will bring up the relevant components in the DevTools:
49
+
50
+
51
+
52
+You can choose "Hide Inspector" in the same menu to exit this mode.
53
+
54
+### Inspecting Component Instances
55
+
56
+When debugging JavaScript in Chrome, you can inspect the props and state of the React components in the browser console.
57
+
58
+First, follow the [instructions for debugging in Chrome](https://facebook.github.io/react-native/docs/debugging.html#chrome-developer-tools) to open the Chrome console.
59
+
60
+Make sure that the dropdown in the top left corner of the Chrome console says `debuggerWorker.js`. **This step is essential.**
61
+
62
+Then select a React component in React DevTools. There is a search box at the top that helps you find one by name. As soon as you select it, it will be available as `$r` in the Chrome console, letting you inspect its props, state, and instance properties.
63
+
64
+
65
+
66
+
67
+## Usage with React DOM
68
+
69
+The standalone shell can also be useful with React DOM (e.g. to debug apps in Safari or inside of an iframe).
70
+
71
+Run `react-devtools` from the terminal to launch the standalone DevTools app:
72
+```sh
73
+react-devtools
74
+```
75
+
76
+Add `<script src="http://localhost:8097"></script>` as the very first `<script>` tag in the `<head>` of your page when developing:
77
+
78
+```html
79
+<!doctype html>
80
+<html lang="en">
81
+ <head>
82
+ <script src="http://localhost:8097"></script>
83
+```
84
+
85
+This will ensure the developer tools are connected. **Don’t forget to remove it before deploying to production!**
86
+
87
+>If you install `react-devtools` as a project dependency, you may also replace the `<script>` suggested above with a JavaScript import (`import 'react-devtools'`). It is important that this import comes before any other imports in your app (especially before `react-dom`). Make sure to remove the import before deploying to production, as it carries a large DevTools client with it. If you use Webpack and have control over its configuration, you could alternatively add `'react-devtools'` as the first item in the `entry` array of the development-only configuration, and then you wouldn’t need to deal either with `<script>` tags or `import` statements.
88
+
89
+## Advanced
90
+
91
+By default DevTools listen to port `8097` on `localhost`. If you need to customize host, port, or other settings, see the `react-devtools-core` package instead.
92
+
93
+## Developing
94
+
95
+* Run `yarn run backend:watch` and `yarn run standalone:watch` in `../react-devtools-core`
96
+* Run `yarn start` in this folder
97
+* Refresh the app after it has recompiled a change
98
+* For React Native, copy `react-devtools-core` to its `node_modules` to test your changes.
\ No newline at end of file
packages/react-devtools/app.html
new
+129
@@ -0,0 +1,129 @@
1
+<!DOCTYPE html>
2
+<html>
3
+ <head>
4
+ <title>React Developer Tools</title>
5
+ <meta charset="utf8" />
6
+ <style>
7
+ html {
8
+ display: flex;
9
+ font-family: sans-serif;
10
+ }
11
+ body {
12
+ margin: 0;
13
+ padding: 0;
14
+ flex: 1;
15
+ display: flex;
16
+ }
17
+ #container {
18
+ display: flex;
19
+ position: fixed;
20
+ top: 0;
21
+ left: 0;
22
+ right: 0;
23
+ bottom: 0;
24
+ background-color: white;
25
+ }
26
+ #logs {
27
+ position: fixed;
28
+ top: 0;
29
+ left: 0;
30
+ white-space: pre;
31
+ }
32
+ #waiting {
33
+ flex-direction: column;
34
+ text-align: center;
35
+ font-family: sans-serif;
36
+ color: #aaa;
37
+ flex: 1;
38
+ overflow: auto;
39
+ }
40
+ #waiting h2 {
41
+ padding: 30px;
42
+ }
43
+ #waiting p {
44
+ padding: 0;
45
+ margin: 0;
46
+ }
47
+ .input {
48
+ display: inline-block;
49
+ max-width: 100%;
50
+ font-weight: 100;
51
+ border: 1px solid #aaa;
52
+ padding: 0.125rem 0.25rem;
53
+ margin: 0.125rem;
54
+ color: #666;
55
+ }
56
+ #loading-status {
57
+ margin-top: 10px;
58
+ }
59
+ </style>
60
+ </head>
61
+ <body>
62
+ <div id="container">
63
+ <div id="waiting" style="-webkit-user-select: none; -webkit-app-region: drag;">
64
+ <h2>Waiting for React to connect…</h2>
65
+ <div>
66
+ <h4>React Native</h4>
67
+ <div>The active app will automatically connect in a few seconds.</div>
68
+ <br />
69
+ <h4>React DOM</h4>
70
+ <div>
71
+ Add <span class="input" contenteditable="true" id="localhost"></span><br />
72
+ or <span class="input" contenteditable="true" id="byip"></span>
73
+ </div>
74
+ <div>
75
+ to the top of the page you want to debug,
76
+ <br />
77
+ <b>before</b> importing React DOM.
78
+ </div>
79
+ </div>
80
+ <br />
81
+ <br />
82
+ <div id="loading-status">Starting the server…</div>
83
+ </div>
84
+ </div>
85
+ <script>
86
+ const port = process.env.PORT || 8097;
87
+ const localIp = require("ip").address();
88
+ const $ = document.querySelector.bind(document);
89
+
90
+ function selectAll(event) {
91
+ const element = event.target;
92
+ if (window.getSelection) {
93
+ const selection = window.getSelection();
94
+ const range = document.createRange();
95
+ range.selectNodeContents(element);
96
+ selection.removeAllRanges();
97
+ selection.addRange(range);
98
+ }
99
+ }
100
+
101
+ const $localhost = $("#localhost");
102
+ $localhost.innerText = `<script src="http://localhost:${port}"></` + 'script>';
103
+ $localhost.addEventListener('click', selectAll);
104
+ $localhost.addEventListener('focus', selectAll);
105
+
106
+ const $byIp = $("#byip");
107
+ $byIp.innerText = `<script src="http://${localIp}:${port}"></` + 'script>';
108
+ $byIp.addEventListener('click', selectAll);
109
+ $byIp.addEventListener('focus', selectAll);
110
+
111
+ let devtools;
112
+ try {
113
+ devtools = require("react-devtools-core/standalone").default;
114
+ } catch (err) {
115
+ alert(
116
+ err.toString() +
117
+ "\n\nDid you run `yarn` and `yarn run build` in packages/react-devtools-core?"
118
+ );
119
+ }
120
+ window.devtools = devtools;
121
+ window.server = devtools
122
+ .setContentDOMNode(document.getElementById("container"))
123
+ .setStatusListener(function(status) {
124
+ document.getElementById("loading-status").innerText = status;
125
+ })
126
+ .startServer(port);
127
+ </script>
128
+ </body>
129
+</html>
packages/react-devtools/app.js
new
+50
@@ -0,0 +1,50 @@
1
+// @flow
2
+
3
+const { app, BrowserWindow } = require('electron'); // Module to create native browser window.
4
+const { join } = require('path');
5
+
6
+const argv = require('minimist')(process.argv.slice(2));
7
+const projectRoots = argv._;
8
+const defaultThemeName = argv.theme;
9
+
10
+let mainWindow = null;
11
+
12
+app.on('window-all-closed', function() {
13
+ app.quit();
14
+});
15
+
16
+app.on('ready', function() {
17
+ // Create the browser window.
18
+ mainWindow = new BrowserWindow({
19
+ width: 800,
20
+ height: 600,
21
+ icon: join(__dirname, 'icons/icon128.png'),
22
+ frame: false,
23
+ //titleBarStyle: 'customButtonsOnHover',
24
+ webPreferences: {
25
+ nodeIntegration: true,
26
+ },
27
+ });
28
+
29
+ // and load the index.html of the app.
30
+ mainWindow.loadURL('file://' + __dirname + '/app.html'); // eslint-disable-line no-path-concat
31
+ mainWindow.webContents.executeJavaScript(
32
+ // We use this so that RN can keep relative JSX __source filenames
33
+ // but "click to open in editor" still works. js1 passes project roots
34
+ // as the argument to DevTools.
35
+ 'window.devtools.setProjectRoots(' + JSON.stringify(projectRoots) + ')'
36
+ );
37
+
38
+ if (argv.theme) {
39
+ mainWindow.webContents.executeJavaScript(
40
+ 'window.devtools.setDefaultThemeName(' +
41
+ JSON.stringify(defaultThemeName) +
42
+ ')'
43
+ );
44
+ }
45
+
46
+ // Emitted when the window is closed.
47
+ mainWindow.on('closed', function() {
48
+ mainWindow = null;
49
+ });
50
+});
packages/react-devtools/bin.js
new
+18
@@ -0,0 +1,18 @@
1
+#!/usr/bin/env node
2
+
3
+// @flow
4
+
5
+const electron = require('electron');
6
+const spawn = require('cross-spawn');
7
+const argv = process.argv.slice(2);
8
+const pkg = require('./package.json');
9
+const updateNotifier = require('update-notifier');
10
+
11
+// notify if there's an update
12
+updateNotifier({ pkg }).notify({ defer: false });
13
+
14
+const result = spawn.sync(electron, [require.resolve('./app')].concat(argv), {
15
+ stdio: 'ignore',
16
+});
17
+
18
+process.exit(result.status);
packages/react-devtools/icons/icon128.png
Binary files /dev/null and b/packages/react-devtools/icons/icon128.png differ
packages/react-devtools/index.js
new
+7
@@ -0,0 +1,7 @@
1
+// @flow
2
+
3
+const { connectToDevTools } = require('react-devtools-core/backend');
4
+
5
+// Connect immediately with default options.
6
+// If you need more control, use `react-devtools-core` directly instead of `react-devtools`.
7
+connectToDevTools();
packages/react-devtools/package.json
new
+31
@@ -0,0 +1,31 @@
1
+{
2
+ "name": "react-devtools",
3
+ "version": "4.0.0",
4
+ "description": "Use react-devtools outside of the browser",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "url": "https://github.com/bvaughn/react-devtools-experimental.git",
8
+ "type": "git"
9
+ },
10
+ "bin": {
11
+ "react-devtools": "./bin.js"
12
+ },
13
+ "files": [
14
+ "bin.js",
15
+ "app.html",
16
+ "app.js",
17
+ "index.js",
18
+ "icons"
19
+ ],
20
+ "scripts": {
21
+ "start": "node bin.js"
22
+ },
23
+ "dependencies": {
24
+ "cross-spawn": "^5.0.1",
25
+ "electron": "^5.0.0",
26
+ "ip": "^1.1.4",
27
+ "minimist": "^1.2.0",
28
+ "react-devtools-core": "^4.0.0",
29
+ "update-notifier": "^2.1.0"
30
+ }
31
+}
shells/browser/shared/src/backend.js
+12
@@ -23,6 +23,8 @@ function setup(hook) {
23
const Agent = require('src/backend/agent').default;
24
const Bridge = require('src/bridge').default;
25
const { initBackend } = require('src/backend');
26
+ const setupNativeStyleEditor = require('src/backend/NativeStyleEditor/setupNativeStyleEditor')
27
+ .default;
28
29
const bridge = new Bridge({
30
listen(fn) {
@@ -62,4 +64,14 @@ function setup(hook) {
64
});
65
66
initBackend(hook, agent, window);
67
+
68
+ // Setup React Native style editor if a renderer like react-native-web has injected it.
69
+ if (!!hook.resolveRNStyle) {
70
+ setupNativeStyleEditor(
71
+ bridge,
72
+ agent,
73
+ hook.resolveRNStyle,
74
+ hook.nativeStyleEditorValidAttributes
75
+ );
76
+ }
77
}
shells/browser/shared/src/main.js
+6
-10
@@ -5,11 +5,7 @@ import { unstable_createRoot as createRoot, flushSync } from 'react-dom';
5
import Bridge from 'src/bridge';
6
import Store from 'src/devtools/store';
7
import inject from './inject';
8
-import {
9
- createViewElementSource,
10
- getBrowserName,
11
- getBrowserTheme,
12
-} from './utils';
8
+import { createViewElementSource, getBrowserTheme } from './utils';
9
import { getSavedComponentFilters } from 'src/utils';
10
import {
11
localStorageGetItem,
@@ -121,8 +117,6 @@ function createPanelIfReactLoaded() {
117
localStorageRemoveItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY);
118
}
119
124
- const browserName = getBrowserName();
125
-
120
store = new Store(bridge, {
121
isProfiling,
122
supportsCaptureScreenshots: true,
@@ -135,7 +129,10 @@ function createPanelIfReactLoaded() {
129
// Otherwise the Store may miss important initial tree op codes.
130
inject(chrome.runtime.getURL('build/backend.js'));
131
138
- const viewElementSource = createViewElementSource(bridge, store);
132
+ const viewElementSourceFunction = createViewElementSource(
133
+ bridge,
134
+ store
135
+ );
136
137
root = createRoot(document.createElement('div'));
138
@@ -145,7 +142,6 @@ function createPanelIfReactLoaded() {
142
root.render(
143
createElement(DevTools, {
144
bridge,
148
- browserName,
145
browserTheme: getBrowserTheme(),
146
componentsPortalContainer,
147
overrideTab,
@@ -153,7 +149,7 @@ function createPanelIfReactLoaded() {
149
settingsPortalContainer,
150
showTabBar: false,
151
store,
156
- viewElementSource,
152
+ viewElementSourceFunction,
153
})
154
);
155
};
shells/browser/shared/src/utils.js
+6
-2
@@ -23,11 +23,15 @@ export function createViewElementSource(bridge: Bridge, store: Store) {
23
};
24
}
25
26
-export function getBrowserName() {
26
+export type BrowserName = 'Chrome' | 'Firefox';
27
+
28
+export function getBrowserName(): BrowserName {
29
return IS_CHROME ? 'Chrome' : 'Firefox';
30
}
31
30
-export function getBrowserTheme() {
32
+export type BrowserTheme = 'dark' | 'light';
33
+
34
+export function getBrowserTheme(): BrowserTheme {
35
if (IS_CHROME) {
36
// chrome.devtools.panels added in Chrome 18.
37
// chrome.devtools.panels.themeName added in Chrome 54.
shells/dev/app/InspectableElements/InspectableElements.js
+2
@@ -5,6 +5,7 @@ import Contexts from './Contexts';
5
import CustomHooks from './CustomHooks';
6
import CustomObject from './CustomObject';
7
import NestedProps from './NestedProps';
8
+import SimpleValues from './SimpleValues';
9
10
// TODO Add Immutable JS example
11
@@ -12,6 +13,7 @@ export default function InspectableElements() {
13
return (
14
<Fragment>
15
<h1>Inspectable elements</h1>
16
+ <SimpleValues />
17
<NestedProps />
18
<Contexts />
19
<CustomHooks />
shells/dev/app/InspectableElements/SimpleValues.js
new
+23
@@ -0,0 +1,23 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+export default function SimpleValues() {
6
+ return (
7
+ <ChildComponent
8
+ string="abc"
9
+ emptyString=""
10
+ number={123}
11
+ undefined={undefined}
12
+ null={null}
13
+ nan={NaN}
14
+ infinity={Infinity}
15
+ true={true}
16
+ false={false}
17
+ />
18
+ );
19
+}
20
+
21
+function ChildComponent(props: any) {
22
+ return null;
23
+}
shells/dev/src/devtools.js
-1
@@ -73,7 +73,6 @@ inject('dist/app.js', () => {
73
batch.render(
74
createElement(DevTools, {
75
bridge,
76
- browserName: 'Chrome',
76
browserTheme: 'light',
77
showTabBar: true,
78
store,
src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
+23
@@ -473,3 +473,26 @@ exports[`InspectedElementContext should support custom objects with enumerable p
473
"state": null
474
}
475
`;
476
+
477
+exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = `
478
+{
479
+ "id": 2,
480
+ "owners": null,
481
+ "context": null,
482
+ "events": null,
483
+ "hooks": null,
484
+ "props": {
485
+ "boolean_false": false,
486
+ "boolean_true": true,
487
+ "infinity": null,
488
+ "integer_zero": 0,
489
+ "integer_one": 1,
490
+ "float": 1.23,
491
+ "string": "abc",
492
+ "string_empty": "",
493
+ "nan": null,
494
+ "value_null": null
495
+ },
496
+ "state": null
497
+}
498
+`;
src/__tests__/__snapshots__/profilingCache-test.js.snap
+5
-5
@@ -1289,7 +1289,7 @@ Object {
1289
"interactionCommits": Map {},
1290
"interactions": Map {},
1291
"operations": Array [
1292
- Uint32Array [
1292
+ Array [
1293
1,
1294
1,
1295
8,
@@ -1325,7 +1325,7 @@ Object {
1325
1,
1326
14000,
1327
],
1328
- Uint32Array [
1328
+ Array [
1329
1,
1330
1,
1331
0,
@@ -1345,7 +1345,7 @@ Object {
1345
1,
1346
11000,
1347
],
1348
- Uint32Array [
1348
+ Array [
1349
1,
1350
1,
1351
0,
@@ -1452,7 +1452,7 @@ Object {
1452
"interactionCommits": Map {},
1453
"interactions": Map {},
1454
"operations": Array [
1455
- Uint32Array [
1455
+ Array [
1456
1,
1457
11,
1458
15,
@@ -1540,7 +1540,7 @@ Object {
1540
"interactionCommits": Map {},
1541
"interactions": Map {},
1542
"operations": Array [
1543
- Uint32Array [
1543
+ Array [
1544
1,
1545
6,
1546
0,
src/__tests__/inspectedElementContext-test.js
+66
@@ -255,6 +255,72 @@ describe('InspectedElementContext', () => {
255
done();
256
});
257
258
+ it('should support simple data types', async done => {
259
+ const Example = () => null;
260
+
261
+ const container = document.createElement('div');
262
+ await utils.actAsync(() =>
263
+ ReactDOM.render(
264
+ <Example
265
+ boolean_false={false}
266
+ boolean_true={true}
267
+ infinity={Infinity}
268
+ integer_zero={0}
269
+ integer_one={1}
270
+ float={1.23}
271
+ string="abc"
272
+ string_empty=""
273
+ nan={NaN}
274
+ value_null={null}
275
+ value_undefined={undefined}
276
+ />,
277
+ container
278
+ )
279
+ );
280
+
281
+ const id = ((store.getElementIDAtIndex(0): any): number);
282
+ let inspectedElement = null;
283
+
284
+ function Suspender({ target }) {
285
+ const { getInspectedElement } = React.useContext(InspectedElementContext);
286
+ inspectedElement = getInspectedElement(id);
287
+ return null;
288
+ }
289
+
290
+ await utils.actAsync(
291
+ () =>
292
+ TestRenderer.create(
293
+ <Contexts
294
+ defaultSelectedElementID={id}
295
+ defaultSelectedElementIndex={0}
296
+ >
297
+ <React.Suspense fallback={null}>
298
+ <Suspender target={id} />
299
+ </React.Suspense>
300
+ </Contexts>
301
+ ),
302
+ false
303
+ );
304
+
305
+ expect(inspectedElement).not.toBeNull();
306
+ expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
307
+
308
+ const { props } = (inspectedElement: any);
309
+ expect(props.boolean_false).toBe(false);
310
+ expect(props.boolean_true).toBe(true);
311
+ expect(Number.isFinite(props.infinity)).toBe(false);
312
+ expect(props.integer_zero).toEqual(0);
313
+ expect(props.integer_one).toEqual(1);
314
+ expect(props.float).toEqual(1.23);
315
+ expect(props.string).toEqual('abc');
316
+ expect(props.string_empty).toEqual('');
317
+ expect(props.nan).toBeNaN();
318
+ expect(props.value_null).toBeNull();
319
+ expect(props.value_undefined).toBeUndefined();
320
+
321
+ done();
322
+ });
323
+
324
it('should support complex data types', async done => {
325
const Example = () => null;
326
src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap
+27
@@ -165,3 +165,30 @@ Object {
165
},
166
}
167
`;
168
+
169
+exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = `
170
+Object {
171
+ "id": 2,
172
+ "type": "full-data",
173
+ "value": {
174
+ "id": 2,
175
+ "owners": null,
176
+ "context": {},
177
+ "events": null,
178
+ "hooks": null,
179
+ "props": {
180
+ "boolean_false": false,
181
+ "boolean_true": true,
182
+ "infinity": null,
183
+ "integer_zero": 0,
184
+ "integer_one": 1,
185
+ "float": 1.23,
186
+ "string": "abc",
187
+ "string_empty": "",
188
+ "nan": null,
189
+ "value_null": null
190
+ },
191
+ "state": null
192
+},
193
+}
194
+`;
src/__tests__/legacy/inspectElement-test.js
+43
@@ -88,6 +88,49 @@ describe('InspectedElementContext', () => {
88
done();
89
});
90
91
+ it('should support simple data types', async done => {
92
+ const Example = () => null;
93
+
94
+ act(() =>
95
+ ReactDOM.render(
96
+ <Example
97
+ boolean_false={false}
98
+ boolean_true={true}
99
+ infinity={Infinity}
100
+ integer_zero={0}
101
+ integer_one={1}
102
+ float={1.23}
103
+ string="abc"
104
+ string_empty=""
105
+ nan={NaN}
106
+ value_null={null}
107
+ value_undefined={undefined}
108
+ />,
109
+ document.createElement('div')
110
+ )
111
+ );
112
+
113
+ const id = ((store.getElementIDAtIndex(0): any): number);
114
+ const inspectedElement = await read(id);
115
+
116
+ expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
117
+
118
+ const { props } = inspectedElement.value;
119
+ expect(props.boolean_false).toBe(false);
120
+ expect(props.boolean_true).toBe(true);
121
+ expect(Number.isFinite(props.infinity)).toBe(false);
122
+ expect(props.integer_zero).toEqual(0);
123
+ expect(props.integer_one).toEqual(1);
124
+ expect(props.float).toEqual(1.23);
125
+ expect(props.string).toEqual('abc');
126
+ expect(props.string_empty).toEqual('');
127
+ expect(props.nan).toBeNaN();
128
+ expect(props.value_null).toBeNull();
129
+ expect(props.value_undefined).toBeUndefined();
130
+
131
+ done();
132
+ });
133
+
134
it('should support complex data types', async done => {
135
const Example = () => null;
136
src/backend/NativeStyleEditor/resolveBoxStyle.js
new
+85
@@ -0,0 +1,85 @@
1
+// @flow
2
+
3
+import type { BoxStyle } from './types';
4
+
5
+/**
6
+ * This mirrors react-native/Libraries/Inspector/resolveBoxStyle.js (but without RTL support).
7
+ *
8
+ * Resolve a style property into it's component parts, e.g.
9
+ *
10
+ * resolveBoxStyle('margin', {margin: 5, marginBottom: 10})
11
+ * -> {top: 5, left: 5, right: 5, bottom: 10}
12
+ */
13
+export default function resolveBoxStyle(
14
+ prefix: string,
15
+ style: Object
16
+): BoxStyle | null {
17
+ let hasParts = false;
18
+ const result = {
19
+ bottom: 0,
20
+ left: 0,
21
+ right: 0,
22
+ top: 0,
23
+ };
24
+
25
+ const styleForAll = style[prefix];
26
+ if (styleForAll != null) {
27
+ for (const key of Object.keys(result)) {
28
+ result[key] = styleForAll;
29
+ }
30
+ hasParts = true;
31
+ }
32
+
33
+ const styleForHorizontal = style[prefix + 'Horizontal'];
34
+ if (styleForHorizontal != null) {
35
+ result.left = styleForHorizontal;
36
+ result.right = styleForHorizontal;
37
+ hasParts = true;
38
+ } else {
39
+ const styleForLeft = style[prefix + 'Left'];
40
+ if (styleForLeft != null) {
41
+ result.left = styleForLeft;
42
+ hasParts = true;
43
+ }
44
+
45
+ const styleForRight = style[prefix + 'Right'];
46
+ if (styleForRight != null) {
47
+ result.right = styleForRight;
48
+ hasParts = true;
49
+ }
50
+
51
+ const styleForEnd = style[prefix + 'End'];
52
+ if (styleForEnd != null) {
53
+ // TODO RTL support
54
+ result.right = styleForEnd;
55
+ hasParts = true;
56
+ }
57
+ const styleForStart = style[prefix + 'Start'];
58
+ if (styleForStart != null) {
59
+ // TODO RTL support
60
+ result.left = styleForStart;
61
+ hasParts = true;
62
+ }
63
+ }
64
+
65
+ const styleForVertical = style[prefix + 'Vertical'];
66
+ if (styleForVertical != null) {
67
+ result.bottom = styleForVertical;
68
+ result.top = styleForVertical;
69
+ hasParts = true;
70
+ } else {
71
+ const styleForBottom = style[prefix + 'Bottom'];
72
+ if (styleForBottom != null) {
73
+ result.bottom = styleForBottom;
74
+ hasParts = true;
75
+ }
76
+
77
+ const styleForTop = style[prefix + 'Top'];
78
+ if (styleForTop != null) {
79
+ result.top = styleForTop;
80
+ hasParts = true;
81
+ }
82
+ }
83
+
84
+ return hasParts ? result : null;
85
+}
src/backend/NativeStyleEditor/setupNativeStyleEditor.js
new
+315
@@ -0,0 +1,315 @@
1
+// @flow
2
+
3
+import Agent from 'src/backend/agent';
4
+import Bridge from 'src/bridge';
5
+import resolveBoxStyle from './resolveBoxStyle';
6
+
7
+import type { RendererID } from '../types';
8
+import type { StyleAndLayout } from './types';
9
+
10
+export type ResolveNativeStyle = (stylesheetID: number) => ?Object;
11
+
12
+export default function setupNativeStyleEditor(
13
+ bridge: Bridge,
14
+ agent: Agent,
15
+ resolveNativeStyle: ResolveNativeStyle,
16
+ validAttributes?: $ReadOnlyArray<string> | null
17
+) {
18
+ bridge.addListener(
19
+ 'NativeStyleEditor_measure',
20
+ ({ id, rendererID }: {| id: number, rendererID: RendererID |}) => {
21
+ measureStyle(agent, bridge, resolveNativeStyle, id, rendererID);
22
+ }
23
+ );
24
+
25
+ bridge.addListener(
26
+ 'NativeStyleEditor_renameAttribute',
27
+ ({
28
+ id,
29
+ rendererID,
30
+ oldName,
31
+ newName,
32
+ value,
33
+ }: {|
34
+ id: number,
35
+ rendererID: RendererID,
36
+ oldName: string,
37
+ newName: string,
38
+ value: string,
39
+ |}) => {
40
+ renameStyle(agent, id, rendererID, oldName, newName, value);
41
+ setTimeout(() =>
42
+ measureStyle(agent, bridge, resolveNativeStyle, id, rendererID)
43
+ );
44
+ }
45
+ );
46
+
47
+ bridge.addListener(
48
+ 'NativeStyleEditor_setValue',
49
+ ({
50
+ id,
51
+ rendererID,
52
+ name,
53
+ value,
54
+ }: {|
55
+ id: number,
56
+ rendererID: number,
57
+ name: string,
58
+ value: string,
59
+ |}) => {
60
+ setStyle(agent, id, rendererID, name, value);
61
+ setTimeout(() =>
62
+ measureStyle(agent, bridge, resolveNativeStyle, id, rendererID)
63
+ );
64
+ }
65
+ );
66
+
67
+ bridge.send('isNativeStyleEditorSupported', {
68
+ isSupported: true,
69
+ validAttributes,
70
+ });
71
+}
72
+
73
+const EMPTY_BOX_STYLE = {
74
+ top: 0,
75
+ left: 0,
76
+ right: 0,
77
+ bottom: 0,
78
+};
79
+
80
+const componentIDToStyleOverrides: Map<number, Object> = new Map();
81
+
82
+function measureStyle(
83
+ agent: Agent,
84
+ bridge: Bridge,
85
+ resolveNativeStyle: ResolveNativeStyle,
86
+ id: number,
87
+ rendererID: RendererID
88
+) {
89
+ const data = agent.getInstanceAndStyle({ id, rendererID });
90
+ if (!data || !data.style) {
91
+ bridge.send(
92
+ 'NativeStyleEditor_styleAndLayout',
93
+ ({
94
+ id,
95
+ layout: null,
96
+ style: null,
97
+ }: StyleAndLayout)
98
+ );
99
+ return;
100
+ }
101
+
102
+ const { instance, style } = data;
103
+
104
+ let resolvedStyle = resolveNativeStyle(style);
105
+
106
+ // If it's a host component we edited before, amend styles.
107
+ const styleOverrides = componentIDToStyleOverrides.get(id);
108
+ if (styleOverrides != null) {
109
+ resolvedStyle = Object.assign({}, resolvedStyle, styleOverrides);
110
+ }
111
+
112
+ if (!instance || typeof instance.measure !== 'function') {
113
+ bridge.send(
114
+ 'NativeStyleEditor_styleAndLayout',
115
+ ({
116
+ id,
117
+ layout: null,
118
+ style: resolvedStyle || null,
119
+ }: StyleAndLayout)
120
+ );
121
+ return;
122
+ }
123
+
124
+ // $FlowFixMe the parameter types of an unknown function are unknown
125
+ instance.measure((x, y, width, height, left, top) => {
126
+ // RN Android sometimes returns undefined here. Don't send measurements in this case.
127
+ // https://github.com/jhen0409/react-native-debugger/issues/84#issuecomment-304611817
128
+ if (typeof x !== 'number') {
129
+ bridge.send(
130
+ 'NativeStyleEditor_styleAndLayout',
131
+ ({
132
+ id,
133
+ layout: null,
134
+ style: resolvedStyle || null,
135
+ }: StyleAndLayout)
136
+ );
137
+ return;
138
+ }
139
+ const margin = resolveBoxStyle('margin', resolvedStyle) || EMPTY_BOX_STYLE;
140
+ const padding =
141
+ resolveBoxStyle('padding', resolvedStyle) || EMPTY_BOX_STYLE;
142
+ bridge.send(
143
+ 'NativeStyleEditor_styleAndLayout',
144
+ ({
145
+ id,
146
+ layout: {
147
+ x,
148
+ y,
149
+ width,
150
+ height,
151
+ left,
152
+ top,
153
+ margin,
154
+ padding,
155
+ },
156
+ style: resolvedStyle || null,
157
+ }: StyleAndLayout)
158
+ );
159
+ });
160
+}
161
+
162
+function shallowClone(object: Object): Object {
163
+ const cloned = {};
164
+ for (let n in object) {
165
+ cloned[n] = object[n];
166
+ }
167
+ return cloned;
168
+}
169
+
170
+function renameStyle(
171
+ agent: Agent,
172
+ id: number,
173
+ rendererID: RendererID,
174
+ oldName: string,
175
+ newName: string,
176
+ value: string
177
+): void {
178
+ const data = agent.getInstanceAndStyle({ id, rendererID });
179
+ if (!data || !data.style) {
180
+ return;
181
+ }
182
+
183
+ const { instance, style } = data;
184
+
185
+ const newStyle = newName
186
+ ? { [oldName]: undefined, [newName]: value }
187
+ : { [oldName]: undefined };
188
+
189
+ let customStyle;
190
+
191
+ // TODO It would be nice if the renderer interface abstracted this away somehow.
192
+ if (instance !== null && typeof instance.setNativeProps === 'function') {
193
+ // In the case of a host component, we need to use setNativeProps().
194
+ // Remember to "correct" resolved styles when we read them next time.
195
+ const styleOverrides = componentIDToStyleOverrides.get(id);
196
+ if (!styleOverrides) {
197
+ componentIDToStyleOverrides.set(id, newStyle);
198
+ } else {
199
+ Object.assign(styleOverrides, newStyle);
200
+ }
201
+ // TODO Fabric does not support setNativeProps; chat with Sebastian or Eli
202
+ instance.setNativeProps({ style: newStyle });
203
+ } else if (Array.isArray(style)) {
204
+ const lastIndex = style.length - 1;
205
+ if (
206
+ typeof style[lastIndex] === 'object' &&
207
+ !Array.isArray(style[lastIndex])
208
+ ) {
209
+ customStyle = shallowClone(style[lastIndex]);
210
+ delete customStyle[oldName];
211
+ if (newName) {
212
+ customStyle[newName] = value;
213
+ } else {
214
+ customStyle[oldName] = undefined;
215
+ }
216
+
217
+ agent.overrideProps({
218
+ id,
219
+ rendererID,
220
+ path: ['style', lastIndex],
221
+ value: customStyle,
222
+ });
223
+ } else {
224
+ agent.overrideProps({
225
+ id,
226
+ rendererID,
227
+ path: ['style'],
228
+ value: style.concat([newStyle]),
229
+ });
230
+ }
231
+ } else if (typeof style === 'object') {
232
+ customStyle = shallowClone(style);
233
+ delete customStyle[oldName];
234
+ if (newName) {
235
+ customStyle[newName] = value;
236
+ } else {
237
+ customStyle[oldName] = undefined;
238
+ }
239
+
240
+ agent.overrideProps({
241
+ id,
242
+ rendererID,
243
+ path: ['style'],
244
+ value: customStyle,
245
+ });
246
+ } else {
247
+ agent.overrideProps({
248
+ id,
249
+ rendererID,
250
+ path: ['style'],
251
+ value: [style, newStyle],
252
+ });
253
+ }
254
+
255
+ agent.emit('hideNativeHighlight');
256
+}
257
+
258
+function setStyle(
259
+ agent: Agent,
260
+ id: number,
261
+ rendererID: RendererID,
262
+ name: string,
263
+ value: string
264
+) {
265
+ const data = agent.getInstanceAndStyle({ id, rendererID });
266
+ if (!data || !data.style) {
267
+ return;
268
+ }
269
+
270
+ const { instance, style } = data;
271
+ const newStyle = { [name]: value };
272
+
273
+ // TODO It would be nice if the renderer interface abstracted this away somehow.
274
+ if (instance !== null && typeof instance.setNativeProps === 'function') {
275
+ // In the case of a host component, we need to use setNativeProps().
276
+ // Remember to "correct" resolved styles when we read them next time.
277
+ const styleOverrides = componentIDToStyleOverrides.get(id);
278
+ if (!styleOverrides) {
279
+ componentIDToStyleOverrides.set(id, newStyle);
280
+ } else {
281
+ Object.assign(styleOverrides, newStyle);
282
+ }
283
+ // TODO Fabric does not support setNativeProps; chat with Sebastian or Eli
284
+ instance.setNativeProps({ style: newStyle });
285
+ } else if (Array.isArray(style)) {
286
+ const lastLength = style.length - 1;
287
+ if (
288
+ typeof style[lastLength] === 'object' &&
289
+ !Array.isArray(style[lastLength])
290
+ ) {
291
+ agent.overrideProps({
292
+ id,
293
+ rendererID,
294
+ path: ['style', lastLength, name],
295
+ value,
296
+ });
297
+ } else {
298
+ agent.overrideProps({
299
+ id,
300
+ rendererID,
301
+ path: ['style'],
302
+ value: style.concat([newStyle]),
303
+ });
304
+ }
305
+ } else {
306
+ agent.overrideProps({
307
+ id,
308
+ rendererID,
309
+ path: ['style'],
310
+ value: [style, newStyle],
311
+ });
312
+ }
313
+
314
+ agent.emit('hideNativeHighlight');
315
+}
src/backend/NativeStyleEditor/types.js
new
+27
@@ -0,0 +1,27 @@
1
+// @flow
2
+
3
+export type BoxStyle = $ReadOnly<{|
4
+ bottom: number,
5
+ left: number,
6
+ right: number,
7
+ top: number,
8
+|}>;
9
+
10
+export type Layout = {|
11
+ x: number,
12
+ y: number,
13
+ width: number,
14
+ height: number,
15
+ left: number,
16
+ top: number,
17
+ margin: BoxStyle,
18
+ padding: BoxStyle,
19
+|};
20
+
21
+export type Style = Object;
22
+
23
+export type StyleAndLayout = {|
24
+ id: number,
25
+ style: Style | null,
26
+ layout: Layout | null,
27
+|};
src/backend/agent.js
+52
-153
@@ -1,7 +1,6 @@
1
// @flow
2
3
import EventEmitter from 'events';
4
-import memoize from 'memoize-one';
4
import throttle from 'lodash.throttle';
5
import Bridge from 'src/bridge';
6
import {
@@ -15,9 +14,11 @@ import {
14
sessionStorageRemoveItem,
15
sessionStorageSetItem,
16
} from 'src/storage';
18
-import { hideOverlay, showOverlay } from './views/Highlighter';
17
+import setupHighlighter from './views/Highlighter';
18
19
import type {
20
+ InstanceAndStyle,
21
+ NativeType,
22
OwnersList,
23
PathFrame,
24
PathMatch,
@@ -75,6 +76,8 @@ type PersistedSelection = {|
76
|};
77
78
export default class Agent extends EventEmitter<{|
79
+ hideNativeHighlight: [],
80
+ showNativeHighlight: [NativeType],
81
shutdown: [],
82
|}> {
83
_bridge: Bridge;
@@ -110,13 +113,8 @@ export default class Agent extends EventEmitter<{|
113
this._bridge = bridge;
114
115
bridge.addListener('captureScreenshot', this.captureScreenshot);
113
- bridge.addListener(
114
- 'clearHighlightedElementInDOM',
115
- this.clearHighlightedElementInDOM
116
- );
116
bridge.addListener('getProfilingData', this.getProfilingData);
117
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
119
- bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
118
bridge.addListener('getOwnersList', this.getOwnersList);
119
bridge.addListener('inspectElement', this.inspectElement);
120
bridge.addListener('logElementToConsole', this.logElementToConsole);
@@ -128,9 +126,7 @@ export default class Agent extends EventEmitter<{|
126
bridge.addListener('reloadAndProfile', this.reloadAndProfile);
127
bridge.addListener('screenshotCaptured', this.screenshotCaptured);
128
bridge.addListener('selectElement', this.selectElement);
131
- bridge.addListener('startInspectingDOM', this.startInspectingDOM);
129
bridge.addListener('startProfiling', this.startProfiling);
133
- bridge.addListener('stopInspectingDOM', this.stopInspectingDOM);
130
bridge.addListener('stopProfiling', this.stopProfiling);
131
bridge.addListener(
132
'syncSelectionFromNativeElementsPanel',
@@ -152,6 +148,12 @@ export default class Agent extends EventEmitter<{|
148
isBackendStorageAPISupported = true;
149
} catch (error) {}
150
bridge.send('isBackendStorageAPISupported', isBackendStorageAPISupported);
151
+
152
+ setupHighlighter(bridge, this);
153
+ }
154
+
155
+ get rendererInterfaces(): { [key: RendererID]: RendererInterface } {
156
+ return this._rendererInterfaces;
157
}
158
159
captureScreenshot = ({
@@ -164,6 +166,18 @@ export default class Agent extends EventEmitter<{|
166
this._bridge.send('captureScreenshot', { commitIndex, rootID });
167
};
168
169
+ getInstanceAndStyle({
170
+ id,
171
+ rendererID,
172
+ }: ElementAndRendererID): InstanceAndStyle | null {
173
+ const renderer = this._rendererInterfaces[rendererID];
174
+ if (renderer == null) {
175
+ console.warn(`Invalid renderer id "${rendererID}"`);
176
+ return null;
177
+ }
178
+ return renderer.getInstanceAndStyle(id);
179
+ }
180
+
181
getIDForNode(node: Object): number | null {
182
for (let rendererID in this._rendererInterfaces) {
183
// A renderer will throw if it can't find a fiber for the specified node.
@@ -190,54 +204,6 @@ export default class Agent extends EventEmitter<{|
204
this._bridge.send('profilingStatus', this._isProfiling);
205
};
206
193
- clearHighlightedElementInDOM = () => {
194
- hideOverlay();
195
- };
196
-
197
- highlightElementInDOM = ({
198
- displayName,
199
- hideAfterTimeout,
200
- id,
201
- openNativeElementsPanel,
202
- rendererID,
203
- scrollIntoView,
204
- }: {
205
- displayName: string,
206
- hideAfterTimeout: boolean,
207
- id: number,
208
- openNativeElementsPanel: boolean,
209
- rendererID: number,
210
- scrollIntoView: boolean,
211
- }) => {
212
- const renderer = this._rendererInterfaces[rendererID];
213
- if (renderer == null) {
214
- console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
215
- }
216
-
217
- let nodes: ?Array<HTMLElement> = null;
218
- if (renderer !== null) {
219
- nodes = ((renderer.findNativeNodesForFiberID(
220
- id
221
- ): any): ?Array<HTMLElement>);
222
- }
223
-
224
- if (nodes != null && nodes[0] != null) {
225
- const node = nodes[0];
226
- if (scrollIntoView && typeof node.scrollIntoView === 'function') {
227
- // If the node isn't visible show it before highlighting it.
228
- // We may want to reconsider this; it might be a little disruptive.
229
- node.scrollIntoView({ block: 'nearest', inline: 'nearest' });
230
- }
231
- showOverlay(nodes, displayName, hideAfterTimeout);
232
- if (openNativeElementsPanel) {
233
- window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = node;
234
- this._bridge.send('syncSelectionToNativeElementsPanel');
235
- }
236
- } else {
237
- hideOverlay();
238
- }
239
- };
240
-
207
getOwnersList = ({ id, rendererID }: ElementAndRendererID) => {
208
const renderer = this._rendererInterfaces[rendererID];
209
if (renderer == null) {
@@ -373,6 +339,13 @@ export default class Agent extends EventEmitter<{|
339
}
340
};
341
342
+ selectNode(target: Object): void {
343
+ const id = this.getIDForNode(target);
344
+ if (id !== null) {
345
+ this._bridge.send('selectFiber', id);
346
+ }
347
+ }
348
+
349
setRendererInterface(
350
rendererID: RendererID,
351
rendererInterface: RendererInterface
@@ -397,28 +370,14 @@ export default class Agent extends EventEmitter<{|
370
if (target == null) {
371
return;
372
}
400
- const id = this.getIDForNode(target);
401
- if (id !== null) {
402
- this._bridge.send('selectFiber', id);
403
- }
373
+ this.selectNode(target);
374
};
375
376
shutdown = () => {
377
// Clean up the overlay if visible, and associated events.
408
- this.stopInspectingDOM();
378
this.emit('shutdown');
379
};
380
412
- startInspectingDOM = () => {
413
- window.addEventListener('click', this._onClick, true);
414
- window.addEventListener('mousedown', this._onMouseEvent, true);
415
- window.addEventListener('mouseover', this._onMouseEvent, true);
416
- window.addEventListener('mouseup', this._onMouseEvent, true);
417
- window.addEventListener('pointerdown', this._onPointerDown, true);
418
- window.addEventListener('pointerover', this._onPointerOver, true);
419
- window.addEventListener('pointerup', this._onPointerUp, true);
420
- };
421
-
381
startProfiling = (recordChangeDescriptions: boolean) => {
382
this._recordChangeDescriptions = recordChangeDescriptions;
383
this._isProfiling = true;
@@ -431,18 +390,6 @@ export default class Agent extends EventEmitter<{|
390
this._bridge.send('profilingStatus', this._isProfiling);
391
};
392
434
- stopInspectingDOM = () => {
435
- hideOverlay();
436
-
437
- window.removeEventListener('click', this._onClick, true);
438
- window.removeEventListener('mousedown', this._onMouseEvent, true);
439
- window.removeEventListener('mouseover', this._onMouseEvent, true);
440
- window.removeEventListener('mouseup', this._onMouseEvent, true);
441
- window.removeEventListener('pointerdown', this._onPointerDown, true);
442
- window.removeEventListener('pointerover', this._onPointerOver, true);
443
- window.removeEventListener('pointerup', this._onPointerUp, true);
444
- };
445
-
393
stopProfiling = () => {
394
this._isProfiling = false;
395
this._recordChangeDescriptions = false;
@@ -473,7 +420,7 @@ export default class Agent extends EventEmitter<{|
420
}
421
};
422
476
- onHookOperations = (operations: Uint32Array) => {
423
+ onHookOperations = (operations: Array<number>) => {
424
if (__DEBUG__) {
425
debug('onHookOperations', operations);
426
}
@@ -505,80 +452,32 @@ export default class Agent extends EventEmitter<{|
452
if (this._persistedSelection.rendererID === rendererID) {
453
// Check if we can select a deeper match for the persisted selection.
454
const renderer = this._rendererInterfaces[rendererID];
508
- const prevMatch = this._persistedSelectionMatch;
509
- const nextMatch = renderer.getBestMatchForTrackedPath();
510
- this._persistedSelectionMatch = nextMatch;
511
- const prevMatchID = prevMatch !== null ? prevMatch.id : null;
512
- const nextMatchID = nextMatch !== null ? nextMatch.id : null;
513
- if (prevMatchID !== nextMatchID) {
514
- if (nextMatchID !== null) {
515
- // We moved forward, unlocking a deeper node.
516
- this._bridge.send('selectFiber', nextMatchID);
455
+ if (renderer == null) {
456
+ console.warn(`Invalid renderer id "${rendererID}"`);
457
+ } else {
458
+ const prevMatch = this._persistedSelectionMatch;
459
+ const nextMatch = renderer.getBestMatchForTrackedPath();
460
+ this._persistedSelectionMatch = nextMatch;
461
+ const prevMatchID = prevMatch !== null ? prevMatch.id : null;
462
+ const nextMatchID = nextMatch !== null ? nextMatch.id : null;
463
+ if (prevMatchID !== nextMatchID) {
464
+ if (nextMatchID !== null) {
465
+ // We moved forward, unlocking a deeper node.
466
+ this._bridge.send('selectFiber', nextMatchID);
467
+ }
468
+ }
469
+ if (nextMatch !== null && nextMatch.isFullMatch) {
470
+ // We've just unlocked the innermost selected node.
471
+ // There's no point tracking it further.
472
+ this._persistedSelection = null;
473
+ this._persistedSelectionMatch = null;
474
+ renderer.setTrackedPath(null);
475
}
518
- }
519
- if (nextMatch !== null && nextMatch.isFullMatch) {
520
- // We've just unlocked the innermost selected node.
521
- // There's no point tracking it further.
522
- this._persistedSelection = null;
523
- this._persistedSelectionMatch = null;
524
- renderer.setTrackedPath(null);
476
}
477
}
478
}
479
};
480
530
- _onClick = (event: MouseEvent) => {
531
- event.preventDefault();
532
- event.stopPropagation();
533
-
534
- this.stopInspectingDOM();
535
-
536
- this._bridge.send('stopInspectingDOM', true);
537
- };
538
-
539
- _onMouseEvent = (event: MouseEvent) => {
540
- event.preventDefault();
541
- event.stopPropagation();
542
- };
543
-
544
- _onPointerDown = (event: MouseEvent) => {
545
- event.preventDefault();
546
- event.stopPropagation();
547
-
548
- this._selectFiberForNode(((event.target: any): HTMLElement));
549
- };
550
-
551
- _onPointerOver = (event: MouseEvent) => {
552
- event.preventDefault();
553
- event.stopPropagation();
554
-
555
- const target = ((event.target: any): HTMLElement);
556
-
557
- // Don't pass the name explicitly.
558
- // It will be inferred from DOM tag and Fiber owner.
559
- showOverlay([target], null, false);
560
-
561
- this._selectFiberForNode(target);
562
- };
563
-
564
- _onPointerUp = (event: MouseEvent) => {
565
- event.preventDefault();
566
- event.stopPropagation();
567
- };
568
-
569
- _selectFiberForNode = throttle(
570
- memoize((node: HTMLElement) => {
571
- const id = this.getIDForNode(node);
572
- if (id !== null) {
573
- this._bridge.send('selectFiber', id);
574
- }
575
- }),
576
- 200,
577
- // Don't change the selection in the very first 200ms
578
- // because those are usually unintentional as you lift the cursor.
579
- { leading: false }
580
- );
581
-
481
_throttledPersistSelection = throttle((rendererID: number, id: number) => {
482
// This is throttled, so both renderer and selected ID
483
// might not be available by the time we read them.
src/backend/legacy/renderer.js
+31
-3
@@ -22,6 +22,7 @@ import type {
22
DevToolsHook,
23
GetFiberIDForNative,
24
InspectedElementPayload,
25
+ InstanceAndStyle,
26
NativeType,
27
PathFrame,
28
PathMatch,
@@ -456,7 +457,7 @@ export function attach(
457
const numUnmountIDs =
458
pendingUnmountedIDs.length + (pendingUnmountedRootID === null ? 0 : 1);
459
459
- const operations = new Uint32Array(
460
+ const operations = new Array(
461
// Identify which renderer this update is coming from.
462
2 + // [rendererID, rootFiberID]
463
// How big is the string table?
@@ -482,7 +483,10 @@ export function attach(
483
operations[i++] = pendingStringTableLength;
484
pendingStringTable.forEach((value, key) => {
485
operations[i++] = key.length;
485
- operations.set(utfEncodeString(key), i);
486
+ const encodedKey = utfEncodeString(key);
487
+ for (let j = 0; j < encodedKey.length; j++) {
488
+ operations[i + j] = encodedKey[j];
489
+ }
490
i += key.length;
491
});
492
@@ -503,7 +507,9 @@ export function attach(
507
}
508
509
// Fill in the rest of the operations.
506
- operations.set(pendingOperations, i);
510
+ for (let j = 0; j < pendingOperations.length; j++) {
511
+ operations[i + j] = pendingOperations[j];
512
+ }
513
i += pendingOperations.length;
514
515
if (__DEBUG__) {
@@ -582,6 +588,27 @@ export function attach(
588
};
589
}
590
591
+ // Fast path props lookup for React Native style editor.
592
+ function getInstanceAndStyle(id: number): InstanceAndStyle {
593
+ let instance = null;
594
+ let style = null;
595
+
596
+ const internalInstance = idToInternalInstanceMap.get(id);
597
+ if (internalInstance != null) {
598
+ instance = internalInstance._instance || null;
599
+
600
+ const element = internalInstance._currentElement;
601
+ if (element != null && element.props != null) {
602
+ style = element.props.style || null;
603
+ }
604
+ }
605
+
606
+ return {
607
+ instance,
608
+ style,
609
+ };
610
+ }
611
+
612
function inspectElement(
613
id: number,
614
path?: Array<string | number>
@@ -879,6 +906,7 @@ export function attach(
906
flushInitialOperations,
907
getBestMatchForTrackedPath,
908
getFiberIDForNative: getInternalIDForNative,
909
+ getInstanceAndStyle,
910
findNativeNodesForFiberID: (id: number) => {
911
const nativeNode = findNativeNodeForInternalID(id);
912
return nativeNode == null ? null : [nativeNode];
src/backend/renderer.js
+31
-5
@@ -47,6 +47,7 @@ import type {
47
Fiber,
48
InspectedElement,
49
InspectedElementPayload,
50
+ InstanceAndStyle,
51
Owner,
52
PathFrame,
53
PathMatch,
@@ -856,7 +857,7 @@ export function attach(
857
let pendingOperations: Array<number> = [];
858
let pendingRealUnmountedIDs: Array<number> = [];
859
let pendingSimulatedUnmountedIDs: Array<number> = [];
859
- let pendingOperationsQueue: Array<Uint32Array> | null = [];
860
+ let pendingOperationsQueue: Array<Array<number>> | null = [];
861
let pendingStringTable: Map<string, number> = new Map();
862
let pendingStringTableLength: number = 0;
863
let pendingUnmountedRootID: number | null = null;
@@ -893,7 +894,7 @@ export function attach(
894
pendingSimulatedUnmountedIDs.length +
895
(pendingUnmountedRootID === null ? 0 : 1);
896
896
- const operations = new Uint32Array(
897
+ const operations = new Array(
898
// Identify which renderer this update is coming from.
899
2 + // [rendererID, rootFiberID]
900
// How big is the string table?
@@ -919,7 +920,10 @@ export function attach(
920
operations[i++] = pendingStringTableLength;
921
pendingStringTable.forEach((value, key) => {
922
operations[i++] = key.length;
922
- operations.set(utfEncodeString(key), i);
923
+ const encodedKey = utfEncodeString(key);
924
+ for (let j = 0; j < encodedKey.length; j++) {
925
+ operations[i + j] = encodedKey[j];
926
+ }
927
i += key.length;
928
});
929
@@ -939,7 +943,9 @@ export function attach(
943
// They go *after* the real unmounts because we know for sure they won't be
944
// children of already pushed "real" IDs. If they were, we wouldn't be able
945
// to discover them during the traversal, as they would have been deleted.
942
- operations.set(pendingSimulatedUnmountedIDs, i);
946
+ for (let j = 0; j < pendingSimulatedUnmountedIDs.length; j++) {
947
+ operations[i + j] = pendingSimulatedUnmountedIDs[j];
948
+ }
949
i += pendingSimulatedUnmountedIDs.length;
950
// The root ID should always be unmounted last.
951
if (pendingUnmountedRootID !== null) {
@@ -948,7 +954,10 @@ export function attach(
954
}
955
}
956
// Fill in the rest of the operations.
951
- operations.set(pendingOperations, i);
957
+ for (let j = 0; j < pendingOperations.length; j++) {
958
+ operations[i + j] = pendingOperations[j];
959
+ }
960
+ i += pendingOperations.length;
961
962
// Let the frontend know about tree operations.
963
// The first value in this array will identify which root it corresponds to,
@@ -1969,6 +1978,22 @@ export function attach(
1978
return owners;
1979
}
1980
1981
+ // Fast path props lookup for React Native style editor.
1982
+ // Could use inspectElementRaw() but that would require shallow rendering hooks components,
1983
+ // and could also mess with memoization.
1984
+ function getInstanceAndStyle(id: number): InstanceAndStyle {
1985
+ let instance = null;
1986
+ let style = null;
1987
+
1988
+ let fiber = findCurrentFiberUsingSlowPathById(id);
1989
+ if (fiber !== null) {
1990
+ instance = fiber.stateNode;
1991
+ style = fiber.memoizedProps.style;
1992
+ }
1993
+
1994
+ return { instance, style };
1995
+ }
1996
+
1997
function inspectElementRaw(id: number): InspectedElement | null {
1998
let fiber = findCurrentFiberUsingSlowPathById(id);
1999
if (fiber == null) {
@@ -2837,6 +2862,7 @@ export function attach(
2862
flushInitialOperations,
2863
getBestMatchForTrackedPath,
2864
getFiberIDForNative,
2865
+ getInstanceAndStyle,
2866
getOwnersList,
2867
getPathForElement,
2868
getProfilingData,
src/backend/types.js
+11
@@ -2,6 +2,7 @@
2
3
import type { ComponentFilter, ElementType } from 'src/types';
4
import type { Interaction } from 'src/devtools/views/Profiler/types';
5
+import type { ResolveNativeStyle } from 'src/backend/NativeStyleEditor/setupNativeStyleEditor';
6
7
type BundleType =
8
| 0 // PROD
@@ -249,12 +250,18 @@ export type InspectedElementPayload =
250
| InspectElementNoChange
251
| InspectElementNotFound;
252
253
+export type InstanceAndStyle = {|
254
+ instance: Object | null,
255
+ style: Object | null,
256
+|};
257
+
258
export type RendererInterface = {
259
cleanup: () => void,
260
findNativeNodesForFiberID: FindNativeNodesForFiberID,
261
flushInitialOperations: () => void,
262
getBestMatchForTrackedPath: () => PathMatch | null,
263
getFiberIDForNative: GetFiberIDForNative,
264
+ getInstanceAndStyle(id: number): InstanceAndStyle,
265
getProfilingData(): ProfilingDataBackend,
266
getOwnersList: (id: number) => Array<Owner> | null,
267
getPathForElement: (id: number) => Array<PathFrame> | null,
@@ -299,6 +306,10 @@ export type DevToolsHook = {
306
reactDevtoolsAgent?: ?Object,
307
sub: (event: string, handler: Handler) => () => void,
308
309
+ // Used by react-native-web and Flipper/Inspector
310
+ resolveRNStyle?: ResolveNativeStyle,
311
+ nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
312
+
313
// React uses these methods.
314
checkDCE: (fn: Function) => void,
315
onCommitFiberUnmount: (rendererID: RendererID, fiber: Object) => void,
src/backend/views/Highlighter/Highlighter.js
renamed
+5
@@ -21,6 +21,11 @@ export function showOverlay(
21
componentName: string | null,
22
hideAfterTimeout: boolean
23
) {
24
+ // TODO (npm-packages) Detect RN and support it somehow
25
+ if (window.document == null) {
26
+ return;
27
+ }
28
+
29
if (timeoutID !== null) {
30
clearTimeout(timeoutID);
31
}
src/backend/views/Highlighter/Overlay.js
renamed
src/backend/views/Highlighter/index.js
new
+142
@@ -0,0 +1,142 @@
1
+// @flow
2
+
3
+import memoize from 'memoize-one';
4
+import throttle from 'lodash.throttle';
5
+import Bridge from 'src/bridge';
6
+import Agent from 'src/backend/agent';
7
+import { hideOverlay, showOverlay } from './Highlighter';
8
+
9
+export default function setup(bridge: Bridge, agent: Agent): void {
10
+ bridge.addListener(
11
+ 'clearNativeElementHighlight',
12
+ clearNativeElementHighlight
13
+ );
14
+ bridge.addListener('highlightNativeElement', highlightNativeElement);
15
+ bridge.addListener('shutdown', stopInspectingNative);
16
+ bridge.addListener('startInspectingNative', startInspectingNative);
17
+ bridge.addListener('stopInspectingNative', stopInspectingNative);
18
+
19
+ function startInspectingNative() {
20
+ window.addEventListener('click', onClick, true);
21
+ window.addEventListener('mousedown', onMouseEvent, true);
22
+ window.addEventListener('mouseover', onMouseEvent, true);
23
+ window.addEventListener('mouseup', onMouseEvent, true);
24
+ window.addEventListener('pointerdown', onPointerDown, true);
25
+ window.addEventListener('pointerover', onPointerOver, true);
26
+ window.addEventListener('pointerup', onPointerUp, true);
27
+ }
28
+
29
+ function stopInspectingNative() {
30
+ hideOverlay();
31
+
32
+ window.removeEventListener('click', onClick, true);
33
+ window.removeEventListener('mousedown', onMouseEvent, true);
34
+ window.removeEventListener('mouseover', onMouseEvent, true);
35
+ window.removeEventListener('mouseup', onMouseEvent, true);
36
+ window.removeEventListener('pointerdown', onPointerDown, true);
37
+ window.removeEventListener('pointerover', onPointerOver, true);
38
+ window.removeEventListener('pointerup', onPointerUp, true);
39
+ }
40
+
41
+ function clearNativeElementHighlight() {
42
+ hideOverlay();
43
+ }
44
+
45
+ function highlightNativeElement({
46
+ displayName,
47
+ hideAfterTimeout,
48
+ id,
49
+ openNativeElementsPanel,
50
+ rendererID,
51
+ scrollIntoView,
52
+ }: {
53
+ displayName: string,
54
+ hideAfterTimeout: boolean,
55
+ id: number,
56
+ openNativeElementsPanel: boolean,
57
+ rendererID: number,
58
+ scrollIntoView: boolean,
59
+ }) {
60
+ const renderer = agent.rendererInterfaces[rendererID];
61
+ if (renderer == null) {
62
+ console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
63
+ }
64
+
65
+ let nodes: ?Array<HTMLElement> = null;
66
+ if (renderer !== null) {
67
+ nodes = ((renderer.findNativeNodesForFiberID(
68
+ id
69
+ ): any): ?Array<HTMLElement>);
70
+ }
71
+
72
+ if (nodes != null && nodes[0] != null) {
73
+ const node = nodes[0];
74
+ if (scrollIntoView && typeof node.scrollIntoView === 'function') {
75
+ // If the node isn't visible show it before highlighting it.
76
+ // We may want to reconsider this; it might be a little disruptive.
77
+ node.scrollIntoView({ block: 'nearest', inline: 'nearest' });
78
+ }
79
+
80
+ showOverlay(nodes, displayName, hideAfterTimeout);
81
+
82
+ if (openNativeElementsPanel) {
83
+ window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = node;
84
+ bridge.send('syncSelectionToNativeElementsPanel');
85
+ }
86
+ } else {
87
+ hideOverlay();
88
+ }
89
+ }
90
+
91
+ function onClick(event: MouseEvent) {
92
+ event.preventDefault();
93
+ event.stopPropagation();
94
+
95
+ stopInspectingNative();
96
+
97
+ bridge.send('stopInspectingNative', true);
98
+ }
99
+
100
+ function onMouseEvent(event: MouseEvent) {
101
+ event.preventDefault();
102
+ event.stopPropagation();
103
+ }
104
+
105
+ function onPointerDown(event: MouseEvent) {
106
+ event.preventDefault();
107
+ event.stopPropagation();
108
+
109
+ selectFiberForNode(((event.target: any): HTMLElement));
110
+ }
111
+
112
+ function onPointerOver(event: MouseEvent) {
113
+ event.preventDefault();
114
+ event.stopPropagation();
115
+
116
+ const target = ((event.target: any): HTMLElement);
117
+
118
+ // Don't pass the name explicitly.
119
+ // It will be inferred from DOM tag and Fiber owner.
120
+ showOverlay([target], null, false);
121
+
122
+ selectFiberForNode(target);
123
+ }
124
+
125
+ function onPointerUp(event: MouseEvent) {
126
+ event.preventDefault();
127
+ event.stopPropagation();
128
+ }
129
+
130
+ const selectFiberForNode = throttle(
131
+ memoize((node: HTMLElement) => {
132
+ const id = agent.getIDForNode(node);
133
+ if (id !== null) {
134
+ bridge.send('selectFiber', id);
135
+ }
136
+ }),
137
+ 200,
138
+ // Don't change the selection in the very first 200ms
139
+ // because those are usually unintentional as you lift the cursor.
140
+ { leading: false }
141
+ );
142
+}
src/bridge.js
+29
-5
@@ -9,6 +9,7 @@ import type {
9
ProfilingDataBackend,
10
RendererID,
11
} from 'src/backend/types';
12
+import type { StyleAndLayout as StyleAndLayoutPayload } from 'src/backend/NativeStyleEditor/types';
13
14
const BATCH_DURATION = 100;
15
@@ -48,20 +49,34 @@ type InspectElementParams = {|
49
path?: Array<string | number>,
50
|};
51
52
+type NativeStyleEditor_RenameAttributeParams = {|
53
+ ...ElementAndRendererID,
54
+ oldName: string,
55
+ newName: string,
56
+ value: string,
57
+|};
58
+
59
+type NativeStyleEditor_SetValueParams = {|
60
+ ...ElementAndRendererID,
61
+ name: string,
62
+ value: string,
63
+|};
64
+
65
export default class Bridge extends EventEmitter<{|
66
captureScreenshot: [{| commitIndex: number, rootID: number |}],
53
- clearHighlightedElementInDOM: [],
67
+ clearNativeElementHighlight: [],
68
getOwnersList: [ElementAndRendererID],
69
getProfilingData: [{| rendererID: RendererID |}],
70
getProfilingStatus: [],
57
- highlightElementInDOM: [HighlightElementInDOM],
71
+ highlightNativeElement: [HighlightElementInDOM],
72
init: [],
73
inspectElement: [InspectElementParams],
74
inspectedElement: [InspectedElementPayload],
75
isBackendStorageAPISupported: [boolean],
76
logElementToConsole: [ElementAndRendererID],
63
- operations: [Uint32Array],
77
+ operations: [Array<number>],
78
ownersList: [OwnersList],
79
+ overrideComponentFilters: [Array<ComponentFilter>],
80
overrideContext: [OverrideValue],
81
overrideHookState: [OverrideHookState],
82
overrideProps: [OverrideValue],
@@ -77,14 +92,23 @@ export default class Bridge extends EventEmitter<{|
92
selectElement: [ElementAndRendererID],
93
selectFiber: [number],
94
shutdown: [],
80
- startInspectingDOM: [],
95
+ startInspectingNative: [],
96
startProfiling: [boolean],
82
- stopInspectingDOM: [boolean],
97
+ stopInspectingNative: [boolean],
98
stopProfiling: [],
99
syncSelectionFromNativeElementsPanel: [],
100
syncSelectionToNativeElementsPanel: [],
101
updateComponentFilters: [Array<ComponentFilter>],
102
viewElementSource: [ElementAndRendererID],
103
+
104
+ // React Native style editor plug-in.
105
+ isNativeStyleEditorSupported: [
106
+ {| isSupported: boolean, validAttributes: $ReadOnlyArray<string> |},
107
+ ],
108
+ NativeStyleEditor_measure: [ElementAndRendererID],
109
+ NativeStyleEditor_renameAttribute: [NativeStyleEditor_RenameAttributeParams],
110
+ NativeStyleEditor_setValue: [NativeStyleEditor_SetValueParams],
111
+ NativeStyleEditor_styleAndLayout: [StyleAndLayoutPayload],
112
|}> {
113
_isShutdown: boolean = false;
114
_messageQueue: Array<any> = [];
src/devtools/ProfilerStore.js
+2
-7
@@ -60,7 +60,7 @@ export default class ProfilerStore extends EventEmitter<{|
60
//
61
// This map is only updated while profiling is in progress;
62
// Upon completion, it is converted into the exportable ProfilingDataFrontend format.
63
- _inProgressOperationsByRootID: Map<number, Array<Uint32Array>> = new Map();
63
+ _inProgressOperationsByRootID: Map<number, Array<Array<number>>> = new Map();
64
65
// Map of root (id) to a Map of screenshots by commit ID.
66
// Stores screenshots for each commit (when profiling).
@@ -228,12 +228,7 @@ export default class ProfilerStore extends EventEmitter<{|
228
}
229
};
230
231
- onBridgeOperations = (operations: Uint32Array) => {
232
- if (!(operations instanceof Uint32Array)) {
233
- // $FlowFixMe TODO HACK Temporary workaround for the fact that Chrome is not transferring the typed array.
234
- operations = Uint32Array.from(Object.values(operations));
235
- }
236
-
231
+ onBridgeOperations = (operations: Array<number>) => {
232
// The first two values are always rendererID and rootID
233
const rendererID = operations[0];
234
const rootID = operations[1];
src/devtools/store.js
+56
-5
@@ -46,6 +46,7 @@ const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
46
type Config = {|
47
isProfiling?: boolean,
48
supportsCaptureScreenshots?: boolean,
49
+ supportsNativeInspection?: boolean,
50
supportsReloadAndProfile?: boolean,
51
supportsProfiling?: boolean,
52
|};
@@ -66,6 +67,7 @@ export default class Store extends EventEmitter<{|
67
mutated: [[Array<number>, Map<number, number>]],
68
recordChangeDescriptions: [],
69
roots: [],
70
+ supportsNativeStyleEditor: [],
71
supportsProfiling: [],
72
supportsReloadAndProfile: [],
73
|}> {
@@ -86,10 +88,15 @@ export default class Store extends EventEmitter<{|
88
// The InspectedElementContext also relies on this mutability for its WeakMap usage.
89
_idToElement: Map<number, Element> = new Map();
90
91
+ // Should the React Native style editor panel be shown?
92
+ _isNativeStyleEditorSupported: boolean = false;
93
+
94
// Can the backend use the Storage API (e.g. localStorage)?
95
// If not, features like reload-and-profile will not work correctly and must be disabled.
96
_isBackendStorageAPISupported: boolean = false;
97
98
+ _nativeStyleEditorValidAttributes: $ReadOnlyArray<string> | null = null;
99
+
100
// Map of element (id) to the set of elements (ids) it owns.
101
// This map enables getOwnersListForElement() to avoid traversing the entire tree.
102
_ownersMap: Map<number, Set<number>> = new Map();
@@ -114,6 +121,7 @@ export default class Store extends EventEmitter<{|
121
// These options may be initially set by a confiugraiton option when constructing the Store.
122
// In the case of "supportsProfiling", the option may be updated based on the injected renderers.
123
_supportsCaptureScreenshots: boolean = false;
124
+ _supportsNativeInspection: boolean = false;
125
_supportsProfiling: boolean = false;
126
_supportsReloadAndProfile: boolean = false;
127
@@ -145,6 +153,7 @@ export default class Store extends EventEmitter<{|
153
154
const {
155
supportsCaptureScreenshots,
156
+ supportsNativeInspection,
157
supportsProfiling,
158
supportsReloadAndProfile,
159
} = config;
@@ -153,6 +162,7 @@ export default class Store extends EventEmitter<{|
162
this._captureScreenshots =
163
localStorageGetItem(LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY) === 'true';
164
}
165
+ this._supportsNativeInspection = supportsNativeInspection !== false;
166
if (supportsProfiling) {
167
this._supportsProfiling = true;
168
}
@@ -163,11 +173,19 @@ export default class Store extends EventEmitter<{|
173
174
this._bridge = bridge;
175
bridge.addListener('operations', this.onBridgeOperations);
176
+ bridge.addListener(
177
+ 'overrideComponentFilters',
178
+ this.onBridgeOverrideComponentFilters
179
+ );
180
bridge.addListener('shutdown', this.onBridgeShutdown);
181
bridge.addListener(
182
'isBackendStorageAPISupported',
183
this.onBridgeStorageSupported
184
);
185
+ bridge.addListener(
186
+ 'isNativeStyleEditorSupported',
187
+ this.onBridgeNativeStyleEditorSupported
188
+ );
189
190
this._profilerStore = new ProfilerStore(bridge, this, isProfiling);
191
}
@@ -283,6 +301,10 @@ export default class Store extends EventEmitter<{|
301
return this._hasOwnerMetadata;
302
}
303
304
+ get nativeStyleEditorValidAttributes(): $ReadOnlyArray<string> | null {
305
+ return this._nativeStyleEditorValidAttributes;
306
+ }
307
+
308
get numElements(): number {
309
return this._weightAcrossRoots;
310
}
@@ -321,6 +343,14 @@ export default class Store extends EventEmitter<{|
343
return this._supportsCaptureScreenshots;
344
}
345
346
+ get supportsNativeInspection(): boolean {
347
+ return this._supportsNativeInspection;
348
+ }
349
+
350
+ get supportsNativeStyleEditor(): boolean {
351
+ return this._isNativeStyleEditorSupported;
352
+ }
353
+
354
get supportsProfiling(): boolean {
355
return this._supportsProfiling;
356
}
@@ -666,12 +696,20 @@ export default class Store extends EventEmitter<{|
696
}
697
};
698
669
- onBridgeOperations = (operations: Uint32Array) => {
670
- if (!(operations instanceof Uint32Array)) {
671
- // $FlowFixMe TODO HACK Temporary workaround for the fact that Chrome is not transferring the typed array.
672
- operations = Uint32Array.from(Object.values(operations));
673
- }
699
+ onBridgeNativeStyleEditorSupported = ({
700
+ isSupported,
701
+ validAttributes,
702
+ }: {|
703
+ isSupported: boolean,
704
+ validAttributes: $ReadOnlyArray<string>,
705
+ |}) => {
706
+ this._isNativeStyleEditorSupported = isSupported;
707
+ this._nativeStyleEditorValidAttributes = validAttributes || null;
708
+
709
+ this.emit('supportsNativeStyleEditor');
710
+ };
711
712
+ onBridgeOperations = (operations: Array<number>) => {
713
if (__DEBUG__) {
714
console.groupCollapsed('onBridgeOperations');
715
debug('onBridgeOperations', operations.join(','));
@@ -965,6 +1003,19 @@ export default class Store extends EventEmitter<{|
1003
this.emit('mutated', [addedElementIDs, removedElementIDs]);
1004
};
1005
1006
+ // Certain backends save filters on a per-domain basis.
1007
+ // In order to prevent filter preferences and applied filters from being out of sync,
1008
+ // this message enables the backend to override the frontend's current ("saved") filters.
1009
+ // This action should also override the saved filters too,
1010
+ // else reloading the frontend without reloading the backend would leave things out of sync.
1011
+ onBridgeOverrideComponentFilters = (
1012
+ componentFilters: Array<ComponentFilter>
1013
+ ) => {
1014
+ this._componentFilters = componentFilters;
1015
+
1016
+ saveComponentFilters(componentFilters);
1017
+ };
1018
+
1019
onBridgeShutdown = () => {
1020
if (__DEBUG__) {
1021
debug('onBridgeShutdown', 'unsubscribing from Bridge');
src/devtools/views/Components/Components.css
+8
@@ -24,6 +24,14 @@
24
.Components {
25
flex-direction: column;
26
}
27
+
28
+ .TreeWrapper {
29
+ flex: 0 0 50%;
30
+ }
31
+
32
+ .SelectedElementWrapper {
33
+ flex: 0 0 50%;
34
+ }
35
}
36
37
.Loading {
src/devtools/views/Components/Components.js
+6
-3
@@ -4,6 +4,7 @@ import React, { Suspense } from 'react';
4
import Tree from './Tree';
5
import SelectedElement from './SelectedElement';
6
import { InspectedElementContextController } from './InspectedElementContext';
7
+import { NativeStyleContextController } from './NativeStyleEditor/context';
8
import { OwnersListContextController } from './OwnersListContext';
9
import portaledContent from '../portaledContent';
10
import { ModalDialog } from '../ModalDialog';
@@ -23,9 +24,11 @@ function Components(_: {||}) {
24
<Tree />
25
</div>
26
<div className={styles.SelectedElementWrapper}>
26
- <Suspense fallback={<Loading />}>
27
- <SelectedElement />
28
- </Suspense>
27
+ <NativeStyleContextController>
28
+ <Suspense fallback={<Loading />}>
29
+ <SelectedElement />
30
+ </Suspense>
31
+ </NativeStyleContextController>
32
</div>
33
<ModalDialog />
34
<SettingsModal />
src/devtools/views/Components/Element.css
-5
@@ -22,10 +22,6 @@
22
user-select: none;
23
}
24
25
-.Bracket {
26
- color: var(--color-jsx-arrow-brackets);
27
-}
28
-
25
.ScrollAnchor {
26
height: 100%;
27
width: 0;
@@ -42,7 +38,6 @@
38
--color-component-badge-background-inverted
39
);
40
--color-component-badge-count: var(--color-component-badge-count-inverted);
45
- --color-jsx-arrow-brackets: var(--color-jsx-arrow-brackets-inverted);
41
--color-attribute-name: var(--color-attribute-name-inverted);
42
--color-attribute-value: var(--color-attribute-value-inverted);
43
--color-expand-collapse-toggle: var(--color-component-name-inverted);
src/devtools/views/Components/Element.js
-2
@@ -119,7 +119,6 @@ export default function ElementView({ data, index, style }: Props) {
119
{ownerID === null ? (
120
<ExpandCollapseToggle element={element} store={store} />
121
) : null}
122
- <span className={styles.Bracket}><</span>
122
<DisplayName displayName={displayName} id={((id: any): number)} />
123
{key && (
124
<Fragment>
@@ -129,7 +128,6 @@ export default function ElementView({ data, index, style }: Props) {
128
</span>
129
</Fragment>
130
)}
132
- <span className={styles.Bracket}>></span>
131
<Badge
132
className={styles.Badge}
133
hocDisplayNames={hocDisplayNames}
src/devtools/views/Components/InspectHostNodesToggle.js
+5
-5
@@ -14,19 +14,19 @@ export default function InspectHostNodesToggle() {
14
setIsInspecting(isChecked);
15
16
if (isChecked) {
17
- bridge.send('startInspectingDOM');
17
+ bridge.send('startInspectingNative');
18
} else {
19
- bridge.send('stopInspectingDOM', false);
19
+ bridge.send('stopInspectingNative', false);
20
}
21
},
22
[bridge]
23
);
24
25
useEffect(() => {
26
- const onStopInspectingDOM = () => setIsInspecting(false);
27
- bridge.addListener('stopInspectingDOM', onStopInspectingDOM);
26
+ const onStopInspectingNative = () => setIsInspecting(false);
27
+ bridge.addListener('stopInspectingNative', onStopInspectingNative);
28
return () =>
29
- bridge.removeListener('stopInspectingDOM', onStopInspectingDOM);
29
+ bridge.removeListener('stopInspectingNative', onStopInspectingNative);
30
}, [bridge]);
31
32
return (
src/devtools/views/Components/NativeStyleEditor/AutoSizeInput.css
new
+26
@@ -0,0 +1,26 @@
1
+.Input {
2
+ width: 0;
3
+ min-width: 0.5rem;
4
+ flex: 1 1 auto;
5
+ border: none;
6
+ background: transparent;
7
+ outline: none;
8
+ padding: 0;
9
+ border: none;
10
+ font-family: var(--font-family-monospace);
11
+ font-size: var(--font-size-monospace-normal);
12
+}
13
+
14
+.Input:focus {
15
+ border-color: var(--color-border);
16
+}
17
+
18
+.HiddenDiv {
19
+ position: absolute;
20
+ top: 0;
21
+ left: 0;
22
+ visibility: hidden;
23
+ height: 0;
24
+ overflow: scroll;
25
+ white-space: pre;
26
+}
src/devtools/views/Components/NativeStyleEditor/AutoSizeInput.js
new
+93
@@ -0,0 +1,93 @@
1
+// @flow
2
+
3
+import React, { Fragment, useLayoutEffect, useRef } from 'react';
4
+import styles from './AutoSizeInput.css';
5
+
6
+type Props = {
7
+ className?: string,
8
+ onFocus?: (event: FocusEvent) => void,
9
+ placeholder?: string,
10
+ value: any,
11
+};
12
+
13
+export default function AutoSizeInput({
14
+ className,
15
+ onFocus,
16
+ placeholder,
17
+ value,
18
+ ...rest
19
+}: Props) {
20
+ const hiddenDivRef = useRef<HTMLDivElement | null>(null);
21
+ const inputRef = useRef<HTMLInputElement | null>(null);
22
+
23
+ const onFocusWrapper = event => {
24
+ if (inputRef.current !== null) {
25
+ inputRef.current.selectionStart = 0;
26
+ inputRef.current.selectionEnd = value.length;
27
+ }
28
+
29
+ if (typeof onFocus === 'function') {
30
+ onFocus(event);
31
+ }
32
+ };
33
+
34
+ // Copy text stlyes from <input> to hidden sizing <div>
35
+ useLayoutEffect(() => {
36
+ if (
37
+ typeof window.getComputedStyle !== 'function' ||
38
+ inputRef.current === null
39
+ ) {
40
+ return;
41
+ }
42
+
43
+ const inputStyle = window.getComputedStyle(inputRef.current);
44
+ if (!inputStyle) {
45
+ return;
46
+ }
47
+
48
+ if (hiddenDivRef.current !== null) {
49
+ const divStyle = hiddenDivRef.current.style;
50
+ divStyle.border = inputStyle.border;
51
+ divStyle.fontFamily = inputStyle.fontFamily;
52
+ divStyle.fontSize = inputStyle.fontSize;
53
+ divStyle.fontStyle = inputStyle.fontStyle;
54
+ divStyle.fontWeight = inputStyle.fontWeight;
55
+ divStyle.letterSpacing = inputStyle.letterSpacing;
56
+ divStyle.padding = inputStyle.padding;
57
+ }
58
+ }, []);
59
+
60
+ // Resize input any time text changes
61
+ useLayoutEffect(() => {
62
+ if (hiddenDivRef.current === null) {
63
+ return;
64
+ }
65
+
66
+ const scrollWidth = hiddenDivRef.current.getBoundingClientRect().width;
67
+ if (!scrollWidth) {
68
+ return;
69
+ }
70
+
71
+ if (inputRef.current !== null) {
72
+ inputRef.current.style.width = `${scrollWidth}px`;
73
+ }
74
+ }, [value]);
75
+
76
+ const isEmpty = value === '' || value === '""';
77
+
78
+ return (
79
+ <Fragment>
80
+ <input
81
+ ref={inputRef}
82
+ className={`${className ? className : ''} ${styles.Input}`}
83
+ onFocus={onFocusWrapper}
84
+ placeholder={placeholder}
85
+ value={isEmpty ? '' : value}
86
+ {...rest}
87
+ />
88
+ <div ref={hiddenDivRef} className={styles.HiddenDiv}>
89
+ {isEmpty ? placeholder : value}
90
+ </div>
91
+ </Fragment>
92
+ );
93
+}
src/devtools/views/Components/NativeStyleEditor/LayoutViewer.css
new
+50
@@ -0,0 +1,50 @@
1
+.LayoutViewer {
2
+ padding: 0.25rem;
3
+ border-top: 1px solid var(--color-border);
4
+ font-family: var(--font-family-monospace);
5
+ font-size: var(--font-size-monospace-small);
6
+}
7
+
8
+.Header {
9
+ font-family: var(--font-family-sans);
10
+}
11
+
12
+.DashedBox,
13
+.SolidBox {
14
+ flex: 1 1 auto;
15
+ display: flex;
16
+ flex-direction: column;
17
+ align-items: center;
18
+ border-width: 1px;
19
+ border-color: var(--color-dim);
20
+ padding: 0.25rem;
21
+ margin: 0.25rem;
22
+}
23
+.DashedBox {
24
+ border-style: dashed;
25
+}
26
+.SolidBox {
27
+ border-style: solid;
28
+}
29
+
30
+.LabelRow {
31
+ width: 100%;
32
+ position: relative;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+}
37
+
38
+.Label {
39
+ position: absolute;
40
+ left: 0.5rem;
41
+ flex: 1 0 100px;
42
+ color: var(--color-attribute-name);
43
+}
44
+
45
+.BoxRow {
46
+ width: 100%;
47
+ display: flex;
48
+ flex-direction: row;
49
+ align-items: center;
50
+}
src/devtools/views/Components/NativeStyleEditor/LayoutViewer.js
new
+64
@@ -0,0 +1,64 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import styles from './LayoutViewer.css';
5
+
6
+import type { Layout } from './types';
7
+
8
+type Props = {|
9
+ id: number,
10
+ layout: Layout,
11
+|};
12
+
13
+export default function LayoutViewer({ id, layout }: Props) {
14
+ const { height, margin, padding, y, width, x } = layout;
15
+
16
+ return (
17
+ <div className={styles.LayoutViewer}>
18
+ <div className={styles.Header}>layout</div>
19
+ <div className={styles.DashedBox}>
20
+ <div className={styles.LabelRow}>
21
+ <label className={styles.Label}>margin</label>
22
+
23
+ <label>{margin.top || '-'}</label>
24
+ </div>
25
+
26
+ <div className={styles.BoxRow}>
27
+ <label>{margin.left || '-'}</label>
28
+
29
+ <div className={styles.SolidBox}>
30
+ <div className={styles.LabelRow}>
31
+ <label className={styles.Label}>padding</label>
32
+
33
+ <label>{padding.top || '-'}</label>
34
+ </div>
35
+
36
+ <div className={styles.BoxRow}>
37
+ <label>{padding.left || '-'}</label>
38
+
39
+ <div className={styles.DashedBox}>
40
+ <div className={styles.LabelRow}>
41
+ {format(width)} x {format(height)} ({format(x)}, {format(y)})
42
+ </div>
43
+ </div>
44
+
45
+ <label>{padding.right || '-'}</label>
46
+ </div>
47
+
48
+ <label>{padding.bottom || '-'}</label>
49
+ </div>
50
+ <label>{margin.right || '-'}</label>
51
+ </div>
52
+ <label>{margin.bottom || '-'}</label>
53
+ </div>
54
+ </div>
55
+ );
56
+}
57
+
58
+function format(number: number): string | number {
59
+ if (Math.round(number) === number) {
60
+ return number;
61
+ } else {
62
+ return number.toFixed(1);
63
+ }
64
+}
src/devtools/views/Components/NativeStyleEditor/StyleEditor.css
new
+52
@@ -0,0 +1,52 @@
1
+.StyleEditor {
2
+ font-family: var(--font-family-monospace);
3
+ font-size: var(--font-size-monospace-normal);
4
+ padding: 0.25rem;
5
+ border-top: 1px solid var(--color-border);
6
+}
7
+
8
+.HeaderRow {
9
+ display: flex;
10
+ align-items: center;
11
+}
12
+
13
+.Header {
14
+ flex: 1 1;
15
+}
16
+
17
+.Brackets {
18
+ font-family: var(--font-family-sans);
19
+ font-size: var(--font-size-sans-small);
20
+}
21
+
22
+.Row {
23
+ white-space: nowrap;
24
+ padding-left: 1rem;
25
+ display: flex;
26
+ align-items: center;
27
+}
28
+
29
+.Invalid {
30
+ background-color: var(--color-background-invalid);
31
+ color: var(--color-text-invalid);
32
+
33
+ --color-border: var(--color-text-invalid);
34
+}
35
+.Attribute {
36
+ color: var(--color-attribute-name);
37
+}
38
+
39
+.Value {
40
+ color: var(--color-attribute-value);
41
+}
42
+
43
+.Input {
44
+ flex: 0 1 auto;
45
+}
46
+
47
+.Empty {
48
+ color: var(--color-dimmer);
49
+ font-style: italic;
50
+ user-select: none;
51
+ padding-left: 1rem;
52
+}
src/devtools/views/Components/NativeStyleEditor/StyleEditor.js
new
+278
@@ -0,0 +1,278 @@
1
+// @flow
2
+
3
+import React, { useContext, useMemo, useRef, useState } from 'react';
4
+import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
5
+import { copy } from 'clipboard-js';
6
+import { BridgeContext, StoreContext } from 'src/devtools/views/context';
7
+import Button from '../../Button';
8
+import ButtonIcon from '../../ButtonIcon';
9
+import { serializeDataForCopy } from '../../utils';
10
+import AutoSizeInput from './AutoSizeInput';
11
+import styles from './StyleEditor.css';
12
+
13
+import type { Style } from './types';
14
+
15
+type Props = {|
16
+ id: number,
17
+ style: Style,
18
+|};
19
+
20
+type ChangeAttributeFn = (oldName: string, newName: string, value: any) => void;
21
+type ChangeValueFn = (name: string, value: any) => void;
22
+
23
+export default function StyleEditor({ id, style }: Props) {
24
+ const bridge = useContext(BridgeContext);
25
+ const store = useContext(StoreContext);
26
+
27
+ const changeAttribute = (oldName: string, newName: string, value: any) => {
28
+ bridge.send('NativeStyleEditor_renameAttribute', {
29
+ id,
30
+ rendererID: store.getRendererIDForElement(id),
31
+ oldName,
32
+ newName,
33
+ value,
34
+ });
35
+ };
36
+
37
+ const changeValue = (name: string, value: any) => {
38
+ bridge.send('NativeStyleEditor_setValue', {
39
+ id,
40
+ rendererID: store.getRendererIDForElement(id),
41
+ name,
42
+ value,
43
+ });
44
+ };
45
+
46
+ const keys = useMemo(() => Array.from(Object.keys(style)), [style]);
47
+
48
+ const handleCopy = () => copy(serializeDataForCopy(style));
49
+
50
+ return (
51
+ <div className={styles.StyleEditor}>
52
+ <div className={styles.HeaderRow}>
53
+ <div className={styles.Header}>
54
+ <div className={styles.Brackets}>{'style {'}</div>
55
+ </div>
56
+ <Button onClick={handleCopy} title="Copy to clipboard">
57
+ <ButtonIcon type="copy" />
58
+ </Button>
59
+ </div>
60
+ {keys.length > 0 &&
61
+ keys.map(attribute => (
62
+ <Row
63
+ key={attribute}
64
+ attribute={attribute}
65
+ changeAttribute={changeAttribute}
66
+ changeValue={changeValue}
67
+ validAttributes={store.nativeStyleEditorValidAttributes}
68
+ value={style[attribute]}
69
+ />
70
+ ))}
71
+ <NewRow
72
+ changeAttribute={changeAttribute}
73
+ changeValue={changeValue}
74
+ validAttributes={store.nativeStyleEditorValidAttributes}
75
+ />
76
+ <div className={styles.Brackets}>{'}'}</div>
77
+ </div>
78
+ );
79
+}
80
+
81
+type NewRowProps = {|
82
+ changeAttribute: ChangeAttributeFn,
83
+ changeValue: ChangeValueFn,
84
+ validAttributes: $ReadOnlyArray<string> | null,
85
+|};
86
+
87
+function NewRow({
88
+ changeAttribute,
89
+ changeValue,
90
+ validAttributes,
91
+}: NewRowProps) {
92
+ const [key, setKey] = useState<number>(0);
93
+ const reset = () => setKey(key + 1);
94
+
95
+ const newAttributeRef = useRef<string>('');
96
+
97
+ const changeAttributeWrapper = (
98
+ oldAttribute: string,
99
+ newAttribute: string,
100
+ value: any
101
+ ) => {
102
+ // Ignore attribute changes until a value has been specified
103
+ newAttributeRef.current = newAttribute;
104
+ };
105
+
106
+ const changeValueWrapper = (attribute: string, value: any) => {
107
+ // Blur events should reset/cancel if there's no value or no attribute
108
+ if (newAttributeRef.current !== '') {
109
+ if (value !== '') {
110
+ changeValue(newAttributeRef.current, value);
111
+ }
112
+ reset();
113
+ }
114
+ };
115
+
116
+ return (
117
+ <Row
118
+ key={key}
119
+ attribute={''}
120
+ attributePlaceholder="attribute"
121
+ changeAttribute={changeAttributeWrapper}
122
+ changeValue={changeValueWrapper}
123
+ validAttributes={validAttributes}
124
+ value={''}
125
+ valuePlaceholder="value"
126
+ />
127
+ );
128
+}
129
+
130
+type RowProps = {|
131
+ attribute: string,
132
+ attributePlaceholder?: string,
133
+ changeAttribute: ChangeAttributeFn,
134
+ changeValue: ChangeValueFn,
135
+ validAttributes: $ReadOnlyArray<string> | null,
136
+ value: any,
137
+ valuePlaceholder?: string,
138
+|};
139
+
140
+function Row({
141
+ attribute,
142
+ attributePlaceholder,
143
+ changeAttribute,
144
+ changeValue,
145
+ validAttributes,
146
+ value,
147
+ valuePlaceholder,
148
+}: RowProps) {
149
+ // TODO (RN style editor) Use @reach/combobox to auto-complete attributes.
150
+ // The list of valid attributes would need to be injected by RN backend,
151
+ // which would need to require them from ReactNativeViewViewConfig "validAttributes.style" keys.
152
+ // This would need to degrade gracefully for react-native-web,
153
+ // althoguh we could let it also inject a custom set of whitelisted attributes.
154
+
155
+ const [localAttribute, setLocalAttribute] = useState(attribute);
156
+ const [localValue, setLocalValue] = useState(JSON.stringify(value));
157
+ const [isAttributeValid, setIsAttributeValid] = useState(true);
158
+ const [isValueValid, setIsValueValid] = useState(true);
159
+
160
+ const validateAndSetLocalAttribute = attribute => {
161
+ const isValid =
162
+ attribute === '' ||
163
+ validAttributes === null ||
164
+ validAttributes.indexOf(attribute) >= 0;
165
+
166
+ batchedUpdates(() => {
167
+ setLocalAttribute(attribute);
168
+ setIsAttributeValid(isValid);
169
+ });
170
+ };
171
+
172
+ const validateAndSetLocalValue = value => {
173
+ let isValid = false;
174
+ try {
175
+ JSON.parse(value);
176
+ isValid = true;
177
+ } catch (error) {}
178
+
179
+ batchedUpdates(() => {
180
+ setLocalValue(value);
181
+ setIsValueValid(isValid);
182
+ });
183
+ };
184
+
185
+ const resetAttribute = () => {
186
+ setLocalAttribute(attribute);
187
+ };
188
+
189
+ const resetValue = () => {
190
+ setLocalValue(value);
191
+ };
192
+
193
+ const submitValueChange = () => {
194
+ if (isValueValid) {
195
+ const parsedLocalValue = JSON.parse(localValue);
196
+ if (value !== parsedLocalValue) {
197
+ changeValue(attribute, parsedLocalValue);
198
+ }
199
+ }
200
+ };
201
+
202
+ const submitAttributeChange = () => {
203
+ if (isAttributeValid && attribute !== localAttribute) {
204
+ changeAttribute(attribute, localAttribute, value);
205
+ }
206
+ };
207
+
208
+ return (
209
+ <div className={styles.Row}>
210
+ <Field
211
+ className={isAttributeValid ? styles.Attribute : styles.Invalid}
212
+ onChange={validateAndSetLocalAttribute}
213
+ onReset={resetAttribute}
214
+ onSubmit={submitAttributeChange}
215
+ placeholder={attributePlaceholder}
216
+ value={localAttribute}
217
+ />
218
+ :
219
+ <Field
220
+ className={isValueValid ? styles.Value : styles.Invalid}
221
+ onChange={validateAndSetLocalValue}
222
+ onReset={resetValue}
223
+ onSubmit={submitValueChange}
224
+ placeholder={valuePlaceholder}
225
+ value={localValue}
226
+ />
227
+ ;
228
+ </div>
229
+ );
230
+}
231
+
232
+type FieldProps = {|
233
+ className: string,
234
+ onChange: (value: any) => void,
235
+ onReset: () => void,
236
+ onSubmit: () => void,
237
+ placeholder?: string,
238
+ value: any,
239
+|};
240
+
241
+function Field({
242
+ className,
243
+ onChange,
244
+ onReset,
245
+ onSubmit,
246
+ placeholder,
247
+ value,
248
+}: FieldProps) {
249
+ const onKeyDown = event => {
250
+ switch (event.key) {
251
+ case 'Enter':
252
+ onSubmit();
253
+ break;
254
+ case 'Escape':
255
+ onReset();
256
+ break;
257
+ case 'ArrowDown':
258
+ case 'ArrowLeft':
259
+ case 'ArrowRight':
260
+ case 'ArrowUp':
261
+ event.stopPropagation();
262
+ break;
263
+ default:
264
+ break;
265
+ }
266
+ };
267
+
268
+ return (
269
+ <AutoSizeInput
270
+ className={`${className} ${styles.Input}`}
271
+ onBlur={onSubmit}
272
+ onChange={event => onChange(event.target.value)}
273
+ onKeyDown={onKeyDown}
274
+ placeholder={placeholder}
275
+ value={value}
276
+ />
277
+ );
278
+}
src/devtools/views/Components/NativeStyleEditor/context.js
new
+187
@@ -0,0 +1,187 @@
1
+// @flow
2
+
3
+import React, {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useState,
10
+} from 'react';
11
+import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
12
+import { createResource } from 'src/devtools/cache';
13
+import { BridgeContext, StoreContext } from 'src/devtools/views/context';
14
+import { TreeStateContext } from '../TreeContext';
15
+
16
+import type { StyleAndLayout as StyleAndLayoutBackend } from 'src/backend/NativeStyleEditor/types';
17
+import type { StyleAndLayout as StyleAndLayoutFrontend } from './types';
18
+import type { Element } from 'src/devtools/views/Components/types';
19
+import type { Resource, Thenable } from 'src/devtools/cache';
20
+
21
+export type GetStyleAndLayout = (id: number) => StyleAndLayoutFrontend | null;
22
+
23
+type Context = {|
24
+ getStyleAndLayout: GetStyleAndLayout,
25
+|};
26
+
27
+const NativeStyleContext = createContext<Context>(((null: any): Context));
28
+NativeStyleContext.displayName = 'NativeStyleContext';
29
+
30
+type ResolveFn = (styleAndLayout: StyleAndLayoutFrontend) => void;
31
+type InProgressRequest = {|
32
+ promise: Thenable<StyleAndLayoutFrontend>,
33
+ resolveFn: ResolveFn,
34
+|};
35
+
36
+const inProgressRequests: WeakMap<Element, InProgressRequest> = new WeakMap();
37
+const resource: Resource<
38
+ Element,
39
+ Element,
40
+ StyleAndLayoutFrontend
41
+> = createResource(
42
+ (element: Element) => {
43
+ let request = inProgressRequests.get(element);
44
+ if (request != null) {
45
+ return request.promise;
46
+ }
47
+
48
+ let resolveFn = ((null: any): ResolveFn);
49
+ const promise = new Promise(resolve => {
50
+ resolveFn = resolve;
51
+ });
52
+
53
+ inProgressRequests.set(element, { promise, resolveFn });
54
+
55
+ return promise;
56
+ },
57
+ (element: Element) => element,
58
+ { useWeakMap: true }
59
+);
60
+
61
+type Props = {|
62
+ children: React$Node,
63
+|};
64
+
65
+function NativeStyleContextController({ children }: Props) {
66
+ const bridge = useContext(BridgeContext);
67
+ const store = useContext(StoreContext);
68
+
69
+ const getStyleAndLayout = useCallback<GetStyleAndLayout>(
70
+ (id: number) => {
71
+ const element = store.getElementByID(id);
72
+ if (element !== null) {
73
+ return resource.read(element);
74
+ } else {
75
+ return null;
76
+ }
77
+ },
78
+ [store]
79
+ );
80
+
81
+ // It's very important that this context consumes selectedElementID and not NativeStyleID.
82
+ // Otherwise the effect that sends the "inspect" message across the bridge-
83
+ // would itself be blocked by the same render that suspends (waiting for the data).
84
+ const { selectedElementID } = useContext(TreeStateContext);
85
+
86
+ const [
87
+ currentStyleAndLayout,
88
+ setCurrentStyleAndLayout,
89
+ ] = useState<StyleAndLayoutFrontend | null>(null);
90
+
91
+ // This effect handler invalidates the suspense cache and schedules rendering updates with React.
92
+ useEffect(() => {
93
+ const onStyleAndLayout = ({ id, layout, style }: StyleAndLayoutBackend) => {
94
+ let element = store.getElementByID(id);
95
+ if (element !== null) {
96
+ const styleAndLayout: StyleAndLayoutFrontend = {
97
+ layout,
98
+ style,
99
+ };
100
+ const request = inProgressRequests.get(element);
101
+ if (request != null) {
102
+ inProgressRequests.delete(element);
103
+ batchedUpdates(() => {
104
+ request.resolveFn(styleAndLayout);
105
+ setCurrentStyleAndLayout(styleAndLayout);
106
+ });
107
+ } else {
108
+ resource.write(element, styleAndLayout);
109
+
110
+ // Schedule update with React if the curently-selected element has been invalidated.
111
+ if (id === selectedElementID) {
112
+ setCurrentStyleAndLayout(styleAndLayout);
113
+ }
114
+ }
115
+ }
116
+ };
117
+
118
+ bridge.addListener('NativeStyleEditor_styleAndLayout', onStyleAndLayout);
119
+ return () =>
120
+ bridge.removeListener(
121
+ 'NativeStyleEditor_styleAndLayout',
122
+ onStyleAndLayout
123
+ );
124
+ }, [bridge, currentStyleAndLayout, selectedElementID, store]);
125
+
126
+ // This effect handler polls for updates on the currently selected element.
127
+ useEffect(() => {
128
+ if (selectedElementID === null) {
129
+ return () => {};
130
+ }
131
+
132
+ const rendererID = store.getRendererIDForElement(selectedElementID);
133
+
134
+ let timeoutID: TimeoutID | null = null;
135
+
136
+ const sendRequest = () => {
137
+ timeoutID = null;
138
+
139
+ bridge.send('NativeStyleEditor_measure', {
140
+ id: selectedElementID,
141
+ rendererID,
142
+ });
143
+ };
144
+
145
+ // Send the initial measurement request.
146
+ // We'll poll for an update in the response handler below.
147
+ sendRequest();
148
+
149
+ const onStyleAndLayout = ({ id }: StyleAndLayoutBackend) => {
150
+ // If this is the element we requested, wait a little bit and then ask for another update.
151
+ if (id === selectedElementID) {
152
+ if (timeoutID !== null) {
153
+ clearTimeout(timeoutID);
154
+ }
155
+ timeoutID = setTimeout(sendRequest, 1000);
156
+ }
157
+ };
158
+
159
+ bridge.addListener('NativeStyleEditor_styleAndLayout', onStyleAndLayout);
160
+
161
+ return () => {
162
+ bridge.removeListener(
163
+ 'NativeStyleEditor_styleAndLayout',
164
+ onStyleAndLayout
165
+ );
166
+
167
+ if (timeoutID !== null) {
168
+ clearTimeout(timeoutID);
169
+ }
170
+ };
171
+ }, [bridge, selectedElementID, store]);
172
+
173
+ const value = useMemo(
174
+ () => ({ getStyleAndLayout }),
175
+ // NativeStyle is used to invalidate the cache and schedule an update with React.
176
+ // eslint-disable-next-line react-hooks/exhaustive-deps
177
+ [currentStyleAndLayout, getStyleAndLayout]
178
+ );
179
+
180
+ return (
181
+ <NativeStyleContext.Provider value={value}>
182
+ {children}
183
+ </NativeStyleContext.Provider>
184
+ );
185
+}
186
+
187
+export { NativeStyleContext, NativeStyleContextController };
src/devtools/views/Components/NativeStyleEditor/index.js
new
+68
@@ -0,0 +1,68 @@
1
+// @flow
2
+
3
+import React, { Fragment, useContext, useMemo } from 'react';
4
+import Store from 'src/devtools/store';
5
+import { StoreContext } from 'src/devtools/views/context';
6
+import { useSubscription } from 'src/devtools/views/hooks';
7
+import { NativeStyleContext } from './context';
8
+import LayoutViewer from './LayoutViewer';
9
+import StyleEditor from './StyleEditor';
10
+import { TreeStateContext } from '../TreeContext';
11
+
12
+type Props = {||};
13
+
14
+export default function NativeStyleEditorWrapper(_: Props) {
15
+ const store = useContext(StoreContext);
16
+
17
+ const subscription = useMemo(
18
+ () => ({
19
+ getCurrentValue: () => store.supportsNativeStyleEditor,
20
+ subscribe: (callback: Function) => {
21
+ store.addListener('supportsNativeStyleEditor', callback);
22
+ return () => {
23
+ store.removeListener('supportsNativeStyleEditor', callback);
24
+ };
25
+ },
26
+ }),
27
+ [store]
28
+ );
29
+ const supportsNativeStyleEditor = useSubscription<boolean, Store>(
30
+ subscription
31
+ );
32
+
33
+ if (!supportsNativeStyleEditor) {
34
+ return null;
35
+ }
36
+
37
+ return <NativeStyleEditor />;
38
+}
39
+
40
+function NativeStyleEditor(_: Props) {
41
+ const { getStyleAndLayout } = useContext(NativeStyleContext);
42
+
43
+ const { inspectedElementID } = useContext(TreeStateContext);
44
+ if (inspectedElementID === null) {
45
+ return null;
46
+ }
47
+
48
+ const maybeStyleAndLayout = getStyleAndLayout(inspectedElementID);
49
+ if (maybeStyleAndLayout === null) {
50
+ return null;
51
+ }
52
+
53
+ const { layout, style } = maybeStyleAndLayout;
54
+
55
+ return (
56
+ <Fragment>
57
+ {layout !== null && (
58
+ <LayoutViewer id={inspectedElementID} layout={layout} />
59
+ )}
60
+ {style !== null && (
61
+ <StyleEditor
62
+ id={inspectedElementID}
63
+ style={style !== null ? style : {}}
64
+ />
65
+ )}
66
+ </Fragment>
67
+ );
68
+}
src/devtools/views/Components/NativeStyleEditor/types.js
new
+13
@@ -0,0 +1,13 @@
1
+// @flow
2
+
3
+import type {
4
+ Layout as LayoutBackend,
5
+ Style as StyleBackend,
6
+} from 'src/backend/NativeStyleEditor/types';
7
+
8
+export type Layout = LayoutBackend;
9
+export type Style = StyleBackend;
10
+export type StyleAndLayout = {|
11
+ layout: LayoutBackend | null,
12
+ style: StyleBackend | null,
13
+|};
src/devtools/views/Components/SelectedElement.css
-12
@@ -41,18 +41,6 @@
41
text-overflow: ellipsis;
42
max-width: 100%;
43
}
44
-.Component:before,
45
-.Owner:before {
46
- white-space: nowrap;
47
- content: '<';
48
- color: var(--color-jsx-arrow-brackets);
49
-}
50
-.Component:after,
51
-.Owner:after {
52
- white-space: nowrap;
53
- content: '>';
54
- color: var(--color-jsx-arrow-brackets);
55
-}
44
45
.Component {
46
flex: 1 1 auto;
src/devtools/views/Components/SelectedElement.js
+15
-6
@@ -12,6 +12,7 @@ import HocBadges from './HocBadges';
12
import InspectedElementTree from './InspectedElementTree';
13
import { InspectedElementContext } from './InspectedElementContext';
14
import ViewElementSourceContext from './ViewElementSourceContext';
15
+import NativeStyleEditor from './NativeStyleEditor';
16
import Toggle from '../Toggle';
17
import Badge from './Badge';
18
import {
@@ -34,7 +35,9 @@ export type Props = {||};
35
export default function SelectedElement(_: Props) {
36
const { inspectedElementID } = useContext(TreeStateContext);
37
const dispatch = useContext(TreeDispatcherContext);
37
- const viewElementSource = useContext(ViewElementSourceContext);
38
+ const { isFileLocationRequired, viewElementSourceFunction } = useContext(
39
+ ViewElementSourceContext
40
+ );
41
const bridge = useContext(BridgeContext);
42
const store = useContext(StoreContext);
43
const { dispatch: modalDialogDispatch } = useContext(ModalDialogContext);
@@ -55,7 +58,7 @@ export default function SelectedElement(_: Props) {
58
if (element !== null && inspectedElementID !== null) {
59
const rendererID = store.getRendererIDForElement(inspectedElementID);
60
if (rendererID !== null) {
58
- bridge.send('highlightElementInDOM', {
61
+ bridge.send('highlightNativeElement', {
62
displayName: element.displayName,
63
hideAfterTimeout: true,
64
id: inspectedElementID,
@@ -80,15 +83,19 @@ export default function SelectedElement(_: Props) {
83
}, [bridge, inspectedElementID, store]);
84
85
const viewSource = useCallback(() => {
83
- if (viewElementSource != null && inspectedElementID !== null) {
84
- viewElementSource(inspectedElementID);
86
+ if (viewElementSourceFunction != null && inspectedElement !== null) {
87
+ viewElementSourceFunction(
88
+ inspectedElement.id,
89
+ ((inspectedElement: any): InspectedElement)
90
+ );
91
}
86
- }, [inspectedElementID, viewElementSource]);
92
+ }, [inspectedElement, viewElementSourceFunction]);
93
94
const canViewSource =
95
inspectedElement &&
96
inspectedElement.canViewSource &&
91
- viewElementSource !== null;
97
+ viewElementSourceFunction !== null &&
98
+ (!isFileLocationRequired || inspectedElement.source !== null);
99
100
const isSuspended =
101
element !== null &&
@@ -344,6 +351,8 @@ function InspectedElementView({
351
/>
352
)}
353
354
+ <NativeStyleEditor />
355
+
356
{ownerID === null && owners !== null && owners.length > 0 && (
357
<div className={styles.Owners}>
358
<div className={styles.OwnersHeader}>rendered by</div>
src/devtools/views/Components/Tree.js
+18
-13
@@ -1,6 +1,7 @@
1
// @flow
2
3
import React, {
4
+ Fragment,
5
Suspense,
6
useState,
7
useCallback,
@@ -77,14 +78,14 @@ export default function Tree(props: Props) {
78
// Picking an element in the inspector should put focus into the tree.
79
// This ensures that keyboard navigation works right after picking a node.
80
useEffect(() => {
80
- function handleStopInspectingDOM(didSelectNode) {
81
+ function handleStopInspectingNative(didSelectNode) {
82
if (didSelectNode && focusTargetRef.current !== null) {
83
focusTargetRef.current.focus();
84
}
85
}
85
- bridge.addListener('stopInspectingDOM', handleStopInspectingDOM);
86
+ bridge.addListener('stopInspectingNative', handleStopInspectingNative);
87
return () =>
87
- bridge.removeListener('stopInspectingDOM', handleStopInspectingDOM);
88
+ bridge.removeListener('stopInspectingNative', handleStopInspectingNative);
89
}, [bridge]);
90
91
// This ref is passed down the context to elements.
@@ -187,12 +188,12 @@ export default function Tree(props: Props) {
188
[dispatch, selectedElementID]
189
);
190
190
- const highlightElementInDOM = useCallback(
191
+ const highlightNativeElement = useCallback(
192
(id: number) => {
193
const element = store.getElementByID(id);
194
const rendererID = store.getRendererIDForElement(id);
195
if (element !== null) {
195
- bridge.send('highlightElementInDOM', {
196
+ bridge.send('highlightNativeElement', {
197
displayName: element.displayName,
198
hideAfterTimeout: false,
199
id,
@@ -220,15 +221,15 @@ export default function Tree(props: Props) {
221
}
222
if (isNavigatingWithKeyboard || didSelectNewSearchResult) {
223
if (selectedElementID !== null) {
223
- highlightElementInDOM(selectedElementID);
224
+ highlightNativeElement(selectedElementID);
225
} else {
225
- bridge.send('clearHighlightedElementInDOM');
226
+ bridge.send('clearNativeElementHighlight');
227
}
228
}
229
}, [
230
bridge,
231
isNavigatingWithKeyboard,
231
- highlightElementInDOM,
232
+ highlightNativeElement,
233
searchIndex,
234
searchResults,
235
selectedElementID,
@@ -240,10 +241,10 @@ export default function Tree(props: Props) {
241
// Ignore hover while we're navigating with keyboard.
242
// This avoids flicker from the hovered nodes under the mouse.
243
if (!isNavigatingWithKeyboard) {
243
- highlightElementInDOM(id);
244
+ highlightNativeElement(id);
245
}
246
},
246
- [isNavigatingWithKeyboard, highlightElementInDOM]
247
+ [isNavigatingWithKeyboard, highlightNativeElement]
248
);
249
250
const handleMouseMove = useCallback(() => {
@@ -253,7 +254,7 @@ export default function Tree(props: Props) {
254
}, []);
255
256
const handleMouseLeave = useCallback(() => {
256
- bridge.send('clearHighlightedElementInDOM');
257
+ bridge.send('clearNativeElementHighlight');
258
}, [bridge]);
259
260
// Let react-window know to re-render any time the underlying tree data changes.
@@ -284,8 +285,12 @@ export default function Tree(props: Props) {
285
<TreeFocusedContext.Provider value={treeFocused}>
286
<div className={styles.Tree} ref={treeRef}>
287
<div className={styles.SearchInput}>
287
- <InspectHostNodesToggle />
288
- <div className={styles.VRule} />
288
+ {store.supportsNativeInspection && (
289
+ <Fragment>
290
+ <InspectHostNodesToggle />
291
+ <div className={styles.VRule} />
292
+ </Fragment>
293
+ )}
294
<Suspense fallback={<Loading />}>
295
{ownerID !== null ? <OwnersStack /> : <SearchInput />}
296
</Suspense>
src/devtools/views/Components/ViewElementSourceContext.js
+8
-1
@@ -2,7 +2,14 @@
2
3
import { createContext } from 'react';
4
5
-const ViewElementSourceContext = createContext<Function | null>(null);
5
+import type { ViewElementSource } from 'src/devtools/views/DevTools';
6
+
7
+export type Context = {|
8
+ isFileLocationRequired: boolean,
9
+ viewElementSourceFunction: ViewElementSource | null,
10
+|};
11
+
12
+const ViewElementSourceContext = createContext<Context>(((null: any): Context));
13
ViewElementSourceContext.displayName = 'ViewElementSourceContext';
14
15
export default ViewElementSourceContext;
src/devtools/views/DevTools.css
+17
-1
@@ -16,6 +16,10 @@
16
border-top: 1px solid var(--color-border);
17
font-family: var(--font-family-sans);
18
font-size: var(--font-size-sans-large);
19
+ user-select: none;
20
+
21
+ /* Electron drag area */
22
+ -webkit-app-region: drag;
23
}
24
25
.Spacer {
@@ -27,12 +31,24 @@
31
overflow: auto;
32
}
33
34
+.DevToolsVersion {
35
+ font-size: var(--font-size-sans-normal);
36
+ margin-right: 0.5rem;
37
+}
38
+
39
.DevToolsVersion:before {
40
+ font-size: var(--font-size-sans-large);
41
content: 'DevTools ';
42
}
43
34
-@media screen and (max-width: 350px) {
44
+@media screen and (max-width: 400px) {
45
.DevToolsVersion:before {
46
content: '';
47
}
48
}
49
+
50
+@media screen and (max-width: 300px) {
51
+ .DevToolsVersion {
52
+ display: none;
53
+ }
54
+}
src/devtools/views/DevTools.js
+20
-7
@@ -5,7 +5,7 @@
5
import '@reach/menu-button/styles.css';
6
import '@reach/tooltip/styles.css';
7
8
-import React, { useState } from 'react';
8
+import React, { useMemo, useState } from 'react';
9
import Bridge from 'src/bridge';
10
import Store from '../store';
11
import { BridgeContext, StoreContext } from './context';
@@ -23,18 +23,23 @@ import styles from './DevTools.css';
23
24
import './root.css';
25
26
-export type BrowserName = 'Chrome' | 'Firefox';
26
+import type { InspectedElement } from 'src/devtools/views/Components/types';
27
+
28
export type BrowserTheme = 'dark' | 'light';
29
export type TabID = 'components' | 'profiler' | 'settings';
30
+export type ViewElementSource = (
31
+ id: number,
32
+ inspectedElement: InspectedElement
33
+) => void;
34
35
export type Props = {|
36
bridge: Bridge,
32
- browserName: BrowserName,
33
- browserTheme: BrowserTheme,
37
+ browserTheme?: BrowserTheme,
38
defaultTab?: TabID,
39
showTabBar?: boolean,
40
store: Store,
37
- viewElementSource?: ?Function,
41
+ viewElementSourceFunction?: ?ViewElementSource,
42
+ viewElementSourceRequiresFileLocation?: boolean,
43
44
// This property is used only by the web extension target.
45
// The built-in tab UI is hidden in that case, in favor of the browser's own panel tabs.
@@ -67,7 +72,6 @@ const tabs = [componentsTab, profilerTab];
72
73
export default function DevTools({
74
bridge,
70
- browserName,
75
browserTheme = 'light',
76
defaultTab = 'components',
77
componentsPortalContainer,
@@ -76,13 +80,22 @@ export default function DevTools({
80
settingsPortalContainer,
81
showTabBar = false,
82
store,
79
- viewElementSource = null,
83
+ viewElementSourceFunction,
84
+ viewElementSourceRequiresFileLocation = false,
85
}: Props) {
86
const [tab, setTab] = useState(defaultTab);
87
if (overrideTab != null && overrideTab !== tab) {
88
setTab(overrideTab);
89
}
90
91
+ const viewElementSource = useMemo(
92
+ () => ({
93
+ isFileLocationRequired: viewElementSourceRequiresFileLocation,
94
+ viewElementSourceFunction: viewElementSourceFunction || null,
95
+ }),
96
+ [viewElementSourceFunction, viewElementSourceRequiresFileLocation]
97
+ );
98
+
99
return (
100
<BridgeContext.Provider value={bridge}>
101
<StoreContext.Provider value={store}>
src/devtools/views/Profiler/CommitTreeBuilder.js
+1
-1
@@ -139,7 +139,7 @@ function recursivelyInitializeTree(
139
140
function updateTree(
141
commitTree: CommitTree,
142
- operations: Uint32Array
142
+ operations: Array<number>
143
): CommitTree {
144
// Clone the original tree so edits don't affect it.
145
const nodes = new Map(commitTree.nodes);
src/devtools/views/Profiler/Profiler.css
+5
@@ -42,6 +42,11 @@
42
flex-direction: column;
43
align-items: center;
44
justify-content: center;
45
+ padding: 0 1rem;
46
+}
47
+
48
+.Paragraph {
49
+ text-align: center;
50
}
51
52
.Row {
src/devtools/views/Profiler/Profiler.js
+16
-18
@@ -150,24 +150,22 @@ const NoProfilingData = () => (
150
const ProfilingNotSupported = () => (
151
<div className={styles.Column}>
152
<div className={styles.Header}>Profiling not supported.</div>
153
- <div className={styles.Column}>
154
- <p>
155
- Profiling support requires either a development or production-profiling
156
- build of React v16.5+.
157
- </p>
158
- <p>
159
- Learn more at{' '}
160
- <a
161
- className={styles.Link}
162
- href="https://fb.me/react-profiling"
163
- rel="noopener noreferrer"
164
- target="_blank"
165
- >
166
- fb.me/react-profiling
167
- </a>
168
- .
169
- </p>
170
- </div>
153
+ <p className={styles.Paragraph}>
154
+ Profiling support requires either a development or production-profiling
155
+ build of React v16.5+.
156
+ </p>
157
+ <p className={styles.Paragraph}>
158
+ Learn more at{' '}
159
+ <a
160
+ className={styles.Link}
161
+ href="https://fb.me/react-profiling"
162
+ rel="noopener noreferrer"
163
+ target="_blank"
164
+ >
165
+ fb.me/react-profiling
166
+ </a>
167
+ .
168
+ </p>
169
</div>
170
);
171
src/devtools/views/Profiler/SidebarSelectedFiberInfo.css
+2
-10
@@ -20,20 +20,12 @@
20
.Component {
21
flex: 1;
22
color: var(--color-component-name);
23
+ font-family: var(--font-family-monospace);
24
+ font-size: var(--font-size-monospace-normal);
25
white-space: nowrap;
26
overflow-x: hidden;
27
text-overflow: ellipsis;
28
}
27
-.Component:before {
28
- white-space: nowrap;
29
- content: '<';
30
- color: var(--color-jsx-arrow-brackets);
31
-}
32
-.Component:after {
33
- white-space: nowrap;
34
- content: '>';
35
- color: var(--color-jsx-arrow-brackets);
36
-}
29
30
.Label {
31
font-weight: bold;
src/devtools/views/Profiler/types.js
+1
-1
@@ -87,7 +87,7 @@ export type ProfilingDataForRootFrontend = {|
87
88
// List of tree mutation that occur during profiling.
89
// These mutations can be used along with initial snapshots to reconstruct the tree for any commit.
90
- operations: Array<Uint32Array>,
90
+ operations: Array<Array<number>>,
91
92
// Identifies the root this profiler data corresponds to.
93
rootID: number,
src/devtools/views/Profiler/utils.js
+3
-3
@@ -28,7 +28,7 @@ const commitGradient = [
28
// This format can then be quickly exported (and re-imported).
29
export function prepareProfilingDataFrontendFromBackendAndStore(
30
dataBackends: Array<ProfilingDataBackend>,
31
- operationsByRootID: Map<number, Array<Uint32Array>>,
31
+ operationsByRootID: Map<number, Array<Array<number>>>,
32
screenshotsByRootID: Map<number, Map<number, string>>,
33
snapshotsByRootID: Map<number, Map<number, SnapshotNode>>
34
): ProfilingDataFrontend {
@@ -137,7 +137,7 @@ export function prepareProfilingDataFrontendFromExport(
137
initialTreeBaseDurations: new Map(initialTreeBaseDurations),
138
interactionCommits: new Map(interactionCommits),
139
interactions: new Map(interactions),
140
- operations: operations.map(array => Uint32Array.from(array)), // Convert Array back to Uint32Array
140
+ operations,
141
rootID,
142
snapshots: new Map(snapshots),
143
});
@@ -194,7 +194,7 @@ export function prepareProfilingDataExport(
194
),
195
interactionCommits: Array.from(interactionCommits.entries()),
196
interactions: Array.from(interactions.entries()),
197
- operations: operations.map(array => Array.from(array)), // Convert Uint32Array to Array for serialization
197
+ operations,
198
rootID,
199
snapshots: Array.from(snapshots.entries()),
200
});
src/devtools/views/Settings/SettingsContext.js
+2
-6
@@ -213,6 +213,7 @@ function updateThemeVariables(
213
updateStyleHelper(theme, 'color-background', documentElements);
214
updateStyleHelper(theme, 'color-background-hover', documentElements);
215
updateStyleHelper(theme, 'color-background-inactive', documentElements);
216
+ updateStyleHelper(theme, 'color-background-invalid', documentElements);
217
updateStyleHelper(theme, 'color-background-selected', documentElements);
218
updateStyleHelper(theme, 'color-border', documentElements);
219
updateStyleHelper(theme, 'color-button-background', documentElements);
@@ -275,12 +276,6 @@ function updateThemeVariables(
276
updateStyleHelper(theme, 'color-dimmer', documentElements);
277
updateStyleHelper(theme, 'color-dimmest', documentElements);
278
updateStyleHelper(theme, 'color-expand-collapse-toggle', documentElements);
278
- updateStyleHelper(theme, 'color-jsx-arrow-brackets', documentElements);
279
- updateStyleHelper(
280
- theme,
281
- 'color-jsx-arrow-brackets-inverted',
282
- documentElements
283
- );
279
updateStyleHelper(theme, 'color-modal-background', documentElements);
280
updateStyleHelper(theme, 'color-record-active', documentElements);
281
updateStyleHelper(theme, 'color-record-hover', documentElements);
@@ -301,6 +296,7 @@ function updateThemeVariables(
296
);
297
updateStyleHelper(theme, 'color-tab-selected-border', documentElements);
298
updateStyleHelper(theme, 'color-text', documentElements);
299
+ updateStyleHelper(theme, 'color-text-invalid', documentElements);
300
updateStyleHelper(theme, 'color-text-selected', documentElements);
301
updateStyleHelper(theme, 'color-toggle-background-invalid', documentElements);
302
updateStyleHelper(theme, 'color-toggle-background-on', documentElements);
src/devtools/views/TabBar.css
+3
@@ -9,6 +9,9 @@
9
border-bottom: 3px solid transparent;
10
user-select: none;
11
color: var(--color-text);
12
+
13
+ /* Electron drag area */
14
+ -webkit-app-region: no-drag;
15
}
16
.Tab:hover,
17
.TabCurrent:hover {
src/devtools/views/root.css
+4
-4
@@ -12,6 +12,7 @@
12
--light-color-background: #ffffff;
13
--light-color-background-hover: rgba(0, 136, 250, 0.1);
14
--light-color-background-inactive: #e5e5e5;
15
+ --light-color-background-invalid: #fff0f0;
16
--light-color-background-selected: #0088fa;
17
--light-color-button-background: #ffffff;
18
--light-color-button-background-focus: #ededed;
@@ -46,8 +47,6 @@
47
--light-color-dimmer: #cfd1d5;
48
--light-color-dimmest: #eff0f1;
49
--light-color-expand-collapse-toggle: #777d88;
49
- --light-color-jsx-arrow-brackets: #333333;
50
- --light-color-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
50
--light-color-modal-background: rgba(255, 255, 255, 0.75);
51
--light-color-record-active: #fc3a4b;
52
--light-color-record-hover: #3578e5;
@@ -60,6 +59,7 @@
59
--light-color-selected-tree-highlight-inactive: rgba(0, 0, 0, 0.05);
60
--light-color-tab-selected-border: #0088fa;
61
--light-color-text: #000000;
62
+ --light-color-text-invalid: #ff0000;
63
--light-color-text-selected: #ffffff;
64
--light-color-toggle-background-invalid: #fc3a4b;
65
--light-color-toggle-background-on: #0088fa;
@@ -77,6 +77,7 @@
77
--dark-color-background: #282c34;
78
--dark-color-background-hover: rgba(255, 255, 255, 0.1);
79
--dark-color-background-inactive: #3d424a;
80
+ --dark-color-background-invalid: #5c0000;
81
--dark-color-background-selected: #178fb9;
82
--dark-color-button-background: #282c34;
83
--dark-color-button-background-focus: #3d424a;
@@ -111,8 +112,6 @@
112
--dark-color-dimmer: #777d88;
113
--dark-color-dimmest: #4f5766;
114
--dark-color-expand-collapse-toggle: #8f949d;
114
- --dark-color-jsx-arrow-brackets: #777d88;
115
- --dark-color-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
115
--dark-color-modal-background: rgba(0, 0, 0, 0.75);
116
--dark-color-record-active: #fc3a4b;
117
--dark-color-record-hover: #a2e9fc;
@@ -125,6 +124,7 @@
124
--dark-color-selected-tree-highlight-inactive: rgba(255, 255, 255, 0.05);
125
--dark-color-tab-selected-border: #178fb9;
126
--dark-color-text: #ffffff;
127
+ --dark-color-text-invalid: #ff8080;
128
--dark-color-text-selected: #ffffff;
129
--dark-color-toggle-background-invalid: #fc3a4b;
130
--dark-color-toggle-background-on: #178fb9;
src/hydration.js
+93
-43
@@ -43,45 +43,79 @@ type Dehydrated = {|
43
// but may decrease the responsiveness of expanding objects/arrays to inspect further.
44
const LEVEL_THRESHOLD = 2;
45
46
+type PropType =
47
+ | 'array'
48
+ | 'array_buffer'
49
+ | 'boolean'
50
+ | 'data_view'
51
+ | 'date'
52
+ | 'function'
53
+ | 'html_element'
54
+ | 'infinity'
55
+ | 'iterator'
56
+ | 'nan'
57
+ | 'null'
58
+ | 'number'
59
+ | 'object'
60
+ | 'react_element'
61
+ | 'string'
62
+ | 'symbol'
63
+ | 'typed_array'
64
+ | 'undefined'
65
+ | 'unknown';
66
+
67
/**
68
* Get a enhanced/artificial type string based on the object instance
69
*/
49
-function getPropType(data: Object): string | null {
50
- if (!data) {
51
- return null;
70
+function getDataType(data: Object): PropType {
71
+ if (data === null) {
72
+ return 'null';
73
+ } else if (data === undefined) {
74
+ return 'undefined';
75
}
76
77
if (isElement(data)) {
78
return 'react_element';
79
}
80
58
- if (data instanceof HTMLElement) {
81
+ if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) {
82
return 'html_element';
83
}
84
85
const type = typeof data;
63
- if (type === 'object') {
64
- if (Array.isArray(data)) {
65
- return 'array';
66
- }
67
- if (ArrayBuffer.isView(data)) {
68
- if (data instanceof DataView) {
69
- return 'data_view';
86
+ switch (type) {
87
+ case 'boolean':
88
+ return 'boolean';
89
+ case 'function':
90
+ return 'function';
91
+ case 'number':
92
+ if (Number.isNaN(data)) {
93
+ return 'nan';
94
+ } else if (!Number.isFinite(data)) {
95
+ return 'infinity';
96
+ } else {
97
+ return 'number';
98
}
71
- return 'typed_array';
72
- }
73
- if (data instanceof ArrayBuffer) {
74
- return 'array_buffer';
75
- }
76
- if (typeof data[Symbol.iterator] === 'function') {
77
- return 'iterator';
78
- }
79
- if (Object.prototype.toString.call(data) === '[object Date]') {
80
- return 'date';
81
- }
99
+ case 'object':
100
+ if (Array.isArray(data)) {
101
+ return 'array';
102
+ } else if (ArrayBuffer.isView(data)) {
103
+ return data instanceof DataView ? 'data_view' : 'typed_array';
104
+ } else if (data instanceof ArrayBuffer) {
105
+ return 'array_buffer';
106
+ } else if (typeof data[Symbol.iterator] === 'function') {
107
+ return 'iterator';
108
+ } else if (Object.prototype.toString.call(data) === '[object Date]') {
109
+ return 'date';
110
+ }
111
+ return 'object';
112
+ case 'string':
113
+ return 'string';
114
+ case 'symbol':
115
+ return 'symbol';
116
+ default:
117
+ return 'unknown';
118
}
83
-
84
- return type;
119
}
120
121
/**
@@ -143,7 +177,7 @@ export function dehydrate(
177
isPathWhitelisted: (path: Array<string | number>) => boolean,
178
level?: number = 0
179
): string | Dehydrated | { [key: string]: string | Dehydrated } {
146
- const type = getPropType(data);
180
+ const type = getDataType(data);
181
182
switch (type) {
183
case 'html_element':
@@ -151,7 +185,7 @@ export function dehydrate(
185
return {
186
inspectable: false,
187
name: data.tagName,
154
- type: 'html_element',
188
+ type,
189
};
190
191
case 'function':
@@ -159,20 +193,18 @@ export function dehydrate(
193
return {
194
inspectable: false,
195
name: data.name,
162
- type: 'function',
196
+ type,
197
};
198
199
case 'string':
200
return data.length <= 500 ? data : data.slice(0, 500) + '...';
201
168
- // We have to do this assignment b/c Flow doesn't think "symbol" is
169
- // something typeof would return. Error 'unexpected predicate "symbol"'
202
case 'symbol':
203
cleaned.push(path);
204
return {
205
inspectable: false,
206
name: data.toString(),
175
- type: 'symbol',
207
+ type,
208
};
209
210
// React Elements aren't very inspector-friendly,
@@ -182,7 +214,7 @@ export function dehydrate(
214
return {
215
inspectable: false,
216
name: getDisplayNameForReactElement(data),
185
- type: 'react_element',
217
+ type,
218
};
219
220
// ArrayBuffers error if you try to inspect them.
@@ -220,7 +252,7 @@ export function dehydrate(
252
return {
253
inspectable: false,
254
name: data.toString(),
223
- type: 'date',
255
+ type,
256
};
257
258
case 'object':
@@ -241,6 +273,16 @@ export function dehydrate(
273
return object;
274
}
275
276
+ case 'infinity':
277
+ case 'nan':
278
+ case 'undefined':
279
+ // Some values are lossy when sent through a WebSocket.
280
+ // We dehydrate+rehydrate them to preserve their type.
281
+ cleaned.push(path);
282
+ return {
283
+ type,
284
+ };
285
+
286
default:
287
return data;
288
}
@@ -271,22 +313,30 @@ export function hydrate(
313
const length = path.length;
314
const last = path[length - 1];
315
const parent = getInObject(object, path.slice(0, length - 1));
274
- if (!parent || !parent[last]) {
316
+ if (!parent || !parent.hasOwnProperty(last)) {
317
return;
318
}
319
320
const value = parent[last];
321
280
- // Replace the string keys with Symbols so they're non-enumerable.
281
- const replaced: { [key: Symbol]: boolean | string } = {};
282
- replaced[meta.inspectable] = !!value.inspectable;
283
- replaced[meta.inspected] = false;
284
- replaced[meta.name] = value.name;
285
- replaced[meta.size] = value.size;
286
- replaced[meta.readonly] = !!value.readonly;
287
- replaced[meta.type] = value.type;
288
-
289
- parent[last] = replaced;
322
+ if (value.type === 'infinity') {
323
+ parent[last] = Infinity;
324
+ } else if (value.type === 'nan') {
325
+ parent[last] = NaN;
326
+ } else if (value.type === 'undefined') {
327
+ parent[last] = undefined;
328
+ } else {
329
+ // Replace the string keys with Symbols so they're non-enumerable.
330
+ const replaced: { [key: Symbol]: boolean | string } = {};
331
+ replaced[meta.inspectable] = !!value.inspectable;
332
+ replaced[meta.inspected] = false;
333
+ replaced[meta.name] = value.name;
334
+ replaced[meta.size] = value.size;
335
+ replaced[meta.readonly] = !!value.readonly;
336
+ replaced[meta.type] = value.type;
337
+
338
+ parent[last] = replaced;
339
+ }
340
});
341
return object;
342
}
src/utils.js
+7
-9
@@ -58,27 +58,25 @@ export function getUID(): number {
58
return ++uidCounter;
59
}
60
61
-export function utfDecodeString(array: Uint32Array): string {
61
+export function utfDecodeString(array: Array<number>): string {
62
return String.fromCodePoint(...array);
63
}
64
65
-export function utfEncodeString(string: string): Uint32Array {
65
+export function utfEncodeString(string: string): Array<number> {
66
let cached = encodedStringCache.get(string);
67
if (cached !== undefined) {
68
return cached;
69
}
70
71
- // $FlowFixMe Flow's Uint32Array.from's type definition is wrong; first argument of mapFn will be string
72
- const encoded = Uint32Array.from(string, toCodePoint);
71
+ const encoded = new Array(string.length);
72
+ for (let i = 0; i < string.length; i++) {
73
+ encoded[i] = string.codePointAt(i);
74
+ }
75
encodedStringCache.set(string, encoded);
76
return encoded;
77
}
78
77
-function toCodePoint(string: string) {
78
- return string.codePointAt(0);
79
-}
80
-
81
-export function printOperationsArray(operations: Uint32Array) {
79
+export function printOperationsArray(operations: Array<number>) {
80
// The first two values are always rendererID and rootID
81
const rendererID = operations[0];
82
const rootID = operations[1];
yarn.lock
+202
-40
@@ -1165,6 +1165,11 @@
1165
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.13.4.tgz#f83ec3c3e05b174b7241fadeb6688267fe5b22ca"
1166
integrity sha512-+rabAZZ3Yn7tF/XPGHupKIL5EcAbrLxnTr/hgQICxbeuAfWtT0UZSfULE+ndusckBItcv4o6ZeOJplQikVcLvQ==
1167
1168
+"@types/node@^10.12.18":
1169
+ version "10.14.9"
1170
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.9.tgz#2e8d678039d27943ce53a1913386133227fd9066"
1171
+ integrity sha512-NelG/dSahlXYtSoVPErrp06tYFrvzj8XLWmKA+X8x0W//4MqbUyZu++giUG/v0bjAT6/Qxa8IjodrfdACyb0Fg==
1172
+
1173
"@types/stack-utils@^1.0.1":
1174
version "1.0.1"
1175
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@@ -1885,7 +1890,7 @@ async-each@^1.0.1:
1890
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
1891
integrity sha1-GdOGodntxufByF04iu28xW0zYC0=
1892
1888
-async-limiter@~1.0.0:
1893
+async-limiter@^1.0.0, async-limiter@~1.0.0:
1894
version "1.0.0"
1895
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
1896
@@ -3297,7 +3302,7 @@ concat-map@0.0.1:
3302
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
3303
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
3304
3300
-concat-stream@^1.4.10, concat-stream@^1.5.0, concat-stream@^1.5.2, concat-stream@^1.6.0:
3305
+concat-stream@1.6.2, concat-stream@^1.4.10, concat-stream@^1.5.0, concat-stream@^1.5.2, concat-stream@^1.6.0:
3306
version "1.6.2"
3307
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
3308
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
@@ -3669,6 +3674,14 @@ create-react-class@^15.6.0:
3674
loose-envify "^1.3.1"
3675
object-assign "^4.1.1"
3676
3677
+cross-env@^3.1.4:
3678
+ version "3.2.4"
3679
+ resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-3.2.4.tgz#9e0585f277864ed421ce756f81a980ff0d698aba"
3680
+ integrity sha1-ngWF8neGTtQhznVvgamA/w1piro=
3681
+ dependencies:
3682
+ cross-spawn "^5.1.0"
3683
+ is-windows "^1.0.0"
3684
+
3685
cross-env@^5.2.0:
3686
version "5.2.0"
3687
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2"
@@ -3901,7 +3914,7 @@ debounce@1.1.0:
3914
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.1.0.tgz#6a1a4ee2a9dc4b7c24bb012558dbcdb05b37f408"
3915
integrity sha512-ZQVKfRVlwRfD150ndzEK8M90ABT+Y/JQKs4Y7U4MXdpuoUkkrr4DwKbVux3YjylA5bUMUj0Nc3pMxPJX6N2QQQ==
3916
3904
-debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9, debug@~2.6.3:
3917
+debug@2, debug@2.6.9, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9, debug@~2.6.3:
3918
version "2.6.9"
3919
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
3920
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@@ -3914,19 +3927,19 @@ debug@^2.1.1:
3927
dependencies:
3928
ms "0.7.2"
3929
3917
-debug@^3.1.0:
3918
- version "3.1.0"
3919
- resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
3920
- dependencies:
3921
- ms "2.0.0"
3922
-
3923
-debug@^3.2.5, debug@^3.2.6:
3930
+debug@^3.0.0, debug@^3.2.5, debug@^3.2.6:
3931
version "3.2.6"
3932
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
3933
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
3934
dependencies:
3935
ms "^2.1.1"
3936
3937
+debug@^3.1.0:
3938
+ version "3.1.0"
3939
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
3940
+ dependencies:
3941
+ ms "2.0.0"
3942
+
3943
debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
3944
version "4.1.1"
3945
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
@@ -4304,11 +4317,35 @@ ee-first@1.1.1:
4317
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
4318
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
4319
4320
+electron-download@^4.1.0:
4321
+ version "4.1.1"
4322
+ resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8"
4323
+ integrity sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg==
4324
+ dependencies:
4325
+ debug "^3.0.0"
4326
+ env-paths "^1.0.0"
4327
+ fs-extra "^4.0.1"
4328
+ minimist "^1.2.0"
4329
+ nugget "^2.0.1"
4330
+ path-exists "^3.0.0"
4331
+ rc "^1.2.1"
4332
+ semver "^5.4.1"
4333
+ sumchecker "^2.0.2"
4334
+
4335
electron-to-chromium@^1.3.113:
4336
version "1.3.113"
4337
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9"
4338
integrity sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g==
4339
4340
+electron@^5.0.0:
4341
+ version "5.0.4"
4342
+ resolved "https://registry.yarnpkg.com/electron/-/electron-5.0.4.tgz#2e0d09055363e983f4a73317cde4821c39617b02"
4343
+ integrity sha512-7QaKorvANvP+azMT7wElx33oLlqw8QxmLs7/outfH7LC5amErk4EUtWDesQ6Zgr+s5pYFbykl8ZtJ4ZGXER05g==
4344
+ dependencies:
4345
+ "@types/node" "^10.12.18"
4346
+ electron-download "^4.1.0"
4347
+ extract-zip "^1.0.3"
4348
+
4349
elegant-spinner@^1.0.1:
4350
version "1.0.1"
4351
resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
@@ -4382,6 +4419,11 @@ entities@^1.1.1, entities@~1.1.1:
4419
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
4420
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
4421
4422
+env-paths@^1.0.0:
4423
+ version "1.0.0"
4424
+ resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0"
4425
+ integrity sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=
4426
+
4427
errno@^0.1.3, errno@~0.1.7:
4428
version "0.1.7"
4429
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
@@ -5171,6 +5213,16 @@ extglob@^2.0.4:
5213
snapdragon "^0.8.1"
5214
to-regex "^3.0.1"
5215
5216
+extract-zip@^1.0.3:
5217
+ version "1.6.7"
5218
+ resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9"
5219
+ integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=
5220
+ dependencies:
5221
+ concat-stream "1.6.2"
5222
+ debug "2.6.9"
5223
+ mkdirp "0.5.1"
5224
+ yauzl "2.4.1"
5225
+
5226
extsprintf@1.3.0:
5227
version "1.3.0"
5228
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@@ -5289,6 +5341,13 @@ fbjs@^0.8.9:
5341
setimmediate "^1.0.5"
5342
ua-parser-js "^0.7.18"
5343
5344
+fd-slicer@~1.0.1:
5345
+ version "1.0.1"
5346
+ resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65"
5347
+ integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=
5348
+ dependencies:
5349
+ pend "~1.2.0"
5350
+
5351
fd-slicer@~1.1.0:
5352
version "1.1.0"
5353
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
@@ -6507,7 +6566,7 @@ ip-regex@^2.1.0:
6566
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
6567
integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
6568
6510
-ip@^1.1.0, ip@^1.1.5:
6569
+ip@^1.1.0, ip@^1.1.4, ip@^1.1.5:
6570
version "1.1.5"
6571
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
6572
integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
@@ -8207,7 +8266,7 @@ memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1:
8266
errno "^0.1.3"
8267
readable-stream "^2.0.1"
8268
8210
-meow@^3.3.0:
8269
+meow@^3.1.0, meow@^3.3.0:
8270
version "3.7.0"
8271
resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
8272
integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=
@@ -8851,6 +8910,19 @@ nth-check@~1.0.1:
8910
dependencies:
8911
boolbase "~1.0.0"
8912
8913
+nugget@^2.0.1:
8914
+ version "2.0.1"
8915
+ resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.1.tgz#201095a487e1ad36081b3432fa3cada4f8d071b0"
8916
+ integrity sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=
8917
+ dependencies:
8918
+ debug "^2.1.3"
8919
+ minimist "^1.1.0"
8920
+ pretty-bytes "^1.0.2"
8921
+ progress-stream "^1.1.0"
8922
+ request "^2.45.0"
8923
+ single-line-log "^1.1.2"
8924
+ throttleit "0.0.2"
8925
+
8926
nullthrows@^1.0.0:
8927
version "1.1.1"
8928
resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
@@ -8901,6 +8973,11 @@ object-keys@^1.0.12:
8973
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2"
8974
integrity sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==
8975
8976
+object-keys@~0.4.0:
8977
+ version "0.4.0"
8978
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
8979
+ integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=
8980
+
8981
object-visit@^1.0.0:
8982
version "1.0.1"
8983
resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
@@ -9545,6 +9622,14 @@ prettier@^1.16.4:
9622
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717"
9623
integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==
9624
9625
+pretty-bytes@^1.0.2:
9626
+ version "1.0.4"
9627
+ resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84"
9628
+ integrity sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=
9629
+ dependencies:
9630
+ get-stdin "^4.0.1"
9631
+ meow "^3.1.0"
9632
+
9633
pretty-format@^23.6.0:
9634
version "23.6.0"
9635
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760"
@@ -9590,6 +9675,14 @@ process@^0.11.0, process@^0.11.10, process@~0.11.0:
9675
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
9676
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
9677
9678
+progress-stream@^1.1.0:
9679
+ version "1.2.0"
9680
+ resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77"
9681
+ integrity sha1-LNPP6jO6OonJwSHsM0er6asSX3c=
9682
+ dependencies:
9683
+ speedometer "~0.1.2"
9684
+ through2 "~0.2.3"
9685
+
9686
progress@^1.1.8:
9687
version "1.1.8"
9688
resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
@@ -9808,7 +9901,7 @@ raw-body@2.3.3:
9901
iconv-lite "0.4.23"
9902
unpipe "1.0.0"
9903
9811
-rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
9904
+rc@^1.0.1, rc@^1.1.6, rc@^1.2.1, rc@^1.2.7:
9905
version "1.2.8"
9906
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
9907
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
@@ -10280,6 +10373,32 @@ request@2.87.0:
10373
tunnel-agent "^0.6.0"
10374
uuid "^3.1.0"
10375
10376
+request@^2.45.0, request@~2.88.0:
10377
+ version "2.88.0"
10378
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
10379
+ integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
10380
+ dependencies:
10381
+ aws-sign2 "~0.7.0"
10382
+ aws4 "^1.8.0"
10383
+ caseless "~0.12.0"
10384
+ combined-stream "~1.0.6"
10385
+ extend "~3.0.2"
10386
+ forever-agent "~0.6.1"
10387
+ form-data "~2.3.2"
10388
+ har-validator "~5.1.0"
10389
+ http-signature "~1.2.0"
10390
+ is-typedarray "~1.0.0"
10391
+ isstream "~0.1.2"
10392
+ json-stringify-safe "~5.0.1"
10393
+ mime-types "~2.1.19"
10394
+ oauth-sign "~0.9.0"
10395
+ performance-now "^2.1.0"
10396
+ qs "~6.5.2"
10397
+ safe-buffer "^5.1.2"
10398
+ tough-cookie "~2.4.3"
10399
+ tunnel-agent "^0.6.0"
10400
+ uuid "^3.3.2"
10401
+
10402
request@^2.83.0:
10403
version "2.83.0"
10404
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
@@ -10307,32 +10426,6 @@ request@^2.83.0:
10426
tunnel-agent "^0.6.0"
10427
uuid "^3.1.0"
10428
10310
-request@~2.88.0:
10311
- version "2.88.0"
10312
- resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
10313
- integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
10314
- dependencies:
10315
- aws-sign2 "~0.7.0"
10316
- aws4 "^1.8.0"
10317
- caseless "~0.12.0"
10318
- combined-stream "~1.0.6"
10319
- extend "~3.0.2"
10320
- forever-agent "~0.6.1"
10321
- form-data "~2.3.2"
10322
- har-validator "~5.1.0"
10323
- http-signature "~1.2.0"
10324
- is-typedarray "~1.0.0"
10325
- isstream "~0.1.2"
10326
- json-stringify-safe "~5.0.1"
10327
- mime-types "~2.1.19"
10328
- oauth-sign "~0.9.0"
10329
- performance-now "^2.1.0"
10330
- qs "~6.5.2"
10331
- safe-buffer "^5.1.2"
10332
- tough-cookie "~2.4.3"
10333
- tunnel-agent "^0.6.0"
10334
- uuid "^3.3.2"
10335
-
10429
require-directory@^2.1.1:
10430
version "2.1.1"
10431
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -10766,7 +10859,7 @@ shebang-regex@^1.0.0:
10859
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
10860
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
10861
10769
-shell-quote@1.6.1:
10862
+shell-quote@1.6.1, shell-quote@^1.6.1:
10863
version "1.6.1"
10864
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767"
10865
integrity sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=
@@ -10819,6 +10912,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
10912
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
10913
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
10914
10915
+single-line-log@^1.1.2:
10916
+ version "1.1.2"
10917
+ resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364"
10918
+ integrity sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=
10919
+ dependencies:
10920
+ string-width "^1.0.1"
10921
+
10922
sisteransi@^1.0.0:
10923
version "1.0.0"
10924
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.0.tgz#77d9622ff909080f1c19e5f4a1df0c1b0a27b88c"
@@ -11062,6 +11162,11 @@ spdy@^4.0.0:
11162
select-hose "^2.0.0"
11163
spdy-transport "^3.0.0"
11164
11165
+speedometer@~0.1.2:
11166
+ version "0.1.4"
11167
+ resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d"
11168
+ integrity sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=
11169
+
11170
split-string@^3.0.1, split-string@^3.0.2:
11171
version "3.1.0"
11172
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@@ -11396,6 +11501,13 @@ style-loader@^0.23.1:
11501
loader-utils "^1.1.0"
11502
schema-utils "^1.0.0"
11503
11504
+sumchecker@^2.0.2:
11505
+ version "2.0.2"
11506
+ resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-2.0.2.tgz#0f42c10e5d05da5d42eea3e56c3399a37d6c5b3e"
11507
+ integrity sha1-D0LBDl0F2l1C7qPlbDOZo31sWz4=
11508
+ dependencies:
11509
+ debug "^2.2.0"
11510
+
11511
supports-color@^2.0.0:
11512
version "2.0.0"
11513
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -11588,6 +11700,11 @@ throat@^4.0.0:
11700
version "4.1.0"
11701
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
11702
11703
+throttleit@0.0.2:
11704
+ version "0.0.2"
11705
+ resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf"
11706
+ integrity sha1-z+34jmDADdlpe2H90qg0OptoDq8=
11707
+
11708
through2@^2.0.0, through2@^2.0.2:
11709
version "2.0.5"
11710
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
@@ -11596,6 +11713,14 @@ through2@^2.0.0, through2@^2.0.2:
11713
readable-stream "~2.3.6"
11714
xtend "~4.0.1"
11715
11716
+through2@~0.2.3:
11717
+ version "0.2.3"
11718
+ resolved "https://registry.yarnpkg.com/through2/-/through2-0.2.3.tgz#eb3284da4ea311b6cc8ace3653748a52abf25a3f"
11719
+ integrity sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=
11720
+ dependencies:
11721
+ readable-stream "~1.1.9"
11722
+ xtend "~2.1.1"
11723
+
11724
through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6:
11725
version "2.3.8"
11726
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -11919,6 +12044,22 @@ update-notifier@2.3.0:
12044
semver-diff "^2.0.0"
12045
xdg-basedir "^3.0.0"
12046
12047
+update-notifier@^2.1.0:
12048
+ version "2.5.0"
12049
+ resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6"
12050
+ integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==
12051
+ dependencies:
12052
+ boxen "^1.2.1"
12053
+ chalk "^2.0.1"
12054
+ configstore "^3.0.0"
12055
+ import-lazy "^2.1.0"
12056
+ is-ci "^1.0.10"
12057
+ is-installed-globally "^0.1.0"
12058
+ is-npm "^1.0.0"
12059
+ latest-version "^3.0.0"
12060
+ semver-diff "^2.0.0"
12061
+ xdg-basedir "^3.0.0"
12062
+
12063
uri-js@^4.2.2:
12064
version "4.2.2"
12065
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
@@ -12480,6 +12621,13 @@ ws@^4.0.0:
12621
safe-buffer "~5.1.0"
12622
ultron "~1.1.0"
12623
12624
+ws@^7:
12625
+ version "7.0.1"
12626
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.0.1.tgz#1a04e86cc3a57c03783f4910fdb090cf31b8e165"
12627
+ integrity sha512-ILHfMbuqLJvnSgYXLgy4kMntroJpe8hT41dOVWM8bxRuw6TK4mgMp9VJUNsZTEc5Bh+Mbs0DJT4M0N+wBG9l9A==
12628
+ dependencies:
12629
+ async-limiter "^1.0.0"
12630
+
12631
xdg-basedir@^3.0.0:
12632
version "3.0.0"
12633
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
@@ -12517,6 +12665,13 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
12665
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
12666
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
12667
12668
+xtend@~2.1.1:
12669
+ version "2.1.2"
12670
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b"
12671
+ integrity sha1-bv7MKk2tjmlixJAbM3znuoe10os=
12672
+ dependencies:
12673
+ object-keys "~0.4.0"
12674
+
12675
y18n@^3.2.1:
12676
version "3.2.1"
12677
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
@@ -12672,6 +12827,13 @@ yauzl@2.10.0:
12827
buffer-crc32 "~0.2.3"
12828
fd-slicer "~1.1.0"
12829
12830
+yauzl@2.4.1:
12831
+ version "2.4.1"
12832
+ resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"
12833
+ integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=
12834
+ dependencies:
12835
+ fd-slicer "~1.0.1"
12836
+
12837
zip-dir@1.0.2:
12838
version "1.0.2"
12839
resolved "https://registry.yarnpkg.com/zip-dir/-/zip-dir-1.0.2.tgz#253f907aead62a21acd8721d8b88032b2411c051"