src/PluginManager.js
import path from 'path';
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: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:invoke:async` - {@link PluginManager#invokeAsync}
*
* `plugins:invoke:sync` - {@link PluginManager#invokeSync}
*
* `plugins:invoke:sync:event` - {@link PluginManager#invokeSyncEvent}
*
* `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:plugin:enabled` - {@link PluginManager#setPluginEnabled}
*
* `plugins:set:plugins:enabled` - {@link PluginManager#setPluginsEnabled}
*
* 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.
*
* 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,
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.
*
* @returns {PluginData|undefined}
*/
add(pluginConfig)
{
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 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';
}
}
const eventProxy = this._eventbus !== null && typeof this._eventbus !== 'undefined' ?
new EventProxy(this._eventbus) : void 0;
const entry = new PluginEntry(pluginConfig.name, type, target, instance, eventProxy, pluginConfig.options);
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);
const pluginData =
{
name: entry.name,
scopedName: `${this._eventPrepend}:${entry.name}`,
target: entry.target,
targetEscaped: entry.targetEscaped,
type: entry.type,
options: JSON.parse(JSON.stringify(entry.options)),
managerEventPrepend: this._eventPrepend
};
// 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;
}
/**
* Initializes multiple plugins in a single call.
*
* @param {Array<PluginConfig>} pluginConfigs - An array of plugin config object hash entries.
*
* @returns {Array<PluginData>}
*/
addAll(pluginConfigs = [])
{
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);
if (result) { pluginsData.push(result); }
}
return 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 {string} pluginName - The plugin name to remove.
*
* @returns {boolean} - Operation success.
* @private
*/
_addEventbus(pluginName)
{
if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); }
return !this._options.noEventAdd ? this.add(pluginName) : false;
}
/**
* 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.
*
* @returns {Array<PluginData>}
* @private
*/
_addAllEventbus(pluginConfigs)
{
if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); }
if (!this._options.noEventAdd) { return this.addAll(pluginConfigs); }
}
/**
* 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}: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}:invoke:async`, this.invokeAsync, this);
this._eventbus.off(`${this._eventPrepend}:invoke:sync`, this.invokeSync, this);
this._eventbus.off(`${this._eventPrepend}:invoke:sync:event`, this.invokeSyncEvent, 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:plugin:enabled`, this.setPluginEnabled, this);
this._eventbus.off(`${this._eventPrepend}:set:plugins:enabled`, this.setPluginsEnabled, this);
}
this._pluginMap = null;
this._eventbus = null;
}
/**
* Provides the eventbus callback which may prevent plugin mananger destruction if optional `noEventDestroy` is
* enabled. This disables the ability for the plugin mananger 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(); }
}
/**
* 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 plugin of this._pluginMap.values())
{
if (plugin.instance && (allEnabled || plugin.enabled === enabled) && (allNames || plugin.name === pluginName))
{
for (const name of s_GET_ALL_PROPERTY_NAMES(plugin.instance))
{
// Skip any names that are not a function or are the constructor.
if (plugin.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 {
name: entry.name,
scopedName: `${this._eventPrepend}:${entry.name}`,
target: entry.target,
targetEscaped: entry.targetEscaped,
type: entry.type,
options: JSON.parse(JSON.stringify(entry.options)),
managerEventPrepend: this._eventPrepend
};
}
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 plugin of this._pluginMap.values())
{
if (plugin.instance && (allPlugins || plugin.enabled === enabled))
{
for (const name of s_GET_ALL_PROPERTY_NAMES(plugin.instance))
{
// Skip any names that are not a function or are the constructor.
if (plugin.instance[name] instanceof Function && name !== 'constructor')
{
results.push({ plugin: plugin.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 plugin of this._pluginMap.values())
{
if (plugin.enabled === enabled) { results.push(plugin.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.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 {*|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;
if (typeof plugin.instance[methodName] === 'function')
{
result = Array.isArray(args) ? plugin.instance[methodName](...args) :
plugin.instance[methodName](args);
// If we received a valid result return immediately.
if (result !== null || typeof result !== 'undefined') { results.push(result); }
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')
{
result = Array.isArray(args) ? plugin.instance[methodName](...args) :
plugin.instance[methodName](args);
// If we received a valid result return immediately.
if (result !== null || typeof result !== 'undefined') { results.push(result); }
hasMethod = true;
}
}
}
}
if (this._options.throwNoPlugin && !hasPlugin)
{
return Promise.reject(new Error(`PluginManager failed to find any target plugins.`));
}
if (this._options.throwNoMethod && !hasMethod)
{
return Promise.reject(new Error(`PluginManager failed to invoke '${methodName}'.`));
}
}
catch (error)
{
return Promise.reject(error);
}
// If there are multiple results then use Promise.all otherwise Promise.resolve.
return results.length > 1 ? Promise.all(results) : Promise.resolve(result);
}
/**
* This dispatch method synchronously passes back a single value or an array with all results returned by any
* invoked targets.
*
* @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 {*|Array<*>}
*/
invokeSync(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; }
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')
{
result = Array.isArray(args) ? plugin.instance[methodName](...args) : plugin.instance[methodName](args);
// If we received a valid result return immediately.
if (result !== null || typeof result !== 'undefined') { results.push(result); }
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')
{
result = Array.isArray(args) ? plugin.instance[methodName](...args) :
plugin.instance[methodName](args);
// If we received a valid result return immediately.
if (result !== null || typeof result !== 'undefined') { results.push(result); }
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}'.`);
}
// Return the results array if there are more than one or just a single result.
return results.length > 1 ? results : result;
}
/**
* This dispatch method synchronously passes to and returns from any invoked targets a PluginEvent.
*
* @param {string} methodName - Method name to invoke.
*
* @param {object} [copyProps={}] - plugin event object.
*
* @param {object} [passthruProps={}] - if true, event has plugin option.
*
* @param {string|Array<string>} [nameOrList] - An optional plugin name or array / iterable of plugin names to
* invoke.
*
* @returns {PluginEvent|undefined}
*/
invokeSyncEvent(methodName, copyProps = {}, passthruProps = {}, nameOrList = void 0)
{
if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); }
if (typeof nameOrList === 'undefined') { nameOrList = this._pluginMap.keys(); }
// Early out if plugins are not enabled.
if (!this._options.pluginsEnabled) { return void 0; }
// Invokes the private internal sync events method with optional error checking enabled.
return s_INVOKE_SYNC_EVENTS(methodName, copyProps, passthruProps, this._extraEventData, nameOrList,
this._pluginMap, this._options);
}
/**
* Performs validation of a PluginConfig.
*
* @param {PluginConfig} pluginConfig - A PluginConfig to validate.
*
* @returns {boolean} True if the given PluginConfig is valid.
*/
isValidConfig(pluginConfig)
{
if (typeof pluginConfig !== 'object') { return false; }
if (typeof pluginConfig.name !== 'string') { return false; }
if (typeof pluginConfig.target !== 'undefined' && typeof pluginConfig.target !== 'string') { return false; }
if (typeof pluginConfig.options !== 'undefined' && typeof pluginConfig.options !== 'object') { return false; }
return true;
}
/**
* Sets the eventbus associated with this plugin manager. If any previous eventbus was associated all plugin manager
* events will be removed then added to the new eventbus. If there are any existing plugins being managed their
* events will be removed from the old eventbus and then `onPluginLoad` will be called with the new eventbus.
*
* @param {TyphonEvents} targetEventbus - The target eventbus to associate.
*
* @param {string} [eventPrepend='plugins'] - An optional string to prepend to all of the event binding
* targets.
*
* @returns {PluginManager}
*/
setEventbus(targetEventbus, eventPrepend = 'plugins')
{
if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); }
if (typeof targetEventbus !== 'object') { throw new TypeError(`'targetEventbus' is not an 'object'.`); }
if (typeof eventPrepend !== 'string') { throw new TypeError(`'eventPrepend' is not a 'string'.`); }
// Early escape if the targetEventbus is the same as the current eventbus.
if (targetEventbus === this._eventbus) { return this; }
const oldPrepend = this._eventPrepend;
/**
* Stores the prepend string for eventbus registration.
* @type {string}
* @private
*/
this._eventPrepend = eventPrepend;
// Unload and reload any existing plugins from the old eventbus to the target eventbus.
if (this._pluginMap.size > 0)
{
// Invoke private module method which allows skipping optional error checking.
s_INVOKE_SYNC_EVENTS('onPluginUnload', {}, {}, this._extraEventData, this._pluginMap.keys(), this._pluginMap,
this._options, false);
for (const entry of this._pluginMap.values())
{
if (entry.eventProxy instanceof EventProxy) { entry.eventProxy.destroy(); }
entry.eventProxy = new EventProxy(targetEventbus);
}
// Invoke private module method which allows skipping optional error checking.
s_INVOKE_SYNC_EVENTS('onPluginLoad', {}, {}, this._extraEventData, this._pluginMap.keys(), this._pluginMap,
this._options, false);
for (const entry of this._pluginMap.values())
{
// Invoke `typhonjs:plugin:manager:eventbus:changed` allowing external code to react to plugin
// changing eventbus.
if (this._eventbus)
{
this._eventbus.trigger(`typhonjs:plugin:manager:eventbus:changed`,
{
name: entry.name,
target: entry.target,
targetEscaped: entry.targetEscaped,
type: entry.type,
options: JSON.parse(JSON.stringify(entry.options)),
oldEventbus: this._eventbus,
oldManagerEventPrepend: oldPrepend,
oldScopedName: `${oldPrepend}:${entry.name}`,
newEventbus: targetEventbus,
newManagerEventPrepend: eventPrepend,
newScopedName: `${eventPrepend}:${entry.name}`
});
}
}
}
if (this._eventbus !== null)
{
this._eventbus.off(`${oldPrepend}:add`, this._addEventbus, this);
this._eventbus.off(`${oldPrepend}:add:all`, this._addAllEventbus, this);
this._eventbus.off(`${oldPrepend}:create:event:proxy`, this.createEventProxy, this);
this._eventbus.off(`${oldPrepend}:destroy:manager`, this._destroyEventbus, this);
this._eventbus.off(`${oldPrepend}:get:all:plugin:data`, this.getAllPluginData, this);
this._eventbus.off(`${oldPrepend}:get:extra:event:data`, this.getExtraEventData, this);
this._eventbus.off(`${oldPrepend}:get:method:names`, this.getMethodNames, this);
this._eventbus.off(`${oldPrepend}:get:options`, this.getOptions, this);
this._eventbus.off(`${oldPrepend}:get:plugin:enabled`, this.getPluginEnabled, this);
this._eventbus.off(`${oldPrepend}:get:plugin:data`, this.getPluginData, this);
this._eventbus.off(`${oldPrepend}:get:plugin:event:names`, this.getPluginEventNames, this);
this._eventbus.off(`${oldPrepend}:get:plugin:method:names`, this.getPluginMethodNames, this);
this._eventbus.off(`${oldPrepend}:get:plugin:names`, this.getPluginNames, this);
this._eventbus.off(`${oldPrepend}:get:plugin:options`, this.getPluginOptions, this);
this._eventbus.off(`${oldPrepend}:get:plugins:enabled`, this.getPluginsEnabled, this);
this._eventbus.off(`${oldPrepend}:get:plugins:by:event:name`, this.getPluginsByEventName, this);
this._eventbus.off(`${oldPrepend}:get:plugins:event:names`, this.getPluginsEventNames, this);
this._eventbus.off(`${oldPrepend}:has:method`, this.hasMethod, this);
this._eventbus.off(`${oldPrepend}:has:plugin`, this.hasPlugin, this);
this._eventbus.off(`${oldPrepend}:has:plugin:method`, this.hasPluginMethod, this);
this._eventbus.off(`${oldPrepend}:invoke`, this.invoke, this);
this._eventbus.off(`${oldPrepend}:invoke:async`, this.invokeAsync, this);
this._eventbus.off(`${oldPrepend}:invoke:sync`, this.invokeSync, this);
this._eventbus.off(`${oldPrepend}:invoke:sync:event`, this.invokeSyncEvent, this);
this._eventbus.off(`${oldPrepend}:is:valid:config`, this.isValidConfig, this);
this._eventbus.off(`${oldPrepend}:remove`, this._removeEventbus, this);
this._eventbus.off(`${oldPrepend}:remove:all`, this._removeAllEventbus, this);
this._eventbus.off(`${oldPrepend}:set:extra:event:data`, this.setExtraEventData, this);
this._eventbus.off(`${oldPrepend}:set:plugin:enabled`, this.setPluginEnabled, this);
this._eventbus.off(`${oldPrepend}:set:plugins:enabled`, this.setPluginsEnabled, this);
// Invoke `typhonjs:plugin:manager:eventbus:removed` allowing external code to react to eventbus removal.
this._eventbus.trigger(`typhonjs:plugin:manager:eventbus:removed`,
{
oldEventbus: this._eventbus,
oldEventPrepend: oldPrepend,
newEventbus: targetEventbus,
newEventPrepend: eventPrepend
});
}
targetEventbus.on(`${eventPrepend}:add`, this._addEventbus, this);
targetEventbus.on(`${eventPrepend}:add:all`, this._addAllEventbus, this);
targetEventbus.on(`${eventPrepend}:create:event:proxy`, this.createEventProxy, this);
targetEventbus.on(`${eventPrepend}:destroy:manager`, this._destroyEventbus, this);
targetEventbus.on(`${eventPrepend}:get:all:plugin:data`, this.getAllPluginData, this);
targetEventbus.on(`${eventPrepend}:get:extra:event:data`, this.getExtraEventData, this);
targetEventbus.on(`${eventPrepend}:get:method:names`, this.getMethodNames, this);
targetEventbus.on(`${eventPrepend}:get:options`, this.getOptions, this);
targetEventbus.on(`${eventPrepend}:get:plugin:data`, this.getPluginData, this);
targetEventbus.on(`${eventPrepend}:get:plugin:enabled`, this.getPluginEnabled, this);
targetEventbus.on(`${eventPrepend}:get:plugin:event:names`, this.getPluginEventNames, this);
targetEventbus.on(`${eventPrepend}:get:plugin:method:names`, this.getPluginMethodNames, this);
targetEventbus.on(`${eventPrepend}:get:plugin:names`, this.getPluginNames, this);
targetEventbus.on(`${eventPrepend}:get:plugin:options`, this.getPluginOptions, this);
targetEventbus.on(`${eventPrepend}:get:plugins:enabled`, this.getPluginsEnabled, this);
targetEventbus.on(`${eventPrepend}:get:plugins:by:event:name`, this.getPluginsByEventName, this);
targetEventbus.on(`${eventPrepend}:get:plugins:event:names`, this.getPluginsEventNames, this);
targetEventbus.on(`${eventPrepend}:has:method`, this.hasMethod, this);
targetEventbus.on(`${eventPrepend}:has:plugin`, this.hasPlugin, this);
targetEventbus.on(`${eventPrepend}:has:plugin:method`, this.hasPluginMethod, this);
targetEventbus.on(`${eventPrepend}:invoke`, this.invoke, this);
targetEventbus.on(`${eventPrepend}:invoke:async`, this.invokeAsync, this);
targetEventbus.on(`${eventPrepend}:invoke:sync`, this.invokeSync, this);
targetEventbus.on(`${eventPrepend}:invoke:sync:event`, this.invokeSyncEvent, this);
targetEventbus.on(`${eventPrepend}:is:valid:config`, this.isValidConfig, this);
targetEventbus.on(`${eventPrepend}:remove`, this._removeEventbus, this);
targetEventbus.on(`${eventPrepend}:remove:all`, this._removeAllEventbus, this);
targetEventbus.on(`${eventPrepend}:set:extra:event:data`, this.setExtraEventData, this);
targetEventbus.on(`${eventPrepend}:set:plugin:enabled`, this.setPluginEnabled, this);
targetEventbus.on(`${eventPrepend}:set:plugins:enabled`, this.setPluginsEnabled, this);
// Invoke `typhonjs:plugin:manager:eventbus:set` allowing external code to react to eventbus set.
targetEventbus.trigger('typhonjs:plugin:manager:eventbus:set',
{
oldEventbus: this._eventbus,
oldEventPrepend: oldPrepend,
newEventbus: targetEventbus,
newEventPrepend: eventPrepend
});
this._eventbus = targetEventbus;
return this;
}
/**
* Sets any extra event data attached to PluginEvent `extra` field.
*
* @param {*} extraEventData - Adds extra data to PluginEvent `extra` field.
*/
setExtraEventData(extraEventData = void 0)
{
if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); }
this._extraEventData = extraEventData;
}
/**
* Set optional parameters. All parameters are off by default.
*
* @param {PluginManagerOptions} options - Defines optional parameters to set.
*/
setOptions(options = {})
{
if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); }
if (typeof options !== 'object') { throw new TypeError(`'options' is not an object.`); }
if (typeof options.pluginsEnabled === 'boolean') { this._options.pluginsEnabled = options.pluginsEnabled; }
if (typeof options.noEventAdd === 'boolean') { this._options.noEventAdd = options.noEventAdd; }
if (typeof options.noEventDestroy === 'boolean') { this._options.noEventDestroy = options.noEventDestroy; }
if (typeof options.noEventRemoval === 'boolean') { this._options.noEventRemoval = options.noEventRemoval; }
if (typeof options.throwNoMethod === 'boolean') { this._options.throwNoMethod = options.throwNoMethod; }
if (typeof options.throwNoPlugin === 'boolean') { this._options.throwNoPlugin = options.throwNoPlugin; }
}
/**
* Enables or disables a single plugin.
*
* @param {string} pluginName - Plugin name to set state.
* @param {boolean} enabled - The new enabled state.
*
* @returns {boolean} - Operation success.
*/
setPluginEnabled(pluginName, enabled)
{
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 enabled !== 'boolean') { throw new TypeError(`'enabled' is not a boolean.`); }
const entry = this._pluginMap.get(pluginName);
if (entry instanceof PluginEntry)
{
entry.enabled = enabled;
// Invoke `typhonjs:plugin:manager:plugin:enabled` allowing external code to react to plugin enabled state.
if (this._eventbus)
{
this._eventbus.trigger(`typhonjs:plugin:manager:plugin:enabled`,
{
enabled,
name: entry.name,
scopedName: `${this._eventPrepend}:${entry.name}`,
target: entry.target,
targetEscaped: entry.targetEscaped,
type: entry.type,
options: JSON.parse(JSON.stringify(entry.options)),
managerEventPrepend: this._eventPrepend
});
}
return true;
}
return false;
}
/**
* Enables or disables a set of plugins given an array or iterabe of plugin names.
*
* @param {Array<string>} pluginNames - An array / iterable of plugin names.
* @param {boolean} enabled - The new enabled state.
*
* @returns {boolean} - Operation success.
*/
setPluginsEnabled(pluginNames, enabled)
{
if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); }
if (typeof enabled !== 'boolean') { throw new TypeError(`'enabled' is not a boolean.`); }
let success = true;
for (const pluginName of pluginNames)
{
if (!this.setPluginEnabled(pluginName, enabled)) { success = false; }
}
return success;
}
/**
* Removes a plugin by name after unloading it and clearing any event bindings automatically.
*
* @param {string} pluginName - The plugin name to remove.
*
* @returns {boolean} - Operation success.
*/
remove(pluginName)
{
if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); }
const entry = this._pluginMap.get(pluginName);
if (entry instanceof PluginEntry)
{
// Invoke private module method which allows skipping optional error checking.
s_INVOKE_SYNC_EVENTS('onPluginUnload', {}, {}, this._extraEventData, pluginName, this._pluginMap,
this._options, false);
if (entry.eventProxy instanceof EventProxy) { entry.eventProxy.destroy(); }
const pluginData = this.getPluginData(pluginName);
this._pluginMap.delete(pluginName);
// Invoke `typhonjs:plugin:manager:plugin:removed` allowing external code to react to plugin removed.
if (this._eventbus)
{
this._eventbus.trigger(`typhonjs:plugin:manager:plugin:removed`, pluginData);
}
return true;
}
return false;
}
/**
* Removes all plugins after unloading them and clearing any event bindings automatically.
*/
removeAll()
{
if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); }
for (const pluginName of this._pluginMap.keys())
{
this.remove(pluginName);
}
this._pluginMap.clear();
}
/**
* Provides the eventbus callback which may prevent removal if optional `noEventRemoval` is enabled. This disables
* the ability for plugins to be removed via events preventing any external code removing plugins in this manner.
*
* @param {string} pluginName - The plugin name to remove.
*
* @returns {boolean} - Operation success.
* @private
*/
_removeEventbus(pluginName)
{
if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); }
return !this._options.noEventRemoval ? this.remove(pluginName) : false;
}
/**
* Provides the eventbus callback which may prevent removal if optional `noEventRemoval` is enabled. This disables
* the ability for plugins to be removed via events preventing any external code removing plugins in this manner.
*
* @private
*/
_removeAllEventbus()
{
if (this._pluginMap === null) { throw new ReferenceError('This PluginManager instance has been destroyed.'); }
if (!this._options.noEventRemoval) { this.removeAll(); }
}
}
/**
* Private implementation to invoke synchronous events. This allows internal calls in PluginManager for
* `onPluginLoad` and `onPluginUnload` callbacks to bypass optional error checking.
*
* This dispatch method synchronously passes to and returns from any invoked targets a PluginEvent.
*
* @param {string} methodName - Method name to invoke.
*
* @param {object} copyProps - plugin event object.
*
* @param {object} passthruProps - if true, event has plugin option.
*
* @param {*} extraEventData - Optional extra data attached to all plugin events.
*
* @param {string|Array<string>} nameOrList - An optional plugin name or array / iterable of plugin names to
* invoke.
*
* @param {Map<string, PluginEvent>} pluginMap - Stores the plugins by name with an associated PluginEntry.
*
* @param {object} options - Defines options for throwing exceptions. Turned off by default.
*
* @param {boolean} [performErrorCheck=true] - If false optional error checking is disabled.
*
* @returns {PluginEvent}
*/
const s_INVOKE_SYNC_EVENTS = (methodName, copyProps = {}, passthruProps = {}, extraEventData, nameOrList, pluginMap,
options, performErrorCheck = true) =>
{
if (typeof methodName !== 'string') { throw new TypeError(`'methodName' is not a string.`); }
if (typeof passthruProps !== 'object') { throw new TypeError(`'passthruProps' is not an object.`); }
if (typeof copyProps !== 'object') { throw new TypeError(`'copyProps' is not an object.`); }
if (typeof nameOrList !== 'string' && !Array.isArray(nameOrList) &&
typeof nameOrList[Symbol.iterator] !== 'function')
{
throw new TypeError(`'nameOrList' is not a string, array, or iterator.`);
}
// Track how many plugins were invoked.
let pluginInvokeCount = 0;
const pluginInvokeNames = [];
// Track if a plugin method is invoked
let hasMethod = false;
let hasPlugin = false;
// Create plugin event.
const ev = new PluginEvent(copyProps, passthruProps, extraEventData);
if (typeof nameOrList === 'string')
{
const plugin = pluginMap.get(nameOrList);
if (plugin instanceof PluginEntry && plugin.enabled && plugin.instance)
{
hasPlugin = true;
if (typeof plugin.instance[methodName] === 'function')
{
ev.eventbus = plugin.eventProxy;
ev.pluginName = plugin.name;
ev.pluginOptions = plugin.options;
plugin.instance[methodName](ev);
hasMethod = true;
pluginInvokeCount++;
pluginInvokeNames.push(plugin.name);
}
}
}
else
{
for (const name of nameOrList)
{
const plugin = pluginMap.get(name);
if (plugin instanceof PluginEntry && plugin.enabled && plugin.instance)
{
hasPlugin = true;
if (typeof plugin.instance[methodName] === 'function')
{
ev.eventbus = plugin.eventProxy;
ev.pluginName = plugin.name;
ev.pluginOptions = plugin.options;
plugin.instance[methodName](ev);
hasMethod = true;
pluginInvokeCount++;
pluginInvokeNames.push(plugin.name);
}
}
}
}
if (performErrorCheck && options.throwNoPlugin && !hasPlugin)
{
throw new Error(`PluginManager failed to find any target plugins.`);
}
if (performErrorCheck && options.throwNoMethod && !hasMethod)
{
throw new Error(`PluginManager failed to invoke '${methodName}'.`);
}
// Add meta data for plugin invoke count.
ev.data.$$plugin_invoke_count = pluginInvokeCount;
ev.data.$$plugin_invoke_names = pluginInvokeNames;
return ev.data;
};
/**
* Walks an objects inheritance tree collecting property names stopping before `Object` is reached.
*
* @param {object} obj - object to walks.
*
* @returns {Array}
* @ignore
*/
const s_GET_ALL_PROPERTY_NAMES = (obj) =>
{
const props = [];
do
{
Object.getOwnPropertyNames(obj).forEach((prop) => { if (props.indexOf(prop) === -1) { props.push(prop); } });
obj = Object.getPrototypeOf(obj);
} while (typeof obj !== 'undefined' && obj !== null && !(obj === Object.prototype));
return props;
};