Skip to content

[pull] main from facebook:main #190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 98 additions & 1 deletion packages/react-devtools-extensions/src/main/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* global chrome */

import type {SourceSelection} from 'react-devtools-shared/src/devtools/views/Editor/EditorPane';

import {createElement} from 'react';
import {flushSync} from 'react-dom';
import {createRoot} from 'react-dom/client';
Expand Down Expand Up @@ -73,19 +75,60 @@ function createBridge() {
);
});

const sourcesPanel = chrome.devtools.panels.sources;

const onBrowserElementSelectionChanged = () =>
setReactSelectionFromBrowser(bridge);
const onBrowserSourceSelectionChanged = (location: {
url: string,
startLine: number,
startColumn: number,
endLine: number,
endColumn: number,
}) => {
if (
currentSelectedSource === null ||
currentSelectedSource.url !== location.url
) {
currentSelectedSource = {
url: location.url,
selectionRef: {
// We use 1-based line and column, Chrome provides them 0-based.
line: location.startLine + 1,
column: location.startColumn + 1,
},
};
// Rerender with the new file selection.
render();
} else {
// Update the ref to the latest position without updating the url. No need to rerender.
const selectionRef = currentSelectedSource.selectionRef;
selectionRef.line = location.startLine + 1;
selectionRef.column = location.startColumn + 1;
}
};
const onBridgeShutdown = () => {
chrome.devtools.panels.elements.onSelectionChanged.removeListener(
onBrowserElementSelectionChanged,
);
if (sourcesPanel) {
currentSelectedSource = null;
sourcesPanel.onSelectionChanged.removeListener(
onBrowserSourceSelectionChanged,
);
}
};

bridge.addListener('shutdown', onBridgeShutdown);

chrome.devtools.panels.elements.onSelectionChanged.addListener(
onBrowserElementSelectionChanged,
);
if (sourcesPanel) {
sourcesPanel.onSelectionChanged.addListener(
onBrowserSourceSelectionChanged,
);
}
}

function createBridgeAndStore() {
Expand Down Expand Up @@ -152,11 +195,13 @@ function createBridgeAndStore() {
bridge,
browserTheme: getBrowserTheme(),
componentsPortalContainer,
profilerPortalContainer,
editorPortalContainer,
currentSelectedSource,
enabledInspectedElementContextMenu: true,
fetchFileWithCaching,
hookNamesModuleLoaderFunction,
overrideTab,
profilerPortalContainer,
showTabBar: false,
store,
warnIfUnsupportedVersionDetected: true,
Expand Down Expand Up @@ -257,6 +302,53 @@ function createProfilerPanel() {
);
}

function createSourcesEditorPanel() {
if (editorPortalContainer) {
// Panel is created and user opened it at least once
ensureInitialHTMLIsCleared(editorPortalContainer);
render();

return;
}

if (editorPane) {
// Panel is created, but wasn't opened yet, so no document is present for it
return;
}

const sourcesPanel = chrome.devtools.panels.sources;
if (!sourcesPanel) {
// Firefox doesn't currently support extending the source panel.
return;
}

sourcesPanel.createSidebarPane('Code Editor ⚛', createdPane => {
editorPane = createdPane;

createdPane.setPage('panel.html');
createdPane.setHeight('42px');

createdPane.onShown.addListener(portal => {
editorPortalContainer = portal.container;
if (editorPortalContainer != null && render) {
ensureInitialHTMLIsCleared(editorPortalContainer);

render();
portal.injectStyles(cloneStyleTags);

logEvent({event_name: 'selected-editor-pane'});
}
});

createdPane.onShown.addListener(() => {
bridge.emit('extensionEditorPaneShown');
});
createdPane.onHidden.addListener(() => {
bridge.emit('extensionEditorPaneHidden');
});
});
}

function performInTabNavigationCleanup() {
// Potentially, if react hasn't loaded yet and user performs in-tab navigation
clearReactPollingInstance();
Expand Down Expand Up @@ -356,6 +448,7 @@ function mountReactDevTools() {

createComponentsPanel();
createProfilerPanel();
createSourcesEditorPanel();
}

let reactPollingInstance = null;
Expand Down Expand Up @@ -394,13 +487,17 @@ let profilingData = null;

let componentsPanel = null;
let profilerPanel = null;
let editorPane = null;
let componentsPortalContainer = null;
let profilerPortalContainer = null;
let editorPortalContainer = null;

let mostRecentOverrideTab = null;
let render = null;
let root = null;

let currentSelectedSource: null | SourceSelection = null;

let port = null;

// In case when multiple navigation events emitted in a short period of time
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.ButtonLabel {
padding-left: 1.5rem;
margin-left: -1rem;
user-select: none;
flex: 1 0 auto;
text-align: center;
}
20 changes: 20 additions & 0 deletions packages/react-devtools-shared/src/devtools/views/ButtonLabel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import * as React from 'react';

import styles from './ButtonLabel.css';

type Props = {
children: React$Node,
};

export default function ButtonLabel({children}: Props): React.Node {
return <span className={styles.ButtonLabel}>{children}</span>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import * as React from 'react';
Expand All @@ -13,58 +14,14 @@ import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';

import type {ReactFunctionLocation} from 'shared/ReactTypes';

import {checkConditions} from '../Editor/utils';

type Props = {
editorURL: string,
source: ReactFunctionLocation,
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
};

function checkConditions(
editorURL: string,
source: ReactFunctionLocation,
): {url: URL | null, shouldDisableButton: boolean} {
try {
const url = new URL(editorURL);

let [, sourceURL, ,] = source;

// Check if sourceURL is a correct URL, which has a protocol specified
if (sourceURL.includes('://')) {
if (!__IS_INTERNAL_VERSION__) {
// In this case, we can't really determine the path to a file, disable a button
return {url: null, shouldDisableButton: true};
} else {
const endOfSourceMapURLPattern = '.js/';
const endOfSourceMapURLIndex = sourceURL.lastIndexOf(
endOfSourceMapURLPattern,
);

if (endOfSourceMapURLIndex === -1) {
return {url: null, shouldDisableButton: true};
} else {
sourceURL = sourceURL.slice(
endOfSourceMapURLIndex + endOfSourceMapURLPattern.length,
sourceURL.length,
);
}
}
}

const lineNumberAsString = String(source.line);

url.href = url.href
.replace('{path}', sourceURL)
.replace('{line}', lineNumberAsString)
.replace('%7Bpath%7D', sourceURL)
.replace('%7Bline%7D', lineNumberAsString);

return {url, shouldDisableButton: false};
} catch (e) {
// User has provided incorrect editor url
return {url: null, shouldDisableButton: true};
}
}

function OpenInEditorButton({
editorURL,
source,
Expand Down
14 changes: 13 additions & 1 deletion packages/react-devtools-shared/src/devtools/views/DevTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import Components from './Components/Components';
import Profiler from './Profiler/Profiler';
import TabBar from './TabBar';
import EditorPane from './Editor/EditorPane';
import {SettingsContextController} from './Settings/SettingsContext';
import {TreeContextController} from './Components/TreeContext';
import ViewElementSourceContext from './Components/ViewElementSourceContext';
Expand Down Expand Up @@ -51,6 +52,7 @@ import type {HookNamesModuleLoaderFunction} from 'react-devtools-shared/src/devt
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {SourceSelection} from './Editor/EditorPane';

export type TabID = 'components' | 'profiler';

Expand Down Expand Up @@ -97,6 +99,8 @@ export type Props = {
// but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels.
componentsPortalContainer?: Element,
profilerPortalContainer?: Element,
editorPortalContainer?: Element,
currentSelectedSource?: null | SourceSelection,

// Loads and parses source maps for function components
// and extracts hook "names" based on the variables the hook return values get assigned to.
Expand Down Expand Up @@ -126,12 +130,14 @@ export default function DevTools({
browserTheme = 'light',
canViewElementSourceFunction,
componentsPortalContainer,
profilerPortalContainer,
editorPortalContainer,
currentSelectedSource,
defaultTab = 'components',
enabledInspectedElementContextMenu = false,
fetchFileWithCaching,
hookNamesModuleLoaderFunction,
overrideTab,
profilerPortalContainer,
showTabBar = false,
store,
warnIfLegacyBackendDetected = false,
Expand Down Expand Up @@ -316,6 +322,12 @@ export default function DevTools({
/>
</div>
</div>
{editorPortalContainer ? (
<EditorPane
selectedSource={currentSelectedSource}
portalContainer={editorPortalContainer}
/>
) : null}
</ThemeProvider>
</InspectedElementContextController>
</TimelineContextController>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.EditorPane {
position: relative;
display: flex;
flex-direction: row;
background-color: var(--color-background);
color: var(--color-text);
font-family: var(--font-family-sans);
align-items: center;
padding: 0.5rem;
}

.EditorPane, .EditorPane * {
box-sizing: border-box;
-webkit-font-smoothing: var(--font-smoothing);
}

.VRule {
height: 20px;
width: 1px;
flex: 0 0 1px;
margin: 0 0.5rem;
background-color: var(--color-border);
}

.WideButton {
flex: 1 0 auto;
display: flex;
}
Loading
Loading