@samitouri / QOS-React-2 / commits / 53632883b6

Implemented view-source button for DOM extension

Brian Vaughn committed Feb 22, 2019 at 13:24 UTC 53632883b6dc3c40394f44873ce4e22a9f7e5290
8 files changed +117 -15
shells/browser/shared/src/panels/utils.js
+26
@@ -1,3 +1,5 @@
1 +/* global chrome */
2 +
3 import { createElement } from 'react';
4 import { createRoot, flushSync } from 'react-dom';
5 import DevTools from 'src/devtools/views/DevTools';
@@ -23,6 +25,29 @@ export function createPanel(defaultTab) {
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'
@@ -40,6 +65,7 @@ export function createPanel(defaultTab) {
65 defaultTab,
66 showTabBar: false,
67 store: injectedStore,
68 + viewElementSource,
69 })
70 );
71 }
src/backend/agent.js
+10
@@ -55,6 +55,7 @@ export default class Agent extends EventEmitter {
55 bridge.addListener('startInspectingDOM', this.startInspectingDOM);
56 bridge.addListener('stopInspectingDOM', this.stopInspectingDOM);
57 bridge.addListener('shutdown', this.shutdown);
58 + bridge.addListener('viewElementSource', this.viewElementSource);
59 }
60
61 getIDForNode(node: Object): number | null {
@@ -189,6 +190,15 @@ export default class Agent extends EventEmitter {
190 window.removeEventListener('mouseover', this._onMouseOver);
191 };
192
193 + viewElementSource = ({ id, rendererID }: InspectSelectParams) => {
194 + const renderer = this._rendererInterfaces[rendererID];
195 + if (renderer == null) {
196 + console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
197 + } else {
198 + renderer.prepareViewElementSource(id);
199 + }
200 + };
201 +
202 onHookOperations = (operations: Uint32Array) => {
203 debug('onHookOperations', operations);
204
src/backend/renderer.js
+46 -7
@@ -1028,16 +1028,24 @@ export function attach(
1028 return;
1029 }
1030
1031 - switch (fiber.tag) {
1031 + const { memoizedProps, stateNode, tag, type } = fiber;
1032 +
1033 + switch (tag) {
1034 case ClassComponent:
1035 case IncompleteClassComponent:
1036 case IndeterminateComponent:
1035 - global.$r = fiber.stateNode;
1037 + global.$r = stateNode;
1038 break;
1039 case FunctionComponent:
1040 global.$r = {
1039 - props: fiber.memoizedProps,
1040 - type: fiber.type,
1041 + props: memoizedProps,
1042 + type,
1043 + };
1044 + break;
1045 + case ForwardRef:
1046 + global.$r = {
1047 + props: memoizedProps,
1048 + type: type.render,
1049 };
1050 break;
1051 default:
@@ -1046,6 +1054,30 @@ export function attach(
1054 }
1055 }
1056
1057 + function prepareViewElementSource(id: number): void {
1058 + let fiber = idToFiberMap.get(id);
1059 +
1060 + if (fiber == null) {
1061 + console.warn(`Could not find Fiber with id "${id}"`);
1062 + return;
1063 + }
1064 +
1065 + switch (fiber.tag) {
1066 + case ClassComponent:
1067 + case IncompleteClassComponent:
1068 + case IndeterminateComponent:
1069 + case FunctionComponent:
1070 + global.$type = fiber.type;
1071 + break;
1072 + case ForwardRef:
1073 + global.$type = fiber.type.render;
1074 + break;
1075 + default:
1076 + global.$type = null;
1077 + break;
1078 + }
1079 + }
1080 +
1081 function inspectElement(id: number): InspectedElement | null {
1082 let fiber = idToFiberMap.get(id);
1083
@@ -1075,13 +1107,16 @@ export function attach(
1107
1108 const typeSymbol = getTypeSymbol(fiber);
1109
1110 + let canViewSource = false;
1111 let context = null;
1112 if (
1113 tag === ClassComponent ||
1114 tag === FunctionComponent ||
1115 tag === IncompleteClassComponent ||
1083 - tag === IndeterminateComponent
1116 + tag === IndeterminateComponent ||
1117 + tag === ForwardRef
1118 ) {
1119 + canViewSource = true;
1120 if (stateNode && stateNode.context != null) {
1121 context = stateNode.context;
1122 }
@@ -1150,6 +1185,9 @@ export function attach(
1185 // Does the current renderer support editable function props?
1186 canEditFunctionProps: typeof overrideProps === 'function',
1187
1188 + // Can view component source location.
1189 + canViewSource,
1190 +
1191 // Inspectable properties.
1192 // TODO Review sanitization approach for the below inspectable values.
1193 context,
@@ -1227,14 +1265,15 @@ export function attach(
1265 }
1266
1267 return {
1268 + cleanup,
1269 getFiberIDFromNative,
1270 getNativeFromReactElement,
1271 handleCommitFiberRoot,
1272 handleCommitFiberUnmount,
1273 inspectElement,
1235 - selectElement,
1236 - cleanup,
1274 + prepareViewElementSource,
1275 renderer,
1276 + selectElement,
1277 setInContext,
1278 setInHook,
1279 setInProps,
src/backend/types.js
+1
@@ -63,6 +63,7 @@ export type RendererInterface = {
63 handleCommitFiberRoot: (fiber: Object) => void,
64 handleCommitFiberUnmount: (fiber: Object) => void,
65 inspectElement: (id: number) => InspectedElement | null,
66 + prepareViewElementSource: (id: number) => void,
67 renderer: ReactRenderer | null,
68 selectElement: (id: number) => void,
69 setInContext: (id: number, path: Array<string | number>, value: any) => void,
src/devtools/types.js
+3
@@ -54,6 +54,9 @@ export type InspectedElement = {|
54 // Does the current renderer support editable function props?
55 canEditFunctionProps: boolean,
56
57 + // Can view component source location.
58 + canViewSource: boolean,
59 +
60 // Inspectable properties.
61 context: Object | null,
62 hooks: Object | null,
src/devtools/views/DevTools.js
+3 -1
@@ -27,6 +27,7 @@ export type Props = {|
27 browserTheme: BrowserTheme,
28 showTabBar?: boolean,
29 store: Store,
30 + viewElementSource?: ?Function,
31 |};
32
33 export default function DevTools({
@@ -36,6 +37,7 @@ export default function DevTools({
37 browserTheme = 'light',
38 showTabBar = false,
39 store,
40 + viewElementSource = null,
41 }: Props) {
42 const [tab, setTab] = useState(defaultTab);
43
@@ -57,7 +59,7 @@ export default function DevTools({
59 <BridgeContext.Provider value={bridge}>
60 <StoreContext.Provider value={store}>
61 <SettingsContextController browserTheme={browserTheme}>
60 - <TreeContextController>
62 + <TreeContextController viewElementSource={viewElementSource}>
63 <div className={styles.DevTools}>
64 {showTabBar && (
65 <div className={styles.TabBar}>
src/devtools/views/SelectedElement.js
+15 -6
@@ -23,15 +23,16 @@ import type { DehydratedData, Element } from 'src/devtools/types';
23 export type Props = {||};
24
25 export default function SelectedElement(_: Props) {
26 - const { selectedElementID } = useContext(TreeContext);
26 + const { selectedElementID, viewElementSource } = useContext(TreeContext);
27 const bridge = useContext(BridgeContext);
28 const store = useContext(StoreContext);
29 +
30 const element =
31 selectedElementID !== null ? store.getElementByID(selectedElementID) : null;
32
33 const inspectedElement = useInspectedElement(selectedElementID);
34
34 - const handleClick = useCallback(() => {
35 + const highlightElement = useCallback(() => {
36 if (element !== null && selectedElementID !== null) {
37 const rendererID =
38 store.getRendererIDForElement(selectedElementID) || null;
@@ -45,7 +46,11 @@ export default function SelectedElement(_: Props) {
46 }
47 }, [bridge, element, selectedElementID, store]);
48
48 - // TODO Make "view source" button work
49 + const viewSource = useCallback(() => {
50 + if (viewElementSource != null && selectedElementID !== null) {
51 + viewElementSource(selectedElementID);
52 + }
53 + }, [selectedElementID, viewElementSource]);
54
55 if (element === null) {
56 return (
@@ -55,7 +60,10 @@ export default function SelectedElement(_: Props) {
60 );
61 }
62
58 - const source = inspectedElement ? inspectedElement.source : null;
63 + const canViewSource =
64 + inspectedElement &&
65 + inspectedElement.canViewSource &&
66 + viewElementSource !== null;
67
68 return (
69 <div className={styles.SelectedElement}>
@@ -68,14 +76,15 @@ export default function SelectedElement(_: Props) {
76
77 <Button
78 className={styles.IconButton}
71 - onClick={handleClick}
79 + onClick={highlightElement}
80 title="Highlight this element in the page"
81 >
82 <ButtonIcon type="view-dom" />
83 </Button>
76 - {source !== null && (
84 + {canViewSource && (
85 <Button
86 className={styles.IconButton}
87 + onClick={viewSource}
88 title="View source for this element"
89 >
90 <ButtonIcon type="view-source" />
src/devtools/views/TreeContext.js
+13 -1
@@ -56,6 +56,9 @@ type Context = {|
56 ownerStackIndex: number | null,
57 resetOwnerStack(): void,
58 selectOwner(id: number): void,
59 +
60 + // Injected by parent HTML/JavaScript
61 + viewElementSource: Function | null,
62 |};
63
64 const TreeContext = createContext<Context>(((null: any): Context));
@@ -473,8 +476,13 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
476 };
477 }
478
479 +type Props = {|
480 + children: React$Node,
481 + viewElementSource: Function | null,
482 +|};
483 +
484 // TODO Remove TreeContextController wrapper element once global ConsearchText.write API exists.
477 -function TreeContextController({ children }: {| children: React$Node |}) {
485 +function TreeContextController({ children, viewElementSource }: Props) {
486 const bridge = useContext(BridgeContext);
487 const store = useContext(StoreContext);
488
@@ -599,6 +607,9 @@ function TreeContextController({ children }: {| children: React$Node |}) {
607 ownerStackIndex: state.ownerStackIndex,
608 resetOwnerStack,
609 selectOwner,
610 +
611 + // Injected by parent HTML/JavaScript
612 + viewElementSource,
613 }),
614 [
615 getElementAtIndex,
@@ -612,6 +623,7 @@ function TreeContextController({ children }: {| children: React$Node |}) {
623 selectPreviousElementInTree,
624 setSearchText,
625 state,
626 + viewElementSource,
627 ]
628 );
629