From 3e728316997caea43fabf818f1f0def03555e403 Mon Sep 17 00:00:00 2001 From: Andrey Goncharov Date: Mon, 20 Jun 2022 12:18:40 -0700 Subject: [PATCH] Move most of plugin tests to flipper-frontend-core Summary: See D37139129 Reviewed By: passy Differential Revision: D37241829 fbshipit-source-id: d6bef24416e2b999d529fb6e275c64384c775c21 --- .../src/__tests__/plugins.node.tsx | 188 ++++++++++++++++++ desktop/flipper-frontend-core/src/plugins.tsx | 10 +- .../src/dispatcher/__tests__/plugins.node.tsx | 168 ++-------------- .../src/dispatcher/plugins.tsx | 152 +------------- .../flipper-ui-core/src/utils/pluginUtils.tsx | 15 +- 5 files changed, 228 insertions(+), 305 deletions(-) create mode 100644 desktop/flipper-frontend-core/src/__tests__/plugins.node.tsx diff --git a/desktop/flipper-frontend-core/src/__tests__/plugins.node.tsx b/desktop/flipper-frontend-core/src/__tests__/plugins.node.tsx new file mode 100644 index 000000000..731795888 --- /dev/null +++ b/desktop/flipper-frontend-core/src/__tests__/plugins.node.tsx @@ -0,0 +1,188 @@ +/** + * 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 { + getDynamicPlugins, + checkDisabled, + checkGK, + createRequirePluginFunction, + getLatestCompatibleVersionOfEachPlugin, +} from '../plugins'; +import {BundledPluginDetails, InstalledPluginDetails} from 'flipper-common'; +import {_SandyPluginDefinition} from 'flipper-plugin'; +import {getRenderHostInstance} from '../RenderHost'; + +let loadDynamicPluginsMock: jest.Mock; + +const sampleInstalledPluginDetails: InstalledPluginDetails = { + name: 'other Name', + version: '1.0.0', + specVersion: 2, + pluginType: 'client', + main: 'dist/bundle.js', + source: 'src/index.js', + id: 'Sample', + title: 'Sample', + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample', + entry: 'this/path/does not/exist', + isBundled: false, + isActivatable: true, +}; + +const sampleBundledPluginDetails: BundledPluginDetails = { + ...sampleInstalledPluginDetails, + id: 'SampleBundled', + isBundled: true, +}; + +beforeEach(() => { + loadDynamicPluginsMock = getRenderHostInstance().flipperServer.exec = + jest.fn(); + loadDynamicPluginsMock.mockResolvedValue([]); +}); + +test('getDynamicPlugins returns empty array on errors', async () => { + loadDynamicPluginsMock.mockRejectedValue(new Error('ooops')); + const res = await getDynamicPlugins(); + expect(res).toEqual([]); +}); + +test('checkDisabled', () => { + const disabledPlugin = 'pluginName'; + const hostConfig = getRenderHostInstance().serverConfig; + const orig = hostConfig.processConfig; + try { + hostConfig.processConfig = { + ...orig, + disabledPlugins: [disabledPlugin], + }; + const disabled = checkDisabled([]); + + expect( + disabled({ + ...sampleBundledPluginDetails, + name: 'other Name', + version: '1.0.0', + }), + ).toBeTruthy(); + expect( + disabled({ + ...sampleBundledPluginDetails, + name: disabledPlugin, + version: '1.0.0', + }), + ).toBeFalsy(); + } finally { + hostConfig.processConfig = orig; + } +}); + +test('checkGK for plugin without GK', () => { + expect( + checkGK([])({ + ...sampleBundledPluginDetails, + name: 'pluginID', + version: '1.0.0', + }), + ).toBeTruthy(); +}); + +test('checkGK for passing plugin', () => { + expect( + checkGK([])({ + ...sampleBundledPluginDetails, + name: 'pluginID', + gatekeeper: 'TEST_PASSING_GK', + version: '1.0.0', + }), + ).toBeTruthy(); +}); + +test('checkGK for failing plugin', () => { + const gatekeepedPlugins: InstalledPluginDetails[] = []; + const name = 'pluginID'; + const plugins = checkGK(gatekeepedPlugins)({ + ...sampleBundledPluginDetails, + name, + gatekeeper: 'TEST_FAILING_GK', + version: '1.0.0', + }); + + expect(plugins).toBeFalsy(); + expect(gatekeepedPlugins[0].name).toEqual(name); +}); + +test('requirePlugin returns null for invalid requires', async () => { + const requireFn = createRequirePluginFunction(() => { + throw new Error(); + }); + const plugin = await requireFn([])({ + ...sampleInstalledPluginDetails, + name: 'pluginID', + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample', + entry: 'this/path/does not/exist', + version: '1.0.0', + }); + + expect(plugin).toBeNull(); +}); + +test('newest version of each plugin is used', () => { + const bundledPlugins: BundledPluginDetails[] = [ + { + ...sampleBundledPluginDetails, + id: 'TestPlugin1', + name: 'flipper-plugin-test1', + version: '0.1.0', + }, + { + ...sampleBundledPluginDetails, + id: 'TestPlugin2', + name: 'flipper-plugin-test2', + version: '0.1.0-alpha.201', + }, + ]; + const installedPlugins: InstalledPluginDetails[] = [ + { + ...sampleInstalledPluginDetails, + id: 'TestPlugin2', + name: 'flipper-plugin-test2', + version: '0.1.0-alpha.21', + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-test2', + entry: './test/index.js', + }, + { + ...sampleInstalledPluginDetails, + id: 'TestPlugin1', + name: 'flipper-plugin-test1', + version: '0.10.0', + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-test1', + entry: './test/index.js', + }, + ]; + const filteredPlugins = getLatestCompatibleVersionOfEachPlugin( + [...bundledPlugins, ...installedPlugins], + '0.1.0', + ); + expect(filteredPlugins).toHaveLength(2); + expect(filteredPlugins).toContainEqual({ + ...sampleInstalledPluginDetails, + id: 'TestPlugin1', + name: 'flipper-plugin-test1', + version: '0.10.0', + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-test1', + entry: './test/index.js', + }); + expect(filteredPlugins).toContainEqual({ + ...sampleBundledPluginDetails, + id: 'TestPlugin2', + name: 'flipper-plugin-test2', + version: '0.1.0-alpha.201', + }); +}); diff --git a/desktop/flipper-frontend-core/src/plugins.tsx b/desktop/flipper-frontend-core/src/plugins.tsx index c6075ac48..84f63b12d 100644 --- a/desktop/flipper-frontend-core/src/plugins.tsx +++ b/desktop/flipper-frontend-core/src/plugins.tsx @@ -43,6 +43,10 @@ export abstract class AbstractPluginInitializer { return this._initialPlugins; } + get requirePlugin() { + return createRequirePluginFunction(this.requirePluginImpl.bind(this)); + } + protected async _init(): Promise<_SandyPluginDefinition[]> { this.loadDefaultPluginIndex(); this.loadMarketplacePlugins(); @@ -100,9 +104,7 @@ export abstract class AbstractPluginInitializer { } protected async loadPlugins(pluginsToLoad: ActivatablePluginDetails[]) { - const loader = createRequirePluginFunction( - this.requirePluginImpl.bind(this), - )(this.failedPlugins); + const loader = this.requirePlugin(this.failedPlugins); const initialPlugins: _SandyPluginDefinition[] = ( await pMap(pluginsToLoad, loader) ).filter(notNull); @@ -267,7 +269,7 @@ export const createRequirePluginFunction = }; }; -const wrapRequirePlugin = +export const wrapRequirePlugin = ( requirePluginImpl: ( pluginDetails: ActivatablePluginDetails, diff --git a/desktop/flipper-ui-core/src/dispatcher/__tests__/plugins.node.tsx b/desktop/flipper-ui-core/src/dispatcher/__tests__/plugins.node.tsx index d83ccb4b4..7ecb808f5 100644 --- a/desktop/flipper-ui-core/src/dispatcher/__tests__/plugins.node.tsx +++ b/desktop/flipper-ui-core/src/dispatcher/__tests__/plugins.node.tsx @@ -7,21 +7,17 @@ * @format */ -jest.mock('../../../../app/src/defaultPlugins'); -import dispatcher, { - getDynamicPlugins, - checkDisabled, - checkGK, - createRequirePluginFunction, - getLatestCompatibleVersionOfEachPlugin, -} from '../plugins'; -import {BundledPluginDetails, InstalledPluginDetails} from 'flipper-common'; +import dispatcher, {requirePluginInternal} from '../plugins'; +import {InstalledPluginDetails} from 'flipper-common'; import {createRootReducer, State} from '../../reducers/index'; import {getLogger} from 'flipper-common'; import configureStore from 'redux-mock-store'; import TestPlugin from './TestPlugin'; import {_SandyPluginDefinition} from 'flipper-plugin'; -import {getRenderHostInstance} from 'flipper-frontend-core'; +import { + getRenderHostInstance, + createRequirePluginFunction, +} from 'flipper-frontend-core'; import path from 'path'; let loadDynamicPluginsMock: jest.Mock; @@ -46,11 +42,8 @@ const sampleInstalledPluginDetails: InstalledPluginDetails = { isActivatable: true, }; -const sampleBundledPluginDetails: BundledPluginDetails = { - ...sampleInstalledPluginDetails, - id: 'SampleBundled', - isBundled: true, -}; +// bind to empty default plugin index so we try fetching from flipper-server every time +const requirePlugin = requirePluginInternal.bind({}, {}); beforeEach(() => { loadDynamicPluginsMock = getRenderHostInstance().flipperServer.exec = @@ -64,79 +57,8 @@ test('dispatcher dispatches REGISTER_PLUGINS', async () => { expect(actions.map((a) => a.type)).toContain('REGISTER_PLUGINS'); }); -test('getDynamicPlugins returns empty array on errors', async () => { - loadDynamicPluginsMock.mockRejectedValue(new Error('ooops')); - const res = await getDynamicPlugins(); - expect(res).toEqual([]); -}); - -test('checkDisabled', () => { - const disabledPlugin = 'pluginName'; - const hostConfig = getRenderHostInstance().serverConfig; - const orig = hostConfig.processConfig; - try { - hostConfig.processConfig = { - ...orig, - disabledPlugins: [disabledPlugin], - }; - const disabled = checkDisabled([]); - - expect( - disabled({ - ...sampleBundledPluginDetails, - name: 'other Name', - version: '1.0.0', - }), - ).toBeTruthy(); - expect( - disabled({ - ...sampleBundledPluginDetails, - name: disabledPlugin, - version: '1.0.0', - }), - ).toBeFalsy(); - } finally { - hostConfig.processConfig = orig; - } -}); - -test('checkGK for plugin without GK', () => { - expect( - checkGK([])({ - ...sampleBundledPluginDetails, - name: 'pluginID', - version: '1.0.0', - }), - ).toBeTruthy(); -}); - -test('checkGK for passing plugin', () => { - expect( - checkGK([])({ - ...sampleBundledPluginDetails, - name: 'pluginID', - gatekeeper: 'TEST_PASSING_GK', - version: '1.0.0', - }), - ).toBeTruthy(); -}); - -test('checkGK for failing plugin', () => { - const gatekeepedPlugins: InstalledPluginDetails[] = []; - const name = 'pluginID'; - const plugins = checkGK(gatekeepedPlugins)({ - ...sampleBundledPluginDetails, - name, - gatekeeper: 'TEST_FAILING_GK', - version: '1.0.0', - }); - - expect(plugins).toBeFalsy(); - expect(gatekeepedPlugins[0].name).toEqual(name); -}); - -test('requirePlugin returns null for invalid requires', async () => { - const requireFn = createRequirePluginFunction([]); +test('requirePluginInternal returns null for invalid requires', async () => { + const requireFn = createRequirePluginFunction(requirePlugin)([]); const plugin = await requireFn({ ...sampleInstalledPluginDetails, name: 'pluginID', @@ -148,9 +70,9 @@ test('requirePlugin returns null for invalid requires', async () => { expect(plugin).toBeNull(); }); -test('requirePlugin loads plugin', async () => { +test('requirePluginInternal loads plugin', async () => { const name = 'pluginID'; - const requireFn = createRequirePluginFunction([]); + const requireFn = createRequirePluginFunction(requirePlugin)([]); const plugin = await requireFn({ ...sampleInstalledPluginDetails, name, @@ -170,63 +92,9 @@ test('requirePlugin loads plugin', async () => { expect(plugin!.id).toBe(TestPlugin.id); }); -test('newest version of each plugin is used', () => { - const bundledPlugins: BundledPluginDetails[] = [ - { - ...sampleBundledPluginDetails, - id: 'TestPlugin1', - name: 'flipper-plugin-test1', - version: '0.1.0', - }, - { - ...sampleBundledPluginDetails, - id: 'TestPlugin2', - name: 'flipper-plugin-test2', - version: '0.1.0-alpha.201', - }, - ]; - const installedPlugins: InstalledPluginDetails[] = [ - { - ...sampleInstalledPluginDetails, - id: 'TestPlugin2', - name: 'flipper-plugin-test2', - version: '0.1.0-alpha.21', - dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-test2', - entry: './test/index.js', - }, - { - ...sampleInstalledPluginDetails, - id: 'TestPlugin1', - name: 'flipper-plugin-test1', - version: '0.10.0', - dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-test1', - entry: './test/index.js', - }, - ]; - const filteredPlugins = getLatestCompatibleVersionOfEachPlugin([ - ...bundledPlugins, - ...installedPlugins, - ]); - expect(filteredPlugins).toHaveLength(2); - expect(filteredPlugins).toContainEqual({ - ...sampleInstalledPluginDetails, - id: 'TestPlugin1', - name: 'flipper-plugin-test1', - version: '0.10.0', - dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-test1', - entry: './test/index.js', - }); - expect(filteredPlugins).toContainEqual({ - ...sampleBundledPluginDetails, - id: 'TestPlugin2', - name: 'flipper-plugin-test2', - version: '0.1.0-alpha.201', - }); -}); - -test('requirePlugin loads valid Sandy plugin', async () => { +test('requirePluginInternal loads valid Sandy plugin', async () => { const name = 'pluginID'; - const requireFn = createRequirePluginFunction([]); + const requireFn = createRequirePluginFunction(requirePlugin)([]); const plugin = (await requireFn({ ...sampleInstalledPluginDetails, name, @@ -261,10 +129,10 @@ test('requirePlugin loads valid Sandy plugin', async () => { expect(typeof plugin.asPluginModule().plugin).toBe('function'); }); -test('requirePlugin errors on invalid Sandy plugin', async () => { +test('requirePluginInternal errors on invalid Sandy plugin', async () => { const name = 'pluginID'; const failedPlugins: any[] = []; - const requireFn = createRequirePluginFunction(failedPlugins); + const requireFn = createRequirePluginFunction(requirePlugin)(failedPlugins); await requireFn({ ...sampleInstalledPluginDetails, name, @@ -279,9 +147,9 @@ test('requirePlugin errors on invalid Sandy plugin', async () => { ); }); -test('requirePlugin loads valid Sandy Device plugin', async () => { +test('requirePluginInternal loads valid Sandy Device plugin', async () => { const name = 'pluginID'; - const requireFn = createRequirePluginFunction([]); + const requireFn = createRequirePluginFunction(requirePlugin)([]); const plugin = (await requireFn({ ...sampleInstalledPluginDetails, pluginType: 'device', diff --git a/desktop/flipper-ui-core/src/dispatcher/plugins.tsx b/desktop/flipper-ui-core/src/dispatcher/plugins.tsx index d0665e960..8a7b84b5f 100644 --- a/desktop/flipper-ui-core/src/dispatcher/plugins.tsx +++ b/desktop/flipper-ui-core/src/dispatcher/plugins.tsx @@ -8,11 +8,7 @@ */ import type {Store} from '../reducers/index'; -import { - InstalledPluginDetails, - Logger, - tryCatchReportPluginFailuresAsync, -} from 'flipper-common'; +import {Logger} from 'flipper-common'; import {PluginDefinition} from '../plugin'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -30,23 +26,21 @@ import { pluginsInitialized, } from '../reducers/plugins'; import {FlipperBasePlugin} from '../plugin'; -import {ActivatablePluginDetails, ConcretePluginDetails} from 'flipper-common'; -import {reportUsage} from 'flipper-common'; +import {ActivatablePluginDetails} from 'flipper-common'; import * as FlipperPluginSDK from 'flipper-plugin'; import {_SandyPluginDefinition} from 'flipper-plugin'; 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'; - -import {isDevicePluginDefinition} from '../utils/pluginUtils'; import isPluginCompatible from '../utils/isPluginCompatible'; -import isPluginVersionMoreRecent from '../utils/isPluginVersionMoreRecent'; import {createSandyPluginWrapper} from '../utils/createSandyPluginWrapper'; import { AbstractPluginInitializer, getRenderHostInstance, setGlobalObject, + isSandyPlugin, + wrapRequirePlugin, } from 'flipper-frontend-core'; import * as deprecatedExports from '../deprecated-exports'; import {getAppVersion} from '../utils/info'; @@ -124,142 +118,10 @@ export default async (store: Store, _logger: Logger) => { await uiPluginInitializer.init(); }; -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()); -} +export const requirePlugin = (pluginDetails: ActivatablePluginDetails) => + wrapRequirePlugin(uiPluginInitializer!.requirePluginImpl)(pluginDetails); -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 = ( - failedPlugins: Array<[ActivatablePluginDetails, string]>, -) => { - return async ( - pluginDetails: ActivatablePluginDetails, - ): Promise => { - try { - const pluginDefinition = await requirePlugin(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 requirePlugin = ( - pluginDetails: ActivatablePluginDetails, -): Promise => { - reportUsage( - 'plugin:load', - { - version: pluginDetails.version, - }, - pluginDetails.id, - ); - return tryCatchReportPluginFailuresAsync( - () => uiPluginInitializer.requirePluginImpl(pluginDetails), - 'plugin:load', - pluginDetails.id, - ); -}; - -const isSandyPlugin = (pluginDetails: ActivatablePluginDetails) => { - return !!pluginDetails.flipperSDKVersion; -}; - -const requirePluginInternal = async ( +export const requirePluginInternal = async ( defaultPluginsIndex: any, pluginDetails: ActivatablePluginDetails, ): Promise => { diff --git a/desktop/flipper-ui-core/src/utils/pluginUtils.tsx b/desktop/flipper-ui-core/src/utils/pluginUtils.tsx index 1650ad7d1..56117e160 100644 --- a/desktop/flipper-ui-core/src/utils/pluginUtils.tsx +++ b/desktop/flipper-ui-core/src/utils/pluginUtils.tsx @@ -10,7 +10,10 @@ import type {PluginDefinition} from '../plugin'; import type {State, Store} from '../reducers'; import type {State as PluginsState} from '../reducers/plugins'; -import type {BaseDevice} from 'flipper-frontend-core'; +import { + BaseDevice, + getLatestCompatibleVersionOfEachPlugin, +} from 'flipper-frontend-core'; import type Client from '../Client'; import type { ActivatablePluginDetails, @@ -18,8 +21,8 @@ import type { DownloadablePluginDetails, PluginDetails, } from 'flipper-common'; -import {getLatestCompatibleVersionOfEachPlugin} from '../dispatcher/plugins'; import {getPluginKey} from './pluginKey'; +import {getAppVersion} from './info'; export type PluginLists = { devicePlugins: PluginDefinition[]; @@ -181,10 +184,10 @@ export function computePluginLists( } { const enabledDevicePluginsState = connections.enabledDevicePlugins; const enabledPluginsState = connections.enabledPlugins; - const uninstalledMarketplacePlugins = getLatestCompatibleVersionOfEachPlugin([ - ...plugins.bundledPlugins.values(), - ...plugins.marketplacePlugins, - ]).filter((p) => !plugins.loadedPlugins.has(p.id)); + const uninstalledMarketplacePlugins = getLatestCompatibleVersionOfEachPlugin( + [...plugins.bundledPlugins.values(), ...plugins.marketplacePlugins], + getAppVersion(), + ).filter((p) => !plugins.loadedPlugins.has(p.id)); const devicePlugins: PluginDefinition[] = [...plugins.devicePlugins.values()] .filter((p) => device?.supportsPlugin(p)) .filter((p) => enabledDevicePluginsState.has(p.id));