Re-added "Settings" panel to browser extension and (hopefully) fixed a lot of sources of error
Brian Vaughn committed
Feb 14, 2019 at 11:45 UTC
15c53961696c186ee799e7bbbe6efc40d9eff128
17 files changed
+178
-140
shells/browser/chrome/manifest.json
+1
-1
@@ -28,8 +28,8 @@
28
29
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
30
"web_accessible_resources": [
31
+ "elements.html",
32
"main.html",
32
- "panel.html",
33
"settings.html",
34
"build/backend.js"
35
],
shells/browser/firefox/manifest.json
+1
-1
@@ -34,8 +34,8 @@
34
35
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
36
"web_accessible_resources": [
37
+ "elements.html",
38
"main.html",
38
- "panel.html",
39
"settings.html",
40
"build/backend.js"
41
],
shells/browser/shared/build.js
+1
-1
@@ -10,8 +10,8 @@ const { join } = require('path');
10
const STATIC_FILES = [
11
'icons',
12
'popups',
13
+ 'elements.html',
14
'main.html',
14
- 'panel.html',
15
'settings.html',
16
];
17
shells/browser/shared/elements.html
renamed
+1
-1
@@ -27,6 +27,6 @@
27
<body>
28
<!-- main react mount point -->
29
<div id="container">Unable to find React on the page.</div>
30
- <script src="./build/panel.js"></script>
30
+ <script src="./build/elements.js"></script>
31
</body>
32
</html>
shells/browser/shared/src/main.js
+84
-13
@@ -1,5 +1,8 @@
1
/* global chrome */
2
3
+import Bridge from 'src/bridge';
4
+import Store from 'src/devtools/Store';
5
+
6
let panelCreated = false;
7
8
function createPanelIfReactLoaded() {
@@ -9,37 +12,105 @@ function createPanelIfReactLoaded() {
12
13
chrome.devtools.inspectedWindow.eval(
14
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
12
- function(pageHasReact, err) {
15
+ function(pageHasReact, error) {
16
if (!pageHasReact || panelCreated) {
17
return;
18
}
19
17
- clearInterval(loadCheckInterval);
20
panelCreated = true;
19
- chrome.devtools.panels.create('⚛ Elements', '', 'panel.html', panel => {
20
- panel.onShown.addListener(function(window) {
21
- // TODO: When the user switches to the panel, check for an Elements tab selection.
21
+
22
+ clearInterval(loadCheckInterval);
23
+
24
+ let bridge = null;
25
+ let store = null;
26
+ let elementsPanel = null;
27
+ let settingsPanel = null;
28
+
29
+ function initBridgeAndStore() {
30
+ let hasPortBeenDisconnected = false;
31
+ const port = chrome.runtime.connect({
32
+ name: '' + chrome.devtools.inspectedWindow.tabId,
33
});
23
- panel.onHidden.addListener(function() {
24
- // TODO: Stop highlighting and stuff.
34
+ port.onDisconnect.addListener(() => {
35
+ hasPortBeenDisconnected = true;
36
});
26
- });
37
28
- /* TODO Revisit this architecture; currently it causes duplicate agent/bridge traffic.
38
+ bridge = new Bridge({
39
+ listen(fn) {
40
+ port.onMessage.addListener(message => fn(message));
41
+ },
42
+ send(event: string, payload: any, transferable?: Array<any>) {
43
+ if (!hasPortBeenDisconnected) {
44
+ port.postMessage({ event, payload }, transferable);
45
+ }
46
+ },
47
+ });
48
+
49
+ store = new Store(bridge);
50
+
51
+ if (elementsPanel !== null) {
52
+ elementsPanel.injectBridgeAndStore(bridge, store);
53
+ }
54
+ if (settingsPanel !== null) {
55
+ settingsPanel.injectBridgeAndStore(bridge, store);
56
+ }
57
+ }
58
+
59
+ initBridgeAndStore();
60
+
61
+ chrome.devtools.panels.create(
62
+ '⚛ Elements',
63
+ '',
64
+ 'elements.html',
65
+ panel => {
66
+ panel.onShown.addListener(panel => {
67
+ if (elementsPanel === null) {
68
+ panel.injectBridgeAndStore(bridge, store);
69
+ }
70
+
71
+ elementsPanel = panel;
72
+
73
+ // TODO: When the user switches to the panel, check for an Elements tab selection.
74
+ });
75
+ panel.onHidden.addListener(() => {
76
+ // TODO: Stop highlighting and stuff.
77
+ });
78
+ }
79
+ );
80
+
81
chrome.devtools.panels.create(
82
'⚛ Settings',
83
'',
84
'settings.html',
33
- panel => {}
85
+ panel => {
86
+ panel.onShown.addListener(panel => {
87
+ if (settingsPanel === null) {
88
+ panel.injectBridgeAndStore(bridge, store);
89
+ }
90
+
91
+ settingsPanel = panel;
92
+ });
93
+ }
94
);
35
- */
95
+
96
+ chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
97
+
98
+ // Shutdown bridge and re-initialize DevTools panel when a new page is loaded.
99
+ chrome.devtools.network.onNavigated.addListener(function onNavigated() {
100
+ bridge.send('shutdown');
101
+
102
+ initBridgeAndStore();
103
+ });
104
}
105
);
106
}
107
40
-chrome.devtools.network.onNavigated.addListener(function() {
108
+// Load (or reload) the DevTools extension when the user navigates to a new page.
109
+function checkPageForReact() {
110
createPanelIfReactLoaded();
42
-});
111
+}
112
+
113
+chrome.devtools.network.onNavigated.addListener(checkPageForReact);
114
115
// Check to see if React has loaded once per second in case React is added
116
// after page load
shells/browser/shared/src/panel.js
deleted
-63
@@ -1,63 +0,0 @@
1
-/* global chrome */
2
-
3
-import { createElement } from 'react';
4
-import { createRoot, flushSync } from 'react-dom';
5
-import Bridge from 'src/bridge';
6
-import DevTools from 'src/devtools/views/DevTools';
7
-import inject from './inject';
8
-import { getBrowserName, getBrowserTheme } from './utils';
9
-
10
-const container = ((document.getElementById('container'): any): HTMLElement);
11
-
12
-function injectAndInit() {
13
- let disconnected = false;
14
-
15
- const port = chrome.runtime.connect({
16
- name: '' + chrome.devtools.inspectedWindow.tabId,
17
- });
18
- port.onDisconnect.addListener(() => {
19
- disconnected = true;
20
- });
21
-
22
- const bridge = new Bridge({
23
- listen(fn) {
24
- port.onMessage.addListener(message => fn(message));
25
- },
26
- send(event: string, payload: any, transferable?: Array<any>) {
27
- if (disconnected) {
28
- return;
29
- }
30
- port.postMessage({ event, payload }, transferable);
31
- },
32
- });
33
-
34
- // Clear the "React not found" initial message before rendering.
35
- container.innerHTML = '';
36
-
37
- const root = createRoot(container);
38
- root.render(
39
- createElement(DevTools, {
40
- bridge,
41
- browserName: getBrowserName(),
42
- browserTheme: getBrowserTheme(),
43
- defaultTab: 'elements',
44
- showTabBar: false,
45
- })
46
- );
47
-
48
- // Initialize the backend only once the DevTools frontend Store has been initialized.
49
- // Otherwise the Store may miss important initial tree op codes.
50
- inject(chrome.runtime.getURL('build/backend.js'));
51
-
52
- // Reload the DevTools extension when the user navigates to a new page.
53
- function onNavigated() {
54
- chrome.devtools.network.onNavigated.removeListener(onNavigated);
55
-
56
- bridge.send('shutdown');
57
-
58
- flushSync(() => root.unmount(injectAndInit));
59
- }
60
- chrome.devtools.network.onNavigated.addListener(onNavigated);
61
-}
62
-
63
-injectAndInit();
shells/browser/shared/src/panels/elements.js
new
+3
@@ -0,0 +1,3 @@
1
+import { createPanel } from './utils';
2
+
3
+createPanel('elements');
shells/browser/shared/src/panels/settings.js
new
+3
@@ -0,0 +1,3 @@
1
+import { createPanel } from './utils';
2
+
3
+createPanel('settings');
shells/browser/shared/src/panels/utils.js
new
+53
@@ -0,0 +1,53 @@
1
+/* global chrome */
2
+
3
+import { createElement } from 'react';
4
+import { createRoot, flushSync } from 'react-dom';
5
+import DevTools from 'src/devtools/views/DevTools';
6
+import inject from '../inject';
7
+import { getBrowserName, getBrowserTheme } from '../utils';
8
+
9
+export function createPanel(defaultTab) {
10
+ let injectedBridge = null;
11
+ let injectedStore = null;
12
+ let root = null;
13
+
14
+ // All DevTools panel share a single Bridge and Store instance.
15
+ // The main script will inject those shared instances using this method.
16
+ window.injectBridgeAndStore = (bridge, store) => {
17
+ injectedBridge = bridge;
18
+ injectedStore = store;
19
+
20
+ if (root === null) {
21
+ injectAndInit();
22
+ } else {
23
+ // It's easiest to recreate the DevTools panel (to clean up potential stale state).
24
+ // We can revisit this in the future as a small optimization.
25
+ flushSync(() => root.unmount(injectAndInit));
26
+ }
27
+ };
28
+
29
+ function injectAndInit() {
30
+ const container = ((document.getElementById(
31
+ 'container'
32
+ ): any): HTMLElement);
33
+
34
+ // Clear the "React not found" initial message before rendering.
35
+ container.innerHTML = '';
36
+
37
+ root = createRoot(container);
38
+ root.render(
39
+ createElement(DevTools, {
40
+ bridge: injectedBridge,
41
+ browserName: getBrowserName(),
42
+ browserTheme: getBrowserTheme(),
43
+ defaultTab,
44
+ showTabBar: false,
45
+ store: injectedStore,
46
+ })
47
+ );
48
+
49
+ // Initialize the backend only once the DevTools frontend Store has been initialized.
50
+ // Otherwise the Store may miss important initial tree op codes.
51
+ inject(chrome.runtime.getURL('build/backend.js'));
52
+ }
53
+}
shells/browser/shared/src/settings.js
deleted
-48
@@ -1,48 +0,0 @@
1
-/* global chrome */
2
-
3
-import { createElement } from 'react';
4
-import { createRoot, flushSync } from 'react-dom';
5
-import Bridge from 'src/bridge';
6
-import DevTools from 'src/devtools/views/DevTools';
7
-import inject from './inject';
8
-import { getBrowserName, getBrowserTheme } from './utils';
9
-
10
-const container = ((document.getElementById('container'): any): HTMLElement);
11
-
12
-function injectAndInit() {
13
- // Noop Bridge for the Settings panel
14
- const bridge = new Bridge({
15
- listen(fn) {},
16
- send(event: string, payload: any, transferable?: Array<any>) {},
17
- });
18
-
19
- // Clear the "React not found" initial message before rendering.
20
- container.innerHTML = '';
21
-
22
- const root = createRoot(container);
23
- root.render(
24
- createElement(DevTools, {
25
- bridge,
26
- browserName: getBrowserName(),
27
- browserTheme: getBrowserTheme(),
28
- defaultTab: 'settings',
29
- showTabBar: false,
30
- })
31
- );
32
-
33
- // Initialize the backend only once the DevTools frontend Store has been initialized.
34
- // Otherwise the Store may miss important initial tree op codes.
35
- inject(chrome.runtime.getURL('build/backend.js'));
36
-
37
- // Reload the DevTools extension when the user navigates to a new page.
38
- function onNavigated() {
39
- chrome.devtools.network.onNavigated.removeListener(onNavigated);
40
-
41
- bridge.send('shutdown');
42
-
43
- flushSync(() => root.unmount(injectAndInit));
44
- }
45
- chrome.devtools.network.onNavigated.addListener(onNavigated);
46
-}
47
-
48
-injectAndInit();
shells/browser/shared/webpack.config.js
+2
-2
@@ -16,8 +16,8 @@ module.exports = {
16
contentScript: './src/contentScript.js',
17
inject: './src/GlobalHook.js',
18
main: './src/main.js',
19
- panel: './src/panel.js',
20
- settings: './src/settings.js',
19
+ elements: './src/panels/elements.js',
20
+ settings: './src/panels/settings.js',
21
},
22
output: {
23
path: __dirname + '/build',
shells/dev/src/devtools.js
+4
@@ -6,6 +6,7 @@ import { createRoot } from 'react-dom';
6
import Bridge from 'src/bridge';
7
import { installHook } from 'src/hook';
8
import { initDevTools } from 'src/devtools';
9
+import Store from 'src/devtools/Store';
10
import DevTools from 'src/devtools/views/DevTools';
11
12
const iframe = ((document.getElementById('target'): any): HTMLIFrameElement);
@@ -53,6 +54,8 @@ inject('./build/app.js', () => {
54
55
cb(bridge);
56
57
+ const store = new Store(bridge);
58
+
59
const root = createRoot(container);
60
const batch = root.createBatch();
61
batch.render(
@@ -61,6 +64,7 @@ inject('./build/app.js', () => {
64
browserName: 'Chrome',
65
browserTheme: 'light',
66
showTabBar: true,
67
+ store,
68
})
69
);
70
batch.then(() => {
src/backend/agent.js
+12
-3
@@ -25,11 +25,11 @@ export default class Agent extends EventEmitter {
25
addBridge(bridge: Bridge) {
26
this._bridge = bridge;
27
28
- bridge.addListener('shutdown', () => this.emit('shutdown'));
29
-
28
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
29
bridge.addListener('inspectElement', this.inspectElement);
30
bridge.addListener('selectElement', this.selectElement);
31
+ bridge.addListener('shutdown', this.shutdown);
32
+
33
// TODO Listen to bridge for things like selection.
34
// bridge.on('...'), this...);
35
}
@@ -94,8 +94,17 @@ export default class Agent extends EventEmitter {
94
this._rendererInterfaces[rendererID] = rendererInterface;
95
}
96
97
+ shutdown = () => {
98
+ this.emit('shutdown');
99
+ };
100
+
101
onHookOperations = (operations: Uint32Array) => {
102
debug('onHookOperations', operations);
99
- this._bridge.send('operations', operations, [operations.buffer]);
103
+
104
+ // TODO The chrome.runtime does not currently support transferables; it forces JSON serialization.
105
+ // The Store has a fallback in place that parses the message as JSON if the type isn't an array.
106
+ // Sometimes using transferrables also cause Chrome or Firefox to throw "ArrayBuffer at index 0 is already neutered".
107
+ // this._bridge.send('operations', operations, [operations.buffer]);
108
+ this._bridge.send('operations', operations);
109
};
110
}
src/devtools/store.js
+7
-4
@@ -28,6 +28,8 @@ const debug = (methodName, ...args) => {
28
* ContextProviders can subscribe to the Store for specific things they want to provide.
29
*/
30
export default class Store extends EventEmitter {
31
+ _bridge: Bridge;
32
+
33
// Map of ID to Element.
34
// Elements are mutable (for now) to avoid excessive cloning during tree updates.
35
_idToElement: Map<number, Element> = new Map();
@@ -52,8 +54,9 @@ export default class Store extends EventEmitter {
54
55
debug('constructor', 'subscribing to Bridge');
56
55
- bridge.on('operations', this.onBridgeOperations);
56
- bridge.on('shutdown', this.onBridgeShutdown);
57
+ this._bridge = bridge;
58
+ this._bridge.on('operations', this.onBridgeOperations);
59
+ this._bridge.on('shutdown', this.onBridgeShutdown);
60
}
61
62
get numElements(): number {
@@ -402,8 +405,8 @@ export default class Store extends EventEmitter {
405
onBridgeShutdown = () => {
406
debug('onBridgeShutdown', 'unsubscribing from Bridge');
407
405
- bridge.off('operations', this.onBridgeOperations);
406
- bridge.off('shutdown', this.onBridgeShutdown);
408
+ this._bridge.off('operations', this.onBridgeOperations);
409
+ this._bridge.off('shutdown', this.onBridgeShutdown);
410
};
411
412
// DEBUG
src/devtools/views/DevTools.js
+3
-2
@@ -1,6 +1,6 @@
1
// @flow
2
3
-import React, { useMemo, useState } from 'react';
3
+import React, { useState } from 'react';
4
import Store from '../store';
5
import { BridgeContext, StoreContext } from './context';
6
import Elements from './Elements';
@@ -26,6 +26,7 @@ export type Props = {|
26
defaultTab?: TabID,
27
browserTheme: BrowserTheme,
28
showTabBar?: boolean,
29
+ store: Store,
30
|};
31
32
export default function DevTools({
@@ -34,8 +35,8 @@ export default function DevTools({
35
defaultTab = 'elements',
36
browserTheme = 'light',
37
showTabBar = false,
38
+ store,
39
}: Props) {
38
- const store = useMemo<Store>(() => new Store(bridge), []);
40
const [tab, setTab] = useState(defaultTab);
41
42
let tabElement;
src/devtools/views/Element.js
+2
@@ -44,6 +44,8 @@ export default function ElementView({ index, style }: Props) {
44
const isSelected = selectedElementID === id;
45
const showDollarR = isSelected && type === ElementTypeClassOrFunction;
46
47
+ // TODO styles.SelectedElement is 100% width but it doesn't take horizontal overflow into account.
48
+
49
return (
50
<div
51
className={isSelected ? styles.SelectedElement : styles.Element}
src/devtools/views/TreeContext.js
+1
-1
@@ -524,7 +524,7 @@ function TreeContextController({ children }: {| children: React$Node |}) {
524
? store.getElementAtIndex(index)
525
: store.getElementByID(state._ownerFlatTree[index]);
526
},
527
- [state]
527
+ [state, store]
528
);
529
const selectElementAtIndex = useCallback(
530
(index: number) =>