diff --git a/flow.js b/flow.js index fbbc999554..740d69f34b 100644 --- a/flow.js +++ b/flow.js @@ -1,11 +1,17 @@ // @flow declare module 'events' { - declare class EventEmitter { - addListener: (type: string, fn: Function) => void; - emit: (type: string, data: any) => void; - removeListener: (type: string, fn: Function) => void; - removeAllListeners: (type?: string) => void; + declare class EventEmitter { + addListener>( + event: Event, + listener: (...$ElementType) => any + ): void; + emit: >( + event: Event, + ...$ElementType + ) => void; + removeListener(event: $Keys, listener: Function): void; + removeAllListeners(event?: $Keys): void; } declare export default typeof EventEmitter; diff --git a/src/backend/agent.js b/src/backend/agent.js index 12ba9a8a14..d8dcc15862 100644 --- a/src/backend/agent.js +++ b/src/backend/agent.js @@ -3,6 +3,7 @@ import EventEmitter from 'events'; import memoize from 'memoize-one'; import throttle from 'lodash.throttle'; +import Bridge from 'src/bridge'; import { SESSION_STORAGE_LAST_SELECTION_KEY, SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, @@ -23,7 +24,7 @@ import type { RendererID, RendererInterface, } from './types'; -import type { Bridge, ComponentFilter } from '../types'; +import type { ComponentFilter } from '../types'; const debug = (methodName, ...args) => { if (__DEBUG__) { @@ -67,7 +68,9 @@ type PersistedSelection = {| path: Array, |}; -export default class Agent extends EventEmitter { +export default class Agent extends EventEmitter<{| + shutdown: [], +|}> { _bridge: Bridge; _isProfiling: boolean = false; _recordChangeDescriptions: boolean = false; @@ -273,9 +276,11 @@ export default class Agent extends EventEmitter { screenshotCaptured = ({ commitIndex, dataURL, + rootID, }: {| commitIndex: number, dataURL: string, + rootID: number, |}) => { this._bridge.send('screenshotCaptured', { commitIndex, dataURL }); }; diff --git a/src/backend/types.js b/src/backend/types.js index fe7b5dcfca..d2fd74139e 100644 --- a/src/backend/types.js +++ b/src/backend/types.js @@ -13,10 +13,10 @@ export type ExpirationTime = number; export type RefObject = {| current: any, |}; -export type Source = { +export type Source = {| fileName: string, lineNumber: number, -}; +|}; export type HookType = | 'useState' | 'useReducer' diff --git a/src/bridge.js b/src/bridge.js index 1619e51852..c6c3f1b223 100644 --- a/src/bridge.js +++ b/src/bridge.js @@ -2,16 +2,85 @@ import EventEmitter from 'events'; -import type { Wall } from './types'; +import type { ComponentFilter, Wall } from './types'; +import type { + InspectedElement, + OwnersList, + ProfilingDataBackend, + RendererID, +} from 'src/backend/types'; const BATCH_DURATION = 100; +type ElementAndRendererID = {| id: number, rendererID: RendererID |}; + type Message = {| event: string, payload: any, |}; -export default class Bridge extends EventEmitter { +type HighlightElementInDOM = {| + ...ElementAndRendererID, + displayName: string, + hideAfterTimeout: boolean, + openNativeElementsPanel: boolean, + scrollIntoView: boolean, +|}; + +type OverrideValue = {| + ...ElementAndRendererID, + path: Array, + value: any, +|}; + +type OverrideHookState = {| + ...OverrideValue, + hookID: number, +|}; + +type OverrideSuspense = {| + ...ElementAndRendererID, + forceFallback: boolean, +|}; + +export default class Bridge extends EventEmitter<{| + captureScreenshot: [{| commitIndex: number, rootID: number |}], + clearHighlightedElementInDOM: [], + getOwnersList: [ElementAndRendererID], + getProfilingData: [{| rendererID: RendererID |}], + getProfilingStatus: [], + highlightElementInDOM: [HighlightElementInDOM], + init: [], + inspectElement: [ElementAndRendererID], + inspectedElement: [InspectedElement | number | null], + isBackendStorageAPISupported: [boolean], + logElementToConsole: [ElementAndRendererID], + operations: [Uint32Array], + ownersList: [OwnersList], + overrideContext: [OverrideValue], + overrideHookState: [OverrideHookState], + overrideProps: [OverrideValue], + overrideState: [OverrideValue], + overrideSuspense: [OverrideSuspense], + profilingData: [ProfilingDataBackend], + profilingStatus: [boolean], + reloadAndProfile: [boolean], + reloadAppForProfiling: [], + screenshotCaptured: [ + {| commitIndex: number, dataURL: string, rootID: number |}, + ], + selectElement: [ElementAndRendererID], + selectFiber: [number], + shutdown: [], + startInspectingDOM: [], + startProfiling: [boolean], + stopInspectingDOM: [boolean], + stopProfiling: [], + syncSelectionFromNativeElementsPanel: [], + syncSelectionToNativeElementsPanel: [], + updateComponentFilters: [Array], + viewElementSource: [ElementAndRendererID], +|}> { _isShutdown: boolean = false; _messageQueue: Array = []; _timeoutID: TimeoutID | null = null; @@ -25,7 +94,7 @@ export default class Bridge extends EventEmitter { this._wallUnlisten = wall.listen((message: Message) => { - this.emit(message.event, message.payload); + (this: any).emit(message.event, message.payload); }) || null; } @@ -64,7 +133,9 @@ export default class Bridge extends EventEmitter { this._isShutdown = true; // Disable the API inherited from EventEmitter that can add more listeners and send more messages. + // $FlowFixMe This property is not writable. this.addListener = function() {}; + // $FlowFixMe This property is not writable. this.emit = function() {}; // NOTE: There's also EventEmitter API like `on` and `prependListener` that we didn't add to our Flow type of EventEmitter. diff --git a/src/devtools/ProfilerStore.js b/src/devtools/ProfilerStore.js index b15726c542..1f20fa2a05 100644 --- a/src/devtools/ProfilerStore.js +++ b/src/devtools/ProfilerStore.js @@ -3,6 +3,7 @@ import EventEmitter from 'events'; import memoize from 'memoize-one'; import throttle from 'lodash.throttle'; +import Bridge from 'src/bridge'; import { prepareProfilingDataFrontendFromBackendAndStore } from './views/Profiler/utils'; import ProfilingCache from './ProfilingCache'; import Store from './store'; @@ -14,11 +15,14 @@ import type { ProfilingDataFrontend, SnapshotNode, } from './views/Profiler/types'; -import type { Bridge } from '../types'; const THROTTLE_CAPTURE_SCREENSHOT_DURATION = 500; -export default class ProfilerStore extends EventEmitter { +export default class ProfilerStore extends EventEmitter<{| + isProcessingData: [], + isProfiling: [], + profilingData: [], +|}> { _bridge: Bridge; // Suspense cache for lazily calculating derived profiling data. diff --git a/src/devtools/index.js b/src/devtools/index.js index e656198314..b41b51a110 100644 --- a/src/devtools/index.js +++ b/src/devtools/index.js @@ -1,6 +1,6 @@ // @flow -import type { Bridge } from '../types'; +import Bridge from 'src/bridge'; type Shell = {| connect: (callback: Function) => void, diff --git a/src/devtools/store.js b/src/devtools/store.js index d4501273a8..5e2e14d983 100644 --- a/src/devtools/store.js +++ b/src/devtools/store.js @@ -2,6 +2,7 @@ import EventEmitter from 'events'; import { inspect } from 'util'; +import Bridge from 'src/bridge'; import { TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, @@ -21,7 +22,7 @@ import { printStore } from 'src/__tests__/storeSerializer'; import ProfilerStore from './ProfilerStore'; import type { Element } from './views/Components/types'; -import type { Bridge, ComponentFilter, ElementType } from '../types'; +import type { ComponentFilter, ElementType } from '../types'; const debug = (methodName, ...args) => { if (__DEBUG__) { @@ -57,7 +58,16 @@ export type Capabilities = {| * The store is the single source of truth for updates from the backend. * ContextProviders can subscribe to the Store for specific things they want to provide. */ -export default class Store extends EventEmitter { +export default class Store extends EventEmitter<{| + captureScreenshots: [], + collapseNodesByDefault: [], + componentFilters: [], + mutated: [[Array, Map]], + recordChangeDescriptions: [], + roots: [], + supportsProfiling: [], + supportsReloadAndProfile: [], +|}> { _bridge: Bridge; _captureScreenshots: boolean = false; diff --git a/src/devtools/views/Components/InspectedElementContext.js b/src/devtools/views/Components/InspectedElementContext.js index c27d97eb8b..119cb47d9e 100644 --- a/src/devtools/views/Components/InspectedElementContext.js +++ b/src/devtools/views/Components/InspectedElementContext.js @@ -14,28 +14,33 @@ import { hydrate } from 'src/hydration'; import { TreeStateContext } from './TreeContext'; import { separateDisplayNameAndHOCs } from 'src/utils'; +import type { InspectedElement as InspectedElementBackend } from 'src/backend/types'; import type { DehydratedData, Element, - InspectedElement, + InspectedElement as InspectedElementFrontend, } from 'src/devtools/views/Components/types'; import type { Resource, Thenable } from '../../cache'; type Context = {| - read(id: number): InspectedElement | null, + read(id: number): InspectedElementFrontend | null, |}; const InspectedElementContext = createContext(((null: any): Context)); InspectedElementContext.displayName = 'InspectedElementContext'; -type ResolveFn = (inspectedElement: InspectedElement) => void; +type ResolveFn = (inspectedElement: InspectedElementFrontend) => void; type InProgressRequest = {| - promise: Thenable, + promise: Thenable, resolveFn: ResolveFn, |}; const inProgressRequests: WeakMap = new WeakMap(); -const resource: Resource = createResource( +const resource: Resource< + Element, + Element, + InspectedElementFrontend +> = createResource( (element: Element) => { let request = inProgressRequests.get(element); if (request != null) { @@ -85,7 +90,7 @@ function InspectedElementContextController({ children }: Props) { // This effect handler invalidates the suspense cache and schedules rendering updates with React. useEffect(() => { const onInspectedElement = ( - inspectedElement: InspectedElement | number | null + data: InspectedElementBackend | number | null ) => { // A null value means that the element no longer exists in the backend. // If it's the same element that's currently selected, that selection will be removed once the Store updates. @@ -93,23 +98,21 @@ function InspectedElementContextController({ children }: Props) { // Either way there is nothing we need to do in this case. // A numeric value indicates that the element hasn't changed since we last requested its data, // in which case we don't need to invalidate the cache and re-render anything in the DevTools. - if (inspectedElement !== null && typeof inspectedElement === 'object') { - const id = inspectedElement.id; + if (data !== null && typeof data === 'object') { + const id = data.id; - const { displayName, type } = inspectedElement; - - const [ - displayNameWithoutHOCs, - hocDisplayNames, - ] = separateDisplayNameAndHOCs(displayName, type); - - inspectedElement = (({ - ...inspectedElement, - displayName: displayNameWithoutHOCs, + const inspectedElement: InspectedElementFrontend = { + canEditFunctionProps: data.canEditFunctionProps, + canEditHooks: data.canEditHooks, + canToggleSuspense: data.canToggleSuspense, + canViewSource: data.canViewSource, + id: data.id, + source: data.source, + type: data.type, owners: - inspectedElement.owners === null + data.owners === null ? null - : inspectedElement.owners.map(owner => { + : data.owners.map(owner => { const [ displayName, hocDisplayNames, @@ -120,13 +123,12 @@ function InspectedElementContextController({ children }: Props) { hocDisplayNames, }; }), - hocDisplayNames, - context: hydrateHelper(inspectedElement.context), - events: hydrateHelper(inspectedElement.events), - hooks: hydrateHelper(inspectedElement.hooks), - props: hydrateHelper(inspectedElement.props), - state: hydrateHelper(inspectedElement.state), - }: any): InspectedElement); + context: hydrateHelper(data.context), + events: hydrateHelper(data.events), + hooks: hydrateHelper(data.hooks), + props: hydrateHelper(data.props), + state: hydrateHelper(data.state), + }; const element = store.getElementByID(id); if (element !== null) { @@ -174,16 +176,13 @@ function InspectedElementContextController({ children }: Props) { bridge.send('selectElement', { id: selectedElementID, rendererID }); const onInspectedElement = ( - inspectedElement: InspectedElement | number | null + data: InspectedElementBackend | number | null ) => { - if (inspectedElement !== null) { + if (data !== null) { // If this is the element we requested, wait a little bit and then ask for an update. - if (inspectedElement === selectedElementID) { + if (data === selectedElementID) { timeoutID = setTimeout(sendRequest, 1000); - } else if ( - typeof inspectedElement === 'object' && - inspectedElement.id === selectedElementID - ) { + } else if (typeof data === 'object' && data.id === selectedElementID) { timeoutID = setTimeout(sendRequest, 1000); } } diff --git a/src/devtools/views/Components/OwnersListContext.js b/src/devtools/views/Components/OwnersListContext.js index f83b1a6d39..aa7e2dab05 100644 --- a/src/devtools/views/Components/OwnersListContext.js +++ b/src/devtools/views/Components/OwnersListContext.js @@ -11,11 +11,8 @@ import { BridgeContext, StoreContext } from '../context'; import { TreeStateContext } from './TreeContext'; import { separateDisplayNameAndHOCs } from 'src/utils'; -import type { - Element, - Owner, - OwnersList, -} from 'src/devtools/views/Components/types'; +import type { OwnersList } from 'src/backend/types'; +import type { Element, Owner } from 'src/devtools/views/Components/types'; import type { Resource, Thenable } from '../../cache'; type Context = (id: number) => Array | null; diff --git a/src/devtools/views/Components/types.js b/src/devtools/views/Components/types.js index 106b184ac5..a1c2a2b80c 100644 --- a/src/devtools/views/Components/types.js +++ b/src/devtools/views/Components/types.js @@ -52,8 +52,6 @@ export type Source = {| export type InspectedElement = {| id: number, - displayName: string | null, - // Does the current renderer support editable hooks? canEditHooks: boolean, diff --git a/src/devtools/views/DevTools.js b/src/devtools/views/DevTools.js index c02eef4fe1..606ffb706a 100644 --- a/src/devtools/views/DevTools.js +++ b/src/devtools/views/DevTools.js @@ -6,6 +6,7 @@ import '@reach/menu-button/styles.css'; import '@reach/tooltip/styles.css'; import React, { useState } from 'react'; +import Bridge from 'src/bridge'; import Store from '../store'; import { BridgeContext, StoreContext } from './context'; import Components from './Components/Components'; @@ -23,8 +24,6 @@ import styles from './DevTools.css'; import './root.css'; -import type { Bridge } from '../../types'; - export type BrowserName = 'Chrome' | 'Firefox'; export type BrowserTheme = 'dark' | 'light'; export type TabID = 'components' | 'profiler' | 'settings'; diff --git a/src/devtools/views/context.js b/src/devtools/views/context.js index 5e25f1940e..e5ede9ef48 100644 --- a/src/devtools/views/context.js +++ b/src/devtools/views/context.js @@ -1,8 +1,7 @@ // @flow import { createContext } from 'react'; - -import type { Bridge } from '../../types'; +import Bridge from 'src/bridge'; import Store from '../store'; diff --git a/src/types.js b/src/types.js index ca1329af9d..446e63ddc7 100644 --- a/src/types.js +++ b/src/types.js @@ -1,11 +1,5 @@ // @flow -export type Bridge = { - addListener(type: string, callback: Function): void, - removeListener(type: string, callback: Function): void, - send(event: string, payload: any, transferable?: Array): void, -}; - export type Wall = {| // `listen` returns the "unlisten" function. listen: (fn: Function) => Function,