Command processing (3/n): Uninstall plugin
Summary: *Stack summary*: this stack refactors plugin management actions to perform them in a dispatcher rather than in the root reducer (store.tsx) as all of these actions has side effects. To do that, we store requested plugin management actions (install/update/uninstall, star/unstar) in a queue which is then handled by pluginManager dispatcher. This dispatcher then dispatches all required state updates. *Diff summary*: refactored "uninstall plugin" operation to perform it in pluginManager dispatcher Reviewed By: mweststrate Differential Revision: D26166198 fbshipit-source-id: d74a1d690102d9036c6d3d8612d2428f5ecef4e6
This commit is contained in:
committed by
Facebook GitHub Bot
parent
24aed8fd45
commit
01f02b2cab
@@ -556,8 +556,7 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
|
||||
userStarredPlugins,
|
||||
},
|
||||
pluginStates,
|
||||
plugins: {devicePlugins, clientPlugins},
|
||||
pluginManager: {installedPlugins},
|
||||
plugins: {devicePlugins, clientPlugins, installedPlugins},
|
||||
pluginMessageQueue,
|
||||
settingsState,
|
||||
}) => {
|
||||
|
||||
@@ -55,8 +55,10 @@ Object {
|
||||
"disabledPlugins": Array [],
|
||||
"failedPlugins": Array [],
|
||||
"gatekeepedPlugins": Array [],
|
||||
"installedPlugins": Map {},
|
||||
"loadedPlugins": Map {},
|
||||
"marketplacePlugins": Array [],
|
||||
"selectedPlugins": Array [],
|
||||
"uninstalledPlugins": Set {},
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -29,7 +29,7 @@ import React, {useCallback, useState, useEffect} from 'react';
|
||||
import {List} from 'immutable';
|
||||
import {reportPlatformFailures, reportUsage} from '../../utils/metrics';
|
||||
import reloadFlipper from '../../utils/reloadFlipper';
|
||||
import {registerInstalledPlugins} from '../../reducers/pluginManager';
|
||||
import {registerInstalledPlugins} from '../../reducers/plugins';
|
||||
import {
|
||||
UpdateResult,
|
||||
getInstalledPlugins,
|
||||
@@ -366,7 +366,7 @@ function useNPMSearch(
|
||||
PluginInstaller.defaultProps = defaultProps;
|
||||
|
||||
export default connect<PropsFromState, DispatchFromProps, OwnProps, AppState>(
|
||||
({pluginManager: {installedPlugins}}) => ({
|
||||
({plugins: {installedPlugins}}) => ({
|
||||
installedPlugins,
|
||||
}),
|
||||
(dispatch: Dispatch<Action<any>>) => ({
|
||||
|
||||
@@ -24,7 +24,7 @@ const getUpdatablePluginsMock = mocked(getUpdatablePlugins);
|
||||
function getStore(installedPlugins: PluginDetails[] = []): Store {
|
||||
return configureStore([])({
|
||||
application: {sessionId: 'mysession'},
|
||||
pluginManager: {installedPlugins},
|
||||
plugins: {installedPlugins},
|
||||
}) as Store;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
jest.mock('../plugins');
|
||||
jest.mock('../../utils/electronModuleCache');
|
||||
import {loadPlugin} from '../../reducers/pluginManager';
|
||||
import {loadPlugin, uninstallPlugin} from '../../reducers/pluginManager';
|
||||
import {requirePlugin} from '../plugins';
|
||||
import {mocked} from 'ts-jest/utils';
|
||||
import {TestUtils} from 'flipper-plugin';
|
||||
@@ -19,12 +19,14 @@ import MockFlipper from '../../test-utils/MockFlipper';
|
||||
|
||||
const pluginDetails1 = TestUtils.createMockPluginDetails({
|
||||
id: 'plugin1',
|
||||
name: 'flipper-plugin1',
|
||||
version: '0.0.1',
|
||||
});
|
||||
const pluginDefinition1 = new SandyPluginDefinition(pluginDetails1, TestPlugin);
|
||||
|
||||
const pluginDetails1V2 = TestUtils.createMockPluginDetails({
|
||||
id: 'plugin1',
|
||||
name: 'flipper-plugin1',
|
||||
version: '0.0.2',
|
||||
});
|
||||
const pluginDefinition1V2 = new SandyPluginDefinition(
|
||||
@@ -32,7 +34,10 @@ const pluginDefinition1V2 = new SandyPluginDefinition(
|
||||
TestPlugin,
|
||||
);
|
||||
|
||||
const pluginDetails2 = TestUtils.createMockPluginDetails({id: 'plugin2'});
|
||||
const pluginDetails2 = TestUtils.createMockPluginDetails({
|
||||
id: 'plugin2',
|
||||
name: 'flipper-plugin2',
|
||||
});
|
||||
const pluginDefinition2 = new SandyPluginDefinition(pluginDetails2, TestPlugin);
|
||||
|
||||
const mockedRequirePlugin = mocked(requirePlugin);
|
||||
@@ -106,3 +111,48 @@ test('load and enable Sandy plugin', async () => {
|
||||
);
|
||||
expect(mockFlipper.clients[0].sandyPluginStates.has('plugin1')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('uninstall plugin', async () => {
|
||||
mockFlipper.dispatch(
|
||||
loadPlugin({plugin: pluginDetails1, enable: true, notifyIfFailed: false}),
|
||||
);
|
||||
mockFlipper.dispatch(uninstallPlugin({plugin: pluginDefinition1}));
|
||||
expect(
|
||||
mockFlipper.getState().plugins.clientPlugins.has('plugin1'),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
mockFlipper.getState().plugins.loadedPlugins.has('plugin1'),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
mockFlipper.getState().plugins.uninstalledPlugins.has('flipper-plugin1'),
|
||||
).toBeTruthy();
|
||||
expect(mockFlipper.clients[0].sandyPluginStates.has('plugin1')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('uninstall bundled plugin', async () => {
|
||||
const pluginDetails = TestUtils.createMockBundledPluginDetails({
|
||||
id: 'bundled-plugin',
|
||||
name: 'flipper-bundled-plugin',
|
||||
version: '0.43.0',
|
||||
});
|
||||
const pluginDefinition = new SandyPluginDefinition(pluginDetails, TestPlugin);
|
||||
mockedRequirePlugin.mockReturnValue(pluginDefinition);
|
||||
mockFlipper.dispatch(
|
||||
loadPlugin({plugin: pluginDetails, enable: true, notifyIfFailed: false}),
|
||||
);
|
||||
mockFlipper.dispatch(uninstallPlugin({plugin: pluginDefinition}));
|
||||
expect(
|
||||
mockFlipper.getState().plugins.clientPlugins.has('bundled-plugin'),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
mockFlipper.getState().plugins.loadedPlugins.has('bundled-plugin'),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
mockFlipper
|
||||
.getState()
|
||||
.plugins.uninstalledPlugins.has('flipper-bundled-plugin'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
mockFlipper.clients[0].sandyPluginStates.has('bundled-plugin'),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -27,8 +27,9 @@ import path from 'path';
|
||||
import tmp from 'tmp';
|
||||
import {promisify} from 'util';
|
||||
import {reportPlatformFailures, reportUsage} from '../utils/metrics';
|
||||
import {loadPlugin, pluginInstalled} from '../reducers/pluginManager';
|
||||
import {loadPlugin} from '../reducers/pluginManager';
|
||||
import {showErrorNotification} from '../utils/notifications';
|
||||
import {pluginInstalled} from '../reducers/plugins';
|
||||
|
||||
// Adapter which forces node.js implementation for axios instead of browser implementation
|
||||
// used by default in Electron. Node.js implementation is better, because it
|
||||
|
||||
@@ -7,27 +7,33 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {Store} from '../reducers/index';
|
||||
import {Logger} from '../fb-interfaces/Logger';
|
||||
import type {Store} from '../reducers/index';
|
||||
import type {Logger} from '../fb-interfaces/Logger';
|
||||
import {clearPluginState} from '../reducers/pluginStates';
|
||||
import {
|
||||
LoadPluginActionPayload,
|
||||
pluginCommandsProcessed,
|
||||
registerInstalledPlugins,
|
||||
UninstallPluginActionPayload,
|
||||
} from '../reducers/pluginManager';
|
||||
import {
|
||||
getInstalledPlugins,
|
||||
cleanupOldInstalledPluginVersions,
|
||||
removePlugins,
|
||||
ActivatablePluginDetails,
|
||||
} from 'flipper-plugin-lib';
|
||||
import {sideEffect} from '../utils/sideEffect';
|
||||
import {requirePlugin} from './plugins';
|
||||
import {registerPluginUpdate} from '../reducers/connections';
|
||||
import {showErrorNotification} from '../utils/notifications';
|
||||
import type Client from '../Client';
|
||||
import {unloadModule} from '../utils/electronModuleCache';
|
||||
import {pluginUninstalled, registerInstalledPlugins} from '../reducers/plugins';
|
||||
import {defaultEnabledBackgroundPlugins} from '../utils/pluginUtils';
|
||||
|
||||
const maxInstalledPluginVersionsToKeep = 2;
|
||||
|
||||
function refreshInstalledPlugins(store: Store) {
|
||||
removePlugins(store.getState().pluginManager.uninstalledPlugins.values())
|
||||
removePlugins(store.getState().plugins.uninstalledPlugins.values())
|
||||
.then(() =>
|
||||
cleanupOldInstalledPluginVersions(maxInstalledPluginVersionsToKeep),
|
||||
)
|
||||
@@ -65,6 +71,9 @@ export default (
|
||||
case 'LOAD_PLUGIN':
|
||||
loadPlugin(store, command.payload);
|
||||
break;
|
||||
case 'UNINSTALL_PLUGIN':
|
||||
uninstallPlugin(store, command.payload);
|
||||
break;
|
||||
default:
|
||||
console.error('Unexpected plugin command', command);
|
||||
break;
|
||||
@@ -95,8 +104,56 @@ function loadPlugin(store: Store, payload: LoadPluginActionPayload) {
|
||||
);
|
||||
if (payload.notifyIfFailed) {
|
||||
showErrorNotification(
|
||||
`Failed to load plugin "${payload.plugin.title}" v${payload.plugin.version}`,
|
||||
`Failed to activate plugin "${payload.plugin.title}" v${payload.plugin.version}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function uninstallPlugin(store: Store, {plugin}: UninstallPluginActionPayload) {
|
||||
try {
|
||||
const state = store.getState();
|
||||
const clients = state.connections.clients;
|
||||
clients.forEach((client) => {
|
||||
stopPlugin(client, plugin.id);
|
||||
});
|
||||
store.dispatch(clearPluginState({pluginId: plugin.id}));
|
||||
if (!plugin.details.isBundled) {
|
||||
unloadPluginModule(plugin.details);
|
||||
}
|
||||
store.dispatch(pluginUninstalled(plugin.details));
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed to uninstall plugin ${plugin.title} v${plugin.version}`,
|
||||
err,
|
||||
);
|
||||
showErrorNotification(
|
||||
`Failed to uninstall plugin "${plugin.title}" v${plugin.version}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function stopPlugin(
|
||||
client: Client,
|
||||
pluginId: string,
|
||||
forceInitBackgroundPlugin: boolean = false,
|
||||
): boolean {
|
||||
if (
|
||||
(forceInitBackgroundPlugin ||
|
||||
!defaultEnabledBackgroundPlugins.includes(pluginId)) &&
|
||||
client?.isBackgroundPlugin(pluginId)
|
||||
) {
|
||||
client.deinitPlugin(pluginId);
|
||||
}
|
||||
// stop sandy plugins
|
||||
client.stopPluginIfNeeded(pluginId);
|
||||
return true;
|
||||
}
|
||||
|
||||
function unloadPluginModule(plugin: ActivatablePluginDetails) {
|
||||
if (plugin.isBundled) {
|
||||
// We cannot unload bundled plugin.
|
||||
return;
|
||||
}
|
||||
unloadModule(plugin.entry);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export default async (store: Store, logger: Logger) => {
|
||||
|
||||
defaultPluginsIndex = getDefaultPluginsIndex();
|
||||
|
||||
const uninstalledPlugins = store.getState().pluginManager.uninstalledPlugins;
|
||||
const uninstalledPlugins = store.getState().plugins.uninstalledPlugins;
|
||||
|
||||
const bundledPlugins = getBundledPlugins();
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* 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 {default as reducer, registerInstalledPlugins} from '../pluginManager';
|
||||
import {InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||
|
||||
test('reduce empty registerInstalledPlugins', () => {
|
||||
const result = reducer(undefined, registerInstalledPlugins([]));
|
||||
expect(result.installedPlugins).toEqual(new Map());
|
||||
});
|
||||
|
||||
const EXAMPLE_PLUGIN = {
|
||||
name: 'test',
|
||||
version: '0.1',
|
||||
description: 'my test plugin',
|
||||
dir: '/plugins/test',
|
||||
specVersion: 2,
|
||||
source: 'src/index.ts',
|
||||
isBundled: false,
|
||||
isActivatable: true,
|
||||
main: 'lib/index.js',
|
||||
title: 'test',
|
||||
id: 'test',
|
||||
entry: '/plugins/test/lib/index.js',
|
||||
} as InstalledPluginDetails;
|
||||
|
||||
test('reduce registerInstalledPlugins, clear again', () => {
|
||||
const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN]));
|
||||
expect(result.installedPlugins).toEqual(
|
||||
new Map([[EXAMPLE_PLUGIN.name, EXAMPLE_PLUGIN]]),
|
||||
);
|
||||
const result2 = reducer(result, registerInstalledPlugins([]));
|
||||
expect(result2.installedPlugins).toEqual(new Map());
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
default as reducer,
|
||||
registerPlugins,
|
||||
addGatekeepedPlugins,
|
||||
registerInstalledPlugins,
|
||||
} from '../plugins';
|
||||
import {FlipperPlugin, FlipperDevicePlugin, BaseAction} from '../../plugin';
|
||||
import {InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||
@@ -39,6 +40,8 @@ test('add clientPlugin', () => {
|
||||
disabledPlugins: [],
|
||||
selectedPlugins: [],
|
||||
marketplacePlugins: [],
|
||||
uninstalledPlugins: new Set(),
|
||||
installedPlugins: new Map(),
|
||||
},
|
||||
registerPlugins([testPlugin]),
|
||||
);
|
||||
@@ -57,6 +60,8 @@ test('add devicePlugin', () => {
|
||||
disabledPlugins: [],
|
||||
selectedPlugins: [],
|
||||
marketplacePlugins: [],
|
||||
uninstalledPlugins: new Set(),
|
||||
installedPlugins: new Map(),
|
||||
},
|
||||
registerPlugins([testDevicePlugin]),
|
||||
);
|
||||
@@ -75,6 +80,8 @@ test('do not add plugin twice', () => {
|
||||
disabledPlugins: [],
|
||||
selectedPlugins: [],
|
||||
marketplacePlugins: [],
|
||||
uninstalledPlugins: new Set(),
|
||||
installedPlugins: new Map(),
|
||||
},
|
||||
registerPlugins([testPlugin, testPlugin]),
|
||||
);
|
||||
@@ -109,8 +116,39 @@ test('add gatekeeped plugin', () => {
|
||||
disabledPlugins: [],
|
||||
selectedPlugins: [],
|
||||
marketplacePlugins: [],
|
||||
installedPlugins: new Map(),
|
||||
uninstalledPlugins: new Set(),
|
||||
},
|
||||
addGatekeepedPlugins(gatekeepedPlugins),
|
||||
);
|
||||
expect(res.gatekeepedPlugins).toEqual(gatekeepedPlugins);
|
||||
});
|
||||
|
||||
test('reduce empty registerInstalledPlugins', () => {
|
||||
const result = reducer(undefined, registerInstalledPlugins([]));
|
||||
expect(result.installedPlugins).toEqual(new Map());
|
||||
});
|
||||
|
||||
const EXAMPLE_PLUGIN = {
|
||||
name: 'test',
|
||||
version: '0.1',
|
||||
description: 'my test plugin',
|
||||
dir: '/plugins/test',
|
||||
specVersion: 2,
|
||||
source: 'src/index.ts',
|
||||
isBundled: false,
|
||||
isActivatable: true,
|
||||
main: 'lib/index.js',
|
||||
title: 'test',
|
||||
id: 'test',
|
||||
entry: '/plugins/test/lib/index.js',
|
||||
} as InstalledPluginDetails;
|
||||
|
||||
test('reduce registerInstalledPlugins, clear again', () => {
|
||||
const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN]));
|
||||
expect(result.installedPlugins).toEqual(
|
||||
new Map([[EXAMPLE_PLUGIN.name, EXAMPLE_PLUGIN]]),
|
||||
);
|
||||
const result2 = reducer(result, registerInstalledPlugins([]));
|
||||
expect(result2.installedPlugins).toEqual(new Map());
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ export type State = {
|
||||
settingsState: SettingsState & PersistPartial;
|
||||
launcherSettingsState: LauncherSettingsState & PersistPartial;
|
||||
supportForm: SupportFormState;
|
||||
pluginManager: PluginManagerState & PersistPartial;
|
||||
pluginManager: PluginManagerState;
|
||||
healthchecks: HealthcheckState & PersistPartial;
|
||||
usageTracking: TrackingState;
|
||||
pluginDownloads: PluginDownloadsState;
|
||||
@@ -159,20 +159,13 @@ export default combineReducers<State, Actions>({
|
||||
{
|
||||
key: 'plugins',
|
||||
storage,
|
||||
whitelist: ['marketplacePlugins'],
|
||||
whitelist: ['marketplacePlugins', 'uninstalledPlugins'],
|
||||
transforms: [setTransformer({whitelist: ['uninstalledPlugins']})],
|
||||
},
|
||||
plugins,
|
||||
),
|
||||
supportForm,
|
||||
pluginManager: persistReducer<PluginManagerState, Actions>(
|
||||
{
|
||||
key: 'pluginManager',
|
||||
storage,
|
||||
whitelist: ['uninstalledPlugins'],
|
||||
transforms: [setTransformer({whitelist: ['uninstalledPlugins']})],
|
||||
},
|
||||
pluginManager,
|
||||
),
|
||||
pluginManager,
|
||||
user: persistReducer(
|
||||
{
|
||||
key: 'user',
|
||||
|
||||
@@ -7,22 +7,16 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {Actions} from './';
|
||||
import {
|
||||
ActivatablePluginDetails,
|
||||
InstalledPluginDetails,
|
||||
} from 'flipper-plugin-lib';
|
||||
import {PluginDefinition} from '../plugin';
|
||||
import type {Actions} from './';
|
||||
import type {ActivatablePluginDetails} from 'flipper-plugin-lib';
|
||||
import type {PluginDefinition} from '../plugin';
|
||||
import {produce} from 'immer';
|
||||
import semver from 'semver';
|
||||
|
||||
export type State = {
|
||||
installedPlugins: Map<string, InstalledPluginDetails>;
|
||||
uninstalledPlugins: Set<string>;
|
||||
pluginCommandsQueue: PluginCommand[];
|
||||
};
|
||||
|
||||
export type PluginCommand = LoadPluginAction;
|
||||
export type PluginCommand = LoadPluginAction | UninstallPluginAction;
|
||||
|
||||
export type LoadPluginActionPayload = {
|
||||
plugin: ActivatablePluginDetails;
|
||||
@@ -35,29 +29,23 @@ export type LoadPluginAction = {
|
||||
payload: LoadPluginActionPayload;
|
||||
};
|
||||
|
||||
export type UninstallPluginActionPayload = {
|
||||
plugin: PluginDefinition;
|
||||
};
|
||||
|
||||
export type UninstallPluginAction = {
|
||||
type: 'UNINSTALL_PLUGIN';
|
||||
payload: UninstallPluginActionPayload;
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'REGISTER_INSTALLED_PLUGINS';
|
||||
payload: InstalledPluginDetails[];
|
||||
}
|
||||
| {
|
||||
// Implemented by rootReducer in `store.tsx`
|
||||
type: 'UNINSTALL_PLUGIN';
|
||||
payload: PluginDefinition;
|
||||
}
|
||||
| {
|
||||
type: 'PLUGIN_INSTALLED';
|
||||
payload: InstalledPluginDetails;
|
||||
}
|
||||
| {
|
||||
type: 'PLUGIN_COMMANDS_PROCESSED';
|
||||
payload: number;
|
||||
}
|
||||
| LoadPluginAction;
|
||||
| PluginCommand;
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
installedPlugins: new Map<string, InstalledPluginDetails>(),
|
||||
uninstalledPlugins: new Set<string>(),
|
||||
pluginCommandsQueue: [],
|
||||
};
|
||||
|
||||
@@ -65,55 +53,28 @@ export default function reducer(
|
||||
state: State = INITIAL_STATE,
|
||||
action: Actions,
|
||||
): State {
|
||||
if (action.type === 'REGISTER_INSTALLED_PLUGINS') {
|
||||
return produce(state, (draft) => {
|
||||
draft.installedPlugins = new Map(
|
||||
action.payload
|
||||
.filter((p) => !state.uninstalledPlugins?.has(p.name))
|
||||
.map((p) => [p.name, p]),
|
||||
);
|
||||
});
|
||||
} else if (action.type === 'PLUGIN_INSTALLED') {
|
||||
const plugin = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
const existing = draft.installedPlugins.get(plugin.name);
|
||||
if (!existing || semver.gt(plugin.version, existing.version)) {
|
||||
draft.installedPlugins.set(plugin.name, plugin);
|
||||
}
|
||||
});
|
||||
} else if (action.type === 'LOAD_PLUGIN') {
|
||||
return produce(state, (draft) => {
|
||||
draft.pluginCommandsQueue.push({
|
||||
type: 'LOAD_PLUGIN',
|
||||
payload: action.payload,
|
||||
switch (action.type) {
|
||||
case 'LOAD_PLUGIN':
|
||||
case 'UNINSTALL_PLUGIN':
|
||||
return produce(state, (draft) => {
|
||||
draft.pluginCommandsQueue.push(action);
|
||||
});
|
||||
});
|
||||
} else if (action.type === 'PLUGIN_COMMANDS_PROCESSED') {
|
||||
return produce(state, (draft) => {
|
||||
draft.pluginCommandsQueue.splice(0, action.payload);
|
||||
});
|
||||
} else {
|
||||
return {...state};
|
||||
case 'PLUGIN_COMMANDS_PROCESSED':
|
||||
return produce(state, (draft) => {
|
||||
draft.pluginCommandsQueue.splice(0, action.payload);
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const registerInstalledPlugins = (
|
||||
payload: InstalledPluginDetails[],
|
||||
export const uninstallPlugin = (
|
||||
payload: UninstallPluginActionPayload,
|
||||
): Action => ({
|
||||
type: 'REGISTER_INSTALLED_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const uninstallPlugin = (payload: PluginDefinition): Action => ({
|
||||
type: 'UNINSTALL_PLUGIN',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const pluginInstalled = (payload: InstalledPluginDetails): Action => ({
|
||||
type: 'PLUGIN_INSTALLED',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const loadPlugin = (payload: LoadPluginActionPayload): Action => ({
|
||||
type: 'LOAD_PLUGIN',
|
||||
payload,
|
||||
|
||||
@@ -40,6 +40,10 @@ export type Action =
|
||||
| {
|
||||
type: 'CLEAR_CLIENT_PLUGINS_STATE';
|
||||
payload: {clientId: string; devicePlugins: Set<string>};
|
||||
}
|
||||
| {
|
||||
type: 'CLEAR_PLUGIN_STATE';
|
||||
payload: {pluginId: string};
|
||||
};
|
||||
|
||||
const INITIAL_STATE: State = {};
|
||||
@@ -93,6 +97,19 @@ export default function reducer(
|
||||
return newState;
|
||||
}, {});
|
||||
}
|
||||
|
||||
case 'CLEAR_PLUGIN_STATE': {
|
||||
const {pluginId} = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
Object.keys(draft).forEach((pluginKey) => {
|
||||
const pluginKeyParts = deconstructPluginKey(pluginKey);
|
||||
if (pluginKeyParts.pluginName === pluginId) {
|
||||
delete draft[pluginKey];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {Actions} from '.';
|
||||
import {produce} from 'immer';
|
||||
import {Actions} from '.';
|
||||
import {deconstructPluginKey} from '../utils/clientUtils';
|
||||
|
||||
export type State = {
|
||||
@@ -29,6 +30,10 @@ export type Action =
|
||||
| {
|
||||
type: 'CLEAR_CLIENT_PLUGINS_STATE';
|
||||
payload: {clientId: string; devicePlugins: Set<string>};
|
||||
}
|
||||
| {
|
||||
type: 'CLEAR_PLUGIN_STATE';
|
||||
payload: {pluginId: string};
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
@@ -63,6 +68,16 @@ export default function reducer(
|
||||
}
|
||||
return newState;
|
||||
}, {});
|
||||
} else if (action.type === 'CLEAR_PLUGIN_STATE') {
|
||||
const {pluginId} = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
Object.keys(draft).forEach((pluginKey) => {
|
||||
const pluginKeyParts = deconstructPluginKey(pluginKey);
|
||||
if (pluginKeyParts.pluginName === pluginId) {
|
||||
delete draft[pluginKey];
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
@@ -75,3 +90,8 @@ export const setPluginState = (payload: {
|
||||
type: 'SET_PLUGIN_STATE',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const clearPluginState = (payload: {pluginId: string}): Action => ({
|
||||
type: 'CLEAR_PLUGIN_STATE',
|
||||
payload,
|
||||
});
|
||||
|
||||
@@ -16,10 +16,12 @@ import type {
|
||||
DownloadablePluginDetails,
|
||||
ActivatablePluginDetails,
|
||||
BundledPluginDetails,
|
||||
InstalledPluginDetails,
|
||||
} from 'flipper-plugin-lib';
|
||||
import type {Actions} from '.';
|
||||
import produce from 'immer';
|
||||
import {isDevicePluginDefinition} from '../utils/pluginUtils';
|
||||
import semver from 'semver';
|
||||
|
||||
export type State = {
|
||||
devicePlugins: DevicePluginMap;
|
||||
@@ -31,6 +33,8 @@ export type State = {
|
||||
failedPlugins: Array<[ActivatablePluginDetails, string]>;
|
||||
selectedPlugins: Array<string>;
|
||||
marketplacePlugins: Array<DownloadablePluginDetails>;
|
||||
uninstalledPlugins: Set<string>;
|
||||
installedPlugins: Map<string, InstalledPluginDetails>;
|
||||
};
|
||||
|
||||
export type RegisterPluginAction = {
|
||||
@@ -67,20 +71,36 @@ export type Action =
|
||||
| {
|
||||
type: 'REGISTER_BUNDLED_PLUGINS';
|
||||
payload: Array<BundledPluginDetails>;
|
||||
}
|
||||
| {
|
||||
type: 'REGISTER_INSTALLED_PLUGINS';
|
||||
payload: InstalledPluginDetails[];
|
||||
}
|
||||
| {
|
||||
type: 'PLUGIN_INSTALLED';
|
||||
payload: InstalledPluginDetails;
|
||||
}
|
||||
| {
|
||||
type: 'PLUGIN_UNINSTALLED';
|
||||
payload: ActivatablePluginDetails;
|
||||
};
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
devicePlugins: new Map(),
|
||||
clientPlugins: new Map(),
|
||||
loadedPlugins: new Map(),
|
||||
bundledPlugins: new Map(),
|
||||
gatekeepedPlugins: [],
|
||||
disabledPlugins: [],
|
||||
failedPlugins: [],
|
||||
selectedPlugins: [],
|
||||
marketplacePlugins: [],
|
||||
uninstalledPlugins: new Set(),
|
||||
installedPlugins: new Map(),
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: State | undefined = {
|
||||
devicePlugins: new Map(),
|
||||
clientPlugins: new Map(),
|
||||
loadedPlugins: new Map(),
|
||||
bundledPlugins: new Map(),
|
||||
gatekeepedPlugins: [],
|
||||
disabledPlugins: [],
|
||||
failedPlugins: [],
|
||||
selectedPlugins: [],
|
||||
marketplacePlugins: [],
|
||||
},
|
||||
state: State | undefined = INITIAL_STATE,
|
||||
action: Actions,
|
||||
): State {
|
||||
if (action.type === 'REGISTER_PLUGINS') {
|
||||
@@ -133,6 +153,31 @@ export default function reducer(
|
||||
...state,
|
||||
bundledPlugins: new Map(action.payload.map((p) => [p.id, p])),
|
||||
};
|
||||
} else if (action.type === 'REGISTER_INSTALLED_PLUGINS') {
|
||||
return produce(state, (draft) => {
|
||||
draft.installedPlugins.clear();
|
||||
action.payload.forEach((p) => {
|
||||
if (!draft.uninstalledPlugins.has(p.id)) {
|
||||
draft.installedPlugins.set(p.id, p);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (action.type === 'PLUGIN_INSTALLED') {
|
||||
const plugin = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
const existing = draft.installedPlugins.get(plugin.name);
|
||||
if (!existing || semver.gt(plugin.version, existing.version)) {
|
||||
draft.installedPlugins.set(plugin.name, plugin);
|
||||
}
|
||||
});
|
||||
} else if (action.type === 'PLUGIN_UNINSTALLED') {
|
||||
const plugin = action.payload;
|
||||
return produce(state, (draft) => {
|
||||
draft.clientPlugins.delete(plugin.id);
|
||||
draft.devicePlugins.delete(plugin.id);
|
||||
draft.loadedPlugins.delete(plugin.id);
|
||||
draft.uninstalledPlugins.add(plugin.name);
|
||||
});
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
@@ -189,3 +234,22 @@ export const registerBundledPlugins = (
|
||||
type: 'REGISTER_BUNDLED_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const registerInstalledPlugins = (
|
||||
payload: InstalledPluginDetails[],
|
||||
): Action => ({
|
||||
type: 'REGISTER_INSTALLED_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const pluginInstalled = (payload: InstalledPluginDetails): Action => ({
|
||||
type: 'PLUGIN_INSTALLED',
|
||||
payload,
|
||||
});
|
||||
|
||||
export const pluginUninstalled = (
|
||||
payload: ActivatablePluginDetails,
|
||||
): Action => ({
|
||||
type: 'PLUGIN_UNINSTALLED',
|
||||
payload,
|
||||
});
|
||||
|
||||
@@ -164,7 +164,7 @@ export const PluginList = memo(function PluginList({
|
||||
(id: string) => {
|
||||
const plugin = disabledPlugins.find((p) => p.id === id)!;
|
||||
reportUsage('plugin:uninstall', {version: plugin.version}, plugin.id);
|
||||
dispatch(uninstallPlugin(plugin));
|
||||
dispatch(uninstallPlugin({plugin}));
|
||||
},
|
||||
[disabledPlugins, dispatch],
|
||||
);
|
||||
|
||||
@@ -84,9 +84,6 @@ export function rootReducer(
|
||||
} else {
|
||||
return updateClientPlugin(state, plugin, enablePlugin);
|
||||
}
|
||||
} else if (action.type === 'UNINSTALL_PLUGIN' && state) {
|
||||
const plugin = action.payload;
|
||||
return uninstallPlugin(state, plugin);
|
||||
}
|
||||
|
||||
// otherwise
|
||||
@@ -184,27 +181,6 @@ function updateClientPlugin(
|
||||
});
|
||||
}
|
||||
|
||||
function uninstallPlugin(state: StoreState, plugin: PluginDefinition) {
|
||||
const clients = state.connections.clients;
|
||||
return produce(state, (draft) => {
|
||||
clients.forEach((client) => {
|
||||
stopPlugin(client, plugin.id);
|
||||
const pluginKey = getPluginKey(
|
||||
client.id,
|
||||
{serial: client.query.device_id},
|
||||
plugin.id,
|
||||
);
|
||||
delete draft.pluginMessageQueue[pluginKey];
|
||||
});
|
||||
cleanupPluginStates(draft.pluginStates, plugin.id);
|
||||
unloadPluginModule(plugin.details);
|
||||
draft.plugins.clientPlugins.delete(plugin.id);
|
||||
draft.plugins.devicePlugins.delete(plugin.id);
|
||||
draft.plugins.loadedPlugins.delete(plugin.id);
|
||||
draft.pluginManager.uninstalledPlugins.add(plugin.details.name);
|
||||
});
|
||||
}
|
||||
|
||||
function updateDevicePlugin(state: StoreState, plugin: DevicePluginDefinition) {
|
||||
const devices = state.connections.devices;
|
||||
return produce(state, (draft) => {
|
||||
@@ -235,7 +211,7 @@ function registerLoadedPlugin(
|
||||
},
|
||||
plugin: ActivatablePluginDetails,
|
||||
) {
|
||||
draft.pluginManager.uninstalledPlugins.delete(plugin.name);
|
||||
draft.plugins.uninstalledPlugins.delete(plugin.name);
|
||||
draft.plugins.loadedPlugins.set(plugin.id, plugin);
|
||||
}
|
||||
|
||||
|
||||
@@ -766,6 +766,8 @@ test('test determinePluginsToProcess for mutilple clients having plugins present
|
||||
failedPlugins: [],
|
||||
selectedPlugins: ['TestPlugin'],
|
||||
marketplacePlugins: [],
|
||||
installedPlugins: new Map(),
|
||||
uninstalledPlugins: new Set(),
|
||||
};
|
||||
const op = determinePluginsToProcess(
|
||||
[client1, client2, client3],
|
||||
@@ -838,6 +840,8 @@ test('test determinePluginsToProcess for no selected plugin present in any clien
|
||||
failedPlugins: [],
|
||||
selectedPlugins: ['RandomPlugin'],
|
||||
marketplacePlugins: [],
|
||||
installedPlugins: new Map(),
|
||||
uninstalledPlugins: new Set(),
|
||||
};
|
||||
const op = determinePluginsToProcess([client1, client2], device1, plugins);
|
||||
expect(op).toBeDefined();
|
||||
@@ -887,6 +891,8 @@ test('test determinePluginsToProcess for multiple clients on same device', async
|
||||
failedPlugins: [],
|
||||
selectedPlugins: ['TestPlugin'],
|
||||
marketplacePlugins: [],
|
||||
installedPlugins: new Map(),
|
||||
uninstalledPlugins: new Set(),
|
||||
};
|
||||
const op = determinePluginsToProcess([client1, client2], device1, plugins);
|
||||
expect(op).toBeDefined();
|
||||
@@ -974,6 +980,8 @@ test('test determinePluginsToProcess for multiple clients on different device',
|
||||
failedPlugins: [],
|
||||
selectedPlugins: ['TestPlugin'],
|
||||
marketplacePlugins: [],
|
||||
installedPlugins: new Map(),
|
||||
uninstalledPlugins: new Set(),
|
||||
};
|
||||
const op = determinePluginsToProcess(
|
||||
[client1Device1, client2Device1, client1Device2, client2Device2],
|
||||
@@ -1057,6 +1065,8 @@ test('test determinePluginsToProcess to ignore archived clients', async () => {
|
||||
failedPlugins: [],
|
||||
selectedPlugins: ['TestPlugin'],
|
||||
marketplacePlugins: [],
|
||||
installedPlugins: new Map(),
|
||||
uninstalledPlugins: new Set(),
|
||||
};
|
||||
const op = determinePluginsToProcess(
|
||||
[client, archivedClient],
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
act as testingLibAct,
|
||||
} from '@testing-library/react';
|
||||
import {queries} from '@testing-library/dom';
|
||||
import {InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||
import {BundledPluginDetails, InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||
|
||||
import {
|
||||
RealFlipperClient,
|
||||
@@ -414,6 +414,24 @@ export function createMockPluginDetails(
|
||||
};
|
||||
}
|
||||
|
||||
export function createMockBundledPluginDetails(
|
||||
details?: Partial<BundledPluginDetails>,
|
||||
): BundledPluginDetails {
|
||||
return {
|
||||
id: 'TestBundledPlugin',
|
||||
name: 'TestBundledPlugin',
|
||||
specVersion: 0,
|
||||
pluginType: 'client',
|
||||
isBundled: true,
|
||||
isActivatable: true,
|
||||
main: '',
|
||||
source: '',
|
||||
title: 'Testing Bundled Plugin',
|
||||
version: '',
|
||||
...details,
|
||||
};
|
||||
}
|
||||
|
||||
function createMockDevice(options?: StartPluginOptions): RealFlipperDevice {
|
||||
const logListeners: (undefined | DeviceLogListener)[] = [];
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user