flipper-plugin
Version:
Flipper Desktop plugin SDK and components
344 lines • 13.8 kB
JavaScript
/**
* 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
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BasePluginInstance = exports.registerStorageAtom = exports.getCurrentPluginInstance = exports.setCurrentPluginInstance = void 0;
const eventemitter3_1 = __importDefault(require("eventemitter3"));
const MenuEntry_1 = require("./MenuEntry");
const batch_1 = require("../state/batch");
let currentPluginInstance = undefined;
function setCurrentPluginInstance(instance) {
currentPluginInstance = instance;
}
exports.setCurrentPluginInstance = setCurrentPluginInstance;
function getCurrentPluginInstance() {
return currentPluginInstance;
}
exports.getCurrentPluginInstance = getCurrentPluginInstance;
function registerStorageAtom(key, persistable) {
const pluginInstance = getCurrentPluginInstance();
if (key && pluginInstance) {
const { rootStates } = pluginInstance;
if (rootStates[key]) {
throw new Error(`Some other state is already persisting with key "${key}"`);
}
rootStates[key] = persistable;
}
}
exports.registerStorageAtom = registerStorageAtom;
let staticInstanceId = 1;
class BasePluginInstance {
constructor(serverAddOnControls, flipperLib, definition, device, pluginKey, initialStates) {
this.serverAddOnControls = serverAddOnControls;
this.activated = false;
this.destroyed = false;
this.serverAddOnStarted = false;
this.serverAddOnStopped = false;
this.events = new eventemitter3_1.default();
// all the atoms that should be serialized when making an export / import
this.rootStates = {};
this.menuEntries = [];
this.logListeners = [];
this.crashListeners = [];
this.instanceId = ++staticInstanceId;
this.flipperLib = flipperLib;
this.definition = definition;
this.initialStates = initialStates;
this.pluginKey = pluginKey;
if (!device) {
throw new Error('Illegal State: Device has not yet been loaded');
}
this.device = device;
}
initializePlugin(factory) {
// To be called from constructory
setCurrentPluginInstance(this);
try {
this.instanceApi = (0, batch_1.batched)(factory)();
}
finally {
// check if we have both an import handler and rootStates; probably dev error
if (this.importHandler && Object.keys(this.rootStates).length > 0) {
throw new Error(`A custom onImport handler was defined for plugin '${this.definition.id}', the 'persist' option of states ${Object.keys(this.rootStates).join(', ')} should not be set.`);
}
if (this.initialStates) {
try {
if (this.importHandler) {
(0, batch_1.batched)(this.importHandler)(this.initialStates);
}
else {
for (const key in this.rootStates) {
if (key in this.initialStates) {
this.rootStates[key].deserialize(this.initialStates[key]);
}
else {
console.warn(`Tried to initialize plugin with existing data, however data for "${key}" is missing. Was the export created with a different Flipper version?`);
}
}
}
}
catch (e) {
const msg = `An error occurred when importing data for plugin '${this.definition.id}': '${e}`;
// msg is already specific
// eslint-disable-next-line
console.error(msg, e);
this.events.emit('error', msg);
}
}
this.initialStates = undefined;
setCurrentPluginInstance(undefined);
}
try {
this.events.emit('ready');
}
catch (e) {
const msg = `An error occurred when initializing plugin '${this.definition.id}': '${e}`;
// msg is already specific
// eslint-disable-next-line
console.error(msg, e);
this.events.emit('error', msg);
}
}
createBasePluginClient() {
return {
pluginKey: this.pluginKey,
device: this.device,
onActivate: (cb) => {
const cbWrapped = (0, batch_1.batched)(cb);
this.events.on('activate', cbWrapped);
return () => {
this.events.off('activate', cbWrapped);
};
},
onDeactivate: (cb) => {
const cbWrapped = (0, batch_1.batched)(cb);
this.events.on('deactivate', (0, batch_1.batched)(cb));
return () => {
this.events.off('deactivate', cbWrapped);
};
},
onDeepLink: (cb) => {
const cbWrapped = (0, batch_1.batched)(cb);
this.events.on('deeplink', cbWrapped);
return () => {
this.events.off('deeplink', cbWrapped);
};
},
onDestroy: (cb) => {
this.events.on('destroy', (0, batch_1.batched)(cb));
},
onExport: (cb) => {
if (this.exportHandler) {
throw new Error('onExport handler already set');
}
this.exportHandler = cb;
},
onImport: (cb) => {
if (this.importHandler) {
throw new Error('onImport handler already set');
}
this.importHandler = cb;
},
onReady: (cb) => {
const cbWrapped = (0, batch_1.batched)(cb);
this.events.on('ready', cbWrapped);
return () => {
this.events.off('ready', cbWrapped);
};
},
addMenuEntry: (...entries) => {
for (const entry of entries) {
const normalized = (0, MenuEntry_1.normalizeMenuEntry)(entry);
const idx = this.menuEntries.findIndex((existing) => existing.label === normalized.label ||
existing.action === normalized.action);
if (idx !== -1) {
this.menuEntries[idx] = (0, MenuEntry_1.normalizeMenuEntry)(entry);
}
else {
this.menuEntries.push((0, MenuEntry_1.normalizeMenuEntry)(entry));
}
if (this.activated) {
// entries added after initial registration
this.flipperLib.enableMenuEntries(this.menuEntries);
}
}
},
onDeviceLogEntry: (cb) => {
const handle = this.device.addLogListener(cb);
this.logListeners.push(handle);
return () => {
this.device.removeLogListener(handle);
};
},
onDeviceCrash: (cb) => {
const handle = this.device.addCrashListener(cb);
this.crashListeners.push(handle);
return () => {
this.device.removeCrashListener(handle);
};
},
writeTextToClipboard: this.flipperLib.writeTextToClipboard,
createPaste: this.flipperLib.createPaste,
isFB: this.flipperLib.isFB,
GK: this.flipperLib.GK,
showNotification: (notification) => {
this.flipperLib.showNotification(this.pluginKey, notification);
},
logger: this.flipperLib.logger,
onServerAddOnStart: (cb) => {
const cbWrapped = (0, batch_1.batched)(cb);
this.events.on('serverAddOnStart', cbWrapped);
if (this.serverAddOnStarted) {
cbWrapped();
}
return () => {
this.events.off('serverAddOnStart', cbWrapped);
};
},
onServerAddOnStop: (cb) => {
const cbWrapped = (0, batch_1.batched)(cb);
this.events.on('serverAddOnStop', cbWrapped);
if (this.serverAddOnStopped) {
cbWrapped();
}
return () => {
this.events.off('serverAddOnStop', cbWrapped);
};
},
sendToServerAddOn: (method, params) => this.serverAddOnControls.sendMessage(this.definition.packageName, method, params),
onServerAddOnMessage: (event, cb) => {
this.serverAddOnControls.receiveMessage(this.definition.packageName, event, (0, batch_1.batched)(cb));
},
onServerAddOnUnhandledMessage: (cb) => {
this.serverAddOnControls.receiveAnyMessage(this.definition.packageName, (0, batch_1.batched)(cb));
},
};
}
// the plugin is selected in the UI
activate() {
this.assertNotDestroyed();
if (!this.activated) {
this.flipperLib.enableMenuEntries(this.menuEntries);
this.activated = true;
try {
this.events.emit('activate');
}
catch (e) {
console.error(`Failed to activate plugin: ${this.definition.id}`, e);
}
this.flipperLib.logger.trackTimeSince(`activePlugin-${this.definition.id}`);
}
}
deactivate() {
if (this.destroyed) {
return;
}
if (this.activated) {
this.activated = false;
this.lastDeeplink = undefined;
try {
this.events.emit('deactivate');
}
catch (e) {
console.error(`Failed to deactivate plugin: ${this.definition.id}`, e);
}
}
}
destroy() {
this.assertNotDestroyed();
this.deactivate();
this.logListeners.splice(0).forEach((handle) => {
this.device.removeLogListener(handle);
});
this.crashListeners.splice(0).forEach((handle) => {
this.device.removeCrashListener(handle);
});
this.serverAddOnControls.unsubscribePlugin(this.definition.packageName);
this.events.emit('destroy');
this.destroyed = true;
}
triggerDeepLink(deepLink) {
this.assertNotDestroyed();
if (deepLink !== this.lastDeeplink) {
this.lastDeeplink = deepLink;
// we only want to trigger deeplinks after the plugin had a chance to render
setTimeout(() => {
this.events.emit('deeplink', deepLink);
}, 0);
}
}
exportStateSync() {
// This method is mainly intended for unit testing
if (this.exportHandler) {
throw new Error('Cannot export sync a plugin that does have an export handler');
}
return this.serializeRootStates();
}
serializeRootStates() {
return Object.fromEntries(Object.entries(this.rootStates).map(([key, atom]) => {
try {
return [key, atom.serialize()];
}
catch (e) {
throw new Error(`Failed to serialize state '${key}': ${e}`);
}
}));
}
async exportState(idler, onStatusMessage) {
if (this.exportHandler) {
const result = await this.exportHandler(idler, onStatusMessage);
if (result !== undefined) {
return result;
}
// intentional fall-through, the export handler merely updated the state, but prefers the default export format
}
return this.serializeRootStates();
}
isPersistable() {
return !!this.exportHandler || Object.keys(this.rootStates).length > 0;
}
assertNotDestroyed() {
if (this.destroyed) {
throw new Error('Plugin has been destroyed already');
}
}
startServerAddOn() {
const pluginDetails = this.definition.details;
if (pluginDetails.serverAddOn && pluginDetails.serverAddOnEntry) {
this.serverAddOnControls
.start(pluginDetails.name, { path: pluginDetails.serverAddOnEntry }, this.serverAddOnOwner)
.then(() => {
this.events.emit('serverAddOnStart');
this.serverAddOnStarted = true;
})
.catch((e) => {
console.warn('Failed to start a server add on', pluginDetails.name, this.serverAddOnOwner, e);
});
}
}
stopServerAddOn() {
const { serverAddOn, name } = this.definition.details;
if (serverAddOn) {
this.serverAddOnControls
.stop(name, this.serverAddOnOwner)
.finally(() => {
this.events.emit('serverAddOnStop');
this.serverAddOnStopped = true;
})
.catch((e) => {
console.warn('Failed to stop a server add on', name, this.serverAddOnOwner, e);
});
}
}
}
exports.BasePluginInstance = BasePluginInstance;
//# sourceMappingURL=PluginBase.js.map
;