Add plugin initializer to flipper-frontend-core
Summary: Extract plugin initialization Reviewed By: mweststrate Differential Revision: D36129748 fbshipit-source-id: db56e24388f7ed858d2173b67affa37de842a9b1
This commit is contained in:
committed by
Facebook GitHub Bot
parent
ea6c293726
commit
7cae3af507
290
desktop/flipper-frontend-core/src/plugins.tsx
Normal file
290
desktop/flipper-frontend-core/src/plugins.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* 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,
|
||||
BundledPluginDetails,
|
||||
ConcretePluginDetails,
|
||||
} from 'flipper-common';
|
||||
import {reportUsage} from 'flipper-common';
|
||||
import {_SandyPluginDefinition} from 'flipper-plugin';
|
||||
import isPluginCompatible from './utils/isPluginCompatible';
|
||||
import isPluginVersionMoreRecent from './utils/isPluginVersionMoreRecent';
|
||||
import {getRenderHostInstance} from './RenderHost';
|
||||
import pMap from 'p-map';
|
||||
|
||||
export abstract class AbstractPluginInitializer {
|
||||
protected defaultPluginsIndex: any = null;
|
||||
protected gatekeepedPlugins: Array<ActivatablePluginDetails> = [];
|
||||
protected disabledPlugins: Array<ActivatablePluginDetails> = [];
|
||||
protected failedPlugins: Array<[ActivatablePluginDetails, string]> = [];
|
||||
|
||||
protected _loadedPlugins: _SandyPluginDefinition[] = [];
|
||||
|
||||
async init() {
|
||||
this._loadedPlugins = await this._init();
|
||||
}
|
||||
|
||||
get loadedPlugins(): ReadonlyArray<_SandyPluginDefinition> {
|
||||
return this._loadedPlugins;
|
||||
}
|
||||
|
||||
protected async _init(): Promise<_SandyPluginDefinition[]> {
|
||||
this.loadDefaultPluginIndex();
|
||||
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 loadDefaultPluginIndex() {
|
||||
this.defaultPluginsIndex = getRenderHostInstance().loadDefaultPlugins();
|
||||
}
|
||||
protected loadMarketplacePlugins() {}
|
||||
protected loadUninstalledPluginNames(): Set<string> {
|
||||
return new Set();
|
||||
}
|
||||
protected async loadAllLocalVersions(
|
||||
uninstalledPluginNames: Set<string>,
|
||||
): Promise<(BundledPluginDetails | InstalledPluginDetails)[]> {
|
||||
const bundledPlugins = await getBundledPlugins();
|
||||
|
||||
const allLocalVersions = [
|
||||
...bundledPlugins,
|
||||
...(await getDynamicPlugins()),
|
||||
].filter((p) => !uninstalledPluginNames.has(p.name));
|
||||
|
||||
return allLocalVersions;
|
||||
}
|
||||
protected async filterAllLocalVersions(
|
||||
allLocalVersions: (BundledPluginDetails | InstalledPluginDetails)[],
|
||||
): Promise<ActivatablePluginDetails[]> {
|
||||
const flipperVersion = await this.getFlipperVersion();
|
||||
const loadedPlugins = getLatestCompatibleVersionOfEachPlugin(
|
||||
allLocalVersions,
|
||||
flipperVersion,
|
||||
);
|
||||
|
||||
const pluginsToLoad = loadedPlugins
|
||||
.map(reportVersion)
|
||||
.filter(checkDisabled(this.disabledPlugins))
|
||||
.filter(checkGK(this.gatekeepedPlugins));
|
||||
|
||||
return pluginsToLoad;
|
||||
}
|
||||
|
||||
protected async loadPlugins(pluginsToLoad: ActivatablePluginDetails[]) {
|
||||
const loader = createRequirePluginFunction(
|
||||
this.requirePluginImpl.bind(this),
|
||||
)(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 getBundledPlugins(): Promise<
|
||||
Array<BundledPluginDetails>
|
||||
> {
|
||||
if (getRenderHostInstance().serverConfig.env.NODE_ENV === 'test') {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
// defaultPlugins that are included in the Flipper distributive.
|
||||
// List of default bundled plugins is written at build time to defaultPlugins/bundled.json.
|
||||
return await getRenderHostInstance().flipperServer!.exec(
|
||||
'plugins-get-bundled-plugins',
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to load list of bundled plugins', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user