Files
flipper/desktop/app/src/utils/createSandyPluginWrapper.tsx
Michel Weststrate 16154e1343 Remove classic plugin infra
Summary:
This removes all code duplication / old plugin infra that isn't needed anymore when all plugin run on the Sandy plugin infra structure.

The diff is quite large, but the minimal one that passes tests and compiles. Existing tests are preserved by wrapping all remaining tests in `wrapSandy` for classic plugins where needed

Reviewed By: passy

Differential Revision: D29394738

fbshipit-source-id: 1315fabd9f048576aed15ed5f1cb6414d5fdbd40
2021-06-30 10:42:32 -07:00

237 lines
6.6 KiB
TypeScript

/**
* 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} from 'react';
import {
BaseAction,
FlipperDevicePlugin,
FlipperPlugin,
Props as PluginProps,
} from '../plugin';
import {useStore} from './useStore';
import {setStaticView, StaticView} from '../reducers/connections';
import {getStore} from '../store';
import {setActiveNotifications} from '../reducers/notifications';
export type SandyPluginModule = ConstructorParameters<
typeof _SandyPluginDefinition
>[1];
export function createSandyPluginWrapper<S, A extends BaseAction, P>(
Plugin: typeof FlipperPlugin | typeof FlipperDevicePlugin,
): SandyPluginModule {
const isDevicePlugin = Plugin.prototype instanceof FlipperDevicePlugin;
if (process.env.NODE_ENV !== 'test') {
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 legacyPluginWrapper(client: PluginClient | DevicePluginClient) {
const store = getStore();
const appClient = isDevicePlugin
? undefined
: (client as PluginClient<any, any>);
const instanceRef = React.createRef<FlipperPlugin<S, A, P> | null>();
const persistedState = createState<P>(Plugin.defaultPersistedState);
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),
);
}
});
if (
Plugin.persistedStateReducer ||
Plugin.exportPersistedState ||
Plugin.defaultPersistedState
) {
client.onExport(async (idler, onStatusMessage) => {
const state = Plugin.exportPersistedState
? await Plugin.exportPersistedState(
isDevicePlugin
? undefined
: (method: string, params: any) =>
appClient!.send(method, params),
persistedState.get(),
undefined, // passing an undefined Store is safe, as no plugin actually uses this param
idler,
onStatusMessage,
isDevicePlugin
? undefined
: (method: string) => appClient!.supportsMethod(method),
)
: persistedState.get();
// respect custom serialization
return Plugin.serializePersistedState
? await Plugin.serializePersistedState(
state,
onStatusMessage,
idler,
Plugin.id,
)
: state;
});
client.onImport((data) => {
if (Plugin.deserializePersistedState) {
data = Plugin.deserializePersistedState(data);
}
persistedState.set(data);
});
}
if (Plugin.keyboardActions) {
function executeKeyboardAction(action: string) {
instanceRef?.current?.onKeyboardAction?.(action);
}
client.addMenuEntry(
...Plugin.keyboardActions.map((def) => {
if (typeof def === 'string') {
return {
action: def,
handler() {
executeKeyboardAction(def);
},
};
} else {
const {action, label, accelerator, topLevelMenu} = def;
return {
label,
accelerator,
topLevelMenu,
handler() {
executeKeyboardAction(action);
},
};
}
}),
);
}
if (Plugin.getActiveNotifications && !isDevicePlugin) {
const unsub = persistedState.subscribe((state) => {
try {
const notifications = Plugin.getActiveNotifications!(state);
store.dispatch(
setActiveNotifications({
notifications,
client: appClient!.appId,
pluginId: Plugin.id,
}),
);
} catch (e) {
console.error(
'Failed to compute notifications for plugin ' + Plugin.id,
e,
);
}
});
client.onDestroy(unsub);
}
return {
instanceRef,
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;
},
setStaticView(payload: StaticView) {
store.dispatch(setStaticView(payload));
},
};
}
function Component() {
const instance = usePlugin(legacyPluginWrapper);
const logger = useLogger();
const persistedState = useValue(instance.persistedState);
const deepLinkPayload = useValue(instance.deeplink);
const settingsState = useStore((state) => state.settingsState);
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');
}
useEffect(
function triggerInitAndTeardown() {
const ref = instance.instanceRef.current!;
ref._init();
return () => {
ref._teardown();
};
},
[instance.instanceRef],
);
const props: PluginProps<P> = {
logger,
persistedState,
deepLinkPayload,
settingsState,
target,
setPersistedState: instance.setPersistedState,
selectPlugin: instance.selectPlugin,
isArchivedDevice: instance.isArchived,
selectedApp: instance.appName,
setStaticView: instance.setStaticView,
// @ts-ignore ref is not on Props
ref: instance.instanceRef,
};
return React.createElement(Plugin, props);
}
return isDevicePlugin
? {devicePlugin: legacyPluginWrapper, Component}
: {
plugin: legacyPluginWrapper,
Component,
};
}