@samitouri / QOS-React-2 / commits / 255221c869

[DevTools] Add open in editor for fb (#22649)

Co-authored-by: Brian Vaughn <bvaughn@fb.com>

EzzAk committed Nov 3, 2021 at 08:27 UTC 255221c86930e31c144713eb466bf0a00ab8fd7e
11 files changed +108 -3
packages/react-devtools-core/webpack.standalone.js
+2
@@ -30,6 +30,7 @@ const __DEV__ = NODE_ENV === 'development';
30
31 const DEVTOOLS_VERSION = getVersionString();
32
33 +const EDITOR_URL = process.env.EDITOR_URL || null;
34 const LOGGING_URL = process.env.LOGGING_URL || null;
35
36 const featureFlagTarget =
@@ -83,6 +84,7 @@ module.exports = {
84 __TEST__: NODE_ENV === 'test',
85 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
86 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
87 + 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
88 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
89 'process.env.LOGGING_URL': `"${LOGGING_URL}"`,
90 'process.env.NODE_ENV': `"${NODE_ENV}"`,
packages/react-devtools-extensions/webpack.config.js
+2
@@ -32,6 +32,7 @@ const __DEV__ = NODE_ENV === 'development';
32
33 const DEVTOOLS_VERSION = getVersionString(process.env.DEVTOOLS_VERSION);
34
35 +const EDITOR_URL = process.env.EDITOR_URL || null;
36 const LOGGING_URL = process.env.LOGGING_URL || null;
37
38 const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
@@ -92,6 +93,7 @@ module.exports = {
93 __TEST__: NODE_ENV === 'test',
94 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`,
95 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
96 + 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
97 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
98 'process.env.LOGGING_URL': `"${LOGGING_URL}"`,
99 'process.env.NODE_ENV': `"${NODE_ENV}"`,
packages/react-devtools-inline/webpack.config.js
+3
@@ -20,6 +20,8 @@ if (!NODE_ENV) {
20
21 const __DEV__ = NODE_ENV === 'development';
22
23 +const EDITOR_URL = process.env.EDITOR_URL || null;
24 +
25 const DEVTOOLS_VERSION = getVersionString();
26
27 const babelOptions = {
@@ -76,6 +78,7 @@ module.exports = {
78 __TEST__: NODE_ENV === 'test',
79 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-inline"`,
80 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
81 + 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
82 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
83 'process.env.NODE_ENV': `"${NODE_ENV}"`,
84 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
packages/react-devtools-shared/src/constants.js
+3
@@ -32,6 +32,9 @@ export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY =
32 export const SESSION_STORAGE_LAST_SELECTION_KEY =
33 'React::DevTools::lastSelection';
34
35 +export const LOCAL_STORAGE_OPEN_IN_EDITOR_URL =
36 + 'React::DevTools::openInEditorUrl';
37 +
38 export const LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY =
39 'React::DevTools::parseHookNames';
40
packages/react-devtools-shared/src/devtools/views/ButtonIcon.js
+8
@@ -19,6 +19,7 @@ export type IconType =
19 | 'copy'
20 | 'delete'
21 | 'down'
22 + | 'editor'
23 | 'expanded'
24 | 'export'
25 | 'filter'
@@ -72,6 +73,9 @@ export default function ButtonIcon({className = '', type}: Props) {
73 case 'down':
74 pathData = PATH_DOWN;
75 break;
76 + case 'editor':
77 + pathData = PATH_EDITOR;
78 + break;
79 case 'expanded':
80 pathData = PATH_EXPANDED;
81 break;
@@ -268,3 +272,7 @@ const PATH_VIEW_DOM = `
272 const PATH_VIEW_SOURCE = `
273 M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z
274 `;
275 +
276 +const PATH_EDITOR = `
277 + M7 5h10v2h2V3c0-1.1-.9-1.99-2-1.99L7 1c-1.1 0-2 .9-2 2v4h2V5zm8.41 11.59L20 12l-4.59-4.59L14 8.83 17.17 12 14 15.17l1.41 1.42zM10 15.17L6.83 12 10 8.83 8.59 7.41 4 12l4.59 4.59L10 15.17zM17 19H7v-2H5v4c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2v-4h-2v2z
278 +`;
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+38 -2
@@ -8,7 +8,7 @@
8 */
9
10 import * as React from 'react';
11 -import {useCallback, useContext} from 'react';
11 +import {useCallback, useContext, useSyncExternalStore} from 'react';
12 import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
13 import {BridgeContext, StoreContext, OptionsContext} from '../context';
14 import Button from '../Button';
@@ -20,6 +20,8 @@ import {ElementTypeSuspense} from 'react-devtools-shared/src/types';
20 import CannotSuspendWarningMessage from './CannotSuspendWarningMessage';
21 import InspectedElementView from './InspectedElementView';
22 import {InspectedElementContext} from './InspectedElementContext';
23 +import {getOpenInEditorURL} from '../../../utils';
24 +import {LOCAL_STORAGE_OPEN_IN_EDITOR_URL} from '../../../constants';
25
26 import styles from './InspectedElement.css';
27
@@ -123,6 +125,21 @@ export default function InspectedElementWrapper(_: Props) {
125 inspectedElement != null &&
126 inspectedElement.canToggleSuspense;
127
128 + const editorURL = useSyncExternalStore(
129 + function subscribe(callback) {
130 + window.addEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
131 + return function unsubscribe() {
132 + window.removeEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
133 + };
134 + },
135 + function getState() {
136 + return getOpenInEditorURL();
137 + },
138 + );
139 +
140 + const canOpenInEditor =
141 + editorURL && inspectedElement != null && inspectedElement.source != null;
142 +
143 const toggleErrored = useCallback(() => {
144 if (inspectedElement == null || targetErrorBoundaryID == null) {
145 return;
@@ -198,6 +215,18 @@ export default function InspectedElementWrapper(_: Props) {
215 }
216 }, [bridge, dispatch, element, isSuspended, modalDialogDispatch, store]);
217
218 + const onOpenInEditor = useCallback(() => {
219 + const source = inspectedElement?.source;
220 + if (source == null || editorURL == null) {
221 + return;
222 + }
223 +
224 + const url = new URL(editorURL);
225 + url.href = url.href.replace('{path}', source.fileName);
226 + url.href = url.href.replace('{line}', String(source.lineNumber));
227 + window.open(url);
228 + }, [inspectedElement, editorURL]);
229 +
230 if (element === null) {
231 return (
232 <div className={styles.InspectedElement}>
@@ -223,7 +252,14 @@ export default function InspectedElementWrapper(_: Props) {
252 {element.displayName}
253 </div>
254 </div>
226 -
255 + {canOpenInEditor && (
256 + <Button
257 + className={styles.IconButton}
258 + onClick={onOpenInEditor}
259 + title="Open in editor">
260 + <ButtonIcon type="editor" />
261 + </Button>
262 + )}
263 {canToggleError && (
264 <Toggle
265 className={styles.IconButton}
packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js
+21 -1
@@ -16,7 +16,8 @@ import {
16 useRef,
17 useState,
18 } from 'react';
19 -import {useSubscription} from '../hooks';
19 +import {LOCAL_STORAGE_OPEN_IN_EDITOR_URL} from '../../../constants';
20 +import {useLocalStorage, useSubscription} from '../hooks';
21 import {StoreContext} from '../context';
22 import Button from '../Button';
23 import ButtonIcon from '../ButtonIcon';
@@ -37,6 +38,7 @@ import {
38 ElementTypeProfiler,
39 ElementTypeSuspense,
40 } from 'react-devtools-shared/src/types';
41 +import {getDefaultOpenInEditorURL} from 'react-devtools-shared/src/utils';
42
43 import styles from './SettingsShared.css';
44
@@ -81,6 +83,11 @@ export default function ComponentsSettings(_: {||}) {
83 [setParseHookNames],
84 );
85
86 + const [openInEditorURL, setOpenInEditorURL] = useLocalStorage<string>(
87 + LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
88 + getDefaultOpenInEditorURL(),
89 + );
90 +
91 const [componentFilters, setComponentFilters] = useState<
92 Array<ComponentFilter>,
93 >(() => [...store.componentFilters]);
@@ -271,6 +278,19 @@ export default function ComponentsSettings(_: {||}) {
278 <span className={styles.Warning}>(may be slow)</span>
279 </label>
280
281 + <label className={styles.OpenInURLSetting}>
282 + Open in Editor URL:{' '}
283 + <input
284 + className={styles.Input}
285 + type="text"
286 + placeholder={process.env.EDITOR_URL ?? 'vscode://file/{path}:{line}'}
287 + value={openInEditorURL}
288 + onChange={event => {
289 + setOpenInEditorURL(event.target.value);
290 + }}
291 + />
292 + </label>
293 +
294 <div className={styles.Header}>Hide components where...</div>
295
296 <table className={styles.Table}>
packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css
+8
@@ -14,6 +14,10 @@
14 margin-bottom: 0;
15 }
16
17 +.OpenInURLSetting {
18 + margin: 0.5rem 0;
19 +}
20 +
21 .OptionGroup {
22 display: inline-flex;
23 flex-direction: row;
@@ -30,6 +34,10 @@
34 margin-right: 0.5rem;
35 }
36
37 +.Spacer {
38 + height: 0.5rem;
39 +}
40 +
41 .Select {
42 }
43
packages/react-devtools-shared/src/devtools/views/hooks.js
+3
@@ -170,6 +170,9 @@ export function useLocalStorage<T>(
170 value instanceof Function ? (value: any)(storedValue) : value;
171 setStoredValue(valueToStore);
172 localStorageSetItem(key, JSON.stringify(valueToStore));
173 +
174 + // Notify listeners that this setting has changed.
175 + window.dispatchEvent(new Event(key));
176 } catch (error) {
177 console.log(error);
178 }
packages/react-devtools-shared/src/utils.js
+17
@@ -34,6 +34,7 @@ import {
34 import {ElementTypeRoot} from 'react-devtools-shared/src/types';
35 import {
36 LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
37 + LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
38 LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
39 LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
40 LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
@@ -386,6 +387,22 @@ export function setShowInlineWarningsAndErrors(value: boolean): void {
387 );
388 }
389
390 +export function getDefaultOpenInEditorURL(): string {
391 + return typeof process.env.EDITOR_URL === 'string'
392 + ? process.env.EDITOR_URL
393 + : '';
394 +}
395 +
396 +export function getOpenInEditorURL(): string {
397 + try {
398 + const raw = localStorageGetItem(LOCAL_STORAGE_OPEN_IN_EDITOR_URL);
399 + if (raw != null) {
400 + return JSON.parse(raw);
401 + }
402 + } catch (error) {}
403 + return getDefaultOpenInEditorURL();
404 +}
405 +
406 export function separateDisplayNameAndHOCs(
407 displayName: string | null,
408 type: ElementType,
packages/react-devtools-shell/webpack.config.js
+3
@@ -24,6 +24,8 @@ if (!TARGET) {
24 process.exit(1);
25 }
26
27 +const EDITOR_URL = process.env.EDITOR_URL || null;
28 +
29 const builtModulesDir = resolve(
30 __dirname,
31 '..',
@@ -69,6 +71,7 @@ const config = {
71 __PROFILE__: false,
72 __TEST__: NODE_ENV === 'test',
73 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
74 + 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
75 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-shell"`,
76 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
77 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,