/** * 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 'flipper-common'; import {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, pluginsInitialized, } from '../reducers/plugins'; import {FlipperBasePlugin} from '../plugin'; import fs from 'fs-extra'; import path from 'path'; import {notNull} from '../utils/typeUtils'; import { ActivatablePluginDetails, BundledPluginDetails, ConcretePluginDetails, } from 'flipper-plugin-lib'; import {tryCatchReportPluginFailures, reportUsage} from 'flipper-common'; 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'; import {isDevicePluginDefinition} from '../utils/pluginUtils'; import isPluginCompatible from '../utils/isPluginCompatible'; import isPluginVersionMoreRecent from '../utils/isPluginVersionMoreRecent'; import {getStaticPath} from '../utils/pathUtils'; import {createSandyPluginWrapper} from '../utils/createSandyPluginWrapper'; import {getRenderHostInstance} from '../RenderHost'; 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('../deprecated-exports'); 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 = []; const disabledPlugins: Array = []; const failedPlugins: Array<[ActivatablePluginDetails, string]> = []; defaultPluginsIndex = getRenderHostInstance().loadDefaultPlugins(); const marketplacePlugins = selectCompatibleMarketplaceVersions( store.getState().plugins.marketplacePlugins, ); store.dispatch(registerMarketplacePlugins(marketplacePlugins)); const uninstalledPluginNames = store.getState().plugins.uninstalledPluginNames; const bundledPlugins = await getBundledPlugins(); const allLocalVersions = [ ...bundledPlugins, ...(await getDynamicPlugins()), ].filter((p) => !uninstalledPluginNames.has(p.name)); const loadedPlugins = getLatestCompatibleVersionOfEachPlugin(allLocalVersions); const initialPlugins: PluginDefinition[] = loadedPlugins .map(reportVersion) .filter(checkDisabled(disabledPlugins)) .filter(checkGK(gatekeepedPlugins)) .map(createRequirePluginFunction(failedPlugins)) .filter(notNull); const classicPlugins = initialPlugins.filter( (p) => !isSandyPlugin(p.details), ); if (process.env.NODE_ENV !== 'test' && classicPlugins.length) { console.warn( `${ classicPlugins.length } plugin(s) were loaded in legacy mode. Please visit https://fbflipper.com/docs/extending/sandy-migration to learn how to migrate these plugins to the new Sandy architecture: \n${classicPlugins .map((p) => `${p.title} (id: ${p.id})`) .sort() .join('\n')}`, ); } 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(pluginsInitialized()); }; function reportVersion(pluginDetails: ActivatablePluginDetails) { reportUsage( 'plugin:version', { version: pluginDetails.version, }, pluginDetails.id, ); return pluginDetails; } export function getLatestCompatibleVersionOfEachPlugin< T extends ConcretePluginDetails, >(plugins: T[]): T[] { const latestCompatibleVersions: Map = new Map(); for (const plugin of plugins) { if (isPluginCompatible(plugin)) { const loadedVersion = latestCompatibleVersions.get(plugin.id); if (!loadedVersion || isPluginVersionMoreRecent(plugin, loadedVersion)) { latestCompatibleVersions.set(plugin.id, plugin); } } } return Array.from(latestCompatibleVersions.values()); } async function getBundledPlugins(): Promise> { if (process.env.NODE_ENV === 'test') { return []; } // defaultPlugins that are included in the Flipper distributive. // List of default bundled plugins is written at build time to defaultPlugins/bundled.json. const pluginPath = getStaticPath( path.join('defaultPlugins', 'bundled.json'), {asarUnpacked: true}, ); let bundledPlugins: Array = []; try { bundledPlugins = await fs.readJson(pluginPath); } catch (e) { console.error('Failed to load list of 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) => (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 = 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 = ( 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 isSandyPlugin = (pluginDetails: ActivatablePluginDetails) => { return !!pluginDetails.flipperSDKVersion; }; const requirePluginInternal = ( pluginDetails: ActivatablePluginDetails, reqFn: Function = global.electronRequire, ): PluginDefinition => { let plugin = pluginDetails.isBundled ? defaultPluginsIndex[pluginDetails.name] : reqFn(pluginDetails.entry); if (isSandyPlugin(pluginDetails)) { // 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; return createSandyPluginFromClassicPlugin(pluginDetails, plugin); } }; export function createSandyPluginFromClassicPlugin( pluginDetails: ActivatablePluginDetails, plugin: any, ) { pluginDetails.id = plugin.id; // for backward compatibility, see above check! return new _SandyPluginDefinition( pluginDetails, createSandyPluginWrapper(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; }