mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #245 from bvaughn/custom-filtering
Add configurable component filters
This commit is contained in:
+1
-1
@@ -83,7 +83,7 @@ For example, adding a function component `<Foo>` with an id 2:
|
||||
[
|
||||
1, // add operation
|
||||
2, // fiber id
|
||||
2, // ElementTypeFunction
|
||||
1, // ElementTypeClass
|
||||
1, // parent id
|
||||
0, // owner id
|
||||
3, // encoded display name size
|
||||
|
||||
@@ -29,6 +29,7 @@ module.exports = api => {
|
||||
plugins: [
|
||||
['@babel/plugin-transform-flow-strip-types'],
|
||||
['@babel/plugin-proposal-class-properties', { loose: false }],
|
||||
['@babel/plugin-transform-react-jsx-source'],
|
||||
],
|
||||
presets: [
|
||||
['@babel/preset-env', { targets }],
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"@babel/core": "^7.1.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
||||
"@babel/plugin-transform-flow-strip-types": "^7.1.6",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.2.0",
|
||||
"@babel/preset-env": "^7.1.6",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Store component filters should filter by display name: 1: mount 1`] = `
|
||||
[root]
|
||||
▾ <Foo>
|
||||
<Text>
|
||||
▾ <Bar>
|
||||
<Text>
|
||||
▾ <Baz>
|
||||
<Text>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should filter by display name: 2: filter "Foo" 1`] = `
|
||||
[root]
|
||||
<Text>
|
||||
▾ <Bar>
|
||||
<Text>
|
||||
▾ <Baz>
|
||||
<Text>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should filter by display name: 3: filter "Ba" 1`] = `
|
||||
[root]
|
||||
▾ <Foo>
|
||||
<Text>
|
||||
<Text>
|
||||
<Text>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should filter by display name: 4: filter "B.z" 1`] = `
|
||||
[root]
|
||||
▾ <Foo>
|
||||
<Text>
|
||||
▾ <Bar>
|
||||
<Text>
|
||||
<Text>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should filter by path: 1: mount 1`] = `
|
||||
[root]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should filter by path: 2: hide all components declared within this test filed 1`] = `[root]`;
|
||||
|
||||
exports[`Store component filters should filter by path: 3: hide components in a made up fake path 1`] = `
|
||||
[root]
|
||||
▾ <Component>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should ignore invalid ElementTypeRoot filter: 1: mount 1`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should ignore invalid ElementTypeRoot filter: 2: add invalid filter 1`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should support filtering by element type: 1: mount 1`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
▾ <div>
|
||||
▾ <Component>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should support filtering by element type: 2: hide host components 1`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
<Component>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should support filtering by element type: 3: hide class components 1`] = `
|
||||
[root]
|
||||
▾ <div>
|
||||
▾ <Component>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should support filtering by element type: 4: hide class and function components 1`] = `
|
||||
[root]
|
||||
▾ <div>
|
||||
<div>
|
||||
`;
|
||||
|
||||
exports[`Store component filters should support filtering by element type: 5: disable all filters 1`] = `
|
||||
[root]
|
||||
▾ <Root>
|
||||
▾ <div>
|
||||
▾ <Component>
|
||||
<div>
|
||||
`;
|
||||
@@ -0,0 +1,198 @@
|
||||
// @flow
|
||||
|
||||
describe('Store component filters', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let TestUtils;
|
||||
let Types;
|
||||
let store;
|
||||
|
||||
const createElementTypeFilter = (elementType, isEnabled = true) => ({
|
||||
type: Types.ComponentFilterElementType,
|
||||
isEnabled,
|
||||
value: elementType,
|
||||
});
|
||||
|
||||
const createDisplayNameFilter = (source, isEnabled = true) => {
|
||||
let isValid = true;
|
||||
try {
|
||||
new RegExp(source);
|
||||
} catch (error) {
|
||||
isValid = false;
|
||||
}
|
||||
return {
|
||||
type: Types.ComponentFilterDisplayName,
|
||||
isEnabled,
|
||||
isValid,
|
||||
value: source,
|
||||
};
|
||||
};
|
||||
|
||||
const createLocationFilter = (source, isEnabled = true) => {
|
||||
let isValid = true;
|
||||
try {
|
||||
new RegExp(source);
|
||||
} catch (error) {
|
||||
isValid = false;
|
||||
}
|
||||
return {
|
||||
type: Types.ComponentFilterLocation,
|
||||
isEnabled,
|
||||
isValid,
|
||||
value: source,
|
||||
};
|
||||
};
|
||||
|
||||
const act = (callback: Function) => {
|
||||
TestUtils.act(() => {
|
||||
callback();
|
||||
});
|
||||
jest.runAllTimers(); // Flush Bridge operations
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
store.componentFilters = [];
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestUtils = require('react-dom/test-utils');
|
||||
Types = require('src/types');
|
||||
});
|
||||
|
||||
it('should throw if filters are updated while profiling', () => {
|
||||
act(() => store.startProfiling());
|
||||
expect(() => (store.componentFilters = [])).toThrow(
|
||||
'Cannot modify filter preferences while profiling'
|
||||
);
|
||||
});
|
||||
|
||||
it('should support filtering by element type', () => {
|
||||
class Root extends React.Component<{| children: React$Node |}> {
|
||||
render() {
|
||||
return <div>{this.props.children}</div>;
|
||||
}
|
||||
}
|
||||
const Component = () => <div>Hi</div>;
|
||||
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<Root>
|
||||
<Component />
|
||||
</Root>,
|
||||
document.createElement('div')
|
||||
)
|
||||
);
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeHostComponent),
|
||||
])
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('2: hide host components');
|
||||
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeClass),
|
||||
])
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('3: hide class components');
|
||||
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeClass),
|
||||
createElementTypeFilter(Types.ElementTypeFunction),
|
||||
])
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('4: hide class and function components');
|
||||
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeClass, false),
|
||||
createElementTypeFilter(Types.ElementTypeFunction, false),
|
||||
])
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('5: disable all filters');
|
||||
});
|
||||
|
||||
it('should ignore invalid ElementTypeRoot filter', () => {
|
||||
const Root = () => <div>Hi</div>;
|
||||
|
||||
act(() => ReactDOM.render(<Root />, document.createElement('div')));
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeRoot),
|
||||
])
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('2: add invalid filter');
|
||||
});
|
||||
|
||||
it('should filter by display name', () => {
|
||||
const Text = ({ label }) => label;
|
||||
const Foo = () => <Text label="foo" />;
|
||||
const Bar = () => <Text label="bar" />;
|
||||
const Baz = () => <Text label="baz" />;
|
||||
|
||||
act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Foo />
|
||||
<Bar />
|
||||
<Baz />
|
||||
</React.Fragment>,
|
||||
document.createElement('div')
|
||||
)
|
||||
);
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
|
||||
act(() => (store.componentFilters = [createDisplayNameFilter('Foo')]));
|
||||
expect(store).toMatchSnapshot('2: filter "Foo"');
|
||||
|
||||
act(() => (store.componentFilters = [createDisplayNameFilter('Ba')]));
|
||||
expect(store).toMatchSnapshot('3: filter "Ba"');
|
||||
|
||||
act(() => (store.componentFilters = [createDisplayNameFilter('B.z')]));
|
||||
expect(store).toMatchSnapshot('4: filter "B.z"');
|
||||
});
|
||||
|
||||
it('should filter by path', () => {
|
||||
const Component = () => <div>Hi</div>;
|
||||
|
||||
act(() => ReactDOM.render(<Component />, document.createElement('div')));
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createLocationFilter(__filename.replace(__dirname, '')),
|
||||
])
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot(
|
||||
'2: hide all components declared within this test filed'
|
||||
);
|
||||
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createLocationFilter('this:is:a:made:up:path'),
|
||||
])
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('3: hide components in a made up fake path');
|
||||
});
|
||||
});
|
||||
+11
-1
@@ -16,7 +16,7 @@ import type {
|
||||
RendererID,
|
||||
RendererInterface,
|
||||
} from './types';
|
||||
import type { Bridge } from '../types';
|
||||
import type { Bridge, ComponentFilter } from '../types';
|
||||
|
||||
const debug = (methodName, ...args) => {
|
||||
if (__DEBUG__) {
|
||||
@@ -118,6 +118,7 @@ export default class Agent extends EventEmitter {
|
||||
this.syncSelectionFromNativeElementsPanel
|
||||
);
|
||||
bridge.addListener('shutdown', this.shutdown);
|
||||
bridge.addListener('updateComponentFilters', this.updateComponentFilters);
|
||||
bridge.addListener('viewElementSource', this.viewElementSource);
|
||||
|
||||
if (this._isProfiling) {
|
||||
@@ -489,6 +490,15 @@ export default class Agent extends EventEmitter {
|
||||
this._bridge.send('profilingStatus', this._isProfiling);
|
||||
};
|
||||
|
||||
updateComponentFilters = (componentFilters: Array<ComponentFilter>) => {
|
||||
for (let rendererID in this._rendererInterfaces) {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
(rendererID: any)
|
||||
]: any): RendererInterface);
|
||||
renderer.updateComponentFilters(componentFilters);
|
||||
}
|
||||
};
|
||||
|
||||
viewElementSource = ({ id, rendererID }: InspectSelectParams) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
|
||||
+221
-159
@@ -2,19 +2,28 @@
|
||||
|
||||
import { gte } from 'semver';
|
||||
import {
|
||||
ComponentFilterDisplayName,
|
||||
ComponentFilterElementType,
|
||||
ComponentFilterLocation,
|
||||
ElementTypeClass,
|
||||
ElementTypeFunction,
|
||||
ElementTypeContext,
|
||||
ElementTypeEventComponent,
|
||||
ElementTypeEventTarget,
|
||||
ElementTypeFunction,
|
||||
ElementTypeForwardRef,
|
||||
ElementTypeHostComponent,
|
||||
ElementTypeMemo,
|
||||
ElementTypeOtherOrUnknown,
|
||||
ElementTypeProfiler,
|
||||
ElementTypeRoot,
|
||||
ElementTypeSuspense,
|
||||
} from 'src/devtools/types';
|
||||
import { getDisplayName, utfEncodeString } from '../utils';
|
||||
} from 'src/types';
|
||||
import {
|
||||
getDisplayName,
|
||||
getSavedComponentFilters,
|
||||
getUID,
|
||||
utfEncodeString,
|
||||
} from 'src/utils';
|
||||
import { cleanForBridge, copyWithSet, setInObject } from './utils';
|
||||
import {
|
||||
__DEBUG__,
|
||||
@@ -24,7 +33,6 @@ import {
|
||||
TREE_OPERATION_REORDER_CHILDREN,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
} from '../constants';
|
||||
import { getUID } from '../utils';
|
||||
import { inspectHooksOfFiber } from './ReactDebugHooks';
|
||||
|
||||
import type {
|
||||
@@ -32,7 +40,6 @@ import type {
|
||||
DevToolsHook,
|
||||
Fiber,
|
||||
FiberCommitsBackend,
|
||||
FiberData,
|
||||
InteractionBackend,
|
||||
InteractionsBackend,
|
||||
InteractionWithCommitsBackend,
|
||||
@@ -43,6 +50,7 @@ import type {
|
||||
RendererInterface,
|
||||
} from './types';
|
||||
import type { InspectedElement } from 'src/devtools/views/Components/types';
|
||||
import type { ComponentFilter, ElementType } from 'src/types';
|
||||
|
||||
function getInternalReactConstants(version) {
|
||||
const ReactSymbols = {
|
||||
@@ -239,18 +247,15 @@ export function attach(
|
||||
|
||||
const debug = (name: string, fiber: Fiber, parentFiber: ?Fiber): void => {
|
||||
if (__DEBUG__) {
|
||||
const fiberData = getDataForFiber(fiber);
|
||||
const fiberDisplayName = (fiberData && fiberData.displayName) || 'null';
|
||||
const parentFiberData =
|
||||
parentFiber == null ? null : getDataForFiber(parentFiber);
|
||||
const parentFiberDisplayName =
|
||||
(parentFiberData && parentFiberData.displayName) || 'null';
|
||||
const displayName = getDisplayNameForFiber(fiber) || 'null';
|
||||
const parentDisplayName =
|
||||
(parentFiber !== null && getDisplayNameForFiber(parentFiber)) || 'null';
|
||||
// NOTE: calling getFiberID or getPrimaryFiber is unsafe here
|
||||
// because it will put them in the map. For now, we'll omit them.
|
||||
// TODO: better debugging story for this.
|
||||
console.log(
|
||||
`[renderer] %c${name} %c${fiberDisplayName} %c${
|
||||
parentFiber ? parentFiberDisplayName : ''
|
||||
`[renderer] %c${name} %c${displayName} %c${
|
||||
parentFiber ? parentDisplayName : ''
|
||||
}`,
|
||||
'color: red; font-weight: bold;',
|
||||
'color: blue;',
|
||||
@@ -259,20 +264,84 @@ export function attach(
|
||||
}
|
||||
};
|
||||
|
||||
// Keep this function in sync with getDataForFiber()
|
||||
// Configurable Components tree filters.
|
||||
const hideElementsWithDisplayNames: Set<RegExp> = new Set();
|
||||
const hideElementsWithPaths: Set<RegExp> = new Set();
|
||||
const hideElementsWithTypes: Set<ElementType> = new Set();
|
||||
|
||||
function applyComponentFilters(componentFilters: Array<ComponentFilter>) {
|
||||
hideElementsWithTypes.clear();
|
||||
hideElementsWithDisplayNames.clear();
|
||||
hideElementsWithPaths.clear();
|
||||
|
||||
componentFilters.forEach(componentFilter => {
|
||||
if (!componentFilter.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (componentFilter.type) {
|
||||
case ComponentFilterDisplayName:
|
||||
if (componentFilter.isValid && componentFilter.value !== '') {
|
||||
hideElementsWithDisplayNames.add(
|
||||
new RegExp(componentFilter.value, 'i')
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ComponentFilterElementType:
|
||||
hideElementsWithTypes.add(componentFilter.value);
|
||||
break;
|
||||
case ComponentFilterLocation:
|
||||
if (componentFilter.isValid && componentFilter.value !== '') {
|
||||
hideElementsWithPaths.add(new RegExp(componentFilter.value, 'i'));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.warn(
|
||||
`Invalid component filter type "${componentFilter.type}"`
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
applyComponentFilters(getSavedComponentFilters());
|
||||
|
||||
// If necessary, we can revisit optimizing this operation.
|
||||
// For example, we could add a new recursive unmount tree operation.
|
||||
// The unmount operations are already significantly smaller than mount opreations though.
|
||||
// This is something to keep in mind for later.
|
||||
function updateComponentFilters(componentFilters: Array<ComponentFilter>) {
|
||||
if (this._isProfiling) {
|
||||
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
|
||||
// If necessary, we could support this- but it doesn't seem like a necessary use case.
|
||||
throw Error('Cannot modify filter preferences while profiling');
|
||||
}
|
||||
|
||||
// Recursively unmount all roots.
|
||||
hook.getFiberRoots(rendererID).forEach(root => {
|
||||
currentRootID = getFiberID(getPrimaryFiber(root.current));
|
||||
unmountFiberChildrenRecursively(root.current);
|
||||
recordUnmount(root.current, false);
|
||||
currentRootID = -1;
|
||||
});
|
||||
|
||||
applyComponentFilters(componentFilters);
|
||||
|
||||
// Recursively re-mount all roots with new filter criteria applied.
|
||||
hook.getFiberRoots(rendererID).forEach(root => {
|
||||
currentRootID = getFiberID(getPrimaryFiber(root.current));
|
||||
setRootPseudoKey(currentRootID, root.current);
|
||||
mountFiberRecursively(root.current, null);
|
||||
flushPendingEvents(root);
|
||||
currentRootID = -1;
|
||||
});
|
||||
}
|
||||
|
||||
// NOTICE Keep in sync with get*ForFiber methods
|
||||
function shouldFilterFiber(fiber: Fiber): boolean {
|
||||
const { tag } = fiber;
|
||||
const { _debugSource, tag, type } = fiber;
|
||||
|
||||
switch (tag) {
|
||||
case ClassComponent:
|
||||
case FunctionComponent:
|
||||
case IncompleteClassComponent:
|
||||
case IndeterminateComponent:
|
||||
case ForwardRef:
|
||||
case HostRoot:
|
||||
case MemoComponent:
|
||||
case SimpleMemoComponent:
|
||||
return false;
|
||||
case DehydratedSuspenseComponent:
|
||||
// TODO: ideally we would show dehydrated Suspense immediately.
|
||||
// However, it has some special behavior (like disconnecting
|
||||
@@ -282,12 +351,14 @@ export function attach(
|
||||
return true;
|
||||
case EventComponent:
|
||||
case HostPortal:
|
||||
case HostComponent:
|
||||
case HostText:
|
||||
case Fragment:
|
||||
return true;
|
||||
case HostRoot:
|
||||
// It is never valid to filter the root element.
|
||||
return false;
|
||||
default:
|
||||
const typeSymbol = getTypeSymbol(fiber.type);
|
||||
const typeSymbol = getTypeSymbol(type);
|
||||
|
||||
switch (typeSymbol) {
|
||||
case CONCURRENT_MODE_NUMBER:
|
||||
@@ -296,20 +367,37 @@ export function attach(
|
||||
case STRICT_MODE_NUMBER:
|
||||
case STRICT_MODE_SYMBOL_STRING:
|
||||
return true;
|
||||
case CONTEXT_PROVIDER_NUMBER:
|
||||
case CONTEXT_PROVIDER_SYMBOL_STRING:
|
||||
case CONTEXT_CONSUMER_NUMBER:
|
||||
case CONTEXT_CONSUMER_SYMBOL_STRING:
|
||||
case SUSPENSE_NUMBER:
|
||||
case SUSPENSE_SYMBOL_STRING:
|
||||
case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
|
||||
case PROFILER_NUMBER:
|
||||
case PROFILER_SYMBOL_STRING:
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const elementType = getElementTypeForFiber(fiber);
|
||||
if (hideElementsWithTypes.has(elementType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hideElementsWithDisplayNames.size > 0) {
|
||||
const displayName = getDisplayNameForFiber(fiber);
|
||||
if (displayName != null) {
|
||||
for (let displayNameRegExp of hideElementsWithDisplayNames) {
|
||||
if (displayNameRegExp.test(displayName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_debugSource != null && hideElementsWithPaths.size > 0) {
|
||||
const { fileName } = _debugSource;
|
||||
for (let pathRegExp of hideElementsWithPaths) {
|
||||
if (pathRegExp.test(fileName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTypeSymbol(type: any): Symbol | number {
|
||||
@@ -321,10 +409,9 @@ export function attach(
|
||||
: symbolOrNumber;
|
||||
}
|
||||
|
||||
// TODO: we might want to change the data structure once we no longer suppport Stack versions of `getData`.
|
||||
// TODO: Keep in sync with getElementType()
|
||||
function getDataForFiber(fiber: Fiber): FiberData {
|
||||
const { elementType, type, key, tag } = fiber;
|
||||
// NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
|
||||
function getDisplayNameForFiber(fiber: Fiber): string | null {
|
||||
const { elementType, type, tag } = fiber;
|
||||
|
||||
// This is to support lazy components with a Promise as the type.
|
||||
// see https://github.com/facebook/react/pull/13397
|
||||
@@ -335,91 +422,47 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
let fiberData: FiberData = ((null: any): FiberData);
|
||||
let displayName: string = ((null: any): string);
|
||||
let resolvedContext: any = null;
|
||||
|
||||
switch (tag) {
|
||||
case ClassComponent:
|
||||
case IncompleteClassComponent:
|
||||
fiberData = {
|
||||
displayName: getDisplayName(resolvedType),
|
||||
key,
|
||||
type: ElementTypeClass,
|
||||
};
|
||||
break;
|
||||
return getDisplayName(resolvedType);
|
||||
case FunctionComponent:
|
||||
case IndeterminateComponent:
|
||||
fiberData = {
|
||||
displayName: getDisplayName(resolvedType),
|
||||
key,
|
||||
type: ElementTypeFunction,
|
||||
};
|
||||
break;
|
||||
return getDisplayName(resolvedType);
|
||||
case EventComponent:
|
||||
fiberData = {
|
||||
displayName: null,
|
||||
key,
|
||||
type: ElementTypeEventComponent,
|
||||
};
|
||||
break;
|
||||
return null;
|
||||
case EventTarget:
|
||||
switch (getTypeSymbol(elementType.type)) {
|
||||
case EVENT_TARGET_TOUCH_HIT_NUMBER:
|
||||
case EVENT_TARGET_TOUCH_HIT_STRING:
|
||||
displayName = 'TouchHitTarget';
|
||||
break;
|
||||
return 'TouchHitTarget';
|
||||
default:
|
||||
displayName = 'EventTarget';
|
||||
break;
|
||||
return 'EventTarget';
|
||||
}
|
||||
fiberData = {
|
||||
displayName,
|
||||
key,
|
||||
type: ElementTypeEventTarget,
|
||||
};
|
||||
break;
|
||||
case ForwardRef:
|
||||
const functionName = getDisplayName(resolvedType.render, '');
|
||||
displayName =
|
||||
return (
|
||||
resolvedType.displayName ||
|
||||
(functionName !== '' ? `ForwardRef(${functionName})` : 'ForwardRef');
|
||||
|
||||
fiberData = {
|
||||
displayName,
|
||||
key,
|
||||
type: ElementTypeForwardRef,
|
||||
};
|
||||
break;
|
||||
(functionName !== '' ? `ForwardRef(${functionName})` : 'ForwardRef')
|
||||
);
|
||||
case HostRoot:
|
||||
return {
|
||||
displayName: null,
|
||||
key: null,
|
||||
type: ElementTypeRoot,
|
||||
};
|
||||
case HostPortal:
|
||||
return null;
|
||||
case HostComponent:
|
||||
return type;
|
||||
case HostPortal:
|
||||
case HostText:
|
||||
case Fragment:
|
||||
return {
|
||||
displayName: null,
|
||||
key,
|
||||
type: ElementTypeOtherOrUnknown,
|
||||
};
|
||||
return null;
|
||||
case MemoComponent:
|
||||
case SimpleMemoComponent:
|
||||
if (elementType.displayName) {
|
||||
displayName = elementType.displayName;
|
||||
return elementType.displayName;
|
||||
} else {
|
||||
displayName = type.displayName || type.name;
|
||||
displayName = displayName ? `Memo(${displayName})` : 'Memo';
|
||||
const displayName = type.displayName || type.name;
|
||||
return displayName ? `Memo(${displayName})` : 'Memo';
|
||||
}
|
||||
fiberData = {
|
||||
displayName,
|
||||
key,
|
||||
type: ElementTypeMemo,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
const typeSymbol = getTypeSymbol(type);
|
||||
|
||||
@@ -427,83 +470,98 @@ export function attach(
|
||||
case CONCURRENT_MODE_NUMBER:
|
||||
case CONCURRENT_MODE_SYMBOL_STRING:
|
||||
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
|
||||
return {
|
||||
displayName: null,
|
||||
key: null,
|
||||
type: ElementTypeOtherOrUnknown,
|
||||
};
|
||||
return null;
|
||||
case CONTEXT_PROVIDER_NUMBER:
|
||||
case CONTEXT_PROVIDER_SYMBOL_STRING:
|
||||
// 16.3.0 exposed the context object as "context"
|
||||
// PR #12501 changed it to "_context" for 16.3.1+
|
||||
// NOTE Keep in sync with inspectElement()
|
||||
// NOTE Keep in sync with inspectElementRaw()
|
||||
resolvedContext = fiber.type._context || fiber.type.context;
|
||||
displayName = `${resolvedContext.displayName ||
|
||||
'Context'}.Provider`;
|
||||
|
||||
fiberData = {
|
||||
displayName,
|
||||
key,
|
||||
type: ElementTypeContext,
|
||||
};
|
||||
break;
|
||||
return `${resolvedContext.displayName || 'Context'}.Provider`;
|
||||
case CONTEXT_CONSUMER_NUMBER:
|
||||
case CONTEXT_CONSUMER_SYMBOL_STRING:
|
||||
// 16.3-16.5 read from "type" because the Consumer is the actual context object.
|
||||
// 16.6+ should read from "type._context" because Consumer can be different (in DEV).
|
||||
// NOTE Keep in sync with inspectElement()
|
||||
// NOTE Keep in sync with inspectElementRaw()
|
||||
resolvedContext = fiber.type._context || fiber.type;
|
||||
|
||||
// NOTE: TraceUpdatesBackendManager depends on the name ending in '.Consumer'
|
||||
// If you change the name, figure out a more resilient way to detect it.
|
||||
displayName = `${resolvedContext.displayName ||
|
||||
'Context'}.Consumer`;
|
||||
|
||||
fiberData = {
|
||||
displayName,
|
||||
key,
|
||||
type: ElementTypeContext,
|
||||
};
|
||||
break;
|
||||
return `${resolvedContext.displayName || 'Context'}.Consumer`;
|
||||
case STRICT_MODE_NUMBER:
|
||||
case STRICT_MODE_SYMBOL_STRING:
|
||||
fiberData = {
|
||||
displayName: null,
|
||||
key,
|
||||
type: ElementTypeOtherOrUnknown,
|
||||
};
|
||||
break;
|
||||
return null;
|
||||
case SUSPENSE_NUMBER:
|
||||
case SUSPENSE_SYMBOL_STRING:
|
||||
case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
|
||||
fiberData = {
|
||||
displayName: 'Suspense',
|
||||
key,
|
||||
type: ElementTypeSuspense,
|
||||
};
|
||||
break;
|
||||
return 'Suspense';
|
||||
case PROFILER_NUMBER:
|
||||
case PROFILER_SYMBOL_STRING:
|
||||
fiberData = {
|
||||
displayName: `Profiler(${fiber.memoizedProps.id})`,
|
||||
key,
|
||||
type: ElementTypeProfiler,
|
||||
};
|
||||
break;
|
||||
return `Profiler(${fiber.memoizedProps.id})`;
|
||||
default:
|
||||
// Unknown element type.
|
||||
// This may mean a new element type that has not yet been added to DevTools.
|
||||
fiberData = {
|
||||
displayName: null,
|
||||
key,
|
||||
type: ElementTypeOtherOrUnknown,
|
||||
};
|
||||
break;
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return fiberData;
|
||||
// NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
|
||||
function getElementTypeForFiber(fiber: Fiber): ElementType {
|
||||
const { type, tag } = fiber;
|
||||
|
||||
switch (tag) {
|
||||
case ClassComponent:
|
||||
case IncompleteClassComponent:
|
||||
return ElementTypeClass;
|
||||
case FunctionComponent:
|
||||
case IndeterminateComponent:
|
||||
return ElementTypeFunction;
|
||||
case EventComponent:
|
||||
return ElementTypeEventComponent;
|
||||
case EventTarget:
|
||||
return ElementTypeEventTarget;
|
||||
case ForwardRef:
|
||||
return ElementTypeForwardRef;
|
||||
case HostRoot:
|
||||
return ElementTypeRoot;
|
||||
case HostComponent:
|
||||
return ElementTypeHostComponent;
|
||||
case HostPortal:
|
||||
case HostText:
|
||||
case Fragment:
|
||||
return ElementTypeOtherOrUnknown;
|
||||
case MemoComponent:
|
||||
case SimpleMemoComponent:
|
||||
return ElementTypeMemo;
|
||||
default:
|
||||
const typeSymbol = getTypeSymbol(type);
|
||||
|
||||
switch (typeSymbol) {
|
||||
case CONCURRENT_MODE_NUMBER:
|
||||
case CONCURRENT_MODE_SYMBOL_STRING:
|
||||
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
|
||||
return ElementTypeOtherOrUnknown;
|
||||
case CONTEXT_PROVIDER_NUMBER:
|
||||
case CONTEXT_PROVIDER_SYMBOL_STRING:
|
||||
return ElementTypeContext;
|
||||
case CONTEXT_CONSUMER_NUMBER:
|
||||
case CONTEXT_CONSUMER_SYMBOL_STRING:
|
||||
return ElementTypeContext;
|
||||
case STRICT_MODE_NUMBER:
|
||||
case STRICT_MODE_SYMBOL_STRING:
|
||||
return ElementTypeOtherOrUnknown;
|
||||
case SUSPENSE_NUMBER:
|
||||
case SUSPENSE_SYMBOL_STRING:
|
||||
case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
|
||||
return ElementTypeSuspense;
|
||||
case PROFILER_NUMBER:
|
||||
case PROFILER_SYMBOL_STRING:
|
||||
return ElementTypeProfiler;
|
||||
default:
|
||||
return ElementTypeOtherOrUnknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is a slightly annoying indirection.
|
||||
@@ -727,7 +785,9 @@ export function attach(
|
||||
pushOperation(isProfilingSupported ? 1 : 0);
|
||||
pushOperation(hasOwnerMetadata ? 1 : 0);
|
||||
} else {
|
||||
const { displayName, key, type } = getDataForFiber(fiber);
|
||||
const { key } = fiber;
|
||||
const displayName = getDisplayNameForFiber(fiber);
|
||||
const elementType = getElementTypeForFiber(fiber);
|
||||
const { _debugOwner } = fiber;
|
||||
|
||||
const ownerID =
|
||||
@@ -738,7 +798,7 @@ export function attach(
|
||||
let keyStringID = getStringID(key);
|
||||
pushOperation(TREE_OPERATION_ADD);
|
||||
pushOperation(id);
|
||||
pushOperation(type);
|
||||
pushOperation(elementType);
|
||||
pushOperation(parentID);
|
||||
pushOperation(ownerID);
|
||||
pushOperation(displayNameStringID);
|
||||
@@ -1587,7 +1647,7 @@ export function attach(
|
||||
) {
|
||||
// 16.3-16.5 read from "type" because the Consumer is the actual context object.
|
||||
// 16.6+ should read from "type._context" because Consumer can be different (in DEV).
|
||||
// NOTE Keep in sync with getDataForFiber()
|
||||
// NOTE Keep in sync with getDisplayNameForFiber()
|
||||
const consumerResolvedContext = type._context || type;
|
||||
|
||||
// Global context value.
|
||||
@@ -1604,7 +1664,7 @@ export function attach(
|
||||
) {
|
||||
// 16.3.0 exposed the context object as "context"
|
||||
// PR #12501 changed it to "_context" for 16.3.1+
|
||||
// NOTE Keep in sync with getDataForFiber()
|
||||
// NOTE Keep in sync with getDisplayNameForFiber()
|
||||
const providerResolvedContext =
|
||||
currentType._context || currentType.context;
|
||||
if (providerResolvedContext === consumerResolvedContext) {
|
||||
@@ -1629,7 +1689,7 @@ export function attach(
|
||||
let owner = _debugOwner;
|
||||
while (owner !== null) {
|
||||
owners.push({
|
||||
displayName: getDataForFiber(owner).displayName || 'Unknown',
|
||||
displayName: getDisplayNameForFiber(owner) || 'Unknown',
|
||||
id: getFiberID(getPrimaryFiber(owner)),
|
||||
});
|
||||
owner = owner._debugOwner;
|
||||
@@ -1659,7 +1719,7 @@ export function attach(
|
||||
// Can view component source location.
|
||||
canViewSource,
|
||||
|
||||
displayName: getDataForFiber(fiber).displayName,
|
||||
displayName: getDisplayNameForFiber(fiber),
|
||||
|
||||
// Inspectable properties.
|
||||
// TODO Review sanitization approach for the below inspectable values.
|
||||
@@ -2123,7 +2183,7 @@ export function attach(
|
||||
if (child === null) {
|
||||
break;
|
||||
}
|
||||
const displayName = getDataForFiber(child).displayName;
|
||||
const displayName = getDisplayNameForFiber(child);
|
||||
if (displayName !== null) {
|
||||
// Prefer display names that we get from user-defined components.
|
||||
// We want to avoid using e.g. 'Suspense' unless we find nothing else.
|
||||
@@ -2166,7 +2226,8 @@ export function attach(
|
||||
}
|
||||
|
||||
function getPathFrame(fiber: Fiber): PathFrame {
|
||||
let { displayName, key } = getDataForFiber(fiber);
|
||||
const { key } = fiber;
|
||||
let displayName = getDisplayNameForFiber(fiber);
|
||||
const index = fiber.index;
|
||||
switch (fiber.tag) {
|
||||
case HostRoot:
|
||||
@@ -2260,5 +2321,6 @@ export function attach(
|
||||
setTrackedPath,
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
updateComponentFilters,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import type { ElementType } from 'src/devtools/types';
|
||||
import type { ComponentFilter, ElementType } from 'src/types';
|
||||
import type { InspectedElement } from 'src/devtools/views/Components/types';
|
||||
|
||||
type BundleType =
|
||||
@@ -139,6 +139,7 @@ export type RendererInterface = {
|
||||
setTrackedPath: (path: Array<PathFrame> | null) => void,
|
||||
startProfiling: () => void,
|
||||
stopProfiling: () => void,
|
||||
updateComponentFilters: (somponentFilters: Array<ComponentFilter>) => void,
|
||||
};
|
||||
|
||||
export type Handler = (data: any) => void;
|
||||
|
||||
@@ -5,6 +5,9 @@ export const TREE_OPERATION_REMOVE = 2;
|
||||
export const TREE_OPERATION_REORDER_CHILDREN = 3;
|
||||
export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
|
||||
|
||||
export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY =
|
||||
'React::DevTools::componentFilters';
|
||||
|
||||
export const LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY =
|
||||
'React::DevTools::reloadAndProfile';
|
||||
|
||||
|
||||
+33
-4
@@ -9,19 +9,22 @@ import {
|
||||
TREE_OPERATION_REORDER_CHILDREN,
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
} from '../constants';
|
||||
import { ElementTypeRoot } from './types';
|
||||
import { utfDecodeString } from '../utils';
|
||||
import { ElementTypeRoot } from '../types';
|
||||
import {
|
||||
getSavedComponentFilters,
|
||||
saveComponentFilters,
|
||||
utfDecodeString,
|
||||
} from '../utils';
|
||||
import { __DEBUG__ } from '../constants';
|
||||
import ProfilingCache from './ProfilingCache';
|
||||
import { printStore } from 'src/__tests__/storeSerializer';
|
||||
|
||||
import type { ElementType } from './types';
|
||||
import type { Element } from './views/Components/types';
|
||||
import type {
|
||||
ImportedProfilingData,
|
||||
ProfilingSnapshotNode,
|
||||
} from './views/Profiler/types';
|
||||
import type { Bridge } from '../types';
|
||||
import type { Bridge, ComponentFilter, ElementType } from '../types';
|
||||
|
||||
const debug = (methodName, ...args) => {
|
||||
if (__DEBUG__) {
|
||||
@@ -66,6 +69,8 @@ export default class Store extends EventEmitter {
|
||||
// Should new nodes be collapsed by default when added to the tree?
|
||||
_collapseNodesByDefault: boolean = true;
|
||||
|
||||
_componentFilters: Array<ComponentFilter>;
|
||||
|
||||
// At least one of the injected renderers contains (DEV only) owner metadata.
|
||||
_hasOwnerMetadata: boolean = false;
|
||||
|
||||
@@ -138,6 +143,8 @@ export default class Store extends EventEmitter {
|
||||
localStorage.getItem(LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY) !==
|
||||
'false';
|
||||
|
||||
this._componentFilters = getSavedComponentFilters();
|
||||
|
||||
if (config != null) {
|
||||
const {
|
||||
isProfiling,
|
||||
@@ -232,6 +239,28 @@ export default class Store extends EventEmitter {
|
||||
this.emit('collapseNodesByDefault');
|
||||
}
|
||||
|
||||
get componentFilters(): Array<ComponentFilter> {
|
||||
return this._componentFilters;
|
||||
}
|
||||
set componentFilters(value: Array<ComponentFilter>): void {
|
||||
if (this._isProfiling) {
|
||||
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
|
||||
// If necessary, we could support this- but it doesn't seem like a necessary use case.
|
||||
throw Error('Cannot modify filter preferences while profiling');
|
||||
}
|
||||
|
||||
this._componentFilters = value;
|
||||
|
||||
// Update persisted filter preferences stored in localStorage.
|
||||
saveComponentFilters(value);
|
||||
|
||||
// Notify the renderer that filter prefernces have changed.
|
||||
// This is an expensive opreation; it unmounts and remounts the entire tree.
|
||||
this._bridge.send('updateComponentFilters', value);
|
||||
|
||||
this.emit('componentFilters');
|
||||
}
|
||||
|
||||
get hasOwnerMetadata(): boolean {
|
||||
return this._hasOwnerMetadata;
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// @flow
|
||||
|
||||
export const ElementTypeClass = 1;
|
||||
export const ElementTypeEventComponent = 2;
|
||||
export const ElementTypeEventTarget = 3;
|
||||
export const ElementTypeFunction = 4;
|
||||
export const ElementTypeContext = 5;
|
||||
export const ElementTypeForwardRef = 6;
|
||||
export const ElementTypeMemo = 7;
|
||||
export const ElementTypeOtherOrUnknown = 8;
|
||||
export const ElementTypeProfiler = 9;
|
||||
export const ElementTypeRoot = 10;
|
||||
export const ElementTypeSuspense = 11;
|
||||
|
||||
// Different types of elements displayed in the Elements tree.
|
||||
// These types may be used to visually distinguish types,
|
||||
// or to enable/disable certain functionality.
|
||||
export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;
|
||||
@@ -9,13 +9,13 @@ import tooltipStyles from './Tooltip.css';
|
||||
type Props = {
|
||||
children: React$Node,
|
||||
className?: string,
|
||||
title: string,
|
||||
title?: string,
|
||||
};
|
||||
|
||||
export default function Button({
|
||||
children,
|
||||
className = '',
|
||||
title,
|
||||
title = '',
|
||||
...rest
|
||||
}: Props) {
|
||||
let button = (
|
||||
|
||||
@@ -4,10 +4,13 @@ import React from 'react';
|
||||
import styles from './ButtonIcon.css';
|
||||
|
||||
export type IconType =
|
||||
| 'add'
|
||||
| 'cancel'
|
||||
| 'clear'
|
||||
| 'close'
|
||||
| 'collapsed'
|
||||
| 'copy'
|
||||
| 'delete'
|
||||
| 'down'
|
||||
| 'expanded'
|
||||
| 'export'
|
||||
@@ -19,22 +22,31 @@ export type IconType =
|
||||
| 'previous'
|
||||
| 'record'
|
||||
| 'reload'
|
||||
| 'save'
|
||||
| 'search'
|
||||
| 'settings'
|
||||
| 'undo'
|
||||
| 'up'
|
||||
| 'view-dom'
|
||||
| 'view-source';
|
||||
|
||||
type Props = {|
|
||||
className?: string,
|
||||
type: IconType,
|
||||
|};
|
||||
|
||||
export default function ButtonIcon({ type }: Props) {
|
||||
export default function ButtonIcon({ className = '', type }: Props) {
|
||||
let pathData = null;
|
||||
switch (type) {
|
||||
case 'add':
|
||||
pathData = PATH_ADD;
|
||||
break;
|
||||
case 'cancel':
|
||||
pathData = PATH_CANCEL;
|
||||
break;
|
||||
case 'clear':
|
||||
pathData = PATH_CLEAR;
|
||||
break;
|
||||
case 'close':
|
||||
pathData = PATH_CLOSE;
|
||||
break;
|
||||
@@ -44,6 +56,9 @@ export default function ButtonIcon({ type }: Props) {
|
||||
case 'copy':
|
||||
pathData = PATH_COPY;
|
||||
break;
|
||||
case 'delete':
|
||||
pathData = PATH_DELETE;
|
||||
break;
|
||||
case 'down':
|
||||
pathData = PATH_DOWN;
|
||||
break;
|
||||
@@ -77,9 +92,15 @@ export default function ButtonIcon({ type }: Props) {
|
||||
case 'reload':
|
||||
pathData = PATH_RELOAD;
|
||||
break;
|
||||
case 'save':
|
||||
pathData = PATH_SAVE;
|
||||
break;
|
||||
case 'search':
|
||||
pathData = PATH_SEARCH;
|
||||
break;
|
||||
case 'settings':
|
||||
pathData = PATH_SETTINGS;
|
||||
break;
|
||||
case 'undo':
|
||||
pathData = PATH_UNDO;
|
||||
break;
|
||||
@@ -100,7 +121,7 @@ export default function ButtonIcon({ type }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={styles.ButtonIcon}
|
||||
className={`${styles.ButtonIcon} ${className}`}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -111,7 +132,14 @@ export default function ButtonIcon({ type }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const PATH_ADD =
|
||||
'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z';
|
||||
|
||||
const PATH_CANCEL = `
|
||||
M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z
|
||||
`;
|
||||
|
||||
const PATH_CLEAR = `
|
||||
M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69
|
||||
16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z
|
||||
`;
|
||||
@@ -126,6 +154,11 @@ const PATH_COPY = `
|
||||
2v10a2 2 0 0 0 2 2h10c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z
|
||||
`;
|
||||
|
||||
const PATH_DELETE = `
|
||||
M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12
|
||||
13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z
|
||||
`;
|
||||
|
||||
const PATH_DOWN = 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z';
|
||||
|
||||
const PATH_EXPANDED = 'M7 10l5 5 5-5z';
|
||||
@@ -161,11 +194,24 @@ const PATH_RELOAD = `
|
||||
3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z
|
||||
`;
|
||||
|
||||
const PATH_SAVE = `
|
||||
M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z
|
||||
`;
|
||||
|
||||
const PATH_SEARCH = `
|
||||
M8.5,22H3.7l-1.4-1.5V3.8l1.3-1.5h17.2l1,1.5v4.9h-1.3V4.3l-0.4-0.6H4.2L3.6,4.3V20l0.7,0.7h4.2V22z
|
||||
M23,13.9l-4.6,3.6l4.6,4.6l-1.1,1.1l-4.7-4.4l-3.3,4.4l-3.2-12.3L23,13.9z
|
||||
`;
|
||||
|
||||
const PATH_SETTINGS = `
|
||||
M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12
|
||||
2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39
|
||||
0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69
|
||||
1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58
|
||||
1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0
|
||||
.49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z
|
||||
`;
|
||||
|
||||
const PATH_UNDO = `
|
||||
M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88
|
||||
3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
.Background {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
background-color: var(--color-modal-background);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
min-width: 20rem;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
background-color: var(--color-background);
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.LeftRight {
|
||||
display: flex;
|
||||
}
|
||||
.Left {
|
||||
}
|
||||
.Right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ButtonIcon {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.NoFiltersCell {
|
||||
padding: 0.25rem 0;
|
||||
color: var(--color-dim);
|
||||
}
|
||||
|
||||
.Table {
|
||||
min-width: 20rem;
|
||||
margin-top: 0.5rem;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.TableRow {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.TableCell {
|
||||
padding: 0;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
.TableCell:last-of-type {
|
||||
text-align: right;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.Select {
|
||||
}
|
||||
|
||||
.Input {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.125rem;
|
||||
padding: 0.125rem;
|
||||
}
|
||||
|
||||
.CancelButton {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.InvalidRegExp,
|
||||
.InvalidRegExp:active,
|
||||
.InvalidRegExp:focus,
|
||||
.InvalidRegExp:hover {
|
||||
color: var(--color-value-invalid);
|
||||
}
|
||||
|
||||
.ToggleOffInvalid,
|
||||
.ToggleOnInvalid,
|
||||
.ToggleOff,
|
||||
.ToggleOn {
|
||||
border-radius: 0.75rem;
|
||||
width: 1rem;
|
||||
height: 0.625rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.125rem;
|
||||
}
|
||||
.ToggleOffInvalid {
|
||||
background-color: var(--color-toggle-background-invalid);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.ToggleOnInvalid {
|
||||
background-color: var(--color-toggle-background-invalid);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.ToggleOff {
|
||||
background-color: var(--color-toggle-background-off);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.ToggleOn {
|
||||
background-color: var(--color-toggle-background-on);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ToggleInsideOff,
|
||||
.ToggleInsideOn {
|
||||
border-radius: 0.375rem;
|
||||
width: 0.375rem;
|
||||
height: 0.375rem;
|
||||
background-color: var(--color-toggle-text);
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
// @flow
|
||||
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useModalDismissSignal, useSubscription } from '../hooks';
|
||||
import { ComponentFiltersModalContext } from './ComponentFiltersModalContext';
|
||||
import { StoreContext } from '../context';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Toggle from '../Toggle';
|
||||
import Store from 'src/devtools/store';
|
||||
import {
|
||||
ComponentFilterElementType,
|
||||
ComponentFilterDisplayName,
|
||||
ComponentFilterLocation,
|
||||
ElementTypeClass,
|
||||
ElementTypeContext,
|
||||
ElementTypeEventTarget,
|
||||
ElementTypeFunction,
|
||||
ElementTypeForwardRef,
|
||||
ElementTypeHostComponent,
|
||||
ElementTypeMemo,
|
||||
ElementTypeOtherOrUnknown,
|
||||
ElementTypeProfiler,
|
||||
ElementTypeSuspense,
|
||||
} from 'src/types';
|
||||
import styles from './ComponentFiltersModal.css';
|
||||
|
||||
import type {
|
||||
ComponentFilter,
|
||||
ComponentFilterType,
|
||||
ElementType,
|
||||
ElementTypeComponentFilter,
|
||||
RegExpComponentFilter,
|
||||
} from 'src/types';
|
||||
|
||||
export default function ComponentFiltersModalWrapper(_: {||}) {
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const { isModalShowing, setIsModalShowing } = useContext(
|
||||
ComponentFiltersModalContext
|
||||
);
|
||||
|
||||
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
|
||||
// If necessary, we could support this- but it doesn't seem like a necessary use case.
|
||||
const isProfilingSubscription = useMemo(
|
||||
() => ({
|
||||
getCurrentValue: () => store.isProfiling,
|
||||
subscribe: (callback: Function) => {
|
||||
store.addListener('isProfiling', callback);
|
||||
return () => store.removeListener('isProfiling', callback);
|
||||
},
|
||||
}),
|
||||
[store]
|
||||
);
|
||||
const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
|
||||
if (isProfiling && isModalShowing) {
|
||||
setIsModalShowing(false);
|
||||
}
|
||||
|
||||
return isModalShowing ? (
|
||||
<ComponentFiltersModal
|
||||
store={store}
|
||||
setIsModalShowing={setIsModalShowing}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
type Props = {|
|
||||
store: Store,
|
||||
setIsModalShowing: (value: boolean) => void,
|
||||
|};
|
||||
|
||||
function ComponentFiltersModal({ store, setIsModalShowing }: Props) {
|
||||
const dismissModal = useCallback(() => setIsModalShowing(false), [
|
||||
setIsModalShowing,
|
||||
]);
|
||||
|
||||
const modalRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useModalDismissSignal(modalRef, dismissModal);
|
||||
|
||||
const {
|
||||
addFilter,
|
||||
changeFilterType,
|
||||
updateFilterValueElementType,
|
||||
updateFilterValueRegExp,
|
||||
componentFilters,
|
||||
removeFilter,
|
||||
saveFilters,
|
||||
toggleFilterIsEnabled,
|
||||
} = useComponentFilters();
|
||||
|
||||
const saveAndClose = useCallback(() => {
|
||||
saveFilters();
|
||||
dismissModal();
|
||||
}, [dismissModal, saveFilters]);
|
||||
|
||||
return (
|
||||
<div className={styles.Background}>
|
||||
<div className={styles.Modal} ref={modalRef}>
|
||||
<div className={styles.LeftRight}>
|
||||
<div className={styles.Left}>Hide components where...</div>
|
||||
<div className={styles.Right}>
|
||||
<Button onClick={addFilter}>
|
||||
<ButtonIcon className={styles.ButtonIcon} type="add" />
|
||||
Add filter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<table className={styles.Table}>
|
||||
<tbody>
|
||||
{componentFilters.length === 0 && (
|
||||
<tr className={styles.TableRow}>
|
||||
<td className={styles.NoFiltersCell}>
|
||||
No filters have been added.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{componentFilters.map((componentFilter, index) => (
|
||||
<tr className={styles.TableRow} key={index}>
|
||||
<td className={styles.TableCell}>
|
||||
<Toggle
|
||||
className={
|
||||
componentFilter.isValid !== false
|
||||
? ''
|
||||
: styles.InvalidRegExp
|
||||
}
|
||||
isChecked={componentFilter.isEnabled}
|
||||
onChange={isEnabled =>
|
||||
toggleFilterIsEnabled(componentFilter, isEnabled)
|
||||
}
|
||||
title={
|
||||
componentFilter.isValid === false
|
||||
? 'Filter invalid'
|
||||
: componentFilter.isEnabled
|
||||
? 'Filter enabled'
|
||||
: 'Filter disabled'
|
||||
}
|
||||
>
|
||||
<ToggleIcon
|
||||
isEnabled={componentFilter.isEnabled}
|
||||
isValid={
|
||||
componentFilter.isValid == null ||
|
||||
componentFilter.isValid === true
|
||||
}
|
||||
/>
|
||||
</Toggle>
|
||||
</td>
|
||||
<td className={styles.TableCell}>
|
||||
<select
|
||||
className={styles.Select}
|
||||
value={componentFilter.type}
|
||||
onChange={({ currentTarget }) =>
|
||||
changeFilterType(
|
||||
componentFilter,
|
||||
((parseInt(
|
||||
currentTarget.value,
|
||||
10
|
||||
): any): ComponentFilterType)
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value={ComponentFilterLocation}>location</option>
|
||||
<option value={ComponentFilterDisplayName}>name</option>
|
||||
<option value={ComponentFilterElementType}>type</option>
|
||||
</select>
|
||||
</td>
|
||||
<td className={styles.TableCell}>
|
||||
{componentFilter.type === ComponentFilterElementType
|
||||
? 'equals'
|
||||
: 'matches'}
|
||||
</td>
|
||||
<td className={styles.TableCell}>
|
||||
{componentFilter.type === ComponentFilterElementType ? (
|
||||
<select
|
||||
className={styles.Select}
|
||||
value={componentFilter.value}
|
||||
onChange={({ currentTarget }) =>
|
||||
updateFilterValueElementType(
|
||||
componentFilter,
|
||||
((parseInt(
|
||||
currentTarget.value,
|
||||
10
|
||||
): any): ElementType)
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value={ElementTypeClass}>class</option>
|
||||
<option value={ElementTypeContext}>context</option>
|
||||
<option value={ElementTypeEventTarget}>event</option>
|
||||
<option value={ElementTypeFunction}>function</option>
|
||||
<option value={ElementTypeForwardRef}>forward ref</option>
|
||||
<option value={ElementTypeHostComponent}>
|
||||
host (e.g. <div>)
|
||||
</option>
|
||||
<option value={ElementTypeMemo}>memo</option>
|
||||
<option value={ElementTypeOtherOrUnknown}>other</option>
|
||||
<option value={ElementTypeProfiler}>profiler</option>
|
||||
<option value={ElementTypeSuspense}>suspense</option>
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
className={styles.Input}
|
||||
type="text"
|
||||
placeholder="Regular expression"
|
||||
onChange={({ currentTarget }) =>
|
||||
updateFilterValueRegExp(
|
||||
componentFilter,
|
||||
currentTarget.value
|
||||
)
|
||||
}
|
||||
value={componentFilter.value}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className={styles.TableCell}>
|
||||
<Button
|
||||
onClick={() => removeFilter(index)}
|
||||
title="Delete filter"
|
||||
>
|
||||
<ButtonIcon type="delete" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={styles.LeftRight}>
|
||||
<div className={styles.Right}>
|
||||
<Button className={styles.CancelButton} onClick={dismissModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={saveAndClose}>Save Changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ToggleIconProps = {|
|
||||
isEnabled: boolean,
|
||||
isValid: boolean,
|
||||
|};
|
||||
function ToggleIcon({ isEnabled, isValid }: ToggleIconProps) {
|
||||
let className;
|
||||
if (isValid) {
|
||||
className = isEnabled ? styles.ToggleOn : styles.ToggleOff;
|
||||
} else {
|
||||
className = isEnabled ? styles.ToggleOnInvalid : styles.ToggleOffInvalid;
|
||||
}
|
||||
return (
|
||||
<div className={className}>
|
||||
<div
|
||||
className={isEnabled ? styles.ToggleInsideOn : styles.ToggleInsideOff}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useComponentFilters() {
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const [componentFilters, setComponentFilters] = useState<
|
||||
Array<ComponentFilter>
|
||||
>(() => [...store.componentFilters]);
|
||||
|
||||
const addFilter = useCallback(() => {
|
||||
setComponentFilters(componentFilters => {
|
||||
return [
|
||||
...componentFilters,
|
||||
{
|
||||
type: ComponentFilterElementType,
|
||||
value: ElementTypeHostComponent,
|
||||
isEnabled: true,
|
||||
},
|
||||
];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const changeFilterType = useCallback(
|
||||
(componentFilter: ComponentFilter, type: ComponentFilterType) => {
|
||||
setComponentFilters(componentFilters => {
|
||||
const cloned: Array<ComponentFilter> = [...componentFilters];
|
||||
const index = componentFilters.indexOf(componentFilter);
|
||||
if (index >= 0) {
|
||||
if (type === ComponentFilterElementType) {
|
||||
cloned[index] = {
|
||||
type: ComponentFilterElementType,
|
||||
isEnabled: componentFilter.isEnabled,
|
||||
value: ElementTypeHostComponent,
|
||||
};
|
||||
} else if (type === ComponentFilterDisplayName) {
|
||||
cloned[index] = {
|
||||
type: ComponentFilterDisplayName,
|
||||
isEnabled: componentFilter.isEnabled,
|
||||
isValid: true,
|
||||
value: '',
|
||||
};
|
||||
} else if (type === ComponentFilterLocation) {
|
||||
cloned[index] = {
|
||||
type: ComponentFilterLocation,
|
||||
isEnabled: componentFilter.isEnabled,
|
||||
isValid: true,
|
||||
value: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
return cloned;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const updateFilterValueElementType = useCallback(
|
||||
(componentFilter: ComponentFilter, value: ElementType) => {
|
||||
if (componentFilter.type !== ComponentFilterElementType) {
|
||||
throw Error('Invalid value for element type filter');
|
||||
}
|
||||
|
||||
setComponentFilters(componentFilters => {
|
||||
const cloned: Array<ComponentFilter> = [...componentFilters];
|
||||
if (componentFilter.type === ComponentFilterElementType) {
|
||||
const index = componentFilters.indexOf(componentFilter);
|
||||
if (index >= 0) {
|
||||
cloned[index] = {
|
||||
...componentFilter,
|
||||
value,
|
||||
};
|
||||
}
|
||||
}
|
||||
return cloned;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const updateFilterValueRegExp = useCallback(
|
||||
(componentFilter: ComponentFilter, value: string) => {
|
||||
if (componentFilter.type === ComponentFilterElementType) {
|
||||
throw Error('Invalid value for element type filter');
|
||||
}
|
||||
|
||||
setComponentFilters(componentFilters => {
|
||||
const cloned: Array<ComponentFilter> = [...componentFilters];
|
||||
if (
|
||||
componentFilter.type === ComponentFilterDisplayName ||
|
||||
componentFilter.type === ComponentFilterLocation
|
||||
) {
|
||||
const index = componentFilters.indexOf(componentFilter);
|
||||
if (index >= 0) {
|
||||
let isValid = true;
|
||||
try {
|
||||
new RegExp(value);
|
||||
} catch (error) {
|
||||
isValid = false;
|
||||
}
|
||||
cloned[index] = {
|
||||
...componentFilter,
|
||||
isValid,
|
||||
value,
|
||||
};
|
||||
}
|
||||
}
|
||||
return cloned;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const removeFilter = useCallback((index: number) => {
|
||||
setComponentFilters(componentFilters => {
|
||||
const cloned: Array<ComponentFilter> = [...componentFilters];
|
||||
cloned.splice(index, 1);
|
||||
return cloned;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const saveFilters = useCallback(() => {
|
||||
store.componentFilters = [...componentFilters];
|
||||
}, [componentFilters, store]);
|
||||
|
||||
const toggleFilterIsEnabled = useCallback(
|
||||
(componentFilter: ComponentFilter, isEnabled: boolean) => {
|
||||
setComponentFilters(componentFilters => {
|
||||
const cloned: Array<ComponentFilter> = [...componentFilters];
|
||||
const index = componentFilters.indexOf(componentFilter);
|
||||
if (index >= 0) {
|
||||
if (componentFilter.type === ComponentFilterElementType) {
|
||||
cloned[index] = {
|
||||
...((cloned[index]: any): ElementTypeComponentFilter),
|
||||
isEnabled,
|
||||
};
|
||||
} else if (
|
||||
componentFilter.type === ComponentFilterDisplayName ||
|
||||
componentFilter.type === ComponentFilterLocation
|
||||
) {
|
||||
cloned[index] = {
|
||||
...((cloned[index]: any): RegExpComponentFilter),
|
||||
isEnabled,
|
||||
};
|
||||
}
|
||||
}
|
||||
return cloned;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
addFilter,
|
||||
changeFilterType,
|
||||
componentFilters,
|
||||
removeFilter,
|
||||
saveFilters,
|
||||
toggleFilterIsEnabled,
|
||||
updateFilterValueElementType,
|
||||
updateFilterValueRegExp,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// @flow
|
||||
|
||||
import React, { createContext, useMemo, useState } from 'react';
|
||||
|
||||
type Context = {|
|
||||
isModalShowing: boolean,
|
||||
setIsModalShowing: (value: boolean) => void,
|
||||
|};
|
||||
|
||||
const ComponentFiltersModalContext = createContext<Context>(
|
||||
((null: any): Context)
|
||||
);
|
||||
ComponentFiltersModalContext.displayName = 'ComponentFiltersModalContext';
|
||||
|
||||
type Props = {|
|
||||
children: React$Node,
|
||||
|};
|
||||
|
||||
function ComponentFiltersModalContextController({ children }: Props) {
|
||||
const [isModalShowing, setIsModalShowing] = useState(false);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
isModalShowing,
|
||||
setIsModalShowing,
|
||||
}),
|
||||
[isModalShowing]
|
||||
);
|
||||
|
||||
return (
|
||||
<ComponentFiltersModalContext.Provider value={value}>
|
||||
{children}
|
||||
</ComponentFiltersModalContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export { ComponentFiltersModalContext, ComponentFiltersModalContextController };
|
||||
@@ -9,7 +9,7 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ElementTypeClass, ElementTypeFunction } from 'src/devtools/types';
|
||||
import { ElementTypeClass, ElementTypeFunction } from 'src/types';
|
||||
import Store from 'src/devtools/store';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import { createRegExp } from '../utils';
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
ElementTypeFunction,
|
||||
ElementTypeMemo,
|
||||
ElementTypeSuspense,
|
||||
} from '../../types';
|
||||
} from 'src/types';
|
||||
|
||||
import type { Element, InspectedElement } from './types';
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { useSubscription } from '../hooks';
|
||||
import { ComponentFiltersModalContext } from './ComponentFiltersModalContext';
|
||||
import { StoreContext } from '../context';
|
||||
import Toggle from '../Toggle';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Store from 'src/devtools/store';
|
||||
|
||||
export default function ToggleCommitFilterModalButton() {
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const { isModalShowing, setIsModalShowing } = useContext(
|
||||
ComponentFiltersModalContext
|
||||
);
|
||||
|
||||
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
|
||||
// If necessary, we could support this- but it doesn't seem like a necessary use case.
|
||||
const isProfilingSubscription = useMemo(
|
||||
() => ({
|
||||
getCurrentValue: () => store.isProfiling,
|
||||
subscribe: (callback: Function) => {
|
||||
store.addListener('isProfiling', callback);
|
||||
return () => store.removeListener('isProfiling', callback);
|
||||
},
|
||||
}),
|
||||
[store]
|
||||
);
|
||||
const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
|
||||
|
||||
return (
|
||||
<Toggle
|
||||
isChecked={isModalShowing}
|
||||
isDisabled={isProfiling}
|
||||
onChange={setIsModalShowing}
|
||||
title="Filter preferences"
|
||||
>
|
||||
<ButtonIcon type="filter" />
|
||||
</Toggle>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
.Tree {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -18,6 +18,9 @@ import ElementView from './Element';
|
||||
import InspectHostNodesToggle from './InspectHostNodesToggle';
|
||||
import OwnersStack from './OwnersStack';
|
||||
import SearchInput from './SearchInput';
|
||||
import { ComponentFiltersModalContextController } from './ComponentFiltersModalContext';
|
||||
import ToggleComponentFiltersModalButton from './ToggleComponentFiltersModalButton';
|
||||
import ComponentFiltersModal from './ComponentFiltersModal';
|
||||
|
||||
import styles from './Tree.css';
|
||||
|
||||
@@ -269,42 +272,47 @@ export default function Tree(props: Props) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.Tree} ref={treeRef}>
|
||||
<div className={styles.SearchInput}>
|
||||
<InspectHostNodesToggle />
|
||||
<div className={styles.VRule} />
|
||||
{ownerStack.length > 0 ? <OwnersStack /> : <SearchInput />}
|
||||
<ComponentFiltersModalContextController>
|
||||
<div className={styles.Tree} ref={treeRef}>
|
||||
<div className={styles.SearchInput}>
|
||||
<InspectHostNodesToggle />
|
||||
<div className={styles.VRule} />
|
||||
{ownerStack.length > 0 ? <OwnersStack /> : <SearchInput />}
|
||||
<div className={styles.VRule} />
|
||||
<ToggleComponentFiltersModalButton />
|
||||
</div>
|
||||
<div
|
||||
className={styles.AutoSizerWrapper}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
onKeyPress={handleKeyPress}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
ref={focusTargetRef}
|
||||
tabIndex={0}
|
||||
>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
// $FlowFixMe https://github.com/facebook/flow/issues/7341
|
||||
<FixedSizeList
|
||||
className={styles.List}
|
||||
height={height}
|
||||
innerElementType={InnerElementType}
|
||||
itemCount={numElements}
|
||||
itemData={itemData}
|
||||
itemSize={lineHeight}
|
||||
overscanCount={3}
|
||||
ref={listRef}
|
||||
width={width}
|
||||
>
|
||||
{ElementView}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
<ComponentFiltersModal />
|
||||
</div>
|
||||
<div
|
||||
className={styles.AutoSizerWrapper}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
onKeyPress={handleKeyPress}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
ref={focusTargetRef}
|
||||
tabIndex={0}
|
||||
>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
// $FlowFixMe https://github.com/facebook/flow/issues/7341
|
||||
<FixedSizeList
|
||||
className={styles.List}
|
||||
height={height}
|
||||
innerElementType={InnerElementType}
|
||||
itemCount={numElements}
|
||||
itemData={itemData}
|
||||
itemSize={lineHeight}
|
||||
overscanCount={3}
|
||||
ref={listRef}
|
||||
width={width}
|
||||
>
|
||||
{ElementView}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentFiltersModalContextController>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import type { ElementType } from '../../types';
|
||||
import type { ElementType } from 'src/types';
|
||||
|
||||
// Each element on the frontend corresponds to a Fiber on the backend.
|
||||
// Some of its information (e.g. id, type, displayName) come from the backend.
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function ClearProfilingDataButton() {
|
||||
onClick={clear}
|
||||
title="Clear profiling data"
|
||||
>
|
||||
<ButtonIcon type="cancel" />
|
||||
<ButtonIcon type="clear" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import type { CommitDetailsFrontend, CommitTreeFrontend } from './types';
|
||||
export type ItemData = {|
|
||||
chartData: ChartData,
|
||||
scaleX: (value: number, fallbackValue: number) => number,
|
||||
selectedChartNode: ChartNode,
|
||||
selectedChartNode: ChartNode | null,
|
||||
selectedChartNodeIndex: number,
|
||||
selectFiber: (id: number | null, name: string | null) => void,
|
||||
width: number,
|
||||
@@ -127,18 +127,20 @@ function CommitFlamegraph({
|
||||
chartNode => chartNode.id === selectedFiberID
|
||||
): any): ChartNode);
|
||||
}
|
||||
// The selected node might not be in the tree for this commit,
|
||||
// so it's important that we have a fallback plan.
|
||||
if (chartNode == null) {
|
||||
return chartData.rows[0][0];
|
||||
}
|
||||
return chartNode;
|
||||
}, [chartData, selectedFiberID, selectedChartNodeIndex]);
|
||||
|
||||
const itemData = useMemo<ItemData>(
|
||||
() => ({
|
||||
chartData,
|
||||
scaleX: scale(0, selectedChartNode.treeBaseDuration, 0, width),
|
||||
scaleX: scale(
|
||||
0,
|
||||
selectedChartNode !== null
|
||||
? selectedChartNode.treeBaseDuration
|
||||
: chartData.baseDuration,
|
||||
0,
|
||||
width
|
||||
),
|
||||
selectedChartNode,
|
||||
selectedChartNodeIndex,
|
||||
selectFiber,
|
||||
|
||||
@@ -41,7 +41,10 @@ function CommitFlamegraphListItem({ data, index, style }: Props) {
|
||||
|
||||
const row = rows[index];
|
||||
|
||||
let selectedNodeOffset = scaleX(selectedChartNode.offset, width);
|
||||
let selectedNodeOffset = scaleX(
|
||||
selectedChartNode !== null ? selectedChartNode.offset : 0,
|
||||
width
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
} from 'src/constants';
|
||||
import { utfDecodeString } from 'src/utils';
|
||||
import { ElementTypeRoot } from 'src/devtools/types';
|
||||
import { ElementTypeRoot } from 'src/types';
|
||||
import Store from 'src/devtools/store';
|
||||
|
||||
import type { ElementType } from 'src/devtools/types';
|
||||
import type { ElementType } from 'src/types';
|
||||
import type {
|
||||
CommitTreeFrontend,
|
||||
CommitTreeNodeFrontend,
|
||||
|
||||
@@ -16,6 +16,7 @@ export type ChartNode = {|
|
||||
|};
|
||||
|
||||
export type ChartData = {|
|
||||
baseDuration: number,
|
||||
depth: number,
|
||||
idToDepthMap: Map<number, number>,
|
||||
maxSelfDuration: number,
|
||||
@@ -108,10 +109,16 @@ export function getChartData({
|
||||
throw Error(`Could not find root node with id "${rootID}" in commit tree`);
|
||||
}
|
||||
|
||||
// TODO: Looks like there's an assumption here that a root has only one child. Is that so with a fragment in the root?
|
||||
walkTree(root.children[0]);
|
||||
// Don't assume a single root.
|
||||
// Component filters or Fragments might lead to multiple "roots" in a flame graph.
|
||||
let baseDuration = 0;
|
||||
root.children.forEach(childID => {
|
||||
const chartNode = walkTree(childID, baseDuration);
|
||||
baseDuration += chartNode.treeBaseDuration;
|
||||
});
|
||||
|
||||
const chartData = {
|
||||
baseDuration,
|
||||
depth: maxDepth,
|
||||
idToDepthMap,
|
||||
maxSelfDuration,
|
||||
|
||||
@@ -13,48 +13,74 @@
|
||||
}
|
||||
|
||||
.Section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.Section:first-of-type {
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.Header {
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: var(--font-size-sans-large);
|
||||
}
|
||||
|
||||
.OptionGroup {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
margin: 0 1rem 0.5rem 0;
|
||||
}
|
||||
.OptionGroup:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.Option {
|
||||
.OptionLabel {
|
||||
margin-right: 0.5rem;
|
||||
font-size: var(--font-size-sans-normal);
|
||||
}
|
||||
|
||||
.RadioOption {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.Option:hover {
|
||||
.RadioOption:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
|
||||
.Option:first-of-type {
|
||||
.RadioOption:first-of-type {
|
||||
border-top-left-radius: 0.25rem;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.Option:last-of-type {
|
||||
.RadioOption:last-of-type {
|
||||
border-top-right-radius: 0.25rem;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.CheckboxOption {
|
||||
display: block;
|
||||
padding: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.ScreenshotThrottling {
|
||||
display: inline-block;
|
||||
background-color: var(--color-background-hover);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.HRule {
|
||||
height: 1px;
|
||||
background-color: var(--color-border);
|
||||
width: 100%;
|
||||
border: none;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
@@ -73,9 +73,10 @@ function Settings(_: {||}) {
|
||||
return (
|
||||
<div className={styles.Settings}>
|
||||
<div className={styles.Section}>
|
||||
<div className={styles.Header}>Theme</div>
|
||||
<div className={styles.Header}>Display preferences</div>
|
||||
<div className={styles.OptionGroup}>
|
||||
<label className={styles.Option}>
|
||||
<div className={styles.OptionLabel}>Theme</div>
|
||||
<label className={styles.RadioOption}>
|
||||
<input
|
||||
type="radio"
|
||||
name="Settings-theme"
|
||||
@@ -85,7 +86,7 @@ function Settings(_: {||}) {
|
||||
/>{' '}
|
||||
Auto
|
||||
</label>
|
||||
<label className={styles.Option}>
|
||||
<label className={styles.RadioOption}>
|
||||
<input
|
||||
type="radio"
|
||||
name="Settings-theme"
|
||||
@@ -95,7 +96,7 @@ function Settings(_: {||}) {
|
||||
/>{' '}
|
||||
Light
|
||||
</label>
|
||||
<label className={styles.Option}>
|
||||
<label className={styles.RadioOption}>
|
||||
<input
|
||||
type="radio"
|
||||
name="Settings-theme"
|
||||
@@ -106,22 +107,9 @@ function Settings(_: {||}) {
|
||||
Dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.Section}>
|
||||
<div className={styles.Header}>Components tree</div>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={collapseNodesByDefault}
|
||||
onChange={updateCollapseNodesByDefault}
|
||||
/>{' '}
|
||||
Collapse tree by default
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.Section}>
|
||||
<div className={styles.Header}>Display density</div>
|
||||
<div className={styles.OptionGroup}>
|
||||
<label className={styles.Option}>
|
||||
<div className={styles.OptionLabel}>Display density</div>
|
||||
<label className={styles.RadioOption}>
|
||||
<input
|
||||
type="radio"
|
||||
name="Settings-displayDensity"
|
||||
@@ -131,7 +119,7 @@ function Settings(_: {||}) {
|
||||
/>{' '}
|
||||
Compact
|
||||
</label>
|
||||
<label className={styles.Option}>
|
||||
<label className={styles.RadioOption}>
|
||||
<input
|
||||
type="radio"
|
||||
name="Settings-displayDensity"
|
||||
@@ -143,19 +131,31 @@ function Settings(_: {||}) {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.Section}>
|
||||
<div className={styles.Header}>Components tree</div>
|
||||
|
||||
<label className={styles.CheckboxOption}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={collapseNodesByDefault}
|
||||
onChange={updateCollapseNodesByDefault}
|
||||
/>{' '}
|
||||
Collapse newly added components by default
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{store.supportsCaptureScreenshots && (
|
||||
<div>
|
||||
<div className={styles.Section}>
|
||||
<div className={styles.Header}>Profiler</div>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={captureScreenshots}
|
||||
onChange={updateCaptureScreenshotsWhileProfiling}
|
||||
/>{' '}
|
||||
Capture screenshots while profiling
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.Section}>
|
||||
<div className={styles.Header}>Profiler</div>
|
||||
<label className={styles.CheckboxOption}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={captureScreenshots}
|
||||
onChange={updateCaptureScreenshotsWhileProfiling}
|
||||
/>{' '}
|
||||
Capture screenshots while profiling
|
||||
</label>
|
||||
{captureScreenshots && (
|
||||
<div className={styles.ScreenshotThrottling}>
|
||||
Screenshots will be throttled in order to reduce the negative
|
||||
|
||||
@@ -247,6 +247,10 @@ function updateThemeVariables(
|
||||
updateStyleHelper(theme, 'color-tab-selected-border', documentElements);
|
||||
updateStyleHelper(theme, 'color-text', documentElements);
|
||||
updateStyleHelper(theme, 'color-text-selected', documentElements);
|
||||
updateStyleHelper(theme, 'color-toggle-background-invalid', documentElements);
|
||||
updateStyleHelper(theme, 'color-toggle-background-on', documentElements);
|
||||
updateStyleHelper(theme, 'color-toggle-background-off', documentElements);
|
||||
updateStyleHelper(theme, 'color-toggle-text', documentElements);
|
||||
updateStyleHelper(theme, 'color-tooltip-background', documentElements);
|
||||
updateStyleHelper(theme, 'color-tooltip-text', documentElements);
|
||||
|
||||
|
||||
@@ -38,14 +38,20 @@ export function useIsOverflowing(
|
||||
// Forked from https://usehooks.com/useLocalStorage/
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
initialValue: T | (() => T)
|
||||
): [T, (value: T | (() => T)) => void] {
|
||||
const getValueFromLocalStorage = useCallback(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
if (item != null) {
|
||||
return JSON.parse(item);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
if (typeof initialValue === 'function') {
|
||||
return ((initialValue: any): () => T)();
|
||||
} else {
|
||||
return initialValue;
|
||||
}
|
||||
}, [initialValue, key]);
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
--light-color-tab-selected-border: #0088fa;
|
||||
--light-color-text: #000000;
|
||||
--light-color-text-selected: #ffffff;
|
||||
--light-color-toggle-background-invalid: #fc3a4b;
|
||||
--light-color-toggle-background-on: #0088fa;
|
||||
--light-color-toggle-background-off: #cfd1d5;
|
||||
--light-color-toggle-text: #ffffff;
|
||||
--light-color-tooltip-background: rgba(0, 0, 0, 0.9);
|
||||
--light-color-tooltip-text: #ffffff;
|
||||
|
||||
@@ -96,6 +100,10 @@
|
||||
--dark-color-tab-selected-border: #178fb9;
|
||||
--dark-color-text: #ffffff;
|
||||
--dark-color-text-selected: #ffffff;
|
||||
--dark-color-toggle-background-invalid: #fc3a4b;
|
||||
--dark-color-toggle-background-on: #178fb9;
|
||||
--dark-color-toggle-background-off: #777d88;
|
||||
--dark-color-toggle-text: #ffffff;
|
||||
--dark-color-tooltip-background: rgba(255, 255, 255, 0.9);
|
||||
--dark-color-tooltip-text: #000000;
|
||||
|
||||
|
||||
@@ -11,3 +11,56 @@ export type Wall = {|
|
||||
listen: (fn: Function) => Function,
|
||||
send: (event: string, payload: any, transferable?: Array<any>) => void,
|
||||
|};
|
||||
|
||||
// WARNING
|
||||
// The values below are referenced by ComponentFilters (which are saved via localStorage).
|
||||
// Do not change them or it will break previously saved user customizations.
|
||||
// If new element types are added, use new numbers rather than re-ordering existing ones.
|
||||
export const ElementTypeClass = 1;
|
||||
export const ElementTypeContext = 2;
|
||||
export const ElementTypeEventComponent = 3;
|
||||
export const ElementTypeEventTarget = 4;
|
||||
export const ElementTypeFunction = 5;
|
||||
export const ElementTypeForwardRef = 6;
|
||||
export const ElementTypeHostComponent = 7;
|
||||
export const ElementTypeMemo = 8;
|
||||
export const ElementTypeOtherOrUnknown = 9;
|
||||
export const ElementTypeProfiler = 10;
|
||||
export const ElementTypeRoot = 11;
|
||||
export const ElementTypeSuspense = 12;
|
||||
|
||||
// Different types of elements displayed in the Elements tree.
|
||||
// These types may be used to visually distinguish types,
|
||||
// or to enable/disable certain functionality.
|
||||
export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||
|
||||
// WARNING
|
||||
// The values below are referenced by ComponentFilters (which are saved via localStorage).
|
||||
// Do not change them or it will break previously saved user customizations.
|
||||
// If new filter types are added, use new numbers rather than re-ordering existing ones.
|
||||
export const ComponentFilterElementType = 1;
|
||||
export const ComponentFilterDisplayName = 2;
|
||||
export const ComponentFilterLocation = 3;
|
||||
|
||||
export type ComponentFilterType = 1 | 2 | 3;
|
||||
|
||||
// Hide all elements of types in this Set.
|
||||
// We hide host components only by default.
|
||||
export type ElementTypeComponentFilter = {|
|
||||
isEnabled: boolean,
|
||||
type: 1,
|
||||
value: ElementType,
|
||||
|};
|
||||
|
||||
// Hide all elements with displayNames or paths matching one or more of the RegExps in this Set.
|
||||
// Path filters are only used when elements include debug source location.
|
||||
export type RegExpComponentFilter = {|
|
||||
isEnabled: boolean,
|
||||
isValid: boolean,
|
||||
type: 2 | 3,
|
||||
value: string,
|
||||
|};
|
||||
|
||||
export type ComponentFilter =
|
||||
| ElementTypeComponentFilter
|
||||
| RegExpComponentFilter;
|
||||
|
||||
+34
-1
@@ -1,6 +1,10 @@
|
||||
// @flow
|
||||
|
||||
const LRU = require('lru-cache');
|
||||
import LRU from 'lru-cache';
|
||||
import { LOCAL_STORAGE_FILTER_PREFERENCES_KEY } from './constants';
|
||||
import { ComponentFilterElementType, ElementTypeHostComponent } from './types';
|
||||
|
||||
import type { ComponentFilter } from './types';
|
||||
|
||||
const FB_MODULE_RE = /^(.*) \[from (.*)\]$/;
|
||||
const cachedDisplayNames: WeakMap<Function, string> = new WeakMap();
|
||||
@@ -76,3 +80,32 @@ export function utfEncodeString(string: string): Uint32Array {
|
||||
function toCodePoint(string: string) {
|
||||
return string.codePointAt(0);
|
||||
}
|
||||
|
||||
export function getDefaultComponentFilters(): Array<ComponentFilter> {
|
||||
return [
|
||||
{
|
||||
type: ComponentFilterElementType,
|
||||
value: ElementTypeHostComponent,
|
||||
isEnabled: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getSavedComponentFilters(): Array<ComponentFilter> {
|
||||
try {
|
||||
const raw = localStorage.getItem(LOCAL_STORAGE_FILTER_PREFERENCES_KEY);
|
||||
if (raw != null) {
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
} catch (error) {}
|
||||
return getDefaultComponentFilters();
|
||||
}
|
||||
|
||||
export function saveComponentFilters(
|
||||
componentFilters: Array<ComponentFilter>
|
||||
): void {
|
||||
localStorage.setItem(
|
||||
LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
|
||||
JSON.stringify(componentFilters)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -383,6 +383,13 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
|
||||
"@babel/plugin-syntax-jsx@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz#0b85a3b4bc7cdf4cc4b8bf236335b907ca22e7c7"
|
||||
integrity sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
|
||||
"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e"
|
||||
@@ -605,6 +612,14 @@
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
"@babel/plugin-syntax-jsx" "^7.0.0"
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source@^7.2.0":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.2.0.tgz#20c8c60f0140f5dd3cd63418d452801cf3f7180f"
|
||||
integrity sha512-A32OkKTp4i5U6aE88GwwcuV4HAprUgHcTq0sSafLxjr6AW0QahrCRCjxogkbbcdtpbXkuTOlgpjophCxb6sh5g==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.0.0"
|
||||
"@babel/plugin-syntax-jsx" "^7.2.0"
|
||||
|
||||
"@babel/plugin-transform-react-jsx@^7.0.0":
|
||||
version "7.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.1.6.tgz#e6188e7d2a2dcd2796d45a87f8b0a8c906f57d1a"
|
||||
|
||||
Reference in New Issue
Block a user