@samitouri / QOS-React-2 / commits / 0f2fb5badf

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 +![React Native Inspector](http://i.imgur.com/ReFhREb.gif)
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 +![React DevTools Inspector Integration](http://i.imgur.com/wVgV9RP.gif)
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 +![React DevTools Chrome Console Integration](http://i.imgur.com/Cpvhs8i.gif)
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}>&lt;</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}>&gt;</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 + :&nbsp;
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"