fixings
Version:
A general-purpose plugin system to add your own plugin system to any project
492 lines (450 loc) • 15.3 kB
JavaScript
/**
* 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