@samitouri / QOS-React-2 / commits / 15c5396169

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) =>