UNPKG

flipper-plugin

Version:

Flipper Desktop plugin SDK and components

344 lines 13.8 kB
"use strict"; /** * 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