Skip to content

Commit

Permalink
feat[devtools]: symbolicate source for inspected element
Browse files Browse the repository at this point in the history
  • Loading branch information
hoxyq committed Feb 29, 2024
1 parent c9b900b commit 4f61d6f
Show file tree
Hide file tree
Showing 21 changed files with 640 additions and 258 deletions.
7 changes: 7 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,13 @@ module.exports = {
__IS_CHROME__: 'readonly',
__IS_FIREFOX__: 'readonly',
__IS_EDGE__: 'readonly',
__IS_INTERNAL_VERSION__: 'readonly',
},
},
{
files: ['packages/react-devtools-shared/**/*.js'],
globals: {
__IS_INTERNAL_VERSION__: 'readonly',
},
},
],
Expand Down
2 changes: 1 addition & 1 deletion packages/react-debug-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@
"react": "^17.0.0"
},
"dependencies": {
"error-stack-parser": "^2.0.2"
"error-stack-parser": "^2.1.4"
}
}
51 changes: 35 additions & 16 deletions packages/react-devtools-core/src/standalone.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
import {localStorageSetItem} from 'react-devtools-shared/src/storage';

import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
import type {Source} from 'react-devtools-shared/src/shared/types';

installHook(window);

Expand Down Expand Up @@ -127,36 +127,55 @@ function reload() {
store: ((store: any): Store),
warnIfLegacyBackendDetected: true,
viewElementSourceFunction,
fetchFileWithCaching,
}),
);
}, 100);
}

const resourceCache: Map<string, string> = new Map();

// As a potential improvement, this should be done from the backend of RDT.
// Browser extension is doing this via exchanging messages
// between devtools_page and dedicated content script for it, see `fetchFileWithCaching.js`.
async function fetchFileWithCaching(url: string) {
if (resourceCache.has(url)) {
return Promise.resolve(resourceCache.get(url));
}

return fetch(url)
.then(data => data.text())
.then(content => {
resourceCache.set(url, content);

return content;
});
}

function canViewElementSourceFunction(
inspectedElement: InspectedElement,
_source: Source,
symbolicatedSource: Source | null,
): boolean {
if (
inspectedElement.canViewSource === false ||
inspectedElement.source === null
) {
if (symbolicatedSource == null) {
return false;
}

const {source} = inspectedElement;

return doesFilePathExist(source.sourceURL, projectRoots);
return doesFilePathExist(symbolicatedSource.sourceURL, projectRoots);
}

function viewElementSourceFunction(
id: number,
inspectedElement: InspectedElement,
_source: Source,
symbolicatedSource: Source | null,
): void {
const {source} = inspectedElement;
if (source !== null) {
launchEditor(source.sourceURL, source.line, projectRoots);
} else {
log.error('Cannot inspect element', id);
if (symbolicatedSource == null) {
return;
}

launchEditor(
symbolicatedSource.sourceURL,
symbolicatedSource.line,
projectRoots,
);
}

function onDisconnected() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,36 @@ const fetchFromPage = async (url, resolve, reject) => {
});
};

// Fetching files from the extension won't make use of the network cache
// for resources that have already been loaded by the page.
// This helper function allows the extension to request files to be fetched
// by the content script (running in the page) to increase the likelihood of a cache hit.
const fetchFileWithCaching = url => {
// 1. Check if resource is available via chrome.devtools.inspectedWindow.getResources
// 2. Check if resource was loaded previously and available in network cache via chrome.devtools.network.getHAR
// 3. Fallback to fetching directly from the page context (from backend)
async function fetchFileWithCaching(url: string): Promise<string> {
if (__IS_CHROME__ || __IS_EDGE__) {
const resources = await new Promise(resolve =>
chrome.devtools.inspectedWindow.getResources(r => resolve(r)),
);

// This is a hacky way to make it work for Next, since their URLs are not normalized
const normalizedReferenceURL = url.replace('/./', '/');
const resource = resources.find(r => r.url === normalizedReferenceURL);

if (resource != null) {
const content = await new Promise(resolve =>
resource.getContent(fetchedContent => resolve(fetchedContent)),
);

if (content) {
return content;
}
}
}

return new Promise((resolve, reject) => {
// Try fetching from the Network cache first.
// If DevTools was opened after the page started loading, we may have missed some requests.
// So fall back to a fetch() from the page and hope we get a cached response that way.
fetchFromNetworkCache(url, resolve, reject);
});
};
}

export default fetchFileWithCaching;
39 changes: 7 additions & 32 deletions packages/react-devtools-extensions/src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,34 +128,12 @@ function createBridgeAndStore() {
}
};

const viewElementSourceFunction = id => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID != null) {
// Ask the renderer interface to determine the component function,
// and store it as a global variable on the window
bridge.send('viewElementSource', {id, rendererID});
const viewElementSourceFunction = (source, symbolicatedSource) => {
const {sourceURL, line, column} = symbolicatedSource
? symbolicatedSource
: source;

setTimeout(() => {
// Ask Chrome to display the location of the component function,
// or a render method if it is a Class (ideally Class instance, not type)
// assuming the renderer found one.
chrome.devtools.inspectedWindow.eval(`
if (window.$type != null) {
if (
window.$type &&
window.$type.prototype &&
window.$type.prototype.isReactComponent
) {
// inspect Component.render, not constructor
inspect(window.$type.prototype.render);
} else {
// inspect Functional Component
inspect(window.$type);
}
}
`);
}, 100);
}
chrome.devtools.panels.openResource(sourceURL, line, column);
};

// TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
Expand Down Expand Up @@ -183,17 +161,14 @@ function createBridgeAndStore() {
store,
warnIfUnsupportedVersionDetected: true,
viewAttributeSourceFunction,
// Firefox doesn't support chrome.devtools.panels.openResource yet
canViewElementSourceFunction: () => __IS_CHROME__ || __IS_EDGE__,
viewElementSourceFunction,
viewUrlSourceFunction,
}),
);
};
}

const viewUrlSourceFunction = (url, line, col) => {
chrome.devtools.panels.openResource(url, line, col);
};

function ensureInitialHTMLIsCleared(container) {
if (container._hasInitialHTMLBeenCleared) {
return;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-devtools-extensions/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const LOGGING_URL = process.env.LOGGING_URL || null;
const IS_CHROME = process.env.IS_CHROME === 'true';
const IS_FIREFOX = process.env.IS_FIREFOX === 'true';
const IS_EDGE = process.env.IS_EDGE === 'true';
const IS_INTERNAL_VERSION = process.env.FEATURE_FLAG_TARGET === 'extension-fb';

const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';

Expand Down Expand Up @@ -119,6 +120,7 @@ module.exports = {
__IS_CHROME__: IS_CHROME,
__IS_FIREFOX__: IS_FIREFOX,
__IS_EDGE__: IS_EDGE,
__IS_INTERNAL_VERSION__: IS_INTERNAL_VERSION,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
Expand Down
2 changes: 1 addition & 1 deletion packages/react-devtools-shared/src/backend/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ export function parseSourceFromComponentStack(
const frames = componentStack.split('\n');
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const frame of frames) {
const openingBracketIndex = frame.lastIndexOf('(');
const openingBracketIndex = frame.indexOf('(');
if (openingBracketIndex === -1) continue;
const closingBracketIndex = frame.lastIndexOf(')');
if (
Expand Down
4 changes: 3 additions & 1 deletion packages/react-devtools-shared/src/backendAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,9 @@ export function convertInspectedElementBackendToFrontend(
rendererPackageName,
rendererVersion,
rootType,
source,
// Previous backend implementations (<= 5.0.1) have a different interface for Source, with fileName.
// This gates the source features for only compatible backends: >= 5.0.2
source: source && source.sourceURL ? source : null,
type,
owners:
owners === null
Expand Down
Loading

0 comments on commit 4f61d6f

Please sign in to comment.