From 1029a6c97cf8afac0d8f10c359777acfd81290a8 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Wed, 1 Jul 2020 08:58:40 -0700 Subject: [PATCH] Introduce types for Sandy plugins through code base Summary: So far there were 2 types of plugins: `FlipperPlugin` and `FlipperDevicePlugin`. This introduces a third kind: `SandyPluginDefinition`. Unlike with the old plugins, the export of the module is not directly exposed as the plugin definition. Rather, we use class `SandyPluginDefinition` (instance) that holds a loaded definition and its meta data separately (`PluginDetails`). This means that we don't have to mix in and mutate loaded definitions, and that for unit tests we can avoid needing to provide a bunch of meta data. This also prevents a bunch of meta data existing on two places: on the loaded classes as static fields, and in the meta data field of the loaded class as well. Finally, we can now freely extends the `PluginDetails` interface in flipper, without needing to store it on the loaded classes and we are sure that no naming conflicts are caused by this in the future. For compatibility with the existing code base, common fields are delegated from the `SandyPluginDefinition` class to the meta data. Also cleaned up types around plugins a little bit and removed some unnecessary casts. For all features that reason about plugins in general (such as exports), sandy plugins are ignored for now. `SandyPluginInstance` is worked out in further diffs The `instanceof` calls are replaced by a utility function in later diffs. {F241363645} Reviewed By: jknoxville Differential Revision: D22091432 fbshipit-source-id: 3aa6b12fda5925268913779f3c3c9e84494438f8 --- desktop/.eslintrc.js | 1 + desktop/app/src/Client.tsx | 39 ++--- desktop/app/src/MenuBar.tsx | 4 +- desktop/app/src/NotificationsHub.tsx | 10 +- desktop/app/src/PluginContainer.tsx | 19 ++- .../src/chrome/mainsidebar/MainSidebar2.tsx | 13 +- .../src/chrome/mainsidebar/sidebarUtils.tsx | 7 +- .../chrome/plugin-manager/PluginDebugger.tsx | 22 +-- .../src/dispatcher/__tests__/plugins.node.tsx | 1 + desktop/app/src/dispatcher/notifications.tsx | 17 ++- desktop/app/src/dispatcher/plugins.tsx | 14 +- desktop/app/src/plugin.tsx | 20 ++- desktop/app/src/reducers/connections.tsx | 8 +- desktop/app/src/reducers/plugins.tsx | 21 ++- desktop/app/src/utils/exportData.tsx | 33 +++-- desktop/app/src/utils/exportMetrics.tsx | 19 +-- desktop/app/src/utils/messageQueue.tsx | 21 ++- desktop/app/src/utils/onRegisterDevice.tsx | 14 +- desktop/app/src/utils/pluginStateRecorder.tsx | 6 +- desktop/app/src/utils/pluginUtils.tsx | 56 ++++---- desktop/flipper-plugin/src/index.ts | 4 +- desktop/flipper-plugin/src/plugin/Plugin.tsx | 133 ++++++++++++++++++ desktop/flipper-plugin/tsconfig.json | 10 +- 23 files changed, 332 insertions(+), 160 deletions(-) create mode 100644 desktop/flipper-plugin/src/plugin/Plugin.tsx diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js index d38cd3a66..5d7ed4d84 100644 --- a/desktop/.eslintrc.js +++ b/desktop/.eslintrc.js @@ -15,6 +15,7 @@ const pattern = /^\*\r?\n[\S\s]*Facebook[\S\s]* \* @format\r?\n/; const builtInModules = [ 'flipper', 'flipper-plugin', + 'flipper-plugin-lib', 'react', 'react-dom', 'electron', diff --git a/desktop/app/src/Client.tsx b/desktop/app/src/Client.tsx index 81d60dc06..122efd522 100644 --- a/desktop/app/src/Client.tsx +++ b/desktop/app/src/Client.tsx @@ -7,7 +7,7 @@ * @format */ -import {FlipperPlugin, FlipperDevicePlugin} from './plugin'; +import {PluginDefinition, ClientPluginDefinition} from './plugin'; import BaseDevice, {OS} from './devices/BaseDevice'; import {App} from './App'; import {Logger} from './fb-interfaces/Logger'; @@ -32,6 +32,7 @@ import {sideEffect} from './utils/sideEffect'; import {emitBytesReceived} from './dispatcher/tracking'; import {debounce} from 'lodash'; import {batch} from 'react-redux'; +import {SandyPluginDefinition} from 'flipper-plugin'; type Plugins = Array; @@ -130,7 +131,7 @@ export default class Client extends EventEmitter { messageBuffer: Record< string /*pluginKey*/, { - plugin: typeof FlipperPlugin | typeof FlipperDevicePlugin; + plugin: PluginDefinition; messages: Params[]; } > = {}; @@ -244,7 +245,7 @@ export default class Client extends EventEmitter { }); } - supportsPlugin(Plugin: typeof FlipperPlugin): boolean { + supportsPlugin(Plugin: ClientPluginDefinition): boolean { return this.plugins.includes(Plugin.id); } @@ -393,14 +394,18 @@ export default class Client extends EventEmitter { const bytes = msg.length * 2; // string lengths are measured in UTF-16 units (not characters), so 2 bytes per char emitBytesReceived(params.api, bytes); - const persistingPlugin: - | typeof FlipperPlugin - | typeof FlipperDevicePlugin - | undefined = + const persistingPlugin: PluginDefinition | undefined = this.store.getState().plugins.clientPlugins.get(params.api) || this.store.getState().plugins.devicePlugins.get(params.api); - if (persistingPlugin && persistingPlugin.persistedStateReducer) { + let handled = false; // This is just for analysis + // TODO: support Sandy plugins T68683442 + if ( + persistingPlugin && + !(persistingPlugin instanceof SandyPluginDefinition) && + persistingPlugin.persistedStateReducer + ) { + handled = true; const pluginKey = getPluginKey( this.id, {serial: this.query.device_id}, @@ -417,16 +422,18 @@ export default class Client extends EventEmitter { this.flushMessageBufferDebounced(); } const apiCallbacks = this.broadcastCallbacks.get(params.api); - if (!apiCallbacks) { - return; - } - - const methodCallbacks = apiCallbacks.get(params.method); - if (methodCallbacks) { - for (const callback of methodCallbacks) { - callback(params.params); + if (apiCallbacks) { + const methodCallbacks = apiCallbacks.get(params.method); + if (methodCallbacks) { + for (const callback of methodCallbacks) { + handled = true; + callback(params.params); + } } } + if (!handled) { + console.warn(`Unhandled message ${params.api}.${params.method}`); + } } return; // method === 'execute' } diff --git a/desktop/app/src/MenuBar.tsx b/desktop/app/src/MenuBar.tsx index e028dc6c7..82b5f4eeb 100644 --- a/desktop/app/src/MenuBar.tsx +++ b/desktop/app/src/MenuBar.tsx @@ -7,7 +7,7 @@ * @format */ -import {FlipperPlugin, FlipperDevicePlugin} from './plugin'; +import {FlipperPlugin, FlipperDevicePlugin, PluginDefinition} from './plugin'; import { showOpenDialog, startFileExport, @@ -71,7 +71,7 @@ function actionHandler(action: string) { } export function setupMenuBar( - plugins: Array, + plugins: PluginDefinition[], store: Store, logger: Logger, ) { diff --git a/desktop/app/src/NotificationsHub.tsx b/desktop/app/src/NotificationsHub.tsx index b8dc501c3..ee7c9e1b9 100644 --- a/desktop/app/src/NotificationsHub.tsx +++ b/desktop/app/src/NotificationsHub.tsx @@ -7,7 +7,7 @@ * @format */ -import {SearchableProps, FlipperBasePlugin, FlipperPlugin} from 'flipper'; +import {SearchableProps} from 'flipper'; import {Logger} from './fb-interfaces/Logger'; import { Searchable, @@ -21,7 +21,7 @@ import { styled, colors, } from 'flipper'; -import {FlipperDevicePlugin} from './plugin'; +import {PluginDefinition, DevicePluginMap, ClientPluginMap} from './plugin'; import {connect} from 'react-redux'; import React, {Component, Fragment} from 'react'; import {clipboard} from 'electron'; @@ -47,8 +47,8 @@ type StateFromProps = { invalidatedNotifications: Array; blacklistedPlugins: Array; blacklistedCategories: Array; - devicePlugins: Map; - clientPlugins: Map; + devicePlugins: DevicePluginMap; + clientPlugins: ClientPluginMap; }; type DispatchFromProps = { @@ -417,7 +417,7 @@ type ItemProps = { deepLinkPayload: string | null; }) => any; logger?: Logger; - plugin: typeof FlipperBasePlugin | null | undefined; + plugin: PluginDefinition | null | undefined; }; type ItemState = { diff --git a/desktop/app/src/PluginContainer.tsx b/desktop/app/src/PluginContainer.tsx index af71bfa1b..765611996 100644 --- a/desktop/app/src/PluginContainer.tsx +++ b/desktop/app/src/PluginContainer.tsx @@ -11,6 +11,7 @@ import { FlipperPlugin, FlipperDevicePlugin, Props as PluginProps, + PluginDefinition, } from './plugin'; import {Logger} from './fb-interfaces/Logger'; import BaseDevice from './devices/BaseDevice'; @@ -45,6 +46,7 @@ import {Message} from './reducers/pluginMessageQueue'; import {Idler} from './utils/Idler'; import {processMessageQueue} from './utils/messageQueue'; import {ToggleButton, SmallText} from './ui'; +import {SandyPluginDefinition} from 'flipper-plugin'; const Container = styled(FlexColumn)({ width: 0, @@ -95,7 +97,7 @@ type OwnProps = { type StateFromProps = { pluginState: Object; - activePlugin: typeof FlipperPlugin | typeof FlipperDevicePlugin | null; + activePlugin: PluginDefinition | undefined; target: Client | BaseDevice | null; pluginKey: string | null; deepLinkPayload: string | null; @@ -190,6 +192,8 @@ class PluginContainer extends PureComponent { if ( pluginIsEnabled && activePlugin && + // TODO: support sandy: T68683442 + !(activePlugin instanceof SandyPluginDefinition) && activePlugin.persistedStateReducer && pluginKey && pendingMessages?.length @@ -328,6 +332,10 @@ class PluginContainer extends PureComponent { console.warn(`No selected plugin. Rendering empty!`); return null; } + if (activePlugin instanceof SandyPluginDefinition) { + // TODO: + return null; + } const props: PluginProps & { key: string; ref: ( @@ -404,14 +412,11 @@ export default connect( }) => { let pluginKey = null; let target = null; - let activePlugin: - | typeof FlipperDevicePlugin - | typeof FlipperPlugin - | null = null; + let activePlugin: PluginDefinition | undefined; let pluginIsEnabled = false; if (selectedPlugin) { - activePlugin = devicePlugins.get(selectedPlugin) || null; + activePlugin = devicePlugins.get(selectedPlugin); target = selectedDevice; if (selectedDevice && activePlugin) { pluginKey = getPluginKey(selectedDevice.serial, activePlugin.id); @@ -419,7 +424,7 @@ export default connect( } else { target = clients.find((client: Client) => client.id === selectedApp) || null; - activePlugin = clientPlugins.get(selectedPlugin) || null; + activePlugin = clientPlugins.get(selectedPlugin); if (activePlugin && target) { pluginKey = getPluginKey(target.id, activePlugin.id); pluginIsEnabled = pluginIsStarred( diff --git a/desktop/app/src/chrome/mainsidebar/MainSidebar2.tsx b/desktop/app/src/chrome/mainsidebar/MainSidebar2.tsx index 2d598812d..510893960 100644 --- a/desktop/app/src/chrome/mainsidebar/MainSidebar2.tsx +++ b/desktop/app/src/chrome/mainsidebar/MainSidebar2.tsx @@ -20,8 +20,6 @@ import { Glyph, styled, GK, - FlipperPlugin, - FlipperDevicePlugin, ArchivedDevice, SmallText, Info, @@ -62,8 +60,9 @@ import { getFavoritePlugins, } from './sidebarUtils'; import {useLocalStorage} from '../../utils/useLocalStorage'; +import {PluginDefinition, ClientPluginMap, DevicePluginMap} from '../../plugin'; -type FlipperPlugins = typeof FlipperPlugin[]; +type FlipperPlugins = PluginDefinition[]; type PluginsByCategory = [string, FlipperPlugins][]; type SectionLevel = 1 | 2 | 3; @@ -184,8 +183,8 @@ type StateFromProps = { deviceId?: string; errorMessage?: string; }>; - devicePlugins: Map; - clientPlugins: Map; + devicePlugins: DevicePluginMap; + clientPlugins: ClientPluginMap; }; type SelectPlugin = (payload: { @@ -469,7 +468,7 @@ const PluginList = memo(function PluginList({ }: { client: Client; device: BaseDevice; - clientPlugins: Map; + clientPlugins: ClientPluginMap; starPlugin: typeof starPluginAction; userStarredPlugins: Store['connections']['userStarredPlugins']; selectedPlugin?: null | string; @@ -497,7 +496,7 @@ const PluginList = memo(function PluginList({ ); const allPlugins = Array.from(clientPlugins.values()).filter( - (p: typeof FlipperPlugin) => client.plugins.indexOf(p.id) > -1, + (p) => client.plugins.indexOf(p.id) > -1, ); const favoritePlugins: FlipperPlugins = getFavoritePlugins( device, diff --git a/desktop/app/src/chrome/mainsidebar/sidebarUtils.tsx b/desktop/app/src/chrome/mainsidebar/sidebarUtils.tsx index 36064679f..f5a5a79d4 100644 --- a/desktop/app/src/chrome/mainsidebar/sidebarUtils.tsx +++ b/desktop/app/src/chrome/mainsidebar/sidebarUtils.tsx @@ -14,9 +14,7 @@ import { Text, Glyph, styled, - FlipperPlugin, FlexColumn, - FlipperBasePlugin, ToggleButton, brandColors, Spacer, @@ -27,8 +25,9 @@ import { } from 'flipper'; import {BackgroundColorProperty} from 'csstype'; import {getPluginTitle} from '../../utils/pluginUtils'; +import {PluginDefinition} from '../../plugin'; -export type FlipperPlugins = typeof FlipperPlugin[]; +export type FlipperPlugins = PluginDefinition[]; export type PluginsByCategory = [string, FlipperPlugins][]; export const ListItem = styled.div<{active?: boolean; disabled?: boolean}>( @@ -133,7 +132,7 @@ export const Plugins = styled(FlexColumn)({ export const PluginSidebarListItem: React.FC<{ onClick: () => void; isActive: boolean; - plugin: typeof FlipperBasePlugin; + plugin: PluginDefinition; app?: string | null | undefined; helpRef?: any; provided?: any; diff --git a/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx b/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx index 09553cc3b..9cc1d175c 100644 --- a/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx +++ b/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx @@ -12,18 +12,10 @@ import Client from '../../Client'; import {TableBodyRow} from '../../ui/components/table/types'; import React, {Component, Fragment} from 'react'; import {connect} from 'react-redux'; -import { - Text, - ManagedTable, - styled, - colors, - Link, - FlipperPlugin, - FlipperDevicePlugin, - Bordered, -} from 'flipper'; +import {Text, ManagedTable, styled, colors, Link, Bordered} from 'flipper'; import StatusIndicator from '../../ui/components/StatusIndicator'; import {State as Store} from '../../reducers'; +import {DevicePluginDefinition, ClientPluginDefinition} from '../../plugin'; const InfoText = styled(Text)({ lineHeight: '130%', @@ -51,8 +43,8 @@ type StateFromProps = { failedPlugins: Array<[PluginDetails, string]>; clients: Array; selectedDevice: string | null | undefined; - devicePlugins: Array; - clientPlugins: Array; + devicePlugins: DevicePluginDefinition[]; + clientPlugins: ClientPluginDefinition[]; }; type DispatchFromProps = {}; @@ -299,10 +291,8 @@ export default connect( }, connections: {clients, selectedDevice}, }) => ({ - devicePlugins: Array.from( - devicePlugins.values(), - ), - clientPlugins: Array.from(clientPlugins.values()), + devicePlugins: Array.from(devicePlugins.values()), + clientPlugins: Array.from(clientPlugins.values()), gatekeepedPlugins, clients, disabledPlugins, diff --git a/desktop/app/src/dispatcher/__tests__/plugins.node.tsx b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx index 8a62425e3..961db6852 100644 --- a/desktop/app/src/dispatcher/__tests__/plugins.node.tsx +++ b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx @@ -162,6 +162,7 @@ test('requirePlugin loads plugin', () => { version: '1.0.0', }); expect(plugin).not.toBeNull(); + // @ts-ignore expect(plugin!.prototype).toBeInstanceOf(FlipperPlugin); expect(plugin!.id).toBe(TestPlugin.id); }); diff --git a/desktop/app/src/dispatcher/notifications.tsx b/desktop/app/src/dispatcher/notifications.tsx index 0343a5ef9..08dfaa757 100644 --- a/desktop/app/src/dispatcher/notifications.tsx +++ b/desktop/app/src/dispatcher/notifications.tsx @@ -10,7 +10,7 @@ import {Store} from '../reducers/index'; import {Logger} from '../fb-interfaces/Logger'; import {PluginNotification} from '../reducers/notifications'; -import {FlipperPlugin, FlipperDevicePlugin} from '../plugin'; +import {PluginDefinition} from '../plugin'; import isHeadless from '../utils/isHeadless'; import {setStaticView, setDeeplinkPayload} from '../reducers/connections'; import {ipcRenderer, IpcRendererEvent} from 'electron'; @@ -25,6 +25,7 @@ import {deconstructPluginKey} from '../utils/clientUtils'; import NotificationScreen from '../chrome/NotificationScreen'; import {getPluginTitle} from '../utils/pluginUtils'; import {sideEffect} from '../utils/sideEffect'; +import {SandyPluginDefinition} from 'flipper-plugin'; type NotificationEvents = 'show' | 'click' | 'close' | 'reply' | 'action'; const NOTIFICATION_THROTTLE = 5 * 1000; // in milliseconds @@ -110,11 +111,15 @@ export default (store: Store, logger: Logger) => { return; } - const persistingPlugin: - | undefined - | typeof FlipperPlugin - | typeof FlipperDevicePlugin = getPlugin(pluginName); - if (persistingPlugin && persistingPlugin.getActiveNotifications) { + const persistingPlugin: undefined | PluginDefinition = getPlugin( + pluginName, + ); + // TODO: add support for Sandy plugins T68683442 + if ( + persistingPlugin && + !(persistingPlugin instanceof SandyPluginDefinition) && + persistingPlugin.getActiveNotifications + ) { try { const notifications = persistingPlugin.getActiveNotifications( pluginStates[key], diff --git a/desktop/app/src/dispatcher/plugins.tsx b/desktop/app/src/dispatcher/plugins.tsx index e6e573b6e..b14c2ed5a 100644 --- a/desktop/app/src/dispatcher/plugins.tsx +++ b/desktop/app/src/dispatcher/plugins.tsx @@ -9,7 +9,7 @@ import {Store} from '../reducers/index'; import {Logger} from '../fb-interfaces/Logger'; -import {FlipperPlugin, FlipperDevicePlugin} from '../plugin'; +import {PluginDefinition} from '../plugin'; import React from 'react'; import ReactDOM from 'react-dom'; import adbkit from 'adbkit'; @@ -58,9 +58,10 @@ export default (store: Store, logger: Logger) => { const defaultPluginsIndex = getPluginIndex(); - const initialPlugins: Array< - typeof FlipperPlugin | typeof FlipperDevicePlugin - > = filterNewestVersionOfEachPlugin(getBundledPlugins(), getDynamicPlugins()) + const initialPlugins: PluginDefinition[] = filterNewestVersionOfEachPlugin( + getBundledPlugins(), + getDynamicPlugins(), + ) .map(reportVersion) .filter(checkDisabled(disabledPlugins)) .filter(checkGK(gatekeepedPlugins)) @@ -247,9 +248,7 @@ export const requirePlugin = ( defaultPluginsIndex: any, reqFn: Function = global.electronRequire, ) => { - return ( - pluginDetails: PluginDetails, - ): typeof FlipperPlugin | typeof FlipperDevicePlugin | null => { + return (pluginDetails: PluginDetails): PluginDefinition | null => { try { return tryCatchReportPluginFailures( () => requirePluginInternal(pluginDetails, defaultPluginsIndex, reqFn), @@ -281,6 +280,7 @@ const requirePluginInternal = ( plugin.id = plugin.id || pluginDetails.id; plugin.packageName = pluginDetails.name; + plugin.flipperSDKVersion = pluginDetails.flipperSDKVersion; plugin.details = pluginDetails; // set values from package.json as static variables on class diff --git a/desktop/app/src/plugin.tsx b/desktop/app/src/plugin.tsx index 324288b75..b9e60646f 100644 --- a/desktop/app/src/plugin.tsx +++ b/desktop/app/src/plugin.tsx @@ -22,8 +22,21 @@ import {State as ReduxState} from './reducers'; import {DEFAULT_MAX_QUEUE_SIZE} from './reducers/pluginMessageQueue'; import {PluginDetails} from 'flipper-plugin-lib'; import {Settings} from './reducers/settings'; +import {SandyPluginDefinition} from 'flipper-plugin'; type Parameters = {[key: string]: any}; +export type PluginDefinition = ClientPluginDefinition | DevicePluginDefinition; + +// TODO: T68738317 add SandyPluginDefinition +export type DevicePluginDefinition = typeof FlipperDevicePlugin; + +export type ClientPluginDefinition = + | typeof FlipperPlugin + | SandyPluginDefinition; + +export type ClientPluginMap = Map; +export type DevicePluginMap = Map; + // This function is intended to be called from outside of the plugin. // If you want to `call` from the plugin use, this.client.call export function callClient( @@ -100,6 +113,7 @@ export abstract class FlipperBasePlugin< static category: string | null = null; static id: string = ''; static packageName: string = ''; + static flipperSDKVersion: string | undefined = undefined; static version: string = ''; static icon: string | null = null; static gatekeeper: string | null = null; @@ -113,7 +127,7 @@ export abstract class FlipperBasePlugin< static maxQueueSize: number = DEFAULT_MAX_QUEUE_SIZE; static metricsReducer: | ((persistedState: StaticPersistedState) => Promise) - | null; + | undefined; static exportPersistedState: | (( callClient: (method: string, params?: any) => Promise, @@ -123,10 +137,10 @@ export abstract class FlipperBasePlugin< statusUpdate?: (msg: string) => void, supportsMethod?: (method: string) => Promise, ) => Promise) - | null; + | undefined; static getActiveNotifications: | ((persistedState: StaticPersistedState) => Array) - | null; + | undefined; static onRegisterDevice: | (( store: Store, diff --git a/desktop/app/src/reducers/connections.tsx b/desktop/app/src/reducers/connections.tsx index fe97666b2..e9d6a7980 100644 --- a/desktop/app/src/reducers/connections.tsx +++ b/desktop/app/src/reducers/connections.tsx @@ -14,7 +14,6 @@ import MacDevice from '../devices/MacDevice'; import Client from '../Client'; import {UninitializedClient} from '../UninitializedClient'; import {isEqual} from 'lodash'; -import iosUtil from '../utils/iOSContainerUtility'; import {performance} from 'perf_hooks'; import isHeadless from '../utils/isHeadless'; import {Actions} from '.'; @@ -31,6 +30,7 @@ import { import {deconstructClientId} from '../utils/clientUtils'; import {FlipperDevicePlugin} from '../plugin'; import {RegisterPluginAction} from './plugins'; +import {SandyPluginDefinition} from 'flipper-plugin'; export type StaticView = | null @@ -423,7 +423,11 @@ export default (state: State = INITAL_STATE, action: Actions): State => { // plugins are registered after creating the base devices, so update them const plugins = action.payload; plugins.forEach((plugin) => { - if (plugin.prototype instanceof FlipperDevicePlugin) { + // TODO: T68738317 support sandy device plugin + if ( + !(plugin instanceof SandyPluginDefinition) && + plugin.prototype instanceof FlipperDevicePlugin + ) { // smell: devices are mutable state.devices.forEach((device) => { // @ts-ignore diff --git a/desktop/app/src/reducers/plugins.tsx b/desktop/app/src/reducers/plugins.tsx index fe29f8065..9dc450958 100644 --- a/desktop/app/src/reducers/plugins.tsx +++ b/desktop/app/src/reducers/plugins.tsx @@ -7,25 +7,24 @@ * @format */ -import {FlipperPlugin, FlipperDevicePlugin} from '../plugin'; +import {DevicePluginMap, ClientPluginMap, PluginDefinition} from '../plugin'; import {PluginDetails} from 'flipper-plugin-lib'; import {Actions} from '.'; import produce from 'immer'; +import {isDevicePluginDefinition} from '../utils/pluginUtils'; export type State = { - devicePlugins: Map; - clientPlugins: Map; + devicePlugins: DevicePluginMap; + clientPlugins: ClientPluginMap; gatekeepedPlugins: Array; disabledPlugins: Array; failedPlugins: Array<[PluginDetails, string]>; selectedPlugins: Array; }; -type PluginClass = typeof FlipperPlugin | typeof FlipperDevicePlugin; - export type RegisterPluginAction = { type: 'REGISTER_PLUGINS'; - payload: Array; + payload: PluginDefinition[]; }; export type Action = @@ -63,16 +62,14 @@ export default function reducer( if (action.type === 'REGISTER_PLUGINS') { return produce(state, (draft) => { const {devicePlugins, clientPlugins} = draft; - action.payload.forEach((p: PluginClass) => { + action.payload.forEach((p) => { if (devicePlugins.has(p.id) || clientPlugins.has(p.id)) { return; } - if (p.prototype instanceof FlipperDevicePlugin) { - // @ts-ignore doesn't know p must be typeof FlipperDevicePlugin here + if (isDevicePluginDefinition(p)) { devicePlugins.set(p.id, p); - } else if (p.prototype instanceof FlipperPlugin) { - // @ts-ignore doesn't know p must be typeof FlipperPlugin here + } else { clientPlugins.set(p.id, p); } }); @@ -107,7 +104,7 @@ export const selectedPlugins = (payload: Array): Action => ({ payload, }); -export const registerPlugins = (payload: Array): Action => ({ +export const registerPlugins = (payload: PluginDefinition[]): Action => ({ type: 'REGISTER_PLUGINS', payload, }); diff --git a/desktop/app/src/utils/exportData.tsx b/desktop/app/src/utils/exportData.tsx index 43534c3a8..066c88085 100644 --- a/desktop/app/src/utils/exportData.tsx +++ b/desktop/app/src/utils/exportData.tsx @@ -20,10 +20,12 @@ import Client, {ClientExport, ClientQuery} from '../Client'; import {pluginKey} from '../reducers/pluginStates'; import { FlipperDevicePlugin, - FlipperPlugin, callClient, supportsMethod, FlipperBasePlugin, + PluginDefinition, + DevicePluginMap, + ClientPluginMap, } from '../plugin'; import {default as BaseDevice} from '../devices/BaseDevice'; import {default as ArchivedDevice} from '../devices/ArchivedDevice'; @@ -47,6 +49,7 @@ import {processMessageQueue} from './messageQueue'; import {getPluginTitle} from './pluginUtils'; import {capture} from './screenshot'; import {uploadFlipperMedia} from '../fb-stubs/user'; +import {SandyPluginDefinition} from 'flipper-plugin'; export const IMPORT_FLIPPER_TRACE_EVENT = 'import-flipper-trace'; export const EXPORT_FLIPPER_TRACE_EVENT = 'export-flipper-trace'; @@ -93,7 +96,7 @@ type PluginsToProcess = { pluginKey: string; pluginId: string; pluginName: string; - pluginClass: typeof FlipperPlugin | typeof FlipperDevicePlugin; + pluginClass: PluginDefinition; client: Client; }[]; @@ -212,14 +215,17 @@ export function processNotificationStates( const serializePluginStates = async ( pluginStates: PluginStatesState, - clientPlugins: Map, - devicePlugins: Map, + clientPlugins: ClientPluginMap, + devicePlugins: DevicePluginMap, statusUpdate?: (msg: string) => void, idler?: Idler, ): Promise => { const pluginsMap: Map = new Map([]); clientPlugins.forEach((val, key) => { - pluginsMap.set(key, val); + // TODO: Support Sandy T68683449 and use ClientPluginsMap + if (!(val instanceof SandyPluginDefinition)) { + pluginsMap.set(key, val); + } }); devicePlugins.forEach((val, key) => { pluginsMap.set(key, val); @@ -248,12 +254,13 @@ const serializePluginStates = async ( const deserializePluginStates = ( pluginStatesExportState: PluginStatesExportState, - clientPlugins: Map, - devicePlugins: Map, + clientPlugins: ClientPluginMap, + devicePlugins: DevicePluginMap, ): PluginStatesState => { const pluginsMap: Map = new Map([]); clientPlugins.forEach((val, key) => { - pluginsMap.set(key, val); + // TODO: Support Sandy T68683449 + if (!(val instanceof SandyPluginDefinition)) pluginsMap.set(key, val); }); devicePlugins.forEach((val, key) => { pluginsMap.set(key, val); @@ -358,8 +365,8 @@ type ProcessStoreOptions = { device: BaseDevice | null; pluginStates: PluginStatesState; clients: Array; - devicePlugins: Map; - clientPlugins: Map; + devicePlugins: DevicePluginMap; + clientPlugins: ClientPluginMap; salt: string; selectedPlugins: Array; statusUpdate?: (msg: string) => void; @@ -514,7 +521,11 @@ async function processQueues( pluginKey, pluginClass, } of pluginsToProcess) { - if (pluginClass.persistedStateReducer) { + // TODO: Support Sandy T68683449 + if ( + !(pluginClass instanceof SandyPluginDefinition) && + pluginClass.persistedStateReducer + ) { const processQueueMarker = `${EXPORT_FLIPPER_TRACE_EVENT}:process-queue-per-plugin`; performance.mark(processQueueMarker); diff --git a/desktop/app/src/utils/exportMetrics.tsx b/desktop/app/src/utils/exportMetrics.tsx index 8a5a30ad8..7542019c5 100644 --- a/desktop/app/src/utils/exportMetrics.tsx +++ b/desktop/app/src/utils/exportMetrics.tsx @@ -7,7 +7,6 @@ * @format */ -import {FlipperPlugin, FlipperDevicePlugin} from 'flipper'; import {serialize} from './serialization'; import {State as PluginStatesState} from '../reducers/pluginStates'; import {Store} from '../reducers'; @@ -20,6 +19,8 @@ import { import {deserializeObject} from './serialization'; import {deconstructPluginKey} from './clientUtils'; import {pluginsClassMap} from './pluginUtils'; +import {PluginDefinition} from '../plugin'; +import {SandyPluginDefinition} from 'flipper-plugin'; export type MetricType = {[metricName: string]: number}; type MetricPluginType = {[pluginID: string]: MetricType}; @@ -27,7 +28,7 @@ export type ExportMetricType = {[clientID: string]: MetricPluginType}; async function exportMetrics( pluginStates: PluginStatesState, - pluginsMap: Map, + pluginsMap: Map, selectedPlugins: Array, ): Promise { const metrics: ExportMetricType = {}; @@ -46,8 +47,10 @@ async function exportMetrics( const pluginClass = pluginsMap.get(pluginName); const metricsReducer: | ((persistedState: U) => Promise) - | null - | undefined = pluginClass && pluginClass.metricsReducer; + | undefined = + pluginClass && !(pluginClass instanceof SandyPluginDefinition) + ? pluginClass.metricsReducer + : undefined; if (pluginsMap.has(pluginName) && metricsReducer) { const metricsObject = await metricsReducer(pluginStateData); const pluginObject: MetricPluginType = {}; @@ -67,10 +70,7 @@ export async function exportMetricsWithoutTrace( store: Store, pluginStates: PluginStatesState, ): Promise { - const pluginsMap: Map< - string, - typeof FlipperDevicePlugin | typeof FlipperPlugin - > = pluginsClassMap(store.getState().plugins); + const pluginsMap = pluginsClassMap(store.getState().plugins); const {clients, selectedDevice} = store.getState().connections; const pluginsToProcess = determinePluginsToProcess( clients, @@ -107,7 +107,7 @@ function parseJSON(str: string): any { export async function exportMetricsFromTrace( trace: string, - pluginsMap: Map, + pluginsMap: Map, selectedPlugins: Array, ): Promise { const data = fs.readFileSync(trace, 'utf8'); @@ -136,5 +136,6 @@ export async function exportMetricsFromTrace( ), ); } + // TODO: Support Sandy T68683449 and use ClientPluginsMap, or kill feature return await exportMetrics(pluginStates, pluginsMap, selectedPlugins); } diff --git a/desktop/app/src/utils/messageQueue.tsx b/desktop/app/src/utils/messageQueue.tsx index 303e77e35..617d402f2 100644 --- a/desktop/app/src/utils/messageQueue.tsx +++ b/desktop/app/src/utils/messageQueue.tsx @@ -21,6 +21,7 @@ import {pluginIsStarred, getSelectedPluginKey} from '../reducers/connections'; import {deconstructPluginKey} from './clientUtils'; import {onBytesReceived} from '../dispatcher/tracking'; import {defaultEnabledBackgroundPlugins} from './pluginUtils'; +import {SandyPluginDefinition} from 'flipper-plugin'; const MAX_BACKGROUND_TASK_TIME = 25; @@ -189,14 +190,22 @@ export function processMessagesImmediately( export function processMessagesLater( store: MiddlewareAPI, pluginKey: string, - plugin: { - defaultPersistedState: any; - id: string; - persistedStateReducer: PersistedStateReducer | null; - maxQueueSize?: number; - }, + plugin: + | { + defaultPersistedState: any; + id: string; + persistedStateReducer: PersistedStateReducer | null; + maxQueueSize?: number; + } + | SandyPluginDefinition, messages: Message[], ) { + if (plugin instanceof SandyPluginDefinition) { + // TODO: + throw new Error( + 'Receiving messages is not yet supported for Sandy plugins', + ); + } const isSelected = pluginKey === getSelectedPluginKey(store.getState().connections); switch (true) { diff --git a/desktop/app/src/utils/onRegisterDevice.tsx b/desktop/app/src/utils/onRegisterDevice.tsx index 94a3f235d..067fa6076 100644 --- a/desktop/app/src/utils/onRegisterDevice.tsx +++ b/desktop/app/src/utils/onRegisterDevice.tsx @@ -8,21 +8,21 @@ */ import {Store} from '../reducers/index'; -import {FlipperPlugin, FlipperDevicePlugin} from '../plugin'; +import {ClientPluginMap, DevicePluginMap, PluginDefinition} from '../plugin'; import {setPluginState} from '../reducers/pluginStates'; import BaseDevice from '../devices/BaseDevice'; import {getPersistedState} from '../utils/pluginUtils'; +import {SandyPluginDefinition} from 'flipper-plugin'; export function registerDeviceCallbackOnPlugins( store: Store, - devicePlugins: Map, - clientPlugins: Map, + devicePlugins: DevicePluginMap, + clientPlugins: ClientPluginMap, device: BaseDevice, ) { - const callRegisterDeviceHook = ( - plugin: typeof FlipperDevicePlugin | typeof FlipperPlugin, - ) => { - if (plugin.onRegisterDevice) { + const callRegisterDeviceHook = (plugin: PluginDefinition) => { + // This hook is not registered for Sandy plugins, let's see in the future if it is needed + if (!(plugin instanceof SandyPluginDefinition) && plugin.onRegisterDevice) { plugin.onRegisterDevice( store, device, diff --git a/desktop/app/src/utils/pluginStateRecorder.tsx b/desktop/app/src/utils/pluginStateRecorder.tsx index 8725a1b71..8d507163a 100644 --- a/desktop/app/src/utils/pluginStateRecorder.tsx +++ b/desktop/app/src/utils/pluginStateRecorder.tsx @@ -12,6 +12,7 @@ import fs from 'fs'; import {Store, State} from '../reducers'; import {getPluginKey} from './pluginUtils'; import {serialize} from './serialization'; +import {SandyPluginDefinition} from 'flipper-plugin'; let pluginRecordingState: { recording: string; @@ -67,7 +68,10 @@ async function flipperStartPluginRecording(state: State) { // Note that we don't use the plugin's own serializeState, as that might interact with the // device state, and is used for creating Flipper Exports. pluginRecordingState.startState = await serialize( - state.pluginStates[pluginKey] || plugin.defaultPersistedState, + state.pluginStates[pluginKey] || + (plugin instanceof SandyPluginDefinition + ? {} + : plugin.defaultPersistedState), ); console.log( diff --git a/desktop/app/src/utils/pluginUtils.tsx b/desktop/app/src/utils/pluginUtils.tsx index 899d6a955..71df4acea 100644 --- a/desktop/app/src/utils/pluginUtils.tsx +++ b/desktop/app/src/utils/pluginUtils.tsx @@ -7,12 +7,18 @@ * @format */ -import {FlipperDevicePlugin, FlipperPlugin, FlipperBasePlugin} from '../plugin'; +import { + FlipperDevicePlugin, + FlipperBasePlugin, + PluginDefinition, + DevicePluginDefinition, +} from '../plugin'; import {State as PluginStatesState} from '../reducers/pluginStates'; import {State as PluginsState} from '../reducers/plugins'; import {State as PluginMessageQueueState} from '../reducers/pluginMessageQueue'; import {PluginDetails} from 'flipper-plugin-lib'; import {deconstructPluginKey, deconstructClientId} from './clientUtils'; +import {SandyPluginDefinition} from 'flipper-plugin'; type Client = import('../Client').default; @@ -20,11 +26,8 @@ export const defaultEnabledBackgroundPlugins = ['Navigation']; // The navigation export function pluginsClassMap( plugins: PluginsState, -): Map { - const pluginsMap: Map< - string, - typeof FlipperDevicePlugin | typeof FlipperPlugin - > = new Map([]); +): Map { + const pluginsMap: Map = new Map([]); plugins.clientPlugins.forEach((val, key) => { pluginsMap.set(key, val); }); @@ -83,10 +86,7 @@ export function getEnabledOrExportPersistedStatePlugins( plugins: PluginsState, ): Array<{id: string; label: string}> { const appName = deconstructClientId(client.id).app; - const pluginsMap: Map< - string, - typeof FlipperDevicePlugin | typeof FlipperPlugin - > = pluginsClassMap(plugins); + const pluginsMap: Map = pluginsClassMap(plugins); // Enabled Plugins with no exportPersistedState function defined const enabledPlugins = starredPlugin[appName] ? starredPlugin[appName] @@ -141,10 +141,7 @@ export function getActivePersistentPlugins( plugins: PluginsState, selectedClient?: Client, ): {id: string; label: string}[] { - const pluginsMap: Map< - string, - typeof FlipperDevicePlugin | typeof FlipperPlugin - > = pluginsClassMap(plugins); + const pluginsMap: Map = pluginsClassMap(plugins); return getPersistentPlugins(plugins) .map((pluginName) => pluginsMap.get(pluginName)!) .sort(sortPluginsByName) @@ -183,10 +180,7 @@ export function getActivePersistentPlugins( } export function getPersistentPlugins(plugins: PluginsState): Array { - const pluginsMap: Map< - string, - typeof FlipperDevicePlugin | typeof FlipperPlugin - > = pluginsClassMap(plugins); + const pluginsMap: Map = pluginsClassMap(plugins); const arr: Array = plugins.disabledPlugins.concat( plugins.gatekeepedPlugins, @@ -210,32 +204,36 @@ export function getPersistentPlugins(plugins: PluginsState): Array { return ( plugin == 'DeviceLogs' || (pluginClass && + // TODO: support Sandy plugin T68683449 + !(pluginClass instanceof SandyPluginDefinition) && (pluginClass.defaultPersistedState != undefined || pluginClass.exportPersistedState != undefined)) ); }); } -export function getPluginTitle(pluginClass: typeof FlipperBasePlugin) { +export function getPluginTitle(pluginClass: PluginDefinition) { return pluginClass.title || pluginClass.id; } export function sortPluginsByName( - a: typeof FlipperBasePlugin, - b: typeof FlipperBasePlugin, + a: PluginDefinition, + b: PluginDefinition, ): number { // make sure Device plugins are sorted before normal plugins - if ( - a.prototype instanceof FlipperDevicePlugin && - !(b.prototype instanceof FlipperDevicePlugin) - ) { + if (isDevicePluginDefinition(a) && !isDevicePluginDefinition(b)) { return -1; } - if ( - b.prototype instanceof FlipperDevicePlugin && - !(a.prototype instanceof FlipperDevicePlugin) - ) { + if (isDevicePluginDefinition(b) && !isDevicePluginDefinition(a)) { return 1; } return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1; } + +export function isDevicePluginDefinition( + definition: PluginDefinition, +): definition is DevicePluginDefinition { + // TODO: support Sandy device plugins T68738317 + // @ts-ignore + return definition.prototype instanceof FlipperDevicePlugin; +} diff --git a/desktop/flipper-plugin/src/index.ts b/desktop/flipper-plugin/src/index.ts index 02e501ce7..b001bc7c8 100644 --- a/desktop/flipper-plugin/src/index.ts +++ b/desktop/flipper-plugin/src/index.ts @@ -7,6 +7,4 @@ * @format */ -export function hello() { - return 'universe '; -} +export * from './plugin/Plugin'; diff --git a/desktop/flipper-plugin/src/plugin/Plugin.tsx b/desktop/flipper-plugin/src/plugin/Plugin.tsx new file mode 100644 index 000000000..40b39a945 --- /dev/null +++ b/desktop/flipper-plugin/src/plugin/Plugin.tsx @@ -0,0 +1,133 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import {PluginDetails} from 'flipper-plugin-lib'; + +type EventsContract = Record; +type MethodsContract = Record Promise>; + +/** + * API available to a plugin factory + */ +export interface FlipperClient< + Events extends EventsContract, + Methods extends MethodsContract +> {} + +/** + * Internal API exposed by Flipper, and wrapped by FlipperPluginInstance to be passed to the + * Plugin Factory + */ +interface RealFlipperClient {} + +export type FlipperPluginFactory< + Events extends EventsContract, + Methods extends MethodsContract +> = (client: FlipperClient) => object; + +export type FlipperPluginComponent = React.FC<{}>; + +export type FlipperPluginModule = { + /** the factory function that initializes a plugin instance */ + plugin: FlipperPluginFactory; + /** the component type that can render this plugin */ + Component: FlipperPluginComponent; + // TODO: support device plugins T68738317 + // devicePlugin: FlipperPluginFactory +}; + +export class FlipperPluginInstance { + /** base client provided by Flipper */ + realClient: RealFlipperClient; + /** client that is bound to this instance */ + client: FlipperClient; + /** the original plugin definition */ + definition: FlipperPluginModule; + /** the plugin instance api as used inside components and such */ + instanceApi: object; + + constructor(realClient: RealFlipperClient, definition: FlipperPluginModule) { + this.realClient = realClient; + this.definition = definition; + this.client = {}; + this.instanceApi = definition.plugin(this.client); + } + + deactivate() { + // TODO: + } +} + +export class SandyPluginDefinition { + id: string; + module: FlipperPluginModule; + details: PluginDetails; + + // TODO: Implement T68683449 + exportPersistedState: + | (( + callClient: (method: string, params?: any) => Promise, + persistedState: any, // TODO: type StaticPersistedState | undefined, + store: any, // TODO: ReduxState | undefined, + idler?: any, // TODO: Idler, + statusUpdate?: (msg: string) => void, + supportsMethod?: (method: string) => Promise, + ) => Promise) + | undefined = undefined; + + constructor(details: PluginDetails, module: FlipperPluginModule) { + this.id = details.id; + this.details = details; + if (!module.plugin || typeof module.plugin !== 'function') { + throw new Error( + `Sandy plugin ${this.id} doesn't export a named function called 'plugin'`, + ); + } + if (!module.Component || typeof module.Component !== 'function') { + throw new Error( + `Sandy plugin ${this.id} doesn't export a named function called 'Component'`, + ); + } + this.module = module; + this.module.Component.displayName = `FlipperPlugin(${this.id})`; + } + + get packageName() { + return this.details.name; + } + + get title() { + return this.details.title; + } + + get icon() { + return this.details.icon; + } + + get category() { + return this.details.category; + } + + get gatekeeper() { + return this.details.gatekeeper; + } + + get version() { + return this.details.version; + } + + get isDefault() { + return this.details.isDefault; + } + + get keyboardActions() { + // TODO: T68882551 support keyboard actions + return []; + } +} diff --git a/desktop/flipper-plugin/tsconfig.json b/desktop/flipper-plugin/tsconfig.json index ecd4c16fb..9c303bc5a 100644 --- a/desktop/flipper-plugin/tsconfig.json +++ b/desktop/flipper-plugin/tsconfig.json @@ -4,11 +4,7 @@ "outDir": "lib", "rootDir": "src" }, - "include": [ - "src" - ], - "exclude": [ - "node_modules", - "**/__tests__/*" - ] + "references": [{"path": "../plugin-lib"}], + "include": ["src"], + "exclude": ["node_modules", "**/__tests__/*"] }