UNPKG

fixings

Version:

A general-purpose plugin system to add your own plugin system to any project

492 lines (450 loc) 15.3 kB
/** * Fixings/Fixings class * * Plugin manager class. Add and run plugins and hooks. * * @version 1.1.0 * @author Drew Sommer * @license MIT * @copyright 2018 */ /** @ignore */ const Eat = require('./eat') /** * @property {String} name - var name for plugin naming * @property {String} before - var name for plugin hook runs before list * @property {String} after - var name for plugin hook runs after list * @property {String} handler - var name for plugin hook handler function * @property {String} dependencies - var name for plugin dependencies */ var FixingsOptions = {} // eslint-disable-line no-unused-vars class Fixings { /** * Create a new plugin manager * @param {FixingsOptions=} options - options for customizing names * @example * var Plate = new Fixings([FixingsOptions]{@link FixingsOptions}) */ constructor(options) { if ( (typeof options !== 'undefined' && typeof options !== typeof null && typeof options !== typeof {}) || Array.isArray(options) ) { if (Array.isArray(options)) throw new TypeError('options is not a correct object, got [array]') else throw new TypeError(`options is not a correct object, got [${typeof options}]`) } if (!options) options = {} if ( typeof options.name !== 'undefined' && options.name !== null && typeof options.name !== 'string' ) throw new TypeError(`options.name is not a string, got [${typeof options.name}]`) if ( typeof options.before !== 'undefined' && options.before !== null && typeof options.before !== 'string' ) throw new TypeError(`options.before is not a string, got [${typeof options.before}]`) if ( typeof options.after !== 'undefined' && options.after !== null && typeof options.after !== 'string' ) throw new TypeError(`options.after is not a string, got [${typeof options.after}]`) if ( typeof options.handler !== 'undefined' && options.handler !== null && typeof options.handler !== 'string' ) throw new TypeError(`options.handler is not a string, got [${typeof options.handler}]`) if ( typeof options.dependencies !== 'undefined' && options.dependencies !== null && typeof options.dependencies !== 'string' ) throw new TypeError(`options.dependencies is not a string, got [${typeof options.dependencies}]`) if ( typeof options.name !== 'undefined' && options.name !== null && typeof options.dependencies !== 'undefined' && options.dependencies !== null && options.name === options.dependencies ) throw new Error('options.name and options.dependencies cannot have the same value') if ( typeof options.before !== 'undefined' && options.before !== null && typeof options.after !== 'undefined' && options.after !== null && options.before === options.after ) throw new Error('options.before and options.after cannot have the same value') if ( typeof options.before !== 'undefined' && options.before !== null && typeof options.handler !== 'undefined' && options.handler !== null && options.before === options.handler ) throw new Error('options.before and options.handler cannot have the same value') if ( typeof options.after !== 'undefined' && options.after !== null && typeof options.handler !== 'undefined' && options.handler !== null && options.after === options.handler ) throw new Error('options.after and options.handler cannot have the same value') if ( typeof options.after !== 'undefined' && options.after !== null && typeof options.before !== 'undefined' && options.before !== null && typeof options.handler !== 'undefined' && options.handler !== null && options.after === options.handler && options.before === options.handler ) throw new Error('options.after, options.handler, and options.before cannot have the same value') this.$name = options.name || 'name' this.$before = options.before || 'before' this.$after = options.after || 'after' this.$handler = options.handler || 'handler' this.$dependencies = options.dependencies || 'dependencies' this.Plugins = [] this.Depends = [] this.Hooks = {} } /** * Add a new plugin to the list of plugins * @param {PluginHook} plugin - The plugin to add * @example * Plate.addPlugin([Plugin]{@link Plugin}); * @returns {Boolean} Returns true if added, false if not (already present) * @throws {Error} If the plugin $name is missing * @todo rename to addPluginSync */ addPlugin(plugin) { if ( typeof plugin !== typeof {} || Array.isArray(plugin) ) { if (Array.isArray(plugin)) throw new TypeError('Plugin is expected to be an object, got [array]') else throw new TypeError(`Plugin is expected to be an object, got [${typeof plugin}]`) } if (!plugin[this.$name]) throw new Error(`plugin ${this.$name} missing`) // Check if plugin is already registered for (var plug of this.Plugins) if (plug === plugin[this.$name]) return false this.Plugins.push(plugin[this.$name]) // Loop through all the hooks in the plugin adding them to the hooks object for (var hook in plugin) { if (hook === this.$name) continue if (hook === this.$dependencies) continue var pluginTemp = plugin[hook] // If a hook is a function, wrap it up if (typeof pluginTemp === 'function') { pluginTemp = { [this.$handler]: plugin[hook] } } pluginTemp[this.$name] = plugin[this.$name] // Create the hook if it is not present if (!this.Hooks[hook]) { this.Hooks[hook] = { plugins: [], order: [] } } // Add the plugins hook if it is not present if (this.Hooks[hook].plugins.indexOf(pluginTemp) === -1) this.Hooks[hook].plugins.push(pluginTemp) if (plugin[this.$dependencies]) this.Depends = this.Depends.concat(plugin[this.$dependencies]) } return true } /** * @typedef {function(addPluginCallback)} * @callback addPluginCallback * @param {?Error} - Error message or null (if no error) * @param {Boolean} - Successfully added (true) or not (false) if plugin is present */ /** * Add a new plugin to the list of plugins (async) * @param {PluginHook} plugin - The plugin to add * @param {addPluginCallback=} [callback=null] - Callback function * @async * @returns {Boolean} Successfully added (true) or not (false) if plugin is present * @throws {Error} If an error occured * @example * Plate.addPluginAsync([Plugin]{@link Plugin}, (error, results) => {...}) * @example * var results = await Plate.addPluginAsync([Plugin]{@link Plugin}) * @example * var results = Plate.addPluginAsync([Plugin]{@link Plugin}) * .then(...) * .catch(...) */ addPluginAsync(plugin, callback=null) { if(typeof callback === 'function') { setImmediate(() => { try { callback(null, this.addPlugin(plugin)) } catch (error) { callback(error) } }) } else { return new Promise((resolve, reject) => { try { resolve(this.addPlugin(plugin)) } catch (error) { reject(error) } }) } } /** * @typedef MutliPluginSuccessObject * @property {String} this.$name - the name of the plugin * @property {Error|Boolean} success - success flag if the plugin was added (true) * not added (false) or an error occured (error) */ /** * Add multiple plugins at once * @param {PluginHook[]} plugins - A list of plugins to add * @returns {MutliPluginSuccessObject} List of successfully or not added plugins * @example * Plate.addPlugins([[Plugin]{@link Plugin}, Plugin2, Plugin3, ...]) */ addPlugins(plugins) { if (!Array.isArray(plugins)) throw new TypeError(`plugins expected to be an array, got [${typeof plugins}]`) var results = [] for (var plugin of plugins) { try { results.push({ [this.$name]: plugin[this.$name], success: this.addPlugin(plugin) }) } catch(error) { results.push({ [this.$name]: plugin[this.$name], success: error }) } } return results } /** * @typedef {function(addMultiplePluginsCallback)} * @callback * @param {Error|Null} - Error message or null (if no error) * @param {MutliPluginSuccessObject} - List of successfully or not added plugins */ /** * Add multiple plugins asyncronously * @param {PluginHook[]} plugins - A list of plugins to add * @param {addMultiplePluginsCallback} [callback=null] - Callback function * @async * @returns {MutliPluginSuccessObject} List of successfully or not added plugins * @throws {Error} One or more plugins failed error (see returned object) * @example * Plate.addPluginsAsync([[Plugin]{@link Plugin}, Plugin2, ...], (error, results) => {...}) * @example * var results = await Plate.addPluginSync([[Plugin]{@link Plugin}, Plugin2, ...]) * @example * var results = Plate.addPluginSync([[Plugin]{@link Plugin}, Plugin2, ...]) * .then(...) * .catch(...) */ addPluginsAsync(plugins, callback=null) { if (typeof callback === 'function') { setImmediate(() => { try { var results = this.addPlugins(plugins) var error = null for (var r of results) { if (r.success instanceof Error) { error = new Error('One or more plugins failed with an error, see returned object for details') } } callback(error, results) } catch(error) { callback(error) } }) } else { return new Promise((resolve, reject) => { var promises = [] for (var plugin of plugins) { promises.push(new Promise((resolve, reject) => { try { resolve(this.addPlugin(plugin)) } catch (error) { reject(error) } })) } Promise.all(promises) .then(() => resolve(true)) .catch(error => reject(error)) }) } } /** * Create a new Eat class to iterate over the plugins of a specific hook * @param {String} hook - The hook to iterate over * @returns {Eat} Consumer class for iterating over a hook * @throws {Error} Hook is undefined * @example * var [Eater]{@link Eat} = Plate.resolveHook('hook') * @todo add option to include optional function to test each plugin hook before * running the handler, i.e. plugin hook has a "test" param that this function * tests against */ resolveHook(hook) { if (typeof hook !== 'string') throw new TypeError(`hook expected to be a string, got [${typeof hook}]`) if (!this.Hooks[hook]) throw new Error(`Hook: '${hook}' is undefined`) return new Eat(this.Hooks[hook], this.$name, this.$before, this.$after) } /** * @typedef {function(resolveHookCallback)} * @callback * @param {?Error} - Error message or null (if no error) * @param {Eat} - Consumer class for iterating over a hook */ /** * Create a new Eat class to iterate over the plugins of a specific hook asyncronously * @param {String} hook - The hook to iterate over * @param {resolveHookCallback} [callback=null] [description] * @async * @returns {Eat} Consumer class for iterating over a hook * @throws {Error} If hook is undefined throws error * @example * Plate.resolveHookAsync('hook', (error, [Eater]{@link Eat}) => {...}) * @example * var results = await Plate.resolveHookAsync('hook') * @example * var results = Plate.resolveHookAsync('hook') * .then(...) * .catch(...) */ resolveHookAsync(hook, callback=null) { if (typeof callback === 'function') { setImmediate(() => { try { callback(null, new Eat(this.Hooks[hook], this.$name, this.$before, this.$after)) } catch (error) { callback(error) } }) } else { return new Promise((resolve, reject) => { try { resolve(new Eat(this.Hooks[hook], this.$name, this.$before, this.$after)) } catch (error) { reject(error) } }) } } /** * Return a list of installed plugins (names only) * @returns {String[]} A list of plugins returned by name only * @example * var plugins = Plate.getPlugins() */ getPlugins() { return this.Plugins } /** * @typedef {function(getPluginsCallback)} * @callback * @param {?Error} - Error message or null (if no error) * @param {MutliPluginSuccessObject} - List of plugins by name */ /** * Return a list of installed plugins (names only) * @param {getPluginsCallback} [callback=null] - Callback function * @returns {String[]} A list of plugins retuend by name only * @example * Plate.getPluginsAsync((error, plugins) => {...}) * @example * var results = await Plate.getPluginsAsync() * @example * var results = Plate.getPluginsAsync() * .then(...) * .catch(...) */ getPluginsAsync(callback=null) { if (typeof callback === 'function') { setImmediate(() => { callback(null, this.Plugins) }) } else { return new Promise((resolve) => { resolve(this.Plugins) }) } } /** * Verify that plugin dependencies are resolved (does not resolve, only evaluates) * @returns {String[]} returns a list of missing plugins * (empty if not missing any plugins) * @example * var missingPlugins = Plate.verifyDependencies() */ verifyDependencies() { var missing = [] for (var req of this.Depends) { if (!this.Plugins.includes(req) && !missing.includes(req)) missing.push(req) } return missing } /** * @typedef {function(verifyDependenciesCallback)} * @callback * @param {Error|Null} - Error message or null (if no error) * @param {MutliPluginSuccessObject} - A list of missing plugins * (empty if not missing any plugins) */ /** * Verify that plugin dependencies are resolved (does not resolve, only evaluates) * @param {verifyDependenciesCallback} [callback=null] - Callback function * @async * @returns {String[]} returns a list of missing plugins * (empty if not missing any plugins) * @example * Plate.verifyDependenciesAsync((error, missing) => {...}) * @example * var results = await Plate.verifyDependenciesAsync() * @example * var results = Plate.verifyDependenciesAsync() * .then(...) * .catch(...) */ verifyDependenciesAsync(callback=null) { if (typeof callback === 'function') { setImmediate(() => { callback(null, this.verifyDependencies()) }) } else { return new Promise((resolve) => { resolve(this.verifyDependencies()) }) } } } module.exports = Fixings