From c1860ec19ce96d10d7050fdeb1b2372b01230405 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Mon, 21 Jun 2021 08:35:52 -0700 Subject: [PATCH] Introduce Sandy wrapper for legacy plugins Summary: This diff introduces loading classic Flipper plugins in a Sandy container. By wrapping plugins into Sandy we will be able to remove a lot of code / logic duplication related to state, queue processing, serialization etc. This will allow us to remove most or all of the complex plugin logic from the old system, only keeping onto the legacy components which have a lower maintenance burden. Until all plugins are Sandy. This diff is not feature complete but only implements the core mechanisms for (persisted) state and communication. Keyboard support, serialization, and rewiring tests etc will be added in next diff. The feature is introduced behind GK flipper_use_sandy_plugin_wrapper to have kill switch. Tests will be added later in this diff by redirection a part of the current mechanisms to wrapped plugins. (Will land the stack as a whole) Reviewed By: passy Differential Revision: D29165866 fbshipit-source-id: 57f84794a4a5f898bf765ce2de13cc759267fbc6 --- desktop/app/src/dispatcher/plugins.tsx | 10 +- desktop/app/src/plugin.tsx | 10 +- .../src/utils/createSandyPluginWrapper.tsx | 158 ++++++++++++++++++ 3 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 desktop/app/src/utils/createSandyPluginWrapper.tsx diff --git a/desktop/app/src/dispatcher/plugins.tsx b/desktop/app/src/dispatcher/plugins.tsx index 83aed4787..a1ae4fed7 100644 --- a/desktop/app/src/dispatcher/plugins.tsx +++ b/desktop/app/src/dispatcher/plugins.tsx @@ -9,7 +9,7 @@ import type {Store} from '../reducers/index'; import type {Logger} from '../fb-interfaces/Logger'; -import type {PluginDefinition} from '../plugin'; +import {PluginDefinition} from '../plugin'; import React from 'react'; import ReactDOM from 'react-dom'; import adbkit from 'adbkit'; @@ -54,6 +54,7 @@ import {isDevicePluginDefinition} from '../utils/pluginUtils'; import isPluginCompatible from '../utils/isPluginCompatible'; import isPluginVersionMoreRecent from '../utils/isPluginVersionMoreRecent'; import {getStaticPath} from '../utils/pathUtils'; +import {createSandyPluginWrapper} from '../utils/createSandyPluginWrapper'; let defaultPluginsIndex: any = null; export default async (store: Store, logger: Logger) => { @@ -317,6 +318,13 @@ const requirePluginInternal = ( plugin.packageName = pluginDetails.name; plugin.details = pluginDetails; + if (GK.get('flipper_use_sandy_plugin_wrapper')) { + return new _SandyPluginDefinition( + pluginDetails, + createSandyPluginWrapper(plugin), + ); + } + // set values from package.json as static variables on class Object.keys(pluginDetails).forEach((key) => { if (key !== 'name' && key !== 'id') { diff --git a/desktop/app/src/plugin.tsx b/desktop/app/src/plugin.tsx index 432df4b84..db21c101d 100644 --- a/desktop/app/src/plugin.tsx +++ b/desktop/app/src/plugin.tsx @@ -10,7 +10,6 @@ import {KeyboardActions} from './MenuBar'; import {Logger} from './fb-interfaces/Logger'; import Client from './Client'; -import {Store} from './reducers/index'; import {Component} from 'react'; import BaseDevice from './devices/BaseDevice'; import {serialize, deserialize} from './utils/serialization'; @@ -74,7 +73,7 @@ export type Props = { setPersistedState: (state: Partial) => void; target: PluginTarget; deepLinkPayload: unknown; - selectPlugin: (pluginID: string, deepLinkPayload: unknown) => boolean; + selectPlugin: (pluginID: string, deepLinkPayload: unknown) => void; isArchivedDevice: boolean; selectedApp: string | null; setStaticView: (payload: StaticView) => void; @@ -168,13 +167,6 @@ export abstract class FlipperBasePlugin< teardown(): void {} - computeNotifications( - _props: Props, - _state: State, - ): Array { - return []; - } - // methods to be overridden by subclasses _init(): void {} diff --git a/desktop/app/src/utils/createSandyPluginWrapper.tsx b/desktop/app/src/utils/createSandyPluginWrapper.tsx new file mode 100644 index 000000000..f0e123957 --- /dev/null +++ b/desktop/app/src/utils/createSandyPluginWrapper.tsx @@ -0,0 +1,158 @@ +/** + * 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 * as React from 'react'; +import { + createState, + useLogger, + usePlugin, + useValue, + _SandyPluginDefinition, + PluginClient, + DevicePluginClient, +} from 'flipper-plugin'; +import {useEffect, useRef} from 'react'; +import { + BaseAction, + FlipperDevicePlugin, + FlipperPlugin, + Props as PluginProps, +} from '../plugin'; +import {useDispatch, useStore} from './useStore'; +import {setStaticView, StaticView} from '../reducers/connections'; + +export type SandyPluginModule = ConstructorParameters< + typeof _SandyPluginDefinition +>[1]; + +// Wrapped features +// keyboardActions +// defaultPersistedState +// persistedStateReducer +// exportPersistedState +// getActiveNotifications +// reducers? +// onKeyboardAction +// call _init (not .init) in onactivate / deactivate +// serializePersistedState +// static deserializePersistedState: ( +// call _teardown +// dispatchAction + +// Device: +// .device property + +// Client +// this.client + +export function createSandyPluginWrapper( + Plugin: typeof FlipperPlugin | typeof FlipperDevicePlugin, +): SandyPluginModule { + const isDevicePlugin = Plugin.prototype instanceof FlipperDevicePlugin; + console.warn( + `Loading ${isDevicePlugin ? 'device' : 'client'} plugin ${ + Plugin.id + } in legacy mode. Please visit https://fbflipper.com/docs/extending/sandy-migration to learn how to migrate this plugin to the new Sandy architecture`, + ); + + function plugin(client: PluginClient | DevicePluginClient) { + const appClient = isDevicePlugin ? undefined : (client as PluginClient); + + const persistedState = createState

(Plugin.defaultPersistedState, { + persist: 'persistedState', + }); + const deeplink = createState(); + + client.onDeepLink((link) => { + deeplink.set(link); + }); + + appClient?.onUnhandledMessage((event, params) => { + if (Plugin.persistedStateReducer) { + persistedState.set( + Plugin.persistedStateReducer(persistedState.get(), event, params), + ); + } + }); + + return { + device: client.device.realDevice, + persistedState, + deeplink, + selectPlugin: client.selectPlugin, + setPersistedState(state: Partial

) { + persistedState.set({...persistedState.get(), ...state}); + }, + get appId() { + return appClient?.appId; + }, + get appName() { + return appClient?.appName ?? null; + }, + get isArchived() { + return client.device.isArchived; + }, + }; + } + + function Component() { + const instance = usePlugin(plugin); + const logger = useLogger(); + const pluginInstanceRef = useRef>(null); + const persistedState = useValue(instance.persistedState); + const deepLinkPayload = useValue(instance.deeplink); + const dispatch = useDispatch(); + + const target = isDevicePlugin + ? instance.device + : // eslint-disable-next-line + useStore((state) => + state.connections.clients.find((c) => c.id === instance.appId), + ); + if (!target) { + throw new Error('Illegal state: missing target'); + } + + const settingsState = useStore((state) => state.settingsState); + + useEffect(function triggerInitAndTeardown() { + const ref = pluginInstanceRef.current!; + ref._init(); + return () => { + ref._teardown(); + }; + }, []); + + const props: PluginProps

= { + logger, + persistedState, + target, + deepLinkPayload, + settingsState, + setPersistedState: instance.setPersistedState, + selectPlugin: instance.selectPlugin, + isArchivedDevice: instance.isArchived, + selectedApp: instance.appName, + setStaticView(payload: StaticView) { + dispatch(setStaticView(payload)); + }, + // @ts-ignore ref is not on Props + ref: pluginInstanceRef as any, + }; + + return React.createElement(Plugin, props); + } + + return isDevicePlugin + ? {devicePlugin: plugin, Component} + : { + plugin, + Component, + }; +}