Files
flipper/desktop/flipper-frontend-core/src/plugins.tsx
Andrey Goncharov 650ff4bcfb Remove plugin bundling
Summary: Remove the notion of bundled plugins

Reviewed By: lblasa

Differential Revision: D39308888

fbshipit-source-id: aa88ddbf2801ad1da95f89e4c761259b697b0d66
2022-09-15 10:02:19 -07:00

264 lines
7.9 KiB
TypeScript

/**
* Copyright (c) Meta Platforms, Inc. and 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 {
InstalledPluginDetails,
tryCatchReportPluginFailuresAsync,
notNull,
} from 'flipper-common';
import {ActivatablePluginDetails, ConcretePluginDetails} from 'flipper-common';
import {reportUsage} from 'flipper-common';
import {_SandyPluginDefinition} from 'flipper-plugin-core';
import isPluginCompatible from './utils/isPluginCompatible';
import isPluginVersionMoreRecent from './utils/isPluginVersionMoreRecent';
import {getRenderHostInstance} from './RenderHost';
import pMap from 'p-map';
export abstract class AbstractPluginInitializer {
protected gatekeepedPlugins: Array<ActivatablePluginDetails> = [];
protected disabledPlugins: Array<ActivatablePluginDetails> = [];
protected failedPlugins: Array<[ActivatablePluginDetails, string]> = [];
protected loadedPlugins: Array<InstalledPluginDetails> = [];
protected _initialPlugins: _SandyPluginDefinition[] = [];
async init() {
this._initialPlugins = await this._init();
}
get initialPlugins(): ReadonlyArray<_SandyPluginDefinition> {
return this._initialPlugins;
}
get requirePlugin() {
return createRequirePluginFunction(this.requirePluginImpl.bind(this));
}
protected async _init(): Promise<_SandyPluginDefinition[]> {
this.loadMarketplacePlugins();
const uninstalledPluginNames = this.loadUninstalledPluginNames();
const allLocalVersions = await this.loadAllLocalVersions(
uninstalledPluginNames,
);
const pluginsToLoad = await this.filterAllLocalVersions(allLocalVersions);
const initialPlugins = await this.loadPlugins(pluginsToLoad);
return initialPlugins;
}
protected abstract getFlipperVersion(): Promise<string>;
protected abstract requirePluginImpl(
pluginDetails: ActivatablePluginDetails,
): Promise<_SandyPluginDefinition>;
protected loadMarketplacePlugins() {}
protected loadUninstalledPluginNames(): Set<string> {
return new Set();
}
protected async loadAllLocalVersions(
uninstalledPluginNames: Set<string>,
): Promise<InstalledPluginDetails[]> {
const allLocalVersions = [...(await getDynamicPlugins())].filter(
(p) => !uninstalledPluginNames.has(p.name),
);
return allLocalVersions;
}
protected async filterAllLocalVersions(
allLocalVersions: InstalledPluginDetails[],
): Promise<ActivatablePluginDetails[]> {
const flipperVersion = await this.getFlipperVersion();
const loadedPlugins = getLatestCompatibleVersionOfEachPlugin(
allLocalVersions,
flipperVersion,
);
this.loadedPlugins = loadedPlugins;
const pluginsToLoad = loadedPlugins
.map(reportVersion)
.filter(checkDisabled(this.disabledPlugins))
.filter(checkGK(this.gatekeepedPlugins));
return pluginsToLoad;
}
protected async loadPlugins(pluginsToLoad: ActivatablePluginDetails[]) {
const loader = this.requirePlugin(this.failedPlugins);
const initialPlugins: _SandyPluginDefinition[] = (
await pMap(pluginsToLoad, loader)
).filter(notNull);
return initialPlugins;
}
}
export function isDevicePluginDefinition(
definition: _SandyPluginDefinition,
): boolean {
return definition.isDevicePlugin;
}
export function reportVersion(pluginDetails: ActivatablePluginDetails) {
reportUsage(
'plugin:version',
{
version: pluginDetails.version,
},
pluginDetails.id,
);
return pluginDetails;
}
export function getLatestCompatibleVersionOfEachPlugin<
T extends ConcretePluginDetails,
>(plugins: T[], flipperVersion: string): T[] {
const latestCompatibleVersions: Map<string, T> = new Map();
for (const plugin of plugins) {
if (isPluginCompatible(plugin, flipperVersion)) {
const loadedVersion = latestCompatibleVersions.get(plugin.id);
if (
!loadedVersion ||
isPluginVersionMoreRecent(plugin, loadedVersion, flipperVersion)
) {
latestCompatibleVersions.set(plugin.id, plugin);
}
}
}
return Array.from(latestCompatibleVersions.values());
}
export async function getDynamicPlugins(): Promise<InstalledPluginDetails[]> {
try {
return await getRenderHostInstance().flipperServer!.exec(
'plugins-load-dynamic-plugins',
);
} 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 = getRenderHostInstance().GK(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>,
) => {
const config = getRenderHostInstance().serverConfig;
let enabledList: Set<string> | null = null;
let disabledList: Set<string> = new Set();
try {
if (config.env.FLIPPER_ENABLED_PLUGINS) {
enabledList = new Set<string>(
config.env.FLIPPER_ENABLED_PLUGINS.split(','),
);
}
disabledList = new Set(config.processConfig.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 =
(
requirePluginImpl: (
pluginDetails: ActivatablePluginDetails,
) => Promise<_SandyPluginDefinition>,
) =>
(failedPlugins: Array<[ActivatablePluginDetails, string]>) => {
return async (
pluginDetails: ActivatablePluginDetails,
): Promise<_SandyPluginDefinition | null> => {
try {
const requirePluginImplWrapped = wrapRequirePlugin(requirePluginImpl);
const pluginDefinition = await requirePluginImplWrapped(pluginDetails);
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 wrapRequirePlugin =
(
requirePluginImpl: (
pluginDetails: ActivatablePluginDetails,
) => Promise<_SandyPluginDefinition>,
) =>
(
pluginDetails: ActivatablePluginDetails,
): Promise<_SandyPluginDefinition> => {
reportUsage(
'plugin:load',
{
version: pluginDetails.version,
},
pluginDetails.id,
);
return tryCatchReportPluginFailuresAsync(
() => requirePluginImpl(pluginDetails),
'plugin:load',
pluginDetails.id,
);
};
export const isSandyPlugin = (pluginDetails: ActivatablePluginDetails) => {
return !!pluginDetails.flipperSDKVersion;
};