/** * 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 = []; protected disabledPlugins: Array = []; protected failedPlugins: Array<[ActivatablePluginDetails, string]> = []; protected loadedPlugins: Array = []; 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; protected abstract requirePluginImpl( pluginDetails: ActivatablePluginDetails, ): Promise<_SandyPluginDefinition>; protected loadMarketplacePlugins() {} protected loadUninstalledPluginNames(): Set { return new Set(); } protected async loadAllLocalVersions( uninstalledPluginNames: Set, ): Promise { const allLocalVersions = [...(await getDynamicPlugins())].filter( (p) => !uninstalledPluginNames.has(p.name), ); return allLocalVersions; } protected async filterAllLocalVersions( allLocalVersions: InstalledPluginDetails[], ): Promise { 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 = 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 { 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) => (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, ) => { const config = getRenderHostInstance().serverConfig; let enabledList: Set | null = null; let disabledList: Set = new Set(); try { if (config.env.FLIPPER_ENABLED_PLUGINS) { enabledList = new Set( 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; };