mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
feat[devtools]: display Forget badge for the relevant components (#27709)
Adds `Forget` badge to all relevant components. Changes: - If component is compiled with Forget and using a built-in `useMemoCache` hook, it will have a `Forget` badge next to its display name in: - components tree - inspected element view - owners list - Such badges are indexable, so Forget components can be searched using search bar. Fixes: - Displaying the badges for owners list inside the inspected component view Implementation: - React DevTools backend is responsible for identifying if component is compiled with Forget, based on `fiber.updateQueue.memoCache`. It will wrap component's display name with `Forget(...)` prefix before passing operations to the frontend. On the frontend side, we will parse the display name and strip Forget prefix, marking the corresponding element by setting `compiledWithForget` field. Almost the same logic is currently used for HOC display names.
This commit is contained in:
@@ -2765,6 +2765,7 @@ describe('InspectedElement', () => {
|
||||
expect(inspectedElement.owners).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"compiledWithForget": false,
|
||||
"displayName": "Child",
|
||||
"hocDisplayNames": null,
|
||||
"id": 3,
|
||||
@@ -2772,6 +2773,7 @@ describe('InspectedElement', () => {
|
||||
"type": 5,
|
||||
},
|
||||
{
|
||||
"compiledWithForget": false,
|
||||
"displayName": "App",
|
||||
"hocDisplayNames": null,
|
||||
"id": 2,
|
||||
|
||||
@@ -968,6 +968,7 @@ describe('ProfilingCache', () => {
|
||||
"timestamp": 0,
|
||||
"updaters": [
|
||||
{
|
||||
"compiledWithForget": false,
|
||||
"displayName": "render()",
|
||||
"hocDisplayNames": null,
|
||||
"id": 1,
|
||||
@@ -1010,6 +1011,7 @@ describe('ProfilingCache', () => {
|
||||
"timestamp": 0,
|
||||
"updaters": [
|
||||
{
|
||||
"compiledWithForget": false,
|
||||
"displayName": "render()",
|
||||
"hocDisplayNames": null,
|
||||
"id": 1,
|
||||
|
||||
@@ -424,7 +424,10 @@ export function getInternalReactConstants(version: string): {
|
||||
}
|
||||
|
||||
// NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
|
||||
function getDisplayNameForFiber(fiber: Fiber): string | null {
|
||||
function getDisplayNameForFiber(
|
||||
fiber: Fiber,
|
||||
shouldSkipForgetCheck: boolean = false,
|
||||
): string | null {
|
||||
const {elementType, type, tag} = fiber;
|
||||
|
||||
let resolvedType = type;
|
||||
@@ -433,6 +436,18 @@ export function getInternalReactConstants(version: string): {
|
||||
}
|
||||
|
||||
let resolvedContext: any = null;
|
||||
// $FlowFixMe[incompatible-type] fiber.updateQueue is mixed
|
||||
if (!shouldSkipForgetCheck && fiber.updateQueue?.memoCache != null) {
|
||||
const displayNameWithoutForgetWrapper = getDisplayNameForFiber(
|
||||
fiber,
|
||||
true,
|
||||
);
|
||||
if (displayNameWithoutForgetWrapper == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `Forget(${displayNameWithoutForgetWrapper})`;
|
||||
}
|
||||
|
||||
switch (tag) {
|
||||
case CacheComponent:
|
||||
|
||||
14
packages/react-devtools-shared/src/backendAPI.js
vendored
14
packages/react-devtools-shared/src/backendAPI.js
vendored
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
|
||||
import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
|
||||
import {backendToFrontendSerializedElementMapper} from 'react-devtools-shared/src/utils';
|
||||
import Store from 'react-devtools-shared/src/devtools/store';
|
||||
import TimeoutError from 'react-devtools-shared/src/errors/TimeoutError';
|
||||
import ElementPollingCancellationError from 'react-devtools-shared/src/errors/ElementPollingCancellationError';
|
||||
@@ -266,17 +266,7 @@ export function convertInspectedElementBackendToFrontend(
|
||||
owners:
|
||||
owners === null
|
||||
? null
|
||||
: owners.map(owner => {
|
||||
const [displayName, hocDisplayNames] = separateDisplayNameAndHOCs(
|
||||
owner.displayName,
|
||||
owner.type,
|
||||
);
|
||||
return {
|
||||
...owner,
|
||||
displayName,
|
||||
hocDisplayNames,
|
||||
};
|
||||
}),
|
||||
: owners.map(backendToFrontendSerializedElementMapper),
|
||||
context: hydrateHelper(context),
|
||||
hooks: hydrateHelper(hooks),
|
||||
props: hydrateHelper(props),
|
||||
|
||||
@@ -77,6 +77,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
|
||||
'--color-error-border': 'hsl(0, 100%, 92%)',
|
||||
'--color-error-text': '#ff0000',
|
||||
'--color-expand-collapse-toggle': '#777d88',
|
||||
'--color-forget-badge': '#2683E2',
|
||||
'--color-link': '#0000ff',
|
||||
'--color-modal-background': 'rgba(255, 255, 255, 0.75)',
|
||||
'--color-bridge-version-npm-background': '#eff0f1',
|
||||
@@ -221,6 +222,7 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
|
||||
'--color-error-border': '#900',
|
||||
'--color-error-text': '#f55',
|
||||
'--color-expand-collapse-toggle': '#8f949d',
|
||||
'--color-forget-badge': '#2683E2',
|
||||
'--color-link': '#61dafb',
|
||||
'--color-modal-background': 'rgba(0, 0, 0, 0.75)',
|
||||
'--color-bridge-version-npm-background': 'rgba(0, 0, 0, 0.25)',
|
||||
|
||||
@@ -25,9 +25,9 @@ import {ElementTypeRoot} from '../frontend/types';
|
||||
import {
|
||||
getSavedComponentFilters,
|
||||
setSavedComponentFilters,
|
||||
separateDisplayNameAndHOCs,
|
||||
shallowDiffers,
|
||||
utfDecodeStringWithRanges,
|
||||
parseElementDisplayNameFromBackend,
|
||||
} from '../utils';
|
||||
import {localStorageGetItem, localStorageSetItem} from '../storage';
|
||||
import {__DEBUG__} from '../constants';
|
||||
@@ -1033,6 +1033,7 @@ export default class Store extends EventEmitter<{
|
||||
parentID: 0,
|
||||
type,
|
||||
weight: 0,
|
||||
compiledWithForget: false,
|
||||
});
|
||||
|
||||
haveRootsChanged = true;
|
||||
@@ -1071,8 +1072,11 @@ export default class Store extends EventEmitter<{
|
||||
|
||||
parentElement.children.push(id);
|
||||
|
||||
const [displayNameWithoutHOCs, hocDisplayNames] =
|
||||
separateDisplayNameAndHOCs(displayName, type);
|
||||
const {
|
||||
formattedDisplayName: displayNameWithoutHOCs,
|
||||
hocDisplayNames,
|
||||
compiledWithForget,
|
||||
} = parseElementDisplayNameFromBackend(displayName, type);
|
||||
|
||||
const element: Element = {
|
||||
children: [],
|
||||
@@ -1087,6 +1091,7 @@ export default class Store extends EventEmitter<{
|
||||
parentID,
|
||||
type,
|
||||
weight: 1,
|
||||
compiledWithForget,
|
||||
};
|
||||
|
||||
this._idToElement.set(id, element);
|
||||
|
||||
@@ -9,9 +9,3 @@
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-small);
|
||||
}
|
||||
|
||||
.ExtraLabel {
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-small);
|
||||
color: var(--color-component-badge-count);
|
||||
}
|
||||
|
||||
@@ -8,36 +8,14 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Fragment} from 'react';
|
||||
import styles from './Badge.css';
|
||||
|
||||
import type {ElementType} from 'react-devtools-shared/src/frontend/types';
|
||||
import styles from './Badge.css';
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
hocDisplayNames: Array<string> | null,
|
||||
type: ElementType,
|
||||
children: React$Node,
|
||||
};
|
||||
|
||||
export default function Badge({
|
||||
className,
|
||||
hocDisplayNames,
|
||||
type,
|
||||
children,
|
||||
}: Props): React.Node {
|
||||
if (hocDisplayNames === null || hocDisplayNames.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalBadgeCount = hocDisplayNames.length;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={`${styles.Badge} ${className || ''}`}>{children}</div>
|
||||
{totalBadgeCount > 1 && (
|
||||
<div className={styles.ExtraLabel}>+{totalBadgeCount - 1}</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
export default function Badge({className = '', children}: Props): React.Node {
|
||||
return <div className={`${styles.Badge} ${className}`}>{children}</div>;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
color: var(--color-expand-collapse-toggle);
|
||||
}
|
||||
|
||||
.Badge {
|
||||
.BadgesBlock {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
import * as React from 'react';
|
||||
import {Fragment, useContext, useMemo, useState} from 'react';
|
||||
import Store from 'react-devtools-shared/src/devtools/store';
|
||||
import Badge from './Badge';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import {createRegExp} from '../utils';
|
||||
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
|
||||
import {SettingsContext} from '../Settings/SettingsContext';
|
||||
import {StoreContext} from '../context';
|
||||
import {useSubscription} from '../hooks';
|
||||
import {logEvent} from 'react-devtools-shared/src/Logger';
|
||||
import IndexableElementBadges from './IndexableElementBadges';
|
||||
import IndexableDisplayName from './IndexableDisplayName';
|
||||
|
||||
import type {ItemData} from './Tree';
|
||||
import type {Element as ElementType} from 'react-devtools-shared/src/frontend/types';
|
||||
@@ -121,7 +121,7 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
hocDisplayNames,
|
||||
isStrictModeNonCompliant,
|
||||
key,
|
||||
type,
|
||||
compiledWithForget,
|
||||
} = element;
|
||||
|
||||
// Only show strict mode non-compliance badges for top level elements.
|
||||
@@ -155,11 +155,11 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
// We must use padding rather than margin/left because of the selected background color.
|
||||
transform: `translateX(calc(${depth} * var(--indentation-size)))`,
|
||||
}}>
|
||||
{ownerID === null ? (
|
||||
{ownerID === null && (
|
||||
<ExpandCollapseToggle element={element} store={store} />
|
||||
) : null}
|
||||
)}
|
||||
|
||||
<DisplayName displayName={displayName} id={((id: any): number)} />
|
||||
<IndexableDisplayName displayName={displayName} id={id} />
|
||||
|
||||
{key && (
|
||||
<Fragment>
|
||||
@@ -174,14 +174,12 @@ export default function Element({data, index, style}: Props): React.Node {
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{hocDisplayNames !== null && hocDisplayNames.length > 0 ? (
|
||||
<Badge
|
||||
className={styles.Badge}
|
||||
hocDisplayNames={hocDisplayNames}
|
||||
type={type}>
|
||||
<DisplayName displayName={hocDisplayNames[0]} id={id} />
|
||||
</Badge>
|
||||
) : null}
|
||||
<IndexableElementBadges
|
||||
hocDisplayNames={hocDisplayNames}
|
||||
compiledWithForget={compiledWithForget}
|
||||
elementID={id}
|
||||
className={styles.BadgesBlock}
|
||||
/>
|
||||
|
||||
{showInlineWarningsAndErrors && errorCount > 0 && (
|
||||
<Icon
|
||||
@@ -262,47 +260,3 @@ function ExpandCollapseToggle({element, store}: ExpandCollapseToggleProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type DisplayNameProps = {
|
||||
displayName: string | null,
|
||||
id: number,
|
||||
};
|
||||
|
||||
function DisplayName({displayName, id}: DisplayNameProps) {
|
||||
const {searchIndex, searchResults, searchText} = useContext(TreeStateContext);
|
||||
const isSearchResult = useMemo(() => {
|
||||
return searchResults.includes(id);
|
||||
}, [id, searchResults]);
|
||||
const isCurrentResult =
|
||||
searchIndex !== null && id === searchResults[searchIndex];
|
||||
|
||||
if (!isSearchResult || displayName === null) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
const match = createRegExp(searchText).exec(displayName);
|
||||
|
||||
if (match === null) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
const startIndex = match.index;
|
||||
const stopIndex = startIndex + match[0].length;
|
||||
|
||||
const children = [];
|
||||
if (startIndex > 0) {
|
||||
children.push(<span key="begin">{displayName.slice(0, startIndex)}</span>);
|
||||
}
|
||||
children.push(
|
||||
<mark
|
||||
key="middle"
|
||||
className={isCurrentResult ? styles.CurrentHighlight : styles.Highlight}>
|
||||
{displayName.slice(startIndex, stopIndex)}
|
||||
</mark>,
|
||||
);
|
||||
if (stopIndex < displayName.length) {
|
||||
children.push(<span key="end">{displayName.slice(stopIndex)}</span>);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
.Root {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Root *:not(:first-child) {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.ExtraLabel {
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-small);
|
||||
color: var(--color-component-badge-count);
|
||||
}
|
||||
48
packages/react-devtools-shared/src/devtools/views/Components/ElementBadges.js
vendored
Normal file
48
packages/react-devtools-shared/src/devtools/views/Components/ElementBadges.js
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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 Badge from './Badge';
|
||||
import ForgetBadge from './ForgetBadge';
|
||||
|
||||
import styles from './ElementBadges.css';
|
||||
|
||||
type Props = {
|
||||
hocDisplayNames: Array<string> | null,
|
||||
compiledWithForget: boolean,
|
||||
className?: string,
|
||||
};
|
||||
|
||||
export default function ElementBadges({
|
||||
compiledWithForget,
|
||||
hocDisplayNames,
|
||||
className = '',
|
||||
}: Props): React.Node {
|
||||
if (
|
||||
!compiledWithForget &&
|
||||
(hocDisplayNames == null || hocDisplayNames.length === 0)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.Root} ${className}`}>
|
||||
{compiledWithForget && <ForgetBadge indexable={false} />}
|
||||
|
||||
{hocDisplayNames != null && hocDisplayNames.length > 0 && (
|
||||
<Badge>{hocDisplayNames[0]}</Badge>
|
||||
)}
|
||||
|
||||
{hocDisplayNames != null && hocDisplayNames.length > 1 && (
|
||||
<div className={styles.ExtraLabel}>+{hocDisplayNames.length - 1}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.Root {
|
||||
background-color: var(--color-forget-badge);
|
||||
}
|
||||
43
packages/react-devtools-shared/src/devtools/views/Components/ForgetBadge.js
vendored
Normal file
43
packages/react-devtools-shared/src/devtools/views/Components/ForgetBadge.js
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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 Badge from './Badge';
|
||||
import IndexableDisplayName from './IndexableDisplayName';
|
||||
|
||||
import styles from './ForgetBadge.css';
|
||||
|
||||
type CommonProps = {
|
||||
className?: string,
|
||||
};
|
||||
|
||||
type PropsForIndexable = CommonProps & {
|
||||
indexable: true,
|
||||
elementID: number,
|
||||
};
|
||||
|
||||
type PropsForNonIndexable = CommonProps & {
|
||||
indexable: false | void,
|
||||
elementID?: number,
|
||||
};
|
||||
|
||||
type Props = PropsForIndexable | PropsForNonIndexable;
|
||||
|
||||
export default function ForgetBadge(props: Props): React.Node {
|
||||
const {className = ''} = props;
|
||||
|
||||
const innerView = props.indexable ? (
|
||||
<IndexableDisplayName displayName="Forget" id={props.elementID} />
|
||||
) : (
|
||||
'Forget'
|
||||
);
|
||||
|
||||
return <Badge className={`${styles.Root} ${className}`}>{innerView}</Badge>;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
.HocBadges {
|
||||
padding: 0.125rem 0.25rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.Badge {
|
||||
display: inline-block;
|
||||
background-color: var(--color-component-badge-background);
|
||||
color: var(--color-text);
|
||||
padding: 0.125rem 0.25rem;
|
||||
line-height: normal;
|
||||
border-radius: 0.125rem;
|
||||
margin-right: 0.25rem;
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-small);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* 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 './HocBadges.css';
|
||||
|
||||
import type {Element} from 'react-devtools-shared/src/frontend/types';
|
||||
|
||||
type Props = {
|
||||
element: Element,
|
||||
};
|
||||
|
||||
export default function HocBadges({element}: Props): React.Node {
|
||||
const {hocDisplayNames} = ((element: any): Element);
|
||||
|
||||
if (hocDisplayNames === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.HocBadges}>
|
||||
{hocDisplayNames.map(hocDisplayName => (
|
||||
<div key={hocDisplayName} className={styles.Badge}>
|
||||
{hocDisplayName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
63
packages/react-devtools-shared/src/devtools/views/Components/IndexableDisplayName.js
vendored
Normal file
63
packages/react-devtools-shared/src/devtools/views/Components/IndexableDisplayName.js
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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 {createRegExp} from '../utils';
|
||||
|
||||
import {TreeStateContext} from './TreeContext';
|
||||
import styles from './Element.css';
|
||||
|
||||
const {useMemo, useContext} = React;
|
||||
|
||||
type Props = {
|
||||
displayName: string | null,
|
||||
id: number,
|
||||
};
|
||||
|
||||
function IndexableDisplayName({displayName, id}: Props): React.Node {
|
||||
const {searchIndex, searchResults, searchText} = useContext(TreeStateContext);
|
||||
const isSearchResult = useMemo(() => {
|
||||
return searchResults.includes(id);
|
||||
}, [id, searchResults]);
|
||||
const isCurrentResult =
|
||||
searchIndex !== null && id === searchResults[searchIndex];
|
||||
|
||||
if (!isSearchResult || displayName === null) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
const match = createRegExp(searchText).exec(displayName);
|
||||
|
||||
if (match === null) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
const startIndex = match.index;
|
||||
const stopIndex = startIndex + match[0].length;
|
||||
|
||||
const children = [];
|
||||
if (startIndex > 0) {
|
||||
children.push(<span key="begin">{displayName.slice(0, startIndex)}</span>);
|
||||
}
|
||||
children.push(
|
||||
<mark
|
||||
key="middle"
|
||||
className={isCurrentResult ? styles.CurrentHighlight : styles.Highlight}>
|
||||
{displayName.slice(startIndex, stopIndex)}
|
||||
</mark>,
|
||||
);
|
||||
if (stopIndex < displayName.length) {
|
||||
children.push(<span key="end">{displayName.slice(stopIndex)}</span>);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
export default IndexableDisplayName;
|
||||
@@ -0,0 +1,14 @@
|
||||
.Root {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Root *:not(:first-child) {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.ExtraLabel {
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-small);
|
||||
color: var(--color-component-badge-count);
|
||||
}
|
||||
58
packages/react-devtools-shared/src/devtools/views/Components/IndexableElementBadges.js
vendored
Normal file
58
packages/react-devtools-shared/src/devtools/views/Components/IndexableElementBadges.js
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 Badge from './Badge';
|
||||
import ForgetBadge from './ForgetBadge';
|
||||
import IndexableDisplayName from './IndexableDisplayName';
|
||||
|
||||
import styles from './IndexableElementBadges.css';
|
||||
|
||||
type Props = {
|
||||
hocDisplayNames: Array<string> | null,
|
||||
compiledWithForget: boolean,
|
||||
elementID: number,
|
||||
className?: string,
|
||||
};
|
||||
|
||||
export default function IndexableElementBadges({
|
||||
compiledWithForget,
|
||||
hocDisplayNames,
|
||||
elementID,
|
||||
className = '',
|
||||
}: Props): React.Node {
|
||||
if (
|
||||
!compiledWithForget &&
|
||||
(hocDisplayNames == null || hocDisplayNames.length === 0)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.Root} ${className}`}>
|
||||
{compiledWithForget && (
|
||||
<ForgetBadge indexable={true} elementID={elementID} />
|
||||
)}
|
||||
|
||||
{hocDisplayNames != null && hocDisplayNames.length > 0 && (
|
||||
<Badge>
|
||||
<IndexableDisplayName
|
||||
displayName={hocDisplayNames[0]}
|
||||
id={elementID}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{hocDisplayNames != null && hocDisplayNames.length > 1 && (
|
||||
<div className={styles.ExtraLabel}>+{hocDisplayNames.length - 1}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.Root {
|
||||
padding: 0.25rem;
|
||||
user-select: none;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.Root *:not(:first-child) {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
36
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.js
vendored
Normal file
36
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.js
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 type {Element} from 'react-devtools-shared/src/frontend/types';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import Badge from './Badge';
|
||||
import ForgetBadge from './ForgetBadge';
|
||||
|
||||
import styles from './InspectedElementBadges.css';
|
||||
|
||||
type Props = {
|
||||
element: Element,
|
||||
};
|
||||
|
||||
export default function InspectedElementBadges({element}: Props): React.Node {
|
||||
const {hocDisplayNames, compiledWithForget} = element;
|
||||
|
||||
return (
|
||||
<div className={styles.Root}>
|
||||
{compiledWithForget && <ForgetBadge indexable={false} />}
|
||||
|
||||
{hocDisplayNames !== null &&
|
||||
hocDisplayNames.map(hocDisplayName => (
|
||||
<Badge key={hocDisplayName}>{hocDisplayName}</Badge>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import ContextMenuItem from '../../ContextMenu/ContextMenuItem';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Icon from '../Icon';
|
||||
import HocBadges from './HocBadges';
|
||||
import InspectedElementBadges from './InspectedElementBadges';
|
||||
import InspectedElementContextTree from './InspectedElementContextTree';
|
||||
import InspectedElementErrorsAndWarningsTree from './InspectedElementErrorsAndWarningsTree';
|
||||
import InspectedElementHooksTree from './InspectedElementHooksTree';
|
||||
@@ -26,7 +26,7 @@ import InspectedElementStateTree from './InspectedElementStateTree';
|
||||
import InspectedElementStyleXPlugin from './InspectedElementStyleXPlugin';
|
||||
import InspectedElementSuspenseToggle from './InspectedElementSuspenseToggle';
|
||||
import NativeStyleEditor from './NativeStyleEditor';
|
||||
import Badge from './Badge';
|
||||
import ElementBadges from './ElementBadges';
|
||||
import {useHighlightNativeElement} from '../hooks';
|
||||
import {
|
||||
copyInspectedElementPath as copyInspectedElementPathAPI,
|
||||
@@ -41,12 +41,8 @@ import type {ContextMenuContextType} from '../context';
|
||||
import type {
|
||||
Element,
|
||||
InspectedElement,
|
||||
SerializedElement,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {
|
||||
ElementType,
|
||||
HookNames,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {HookNames} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {ToggleParseHookNames} from './InspectedElementContext';
|
||||
|
||||
export type CopyPath = (path: Array<string | number>) => void;
|
||||
@@ -90,7 +86,7 @@ export default function InspectedElementView({
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles.InspectedElement}>
|
||||
<HocBadges element={element} />
|
||||
<InspectedElementBadges element={element} />
|
||||
|
||||
<InspectedElementPropsTree
|
||||
bridge={bridge}
|
||||
@@ -152,17 +148,20 @@ export default function InspectedElementView({
|
||||
className={styles.Owners}
|
||||
data-testname="InspectedElementView-Owners">
|
||||
<div className={styles.OwnersHeader}>rendered by</div>
|
||||
|
||||
{showOwnersList &&
|
||||
((owners: any): Array<SerializedElement>).map(owner => (
|
||||
owners?.map(owner => (
|
||||
<OwnerView
|
||||
key={owner.id}
|
||||
displayName={owner.displayName || 'Anonymous'}
|
||||
hocDisplayNames={owner.hocDisplayNames}
|
||||
compiledWithForget={owner.compiledWithForget}
|
||||
id={owner.id}
|
||||
isInStore={store.containsElement(owner.id)}
|
||||
type={owner.type}
|
||||
/>
|
||||
))}
|
||||
|
||||
{rootType !== null && (
|
||||
<div className={styles.OwnersMetaField}>{rootType}</div>
|
||||
)}
|
||||
@@ -286,17 +285,17 @@ function Source({fileName, lineNumber}: SourceProps) {
|
||||
type OwnerViewProps = {
|
||||
displayName: string,
|
||||
hocDisplayNames: Array<string> | null,
|
||||
compiledWithForget: boolean,
|
||||
id: number,
|
||||
isInStore: boolean,
|
||||
type: ElementType,
|
||||
};
|
||||
|
||||
function OwnerView({
|
||||
displayName,
|
||||
hocDisplayNames,
|
||||
compiledWithForget,
|
||||
id,
|
||||
isInStore,
|
||||
type,
|
||||
}: OwnerViewProps) {
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const {highlightNativeElement, clearHighlightNativeElement} =
|
||||
@@ -313,25 +312,25 @@ function OwnerView({
|
||||
});
|
||||
}, [dispatch, id]);
|
||||
|
||||
const onMouseEnter = () => highlightNativeElement(id);
|
||||
|
||||
const onMouseLeave = clearHighlightNativeElement;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={id}
|
||||
className={styles.OwnerButton}
|
||||
disabled={!isInStore}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}>
|
||||
onMouseEnter={() => highlightNativeElement(id)}
|
||||
onMouseLeave={clearHighlightNativeElement}>
|
||||
<span className={styles.OwnerContent}>
|
||||
<span
|
||||
className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
|
||||
title={displayName}>
|
||||
{displayName}
|
||||
</span>
|
||||
<Badge hocDisplayNames={hocDisplayNames} type={type} />
|
||||
|
||||
<ElementBadges
|
||||
hocDisplayNames={hocDisplayNames}
|
||||
compiledWithForget={compiledWithForget}
|
||||
/>
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import {createContext, useCallback, useContext, useEffect} from 'react';
|
||||
import {createResource} from '../../cache';
|
||||
import {BridgeContext, StoreContext} from '../context';
|
||||
import {TreeStateContext} from './TreeContext';
|
||||
import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
|
||||
import {backendToFrontendSerializedElementMapper} from 'react-devtools-shared/src/utils';
|
||||
|
||||
import type {OwnersList} from 'react-devtools-shared/src/backend/types';
|
||||
import type {
|
||||
@@ -100,16 +100,7 @@ function OwnersListContextController({children}: Props): React.Node {
|
||||
request.resolveFn(
|
||||
ownersList.owners === null
|
||||
? null
|
||||
: ownersList.owners.map(owner => {
|
||||
const [displayNameWithoutHOCs, hocDisplayNames] =
|
||||
separateDisplayNameAndHOCs(owner.displayName, owner.type);
|
||||
|
||||
return {
|
||||
...owner,
|
||||
displayName: displayNameWithoutHOCs,
|
||||
hocDisplayNames,
|
||||
};
|
||||
}),
|
||||
: ownersList.owners.map(backendToFrontendSerializedElementMapper),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,6 @@
|
||||
color: var(--color-dimmest);
|
||||
}
|
||||
|
||||
.Badge {
|
||||
.BadgesBlock {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Toggle from '../Toggle';
|
||||
import Badge from './Badge';
|
||||
import ElementBadges from './ElementBadges';
|
||||
import {OwnersListContext} from './OwnersListContext';
|
||||
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
|
||||
import {useIsOverflowing} from '../hooks';
|
||||
@@ -204,11 +204,7 @@ type ElementsDropdownProps = {
|
||||
selectOwner: SelectOwner,
|
||||
...
|
||||
};
|
||||
function ElementsDropdown({
|
||||
owners,
|
||||
selectedIndex,
|
||||
selectOwner,
|
||||
}: ElementsDropdownProps) {
|
||||
function ElementsDropdown({owners, selectOwner}: ElementsDropdownProps) {
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const menuItems = [];
|
||||
@@ -222,10 +218,10 @@ function ElementsDropdown({
|
||||
onSelect={() => (isInStore ? selectOwner(owner) : null)}>
|
||||
{owner.displayName}
|
||||
|
||||
<Badge
|
||||
className={styles.Badge}
|
||||
<ElementBadges
|
||||
hocDisplayNames={owner.hocDisplayNames}
|
||||
type={owner.type}
|
||||
compiledWithForget={owner.compiledWithForget}
|
||||
className={styles.BadgesBlock}
|
||||
/>
|
||||
</MenuItem>,
|
||||
);
|
||||
@@ -254,7 +250,7 @@ type ElementViewProps = {
|
||||
function ElementView({isSelected, owner, selectOwner}: ElementViewProps) {
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const {displayName, hocDisplayNames, type} = owner;
|
||||
const {displayName, hocDisplayNames, compiledWithForget} = owner;
|
||||
const isInStore = store.containsElement(owner.id);
|
||||
|
||||
const handleChange = useCallback(() => {
|
||||
@@ -270,10 +266,10 @@ function ElementView({isSelected, owner, selectOwner}: ElementViewProps) {
|
||||
onChange={handleChange}>
|
||||
{displayName}
|
||||
|
||||
<Badge
|
||||
className={styles.Badge}
|
||||
<ElementBadges
|
||||
hocDisplayNames={hocDisplayNames}
|
||||
type={type}
|
||||
compiledWithForget={compiledWithForget}
|
||||
className={styles.BadgesBlock}
|
||||
/>
|
||||
</Toggle>
|
||||
);
|
||||
|
||||
@@ -993,10 +993,13 @@ function recursivelySearchTree(
|
||||
regExp: RegExp,
|
||||
searchResults: Array<number>,
|
||||
): void {
|
||||
const {children, displayName, hocDisplayNames} = ((store.getElementByID(
|
||||
elementID,
|
||||
): any): Element);
|
||||
const element = store.getElementByID(elementID);
|
||||
|
||||
if (element == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {children, displayName, hocDisplayNames, compiledWithForget} = element;
|
||||
if (displayName != null && regExp.test(displayName) === true) {
|
||||
searchResults.push(elementID);
|
||||
} else if (
|
||||
@@ -1005,6 +1008,8 @@ function recursivelySearchTree(
|
||||
hocDisplayNames.some(name => regExp.test(name)) === true
|
||||
) {
|
||||
searchResults.push(elementID);
|
||||
} else if (compiledWithForget && regExp.test('Forget')) {
|
||||
searchResults.push(elementID);
|
||||
}
|
||||
|
||||
children.forEach(childID =>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import {PROFILER_EXPORT_VERSION} from 'react-devtools-shared/src/constants';
|
||||
import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
|
||||
import {backendToFrontendSerializedElementMapper} from 'react-devtools-shared/src/utils';
|
||||
|
||||
import type {ProfilingDataBackend} from 'react-devtools-shared/src/backend/types';
|
||||
import type {
|
||||
@@ -106,20 +106,9 @@ export function prepareProfilingDataFrontendFromBackendAndStore(
|
||||
timestamp: commitDataBackend.timestamp,
|
||||
updaters:
|
||||
commitDataBackend.updaters !== null
|
||||
? commitDataBackend.updaters.map(serializedElement => {
|
||||
const [
|
||||
serializedElementDisplayName,
|
||||
serializedElementHocDisplayNames,
|
||||
] = separateDisplayNameAndHOCs(
|
||||
serializedElement.displayName,
|
||||
serializedElement.type,
|
||||
);
|
||||
return {
|
||||
...serializedElement,
|
||||
displayName: serializedElementDisplayName,
|
||||
hocDisplayNames: serializedElementHocDisplayNames,
|
||||
};
|
||||
})
|
||||
? commitDataBackend.updaters.map(
|
||||
backendToFrontendSerializedElementMapper,
|
||||
)
|
||||
: null,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -151,6 +151,10 @@ export type Element = {
|
||||
// This element is not in a StrictMode compliant subtree.
|
||||
// Only true for React versions supporting StrictMode.
|
||||
isStrictModeNonCompliant: boolean,
|
||||
|
||||
// If component is compiled with Forget, the backend will send its name as Forget(...)
|
||||
// Later, on the frontend side, we will strip HOC names and Forget prefix.
|
||||
compiledWithForget: boolean,
|
||||
};
|
||||
|
||||
export type SerializedElement = {
|
||||
@@ -158,6 +162,7 @@ export type SerializedElement = {
|
||||
id: number,
|
||||
key: number | string | null,
|
||||
hocDisplayNames: Array<string> | null,
|
||||
compiledWithForget: boolean,
|
||||
type: ElementType,
|
||||
};
|
||||
|
||||
|
||||
53
packages/react-devtools-shared/src/utils.js
vendored
53
packages/react-devtools-shared/src/utils.js
vendored
@@ -60,8 +60,10 @@ import type {
|
||||
ComponentFilter,
|
||||
ElementType,
|
||||
BrowserTheme,
|
||||
} from './frontend/types';
|
||||
import type {LRUCache} from 'react-devtools-shared/src/frontend/types';
|
||||
SerializedElement as SerializedElementFrontend,
|
||||
LRUCache,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {SerializedElement as SerializedElementBackend} from 'react-devtools-shared/src/backend/types';
|
||||
|
||||
// $FlowFixMe[method-unbinding]
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
@@ -415,16 +417,35 @@ export function getOpenInEditorURL(): string {
|
||||
return getDefaultOpenInEditorURL();
|
||||
}
|
||||
|
||||
export function separateDisplayNameAndHOCs(
|
||||
type ParseElementDisplayNameFromBackendReturn = {
|
||||
formattedDisplayName: string | null,
|
||||
hocDisplayNames: Array<string> | null,
|
||||
compiledWithForget: boolean,
|
||||
};
|
||||
export function parseElementDisplayNameFromBackend(
|
||||
displayName: string | null,
|
||||
type: ElementType,
|
||||
): [string | null, Array<string> | null] {
|
||||
): ParseElementDisplayNameFromBackendReturn {
|
||||
if (displayName === null) {
|
||||
return [null, null];
|
||||
return {
|
||||
formattedDisplayName: null,
|
||||
hocDisplayNames: null,
|
||||
compiledWithForget: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (displayName.startsWith('Forget(')) {
|
||||
const displayNameWithoutForgetWrapper = displayName.slice(
|
||||
7,
|
||||
displayName.length - 1,
|
||||
);
|
||||
|
||||
const {formattedDisplayName, hocDisplayNames} =
|
||||
parseElementDisplayNameFromBackend(displayNameWithoutForgetWrapper, type);
|
||||
return {formattedDisplayName, hocDisplayNames, compiledWithForget: true};
|
||||
}
|
||||
|
||||
let hocDisplayNames = null;
|
||||
|
||||
switch (type) {
|
||||
case ElementTypeClass:
|
||||
case ElementTypeForwardRef:
|
||||
@@ -442,7 +463,11 @@ export function separateDisplayNameAndHOCs(
|
||||
break;
|
||||
}
|
||||
|
||||
return [displayName, hocDisplayNames];
|
||||
return {
|
||||
formattedDisplayName: displayName,
|
||||
hocDisplayNames,
|
||||
compiledWithForget: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Pulled from react-compat
|
||||
@@ -897,3 +922,17 @@ export const isPlainObject = (object: Object): boolean => {
|
||||
const objectParentPrototype = Object.getPrototypeOf(objectPrototype);
|
||||
return !objectParentPrototype;
|
||||
};
|
||||
|
||||
export function backendToFrontendSerializedElementMapper(
|
||||
element: SerializedElementBackend,
|
||||
): SerializedElementFrontend {
|
||||
const {formattedDisplayName, hocDisplayNames, compiledWithForget} =
|
||||
parseElementDisplayNameFromBackend(element.displayName, element.type);
|
||||
|
||||
return {
|
||||
...element,
|
||||
displayName: formattedDisplayName,
|
||||
hocDisplayNames,
|
||||
compiledWithForget,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user