Copy RenderHost, FlipperLib initialization, device definition to flipper-frontend-core

Reviewed By: passy

Differential Revision: D36129746

fbshipit-source-id: 15e32b9482d7fe3a24567d2e6bc087095b98226e
This commit is contained in:
Andrey Goncharov
2022-05-10 05:13:24 -07:00
committed by Facebook GitHub Bot
parent db49673d8a
commit f0b5e7cadb
10 changed files with 1023 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
/**
* 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 {FlipperLib, Notification} from 'flipper-plugin';
import {FlipperServer, FlipperServerConfig} from 'flipper-common';
type NotificationEvents = 'show' | 'click' | 'close' | 'reply' | 'action';
type PluginNotification = {
notification: Notification;
pluginId: string;
client: null | string; // id
};
type Icon = {
name: string;
variant: 'outline' | 'filled';
size: number;
density: number;
};
interface NotificationAction {
// Docs: https://electronjs.org/docs/api/structures/notification-action
/**
* The label for the given action.
*/
text?: string;
/**
* The type of action, can be `button`.
*/
type: 'button';
}
// Subset of electron.d.ts
interface NotificationConstructorOptions {
/**
* A title for the notification, which will be shown at the top of the notification
* window when it is shown.
*/
title: string;
/**
* The body text of the notification, which will be displayed below the title or
* subtitle.
*/
body: string;
/**
* Actions to add to the notification. Please read the available actions and
* limitations in the `NotificationAction` documentation.
*
* @platform darwin
*/
actions?: NotificationAction[];
/**
* A custom title for the close button of an alert. An empty string will cause the
* default localized text to be used.
*
* @platform darwin
*/
closeButtonText?: string;
}
// Events that are emitted from the main.ts ovr the IPC process bridge in Electron
type MainProcessEvents = {
'flipper-protocol-handler': [query: string];
'open-flipper-file': [url: string];
notificationEvent: [
eventName: NotificationEvents,
pluginNotification: PluginNotification,
arg: null | string | number,
];
trackUsage: any[];
getLaunchTime: [launchStartTime: number];
};
// Events that are emitted by the child process, to the main process
type ChildProcessEvents = {
setTheme: [theme: 'dark' | 'light' | 'system'];
sendNotification: [
{
payload: NotificationConstructorOptions;
pluginNotification: PluginNotification;
closeAfter?: number;
},
];
getLaunchTime: [];
componentDidMount: [];
};
/**
* Utilities provided by the render host, e.g. Electron, the Browser, etc
*/
export interface RenderHost {
readTextFromClipboard(): string | undefined;
writeTextToClipboard(text: string): void;
/**
* @deprecated
* WARNING!
* It is a low-level API call that might be removed in the future.
* It is not really deprecated yet, but we'll try to make it so.
* TODO: Remove in favor of "exportFile"
*/
showSaveDialog?(options: {
defaultPath?: string;
message?: string;
title?: string;
}): Promise<string | undefined>;
/**
* @deprecated
* WARNING!
* It is a low-level API call that might be removed in the future.
* It is not really deprecated yet, but we'll try to make it so.
* TODO: Remove in favor of "importFile"
*/
showOpenDialog?(options: {
defaultPath?: string;
filter?: {
extensions: string[];
name: string;
};
}): Promise<string | undefined>;
showSelectDirectoryDialog?(defaultPath?: string): Promise<string | undefined>;
importFile: FlipperLib['importFile'];
exportFile: FlipperLib['exportFile'];
hasFocus(): boolean;
onIpcEvent<Event extends keyof MainProcessEvents>(
event: Event,
callback: (...arg: MainProcessEvents[Event]) => void,
): void;
sendIpcEvent<Event extends keyof ChildProcessEvents>(
event: Event,
...args: ChildProcessEvents[Event]
): void;
shouldUseDarkColors(): boolean;
restartFlipper(update?: boolean): void;
openLink(url: string): void;
loadDefaultPlugins(): Record<string, any>;
GK(gatekeeper: string): boolean;
flipperServer: FlipperServer;
serverConfig: FlipperServerConfig;
requirePlugin(path: string): Promise<any>;
getStaticResourceUrl(relativePath: string): string;
// given the requested icon and proposed public url of the icon, rewrite it to a local icon if needed
getLocalIconUrl?(icon: Icon, publicUrl: string): string;
unloadModule?(path: string): void;
getPercentCPUUsage?(): number;
}
export function getRenderHostInstance(): RenderHost {
if (!FlipperRenderHostInstance) {
throw new Error('global FlipperRenderHostInstance was never set');
}
return FlipperRenderHostInstance;
}

View File

@@ -0,0 +1,84 @@
/**
* 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 BaseDevice from './BaseDevice';
import type {DeviceOS, DeviceType} from 'flipper-plugin';
export default class ArchivedDevice extends BaseDevice {
isArchived = true;
constructor(options: {
serial: string;
deviceType: DeviceType;
title: string;
os: DeviceOS;
screenshotHandle?: string | null;
source?: string;
supportRequestDetails?: object;
}) {
super(
{
async connect() {},
close() {},
exec(command, ..._args: any[]) {
throw new Error(
`[Archived device] Cannot invoke command ${command} on an archived device`,
);
},
on(event) {
console.warn(
`Cannot subscribe to server events from an Archived device: ${event}`,
);
},
off() {},
},
{
deviceType: options.deviceType,
title: options.title,
os: options.os,
serial: options.serial,
icon: 'box',
features: {
screenCaptureAvailable: false,
screenshotAvailable: false,
},
},
);
this.connected.set(false);
this.source = options.source || '';
this.supportRequestDetails = options.supportRequestDetails;
this.archivedScreenshotHandle = options.screenshotHandle ?? null;
}
archivedScreenshotHandle: string | null;
displayTitle(): string {
return `${this.title} ${this.source ? '(Imported)' : '(Offline)'}`;
}
supportRequestDetails?: object;
getArchivedScreenshotHandle(): string | null {
return this.archivedScreenshotHandle;
}
/**
* @override
*/
async startLogging() {
// No-op
}
/**
* @override
*/
async stopLogging() {
// No-op
}
}

View File

@@ -0,0 +1,378 @@
/**
* 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 {
Device,
_SandyDevicePluginInstance,
_SandyPluginDefinition,
DeviceLogListener,
Idler,
createState,
getFlipperLib,
CrashLogListener,
} from 'flipper-plugin';
import {
DeviceLogEntry,
DeviceOS,
DeviceType,
DeviceDescription,
FlipperServer,
CrashLog,
ServerAddOnControls,
} from 'flipper-common';
import {DeviceSpec, PluginDetails} from 'flipper-common';
import {getPluginKey} from '../utils/pluginKey';
import {Base64} from 'js-base64';
import {createServerAddOnControls} from '../utils/createServerAddOnControls';
type PluginDefinition = _SandyPluginDefinition;
type PluginMap = Map<string, PluginDefinition>;
export type DeviceExport = {
os: DeviceOS;
title: string;
deviceType: DeviceType;
serial: string;
pluginStates: Record<string, any>;
};
export default class BaseDevice implements Device {
description: DeviceDescription;
flipperServer: FlipperServer;
isArchived = false;
hasDevicePlugins = false; // true if there are device plugins for this device (not necessarily enabled)
private readonly serverAddOnControls: ServerAddOnControls;
constructor(flipperServer: FlipperServer, description: DeviceDescription) {
this.flipperServer = flipperServer;
this.description = description;
this.serverAddOnControls = createServerAddOnControls(this.flipperServer);
}
get isConnected(): boolean {
return this.connected.get();
}
// operating system of this device
get os() {
return this.description.os;
}
// human readable name for this device
get title(): string {
return this.description.title;
}
// type of this device
get deviceType() {
return this.description.deviceType;
}
// serial number for this device
get serial() {
return this.description.serial;
}
// additional device specs used for plugin compatibility checks
get specs(): DeviceSpec[] {
return this.description.specs ?? [];
}
// possible src of icon to display next to the device title
get icon() {
return this.description.icon;
}
logListeners: Map<Symbol, DeviceLogListener> = new Map();
crashListeners: Map<Symbol, CrashLogListener> = new Map();
readonly connected = createState(true);
// if imported, stores the original source location
source = '';
// TODO: ideally we don't want BasePlugin to know about the concept of plugins
sandyPluginStates: Map<string, _SandyDevicePluginInstance> = new Map<
string,
_SandyDevicePluginInstance
>();
supportsOS(os: DeviceOS) {
return os.toLowerCase() === this.os.toLowerCase();
}
displayTitle(): string {
return this.connected.get() ? this.title : `${this.title} (Offline)`;
}
async exportState(
idler: Idler,
onStatusMessage: (msg: string) => void,
selectedPlugins: string[],
): Promise<Record<string, any>> {
const pluginStates: Record<string, any> = {};
for (const instance of this.sandyPluginStates.values()) {
if (
selectedPlugins.includes(instance.definition.id) &&
instance.isPersistable()
) {
pluginStates[instance.definition.id] = await instance.exportState(
idler,
onStatusMessage,
);
}
}
return pluginStates;
}
toJSON() {
return {
os: this.os,
title: this.title,
deviceType: this.deviceType,
serial: this.serial,
};
}
private deviceLogEventHandler = (payload: {
serial: string;
entry: DeviceLogEntry;
}) => {
if (payload.serial === this.serial && this.logListeners.size > 0) {
this.addLogEntry(payload.entry);
}
};
addLogEntry(entry: DeviceLogEntry) {
this.logListeners.forEach((listener) => {
// prevent breaking other listeners, if one listener doesn't work.
try {
listener(entry);
} catch (e) {
console.error(`Log listener exception:`, e);
}
});
}
async startLogging() {
this.flipperServer.on('device-log', this.deviceLogEventHandler);
}
stopLogging() {
this.flipperServer.off('device-log', this.deviceLogEventHandler);
}
addLogListener(callback: DeviceLogListener): Symbol {
if (this.logListeners.size === 0) {
this.startLogging();
}
const id = Symbol();
this.logListeners.set(id, callback);
return id;
}
removeLogListener(id: Symbol) {
this.logListeners.delete(id);
if (this.logListeners.size === 0) {
this.stopLogging();
}
}
private crashLogEventHandler = (payload: {
serial: string;
crash: CrashLog;
}) => {
if (payload.serial === this.serial && this.crashListeners.size > 0) {
this.addCrashEntry(payload.crash);
}
};
addCrashEntry(entry: CrashLog) {
this.crashListeners.forEach((listener) => {
// prevent breaking other listeners, if one listener doesn't work.
try {
listener(entry);
} catch (e) {
console.error(`Crash listener exception:`, e);
}
});
}
async startCrashWatcher() {
this.flipperServer.on('device-crash', this.crashLogEventHandler);
}
stopCrashWatcher() {
this.flipperServer.off('device-crash', this.crashLogEventHandler);
}
addCrashListener(callback: CrashLogListener): Symbol {
if (this.crashListeners.size === 0) {
this.startCrashWatcher();
}
const id = Symbol();
this.crashListeners.set(id, callback);
return id;
}
removeCrashListener(id: Symbol) {
this.crashListeners.delete(id);
if (this.crashListeners.size === 0) {
this.stopCrashWatcher();
}
}
async navigateToLocation(location: string) {
return this.flipperServer.exec('device-navigate', this.serial, location);
}
async screenshot(): Promise<Uint8Array | undefined> {
if (!this.description.features.screenshotAvailable || this.isArchived) {
return;
}
return Base64.toUint8Array(
await this.flipperServer.exec('device-take-screenshot', this.serial),
);
}
async startScreenCapture(destination: string): Promise<void> {
return this.flipperServer.exec(
'device-start-screencapture',
this.serial,
destination,
);
}
async stopScreenCapture(): Promise<string | null> {
return this.flipperServer.exec('device-stop-screencapture', this.serial);
}
async executeShell(command: string): Promise<string> {
return this.flipperServer.exec('device-shell-exec', this.serial, command);
}
async sendMetroCommand(command: string): Promise<void> {
return this.flipperServer.exec('metro-command', this.serial, command);
}
async forwardPort(local: string, remote: string): Promise<boolean> {
return this.flipperServer.exec(
'device-forward-port',
this.serial,
local,
remote,
);
}
async clearLogs() {
return this.flipperServer.exec('device-clear-logs', this.serial);
}
supportsPlugin(plugin: PluginDefinition | PluginDetails) {
let pluginDetails: PluginDetails;
if (plugin instanceof _SandyPluginDefinition) {
pluginDetails = plugin.details;
if (!pluginDetails.pluginType && !pluginDetails.supportedDevices) {
// TODO T84453692: this branch is to support plugins defined with the legacy approach. Need to remove this branch after some transition period when
// all the plugins will be migrated to the new approach with static compatibility metadata in package.json.
if (plugin instanceof _SandyPluginDefinition) {
return (
plugin.isDevicePlugin &&
(plugin.asDevicePluginModule().supportsDevice?.(this as any) ??
false)
);
} else {
return (plugin as any).supportsDevice(this);
}
}
} else {
pluginDetails = plugin;
}
return (
pluginDetails.pluginType === 'device' &&
(!pluginDetails.supportedDevices ||
pluginDetails.supportedDevices?.some(
(d) =>
(!d.os || d.os === this.os) &&
(!d.type || d.type === this.deviceType) &&
(d.archived === undefined || d.archived === this.isArchived) &&
(!d.specs || d.specs.every((spec) => this.specs.includes(spec))),
))
);
}
loadDevicePlugins(
devicePlugins: PluginMap,
enabledDevicePlugins: Set<string>,
pluginStates?: Record<string, any>,
) {
if (!devicePlugins) {
return;
}
const plugins = Array.from(devicePlugins.values()).filter((p) =>
enabledDevicePlugins?.has(p.id),
);
for (const plugin of plugins) {
this.loadDevicePlugin(plugin, pluginStates?.[plugin.id]);
}
}
loadDevicePlugin(plugin: PluginDefinition, initialState?: any) {
if (!this.supportsPlugin(plugin)) {
return;
}
this.hasDevicePlugins = true;
if (plugin instanceof _SandyPluginDefinition) {
try {
this.sandyPluginStates.set(
plugin.id,
new _SandyDevicePluginInstance(
this.serverAddOnControls,
getFlipperLib(),
plugin,
this,
// break circular dep, one of those days again...
getPluginKey(undefined, {serial: this.serial}, plugin.id),
initialState,
),
);
} catch (e) {
console.error(`Failed to start device plugin '${plugin.id}': `, e);
}
}
}
unloadDevicePlugin(pluginId: string) {
const instance = this.sandyPluginStates.get(pluginId);
if (instance) {
instance.destroy();
this.sandyPluginStates.delete(pluginId);
}
}
disconnect() {
this.logListeners.clear();
this.stopLogging();
this.crashListeners.clear();
this.stopCrashWatcher();
this.connected.set(false);
}
destroy() {
this.disconnect();
this.sandyPluginStates.forEach((instance) => {
instance.destroy();
});
this.sandyPluginStates.clear();
this.serverAddOnControls.unsubscribe();
}
}

View File

@@ -0,0 +1,12 @@
/**
* 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
*/
export default Object.freeze({
IS_PUBLIC_BUILD: true,
});

View File

@@ -0,0 +1,72 @@
/**
* 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 {assertNever, DownloadFileUpdate} from 'flipper-common';
import {FlipperLib, DownloadFileResponse} from 'flipper-plugin';
import {RenderHost} from '../RenderHost';
export const downloadFileFactory =
(renderHost: RenderHost): FlipperLib['remoteServerContext']['downloadFile'] =>
async (url, dest, {onProgressUpdate, ...options} = {}) => {
const downloadDescriptor = (await renderHost.flipperServer.exec(
'download-file-start',
url,
dest,
options,
// Casting to DownloadFileResponse to add `completed` field to `downloadDescriptor`.
)) as DownloadFileResponse;
let onProgressUpdateWrapped: (progressUpdate: DownloadFileUpdate) => void;
const completed = new Promise<number>((resolve, reject) => {
onProgressUpdateWrapped = (progressUpdate: DownloadFileUpdate) => {
if (progressUpdate.id === downloadDescriptor.id) {
const {status} = progressUpdate;
switch (status) {
case 'downloading': {
onProgressUpdate?.(progressUpdate);
break;
}
case 'success': {
resolve(progressUpdate.downloaded);
break;
}
case 'error': {
reject(
new Error(
`File download failed. Last message: ${JSON.stringify(
progressUpdate,
)}`,
),
);
break;
}
default: {
assertNever(status);
}
}
}
};
renderHost.flipperServer.on(
'download-file-update',
onProgressUpdateWrapped,
);
});
// eslint-disable-next-line promise/catch-or-return
completed.finally(() => {
renderHost.flipperServer.off(
'download-file-update',
onProgressUpdateWrapped,
);
});
downloadDescriptor.completed = completed;
return downloadDescriptor;
};

View File

@@ -0,0 +1,113 @@
/**
* 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 {RemoteServerContext, FlipperLib} from 'flipper-plugin';
import {
BufferEncoding,
ExecOptions,
fsConstants,
Logger,
MkdirOptions,
RmOptions,
} from 'flipper-common';
import constants from '../fb-stubs/constants';
import {RenderHost} from '../RenderHost';
import {downloadFileFactory} from './downloadFile';
import {Base64} from 'js-base64';
export function baseFlipperLibImplementation(
renderHost: RenderHost,
logger: Logger,
): Omit<
FlipperLib,
'enableMenuEntries' | 'selectPlugin' | 'showNotification' | 'createPaste'
> {
return {
isFB: !constants.IS_PUBLIC_BUILD,
logger,
GK: renderHost.GK,
writeTextToClipboard: renderHost.writeTextToClipboard,
openLink: renderHost.openLink,
importFile: renderHost.importFile,
exportFile: renderHost.exportFile,
paths: {
appPath: renderHost.serverConfig.paths.appPath,
homePath: renderHost.serverConfig.paths.homePath,
staticPath: renderHost.serverConfig.paths.staticPath,
tempPath: renderHost.serverConfig.paths.tempPath,
},
environmentInfo: {
os: renderHost.serverConfig.environmentInfo.os,
},
remoteServerContext: {
childProcess: {
exec: async (
command: string,
options?: ExecOptions & {encoding?: BufferEncoding},
) => renderHost.flipperServer.exec('node-api-exec', command, options),
},
fs: {
access: async (path: string, mode?: number) =>
renderHost.flipperServer.exec('node-api-fs-access', path, mode),
pathExists: async (path: string, mode?: number) =>
renderHost.flipperServer.exec('node-api-fs-pathExists', path, mode),
unlink: async (path: string) =>
renderHost.flipperServer.exec('node-api-fs-unlink', path),
mkdir: (async (
path: string,
options?: {recursive?: boolean} & MkdirOptions,
) =>
renderHost.flipperServer.exec(
'node-api-fs-mkdir',
path,
options,
)) as RemoteServerContext['fs']['mkdir'],
rm: async (path: string, options?: RmOptions) =>
renderHost.flipperServer.exec('node-api-fs-rm', path, options),
copyFile: async (src: string, dest: string, flags?: number) =>
renderHost.flipperServer.exec(
'node-api-fs-copyFile',
src,
dest,
flags,
),
constants: fsConstants,
stat: async (path: string) =>
renderHost.flipperServer.exec('node-api-fs-stat', path),
readlink: async (path: string) =>
renderHost.flipperServer.exec('node-api-fs-readlink', path),
readFile: (path, options) =>
renderHost.flipperServer.exec('node-api-fs-readfile', path, options),
readFileBinary: async (path) =>
Base64.toUint8Array(
await renderHost.flipperServer.exec(
'node-api-fs-readfile-binary',
path,
),
),
writeFile: (path, contents, options) =>
renderHost.flipperServer.exec(
'node-api-fs-writefile',
path,
contents,
options,
),
writeFileBinary: async (path, contents) => {
const base64contents = Base64.fromUint8Array(contents);
return await renderHost.flipperServer.exec(
'node-api-fs-writefile-binary',
path,
base64contents,
);
},
},
downloadFile: downloadFileFactory(renderHost),
},
};
}

View File

@@ -0,0 +1,99 @@
/**
* 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 {
ExecuteMessage,
FlipperServer,
ServerAddOnControls,
deserializeRemoteError,
} from 'flipper-common';
type PluginName = string;
type Method = string;
export const createServerAddOnControls = (
flipperServer: FlipperServer,
): ServerAddOnControls => {
const methodHandlers = new Map<
PluginName,
Map<Method, (data: unknown) => void>
>();
const catchAllHandlers = new Map<
PluginName,
(method: string, data: unknown) => void
>();
let subscribed = false;
const subscriptionCb = ({params}: ExecuteMessage) => {
const pluginName = params.api;
const methodHandler = methodHandlers.get(pluginName)?.get(params.method);
if (methodHandler) {
methodHandler(params.params);
return;
}
const catchAllHandler = catchAllHandlers.get(pluginName);
catchAllHandler?.(params.method, params.params);
};
return {
start: (pluginName, details, owner) =>
flipperServer.exec(
'plugins-server-add-on-start',
pluginName,
details,
owner,
),
stop: (pluginName, owner) =>
flipperServer.exec('plugins-server-add-on-stop', pluginName, owner),
sendMessage: async (pluginName, method, params) => {
const res = await flipperServer.exec(
'plugins-server-add-on-request-response',
{
method: 'execute',
params: {
method,
api: pluginName,
params,
},
},
);
if (res.error) {
throw deserializeRemoteError(res.error);
}
return res.success;
},
receiveMessage: (pluginName, method, receiver) => {
if (!methodHandlers.has(pluginName)) {
methodHandlers.set(pluginName, new Map());
}
methodHandlers.get(pluginName)!.set(method, receiver);
// Subscribe client/device to messages from flipper server only when the first plugin subscribes to them
if (!subscribed) {
subscribed = true;
flipperServer.on('plugins-server-add-on-message', subscriptionCb);
}
},
receiveAnyMessage: (pluginName, receiver) => {
catchAllHandlers.set(pluginName, receiver);
},
unsubscribePlugin: (pluginName) => {
methodHandlers.delete(pluginName);
catchAllHandlers.delete(pluginName);
},
unsubscribe: () => {
flipperServer.off('plugins-server-add-on-message', subscriptionCb);
},
};
};

View File

@@ -0,0 +1,26 @@
/**
* 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 {PluginDetails} from 'flipper-common';
import semver from 'semver';
import {getRenderHostInstance} from '../RenderHost';
export function isPluginCompatible(
plugin: PluginDetails,
flipperVersion: string,
) {
return (
getRenderHostInstance().GK('flipper_disable_plugin_compatibility_checks') ||
flipperVersion === '0.0.0' ||
!plugin.engines?.flipper ||
semver.lte(plugin.engines?.flipper, flipperVersion)
);
}
export default isPluginCompatible;

View File

@@ -0,0 +1,54 @@
/**
* 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 {ConcretePluginDetails} from 'flipper-common';
import semver from 'semver';
import isPluginCompatible from './isPluginCompatible';
export function isPluginVersionMoreRecent(
versionDetails: ConcretePluginDetails,
otherVersionDetails: ConcretePluginDetails,
flipperVersion: string,
) {
const isPlugin1Compatible = isPluginCompatible(
versionDetails,
flipperVersion,
);
const isPlugin2Compatible = isPluginCompatible(
otherVersionDetails,
flipperVersion,
);
// prefer compatible plugins
if (isPlugin1Compatible && !isPlugin2Compatible) return true;
if (!isPlugin1Compatible && isPlugin2Compatible) return false;
// prefer plugins with greater version
if (semver.gt(versionDetails.version, otherVersionDetails.version)) {
return true;
}
if (
semver.eq(versionDetails.version, otherVersionDetails.version) &&
versionDetails.isBundled
) {
// prefer bundled versions
return true;
}
if (
semver.eq(versionDetails.version, otherVersionDetails.version) &&
versionDetails.isActivatable &&
!otherVersionDetails.isActivatable
) {
// prefer locally available versions to the versions available remotely on marketplace
return true;
}
return false;
}
export default isPluginVersionMoreRecent;

View File

@@ -0,0 +1,27 @@
/**
* 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
*/
export function getPluginKey(
selectedAppId: string | null | undefined,
baseDevice: {serial: string} | null | undefined,
pluginID: string,
): string {
if (selectedAppId) {
return `${selectedAppId}#${pluginID}`;
}
if (baseDevice) {
// If selected App is not defined, then the plugin is a device plugin
return `${baseDevice.serial}#${pluginID}`;
}
return `unknown#${pluginID}`;
}
export const pluginKey = (serial: string, pluginName: string): string => {
return `${serial}#${pluginName}`;
};