From 7747a0714d64fc9fef4fa45c9fb4a3709d28812a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20B=C3=BCchele?= Date: Thu, 15 Nov 2018 07:25:58 -0800 Subject: [PATCH] plugin redux Summary: Plugins were loaded in `/plugins/index.js` which was loaded once at launch of the app. This moves the list of available plugins to redux. This way, plugins can be dynamically added. The redux store keeps to Maps of plugins (devicePlugins and clientPlugins) with their ID as key: ``` devicePlugins: Map>>, clientPlugins: Map>>, ``` On launch of the app, all plugins bundled with the app and the one found in `pluginsPath` are dynamically added. This changes now allows to add new plugins at any time. All components that need to know which plugins are available (e.g. the sidebar) are connected to the redux store. This way, they will automatically update, whenever a new plugin is added. - add `plugins` to the redux store to keep the list of available plugins - add a plugins dispatcher, responsible for loading the plugins on launch - connecting all React components that imported `plugins/index.js` before to the redux store to get the plugins from there. - moved the updating of the MenuBar to the plugins dispatcher as it needs to update whenever a new plugin is added. Reviewed By: jknoxville, passy Differential Revision: D12449236 fbshipit-source-id: 6ef3e243e2c80443614b901ccbfde485fcb4301c --- src/Client.js | 18 ++--- src/MenuBar.js | 16 +++-- src/NotificationsHub.js | 36 ++++++---- src/PluginContainer.js | 57 ++++++++------- src/chrome/MainSidebar.js | 10 ++- src/dispatcher/index.js | 2 + src/dispatcher/notifications.js | 7 +- src/dispatcher/plugins.js | 122 ++++++++++++++++++++++++++++++++ src/init.js | 2 - src/plugins/index.js | 97 ------------------------- src/reducers/index.js | 20 ++++-- src/reducers/plugins.js | 62 ++++++++++++++++ 12 files changed, 280 insertions(+), 169 deletions(-) create mode 100644 src/dispatcher/plugins.js delete mode 100644 src/plugins/index.js create mode 100644 src/reducers/plugins.js diff --git a/src/Client.js b/src/Client.js index ea0c48eec..29cb48fd1 100644 --- a/src/Client.js +++ b/src/Client.js @@ -11,7 +11,6 @@ import type Logger from './fb-stubs/Logger.js'; import type {Store} from './reducers/index.js'; import {setPluginState} from './reducers/pluginStates.js'; -import {clientPlugins} from './plugins/index.js'; import {ReactiveSocket, PartialResponder} from 'rsocket-core'; const EventEmitter = (require('events'): any); @@ -97,14 +96,6 @@ export default class Client extends EventEmitter { return this.plugins.includes(Plugin.id); } - getFirstSupportedPlugin(): ?string { - for (const Plugin of clientPlugins) { - if (this.supportsPlugin(Plugin)) { - return Plugin.id; - } - } - } - async init() { await this.getPlugins(); } @@ -162,12 +153,11 @@ export default class Client extends EventEmitter { const params = data.params; invariant(params, 'expected params'); - const persistingPlugin: ?Class> = clientPlugins.find( - (p: Class>) => - p.id === params.api && p.persistedStateReducer, - ); + const persistingPlugin: ?Class< + FlipperPlugin<>, + > = this.store.getState().plugins.clientPlugins.get(params.api); - if (persistingPlugin) { + if (persistingPlugin && persistingPlugin.persistedStateReducer) { const pluginKey = `${this.id}#${params.api}`; const persistedState = { ...persistingPlugin.defaultPersistedState, diff --git a/src/MenuBar.js b/src/MenuBar.js index f4ba77b59..105680ddb 100644 --- a/src/MenuBar.js +++ b/src/MenuBar.js @@ -5,9 +5,8 @@ * @format */ -import type {FlipperBasePlugin} from './plugin.js'; +import type {FlipperPlugin, FlipperDevicePlugin} from './plugin.js'; -import plugins from './plugins/index.js'; import electron from 'electron'; export type DefaultKeyboardAction = 'clear' | 'goToBottom' | 'createPaste'; @@ -63,13 +62,18 @@ function actionHandler(action: string) { } } -export function setupMenuBar() { +export function setupMenuBar( + plugins: Array | FlipperDevicePlugin<>>>, +) { const template = getTemplate(electron.remote.app, electron.remote.shell); // collect all keyboard actions from all plugins const registeredActions: Set = new Set( plugins - .map((plugin: Class>) => plugin.keyboardActions || []) + .map( + (plugin: Class | FlipperDevicePlugin<>>) => + plugin.keyboardActions || [], + ) .reduce((acc: KeyboardActions, cv) => acc.concat(cv), []) .map( (action: DefaultKeyboardAction | KeyboardAction) => @@ -132,7 +136,9 @@ function appendMenuItem( } } -export function activateMenuItems(activePlugin: FlipperBasePlugin<>) { +export function activateMenuItems( + activePlugin: FlipperPlugin<> | FlipperDevicePlugin<>, +) { // disable all keyboard actions for (const item of menuItems) { item[1].enabled = false; diff --git a/src/NotificationsHub.js b/src/NotificationsHub.js index b921201e2..04aba1192 100644 --- a/src/NotificationsHub.js +++ b/src/NotificationsHub.js @@ -5,7 +5,12 @@ * @format */ -import type {SearchableProps, FlipperBasePlugin, Device} from 'flipper'; +import type { + SearchableProps, + FlipperBasePlugin, + FlipperPlugin, + Device, +} from 'flipper'; import type {PluginNotification} from './reducers/notifications'; import type Logger from './fb-stubs/Logger'; @@ -24,7 +29,6 @@ import { } from 'flipper'; import {connect} from 'react-redux'; import React, {Component, Fragment} from 'react'; -import plugins from './plugins/index'; import {clipboard} from 'electron'; import PropTypes from 'prop-types'; import { @@ -100,6 +104,8 @@ type Props = {| invalidatedNotifications: Array, blacklistedPlugins: Array, blacklistedCategories: Array, + devicePlugins: Map>>, + clientPlugins: Map>>, onClear: () => void, updatePluginBlacklist: (blacklist: Array) => mixed, updateCategoryBlacklist: (blacklist: Array) => mixed, @@ -243,6 +249,9 @@ class NotificationsTable extends Component { return false; }; + getPlugin = (id: string) => + this.props.clientPlugins.get(id) || this.props.devicePlugins.get(id); + render() { const activeNotifications = this.props.activeNotifications .filter(this.getFilter()) @@ -253,6 +262,7 @@ class NotificationsTable extends Component { this.setState({selectedNotification: n.notification.id}) @@ -275,6 +285,7 @@ class NotificationsTable extends Component { @@ -323,11 +334,14 @@ const ConnectedNotificationsTable = connect( blacklistedPlugins, blacklistedCategories, }, + plugins: {devicePlugins, clientPlugins}, }) => ({ activeNotifications, invalidatedNotifications, blacklistedPlugins, blacklistedCategories, + devicePlugins, + clientPlugins, }), { updatePluginBlacklist, @@ -445,6 +459,7 @@ type ItemProps = { deepLinkPayload?: ?string, }) => mixed, logger?: Logger, + plugin: ?Class>, }; type ItemState = {| @@ -454,12 +469,10 @@ type ItemState = {| class NotificationItem extends Component { constructor(props: ItemProps) { super(props); - const plugin = plugins.find(p => p.id === props.pluginId); - const items = []; - if (props.onHidePlugin && plugin) { + if (props.onHidePlugin && props.plugin) { items.push({ - label: `Hide ${plugin.title} plugin`, + label: `Hide ${props.plugin.title} plugin`, click: this.props.onHidePlugin, }); } @@ -475,11 +488,9 @@ class NotificationItem extends Component { ); this.contextMenuItems = items; - this.plugin = plugin; } state = {reportedNotHelpful: false}; - plugin: ?Class>; contextMenuItems; deepLinkButton = React.createRef(); @@ -541,6 +552,7 @@ class NotificationItem extends Component { inactive, onHidePlugin, onHideCategory, + plugin, } = this.props; const {action} = notification; @@ -553,19 +565,19 @@ class NotificationItem extends Component { isSelected={isSelected} inactive={inactive} items={this.contextMenuItems}> - + {notification.title} {notification.message} {!inactive && isSelected && - this.plugin && + plugin && (action || onHidePlugin || onHideCategory) && ( {action && ( )} @@ -574,7 +586,7 @@ class NotificationItem extends Component { )} {onHidePlugin && ( )} diff --git a/src/PluginContainer.js b/src/PluginContainer.js index d8fa1899e..b609a05bd 100644 --- a/src/PluginContainer.js +++ b/src/PluginContainer.js @@ -4,12 +4,11 @@ * LICENSE file in the root directory of this source tree. * @format */ -import type {FlipperPlugin, FlipperBasePlugin} from './plugin.js'; +import type {FlipperPlugin, FlipperDevicePlugin} from './plugin.js'; import type LogManager from './fb-stubs/Logger'; import type BaseDevice from './devices/BaseDevice.js'; import type {Props as PluginProps} from './plugin'; -import {FlipperDevicePlugin} from './plugin.js'; import Client from './Client.js'; import { ErrorBoundary, @@ -23,7 +22,6 @@ import React from 'react'; import {connect} from 'react-redux'; import {setPluginState} from './reducers/pluginStates.js'; import {selectPlugin} from './reducers/connections'; -import {devicePlugins, clientPlugins} from './plugins/index.js'; import NotificationsHub from './NotificationsHub'; import {activateMenuItems} from './MenuBar.js'; @@ -59,36 +57,44 @@ type Props = { selectedApp?: ?string, deepLinkPayload: ?string, |}) => mixed, + devicePlugins: Map>>, + clientPlugins: Map>>, }; type State = { - activePlugin: ?Class>, + activePlugin: ?Class | FlipperDevicePlugin<>>, target: Client | BaseDevice | null, pluginKey: string, }; class PluginContainer extends Component { static getDerivedStateFromProps(props: Props): State { - let activePlugin = [NotificationsHub, ...devicePlugins].find( - (p: Class>) => p.id === props.selectedPlugin, - ); - let target = props.selectedDevice; + const {selectedPlugin} = props; let pluginKey = 'unknown'; - if (activePlugin) { - pluginKey = `${props.selectedDevice.serial}#${activePlugin.id}`; - } else { - target = props.clients.find( - (client: Client) => client.id === props.selectedApp, - ); - activePlugin = clientPlugins.find( - (p: Class>) => p.id === props.selectedPlugin, - ); - if (!activePlugin || !target) { - throw new Error( - `Plugin "${props.selectedPlugin || ''}" could not be found.`, - ); + let target = null; + let activePlugin: ?Class | FlipperDevicePlugin<>> = null; + + if (selectedPlugin) { + if (selectedPlugin === NotificationsHub.id) { + activePlugin = NotificationsHub; + } else if (props.selectedPlugin) { + activePlugin = props.devicePlugins.get(props.selectedPlugin); + } + target = props.selectedDevice; + if (activePlugin) { + pluginKey = `${props.selectedDevice.serial}#${activePlugin.id}`; + } else { + target = props.clients.find( + (client: Client) => client.id === props.selectedApp, + ); + activePlugin = props.clientPlugins.get(selectedPlugin); + if (!activePlugin || !target) { + throw new Error( + `Plugin "${props.selectedPlugin || ''}" could not be found.`, + ); + } + pluginKey = `${target.id}#${activePlugin.id}`; } - pluginKey = `${target.id}#${activePlugin.id}`; } return { @@ -99,9 +105,9 @@ class PluginContainer extends Component { } state: State = this.constructor.getDerivedStateFromProps(this.props); - plugin: ?FlipperBasePlugin<>; + plugin: ?FlipperPlugin<> | FlipperDevicePlugin<>; - refChanged = (ref: ?FlipperBasePlugin<>) => { + refChanged = (ref: ?FlipperPlugin<> | FlipperDevicePlugin<>) => { if (this.plugin) { this.plugin._teardown(); this.plugin = null; @@ -183,6 +189,7 @@ export default connect( deepLinkPayload, }, pluginStates, + plugins: {devicePlugins, clientPlugins}, }) => ({ selectedPlugin, selectedDevice, @@ -190,6 +197,8 @@ export default connect( selectedApp, clients, deepLinkPayload, + devicePlugins, + clientPlugins, }), { setPluginState, diff --git a/src/chrome/MainSidebar.js b/src/chrome/MainSidebar.js index a28abdfe4..25dbd39b0 100644 --- a/src/chrome/MainSidebar.js +++ b/src/chrome/MainSidebar.js @@ -26,7 +26,6 @@ import { LoadingIndicator, } from 'flipper'; import React from 'react'; -import {devicePlugins, clientPlugins} from '../plugins/index.js'; import NotificationsHub from '../NotificationsHub.js'; import {selectPlugin} from '../reducers/connections.js'; import {connect} from 'react-redux'; @@ -179,6 +178,8 @@ type MainSidebarProps = {| }>, activeNotifications: Array, blacklistedPlugins: Array, + devicePlugins: Map>>, + clientPlugins: Map>>, |}; class MainSidebar extends Component { @@ -239,7 +240,7 @@ class MainSidebar extends Component { {selectedDevice.title} )} {selectedDevice && - devicePlugins + Array.from(this.props.devicePlugins.values()) .filter(plugin => plugin.supportsDevice(selectedDevice)) .map((plugin: Class>) => ( { .map((client: Client) => ( {client.query.app} - {clientPlugins + {Array.from(this.props.clientPlugins.values()) .filter( (p: Class>) => client.plugins.indexOf(p.id) > -1, @@ -315,6 +316,7 @@ export default connect( uninitializedClients, }, notifications: {activeNotifications, blacklistedPlugins}, + plugins: {devicePlugins, clientPlugins}, }) => ({ blacklistedPlugins, activeNotifications, @@ -324,6 +326,8 @@ export default connect( selectedApp, clients, uninitializedClients, + devicePlugins, + clientPlugins, }), { selectPlugin, diff --git a/src/dispatcher/index.js b/src/dispatcher/index.js index a02f060d4..4bae83bf8 100644 --- a/src/dispatcher/index.js +++ b/src/dispatcher/index.js @@ -12,6 +12,7 @@ import application from './application'; import tracking from './tracking'; import server from './server'; import notifications from './notifications'; +import plugins from './plugins'; import type Logger from '../fb-stubs/Logger.js'; import type {Store} from '../reducers/index.js'; @@ -25,4 +26,5 @@ export default (store: Store, logger: Logger) => tracking, server, notifications, + plugins, ].forEach(fn => fn(store, logger)); diff --git a/src/dispatcher/notifications.js b/src/dispatcher/notifications.js index 476ed4db8..73aaa1f06 100644 --- a/src/dispatcher/notifications.js +++ b/src/dispatcher/notifications.js @@ -18,7 +18,6 @@ import { updateCategoryBlacklist, } from '../reducers/notifications'; import {textContent} from '../utils/index'; -import {clientPlugins} from '../plugins/index.js'; import GK from '../fb-stubs/GK'; type NotificationEvents = 'show' | 'click' | 'close' | 'reply' | 'action'; @@ -83,11 +82,7 @@ export default (store: Store, logger: Logger) => { store.subscribe(() => { const {notifications, pluginStates} = store.getState(); - - const pluginMap: Map>> = clientPlugins.reduce( - (acc, cv) => acc.set(cv.id, cv), - new Map(), - ); + const pluginMap = store.getState().plugins.clientPlugins; Object.keys(pluginStates).forEach(key => { if (knownPluginStates.get(key) !== pluginStates[key]) { diff --git a/src/dispatcher/plugins.js b/src/dispatcher/plugins.js new file mode 100644 index 000000000..9b305607c --- /dev/null +++ b/src/dispatcher/plugins.js @@ -0,0 +1,122 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + */ + +import type {Store} from '../reducers/index.js'; +import type Logger from '../fb-stubs/Logger.js'; +import type {FlipperPlugin, FlipperDevicePlugin} from '../plugin.js'; +import type {State} from '../reducers/plugins'; + +import React from 'react'; +import ReactDOM from 'react-dom'; +import * as Flipper from 'flipper'; +import {registerPlugins} from '../reducers/plugins'; +import {remote} from 'electron'; +import {GK} from 'flipper'; +import {FlipperBasePlugin} from '../plugin.js'; +import {setupMenuBar} from '../MenuBar.js'; + +type PluginDefinition = { + name: string, + out: string, + gatekeeper?: string, +}; + +export default (store: Store, logger: Logger) => { + // expose Flipper and exact globally for dynamically loaded plugins + window.React = React; + window.ReactDOM = ReactDOM; + window.Flipper = Flipper; + + const disabled = checkDisabled(); + const initialPlugins: Array< + Class | FlipperDevicePlugin<>>, + > = [...getBundledPlugins(), ...getDynamicPlugins()] + .filter(disabled) + .filter(checkGK) + .map(requirePlugin) + .filter(Boolean); + + store.dispatch(registerPlugins(initialPlugins)); + + let state: ?State = null; + store.subscribe(() => { + const newState = store.getState().plugins; + if (state !== newState) { + setupMenuBar([ + ...newState.devicePlugins.values(), + ...newState.clientPlugins.values(), + ]); + } + state = newState; + }); +}; + +function getBundledPlugins(): Array { + // DefaultPlugins that are included in the bundle. + // List of defaultPlugins is written at build time + let bundledPlugins: Array = []; + try { + bundledPlugins = window.electronRequire('./defaultPlugins/index.json'); + } catch (e) {} + + return bundledPlugins.map(plugin => ({ + ...plugin, + out: './' + plugin.out, + })); +} + +function getDynamicPlugins() { + let dynamicPlugins: Array = []; + try { + // $FlowFixMe process.env not defined in electron API spec + dynamicPlugins = JSON.parse(remote?.process.env.PLUGINS || '[]'); + } catch (e) { + console.error(e); + } + return dynamicPlugins; +} + +function checkGK(plugin: PluginDefinition): boolean { + const result = plugin.gatekeeper && !GK.get(plugin.gatekeeper); + if (!result) { + console.warn( + 'Plugin %s will be ignored as user is not part of the gatekeeper "%s".', + plugin.name, + plugin.gatekeeper, + ); + } + return !result; +} + +function checkDisabled(): (plugin: PluginDefinition) => boolean { + let disabledPlugins: Set = new Set(); + try { + disabledPlugins = new Set( + // $FlowFixMe process.env not defined in electron API spec + JSON.parse(remote?.process.env.CONFIG || '{}').disabledPlugins || [], + ); + } catch (e) { + console.error(e); + } + + return (plugin: PluginDefinition) => !disabledPlugins.has(plugin.name); +} + +function requirePlugin( + pluginDefinition: PluginDefinition, +): ?Class | FlipperDevicePlugin<>> { + try { + const plugin = window.electronRequire(pluginDefinition.out); + if (!plugin.prototype instanceof FlipperBasePlugin) { + throw new Error(`Plugin ${plugin.name} is not a FlipperBasePlugin`); + } + return plugin; + } catch (e) { + console.error(pluginDefinition, e); + return null; + } +} diff --git a/src/init.js b/src/init.js index aa6acd9f9..311029738 100644 --- a/src/init.js +++ b/src/init.js @@ -17,7 +17,6 @@ import {createStore} from 'redux'; import {persistStore} from 'redux-persist'; import reducers from './reducers/index.js'; import dispatcher from './dispatcher/index.js'; -import {setupMenuBar} from './MenuBar.js'; import TooltipProvider from './ui/components/TooltipProvider.js'; const path = require('path'); @@ -31,7 +30,6 @@ const logger = new Logger(); const bugReporter = new BugReporter(logger, store); dispatcher(store, logger); GK.init(); -setupMenuBar(); const AppFrame = () => ( diff --git a/src/plugins/index.js b/src/plugins/index.js deleted file mode 100644 index 4959e4767..000000000 --- a/src/plugins/index.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright 2018-present Facebook. - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * @format - */ - -import {GK} from 'flipper'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import * as Flipper from 'flipper'; -import { - FlipperPlugin, - FlipperBasePlugin, - FlipperDevicePlugin, -} from '../plugin.js'; -import {remote} from 'electron'; - -const plugins = new Map(); - -// expose Flipper and exact globally for dynamically loaded plugins -window.React = React; -window.ReactDOM = ReactDOM; -window.Flipper = Flipper; - -const addIfNotAdded = plugin => { - if (!plugins.has(plugin.name)) { - plugins.set(plugin.name, plugin); - } -}; - -let disabledPlugins = []; -try { - disabledPlugins = - // $FlowFixMe process.env not defined in electron API spec - JSON.parse(remote?.process.env.CONFIG || '{}').disabledPlugins || []; -} catch (e) { - console.error(e); -} - -// Load dynamic plugins -try { - // $FlowFixMe process.env not defined in electron API spec - JSON.parse(remote?.process.env.PLUGINS || '[]').forEach(addIfNotAdded); -} catch (e) { - console.error(e); -} - -// DefaultPlugins that are included in the bundle. -// List of defaultPlugins is written at build time -let bundledPlugins = []; -try { - bundledPlugins = window.electronRequire('./defaultPlugins/index.json'); -} catch (e) {} -bundledPlugins - .map(plugin => ({ - ...plugin, - out: './' + plugin.out, - })) - .forEach(addIfNotAdded); - -const exportedPlugins: Array>> = Array.from( - plugins.values(), -) - .map(plugin => { - if ( - (plugin.gatekeeper && !GK.get(plugin.gatekeeper)) || - disabledPlugins.indexOf(plugin.name) > -1 - ) { - console.warn( - 'Plugin %s will be ignored as user is not part of the gatekeeper "%s".', - plugin.name, - plugin.gatekeeper, - ); - return null; - } else { - try { - return window.electronRequire(plugin.out); - } catch (e) { - console.error(plugin, e); - return null; - } - } - }) - .filter(Boolean) - .filter(plugin => plugin.prototype instanceof FlipperBasePlugin) - .sort((a, b) => (a.title || '').localeCompare(b.title || '')); - -export default exportedPlugins; -export const devicePlugins: Array>> = - // $FlowFixMe - exportedPlugins.filter( - plugin => plugin.prototype instanceof FlipperDevicePlugin, - ); -export const clientPlugins: Array>> = - // $FlowFixMe - exportedPlugins.filter(plugin => plugin.prototype instanceof FlipperPlugin); diff --git a/src/reducers/index.js b/src/reducers/index.js index baf329bb7..3b30d4db3 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -10,6 +10,7 @@ import application from './application.js'; import connections from './connections.js'; import pluginStates from './pluginStates.js'; import notifications from './notifications.js'; +import plugins from './plugins.js'; import {persistReducer} from 'redux-persist'; import storage from 'redux-persist/lib/storage/index.js'; @@ -22,26 +23,32 @@ import type { Action as DevicesAction, } from './connections.js'; import type { - State as PluginsState, - Action as PluginsAction, + State as PluginStatesState, + Action as PluginStatesAction, } from './pluginStates.js'; import type { State as NotificationsState, Action as NotificationsAction, } from './notifications.js'; +import type { + State as PluginsState, + Action as PluginsAction, +} from './plugins.js'; import type {Store as ReduxStore} from 'redux'; export type Store = ReduxStore< - { + {| application: ApplicationState, connections: DevicesState, - pluginStates: PluginsState, + pluginStates: PluginStatesState, notifications: NotificationsState, - }, + plugins: PluginsState, + |}, | ApplicationAction | DevicesAction - | PluginsAction + | PluginStatesAction | NotificationsAction + | PluginsAction | {|type: 'INIT'|}, >; @@ -72,4 +79,5 @@ export default combineReducers({ }, notifications, ), + plugins, }); diff --git a/src/reducers/plugins.js b/src/reducers/plugins.js new file mode 100644 index 000000000..ddcb31751 --- /dev/null +++ b/src/reducers/plugins.js @@ -0,0 +1,62 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + */ + +import {FlipperPlugin, FlipperDevicePlugin} from '../plugin.js'; + +export type State = { + devicePlugins: Map>>, + clientPlugins: Map>>, +}; + +type P = Class | FlipperDevicePlugin<>>; + +export type Action = { + type: 'REGISTER_PLUGINS', + payload: Array

, +}; + +const INITIAL_STATE: State = { + devicePlugins: new Map(), + clientPlugins: new Map(), +}; + +export default function reducer( + state: State = INITIAL_STATE, + action: Action, +): State { + if (action.type === 'REGISTER_PLUGINS') { + const {devicePlugins, clientPlugins} = state; + + action.payload.forEach((p: P) => { + if (devicePlugins.has(p.id) || clientPlugins.has(p.id)) { + return; + } + + // $FlowFixMe Flow doesn't know prototype + if (p.prototype instanceof FlipperDevicePlugin) { + // $FlowFixMe Flow doesn't know p must be Class here + devicePlugins.set(p.id, p); + } else if (p.prototype instanceof FlipperPlugin) { + // $FlowFixMe Flow doesn't know p must be Class here + clientPlugins.set(p.id, p); + } + }); + + return { + ...state, + devicePlugins, + clientPlugins, + }; + } else { + return state; + } +} + +export const registerPlugins = (payload: Array

): Action => ({ + type: 'REGISTER_PLUGINS', + payload, +});