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, +});