Skip to content
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

[WIP]: Injecting parse function from extension to devtools #91

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Expand Up @@ -317,12 +317,19 @@ const Dispatcher: DispatcherType = {

// Inspect

type HookSource = {
lineNumber: number | null,
columnNumber: number | null,
fileName: string | null,
functionName: string | null,
}
export type HooksNode = {
id: number | null,
isStateEditable: boolean,
name: string,
value: mixed,
subHooks: Array<HooksNode>,
hookSource: HookSource,
...
};
export type HooksTree = Array<HooksNode>;
Expand Down Expand Up @@ -499,6 +506,12 @@ function buildTree(rootStack, readHookLog): HooksTree {
name: parseCustomHookName(stack[j - 1].functionName),
value: undefined,
subHooks: children,
hookSource: {
lineNumber: stack[j-1].lineNumber,
columnNumber: stack[j-1].columnNumber,
functionName: stack[j-1].functionName,
fileName: stack[j-1].fileName
}
});
stackOfChildren.push(levelChildren);
levelChildren = children;
Expand All @@ -516,13 +529,22 @@ function buildTree(rootStack, readHookLog): HooksTree {

// For the time being, only State and Reducer hooks support runtime overrides.
const isStateEditable = primitive === 'Reducer' || primitive === 'State';
const hookSource: HookSource = {lineNumber: null, functionName: null, fileName: null, columnNumber: null}
if (stack && stack.length === 1) {
const stackFrame = stack[0]
hookSource.lineNumber = stackFrame.lineNumber
hookSource.functionName = stackFrame.functionName
hookSource.fileName = stackFrame.fileName
hookSource.columnNumber = stackFrame.columnNumber
}

levelChildren.push({
id,
isStateEditable,
name: primitive,
value: hook.value,
subHooks: [],
hookSource
});
}

Expand Down
14 changes: 14 additions & 0 deletions packages/react-devtools-extensions/src/main.js
Expand Up @@ -202,6 +202,19 @@ function createPanelIfReactLoaded() {
}
};

function injectHookVariableNamesFunction(id, hookLog) {
// TODO Load source and source map, parse AST, mix in real names.
const namedHookLogPromise = new Promise((resolve, reject) => {
setTimeout(() => {
const newHookLog = hookLog.map(hook => {
return {...hook, name: 'hook-variable-name'};
});
resolve(newHookLog);
}, 2000);
});
return namedHookLogPromise;
}

root = createRoot(document.createElement('div'));

render = (overrideTab = mostRecentOverrideTab) => {
Expand All @@ -220,6 +233,7 @@ function createPanelIfReactLoaded() {
warnIfUnsupportedVersionDetected: true,
viewAttributeSourceFunction,
viewElementSourceFunction,
injectHookVariableNamesFunction,
}),
);
};
Expand Down
@@ -0,0 +1,15 @@
// @flow

import {createContext} from 'react';
import type {InjectHookVariableNamesFunction} from '../DevTools';

export type Context = {|
injectHookVariableNamesFunction: InjectHookVariableNamesFunction | null,
|};

const InjectHookVariableNamesFunctionContext = createContext<Context>(
((null: any): Context),
);
InjectHookVariableNamesFunctionContext.displayName =
'InjectHookVariableNamesFunctionContext';
export default InjectHookVariableNamesFunctionContext;
Expand Up @@ -52,6 +52,7 @@ export default function InspectedElementWrapper(_: Props) {

const inspectedElement =
inspectedElementID != null ? getInspectedElement(inspectedElementID) : null;
console.log('<InspectedElement> inspectedElement:', inspectedElement);

const highlightElement = useCallback(() => {
if (element !== null && inspectedElementID !== null) {
Expand Down
Expand Up @@ -22,6 +22,7 @@ import {createResource} from '../../cache';
import {BridgeContext, StoreContext} from '../context';
import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
import {TreeStateContext} from './TreeContext';
import InjectHookVariableNamesFunctionContext from './InjectHookVariableNamesFunctionContext';
import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';

import type {
Expand All @@ -33,6 +34,10 @@ import type {
Element,
InspectedElement as InspectedElementFrontend,
} from 'react-devtools-shared/src/devtools/views/Components/types';
import type {
InjectHookVariableNamesFunction,
HookLog,
} from 'react-devtools-shared/src/devtools/views/DevTools';
import type {Resource, Thenable} from '../../cache';

export type StoreAsGlobal = (id: number, path: Array<string | number>) => void;
Expand Down Expand Up @@ -70,7 +75,7 @@ type InProgressRequest = {|
|};

const inProgressRequests: WeakMap<Element, InProgressRequest> = new WeakMap();
const resource: Resource<
const inspectedElementResource: Resource<
Element,
Element,
InspectedElementFrontend,
Expand All @@ -94,14 +99,38 @@ const resource: Resource<
{useWeakMap: true},
);

type NamedHooksResourceKey = [
Element,
InspectedElementFrontend,
InjectHookVariableNamesFunction,
];

const namedHooksResource: Resource<
NamedHooksResourceKey,
Element,
HookLog,
> = createResource(
(key: NamedHooksResourceKey) => {
// eslint-disable-next-line no-unused-vars
const [element, inspectedElement, injectHookVariableNamesFunction] = key;
const {id, hooks} = inspectedElement;
return injectHookVariableNamesFunction(id, hooks);
},
// Key the WeakMap on the Element (in the Store) since it's stable.
(key: NamedHooksResourceKey) => key[0],
{useWeakMap: true},
);

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

function InspectedElementContextController({children}: Props) {
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);

const {injectHookVariableNamesFunction} = useContext(
InjectHookVariableNamesFunctionContext,
);
const storeAsGlobalCount = useRef(1);

// Ask the backend to store the value at the specified path as a global variable.
Expand Down Expand Up @@ -146,7 +175,22 @@ function InspectedElementContextController({children}: Props) {
(id: number) => {
const element = store.getElementByID(id);
if (element !== null) {
return resource.read(element);
const inspectedElement = inspectedElementResource.read(element);

// Mix additional hook name data into the inspected resource if we can.
if (
inspectedElement.hooks !== null &&
injectHookVariableNamesFunction !== null
) {
inspectedElement.hooks = namedHooksResource.read([
element,
inspectedElement,
injectHookVariableNamesFunction,
]);
inspectedElementResource.write(element, inspectedElement);
}

return inspectedElement;
} else {
return null;
}
Expand Down Expand Up @@ -186,7 +230,7 @@ function InspectedElementContextController({children}: Props) {

fillInPath(inspectedElement, data.value, data.path, value);

resource.write(element, inspectedElement);
inspectedElementResource.write(element, inspectedElement);

// Schedule update with React if the currently-selected element has been invalidated.
if (id === selectedElementID) {
Expand Down Expand Up @@ -269,7 +313,7 @@ function InspectedElementContextController({children}: Props) {
setCurrentlyInspectedElement(inspectedElement);
});
} else {
resource.write(element, inspectedElement);
inspectedElementResource.write(element, inspectedElement);

// Schedule update with React if the currently-selected element has been invalidated.
if (id === selectedElementID) {
Expand Down
90 changes: 58 additions & 32 deletions packages/react-devtools-shared/src/devtools/views/DevTools.js
Expand Up @@ -22,6 +22,7 @@ import TabBar from './TabBar';
import {SettingsContextController} from './Settings/SettingsContext';
import {TreeContextController} from './Components/TreeContext';
import ViewElementSourceContext from './Components/ViewElementSourceContext';
import InjectHookVariableNamesFunctionContext from './Components/InjectHookVariableNamesFunctionContext';
import {ProfilerContextController} from './Profiler/ProfilerContext';
import {ModalDialogContextController} from './ModalDialog';
import ReactLogo from './ReactLogo';
Expand All @@ -35,13 +36,26 @@ import './root.css';

import type {InspectedElement} from 'react-devtools-shared/src/devtools/views/Components/types';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {Thenable} from '../cache';

export type BrowserTheme = 'dark' | 'light';
export type TabID = 'components' | 'profiler';
type HookLogEntry = {
primitive: string,
stackError: Error,
value: mixed,
...
};
export type HookLog = Array<HookLogEntry> | null;

export type ViewElementSource = (
id: number,
inspectedElement: InspectedElement,
) => void;
export type InjectHookVariableNamesFunction = (
id: number,
hookLog: HookLog,
) => Thenable<HookLog>;
export type ViewAttributeSource = (
id: number,
path: Array<string | number>,
Expand All @@ -62,6 +76,8 @@ export type Props = {|
warnIfUnsupportedVersionDetected?: boolean,
viewAttributeSourceFunction?: ?ViewAttributeSource,
viewElementSourceFunction?: ?ViewElementSource,
// Passing HookVariableNamesFunction as a Prop
injectHookVariableNamesFunction?: ?InjectHookVariableNamesFunction,

// This property is used only by the web extension target.
// The built-in tab UI is hidden in that case, in favor of the browser's own panel tabs.
Expand Down Expand Up @@ -106,6 +122,7 @@ export default function DevTools({
warnIfUnsupportedVersionDetected = false,
viewAttributeSourceFunction,
viewElementSourceFunction,
injectHookVariableNamesFunction,
}: Props) {
const [currentTab, setTab] = useLocalStorage<TabID>(
'React::DevTools::defaultTab',
Expand All @@ -118,6 +135,13 @@ export default function DevTools({
tab = overrideTab;
}

const injectHookVariableNames = useMemo(
() => ({
injectHookVariableNamesFunction: injectHookVariableNamesFunction || null,
}),
[injectHookVariableNamesFunction],
);

const viewElementSource = useMemo(
() => ({
canViewElementSourceFunction: canViewElementSourceFunction || null,
Expand Down Expand Up @@ -179,7 +203,6 @@ export default function DevTools({
}
};
}, [bridge]);

return (
<BridgeContext.Provider value={bridge}>
<StoreContext.Provider value={store}>
Expand All @@ -190,40 +213,43 @@ export default function DevTools({
componentsPortalContainer={componentsPortalContainer}
profilerPortalContainer={profilerPortalContainer}>
<ViewElementSourceContext.Provider value={viewElementSource}>
<TreeContextController>
<ProfilerContextController>
<div className={styles.DevTools} ref={devToolsRef}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={setTab}
tabs={tabs}
type="navigation"
<InjectHookVariableNamesFunctionContext.Provider
value={injectHookVariableNames}>
<TreeContextController>
<ProfilerContextController>
<div className={styles.DevTools} ref={devToolsRef}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={setTab}
tabs={tabs}
type="navigation"
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={componentsPortalContainer}
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={componentsPortalContainer}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler portalContainer={profilerPortalContainer} />
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler portalContainer={profilerPortalContainer} />
</div>
</div>
</div>
</ProfilerContextController>
</TreeContextController>
</ProfilerContextController>
</TreeContextController>
</InjectHookVariableNamesFunctionContext.Provider>
</ViewElementSourceContext.Provider>
</SettingsContextController>
{warnIfLegacyBackendDetected && <WarnIfLegacyBackendDetected />}
Expand Down