diff --git a/src/backend/renderer.js b/src/backend/renderer.js index 153018f7e3..4808b694e4 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -37,7 +37,6 @@ import type { DevToolsHook, Fiber, FiberCommitsBackend, - FiberData, InteractionBackend, InteractionsBackend, InteractionWithCommitsBackend, @@ -48,7 +47,7 @@ import type { RendererInterface, } from './types'; import type { InspectedElement } from 'src/devtools/views/Components/types'; -import type { FilterPreferences } from 'src/types'; +import type { ElementType, FilterPreferences } from 'src/types'; function getInternalReactConstants(version) { const ReactSymbols = { @@ -245,18 +244,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;', @@ -271,7 +267,10 @@ export function attach( hideElementsWithPaths, } = getSavedFilterPreferences(); - // TODO (filter) We could make this more efficient. + // TODO (filter) Should we make this operation more efficient? + // 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 updateFilterPreferences(filterPreferences: FilterPreferences) { if (this._isProfiling) { // Re-mounting a tree while profiling is in progress might break a lot of assumptions. @@ -302,32 +301,11 @@ export function attach( }); } - // NOTICE Keep in sync with getDataForFiber() + // NOTICE Keep in sync with get*ForFiber methods function shouldFilterFiber(fiber: Fiber): boolean { - const { tag } = fiber; - - // TODO (filter) This does not yet support display name or path based filtering. + const { _debugSource, tag, type } = fiber; switch (tag) { - case ClassComponent: - case IncompleteClassComponent: - return hideElementsWithTypes.has(ElementTypeClass); - case FunctionComponent: - return hideElementsWithTypes.has(ElementTypeFunction); - case IndeterminateComponent: - return ( - hideElementsWithTypes.has(ElementTypeClass) || - hideElementsWithTypes.has(ElementTypeFunction) - ); - case ForwardRef: - return hideElementsWithTypes.has(ElementTypeForwardRef); - case MemoComponent: - case SimpleMemoComponent: - return hideElementsWithTypes.has(ElementTypeMemo); - case HostComponent: - return hideElementsWithTypes.has(ElementTypeHostComponent); - case HostRoot: - return false; // We never support filtering roots case DehydratedSuspenseComponent: // TODO: ideally we would show dehydrated Suspense immediately. // However, it has some special behavior (like disconnecting @@ -341,7 +319,7 @@ export function attach( case Fragment: return true; default: - const typeSymbol = getTypeSymbol(fiber.type); + const typeSymbol = getTypeSymbol(type); switch (typeSymbol) { case CONCURRENT_MODE_NUMBER: @@ -350,22 +328,35 @@ 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: - return hideElementsWithTypes.has(ElementTypeContext); - case SUSPENSE_NUMBER: - case SUSPENSE_SYMBOL_STRING: - case DEPRECATED_PLACEHOLDER_SYMBOL_STRING: - return hideElementsWithTypes.has(ElementTypeSuspense); - case PROFILER_NUMBER: - case PROFILER_SYMBOL_STRING: - return hideElementsWithTypes.has(ElementTypeProfiler); default: - return false; + break; } } + + const elementType = getTypeForFiber(fiber); + if (hideElementsWithTypes.has(elementType)) { + return true; + } + + if (hideElementsWithDisplayNames.size > 0) { + const displayName = getDisplayNameForFiber(fiber) || ''; + 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 { @@ -377,9 +368,9 @@ export function attach( : symbolOrNumber; } - // NOTICE Keep in sync with shouldFilterFiber() - 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 @@ -390,96 +381,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, - }; + return null; case HostComponent: - return { - displayName: type, - key, - type: ElementTypeHostComponent, - }; + 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); @@ -487,26 +429,14 @@ 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() 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. @@ -516,54 +446,81 @@ export function attach( // 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 getTypeForFiber(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. @@ -787,7 +744,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 type = getTypeForFiber(fiber); const { _debugOwner } = fiber; const ownerID = @@ -1647,7 +1606,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 get*ForFiber methods const consumerResolvedContext = type._context || type; // Global context value. @@ -1664,7 +1623,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 get*ForFiber methods const providerResolvedContext = currentType._context || currentType.context; if (providerResolvedContext === consumerResolvedContext) { @@ -1689,7 +1648,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; @@ -1719,7 +1678,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. @@ -2183,7 +2142,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. @@ -2226,7 +2185,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: diff --git a/src/devtools/store.js b/src/devtools/store.js index 539dccf71c..7c062247cf 100644 --- a/src/devtools/store.js +++ b/src/devtools/store.js @@ -251,10 +251,11 @@ export default class Store extends EventEmitter { this._filterPreferences = value; + // Update persisted filter preferences stored in localStorage. saveFilterPreferences(value); - // TODO (filter) Dump all nodes, update renderer preferences, and re-initialize tree. - // TODO (filter) Flushing every time a filter setting is changed is too expensive. We probably need an explitit configm + // Notify the renderer that filter prefernces have changed. + // This is an expensive opreation; it unmounts and remounts the entire tree. this._bridge.send('updateFilterPreferences', value); this.emit('filterPreferences'); diff --git a/src/utils.js b/src/utils.js index b20abd07fd..0e5caa2c93 100644 --- a/src/utils.js +++ b/src/utils.js @@ -89,35 +89,57 @@ export function getDefaultFilterPreferences(): FilterPreferences { }; } +function getSavedFilterPreferencesFilter(key, value) { + if (typeof value === 'string' && value.indexOf('__REGEXP__') === 0) { + const match = value.substr(9).match(/\/(.*)\/(.*)?/); + return new RegExp(match[1], match[2] || ''); + } + return value; +} + export function getSavedFilterPreferences(): FilterPreferences { const raw = localStorage.getItem(LOCAL_STORAGE_FILTER_PREFERENCES_KEY); if (raw != null) { - const json = JSON.parse(raw); + const json = JSON.parse(raw, getSavedFilterPreferencesFilter); return { hideElementsWithTypes: new Set(json.hideElementsWithTypes), - hideElementsWithDisplayNames: new Set(json.hideElementsWithDisplayNames), - hideElementsWithPaths: new Set(json.hideElementsWithPaths), + hideElementsWithDisplayNames: new Set( + json.hideElementsWithDisplayNames.map(source => new RegExp(source)) + ), + hideElementsWithPaths: new Set( + json.hideElementsWithPaths.map(source => new RegExp(source)) + ), }; } else { return getDefaultFilterPreferences(); } } +function saveFilterPreferencesFilter(key, value) { + if (value instanceof RegExp) { + return '__REGEXP__' + value.toString(); + } + return value; +} + export function saveFilterPreferences( filterPreferences: FilterPreferences ): void { localStorage.setItem( LOCAL_STORAGE_FILTER_PREFERENCES_KEY, - JSON.stringify({ - hideElementsWithTypes: Array.from( - filterPreferences.hideElementsWithTypes - ), - hideElementsWithDisplayNames: Array.from( - filterPreferences.hideElementsWithDisplayNames - ), - hideElementsWithPaths: Array.from( - filterPreferences.hideElementsWithPaths - ), - }) + JSON.stringify( + { + hideElementsWithTypes: Array.from( + filterPreferences.hideElementsWithTypes + ), + hideElementsWithDisplayNames: Array.from( + filterPreferences.hideElementsWithDisplayNames + ), + hideElementsWithPaths: Array.from( + filterPreferences.hideElementsWithPaths + ), + }, + saveFilterPreferencesFilter + ) ); }