Make plugin loading async
Summary: This diff makes plugin loading async, which we'd need in a browser env (either because we'd use `import()` or we need to fetch the source and than eval it), and deals with all the fallout of that Reviewed By: timur-valiev Differential Revision: D32669995 fbshipit-source-id: 73babf38a6757c451b8200c3b320409f127b8b5b
This commit is contained in:
committed by
Facebook GitHub Bot
parent
64747dc417
commit
de59bbedd2
@@ -23,6 +23,7 @@ import MockFlipper from '../../test-utils/MockFlipper';
|
||||
import Client from '../../Client';
|
||||
import React from 'react';
|
||||
import BaseDevice from '../../devices/BaseDevice';
|
||||
import {awaitPluginCommandQueueEmpty} from '../pluginManager';
|
||||
|
||||
const pluginDetails1 = TestUtils.createMockPluginDetails({
|
||||
id: 'plugin1',
|
||||
@@ -71,7 +72,7 @@ let mockDevice: BaseDevice;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockedRequirePlugin.mockImplementation(
|
||||
(details) =>
|
||||
async (details) =>
|
||||
(details === pluginDetails1
|
||||
? pluginDefinition1
|
||||
: details === pluginDetails2
|
||||
@@ -99,6 +100,8 @@ test('load plugin when no other version loaded', async () => {
|
||||
mockFlipper.dispatch(
|
||||
loadPlugin({plugin: pluginDetails1, enable: false, notifyIfFailed: false}),
|
||||
);
|
||||
|
||||
await awaitPluginCommandQueueEmpty(mockFlipper.store);
|
||||
expect(mockFlipper.getState().plugins.clientPlugins.get('plugin1')).toBe(
|
||||
pluginDefinition1,
|
||||
);
|
||||
@@ -119,6 +122,8 @@ test('load plugin when other version loaded', async () => {
|
||||
notifyIfFailed: false,
|
||||
}),
|
||||
);
|
||||
|
||||
await awaitPluginCommandQueueEmpty(mockFlipper.store);
|
||||
expect(mockFlipper.getState().plugins.clientPlugins.get('plugin1')).toBe(
|
||||
pluginDefinition1V2,
|
||||
);
|
||||
@@ -132,6 +137,8 @@ test('load and enable Sandy plugin', async () => {
|
||||
mockFlipper.dispatch(
|
||||
loadPlugin({plugin: pluginDetails1, enable: true, notifyIfFailed: false}),
|
||||
);
|
||||
|
||||
await awaitPluginCommandQueueEmpty(mockFlipper.store);
|
||||
expect(mockFlipper.getState().plugins.clientPlugins.get('plugin1')).toBe(
|
||||
pluginDefinition1,
|
||||
);
|
||||
@@ -146,6 +153,8 @@ test('uninstall plugin', async () => {
|
||||
loadPlugin({plugin: pluginDetails1, enable: true, notifyIfFailed: false}),
|
||||
);
|
||||
mockFlipper.dispatch(uninstallPlugin({plugin: pluginDefinition1}));
|
||||
|
||||
await awaitPluginCommandQueueEmpty(mockFlipper.store);
|
||||
expect(
|
||||
mockFlipper.getState().plugins.clientPlugins.has('plugin1'),
|
||||
).toBeFalsy();
|
||||
@@ -167,11 +176,13 @@ test('uninstall bundled plugin', async () => {
|
||||
version: '0.43.0',
|
||||
});
|
||||
const pluginDefinition = new SandyPluginDefinition(pluginDetails, TestPlugin);
|
||||
mockedRequirePlugin.mockReturnValue(pluginDefinition);
|
||||
mockedRequirePlugin.mockReturnValue(Promise.resolve(pluginDefinition));
|
||||
mockFlipper.dispatch(
|
||||
loadPlugin({plugin: pluginDetails, enable: true, notifyIfFailed: false}),
|
||||
);
|
||||
mockFlipper.dispatch(uninstallPlugin({plugin: pluginDefinition}));
|
||||
|
||||
await awaitPluginCommandQueueEmpty(mockFlipper.store);
|
||||
expect(
|
||||
mockFlipper.getState().plugins.clientPlugins.has('bundled-plugin'),
|
||||
).toBeFalsy();
|
||||
@@ -196,6 +207,8 @@ test('star plugin', async () => {
|
||||
selectedApp: mockClient.query.app,
|
||||
}),
|
||||
);
|
||||
|
||||
await awaitPluginCommandQueueEmpty(mockFlipper.store);
|
||||
expect(
|
||||
mockFlipper.getState().connections.enabledPlugins[mockClient.query.app],
|
||||
).toContain('plugin1');
|
||||
@@ -218,6 +231,8 @@ test('disable plugin', async () => {
|
||||
selectedApp: mockClient.query.app,
|
||||
}),
|
||||
);
|
||||
|
||||
await awaitPluginCommandQueueEmpty(mockFlipper.store);
|
||||
expect(
|
||||
mockFlipper.getState().connections.enabledPlugins[mockClient.query.app],
|
||||
).not.toContain('plugin1');
|
||||
@@ -237,6 +252,8 @@ test('star device plugin', async () => {
|
||||
plugin: devicePluginDefinition,
|
||||
}),
|
||||
);
|
||||
|
||||
await awaitPluginCommandQueueEmpty(mockFlipper.store);
|
||||
expect(
|
||||
mockFlipper.getState().connections.enabledDevicePlugins.has('device'),
|
||||
).toBeTruthy();
|
||||
@@ -261,6 +278,8 @@ test('disable device plugin', async () => {
|
||||
plugin: devicePluginDefinition,
|
||||
}),
|
||||
);
|
||||
|
||||
await awaitPluginCommandQueueEmpty(mockFlipper.store);
|
||||
expect(
|
||||
mockFlipper.getState().connections.enabledDevicePlugins.has('device'),
|
||||
).toBeFalsy();
|
||||
|
||||
@@ -135,9 +135,9 @@ test('checkGK for failing plugin', () => {
|
||||
expect(gatekeepedPlugins[0].name).toEqual(name);
|
||||
});
|
||||
|
||||
test('requirePlugin returns null for invalid requires', () => {
|
||||
const requireFn = createRequirePluginFunction([], require);
|
||||
const plugin = requireFn({
|
||||
test('requirePlugin returns null for invalid requires', async () => {
|
||||
const requireFn = createRequirePluginFunction([]);
|
||||
const plugin = await requireFn({
|
||||
...sampleInstalledPluginDetails,
|
||||
name: 'pluginID',
|
||||
dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample',
|
||||
@@ -148,10 +148,10 @@ test('requirePlugin returns null for invalid requires', () => {
|
||||
expect(plugin).toBeNull();
|
||||
});
|
||||
|
||||
test('requirePlugin loads plugin', () => {
|
||||
test('requirePlugin loads plugin', async () => {
|
||||
const name = 'pluginID';
|
||||
const requireFn = createRequirePluginFunction([], require);
|
||||
const plugin = requireFn({
|
||||
const requireFn = createRequirePluginFunction([]);
|
||||
const plugin = await requireFn({
|
||||
...sampleInstalledPluginDetails,
|
||||
name,
|
||||
dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample',
|
||||
@@ -224,10 +224,10 @@ test('newest version of each plugin is used', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('requirePlugin loads valid Sandy plugin', () => {
|
||||
test('requirePlugin loads valid Sandy plugin', async () => {
|
||||
const name = 'pluginID';
|
||||
const requireFn = createRequirePluginFunction([], require);
|
||||
const plugin = requireFn({
|
||||
const requireFn = createRequirePluginFunction([]);
|
||||
const plugin = (await requireFn({
|
||||
...sampleInstalledPluginDetails,
|
||||
name,
|
||||
dir: path.join(
|
||||
@@ -240,7 +240,7 @@ test('requirePlugin loads valid Sandy plugin', () => {
|
||||
),
|
||||
version: '1.0.0',
|
||||
flipperSDKVersion: '0.0.0',
|
||||
}) as _SandyPluginDefinition;
|
||||
})) as _SandyPluginDefinition;
|
||||
expect(plugin).not.toBeNull();
|
||||
expect(plugin).toBeInstanceOf(_SandyPluginDefinition);
|
||||
expect(plugin.id).toBe('Sample');
|
||||
@@ -261,11 +261,11 @@ test('requirePlugin loads valid Sandy plugin', () => {
|
||||
expect(typeof plugin.asPluginModule().plugin).toBe('function');
|
||||
});
|
||||
|
||||
test('requirePlugin errors on invalid Sandy plugin', () => {
|
||||
test('requirePlugin errors on invalid Sandy plugin', async () => {
|
||||
const name = 'pluginID';
|
||||
const failedPlugins: any[] = [];
|
||||
const requireFn = createRequirePluginFunction(failedPlugins, require);
|
||||
requireFn({
|
||||
const requireFn = createRequirePluginFunction(failedPlugins);
|
||||
await requireFn({
|
||||
...sampleInstalledPluginDetails,
|
||||
name,
|
||||
// Intentionally the wrong file:
|
||||
@@ -279,10 +279,10 @@ test('requirePlugin errors on invalid Sandy plugin', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('requirePlugin loads valid Sandy Device plugin', () => {
|
||||
test('requirePlugin loads valid Sandy Device plugin', async () => {
|
||||
const name = 'pluginID';
|
||||
const requireFn = createRequirePluginFunction([], require);
|
||||
const plugin = requireFn({
|
||||
const requireFn = createRequirePluginFunction([]);
|
||||
const plugin = (await requireFn({
|
||||
...sampleInstalledPluginDetails,
|
||||
pluginType: 'device',
|
||||
name,
|
||||
@@ -296,7 +296,7 @@ test('requirePlugin loads valid Sandy Device plugin', () => {
|
||||
),
|
||||
version: '1.0.0',
|
||||
flipperSDKVersion: '0.0.0',
|
||||
}) as _SandyPluginDefinition;
|
||||
})) as _SandyPluginDefinition;
|
||||
expect(plugin).not.toBeNull();
|
||||
expect(plugin).toBeInstanceOf(_SandyPluginDefinition);
|
||||
expect(plugin.id).toBe('Sample');
|
||||
|
||||
@@ -75,6 +75,7 @@ export default (
|
||||
});
|
||||
}
|
||||
|
||||
let running = false;
|
||||
const unsubscribeHandlePluginCommands = sideEffect(
|
||||
store,
|
||||
{
|
||||
@@ -85,14 +86,49 @@ export default (
|
||||
noTimeBudgetWarns: true, // These side effects are critical, so we're doing them with zero throttling and want to avoid unnecessary warns
|
||||
},
|
||||
(state) => state.pluginManager.pluginCommandsQueue,
|
||||
processPluginCommandsQueue,
|
||||
async (_queue: PluginCommand[], store: Store) => {
|
||||
// To make sure all commands are running in order, and not kicking off parallel command
|
||||
// processing when new commands arrive (sideEffect doesn't await)
|
||||
// we keep the 'running' flag, and keep running in a loop until the commandQueue is empty,
|
||||
// to make sure any commands that have arrived during execution are executed
|
||||
if (running) {
|
||||
return; // will be picked up in while(true) loop
|
||||
}
|
||||
running = true;
|
||||
try {
|
||||
while (true) {
|
||||
const remaining = store.getState().pluginManager.pluginCommandsQueue;
|
||||
if (!remaining.length) {
|
||||
return; // done
|
||||
}
|
||||
await processPluginCommandsQueue(remaining, store);
|
||||
store.dispatch(pluginCommandsProcessed(remaining.length));
|
||||
}
|
||||
} finally {
|
||||
running = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
return async () => {
|
||||
unsubscribeHandlePluginCommands();
|
||||
};
|
||||
};
|
||||
|
||||
export function processPluginCommandsQueue(
|
||||
export async function awaitPluginCommandQueueEmpty(store: Store) {
|
||||
if (store.getState().pluginManager.pluginCommandsQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
if (store.getState().pluginManager.pluginCommandsQueue.length === 0) {
|
||||
unsubscribe();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function processPluginCommandsQueue(
|
||||
queue: PluginCommand[],
|
||||
store: Store,
|
||||
) {
|
||||
@@ -100,7 +136,7 @@ export function processPluginCommandsQueue(
|
||||
try {
|
||||
switch (command.type) {
|
||||
case 'LOAD_PLUGIN':
|
||||
loadPlugin(store, command.payload);
|
||||
await loadPlugin(store, command.payload);
|
||||
break;
|
||||
case 'UNINSTALL_PLUGIN':
|
||||
uninstallPlugin(store, command.payload);
|
||||
@@ -121,12 +157,11 @@ export function processPluginCommandsQueue(
|
||||
console.error('Failed to process command', command);
|
||||
}
|
||||
}
|
||||
store.dispatch(pluginCommandsProcessed(queue.length));
|
||||
}
|
||||
|
||||
function loadPlugin(store: Store, payload: LoadPluginActionPayload) {
|
||||
async function loadPlugin(store: Store, payload: LoadPluginActionPayload) {
|
||||
try {
|
||||
const plugin = requirePlugin(payload.plugin);
|
||||
const plugin = await requirePlugin(payload.plugin);
|
||||
const enablePlugin = payload.enable;
|
||||
updatePlugin(store, {plugin, enablePlugin});
|
||||
} catch (err) {
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
*/
|
||||
|
||||
import type {Store} from '../reducers/index';
|
||||
import type {InstalledPluginDetails, Logger} from 'flipper-common';
|
||||
import {
|
||||
InstalledPluginDetails,
|
||||
Logger,
|
||||
tryCatchReportPluginFailuresAsync,
|
||||
} from 'flipper-common';
|
||||
import {PluginDefinition} from '../plugin';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
@@ -31,7 +35,7 @@ import {
|
||||
BundledPluginDetails,
|
||||
ConcretePluginDetails,
|
||||
} from 'flipper-common';
|
||||
import {tryCatchReportPluginFailures, reportUsage} from 'flipper-common';
|
||||
import {reportUsage} from 'flipper-common';
|
||||
import * as FlipperPluginSDK from 'flipper-plugin';
|
||||
import {_SandyPluginDefinition} from 'flipper-plugin';
|
||||
import * as Immer from 'immer';
|
||||
@@ -46,6 +50,8 @@ import isPluginCompatible from '../utils/isPluginCompatible';
|
||||
import isPluginVersionMoreRecent from '../utils/isPluginVersionMoreRecent';
|
||||
import {createSandyPluginWrapper} from '../utils/createSandyPluginWrapper';
|
||||
import {getRenderHostInstance} from '../RenderHost';
|
||||
import pMap from 'p-map';
|
||||
|
||||
let defaultPluginsIndex: any = null;
|
||||
|
||||
export default async (store: Store, _logger: Logger) => {
|
||||
@@ -88,12 +94,15 @@ export default async (store: Store, _logger: Logger) => {
|
||||
const loadedPlugins =
|
||||
getLatestCompatibleVersionOfEachPlugin(allLocalVersions);
|
||||
|
||||
const initialPlugins: PluginDefinition[] = loadedPlugins
|
||||
const pluginsToLoad = loadedPlugins
|
||||
.map(reportVersion)
|
||||
.filter(checkDisabled(disabledPlugins))
|
||||
.filter(checkGK(gatekeepedPlugins))
|
||||
.map(createRequirePluginFunction(failedPlugins))
|
||||
.filter(notNull);
|
||||
.filter(checkGK(gatekeepedPlugins));
|
||||
const loader = createRequirePluginFunction(failedPlugins);
|
||||
|
||||
const initialPlugins: PluginDefinition[] = (
|
||||
await pMap(pluginsToLoad, loader)
|
||||
).filter(notNull);
|
||||
|
||||
const classicPlugins = initialPlugins.filter(
|
||||
(p) => !isSandyPlugin(p.details),
|
||||
@@ -235,11 +244,12 @@ export const checkDisabled = (
|
||||
|
||||
export const createRequirePluginFunction = (
|
||||
failedPlugins: Array<[ActivatablePluginDetails, string]>,
|
||||
reqFn: Function = global.electronRequire,
|
||||
) => {
|
||||
return (pluginDetails: ActivatablePluginDetails): PluginDefinition | null => {
|
||||
return async (
|
||||
pluginDetails: ActivatablePluginDetails,
|
||||
): Promise<PluginDefinition | null> => {
|
||||
try {
|
||||
const pluginDefinition = requirePlugin(pluginDetails, reqFn);
|
||||
const pluginDefinition = await requirePlugin(pluginDetails);
|
||||
if (
|
||||
pluginDefinition &&
|
||||
isDevicePluginDefinition(pluginDefinition) &&
|
||||
@@ -260,8 +270,7 @@ export const createRequirePluginFunction = (
|
||||
|
||||
export const requirePlugin = (
|
||||
pluginDetails: ActivatablePluginDetails,
|
||||
reqFn: Function = global.electronRequire,
|
||||
): PluginDefinition => {
|
||||
): Promise<PluginDefinition> => {
|
||||
reportUsage(
|
||||
'plugin:load',
|
||||
{
|
||||
@@ -269,8 +278,8 @@ export const requirePlugin = (
|
||||
},
|
||||
pluginDetails.id,
|
||||
);
|
||||
return tryCatchReportPluginFailures(
|
||||
() => requirePluginInternal(pluginDetails, reqFn),
|
||||
return tryCatchReportPluginFailuresAsync(
|
||||
() => requirePluginInternal(pluginDetails),
|
||||
'plugin:load',
|
||||
pluginDetails.id,
|
||||
);
|
||||
@@ -280,13 +289,12 @@ const isSandyPlugin = (pluginDetails: ActivatablePluginDetails) => {
|
||||
return !!pluginDetails.flipperSDKVersion;
|
||||
};
|
||||
|
||||
const requirePluginInternal = (
|
||||
const requirePluginInternal = async (
|
||||
pluginDetails: ActivatablePluginDetails,
|
||||
reqFn: Function = global.electronRequire,
|
||||
): PluginDefinition => {
|
||||
): Promise<PluginDefinition> => {
|
||||
let plugin = pluginDetails.isBundled
|
||||
? defaultPluginsIndex[pluginDetails.name]
|
||||
: reqFn(pluginDetails.entry);
|
||||
: await getRenderHostInstance().requirePlugin(pluginDetails.entry);
|
||||
if (isSandyPlugin(pluginDetails)) {
|
||||
// Sandy plugin
|
||||
return new _SandyPluginDefinition(pluginDetails, plugin);
|
||||
|
||||
Reference in New Issue
Block a user