UNPKG

typhonjs-plugin-manager

Version:

Provides a plugin manager that dispatches events to loaded plugins.

1,253 lines (1,063 loc) 99.1 kB
import path from 'path'; import ObjectUtil from 'typhonjs-object-util'; import EventProxy from 'backbone-esnext-events/src/EventProxy'; import PluginEntry from './PluginEntry.js'; import PluginEvent from './PluginEvent.js'; /** * Provides a lightweight plugin manager for Node / NPM with optional `backbone-esnext-events` * integration for plugins in a safe and protected manner across NPM modules, local files, and preloaded object * instances. This pattern facilitates message passing between modules versus direct dependencies / method invocation. * * It isn't necessary to use an eventbus associated with the plugin manager though invocation then relies on invoking * methods directly with the plugin manager instance. * * When passing in an eventbus from `backbone-esnext-events` the plugin manager will register by default under these * event categories: * * `plugins:add` - {@link PluginManager#add} * * `plugins:add:all` - {@link PluginManager#addAll} * * `plugins:async:add` - {@link PluginManager#addAsync} * * `plugins:async:add:all` - {@link PluginManager#addAllAsync} * * `plugins:async:destroy:manager` - {@link PluginManager#destroyAsync} * * `plugins:async:invoke` - {@link PluginManager#invokeAsync} * * `plugins:async:invoke:event` - {@link PluginManager#invokeAsyncEvent} * * `plugins:async:remove` - {@link PluginManager#removeAsync} * * `plugins:async:remove:all` - {@link PluginManager#removeAllAsync} * * `plugins:create:event:proxy` - {@link PluginManager#createEventProxy} * * `plugins:destroy:manager` - {@link PluginManager#destroy} * * `plugins:get:all:plugin:data` - {@link PluginManager#getAllPluginData} * * `plugins:get:extra:event:data` - {@link PluginManager#getExtraEventData} * * `plugins:get:method:names` - {@link PluginManager#getMethodNames} * * `plugins:get:options` - {@link PluginManager#getOptions} * * `plugins:get:plugin:data` - {@link PluginManager#getPluginData} * * `plugins:get:plugin:enabled` - {@link PluginManager#getPluginEnabled} * * `plugins:get:plugin:event:names` - {@link PluginManager#getPluginEventNames} * * `plugins:get:plugin:method:names` - {@link PluginManager#getPluginMethodNames} * * `plugins:get:plugin:names` - {@link PluginManager#getPluginNames} * * `plugins:get:plugin:options` - {@link PluginManager#getPluginOptions} * * `plugins:get:plugins:enabled` - {@link PluginManager#getPluginsEnabled} * * `plugins:get:plugins:by:event:name` - {@link PluginManager#getPluginsByEventName} * * `plugins:get:plugins:event:names` - {@link PluginManager#getPluginsEventNames} * * `plugins:has:method` - {@link PluginManager#hasMethod} * * `plugins:has:plugin` - {@link PluginManager#hasPlugin} * * `plugins:has:plugin:method` - {@link PluginManager#hasPluginMethod} * * `plugins:invoke` - {@link PluginManager#invoke} * * `plugins:is:valid:config` - {@link PluginManager#isValidConfig} * * `plugins:remove` - {@link PluginManager#remove} * * `plugins:remove:all` - {@link PluginManager#removeAll} * * `plugins:set:extra:event:data` - {@link PluginManager#setExtraEventData} * * `plugins:set:options` - {@link PluginManager#setOptions} * * `plugins:set:plugin:enabled` - {@link PluginManager#setPluginEnabled} * * `plugins:set:plugins:enabled` - {@link PluginManager#setPluginsEnabled} * * `plugins:sync:invoke` - {@link PluginManager#invokeSync} * * `plugins:sync:invoke:event` - {@link PluginManager#invokeSyncEvent} * * Automatically when a plugin is loaded and unloaded respective callbacks `onPluginLoad` and `onPluginUnload` will * be attempted to be invoked on the plugin. This is an opportunity for the plugin to receive any associated eventbus * and wire itself into it. It should be noted that a protected proxy around the eventbus is passed to the plugins * such that when the plugin is removed automatically all events registered on the eventbus are cleaned up without * a plugin author needing to do this manually in the `onPluginUnload` callback. This solves any dangling event binding * issues. * * The plugin manager also supports asynchronous operation with the methods ending in `Async` along with event bindings * that include `async`. For asynchronous variations of `add`, `destroy`, and `remove` the lifecycle methods * `onPluginLoad` and `onPluginUnload` will be awaited on such that if a plugin returns a Promise or is an async method * then it must complete before execution continues. One can use Promises to interact with the plugin manager * asynchronously, but usage via async / await is recommended. * * If eventbus functionality is enabled it is important especially if using a process / global level eventbus such as * `backbone-esnext-eventbus` to call {@link PluginManager#destroy} to clean up all plugin eventbus resources and * the plugin manager event bindings. * * @see https://www.npmjs.com/package/backbone-esnext-events * @see https://www.npmjs.com/package/backbone-esnext-eventbus * * @example * import Events from 'backbone-esnext-events'; // Imports the TyphonEvents class for local usage. * ::or alternatively:: * import eventbus from 'backbone-esnext-eventbus'; // Imports a global / process level eventbus. * * import PluginManager from 'typhonjs-plugin-manager'; * * const pluginManager = new PluginManager({ eventbus }); * * pluginManager.add({ name: 'an-npm-plugin-enabled-module' }); * pluginManager.add({ name: 'my-local-module', target: './myModule.js' }); * * // Let's say an-npm-plugin-enabled-module responds to 'cool:event' which returns 'true'. * // Let's say my-local-module responds to 'hot:event' which returns 'false'. * // Both of the plugin / modules will have 'onPluginLoaded' invoked with a proxy to the eventbus and any plugin * // options defined. * * // One can then use the eventbus functionality to invoke associated module / plugin methods even retrieving results. * assert(eventbus.triggerSync('cool:event') === true); * assert(eventbus.triggerSync('hot:event') === false); * * // One can also indirectly invoke any method of the plugin via: * eventbus.triggerSync('plugins:invoke:sync:event', 'aCoolMethod'); // Any plugin with a method named `aCoolMethod` is invoked. * eventbus.triggerSync('plugins:invoke:sync:event', 'aCoolMethod', {}, {}, 'an-npm-plugin-enabled-module'); // specific invocation. * * // The 3rd parameter will make a copy of the hash and the 4th defines a pass through object hash sending a single * // event / object hash to the invoked method. * * // ----------------------- * * // Given that `backbone-esnext-eventbus` defines a global / process level eventbus you can import it in an entirely * // different file or even NPM module and invoke methods of loaded plugins like this: * * import eventbus from 'backbone-esnext-eventbus'; * * eventbus.triggerSync('plugins:invoke', 'aCoolMethod'); // Any plugin with a method named `aCoolMethod` is invoked. * * assert(eventbus.triggerSync('cool:event') === true); * * eventbus.trigger('plugins:remove', 'an-npm-plugin-enabled-module'); // Removes the plugin and unregisters events. * * assert(eventbus.triggerSync('cool:event') === true); // Will now fail! * * // In this case though when using the global eventbus be mindful to always call `pluginManager.destroy()` in the main * // thread of execution scope to remove all plugins and the plugin manager event bindings! */ export default class PluginManager { /** * Instantiates PluginManager * * @param {object} [options] - Provides various configuration options: * * @param {TyphonEvents} [options.eventbus] - An instance of 'backbone-esnext-events' used as the plugin eventbus. * * @param {string} [options.eventPrepend='plugin'] - A customized name to prepend PluginManager events on the * eventbus. * * @param {boolean} [options.throwNoMethod=false] - If true then when a method fails to be invoked by any plugin * an exception will be thrown. * * @param {boolean} [options.throwNoPlugin=false] - If true then when no plugin is matched to be invoked an * exception will be thrown. * * * @param {object} [extraEventData] - Provides additional optional data to attach to PluginEvent callbacks. */ constructor(options = {}, extraEventData = void 0) { if (typeof options !== 'object') { throw new TypeError(`'options' is not an object.`); } /** * Stores the plugins by name with an associated PluginEntry. * @type {Map<string, PluginEntry>} * @private */ this._pluginMap = new Map(); /** * Stores any associated eventbus. * @type {TyphonEvents} * @private */ this._eventbus = null; /** * Stores any extra options / data to add to PluginEvent callbacks. * @type {Object} * @private */ this._extraEventData = extraEventData; /** * Defines options for throwing exceptions. Turned off by default. * @type {PluginManagerOptions} * @private */ this._options = { pluginsEnabled: true, noEventAdd: false, noEventDestroy: false, noEventOptions: true, noEventRemoval: false, throwNoMethod: false, throwNoPlugin: false }; if (typeof options.eventbus === 'object') { this.setEventbus(options.eventbus, options.eventPrepend); } this.setOptions(options); } /** * Adds a plugin by the given configuration parameters. A plugin `name` is always required. If no other options * are provided then the `name` doubles as the NPM module / local file to load. The loading first checks for an * existing `instance` to use as the plugin. Then the `target` is chosen as the NPM module / local file to load. * By passing in `options` this will be stored and accessible to the plugin during all callbacks. * * @param {PluginConfig} pluginConfig - Defines the plugin to load. * * @param {object} [moduleData] - Optional object hash to associate with plugin. * * @returns {PluginData|undefined} */ add(pluginConfig, moduleData) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof pluginConfig !== 'object') { throw new TypeError(`'pluginConfig' is not an 'object'.`); } if (typeof pluginConfig.name !== 'string') { throw new TypeError(`'pluginConfig.name' is not a 'string' for entry: ${JSON.stringify(pluginConfig)}.`); } if (typeof pluginConfig.target !== 'undefined' && typeof pluginConfig.target !== 'string') { throw new TypeError(`'pluginConfig.target' is not a string for entry: ${JSON.stringify(pluginConfig)}.`); } if (typeof pluginConfig.options !== 'undefined' && typeof pluginConfig.options !== 'object') { throw new TypeError(`'pluginConfig.options' is not an 'object' for entry: ${JSON.stringify(pluginConfig)}.`); } if (typeof moduleData !== 'undefined' && typeof moduleData !== 'object') { throw new TypeError(`'moduleData' is not an 'object' for entry: ${JSON.stringify(pluginConfig)}.`); } // If a plugin with the same name already exists post a warning and exit early. if (this._pluginMap.has(pluginConfig.name)) { // Please note that a plugin or other logger must be setup on the associated eventbus. if (this._eventbus !== null && typeof this._eventbus !== 'undefined') { this._eventbus.trigger('log:warn', `A plugin already exists with name: ${pluginConfig.name}.`); } return void 0; } let instance, target, type; // Use an existing instance of a plugin; a static class is assumed when instance is a function. if (typeof pluginConfig.instance === 'object' || typeof pluginConfig.instance === 'function') { instance = pluginConfig.instance; target = pluginConfig.name; type = 'instance'; } else { // If a target is defined use it instead of the name. target = pluginConfig.target || pluginConfig.name; if (target.match(/^[.\/\\]/)) { instance = require(path.resolve(target)); // eslint-disable global-require type = 'require-path'; } else { instance = require(target); // eslint-disable global-require type = 'require-module'; } } // Create an object hash with data describing the plugin, manager, and any extra module data. const pluginData = JSON.parse(JSON.stringify( { manager: { eventPrepend: this._eventPrepend }, module: moduleData || {}, plugin: { name: pluginConfig.name, scopedName: `${this._eventPrepend}:${pluginConfig.name}`, target, targetEscaped: PluginEntry.escape(target), type, options: pluginConfig.options || {} } })); ObjectUtil.deepFreeze(pluginData, ['eventPrepend', 'scopedName']); const eventProxy = this._eventbus !== null && typeof this._eventbus !== 'undefined' ? new EventProxy(this._eventbus) : void 0; const entry = new PluginEntry(pluginConfig.name, pluginData, instance, eventProxy); this._pluginMap.set(pluginConfig.name, entry); // Invoke private module method which allows skipping optional error checking. s_INVOKE_SYNC_EVENTS('onPluginLoad', {}, {}, this._extraEventData, pluginConfig.name, this._pluginMap, this._options, false); // Invoke `typhonjs:plugin:manager:plugin:added` allowing external code to react to plugin addition. if (this._eventbus) { this._eventbus.trigger(`typhonjs:plugin:manager:plugin:added`, pluginData); } return pluginData; } /** * Adds a plugin by the given configuration parameters. A plugin `name` is always required. If no other options * are provided then the `name` doubles as the NPM module / local file to load. The loading first checks for an * existing `instance` to use as the plugin. Then the `target` is chosen as the NPM module / local file to load. * By passing in `options` this will be stored and accessible to the plugin during all callbacks. * * @param {PluginConfig} pluginConfig - Defines the plugin to load. * * @param {object} [moduleData] - Optional object hash to associate with plugin. * * @returns {Promise<PluginData|undefined>} */ async addAsync(pluginConfig, moduleData) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof pluginConfig !== 'object') { throw new TypeError(`'pluginConfig' is not an 'object'.`); } if (typeof pluginConfig.name !== 'string') { throw new TypeError(`'pluginConfig.name' is not a 'string' for entry: ${JSON.stringify(pluginConfig)}.`); } if (typeof pluginConfig.target !== 'undefined' && typeof pluginConfig.target !== 'string') { throw new TypeError(`'pluginConfig.target' is not a string for entry: ${JSON.stringify(pluginConfig)}.`); } if (typeof pluginConfig.options !== 'undefined' && typeof pluginConfig.options !== 'object') { throw new TypeError(`'pluginConfig.options' is not an 'object' for entry: ${JSON.stringify(pluginConfig)}.`); } if (typeof moduleData !== 'undefined' && typeof moduleData !== 'object') { throw new TypeError(`'moduleData' is not an 'object' for entry: ${JSON.stringify(pluginConfig)}.`); } // If a plugin with the same name already exists post a warning and exit early. if (this._pluginMap.has(pluginConfig.name)) { // Please note that a plugin or other logger must be setup on the associated eventbus. if (this._eventbus !== null && typeof this._eventbus !== 'undefined') { this._eventbus.trigger('log:warn', `A plugin already exists with name: ${pluginConfig.name}.`); } return void 0; } let instance, target, type; // Use an existing instance of a plugin; a static class is assumed when instance is a function. if (typeof pluginConfig.instance === 'object' || typeof pluginConfig.instance === 'function') { instance = pluginConfig.instance; target = pluginConfig.name; type = 'instance'; } else { // If a target is defined use it instead of the name. target = pluginConfig.target || pluginConfig.name; if (target.match(/^[.\/\\]/)) { instance = require(path.resolve(target)); // eslint-disable global-require type = 'require-path'; } else { instance = require(target); // eslint-disable global-require type = 'require-module'; } } // Create an object hash with data describing the plugin, manager, and any extra module data. const pluginData = JSON.parse(JSON.stringify( { manager: { eventPrepend: this._eventPrepend }, module: moduleData || {}, plugin: { name: pluginConfig.name, scopedName: `${this._eventPrepend}:${pluginConfig.name}`, target, targetEscaped: PluginEntry.escape(target), type, options: pluginConfig.options || {} } })); ObjectUtil.deepFreeze(pluginData, ['eventPrepend', 'scopedName']); const eventProxy = this._eventbus !== null && typeof this._eventbus !== 'undefined' ? new EventProxy(this._eventbus) : void 0; const entry = new PluginEntry(pluginConfig.name, pluginData, instance, eventProxy); this._pluginMap.set(pluginConfig.name, entry); // Invoke private module method which allows skipping optional error checking. await s_INVOKE_ASYNC_EVENTS('onPluginLoad', {}, {}, this._extraEventData, pluginConfig.name, this._pluginMap, this._options, false); // Invoke `typhonjs:plugin:manager:plugin:added` allowing external code to react to plugin addition. if (this._eventbus) { await this._eventbus.triggerAsync(`typhonjs:plugin:manager:plugin:added`, pluginData); } return pluginData; } /** * Initializes multiple plugins in a single call. * * @param {Array<PluginConfig>} pluginConfigs - An array of plugin config object hash entries. * * @param {object} [moduleData] - Optional object hash to associate with all plugins. * * @returns {Array<PluginData>} */ addAll(pluginConfigs = [], moduleData) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (!Array.isArray(pluginConfigs)) { throw new TypeError(`'plugins' is not an array.`); } const pluginsData = []; for (const pluginConfig of pluginConfigs) { const result = this.add(pluginConfig, moduleData); if (result) { pluginsData.push(result); } } return pluginsData; } /** * Initializes multiple plugins in a single call. * * @param {Array<PluginConfig>} pluginConfigs - An array of plugin config object hash entries. * * @param {object} [moduleData] - Optional object hash to associate with all plugins. * * @returns {Promise<Array<PluginData>>} */ addAllAsync(pluginConfigs = [], moduleData) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (!Array.isArray(pluginConfigs)) { throw new TypeError(`'plugins' is not an array.`); } const pluginsData = []; for (const pluginConfig of pluginConfigs) { const result = this.addAsync(pluginConfig, moduleData); if (result) { pluginsData.push(result); } } return Promise.all(pluginsData); } /** * Provides the eventbus callback which may prevent addition if optional `noEventAdd` is enabled. This disables * the ability for plugins to be added via events preventing any external code adding plugins in this manner. * * @param {PluginConfig} pluginConfig - Defines the plugin to load. * * @param {object} [moduleData] - Optional object hash to associate with all plugins. * * @returns {PluginData|undefined} - Operation success. * @private */ _addEventbus(pluginConfig, moduleData) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } return !this._options.noEventAdd ? this.add(pluginConfig, moduleData) : void 0; } /** * Provides the eventbus callback which may prevent addition if optional `noEventAdd` is enabled. This disables * the ability for plugins to be added via events preventing any external code adding plugins in this manner. * * @param {PluginConfig} pluginConfig - Defines the plugin to load. * * @param {object} [moduleData] - Optional object hash to associate with all plugins. * * @returns {Promise<PluginData|undefined>} - Operation success. * @private */ _addEventbusAsync(pluginConfig, moduleData) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } return !this._options.noEventAdd ? this.addAsync(pluginConfig, moduleData) : Promise.resolve(void 0); } /** * Provides the eventbus callback which may prevent addition if optional `noEventAdd` is enabled. This disables * the ability for plugins to be added via events preventing any external code adding plugins in this manner. * * @param {Array<PluginConfig>} pluginConfigs - An array of plugin config object hash entries. * * @param {object} [moduleData] - Optional object hash to associate with all plugins. * * @returns {Array<PluginData>} * @private */ _addAllEventbus(pluginConfigs, moduleData) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (!this._options.noEventAdd) { return this.addAll(pluginConfigs, moduleData); } } /** * Provides the eventbus callback which may prevent addition if optional `noEventAdd` is enabled. This disables * the ability for plugins to be added via events preventing any external code adding plugins in this manner. * * @param {Array<PluginConfig>} pluginConfigs - An array of plugin config object hash entries. * * @param {object} [moduleData] - Optional object hash to associate with all plugins. * * @returns {Promise<Array<PluginData>>} * @private */ _addAllEventbusAsync(pluginConfigs, moduleData) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (!this._options.noEventAdd) { return this.addAllAsync(pluginConfigs, moduleData); } } /** * If an eventbus is assigned to this plugin manager then a new EventProxy wrapping this eventbus is returned. * * @returns {EventProxy} */ createEventProxy() { return this._eventbus !== null ? new EventProxy(this._eventbus) : void 0; } /** * Destroys all managed plugins after unloading them. */ destroy() { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } this.removeAll(); if (this._eventbus !== null && typeof this._eventbus !== 'undefined') { this._eventbus.off(`${this._eventPrepend}:add`, this._addEventbus, this); this._eventbus.off(`${this._eventPrepend}:add:all`, this._addAllEventbus, this); this._eventbus.off(`${this._eventPrepend}:async:add`, this._addEventbusAsync, this); this._eventbus.off(`${this._eventPrepend}:async:add:all`, this._addAllEventbusAsync, this); this._eventbus.off(`${this._eventPrepend}:async:destroy:manager`, this._destroyEventbusAsync, this); this._eventbus.off(`${this._eventPrepend}:async:invoke`, this.invokeAsync, this); this._eventbus.off(`${this._eventPrepend}:async:invoke:event`, this.invokeAsyncEvent, this); this._eventbus.off(`${this._eventPrepend}:async:remove`, this._removeEventbusAsync, this); this._eventbus.off(`${this._eventPrepend}:async:remove:all`, this._removeAllEventbusAsync, this); this._eventbus.off(`${this._eventPrepend}:create:event:proxy`, this.createEventProxy, this); this._eventbus.off(`${this._eventPrepend}:destroy:manager`, this._destroyEventbus, this); this._eventbus.off(`${this._eventPrepend}:get:all:plugin:data`, this.getAllPluginData, this); this._eventbus.off(`${this._eventPrepend}:get:extra:event:data`, this.getExtraEventData, this); this._eventbus.off(`${this._eventPrepend}:get:method:names`, this.getMethodNames, this); this._eventbus.off(`${this._eventPrepend}:get:options`, this.getOptions, this); this._eventbus.off(`${this._eventPrepend}:get:plugin:enabled`, this.getPluginEnabled, this); this._eventbus.off(`${this._eventPrepend}:get:plugin:data`, this.getPluginData, this); this._eventbus.off(`${this._eventPrepend}:get:plugin:event:names`, this.getPluginEventNames, this); this._eventbus.off(`${this._eventPrepend}:get:plugin:method:names`, this.getPluginMethodNames, this); this._eventbus.off(`${this._eventPrepend}:get:plugin:names`, this.getPluginNames, this); this._eventbus.off(`${this._eventPrepend}:get:plugin:options`, this.getPluginOptions, this); this._eventbus.off(`${this._eventPrepend}:get:plugins:enabled`, this.getPluginsEnabled, this); this._eventbus.off(`${this._eventPrepend}:get:plugins:by:event:name`, this.getPluginsByEventName, this); this._eventbus.off(`${this._eventPrepend}:get:plugins:event:names`, this.getPluginsEventNames, this); this._eventbus.off(`${this._eventPrepend}:has:method`, this.hasMethod, this); this._eventbus.off(`${this._eventPrepend}:has:plugin`, this.hasPlugin, this); this._eventbus.off(`${this._eventPrepend}:has:plugin:method`, this.hasPluginMethod, this); this._eventbus.off(`${this._eventPrepend}:invoke`, this.invoke, this); this._eventbus.off(`${this._eventPrepend}:is:valid:config`, this.isValidConfig, this); this._eventbus.off(`${this._eventPrepend}:remove`, this._removeEventbus, this); this._eventbus.off(`${this._eventPrepend}:remove:all`, this._removeAllEventbus, this); this._eventbus.off(`${this._eventPrepend}:set:extra:event:data`, this.setExtraEventData, this); this._eventbus.off(`${this._eventPrepend}:set:options`, this._setOptionsEventbus, this); this._eventbus.off(`${this._eventPrepend}:set:plugin:enabled`, this.setPluginEnabled, this); this._eventbus.off(`${this._eventPrepend}:set:plugins:enabled`, this.setPluginsEnabled, this); this._eventbus.off(`${this._eventPrepend}:sync:invoke`, this.invokeSync, this); this._eventbus.off(`${this._eventPrepend}:sync:invoke:event`, this.invokeSyncEvent, this); } this._pluginMap = null; this._eventbus = null; } /** * Destroys all managed plugins after unloading them. */ async destroyAsync() { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } await this.removeAll(); if (this._eventbus !== null && typeof this._eventbus !== 'undefined') { this._eventbus.off(`${this._eventPrepend}:add`, this._addEventbus, this); this._eventbus.off(`${this._eventPrepend}:add:all`, this._addAllEventbus, this); this._eventbus.off(`${this._eventPrepend}:async:add`, this._addEventbusAsync, this); this._eventbus.off(`${this._eventPrepend}:async:add:all`, this._addAllEventbusAsync, this); this._eventbus.off(`${this._eventPrepend}:async:destroy:manager`, this._destroyEventbusAsync, this); this._eventbus.off(`${this._eventPrepend}:async:invoke`, this.invokeAsync, this); this._eventbus.off(`${this._eventPrepend}:async:invoke:event`, this.invokeAsyncEvent, this); this._eventbus.off(`${this._eventPrepend}:async:remove`, this._removeEventbusAsync, this); this._eventbus.off(`${this._eventPrepend}:async:remove:all`, this._removeAllEventbusAsync, this); this._eventbus.off(`${this._eventPrepend}:create:event:proxy`, this.createEventProxy, this); this._eventbus.off(`${this._eventPrepend}:destroy:manager`, this._destroyEventbus, this); this._eventbus.off(`${this._eventPrepend}:get:all:plugin:data`, this.getAllPluginData, this); this._eventbus.off(`${this._eventPrepend}:get:extra:event:data`, this.getExtraEventData, this); this._eventbus.off(`${this._eventPrepend}:get:method:names`, this.getMethodNames, this); this._eventbus.off(`${this._eventPrepend}:get:options`, this.getOptions, this); this._eventbus.off(`${this._eventPrepend}:get:plugin:enabled`, this.getPluginEnabled, this); this._eventbus.off(`${this._eventPrepend}:get:plugin:data`, this.getPluginData, this); this._eventbus.off(`${this._eventPrepend}:get:plugin:event:names`, this.getPluginEventNames, this); this._eventbus.off(`${this._eventPrepend}:get:plugin:method:names`, this.getPluginMethodNames, this); this._eventbus.off(`${this._eventPrepend}:get:plugin:names`, this.getPluginNames, this); this._eventbus.off(`${this._eventPrepend}:get:plugin:options`, this.getPluginOptions, this); this._eventbus.off(`${this._eventPrepend}:get:plugins:enabled`, this.getPluginsEnabled, this); this._eventbus.off(`${this._eventPrepend}:get:plugins:by:event:name`, this.getPluginsByEventName, this); this._eventbus.off(`${this._eventPrepend}:get:plugins:event:names`, this.getPluginsEventNames, this); this._eventbus.off(`${this._eventPrepend}:has:method`, this.hasMethod, this); this._eventbus.off(`${this._eventPrepend}:has:plugin`, this.hasPlugin, this); this._eventbus.off(`${this._eventPrepend}:has:plugin:method`, this.hasPluginMethod, this); this._eventbus.off(`${this._eventPrepend}:invoke`, this.invoke, this); this._eventbus.off(`${this._eventPrepend}:is:valid:config`, this.isValidConfig, this); this._eventbus.off(`${this._eventPrepend}:remove`, this._removeEventbus, this); this._eventbus.off(`${this._eventPrepend}:remove:all`, this._removeAllEventbus, this); this._eventbus.off(`${this._eventPrepend}:set:extra:event:data`, this.setExtraEventData, this); this._eventbus.off(`${this._eventPrepend}:set:options`, this._setOptionsEventbus, this); this._eventbus.off(`${this._eventPrepend}:set:plugin:enabled`, this.setPluginEnabled, this); this._eventbus.off(`${this._eventPrepend}:set:plugins:enabled`, this.setPluginsEnabled, this); this._eventbus.off(`${this._eventPrepend}:sync:invoke`, this.invokeSync, this); this._eventbus.off(`${this._eventPrepend}:sync:invoke:event`, this.invokeSyncEvent, this); } this._pluginMap = null; this._eventbus = null; } /** * Provides the eventbus callback which may prevent plugin manager destruction if optional `noEventDestroy` is * enabled. This disables the ability for the plugin manager to be destroyed via events preventing any external * code removing plugins in this manner. * * @private */ _destroyEventbus() { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (!this._options.noEventDestroy) { this.destroy(); } } /** * Provides the eventbus callback which may prevent plugin manager destruction if optional `noEventDestroy` is * enabled. This disables the ability for the plugin manager to be destroyed via events preventing any external * code removing plugins in this manner. * * @private */ async _destroyEventbusAsync() { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (!this._options.noEventDestroy) { await this.destroyAsync(); } } /** * Returns the enabled state of a plugin. * * @param {string} pluginName - Plugin name to set state. * * @returns {boolean} - Operation success. */ getPluginEnabled(pluginName) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof pluginName !== 'string') { throw new TypeError(`'pluginName' is not a string.`); } const entry = this._pluginMap.get(pluginName); return entry instanceof PluginEntry && entry.enabled; } /** * Returns the event binding names registered on any associated plugin EventProxy. * * @param {string} pluginName - Plugin name to set state. * * @returns {string[]} - Event binding names registered from the plugin. */ getPluginEventNames(pluginName) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof pluginName !== 'string') { throw new TypeError(`'pluginName' is not a string.`); } const entry = this._pluginMap.get(pluginName); return entry instanceof PluginEntry && entry._eventProxy ? entry._eventProxy.getEventNames() : []; } /** * Returns the enabled state of a list of plugins. * * @param {Array<string>} pluginNames - An array / iterable of plugin names. * * @returns {Array<{pluginName: string, enabled: boolean}>} A list of objects with plugin name and enabled state. */ getPluginsEnabled(pluginNames) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } const results = []; for (const pluginName of pluginNames) { results.push({ pluginName, enabled: this.getPluginEnabled(pluginName) }); } return results; } /** * Returns the event binding names registered from each plugin. * * @param {string|string[]} [nameOrList] - An array / iterable of plugin names. * * @returns {Array<{pluginName: string, events: string[]}>} A list of objects with plugin name and event binding * names registered from the plugin. */ getPluginsEventNames(nameOrList) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof nameOrList === 'undefined') { nameOrList = this._pluginMap.keys(); } if (typeof nameOrList === 'string') { nameOrList = [nameOrList]; } const results = []; for (const pluginName of nameOrList) { results.push({ pluginName, events: this.getPluginEventNames(pluginName) }); } return results; } /** * Returns the plugin names that registered the given event binding name. * * @param {string} eventName - An event name that plugins may have registered. * * @returns {Array<string[]>} A list of plugin names that has registered the given event name. */ getPluginsByEventName(eventName) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof eventName !== 'string') { throw new TypeError(`'eventName' is not a 'string'.`); } const results = []; const pluginEventNames = this.getPluginsEventNames(); for (const entry of pluginEventNames) { if (entry.events.indexOf(eventName) >= 0) { results.push(entry.pluginName); } } return results; } /** * Returns all plugin data or if a boolean is passed in will return plugin data by current enabled state. * * @param {boolean|undefined} enabled - If enabled is a boolean it will return plugins given their enabled state. * * @returns {Array<PluginData>} */ getAllPluginData(enabled = void 0) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof enabled !== 'boolean' && typeof enabled !== 'undefined') { throw new TypeError(`'enabled' is not a 'boolean' or 'undefined'.`); } const results = []; // Return all plugin data if enabled is not defined. const allPlugins = typeof enabled === 'undefined'; for (const entry of this._pluginMap.values()) { if (allPlugins || entry.enabled === enabled) { results.push(this.getPluginData(entry.name)); } } return results; } /** * Returns any associated eventbus. * * @returns {TyphonEvents|null} */ getEventbus() { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } return this._eventbus; } /** * Returns any extra event data associated with PluginEvents. * * @returns {*} */ getExtraEventData() { return this._extraEventData; } /** * Returns all method names or if a boolean is passed in will return method names for plugins by current enabled * state. * * @param {boolean|undefined} enabled - If enabled is a boolean it will return plugin methods names given their * enabled state. * * @param {string|undefined} pluginName - If a string then just this plugins methods names are returned. * * @returns {Array<string>} */ getMethodNames(enabled = void 0, pluginName = void 0) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof enabled !== 'boolean' && typeof enabled !== 'undefined') { throw new TypeError(`'enabled' is not a 'boolean' or 'undefined'.`); } const results = {}; const allEnabled = typeof enabled === 'undefined'; const allNames = typeof pluginName === 'undefined'; for (const entry of this._pluginMap.values()) { if (entry.instance && (allEnabled || entry.enabled === enabled) && (allNames || entry.name === pluginName)) { for (const name of s_GET_ALL_PROPERTY_NAMES(entry.instance)) { // Skip any names that are not a function or are the constructor. if (entry.instance[name] instanceof Function && name !== 'constructor') { results[name] = true; } } } } return Object.keys(results); } /** * Returns a copy of the plugin manager options. * * @returns {PluginManagerOptions} */ getOptions() { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } return JSON.parse(JSON.stringify(this._options)); } /** * Gets the plugin data for a plugin by name. * * @param {string} pluginName - A plugin name. * * @returns {PluginData|undefined} */ getPluginData(pluginName) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof pluginName !== 'string') { throw new TypeError(`'pluginName' is not a string.`); } const entry = this._pluginMap.get(pluginName); if (entry instanceof PluginEntry) { return JSON.parse(JSON.stringify(entry.data)); } return void 0; } /** * Returns all plugin names or if a boolean is passed in will return plugin names by current enabled state. * * @param {boolean|undefined} enabled - If enabled is a boolean it will return plugins given their enabled state. * * @returns {Array<{plugin: string, method: string}>} */ getPluginMethodNames(enabled = void 0) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof enabled !== 'boolean' && typeof enabled !== 'undefined') { throw new TypeError(`'enabled' is not a 'boolean' or 'undefined'.`); } const results = []; const allPlugins = typeof enabled === 'undefined'; for (const entry of this._pluginMap.values()) { if (entry.instance && (allPlugins || entry.enabled === enabled)) { for (const name of s_GET_ALL_PROPERTY_NAMES(entry.instance)) { // Skip any names that are not a function or are the constructor. if (entry.instance[name] instanceof Function && name !== 'constructor') { results.push({ plugin: entry.name, method: name }); } } } } return results; } /** * Returns all plugin names or if a boolean is passed in will return plugin names by current enabled state. * * @param {boolean|undefined} enabled - If enabled is a boolean it will return plugins given their enabled state. * * @returns {Array<string>} */ getPluginNames(enabled = void 0) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof enabled !== 'boolean' && typeof enabled !== 'undefined') { throw new TypeError(`'enabled' is not a 'boolean' or 'undefined'.`); } // Return all plugin names if enabled is not defined. if (enabled === void 0) { return Array.from(this._pluginMap.keys()); } const results = []; for (const entry of this._pluginMap.values()) { if (entry.enabled === enabled) { results.push(entry.name); } } return results; } /** * Returns a copy of the given plugin options. * * @param {string} pluginName - Plugin name to retrieve. * * @returns {*} */ getPluginOptions(pluginName) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof pluginName !== 'string') { throw new TypeError(`'pluginName' is not a string.`); } let result; const entry = this._pluginMap.get(pluginName); if (entry instanceof PluginEntry) { result = JSON.parse(JSON.stringify(entry.data.plugin.options)); } return result; } /** * Returns true if there is at least one plugin loaded with the given method name. * * @param {string} methodName - Method name to test. * * @returns {boolean} - True method is found. */ hasMethod(methodName) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof methodName !== 'string') { throw new TypeError(`'methodName' is not a string.`); } for (const plugin of this._pluginMap.values()) { if (typeof plugin.instance[methodName] === 'function') { return true; } } return false; } /** * Returns true if there is a plugin loaded with the given plugin name. * * @param {string} pluginName - Plugin name to test. * * @returns {boolean} - True if a plugin exists. */ hasPlugin(pluginName) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof pluginName !== 'string') { throw new TypeError(`'pluginName' is not a string.`); } return this._pluginMap.has(pluginName); } /** * Returns true if there is a plugin loaded with the given plugin name that also has a method with the given * method name. * * @param {string} pluginName - Plugin name to test. * @param {string} methodName - Method name to test. * * @returns {boolean} - True if a plugin and method exists. */ hasPluginMethod(pluginName, methodName) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof pluginName !== 'string') { throw new TypeError(`'pluginName' is not a string.`); } if (typeof methodName !== 'string') { throw new TypeError(`'methodName' is not a string.`); } const plugin = this._pluginMap.get(pluginName); return plugin instanceof PluginEntry && typeof plugin[methodName] === 'function'; } /** * This dispatch method simply invokes any plugin targets for the given methodName.. * * @param {string} methodName - Method name to invoke. * * @param {*|Array<*>} [args] - Optional arguments. An array will be spread as multiple arguments. * * @param {string|Array<string>} [nameOrList] - An optional plugin name or array / iterable of plugin names to * invoke. */ invoke(methodName, args = void 0, nameOrList = void 0) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof methodName !== 'string') { throw new TypeError(`'methodName' is not a string.`); } if (typeof nameOrList === 'undefined') { nameOrList = this._pluginMap.keys(); } if (typeof nameOrList !== 'string' && !Array.isArray(nameOrList) && typeof nameOrList[Symbol.iterator] !== 'function') { throw new TypeError(`'nameOrList' is not a string, array, or iterator.`); } // Track if a plugin method is invoked. let hasMethod = false; let hasPlugin = false; // Early out if plugins are not enabled. if (!this._options.pluginsEnabled) { return; } if (typeof nameOrList === 'string') { const plugin = this._pluginMap.get(nameOrList); if (plugin instanceof PluginEntry && plugin.enabled && plugin.instance) { hasPlugin = true; if (typeof plugin.instance[methodName] === 'function') { Array.isArray(args) ? plugin.instance[methodName](...args) : plugin.instance[methodName](args); hasMethod = true; } } } else { for (const name of nameOrList) { const plugin = this._pluginMap.get(name); if (plugin instanceof PluginEntry && plugin.enabled && plugin.instance) { hasPlugin = true; if (typeof plugin.instance[methodName] === 'function') { Array.isArray(args) ? plugin.instance[methodName](...args) : plugin.instance[methodName](args); hasMethod = true; } } } } if (this._options.throwNoPlugin && !hasPlugin) { throw new Error(`PluginManager failed to find any target plugins.`); } if (this._options.throwNoMethod && !hasMethod) { throw new Error(`PluginManager failed to invoke '${methodName}'.`); } } /** * This dispatch method uses ES6 Promises and adds any returned results to an array which is added to a Promise.all * construction which passes back a Promise which waits until all Promises complete. Any target invoked may return a * Promise or any result. This is very useful to use for any asynchronous operations. * * @param {string} methodName - Method name to invoke. * * @param {*|Array<*>} [args] - Optional arguments. An array will be spread as multiple arguments. * * @param {string|Array<string>} [nameOrList] - An optional plugin name or array / iterable of plugin names to * invoke. * * @returns {Promise<*|Array<*>>} */ invokeAsync(methodName, args = void 0, nameOrList = void 0) { if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); } if (typeof methodName !== 'string') { throw new TypeError(`'methodName' is not a string.`); } if (typeof nameOrList === 'undefined') { nameOrList = this._pluginMap.keys(); } if (typeof nameOrList !== 'string' && !Array.isArray(nameOrList) && typeof nameOrList[Symbol.iterator] !== 'function') { throw new TypeError(`'nameOrList' is not a string, array, or iterator.`); } // Track if a plugin method is invoked. let hasMethod = false; let hasPlugin = false; // Capture results. let result = void 0; const results = []; // Early out if plugins are not enabled. if (!this._options.pluginsEnabled) { return result; } try { if (typeof nameOrList === 'string') { const plugin = this._pluginMap.get(nameOrList); if (plugin instanceof PluginEntry && plugin.enabled && plugin.instance) { hasPlugin = true;