Move most of plugin tests to flipper-frontend-core

Summary: See D37139129

Reviewed By: passy

Differential Revision: D37241829

fbshipit-source-id: d6bef24416e2b999d529fb6e275c64384c775c21
This commit is contained in:
Andrey Goncharov
2022-06-20 12:18:40 -07:00
committed by Facebook GitHub Bot
parent f4fc07ffd2
commit 3e72831699
5 changed files with 228 additions and 305 deletions

View File

@@ -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',
});
});

View File

@@ -43,6 +43,10 @@ export abstract class AbstractPluginInitializer {
return this._initialPlugins; return this._initialPlugins;
} }
get requirePlugin() {
return createRequirePluginFunction(this.requirePluginImpl.bind(this));
}
protected async _init(): Promise<_SandyPluginDefinition[]> { protected async _init(): Promise<_SandyPluginDefinition[]> {
this.loadDefaultPluginIndex(); this.loadDefaultPluginIndex();
this.loadMarketplacePlugins(); this.loadMarketplacePlugins();
@@ -100,9 +104,7 @@ export abstract class AbstractPluginInitializer {
} }
protected async loadPlugins(pluginsToLoad: ActivatablePluginDetails[]) { protected async loadPlugins(pluginsToLoad: ActivatablePluginDetails[]) {
const loader = createRequirePluginFunction( const loader = this.requirePlugin(this.failedPlugins);
this.requirePluginImpl.bind(this),
)(this.failedPlugins);
const initialPlugins: _SandyPluginDefinition[] = ( const initialPlugins: _SandyPluginDefinition[] = (
await pMap(pluginsToLoad, loader) await pMap(pluginsToLoad, loader)
).filter(notNull); ).filter(notNull);
@@ -267,7 +269,7 @@ export const createRequirePluginFunction =
}; };
}; };
const wrapRequirePlugin = export const wrapRequirePlugin =
( (
requirePluginImpl: ( requirePluginImpl: (
pluginDetails: ActivatablePluginDetails, pluginDetails: ActivatablePluginDetails,

View File

@@ -7,21 +7,17 @@
* @format * @format
*/ */
jest.mock('../../../../app/src/defaultPlugins'); import dispatcher, {requirePluginInternal} from '../plugins';
import dispatcher, { import {InstalledPluginDetails} from 'flipper-common';
getDynamicPlugins,
checkDisabled,
checkGK,
createRequirePluginFunction,
getLatestCompatibleVersionOfEachPlugin,
} from '../plugins';
import {BundledPluginDetails, InstalledPluginDetails} from 'flipper-common';
import {createRootReducer, State} from '../../reducers/index'; import {createRootReducer, State} from '../../reducers/index';
import {getLogger} from 'flipper-common'; import {getLogger} from 'flipper-common';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import TestPlugin from './TestPlugin'; import TestPlugin from './TestPlugin';
import {_SandyPluginDefinition} from 'flipper-plugin'; import {_SandyPluginDefinition} from 'flipper-plugin';
import {getRenderHostInstance} from 'flipper-frontend-core'; import {
getRenderHostInstance,
createRequirePluginFunction,
} from 'flipper-frontend-core';
import path from 'path'; import path from 'path';
let loadDynamicPluginsMock: jest.Mock; let loadDynamicPluginsMock: jest.Mock;
@@ -46,11 +42,8 @@ const sampleInstalledPluginDetails: InstalledPluginDetails = {
isActivatable: true, isActivatable: true,
}; };
const sampleBundledPluginDetails: BundledPluginDetails = { // bind to empty default plugin index so we try fetching from flipper-server every time
...sampleInstalledPluginDetails, const requirePlugin = requirePluginInternal.bind({}, {});
id: 'SampleBundled',
isBundled: true,
};
beforeEach(() => { beforeEach(() => {
loadDynamicPluginsMock = getRenderHostInstance().flipperServer.exec = loadDynamicPluginsMock = getRenderHostInstance().flipperServer.exec =
@@ -64,79 +57,8 @@ test('dispatcher dispatches REGISTER_PLUGINS', async () => {
expect(actions.map((a) => a.type)).toContain('REGISTER_PLUGINS'); expect(actions.map((a) => a.type)).toContain('REGISTER_PLUGINS');
}); });
test('getDynamicPlugins returns empty array on errors', async () => { test('requirePluginInternal returns null for invalid requires', async () => {
loadDynamicPluginsMock.mockRejectedValue(new Error('ooops')); const requireFn = createRequirePluginFunction(requirePlugin)([]);
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([]);
const plugin = await requireFn({ const plugin = await requireFn({
...sampleInstalledPluginDetails, ...sampleInstalledPluginDetails,
name: 'pluginID', name: 'pluginID',
@@ -148,9 +70,9 @@ test('requirePlugin returns null for invalid requires', async () => {
expect(plugin).toBeNull(); expect(plugin).toBeNull();
}); });
test('requirePlugin loads plugin', async () => { test('requirePluginInternal loads plugin', async () => {
const name = 'pluginID'; const name = 'pluginID';
const requireFn = createRequirePluginFunction([]); const requireFn = createRequirePluginFunction(requirePlugin)([]);
const plugin = await requireFn({ const plugin = await requireFn({
...sampleInstalledPluginDetails, ...sampleInstalledPluginDetails,
name, name,
@@ -170,63 +92,9 @@ test('requirePlugin loads plugin', async () => {
expect(plugin!.id).toBe(TestPlugin.id); expect(plugin!.id).toBe(TestPlugin.id);
}); });
test('newest version of each plugin is used', () => { test('requirePluginInternal loads valid Sandy plugin', async () => {
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 () => {
const name = 'pluginID'; const name = 'pluginID';
const requireFn = createRequirePluginFunction([]); const requireFn = createRequirePluginFunction(requirePlugin)([]);
const plugin = (await requireFn({ const plugin = (await requireFn({
...sampleInstalledPluginDetails, ...sampleInstalledPluginDetails,
name, name,
@@ -261,10 +129,10 @@ test('requirePlugin loads valid Sandy plugin', async () => {
expect(typeof plugin.asPluginModule().plugin).toBe('function'); 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 name = 'pluginID';
const failedPlugins: any[] = []; const failedPlugins: any[] = [];
const requireFn = createRequirePluginFunction(failedPlugins); const requireFn = createRequirePluginFunction(requirePlugin)(failedPlugins);
await requireFn({ await requireFn({
...sampleInstalledPluginDetails, ...sampleInstalledPluginDetails,
name, 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 name = 'pluginID';
const requireFn = createRequirePluginFunction([]); const requireFn = createRequirePluginFunction(requirePlugin)([]);
const plugin = (await requireFn({ const plugin = (await requireFn({
...sampleInstalledPluginDetails, ...sampleInstalledPluginDetails,
pluginType: 'device', pluginType: 'device',

View File

@@ -8,11 +8,7 @@
*/ */
import type {Store} from '../reducers/index'; import type {Store} from '../reducers/index';
import { import {Logger} from 'flipper-common';
InstalledPluginDetails,
Logger,
tryCatchReportPluginFailuresAsync,
} from 'flipper-common';
import {PluginDefinition} from '../plugin'; import {PluginDefinition} from '../plugin';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
@@ -30,23 +26,21 @@ import {
pluginsInitialized, pluginsInitialized,
} from '../reducers/plugins'; } from '../reducers/plugins';
import {FlipperBasePlugin} from '../plugin'; import {FlipperBasePlugin} from '../plugin';
import {ActivatablePluginDetails, ConcretePluginDetails} from 'flipper-common'; import {ActivatablePluginDetails} from 'flipper-common';
import {reportUsage} from 'flipper-common';
import * as FlipperPluginSDK from 'flipper-plugin'; import * as FlipperPluginSDK from 'flipper-plugin';
import {_SandyPluginDefinition} from 'flipper-plugin'; import {_SandyPluginDefinition} from 'flipper-plugin';
import * as Immer from 'immer'; import * as Immer from 'immer';
import * as antd from 'antd'; import * as antd from 'antd';
import * as emotion_styled from '@emotion/styled'; import * as emotion_styled from '@emotion/styled';
import * as antdesign_icons from '@ant-design/icons'; import * as antdesign_icons from '@ant-design/icons';
import {isDevicePluginDefinition} from '../utils/pluginUtils';
import isPluginCompatible from '../utils/isPluginCompatible'; import isPluginCompatible from '../utils/isPluginCompatible';
import isPluginVersionMoreRecent from '../utils/isPluginVersionMoreRecent';
import {createSandyPluginWrapper} from '../utils/createSandyPluginWrapper'; import {createSandyPluginWrapper} from '../utils/createSandyPluginWrapper';
import { import {
AbstractPluginInitializer, AbstractPluginInitializer,
getRenderHostInstance, getRenderHostInstance,
setGlobalObject, setGlobalObject,
isSandyPlugin,
wrapRequirePlugin,
} from 'flipper-frontend-core'; } from 'flipper-frontend-core';
import * as deprecatedExports from '../deprecated-exports'; import * as deprecatedExports from '../deprecated-exports';
import {getAppVersion} from '../utils/info'; import {getAppVersion} from '../utils/info';
@@ -124,142 +118,10 @@ export default async (store: Store, _logger: Logger) => {
await uiPluginInitializer.init(); await uiPluginInitializer.init();
}; };
export function getLatestCompatibleVersionOfEachPlugin< export const requirePlugin = (pluginDetails: ActivatablePluginDetails) =>
T extends ConcretePluginDetails, wrapRequirePlugin(uiPluginInitializer!.requirePluginImpl)(pluginDetails);
>(plugins: T[]): T[] {
const latestCompatibleVersions: Map<string, T> = 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 async function getDynamicPlugins(): Promise<InstalledPluginDetails[]> { export const requirePluginInternal = async (
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 = (
failedPlugins: Array<[ActivatablePluginDetails, string]>,
) => {
return async (
pluginDetails: ActivatablePluginDetails,
): Promise<PluginDefinition | null> => {
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<PluginDefinition> => {
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 (
defaultPluginsIndex: any, defaultPluginsIndex: any,
pluginDetails: ActivatablePluginDetails, pluginDetails: ActivatablePluginDetails,
): Promise<PluginDefinition> => { ): Promise<PluginDefinition> => {

View File

@@ -10,7 +10,10 @@
import type {PluginDefinition} from '../plugin'; import type {PluginDefinition} from '../plugin';
import type {State, Store} from '../reducers'; import type {State, Store} from '../reducers';
import type {State as PluginsState} from '../reducers/plugins'; 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 Client from '../Client';
import type { import type {
ActivatablePluginDetails, ActivatablePluginDetails,
@@ -18,8 +21,8 @@ import type {
DownloadablePluginDetails, DownloadablePluginDetails,
PluginDetails, PluginDetails,
} from 'flipper-common'; } from 'flipper-common';
import {getLatestCompatibleVersionOfEachPlugin} from '../dispatcher/plugins';
import {getPluginKey} from './pluginKey'; import {getPluginKey} from './pluginKey';
import {getAppVersion} from './info';
export type PluginLists = { export type PluginLists = {
devicePlugins: PluginDefinition[]; devicePlugins: PluginDefinition[];
@@ -181,10 +184,10 @@ export function computePluginLists(
} { } {
const enabledDevicePluginsState = connections.enabledDevicePlugins; const enabledDevicePluginsState = connections.enabledDevicePlugins;
const enabledPluginsState = connections.enabledPlugins; const enabledPluginsState = connections.enabledPlugins;
const uninstalledMarketplacePlugins = getLatestCompatibleVersionOfEachPlugin([ const uninstalledMarketplacePlugins = getLatestCompatibleVersionOfEachPlugin(
...plugins.bundledPlugins.values(), [...plugins.bundledPlugins.values(), ...plugins.marketplacePlugins],
...plugins.marketplacePlugins, getAppVersion(),
]).filter((p) => !plugins.loadedPlugins.has(p.id)); ).filter((p) => !plugins.loadedPlugins.has(p.id));
const devicePlugins: PluginDefinition[] = [...plugins.devicePlugins.values()] const devicePlugins: PluginDefinition[] = [...plugins.devicePlugins.values()]
.filter((p) => device?.supportsPlugin(p)) .filter((p) => device?.supportsPlugin(p))
.filter((p) => enabledDevicePluginsState.has(p.id)); .filter((p) => enabledDevicePluginsState.has(p.id));