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
This commit is contained in:
Michel Weststrate
2021-06-21 08:35:52 -07:00
committed by Facebook GitHub Bot
parent 07199323d1
commit c1860ec19c
3 changed files with 168 additions and 10 deletions

View File

@@ -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') {

View File

@@ -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<T> = {
setPersistedState: (state: Partial<T>) => 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<PersistedState>,
_state: State,
): Array<Notification> {
return [];
}
// methods to be overridden by subclasses
_init(): void {}

View File

@@ -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<S, A extends BaseAction, P>(
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<P>(Plugin.defaultPersistedState, {
persist: 'persistedState',
});
const deeplink = createState<unknown>();
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<P>) {
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<FlipperPlugin<S, A, P>>(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<P> = {
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,
};
}