@samitouri / QOS-React / commits / f3f4643a3a

Experimenting with portals

Brian Vaughn committed Mar 17, 2019 at 13:52 UTC f3f4643a3af686b1e4d8711df17f6465a855925c
15 files changed +136 -223
shells/browser/chrome/manifest.json
+1 -7
@@ -27,13 +27,7 @@
27 "devtools_page": "main.html",
28
29 "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
30 - "web_accessible_resources": [
31 - "elements.html",
32 - "main.html",
33 - "profiler.html",
34 - "settings.html",
35 - "build/backend.js"
36 - ],
30 + "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"],
31
32 "background": {
33 "scripts": ["build/background.js"],
shells/browser/firefox/manifest.json
+1 -7
@@ -33,13 +33,7 @@
33 "devtools_page": "main.html",
34
35 "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
36 - "web_accessible_resources": [
37 - "elements.html",
38 - "main.html",
39 - "profiler.html",
40 - "settings.html",
41 - "build/backend.js"
42 - ],
36 + "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"],
37
38 "background": {
39 "scripts": ["build/background.js"],
shells/browser/shared/build.js
+1 -8
@@ -7,14 +7,7 @@ const { join } = require('path');
7
8 // These files are copied along with Webpack-bundled files
9 // to produce the final web extension
10 -const STATIC_FILES = [
11 - 'icons',
12 - 'popups',
13 - 'elements.html',
14 - 'main.html',
15 - 'profiler.html',
16 - 'settings.html',
17 -];
10 +const STATIC_FILES = ['icons', 'popups', 'main.html', 'panel.html'];
11
12 const preProcess = async (destinationPath, tempPath) => {
13 await remove(destinationPath); // Clean up from previously completed builds
shells/browser/shared/panel.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/elements.js"></script>
30 + <script src="./build/panel.js"></script>
31 </body>
32 </html>
shells/browser/shared/profiler.html deleted
-32
@@ -1,32 +0,0 @@
1 -<!doctype html>
2 -<html style="display: flex">
3 - <head>
4 - <meta charset="utf8">
5 - <style>
6 - html {
7 - display: flex;
8 - }
9 - body {
10 - margin: 0;
11 - padding: 0;
12 - flex: 1;
13 - display: flex;
14 - }
15 - #container {
16 - display: flex;
17 - flex: 1;
18 - width: 100%;
19 - position: fixed;
20 - top: 0;
21 - left: 0;
22 - right: 0;
23 - bottom: 0;
24 - }
25 - </style>
26 - </head>
27 - <body>
28 - <!-- main react mount point -->
29 - <div id="container">Unable to find React on the page.</div>
30 - <script src="./build/profiler.js"></script>
31 - </body>
32 -</html>
shells/browser/shared/settings.html deleted
-32
@@ -1,32 +0,0 @@
1 -<!doctype html>
2 -<html style="display: flex">
3 - <head>
4 - <meta charset="utf8">
5 - <style>
6 - html {
7 - display: flex;
8 - }
9 - body {
10 - margin: 0;
11 - padding: 0;
12 - flex: 1;
13 - display: flex;
14 - }
15 - #container {
16 - display: flex;
17 - flex: 1;
18 - width: 100%;
19 - position: fixed;
20 - top: 0;
21 - left: 0;
22 - right: 0;
23 - bottom: 0;
24 - }
25 - </style>
26 - </head>
27 - <body>
28 - <!-- main react mount point -->
29 - <div id="container">Unable to find React on the page.</div>
30 - <script src="./build/settings.js"></script>
31 - </body>
32 -</html>
shells/browser/shared/src/main.js
+76 -51
@@ -1,8 +1,16 @@
1 /* global chrome */
2
3 +import { createElement } from 'react';
4 +import { unstable_createRoot as createRoot } 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';
13 +import DevTools from 'src/devtools/views/DevTools';
14
15 let panelCreated = false;
16
@@ -22,6 +30,7 @@ function createPanelIfReactLoaded() {
30
31 clearInterval(loadCheckInterval);
32
33 + let renderRootToPortal = null;
34 let bridge = null;
35 let store = null;
36 let elementsPanel = null;
@@ -54,69 +63,85 @@ function createPanelIfReactLoaded() {
63 // Otherwise the Store may miss important initial tree op codes.
64 inject(chrome.runtime.getURL('build/backend.js'));
65
66 + const viewElementSource = createViewElementSource(bridge, store);
67 +
68 + const container = document.createElement('div');
69 + const root = createRoot(container);
70 +
71 + renderRootToPortal = ({ overrideTab, portalContainer }) => {
72 + root.render(
73 + createElement(DevTools, {
74 + bridge,
75 + browserName: getBrowserName(),
76 + browserTheme: getBrowserTheme(),
77 + overrideTab,
78 + portalContainer,
79 + showTabBar: false,
80 + store,
81 + viewElementSource,
82 + })
83 + );
84 +
85 + const oldLinkTags = document.getElementsByTagName('link');
86 + const newLinkTags = [];
87 + for (let oldLinkTag of oldLinkTags) {
88 + if (oldLinkTag.rel === 'stylesheet') {
89 + const newLinkTag = document.createElement('link');
90 + for (let attribute of oldLinkTag.attributes) {
91 + newLinkTag.setAttribute(
92 + attribute.nodeName,
93 + attribute.nodeValue
94 + );
95 + }
96 + newLinkTags.push(newLinkTag);
97 + }
98 + }
99 +
100 + return newLinkTags;
101 + };
102 +
103 if (elementsPanel !== null) {
58 - elementsPanel.injectBridgeAndStore(bridge, store);
59 - }
60 - if (profilerPanel !== null) {
61 - profilerPanel.injectBridgeAndStore(bridge, store);
62 - }
63 - if (settingsPanel !== null) {
64 - settingsPanel.injectBridgeAndStore(bridge, store);
104 + elementsPanel.render(renderRootToPortal, 'elements');
105 }
106 }
107
108 initBridgeAndStore();
109
70 - chrome.devtools.panels.create(
71 - '⚛ Elements',
72 - '',
73 - 'elements.html',
74 - panel => {
75 - panel.onShown.addListener(panel => {
76 - if (elementsPanel === null) {
77 - panel.injectBridgeAndStore(bridge, store);
78 - }
110 + chrome.devtools.panels.create('⚛ Elements', '', 'panel.html', panel => {
111 + panel.onShown.addListener(panel => {
112 + elementsPanel = panel;
113
80 - elementsPanel = panel;
114 + if (renderRootToPortal !== null) {
115 + elementsPanel.render(renderRootToPortal, 'elements');
116 + }
117
82 - // TODO: When the user switches to the panel, check for an Elements tab selection.
83 - });
84 - panel.onHidden.addListener(() => {
85 - // TODO: Stop highlighting and stuff.
86 - });
87 - }
88 - );
118 + // TODO: When the user switches to the panel, check for an Elements tab selection.
119 + });
120 + panel.onHidden.addListener(() => {
121 + // TODO: Stop highlighting and stuff.
122 + });
123 + });
124
125 // TODO (profiling) Is there a way to detect profiling support and conditionally register this panel?
91 - chrome.devtools.panels.create(
92 - '⚛ Profiler',
93 - '',
94 - 'profiler.html',
95 - panel => {
96 - panel.onShown.addListener(panel => {
97 - if (settingsPanel === null) {
98 - panel.injectBridgeAndStore(bridge, store);
99 - }
126 + chrome.devtools.panels.create('⚛ Profiler', '', 'panel.html', panel => {
127 + panel.onShown.addListener(panel => {
128 + profilerPanel = panel;
129
101 - profilerPanel = panel;
102 - });
103 - }
104 - );
105 -
106 - chrome.devtools.panels.create(
107 - '⚛ Settings',
108 - '',
109 - 'settings.html',
110 - panel => {
111 - panel.onShown.addListener(panel => {
112 - if (settingsPanel === null) {
113 - panel.injectBridgeAndStore(bridge, store);
114 - }
130 + if (renderRootToPortal !== null) {
131 + profilerPanel.render(renderRootToPortal, 'profiler');
132 + }
133 + });
134 + });
135
116 - settingsPanel = panel;
117 - });
118 - }
119 - );
136 + chrome.devtools.panels.create('⚛ Settings', '', 'panel.html', panel => {
137 + panel.onShown.addListener(panel => {
138 + settingsPanel = panel;
139 +
140 + if (renderRootToPortal !== null) {
141 + settingsPanel.render(renderRootToPortal, 'settings');
142 + }
143 + });
144 + });
145
146 chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
147
shells/browser/shared/src/panel.js new
+20
@@ -0,0 +1,20 @@
1 +const container = document.getElementById('container');
2 +
3 +let hasInjectedStyles = false;
4 +
5 +window.render = (renderRootToPortal, tab) => {
6 + container.innerHTML = '';
7 +
8 + const linkTags = renderRootToPortal({
9 + overrideTab: tab,
10 + portalContainer: container,
11 + });
12 +
13 + if (!hasInjectedStyles) {
14 + hasInjectedStyles = true;
15 +
16 + for (let linkTag of linkTags) {
17 + document.head.appendChild(linkTag);
18 + }
19 + }
20 +};
shells/browser/shared/src/panels/elements.js deleted
-3
@@ -1,3 +0,0 @@
1 -import { createPanel } from './utils';
2 -
3 -createPanel('elements');
shells/browser/shared/src/panels/profiler.js deleted
-3
@@ -1,3 +0,0 @@
1 -import { createPanel } from './utils';
2 -
3 -createPanel('profiler');
shells/browser/shared/src/panels/settings.js deleted
-3
@@ -1,3 +0,0 @@
1 -import { createPanel } from './utils';
2 -
3 -createPanel('settings');
shells/browser/shared/src/panels/utils.js deleted
-72
@@ -1,72 +0,0 @@
1 -/* global chrome */
2 -
3 -import { createElement } from 'react';
4 -import { unstable_createRoot as createRoot, flushSync } from 'react-dom';
5 -import DevTools from 'src/devtools/views/DevTools';
6 -import { getBrowserName, getBrowserTheme } from '../utils';
7 -
8 -export function createPanel(defaultTab) {
9 - let injectedBridge = null;
10 - let injectedStore = null;
11 - let root = null;
12 -
13 - // All DevTools panel share a single Bridge and Store instance.
14 - // The main script will inject those shared instances using this method.
15 - window.injectBridgeAndStore = (bridge, store) => {
16 - injectedBridge = bridge;
17 - injectedStore = store;
18 -
19 - if (root === null) {
20 - injectAndInit();
21 - } else {
22 - // It's easiest to recreate the DevTools panel (to clean up potential stale state).
23 - // We can revisit this in the future as a small optimization.
24 - flushSync(() => root.unmount(injectAndInit));
25 - }
26 - };
27 -
28 - function viewElementSource(id) {
29 - if (injectedBridge == null || injectedStore == null) {
30 - return;
31 - }
32 -
33 - const rendererID = injectedStore.getRendererIDForElement(id);
34 - if (rendererID != null) {
35 - // Ask the renderer interface to determine the component function,
36 - // and store it as a global variable on the window
37 - injectedBridge.send('viewElementSource', { id, rendererID });
38 -
39 - setTimeout(() => {
40 - // Ask Chrome to display the location of the component function,
41 - // assuming the renderer found one.
42 - chrome.devtools.inspectedWindow.eval(`
43 - if (window.$type != null) {
44 - inspect(window.$type);
45 - }
46 - `);
47 - }, 100);
48 - }
49 - }
50 -
51 - function injectAndInit() {
52 - const container = ((document.getElementById(
53 - 'container'
54 - ): any): HTMLElement);
55 -
56 - // Clear the "React not found" initial message before rendering.
57 - container.innerHTML = '';
58 -
59 - root = createRoot(container);
60 - root.render(
61 - createElement(DevTools, {
62 - bridge: injectedBridge,
63 - browserName: getBrowserName(),
64 - browserTheme: getBrowserTheme(),
65 - defaultTab,
66 - showTabBar: false,
67 - store: injectedStore,
68 - viewElementSource,
69 - })
70 - );
71 - }
72 -}
shells/browser/shared/src/utils.js
+21
@@ -2,6 +2,27 @@
2
3 const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0;
4
5 +export function createViewElementSource(bridge: Bridge, store: Store) {
6 + return function viewElementSource(id) {
7 + const rendererID = store.getRendererIDForElement(id);
8 + if (rendererID != null) {
9 + // Ask the renderer interface to determine the component function,
10 + // and store it as a global variable on the window
11 + bridge.send('viewElementSource', { id, rendererID });
12 +
13 + setTimeout(() => {
14 + // Ask Chrome to display the location of the component function,
15 + // assuming the renderer found one.
16 + chrome.devtools.inspectedWindow.eval(`
17 + if (window.$type != null) {
18 + inspect(window.$type);
19 + }
20 + `);
21 + }, 100);
22 + }
23 + };
24 +}
25 +
26 export function getBrowserName() {
27 return IS_CHROME ? 'Chrome' : 'Firefox';
28 }
shells/browser/shared/webpack.config.js
+1 -3
@@ -16,9 +16,7 @@ module.exports = {
16 contentScript: './src/contentScript.js',
17 inject: './src/GlobalHook.js',
18 main: './src/main.js',
19 - elements: './src/panels/elements.js',
20 - profiler: './src/panels/profiler.js',
21 - settings: './src/panels/settings.js',
19 + panel: './src/panel.js',
20 },
21 output: {
22 path: __dirname + '/build',
src/devtools/views/DevTools.js
+14 -1
@@ -1,6 +1,7 @@
1 // @flow
2
3 import React, { useEffect, useState } from 'react';
4 +import { createPortal } from 'react-dom';
5 import Store from '../store';
6 import { BridgeContext, StoreContext } from './context';
7 import Elements from './Elements/Elements';
@@ -27,6 +28,8 @@ export type Props = {|
28 browserName: BrowserName,
29 defaultTab?: TabID,
30 browserTheme: BrowserTheme,
31 + overrideTab?: TabID,
32 + portalContainer?: Element,
33 showTabBar?: boolean,
34 store: Store,
35 viewElementSource?: ?Function,
@@ -59,11 +62,17 @@ export default function DevTools({
62 browserName,
63 defaultTab = 'elements',
64 browserTheme = 'light',
65 + overrideTab,
66 + portalContainer,
67 showTabBar = false,
68 store,
69 viewElementSource = null,
70 }: Props) {
71 const [tab, setTab] = useState(defaultTab);
72 + if (overrideTab != null && overrideTab !== tab) {
73 + setTab(overrideTab);
74 + }
75 +
76 const [supportsProfiling, setSupportsProfiling] = useState(
77 store.supportsProfiling
78 );
@@ -100,7 +109,7 @@ export default function DevTools({
109 break;
110 }
111
103 - return (
112 + const children = (
113 <BridgeContext.Provider value={bridge}>
114 <StoreContext.Provider value={store}>
115 <SettingsContextController browserTheme={browserTheme}>
@@ -135,4 +144,8 @@ export default function DevTools({
144 </StoreContext.Provider>
145 </BridgeContext.Provider>
146 );
147 +
148 + return portalContainer != null
149 + ? createPortal(children, portalContainer)
150 + : children;
151 }