Introduce DevicePlugin APIs
Summary: This stack introduces Sandy device plugins, they are quite similar to normal plugins, but, a devicePlugin module is organized as ``` export function supportsDevice(device): boolean export function devicePlugin(devicePluginClient) export function Component ``` Device plugins get access to the device meta data and can subscribe to the `onLogEntry` callback and `onDestroy` lifecycle. They will be able to store state just as normal plugins, but can't send or receive methods, so devicePluginClient is a bit limited. This diff only sets up most of the new data structures, and makes sure everything still compiles and no existing tests fail. To prevent this diff from becoming to big, actually loading, rendering and testing device plugins will be done in next diffs Please take a critical look at the api proposed and the (especially) the public names used :) Reviewed By: passy, nikoant Differential Revision: D22691351 fbshipit-source-id: bdbbd7f86d14b646fc9a693ad19f33583a76f26d
This commit is contained in:
committed by
Facebook GitHub Bot
parent
6083534025
commit
91ed4e31c0
@@ -7,11 +7,12 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import BaseDevice, {DeviceType, LogLevel} from './BaseDevice';
|
import BaseDevice, {DeviceType} from './BaseDevice';
|
||||||
import adb, {Client as ADBClient} from 'adbkit';
|
import adb, {Client as ADBClient} from 'adbkit';
|
||||||
import {Priority} from 'adbkit-logcat';
|
import {Priority} from 'adbkit-logcat';
|
||||||
import ArchivedDevice from './ArchivedDevice';
|
import ArchivedDevice from './ArchivedDevice';
|
||||||
import {createWriteStream} from 'fs';
|
import {createWriteStream} from 'fs';
|
||||||
|
import {LogLevel} from 'flipper-plugin';
|
||||||
|
|
||||||
const DEVICE_RECORDING_DIR = '/sdcard/flipper_recorder';
|
const DEVICE_RECORDING_DIR = '/sdcard/flipper_recorder';
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {DeviceLogEntry} from 'flipper-plugin';
|
||||||
import BaseDevice from './BaseDevice';
|
import BaseDevice from './BaseDevice';
|
||||||
import {DeviceType, OS, DeviceShell, DeviceLogEntry} from './BaseDevice';
|
import {DeviceType, OS, DeviceShell} from './BaseDevice';
|
||||||
import {SupportFormRequestDetailsState} from '../reducers/supportForm';
|
import {SupportFormRequestDetailsState} from '../reducers/supportForm';
|
||||||
|
|
||||||
function normalizeArchivedDeviceType(deviceType: DeviceType): DeviceType {
|
function normalizeArchivedDeviceType(deviceType: DeviceType): DeviceType {
|
||||||
|
|||||||
@@ -8,27 +8,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import stream from 'stream';
|
import stream from 'stream';
|
||||||
import {FlipperDevicePlugin} from 'flipper';
|
import {FlipperDevicePlugin, DeviceLogListener} from 'flipper';
|
||||||
import {sortPluginsByName} from '../utils/pluginUtils';
|
import {sortPluginsByName} from '../utils/pluginUtils';
|
||||||
|
import {DeviceLogEntry} from 'flipper-plugin';
|
||||||
export type LogLevel =
|
|
||||||
| 'unknown'
|
|
||||||
| 'verbose'
|
|
||||||
| 'debug'
|
|
||||||
| 'info'
|
|
||||||
| 'warn'
|
|
||||||
| 'error'
|
|
||||||
| 'fatal';
|
|
||||||
|
|
||||||
export type DeviceLogEntry = {
|
|
||||||
readonly date: Date;
|
|
||||||
readonly pid: number;
|
|
||||||
readonly tid: number;
|
|
||||||
readonly app?: string;
|
|
||||||
readonly type: LogLevel;
|
|
||||||
readonly tag: string;
|
|
||||||
readonly message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DeviceShell = {
|
export type DeviceShell = {
|
||||||
stdout: stream.Readable;
|
stdout: stream.Readable;
|
||||||
@@ -36,8 +18,6 @@ export type DeviceShell = {
|
|||||||
stdin: stream.Writable;
|
stdin: stream.Writable;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeviceLogListener = (entry: DeviceLogEntry) => void;
|
|
||||||
|
|
||||||
export type DeviceType =
|
export type DeviceType =
|
||||||
| 'emulator'
|
| 'emulator'
|
||||||
| 'physical'
|
| 'physical'
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {DeviceType, LogLevel, DeviceLogEntry} from './BaseDevice';
|
import {LogLevel, DeviceLogEntry} from 'flipper-plugin';
|
||||||
|
import {DeviceType} from './BaseDevice';
|
||||||
import child_process, {ChildProcess} from 'child_process';
|
import child_process, {ChildProcess} from 'child_process';
|
||||||
import BaseDevice from './BaseDevice';
|
import BaseDevice from './BaseDevice';
|
||||||
import JSONStream from 'JSONStream';
|
import JSONStream from 'JSONStream';
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import BaseDevice, {LogLevel} from './BaseDevice';
|
import {LogLevel} from 'flipper-plugin';
|
||||||
|
import BaseDevice from './BaseDevice';
|
||||||
import ArchivedDevice from './ArchivedDevice';
|
import ArchivedDevice from './ArchivedDevice';
|
||||||
import {EventEmitter} from 'events';
|
import {EventEmitter} from 'events';
|
||||||
|
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ test('requirePlugin loads valid Sandy plugin', () => {
|
|||||||
});
|
});
|
||||||
expect(typeof plugin.module.Component).toBe('function');
|
expect(typeof plugin.module.Component).toBe('function');
|
||||||
expect(plugin.module.Component.displayName).toBe('FlipperPlugin(Sample)');
|
expect(plugin.module.Component.displayName).toBe('FlipperPlugin(Sample)');
|
||||||
expect(typeof plugin.module.plugin).toBe('function');
|
expect(typeof plugin.asPluginModule().plugin).toBe('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('requirePlugin errors on invalid Sandy plugin', () => {
|
test('requirePlugin errors on invalid Sandy plugin', () => {
|
||||||
|
|||||||
@@ -44,11 +44,7 @@ export {getPluginKey, getPersistedState} from './utils/pluginUtils';
|
|||||||
export {Idler} from './utils/Idler';
|
export {Idler} from './utils/Idler';
|
||||||
export {Store, MiddlewareAPI, State as ReduxState} from './reducers/index';
|
export {Store, MiddlewareAPI, State as ReduxState} from './reducers/index';
|
||||||
export {default as BaseDevice} from './devices/BaseDevice';
|
export {default as BaseDevice} from './devices/BaseDevice';
|
||||||
export {
|
export {DeviceLogEntry, LogLevel, DeviceLogListener} from 'flipper-plugin';
|
||||||
DeviceLogListener,
|
|
||||||
DeviceLogEntry,
|
|
||||||
LogLevel,
|
|
||||||
} from './devices/BaseDevice';
|
|
||||||
export {shouldParseAndroidLog} from './utils/crashReporterUtility';
|
export {shouldParseAndroidLog} from './utils/crashReporterUtility';
|
||||||
export {default as isProduction} from './utils/isProduction';
|
export {default as isProduction} from './utils/isProduction';
|
||||||
export {createTablePlugin} from './createTablePlugin';
|
export {createTablePlugin} from './createTablePlugin';
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ test('it should initialize starred sandy plugins', async () => {
|
|||||||
test('it should cleanup a plugin if disabled', async () => {
|
test('it should cleanup a plugin if disabled', async () => {
|
||||||
const {client, store} = await createMockFlipperWithPlugin(TestPlugin);
|
const {client, store} = await createMockFlipperWithPlugin(TestPlugin);
|
||||||
|
|
||||||
expect(TestPlugin.module.plugin).toBeCalledTimes(1);
|
expect(TestPlugin.asPluginModule().plugin).toBeCalledTimes(1);
|
||||||
const pluginInstance: PluginApi = client.sandyPluginStates.get(TestPlugin.id)!
|
const pluginInstance: PluginApi = client.sandyPluginStates.get(TestPlugin.id)!
|
||||||
.instanceApi;
|
.instanceApi;
|
||||||
expect(pluginInstance.destroyStub).toHaveBeenCalledTimes(0);
|
expect(pluginInstance.destroyStub).toHaveBeenCalledTimes(0);
|
||||||
@@ -150,7 +150,7 @@ test('it should not initialize a sandy plugin if not enabled', async () => {
|
|||||||
await client.refreshPlugins();
|
await client.refreshPlugins();
|
||||||
// not yet enabled, so not yet started
|
// not yet enabled, so not yet started
|
||||||
expect(client.sandyPluginStates.get(Plugin2.id)).toBeUndefined();
|
expect(client.sandyPluginStates.get(Plugin2.id)).toBeUndefined();
|
||||||
expect(Plugin2.module.plugin).toBeCalledTimes(0);
|
expect(Plugin2.asPluginModule().plugin).toBeCalledTimes(0);
|
||||||
|
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
starPlugin({
|
starPlugin({
|
||||||
@@ -166,8 +166,8 @@ test('it should not initialize a sandy plugin if not enabled', async () => {
|
|||||||
.instanceApi as PluginApi;
|
.instanceApi as PluginApi;
|
||||||
expect(client.sandyPluginStates.get(TestPlugin.id)).toBe(pluginState1); // not reinitialized
|
expect(client.sandyPluginStates.get(TestPlugin.id)).toBe(pluginState1); // not reinitialized
|
||||||
|
|
||||||
expect(TestPlugin.module.plugin).toBeCalledTimes(1);
|
expect(TestPlugin.asPluginModule().plugin).toBeCalledTimes(1);
|
||||||
expect(Plugin2.module.plugin).toBeCalledTimes(1);
|
expect(Plugin2.asPluginModule().plugin).toBeCalledTimes(1);
|
||||||
expect(instance.destroyStub).toHaveBeenCalledTimes(0);
|
expect(instance.destroyStub).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
// disable plugin again
|
// disable plugin again
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {DeviceLogEntry} from '../devices/BaseDevice';
|
import {DeviceLogEntry} from 'flipper-plugin';
|
||||||
|
|
||||||
export function shouldParseAndroidLog(
|
export function shouldParseAndroidLog(
|
||||||
entry: DeviceLogEntry,
|
entry: DeviceLogEntry,
|
||||||
|
|||||||
@@ -10,6 +10,13 @@
|
|||||||
import * as TestUtilites from './test-utils/test-utils';
|
import * as TestUtilites from './test-utils/test-utils';
|
||||||
|
|
||||||
export {SandyPluginInstance, FlipperClient} from './plugin/Plugin';
|
export {SandyPluginInstance, FlipperClient} from './plugin/Plugin';
|
||||||
|
export {
|
||||||
|
Device,
|
||||||
|
DeviceLogEntry,
|
||||||
|
DeviceLogListener,
|
||||||
|
DevicePluginClient,
|
||||||
|
LogLevel,
|
||||||
|
} from './plugin/DevicePlugin';
|
||||||
export {SandyPluginDefinition} from './plugin/SandyPluginDefinition';
|
export {SandyPluginDefinition} from './plugin/SandyPluginDefinition';
|
||||||
export {SandyPluginRenderer} from './plugin/PluginRenderer';
|
export {SandyPluginRenderer} from './plugin/PluginRenderer';
|
||||||
export {SandyPluginContext, usePlugin} from './plugin/PluginContext';
|
export {SandyPluginContext, usePlugin} from './plugin/PluginContext';
|
||||||
|
|||||||
198
desktop/flipper-plugin/src/plugin/DevicePlugin.tsx
Normal file
198
desktop/flipper-plugin/src/plugin/DevicePlugin.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* 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 {SandyPluginDefinition} from './SandyPluginDefinition';
|
||||||
|
import {EventEmitter} from 'events';
|
||||||
|
import {Atom} from '../state/atom';
|
||||||
|
import {setCurrentPluginInstance} from './Plugin';
|
||||||
|
|
||||||
|
export type DeviceLogListener = (entry: DeviceLogEntry) => void;
|
||||||
|
|
||||||
|
export type DeviceLogEntry = {
|
||||||
|
readonly date: Date;
|
||||||
|
readonly pid: number;
|
||||||
|
readonly tid: number;
|
||||||
|
readonly app?: string;
|
||||||
|
readonly type: LogLevel;
|
||||||
|
readonly tag: string;
|
||||||
|
readonly message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LogLevel =
|
||||||
|
| 'unknown'
|
||||||
|
| 'verbose'
|
||||||
|
| 'debug'
|
||||||
|
| 'info'
|
||||||
|
| 'warn'
|
||||||
|
| 'error'
|
||||||
|
| 'fatal';
|
||||||
|
|
||||||
|
export interface Device {
|
||||||
|
isArchived: boolean;
|
||||||
|
onLogEntry(cb: DeviceLogListener): () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DevicePluginPredicate = (device: Device) => boolean;
|
||||||
|
|
||||||
|
export type DevicePluginFactory = (client: DevicePluginClient) => object;
|
||||||
|
|
||||||
|
export interface DevicePluginClient {
|
||||||
|
readonly device: Device;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the onDestroy event is fired whenever a device is unloaded from Flipper, or a plugin is disabled.
|
||||||
|
*/
|
||||||
|
onDestroy(cb: () => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the onActivate event is fired whenever the plugin is actived in the UI
|
||||||
|
*/
|
||||||
|
onActivate(cb: () => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The counterpart of the `onActivate` handler.
|
||||||
|
*/
|
||||||
|
onDeactivate(cb: () => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RealFlipperDevice {
|
||||||
|
isArchived: boolean;
|
||||||
|
addLogListener(callback: DeviceLogListener): Symbol;
|
||||||
|
removeLogListener(id: Symbol): void;
|
||||||
|
addLogEntry(entry: DeviceLogEntry): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SandyDevicePluginInstance {
|
||||||
|
static is(thing: any): thing is SandyDevicePluginInstance {
|
||||||
|
return thing instanceof SandyDevicePluginInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** client that is bound to this instance */
|
||||||
|
client: DevicePluginClient;
|
||||||
|
/** the original plugin definition */
|
||||||
|
definition: SandyPluginDefinition;
|
||||||
|
/** the plugin instance api as used inside components and such */
|
||||||
|
instanceApi: any;
|
||||||
|
|
||||||
|
activated = false;
|
||||||
|
destroyed = false;
|
||||||
|
events = new EventEmitter();
|
||||||
|
|
||||||
|
// temporarily field that is used during deserialization
|
||||||
|
initialStates?: Record<string, any>;
|
||||||
|
// all the atoms that should be serialized when making an export / import
|
||||||
|
rootStates: Record<string, Atom<any>> = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
realDevice: RealFlipperDevice,
|
||||||
|
definition: SandyPluginDefinition,
|
||||||
|
initialStates?: Record<string, any>,
|
||||||
|
) {
|
||||||
|
this.definition = definition;
|
||||||
|
const device: Device = {
|
||||||
|
get isArchived() {
|
||||||
|
return realDevice.isArchived;
|
||||||
|
},
|
||||||
|
onLogEntry(cb) {
|
||||||
|
const handle = realDevice.addLogListener(cb);
|
||||||
|
return () => {
|
||||||
|
realDevice.removeLogListener(handle);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.client = {
|
||||||
|
device,
|
||||||
|
onDestroy: (cb) => {
|
||||||
|
this.events.on('destroy', cb);
|
||||||
|
},
|
||||||
|
onActivate: (cb) => {
|
||||||
|
this.events.on('activate', cb);
|
||||||
|
},
|
||||||
|
onDeactivate: (cb) => {
|
||||||
|
this.events.on('deactivate', cb);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setCurrentPluginInstance(this);
|
||||||
|
this.initialStates = initialStates;
|
||||||
|
try {
|
||||||
|
this.instanceApi = definition
|
||||||
|
.asDevicePluginModule()
|
||||||
|
.devicePlugin(this.client);
|
||||||
|
} finally {
|
||||||
|
this.initialStates = undefined;
|
||||||
|
setCurrentPluginInstance(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the plugin is selected in the UI
|
||||||
|
activate() {
|
||||||
|
this.assertNotDestroyed();
|
||||||
|
if (!this.activated) {
|
||||||
|
this.activated = true;
|
||||||
|
this.events.emit('activate');
|
||||||
|
}
|
||||||
|
// TODO:
|
||||||
|
// const pluginId = this.definition.id;
|
||||||
|
// if (!this.realClient.isBackgroundPlugin(pluginId)) {
|
||||||
|
// this.realClient.initPlugin(pluginId); // will call connect() if needed
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// the plugin is deselected in the UI
|
||||||
|
deactivate() {
|
||||||
|
// TODO:
|
||||||
|
// if (this.destroyed) {
|
||||||
|
// // this can happen if the plugin is disabled while active in the UI.
|
||||||
|
// // In that case deinit & destroy is already triggered from the STAR_PLUGIN action
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// const pluginId = this.definition.id;
|
||||||
|
// if (!this.realClient.isBackgroundPlugin(pluginId)) {
|
||||||
|
// this.realClient.deinitPlugin(pluginId);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.assertNotDestroyed();
|
||||||
|
if (this.activated) {
|
||||||
|
this.activated = false;
|
||||||
|
this.events.emit('deactivate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.assertNotDestroyed();
|
||||||
|
// TODO:
|
||||||
|
// if (this.activated) {
|
||||||
|
// this.realClient.deinitPlugin(this.definition.id);
|
||||||
|
// }
|
||||||
|
this.events.emit('destroy');
|
||||||
|
this.destroyed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return '[SandyDevicePluginInstance]';
|
||||||
|
}
|
||||||
|
|
||||||
|
exportState() {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(this.rootStates).map(([key, atom]) => [key, atom.get()]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isPersistable(): boolean {
|
||||||
|
return Object.keys(this.rootStates).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertNotDestroyed() {
|
||||||
|
if (this.destroyed) {
|
||||||
|
throw new Error('Plugin has been destroyed already');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
import {SandyPluginDefinition} from './SandyPluginDefinition';
|
import {SandyPluginDefinition} from './SandyPluginDefinition';
|
||||||
import {EventEmitter} from 'events';
|
import {EventEmitter} from 'events';
|
||||||
import {Atom} from '../state/atom';
|
import {Atom} from '../state/atom';
|
||||||
|
import {SandyDevicePluginInstance} from './DevicePlugin';
|
||||||
|
|
||||||
type EventsContract = Record<string, any>;
|
type EventsContract = Record<string, any>;
|
||||||
type MethodsContract = Record<string, (params: any) => Promise<any>>;
|
type MethodsContract = Record<string, (params: any) => Promise<any>>;
|
||||||
@@ -87,14 +88,27 @@ export interface RealFlipperClient {
|
|||||||
): Promise<Object>;
|
): Promise<Object>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FlipperPluginFactory<
|
export type PluginFactory<
|
||||||
Events extends EventsContract,
|
Events extends EventsContract,
|
||||||
Methods extends MethodsContract
|
Methods extends MethodsContract
|
||||||
> = (client: FlipperClient<Events, Methods>) => object;
|
> = (client: FlipperClient<Events, Methods>) => object;
|
||||||
|
|
||||||
export type FlipperPluginComponent = React.FC<{}>;
|
export type FlipperPluginComponent = React.FC<{}>;
|
||||||
|
|
||||||
export let currentPluginInstance: SandyPluginInstance | undefined = undefined;
|
let currentPluginInstance:
|
||||||
|
| SandyPluginInstance
|
||||||
|
| SandyDevicePluginInstance
|
||||||
|
| undefined = undefined;
|
||||||
|
|
||||||
|
export function setCurrentPluginInstance(
|
||||||
|
instance: typeof currentPluginInstance,
|
||||||
|
) {
|
||||||
|
currentPluginInstance = instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentPluginInstance(): typeof currentPluginInstance {
|
||||||
|
return currentPluginInstance;
|
||||||
|
}
|
||||||
|
|
||||||
export class SandyPluginInstance {
|
export class SandyPluginInstance {
|
||||||
static is(thing: any): thing is SandyPluginInstance {
|
static is(thing: any): thing is SandyPluginInstance {
|
||||||
@@ -154,13 +168,13 @@ export class SandyPluginInstance {
|
|||||||
this.events.on('deeplink', callback);
|
this.events.on('deeplink', callback);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
currentPluginInstance = this;
|
setCurrentPluginInstance(this);
|
||||||
this.initialStates = initialStates;
|
this.initialStates = initialStates;
|
||||||
try {
|
try {
|
||||||
this.instanceApi = definition.module.plugin(this.client);
|
this.instanceApi = definition.asPluginModule().plugin(this.client);
|
||||||
} finally {
|
} finally {
|
||||||
this.initialStates = undefined;
|
this.initialStates = undefined;
|
||||||
currentPluginInstance = undefined;
|
setCurrentPluginInstance(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {createContext, useContext} from 'react';
|
import {createContext, useContext} from 'react';
|
||||||
import {SandyPluginInstance, FlipperPluginFactory} from './Plugin';
|
import {SandyPluginInstance, PluginFactory} from './Plugin';
|
||||||
|
import {SandyDevicePluginInstance, DevicePluginFactory} from './DevicePlugin';
|
||||||
|
|
||||||
export const SandyPluginContext = createContext<
|
export const SandyPluginContext = createContext<
|
||||||
SandyPluginInstance | undefined
|
SandyPluginInstance | SandyDevicePluginInstance | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
|
||||||
export function usePlugin<PluginFactory extends FlipperPluginFactory<any, any>>(
|
export function usePlugin<
|
||||||
plugin: PluginFactory,
|
Factory extends PluginFactory<any, any> | DevicePluginFactory
|
||||||
): ReturnType<PluginFactory> {
|
>(plugin: Factory): ReturnType<Factory> {
|
||||||
const pluginInstance = useContext(SandyPluginContext);
|
const pluginInstance = useContext(SandyPluginContext);
|
||||||
if (!pluginInstance) {
|
if (!pluginInstance) {
|
||||||
throw new Error('Plugin context not available');
|
throw new Error('Plugin context not available');
|
||||||
@@ -25,9 +26,12 @@ export function usePlugin<PluginFactory extends FlipperPluginFactory<any, any>>(
|
|||||||
// return of this function is strongly typed, without the user needing to create it's own
|
// return of this function is strongly typed, without the user needing to create it's own
|
||||||
// context.
|
// context.
|
||||||
// But since we pass it, let's make sure the user did request the proper context
|
// But since we pass it, let's make sure the user did request the proper context
|
||||||
if (pluginInstance.definition.module.plugin !== plugin) {
|
const pluginFromDefinition = pluginInstance.definition.isDevicePlugin
|
||||||
|
? pluginInstance.definition.asDevicePluginModule().devicePlugin
|
||||||
|
: pluginInstance.definition.asPluginModule().plugin;
|
||||||
|
if (pluginFromDefinition !== plugin) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Plugin context (${pluginInstance.definition.module.plugin}) didn't match the type of the requested plugin (${plugin})`,
|
`Plugin in context (${pluginFromDefinition}) didn't match the type of the requested plugin (${plugin})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return pluginInstance.instanceApi;
|
return pluginInstance.instanceApi;
|
||||||
|
|||||||
@@ -10,9 +10,10 @@
|
|||||||
import React, {memo, useEffect, createElement} from 'react';
|
import React, {memo, useEffect, createElement} from 'react';
|
||||||
import {SandyPluginContext} from './PluginContext';
|
import {SandyPluginContext} from './PluginContext';
|
||||||
import {SandyPluginInstance} from './Plugin';
|
import {SandyPluginInstance} from './Plugin';
|
||||||
|
import {SandyDevicePluginInstance} from './DevicePlugin';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
plugin: SandyPluginInstance;
|
plugin: SandyPluginInstance | SandyDevicePluginInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,20 +8,29 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {PluginDetails} from 'flipper-plugin-lib';
|
import {PluginDetails} from 'flipper-plugin-lib';
|
||||||
import {FlipperPluginFactory, FlipperPluginComponent} from './Plugin';
|
import {PluginFactory, FlipperPluginComponent} from './Plugin';
|
||||||
|
import {DevicePluginPredicate, DevicePluginFactory} from './DevicePlugin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FlipperPluginModule describe the exports that are provided by a typical Flipper Desktop plugin
|
* FlipperPluginModule describe the exports that are provided by a typical Flipper Desktop plugin
|
||||||
*/
|
*/
|
||||||
export type FlipperPluginModule<
|
export type FlipperDevicePluginModule = {
|
||||||
Factory extends FlipperPluginFactory<any, any>
|
/** predicate that determines if this plugin applies to the currently selcted device */
|
||||||
> = {
|
supportsDevice: DevicePluginPredicate;
|
||||||
|
/** the factory function that initializes a plugin instance */
|
||||||
|
devicePlugin: DevicePluginFactory;
|
||||||
|
/** the component type that can render this plugin */
|
||||||
|
Component: FlipperPluginComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FlipperPluginModule describe the exports that are provided by a typical Flipper Desktop plugin
|
||||||
|
*/
|
||||||
|
export type FlipperPluginModule<Factory extends PluginFactory<any, any>> = {
|
||||||
/** the factory function that initializes a plugin instance */
|
/** the factory function that initializes a plugin instance */
|
||||||
plugin: Factory;
|
plugin: Factory;
|
||||||
/** the component type that can render this plugin */
|
/** the component type that can render this plugin */
|
||||||
Component: FlipperPluginComponent;
|
Component: FlipperPluginComponent;
|
||||||
// TODO: support device plugins T68738317
|
|
||||||
// devicePlugin: FlipperPluginFactory
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,8 +41,9 @@ export type FlipperPluginModule<
|
|||||||
*/
|
*/
|
||||||
export class SandyPluginDefinition {
|
export class SandyPluginDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
module: FlipperPluginModule<any>;
|
module: FlipperPluginModule<any> | FlipperDevicePluginModule;
|
||||||
details: PluginDetails;
|
details: PluginDetails;
|
||||||
|
isDevicePlugin: boolean;
|
||||||
|
|
||||||
// TODO: Implement T68683476
|
// TODO: Implement T68683476
|
||||||
exportPersistedState:
|
exportPersistedState:
|
||||||
@@ -47,14 +57,29 @@ export class SandyPluginDefinition {
|
|||||||
) => Promise<any /* TODO: StaticPersistedState | undefined */>)
|
) => Promise<any /* TODO: StaticPersistedState | undefined */>)
|
||||||
| undefined = undefined;
|
| undefined = undefined;
|
||||||
|
|
||||||
constructor(details: PluginDetails, module: FlipperPluginModule<any>) {
|
constructor(
|
||||||
|
details: PluginDetails,
|
||||||
|
module: FlipperPluginModule<any> | FlipperDevicePluginModule,
|
||||||
|
);
|
||||||
|
constructor(details: PluginDetails, module: any) {
|
||||||
this.id = details.id;
|
this.id = details.id;
|
||||||
this.details = details;
|
this.details = details;
|
||||||
|
if (module.supportsDevice) {
|
||||||
|
// device plugin
|
||||||
|
this.isDevicePlugin = true;
|
||||||
|
if (!module.devicePlugin || typeof module.devicePlugin !== 'function') {
|
||||||
|
throw new Error(
|
||||||
|
`Flipper device plugin '${this.id}' should export named function called 'devicePlugin'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isDevicePlugin = false;
|
||||||
if (!module.plugin || typeof module.plugin !== 'function') {
|
if (!module.plugin || typeof module.plugin !== 'function') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Flipper plugin '${this.id}' should export named function called 'plugin'`,
|
`Flipper plugin '${this.id}' should export named function called 'plugin'`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!module.Component || typeof module.Component !== 'function') {
|
if (!module.Component || typeof module.Component !== 'function') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Flipper plugin '${this.id}' should export named function called 'Component'`,
|
`Flipper plugin '${this.id}' should export named function called 'Component'`,
|
||||||
@@ -64,6 +89,16 @@ export class SandyPluginDefinition {
|
|||||||
this.module.Component.displayName = `FlipperPlugin(${this.id})`;
|
this.module.Component.displayName = `FlipperPlugin(${this.id})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
asDevicePluginModule(): FlipperDevicePluginModule {
|
||||||
|
if (!this.isDevicePlugin) throw new Error('Not a device plugin');
|
||||||
|
return this.module as FlipperDevicePluginModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
asPluginModule(): FlipperPluginModule<any> {
|
||||||
|
if (this.isDevicePlugin) throw new Error('Not an application plugin');
|
||||||
|
return this.module as FlipperPluginModule<any>;
|
||||||
|
}
|
||||||
|
|
||||||
get packageName() {
|
get packageName() {
|
||||||
return this.details.name;
|
return this.details.name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import {produce} from 'immer';
|
import {produce} from 'immer';
|
||||||
import {useState, useEffect} from 'react';
|
import {useState, useEffect} from 'react';
|
||||||
import {currentPluginInstance} from '../plugin/Plugin';
|
import {getCurrentPluginInstance} from '../plugin/Plugin';
|
||||||
|
|
||||||
export type Atom<T> = {
|
export type Atom<T> = {
|
||||||
get(): T;
|
get(): T;
|
||||||
@@ -70,8 +70,8 @@ export function createState<T>(
|
|||||||
options: StateOptions = {},
|
options: StateOptions = {},
|
||||||
): Atom<T> {
|
): Atom<T> {
|
||||||
const atom = new AtomValue<T>(initialValue);
|
const atom = new AtomValue<T>(initialValue);
|
||||||
if (currentPluginInstance && options.persist) {
|
if (getCurrentPluginInstance() && options.persist) {
|
||||||
const {initialStates, rootStates} = currentPluginInstance;
|
const {initialStates, rootStates} = getCurrentPluginInstance()!;
|
||||||
if (initialStates) {
|
if (initialStates) {
|
||||||
if (options.persist in initialStates) {
|
if (options.persist in initialStates) {
|
||||||
atom.set(initialStates[options.persist]);
|
atom.set(initialStates[options.persist]);
|
||||||
|
|||||||
@@ -24,14 +24,22 @@ import {
|
|||||||
import {
|
import {
|
||||||
SandyPluginDefinition,
|
SandyPluginDefinition,
|
||||||
FlipperPluginModule,
|
FlipperPluginModule,
|
||||||
|
FlipperDevicePluginModule,
|
||||||
} from '../plugin/SandyPluginDefinition';
|
} from '../plugin/SandyPluginDefinition';
|
||||||
import {SandyPluginRenderer} from '../plugin/PluginRenderer';
|
import {SandyPluginRenderer} from '../plugin/PluginRenderer';
|
||||||
import {act} from '@testing-library/react';
|
import {act} from '@testing-library/react';
|
||||||
|
import {
|
||||||
|
DeviceLogEntry,
|
||||||
|
SandyDevicePluginInstance,
|
||||||
|
RealFlipperDevice,
|
||||||
|
DeviceLogListener,
|
||||||
|
} from '../plugin/DevicePlugin';
|
||||||
|
|
||||||
type Renderer = RenderResult<typeof queries>;
|
type Renderer = RenderResult<typeof queries>;
|
||||||
|
|
||||||
interface StartPluginOptions {
|
interface StartPluginOptions {
|
||||||
initialState?: Record<string, any>;
|
initialState?: Record<string, any>;
|
||||||
|
isArchived?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExtractClientType<Module extends FlipperPluginModule<any>> = Parameters<
|
type ExtractClientType<Module extends FlipperPluginModule<any>> = Parameters<
|
||||||
@@ -105,6 +113,37 @@ interface StartPluginResult<Module extends FlipperPluginModule<any>> {
|
|||||||
exportState(): any;
|
exportState(): any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface StartDevicePluginResult<Module extends FlipperDevicePluginModule> {
|
||||||
|
/**
|
||||||
|
* the instantiated plugin for this test
|
||||||
|
*/
|
||||||
|
instance: ReturnType<Module['devicePlugin']>;
|
||||||
|
/**
|
||||||
|
* module, from which any other exposed methods can be accessed during testing
|
||||||
|
*/
|
||||||
|
module: Module;
|
||||||
|
/**
|
||||||
|
* Emulates the 'onActivate' event
|
||||||
|
*/
|
||||||
|
activate(): void;
|
||||||
|
/**
|
||||||
|
* Emulatese the 'onDeactivate' event
|
||||||
|
*/
|
||||||
|
deactivate(): void;
|
||||||
|
/**
|
||||||
|
* Emulates the 'destroy' event. After calling destroy this plugin instance won't be usable anymore
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
/**
|
||||||
|
* Emulates sending a log message arriving from the device
|
||||||
|
*/
|
||||||
|
sendLogEntry(logEntry: DeviceLogEntry): void;
|
||||||
|
/**
|
||||||
|
* Grabs the current (exportable) state
|
||||||
|
*/
|
||||||
|
exportState(): any;
|
||||||
|
}
|
||||||
|
|
||||||
export function startPlugin<Module extends FlipperPluginModule<any>>(
|
export function startPlugin<Module extends FlipperPluginModule<any>>(
|
||||||
module: Module,
|
module: Module,
|
||||||
options?: StartPluginOptions,
|
options?: StartPluginOptions,
|
||||||
@@ -113,6 +152,11 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
|
|||||||
createMockPluginDetails(),
|
createMockPluginDetails(),
|
||||||
module,
|
module,
|
||||||
);
|
);
|
||||||
|
if (definition.isDevicePlugin) {
|
||||||
|
throw new Error(
|
||||||
|
'Use `startDevicePlugin` or `renderDevicePlugin` to test device plugins',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const sendStub = jest.fn();
|
const sendStub = jest.fn();
|
||||||
const fakeFlipper: RealFlipperClient = {
|
const fakeFlipper: RealFlipperClient = {
|
||||||
@@ -199,6 +243,71 @@ export function renderPlugin<Module extends FlipperPluginModule<any>>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function startDevicePlugin<Module extends FlipperDevicePluginModule>(
|
||||||
|
module: Module,
|
||||||
|
options?: StartPluginOptions,
|
||||||
|
): StartDevicePluginResult<Module> {
|
||||||
|
const definition = new SandyPluginDefinition(
|
||||||
|
createMockPluginDetails(),
|
||||||
|
module,
|
||||||
|
);
|
||||||
|
if (definition.isDevicePlugin) {
|
||||||
|
throw new Error(
|
||||||
|
'Use `startPlugin` or `renderPlugin` to test non-device plugins',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const testDevice = createMockDevice(options);
|
||||||
|
const pluginInstance = new SandyDevicePluginInstance(
|
||||||
|
testDevice,
|
||||||
|
definition,
|
||||||
|
options?.initialState,
|
||||||
|
);
|
||||||
|
// we start connected
|
||||||
|
pluginInstance.activate();
|
||||||
|
|
||||||
|
const res: StartDevicePluginResult<Module> = {
|
||||||
|
module,
|
||||||
|
instance: pluginInstance.instanceApi,
|
||||||
|
activate: () => pluginInstance.activate(),
|
||||||
|
deactivate: () => pluginInstance.deactivate(),
|
||||||
|
destroy: () => pluginInstance.destroy(),
|
||||||
|
sendLogEntry: (entry) => {
|
||||||
|
act(() => {
|
||||||
|
testDevice.addLogEntry(entry);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
exportState: () => pluginInstance.exportState(),
|
||||||
|
};
|
||||||
|
// @ts-ignore
|
||||||
|
res._backingInstance = pluginInstance;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderDevicePlugin<Module extends FlipperDevicePluginModule>(
|
||||||
|
module: Module,
|
||||||
|
options?: StartPluginOptions,
|
||||||
|
): StartDevicePluginResult<Module> & {
|
||||||
|
renderer: Renderer;
|
||||||
|
act: (cb: () => void) => void;
|
||||||
|
} {
|
||||||
|
const res = startDevicePlugin(module, options);
|
||||||
|
// @ts-ignore hidden api
|
||||||
|
const pluginInstance: SandyDevicePluginInstance = res._backingInstance;
|
||||||
|
|
||||||
|
const renderer = render(<SandyPluginRenderer plugin={pluginInstance} />);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
renderer,
|
||||||
|
act: testingLibAct,
|
||||||
|
destroy: () => {
|
||||||
|
renderer.unmount();
|
||||||
|
pluginInstance.destroy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createMockPluginDetails(
|
export function createMockPluginDetails(
|
||||||
details?: Partial<PluginDetails>,
|
details?: Partial<PluginDetails>,
|
||||||
): PluginDetails {
|
): PluginDetails {
|
||||||
@@ -216,3 +325,20 @@ export function createMockPluginDetails(
|
|||||||
...details,
|
...details,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createMockDevice(options?: StartPluginOptions): RealFlipperDevice {
|
||||||
|
const logListeners: (undefined | DeviceLogListener)[] = [];
|
||||||
|
return {
|
||||||
|
isArchived: !!options?.isArchived,
|
||||||
|
addLogListener(cb) {
|
||||||
|
logListeners.push(cb);
|
||||||
|
return (logListeners.length - 1) as any;
|
||||||
|
},
|
||||||
|
removeLogListener(idx) {
|
||||||
|
logListeners[idx as any] = undefined;
|
||||||
|
},
|
||||||
|
addLogEntry(entry: DeviceLogEntry) {
|
||||||
|
logListeners.forEach((f) => f?.(entry));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user