Summary: Fixed a bug when plugin installation status wouldn't be saved between sessions when plugin uninstalled and then re-installed again. Before the fix, after Flipper restart, such plugin was uninstalled again because its package name was not removed from "uninstalledPlugins" state. This was because plugin id was used by mistake instead of name in few places. To try avoiding this issue in future I've also renamed "uninstalledPlugins" to "uninstalledPluginNames" to make it more clear than package name should be used there rather than ID. As this field is persisted, I also added migration which moves data to the renamed field. Reviewed By: passy Differential Revision: D28314447 fbshipit-source-id: fbe3edc258b78fe7fbb0d966f93aabcdf3b66d4b
360 lines
11 KiB
TypeScript
360 lines
11 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 type {Store} from '../reducers/index';
|
|
import type {Logger} from '../fb-interfaces/Logger';
|
|
import type {PluginDefinition} from '../plugin';
|
|
import React from 'react';
|
|
import ReactDOM from 'react-dom';
|
|
import adbkit from 'adbkit';
|
|
import {
|
|
registerPlugins,
|
|
addGatekeepedPlugins,
|
|
addDisabledPlugins,
|
|
addFailedPlugins,
|
|
registerLoadedPlugins,
|
|
registerBundledPlugins,
|
|
registerMarketplacePlugins,
|
|
MarketplacePluginDetails,
|
|
pluginsInitialised,
|
|
} from '../reducers/plugins';
|
|
import GK from '../fb-stubs/GK';
|
|
import {FlipperBasePlugin} from '../plugin';
|
|
import {setupMenuBar} from '../MenuBar';
|
|
import path from 'path';
|
|
import {default as config} from '../utils/processConfig';
|
|
import isProduction from '../utils/isProduction';
|
|
import {notNull} from '../utils/typeUtils';
|
|
import {sideEffect} from '../utils/sideEffect';
|
|
import semver from 'semver';
|
|
import {
|
|
ActivatablePluginDetails,
|
|
BundledPluginDetails,
|
|
PluginDetails,
|
|
} from 'flipper-plugin-lib';
|
|
import {tryCatchReportPluginFailures, reportUsage} from '../utils/metrics';
|
|
import * as FlipperPluginSDK from 'flipper-plugin';
|
|
import {_SandyPluginDefinition} from 'flipper-plugin';
|
|
import loadDynamicPlugins from '../utils/loadDynamicPlugins';
|
|
import * as Immer from 'immer';
|
|
import * as antd from 'antd';
|
|
import * as emotion_styled from '@emotion/styled';
|
|
import * as antdesign_icons from '@ant-design/icons';
|
|
// @ts-ignore
|
|
import * as crc32 from 'crc32';
|
|
|
|
// eslint-disable-next-line import/no-unresolved
|
|
import getDefaultPluginsIndex from '../utils/getDefaultPluginsIndex';
|
|
import {isDevicePluginDefinition} from '../utils/pluginUtils';
|
|
import {isPluginCompatible} from '../utils/isPluginCompatible';
|
|
|
|
let defaultPluginsIndex: any = null;
|
|
|
|
export default async (store: Store, logger: Logger) => {
|
|
// expose Flipper and exact globally for dynamically loaded plugins
|
|
const globalObject: any = typeof window === 'undefined' ? global : window;
|
|
|
|
// this list should match `replace-flipper-requires.tsx` and the `builtInModules` in `desktop/.eslintrc`
|
|
globalObject.React = React;
|
|
globalObject.ReactDOM = ReactDOM;
|
|
globalObject.Flipper = require('../index');
|
|
globalObject.adbkit = adbkit;
|
|
globalObject.FlipperPlugin = FlipperPluginSDK;
|
|
globalObject.Immer = Immer;
|
|
globalObject.antd = antd;
|
|
globalObject.emotion_styled = emotion_styled;
|
|
globalObject.antdesign_icons = antdesign_icons;
|
|
globalObject.crc32_hack_fix_me = crc32;
|
|
|
|
const gatekeepedPlugins: Array<ActivatablePluginDetails> = [];
|
|
const disabledPlugins: Array<ActivatablePluginDetails> = [];
|
|
const failedPlugins: Array<[ActivatablePluginDetails, string]> = [];
|
|
|
|
defaultPluginsIndex = getDefaultPluginsIndex();
|
|
|
|
const marketplacePlugins = store.getState().plugins.marketplacePlugins;
|
|
store.dispatch(
|
|
registerMarketplacePlugins(
|
|
selectCompatibleMarketplaceVersions(marketplacePlugins),
|
|
),
|
|
);
|
|
|
|
const uninstalledPluginNames =
|
|
store.getState().plugins.uninstalledPluginNames;
|
|
|
|
const bundledPlugins = getBundledPlugins();
|
|
|
|
const loadedPlugins = filterNewestVersionOfEachPlugin(
|
|
bundledPlugins,
|
|
await getDynamicPlugins(),
|
|
).filter((p) => !uninstalledPluginNames.has(p.name));
|
|
|
|
const initialPlugins: PluginDefinition[] = loadedPlugins
|
|
.map(reportVersion)
|
|
.filter(checkDisabled(disabledPlugins))
|
|
.filter(checkGK(gatekeepedPlugins))
|
|
.map(createRequirePluginFunction(failedPlugins))
|
|
.filter(notNull);
|
|
|
|
store.dispatch(registerBundledPlugins(bundledPlugins));
|
|
store.dispatch(registerLoadedPlugins(loadedPlugins));
|
|
store.dispatch(addGatekeepedPlugins(gatekeepedPlugins));
|
|
store.dispatch(addDisabledPlugins(disabledPlugins));
|
|
store.dispatch(addFailedPlugins(failedPlugins));
|
|
store.dispatch(registerPlugins(initialPlugins));
|
|
store.dispatch(pluginsInitialised());
|
|
|
|
sideEffect(
|
|
store,
|
|
{name: 'setupMenuBar', throttleMs: 1000, fireImmediately: true},
|
|
(state) => state.plugins,
|
|
(plugins, store) => {
|
|
setupMenuBar(
|
|
[...plugins.devicePlugins.values(), ...plugins.clientPlugins.values()],
|
|
store,
|
|
logger,
|
|
);
|
|
},
|
|
);
|
|
};
|
|
|
|
function reportVersion(pluginDetails: ActivatablePluginDetails) {
|
|
reportUsage(
|
|
'plugin:version',
|
|
{
|
|
version: pluginDetails.version,
|
|
},
|
|
pluginDetails.id,
|
|
);
|
|
return pluginDetails;
|
|
}
|
|
|
|
export function filterNewestVersionOfEachPlugin<
|
|
T1 extends PluginDetails,
|
|
T2 extends PluginDetails,
|
|
>(bundledPlugins: T1[], dynamicPlugins: T2[]): (T1 | T2)[] {
|
|
const pluginByName: {[key: string]: T1 | T2} = {};
|
|
for (const plugin of bundledPlugins) {
|
|
pluginByName[plugin.name] = plugin;
|
|
}
|
|
for (const plugin of dynamicPlugins) {
|
|
if (
|
|
!pluginByName[plugin.name] ||
|
|
(!process.env.FLIPPER_DISABLE_PLUGIN_AUTO_UPDATE &&
|
|
semver.gt(plugin.version, pluginByName[plugin.name].version, true))
|
|
) {
|
|
pluginByName[plugin.name] = plugin;
|
|
}
|
|
}
|
|
return Object.values(pluginByName);
|
|
}
|
|
|
|
function getBundledPlugins(): Array<BundledPluginDetails> {
|
|
// DefaultPlugins that are included in the bundle.
|
|
// List of defaultPlugins is written at build time
|
|
const pluginPath =
|
|
process.env.BUNDLED_PLUGIN_PATH ||
|
|
(isProduction()
|
|
? path.join(__dirname, 'defaultPlugins')
|
|
: './defaultPlugins/index.json');
|
|
|
|
let bundledPlugins: Array<BundledPluginDetails> = [];
|
|
try {
|
|
bundledPlugins = global.electronRequire(pluginPath);
|
|
} catch (e) {
|
|
console.error('Failed to load bundled plugins', e);
|
|
}
|
|
|
|
return bundledPlugins;
|
|
}
|
|
|
|
export async function getDynamicPlugins() {
|
|
try {
|
|
return await loadDynamicPlugins();
|
|
} catch (e) {
|
|
console.error('Failed to load dynamic plugins', e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export const checkGK =
|
|
(gatekeepedPlugins: Array<ActivatablePluginDetails>) =>
|
|
(plugin: ActivatablePluginDetails): boolean => {
|
|
try {
|
|
if (!plugin.gatekeeper) {
|
|
return true;
|
|
}
|
|
const result = GK.get(plugin.gatekeeper);
|
|
if (!result) {
|
|
gatekeepedPlugins.push(plugin);
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
console.error(`Failed to check GK for plugin ${plugin.id}`, err);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const checkDisabled = (
|
|
disabledPlugins: Array<ActivatablePluginDetails>,
|
|
) => {
|
|
let enabledList: Set<string> | null = null;
|
|
let disabledList: Set<string> = new Set();
|
|
try {
|
|
if (process.env.FLIPPER_ENABLED_PLUGINS) {
|
|
enabledList = new Set<string>(
|
|
process.env.FLIPPER_ENABLED_PLUGINS.split(','),
|
|
);
|
|
}
|
|
disabledList = config().disabledPlugins;
|
|
} catch (e) {
|
|
console.error('Failed to compute enabled/disabled plugins', e);
|
|
}
|
|
return (plugin: ActivatablePluginDetails): boolean => {
|
|
try {
|
|
if (disabledList.has(plugin.name)) {
|
|
disabledPlugins.push(plugin);
|
|
return false;
|
|
}
|
|
if (
|
|
enabledList &&
|
|
!(
|
|
enabledList.has(plugin.name) ||
|
|
enabledList.has(plugin.id) ||
|
|
enabledList.has(plugin.name.replace('flipper-plugin-', ''))
|
|
)
|
|
) {
|
|
disabledPlugins.push(plugin);
|
|
return false;
|
|
}
|
|
return true;
|
|
} catch (e) {
|
|
console.error(
|
|
`Failed to check whether plugin ${plugin.id} is disabled`,
|
|
e,
|
|
);
|
|
return false;
|
|
}
|
|
};
|
|
};
|
|
|
|
export const createRequirePluginFunction = (
|
|
failedPlugins: Array<[ActivatablePluginDetails, string]>,
|
|
reqFn: Function = global.electronRequire,
|
|
) => {
|
|
return (pluginDetails: ActivatablePluginDetails): PluginDefinition | null => {
|
|
try {
|
|
const pluginDefinition = requirePlugin(pluginDetails, reqFn);
|
|
if (
|
|
pluginDefinition &&
|
|
isDevicePluginDefinition(pluginDefinition) &&
|
|
pluginDefinition.details.pluginType !== 'device'
|
|
) {
|
|
console.warn(
|
|
`Package ${pluginDefinition.details.name} contains the device plugin "${pluginDefinition.title}" defined in a wrong format. Specify "pluginType" and "supportedDevices" properties and remove exported function "supportsDevice". See details at https://fbflipper.com/docs/extending/desktop-plugin-structure#creating-a-device-plugin.`,
|
|
);
|
|
}
|
|
return pluginDefinition;
|
|
} catch (e) {
|
|
failedPlugins.push([pluginDetails, e.message]);
|
|
console.error(`Plugin ${pluginDetails.id} failed to load`, e);
|
|
return null;
|
|
}
|
|
};
|
|
};
|
|
|
|
export const requirePlugin = (
|
|
pluginDetails: ActivatablePluginDetails,
|
|
reqFn: Function = global.electronRequire,
|
|
): PluginDefinition => {
|
|
reportUsage(
|
|
'plugin:load',
|
|
{
|
|
version: pluginDetails.version,
|
|
},
|
|
pluginDetails.id,
|
|
);
|
|
return tryCatchReportPluginFailures(
|
|
() => requirePluginInternal(pluginDetails, reqFn),
|
|
'plugin:load',
|
|
pluginDetails.id,
|
|
);
|
|
};
|
|
|
|
const requirePluginInternal = (
|
|
pluginDetails: ActivatablePluginDetails,
|
|
reqFn: Function = global.electronRequire,
|
|
): PluginDefinition => {
|
|
let plugin = pluginDetails.isBundled
|
|
? defaultPluginsIndex[pluginDetails.name]
|
|
: reqFn(pluginDetails.entry);
|
|
if (pluginDetails.flipperSDKVersion) {
|
|
// Sandy plugin
|
|
return new _SandyPluginDefinition(pluginDetails, plugin);
|
|
} else {
|
|
// classic plugin
|
|
if (plugin.default) {
|
|
plugin = plugin.default;
|
|
}
|
|
if (plugin.prototype === undefined) {
|
|
throw new Error(
|
|
`Plugin ${pluginDetails.name} is neither a class-based plugin nor a Sandy-based one.
|
|
Ensure that it exports either a FlipperPlugin class or has flipper-plugin declared as a peer-dependency and exports a plugin and Component.
|
|
See https://fbflipper.com/docs/extending/sandy-migration/ for more information.`,
|
|
);
|
|
} else if (!(plugin.prototype instanceof FlipperBasePlugin)) {
|
|
throw new Error(
|
|
`Plugin ${pluginDetails.name} is not a FlipperBasePlugin`,
|
|
);
|
|
}
|
|
|
|
if (plugin.id && pluginDetails.id !== plugin.id) {
|
|
console.error(
|
|
`Plugin name mismatch: Package '${pluginDetails.id}' exposed a plugin with id '${plugin.id}'. Please update the 'package.json' to match the exposed plugin id`,
|
|
);
|
|
}
|
|
plugin.id = plugin.id || pluginDetails.id;
|
|
plugin.packageName = pluginDetails.name;
|
|
plugin.details = pluginDetails;
|
|
|
|
// set values from package.json as static variables on class
|
|
Object.keys(pluginDetails).forEach((key) => {
|
|
if (key !== 'name' && key !== 'id') {
|
|
plugin[key] =
|
|
plugin[key] || pluginDetails[key as keyof ActivatablePluginDetails];
|
|
}
|
|
});
|
|
}
|
|
return plugin;
|
|
};
|
|
|
|
export function selectCompatibleMarketplaceVersions(
|
|
availablePlugins: MarketplacePluginDetails[],
|
|
): MarketplacePluginDetails[] {
|
|
const plugins: MarketplacePluginDetails[] = [];
|
|
for (const plugin of availablePlugins) {
|
|
if (!isPluginCompatible(plugin)) {
|
|
const compatibleVersion =
|
|
plugin.availableVersions?.find(isPluginCompatible) ??
|
|
plugin.availableVersions?.slice(-1).pop();
|
|
if (compatibleVersion) {
|
|
plugins.push({
|
|
...compatibleVersion,
|
|
availableVersions: plugin?.availableVersions,
|
|
});
|
|
} else {
|
|
plugins.push(plugin);
|
|
}
|
|
} else {
|
|
plugins.push(plugin);
|
|
}
|
|
}
|
|
return plugins;
|
|
}
|