mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
329 lines
10 KiB
JavaScript
329 lines
10 KiB
JavaScript
/* global chrome */
|
|
|
|
import {createElement} from 'react';
|
|
import {unstable_createRoot as createRoot, flushSync} from 'react-dom';
|
|
import Bridge from 'react-devtools-shared/src/bridge';
|
|
import Store from 'react-devtools-shared/src/devtools/store';
|
|
import {
|
|
createViewElementSource,
|
|
getBrowserName,
|
|
getBrowserTheme,
|
|
} from './utils';
|
|
import {
|
|
getSavedComponentFilters,
|
|
getAppendComponentStack,
|
|
} from 'react-devtools-shared/src/utils';
|
|
import {
|
|
localStorageGetItem,
|
|
localStorageRemoveItem,
|
|
localStorageSetItem,
|
|
} from 'react-devtools-shared/src/storage';
|
|
import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
|
|
|
|
const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
|
|
'React::DevTools::supportsProfiling';
|
|
|
|
const isChrome = getBrowserName() === 'Chrome';
|
|
|
|
let panelCreated = false;
|
|
|
|
// The renderer interface can't read saved component filters directly,
|
|
// because they are stored in localStorage within the context of the extension.
|
|
// Instead it relies on the extension to pass filters through.
|
|
function syncSavedPreferences() {
|
|
const componentFilters = getSavedComponentFilters();
|
|
chrome.devtools.inspectedWindow.eval(
|
|
`window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
|
|
componentFilters,
|
|
)};`,
|
|
);
|
|
|
|
const appendComponentStack = getAppendComponentStack();
|
|
chrome.devtools.inspectedWindow.eval(
|
|
`window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
|
|
appendComponentStack,
|
|
)};`,
|
|
);
|
|
}
|
|
|
|
syncSavedPreferences();
|
|
|
|
function createPanelIfReactLoaded() {
|
|
if (panelCreated) {
|
|
return;
|
|
}
|
|
|
|
chrome.devtools.inspectedWindow.eval(
|
|
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
|
|
function(pageHasReact, error) {
|
|
if (!pageHasReact || panelCreated) {
|
|
return;
|
|
}
|
|
|
|
panelCreated = true;
|
|
|
|
clearInterval(loadCheckInterval);
|
|
|
|
let bridge = null;
|
|
let store = null;
|
|
|
|
let profilingData = null;
|
|
|
|
let componentsPortalContainer = null;
|
|
let profilerPortalContainer = null;
|
|
|
|
let cloneStyleTags = null;
|
|
let mostRecentOverrideTab = null;
|
|
let render = null;
|
|
let root = null;
|
|
|
|
const tabId = chrome.devtools.inspectedWindow.tabId;
|
|
|
|
function initBridgeAndStore() {
|
|
const port = chrome.runtime.connect({
|
|
name: '' + tabId,
|
|
});
|
|
// Looks like `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation,
|
|
// so it makes no sense to handle it here.
|
|
|
|
bridge = new Bridge({
|
|
listen(fn) {
|
|
const listener = message => fn(message);
|
|
// Store the reference so that we unsubscribe from the same object.
|
|
const portOnMessage = port.onMessage;
|
|
portOnMessage.addListener(listener);
|
|
return () => {
|
|
portOnMessage.removeListener(listener);
|
|
};
|
|
},
|
|
send(event: string, payload: any, transferable?: Array<any>) {
|
|
port.postMessage({event, payload}, transferable);
|
|
},
|
|
});
|
|
bridge.addListener('reloadAppForProfiling', () => {
|
|
localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
|
|
chrome.devtools.inspectedWindow.eval('window.location.reload();');
|
|
});
|
|
bridge.addListener('syncSelectionToNativeElementsPanel', () => {
|
|
setBrowserSelectionFromReact();
|
|
});
|
|
|
|
// This flag lets us tip the Store off early that we expect to be profiling.
|
|
// This avoids flashing a temporary "Profiling not supported" message in the Profiler tab,
|
|
// after a user has clicked the "reload and profile" button.
|
|
let isProfiling = false;
|
|
let supportsProfiling = false;
|
|
if (
|
|
localStorageGetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY) === 'true'
|
|
) {
|
|
supportsProfiling = true;
|
|
isProfiling = true;
|
|
localStorageRemoveItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY);
|
|
}
|
|
|
|
if (store !== null) {
|
|
profilingData = store.profilerStore.profilingData;
|
|
}
|
|
|
|
store = new Store(bridge, {
|
|
isProfiling,
|
|
supportsReloadAndProfile: isChrome,
|
|
supportsProfiling,
|
|
});
|
|
store.profilerStore.profilingData = profilingData;
|
|
|
|
// Initialize the backend only once the Store has been initialized.
|
|
// Otherwise the Store may miss important initial tree op codes.
|
|
chrome.devtools.inspectedWindow.eval(
|
|
`window.postMessage({ source: 'react-devtools-inject-backend' }, window.origin);`,
|
|
function(response, evalError) {
|
|
if (evalError) {
|
|
console.error(evalError);
|
|
}
|
|
},
|
|
);
|
|
|
|
const viewElementSourceFunction = createViewElementSource(
|
|
bridge,
|
|
store,
|
|
);
|
|
|
|
root = createRoot(document.createElement('div'));
|
|
|
|
render = (overrideTab = mostRecentOverrideTab) => {
|
|
mostRecentOverrideTab = overrideTab;
|
|
|
|
root.render(
|
|
createElement(DevTools, {
|
|
bridge,
|
|
browserTheme: getBrowserTheme(),
|
|
componentsPortalContainer,
|
|
overrideTab,
|
|
profilerPortalContainer,
|
|
showTabBar: false,
|
|
warnIfUnsupportedVersionDetected: true,
|
|
store,
|
|
viewElementSourceFunction,
|
|
}),
|
|
);
|
|
};
|
|
|
|
render();
|
|
}
|
|
|
|
cloneStyleTags = () => {
|
|
const linkTags = [];
|
|
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
|
for (let linkTag of document.getElementsByTagName('link')) {
|
|
if (linkTag.rel === 'stylesheet') {
|
|
const newLinkTag = document.createElement('link');
|
|
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
|
for (let attribute of linkTag.attributes) {
|
|
newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue);
|
|
}
|
|
linkTags.push(newLinkTag);
|
|
}
|
|
}
|
|
return linkTags;
|
|
};
|
|
|
|
initBridgeAndStore();
|
|
|
|
function ensureInitialHTMLIsCleared(container) {
|
|
if (container._hasInitialHTMLBeenCleared) {
|
|
return;
|
|
}
|
|
container.innerHTML = '';
|
|
container._hasInitialHTMLBeenCleared = true;
|
|
}
|
|
|
|
function setBrowserSelectionFromReact() {
|
|
// This is currently only called on demand when you press "view DOM".
|
|
// In the future, if Chrome adds an inspect() that doesn't switch tabs,
|
|
// we could make this happen automatically when you select another component.
|
|
chrome.devtools.inspectedWindow.eval(
|
|
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
|
|
'(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
|
|
'false',
|
|
(didSelectionChange, evalError) => {
|
|
if (evalError) {
|
|
console.error(evalError);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
function setReactSelectionFromBrowser() {
|
|
// When the user chooses a different node in the browser Elements tab,
|
|
// copy it over to the hook object so that we can sync the selection.
|
|
chrome.devtools.inspectedWindow.eval(
|
|
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
|
|
'(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
|
|
'false',
|
|
(didSelectionChange, evalError) => {
|
|
if (evalError) {
|
|
console.error(evalError);
|
|
} else if (didSelectionChange) {
|
|
// Remember to sync the selection next time we show Components tab.
|
|
needsToSyncElementSelection = true;
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
setReactSelectionFromBrowser();
|
|
chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
|
|
setReactSelectionFromBrowser();
|
|
});
|
|
|
|
let currentPanel = null;
|
|
let needsToSyncElementSelection = false;
|
|
|
|
chrome.devtools.panels.create(
|
|
isChrome ? '⚛ Components' : 'Components',
|
|
'',
|
|
'panel.html',
|
|
extensionPanel => {
|
|
extensionPanel.onShown.addListener(panel => {
|
|
if (needsToSyncElementSelection) {
|
|
needsToSyncElementSelection = false;
|
|
bridge.send('syncSelectionFromNativeElementsPanel');
|
|
}
|
|
|
|
if (currentPanel === panel) {
|
|
return;
|
|
}
|
|
|
|
currentPanel = panel;
|
|
componentsPortalContainer = panel.container;
|
|
|
|
if (componentsPortalContainer != null) {
|
|
ensureInitialHTMLIsCleared(componentsPortalContainer);
|
|
render('components');
|
|
panel.injectStyles(cloneStyleTags);
|
|
}
|
|
});
|
|
extensionPanel.onHidden.addListener(panel => {
|
|
// TODO: Stop highlighting and stuff.
|
|
});
|
|
},
|
|
);
|
|
|
|
chrome.devtools.panels.create(
|
|
isChrome ? '⚛ Profiler' : 'Profiler',
|
|
'',
|
|
'panel.html',
|
|
extensionPanel => {
|
|
extensionPanel.onShown.addListener(panel => {
|
|
if (currentPanel === panel) {
|
|
return;
|
|
}
|
|
|
|
currentPanel = panel;
|
|
profilerPortalContainer = panel.container;
|
|
|
|
if (profilerPortalContainer != null) {
|
|
ensureInitialHTMLIsCleared(profilerPortalContainer);
|
|
render('profiler');
|
|
panel.injectStyles(cloneStyleTags);
|
|
}
|
|
});
|
|
},
|
|
);
|
|
|
|
chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
|
|
|
|
// Re-initialize DevTools panel when a new page is loaded.
|
|
chrome.devtools.network.onNavigated.addListener(function onNavigated() {
|
|
// Re-initialize saved filters on navigation,
|
|
// since global values stored on window get reset in this case.
|
|
syncSavedPreferences();
|
|
|
|
// It's easiest to recreate the DevTools panel (to clean up potential stale state).
|
|
// We can revisit this in the future as a small optimization.
|
|
flushSync(() => {
|
|
root.unmount(() => {
|
|
initBridgeAndStore();
|
|
});
|
|
});
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
// Load (or reload) the DevTools extension when the user navigates to a new page.
|
|
function checkPageForReact() {
|
|
syncSavedPreferences();
|
|
createPanelIfReactLoaded();
|
|
}
|
|
|
|
chrome.devtools.network.onNavigated.addListener(checkPageForReact);
|
|
|
|
// Check to see if React has loaded once per second in case React is added
|
|
// after page load
|
|
const loadCheckInterval = setInterval(function() {
|
|
createPanelIfReactLoaded();
|
|
}, 1000);
|
|
|
|
createPanelIfReactLoaded();
|