Created
March 11, 2020 23:28
-
-
Save RichardMarks/577e07c3a5727a5b686cdee0e0258b1a to your computer and use it in GitHub Desktop.
Versatile JS Controller/Plugin System
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const Controller = () => { | |
const controller = { | |
__VERSION__: '1.0.0', | |
plugins: {}, | |
installPlugin (plugin, override) { | |
if (!plugin || !('__pluginName__' in plugin)) { | |
throw new Error(`Controller was unable to install plugin - Invalid plugin was provided.`) | |
} | |
if (plugin.__pluginName__ in controller.plugins) { | |
if (!override) { | |
throw new Error(`Controller was unable to install plugin "${plugin.__pluginName__}" - Already installed and no override was requested.`) | |
} | |
controller.uninstallPlugin(plugin.__pluginName__) | |
} | |
const install = plugin.__installer__ | |
if (typeof install !== 'function') { | |
throw new Error(`Controller was unable to install plugin - Missing installer implementation.`) | |
} | |
const pluginContext = { | |
__pluginContext__: plugin.__pluginName__, | |
state: {} | |
} | |
install(pluginContext, controller) | |
controller.plugins[plugin.__pluginName__] = pluginContext | |
}, | |
uninstallPlugin (pluginName) { | |
if (!(pluginName in controller.plugins)) { | |
throw new Error(`Controller was unable to uninstall plugin "${pluginName}" - Plugin was not found.`) | |
} | |
const pluginContext = controller.plugins[pluginName] | |
const uninstall = pluginContext.__uninstall__ | |
typeof uninstall === 'function' && uninstall(controller) | |
delete controller.plugins[pluginName] | |
} | |
} | |
return Object.freeze(controller) | |
} | |
const Plugin = (name, construct, destruct) => { | |
if (!name) { | |
throw new Error('Plugin name is required') | |
} | |
if (!construct || typeof construct !== 'object') { | |
throw new Error('Plugin construct object is required') | |
} | |
// ensure a default state object exists | |
if (!('state' in construct)) { | |
construct.state = {} | |
} | |
// ensure that a default methods object exists | |
if (!('methods' in construct)) { | |
construct.methods = {} | |
} | |
const plugin = { | |
__pluginName__: name, | |
__installer__ (pluginContext, controller) { | |
if (!pluginContext || pluginContext.__pluginContext__ !== plugin.__pluginName__) { | |
throw new Error(`Unable to install plugin "${plugin.__pluginName__}" - Invalid plugin context was provided.`) | |
} | |
if (!controller || controller.__VERSION__ !== '1.0.0') { | |
throw new Error(`Unable to install plugin "${plugin.__pluginName__}" - Invalid controller was provided.`) | |
} | |
const stateKeys = Object.keys(construct.state) | |
const methodNames = Object.keys(construct.methods) | |
const validStateKeyTypes = [ | |
'string', | |
'boolean', | |
'number', | |
'object' | |
] | |
stateKeys.forEach(stateKey => { | |
const stateKeyType = typeof construct.state[stateKey] | |
if (!validStateKeyTypes.includes(stateKeyType)) { | |
throw new Error(`Unable to install plugin "${plugin.__pluginName__}" - Invalid state key "${stateKey}" of type "${stateKeyType}" was provided.`) | |
} | |
pluginContext.state[stateKey] = construct.state[stateKey] | |
console.log(`${plugin.__pluginName__} plugin installed state ${stateKey} of type ${stateKeyType}`) | |
}) | |
methodNames.forEach(methodName => { | |
const methodType = typeof construct.methods[methodName] | |
if (methodType !== 'function') { | |
throw new Error(`Unable to install plugin "${plugin.__pluginName__}" - Invalid method "${methodName}" of type "${methodType}" was provided.`) | |
} | |
if (methodName === 'state' || methodName === '__pluginContext__') { | |
throw new Error(`Unable to install plugin "${plugin.__pluginName__}" - Invalid method "${methodName}" with reserved name was provided.`) | |
} | |
const methodWrapper = (...methodArgs) => { | |
construct.methods[methodName](pluginContext, controller, ...methodArgs) | |
} | |
pluginContext[methodName] = methodWrapper | |
console.log(`${plugin.__pluginName__} plugin installed method ${methodName}`) | |
}) | |
if (typeof destruct === 'function') { | |
pluginContext.__uninstall__ = destruct | |
} else { | |
pluginContext.__uninstall__ = (pluginContext, controller) => {} | |
} | |
} | |
} | |
return Object.freeze(plugin) | |
} | |
module.exports = Controller |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const Plugin = require('./Plugin') | |
// define a plugin | |
const plugin = Plugin( | |
// name of the plugin | |
'foo', { | |
// state for the plugin | |
state: { | |
bar: 'bar', | |
baz: 10, | |
biz: true | |
}, | |
// methods the plugin provides | |
methods: { | |
// the params are automatically provided by the controller | |
// ctx is the plugin context | |
// controller is the controller that the plugin is installed to | |
foo (ctx, controller) { | |
console.log('running foo plugin method foo()') | |
console.log('plugins installed:', Object.keys(controller.plugins).join(', ')) | |
}, | |
// can destruct the ctx for easy state access | |
// can pass any arguments you want to the method when calling it | |
bar ({ state }, controller, ...ex) { | |
console.log('running foo plugin method bar()') | |
console.log('state', JSON.stringify(state, null, 2)) | |
console.log('bar args(ex)', { ex }) | |
} | |
} | |
}, (pluginContext, controller) => { | |
// if you need to do anything when uninstalling your plugin, do it here | |
console.log('uninstalling the foo plugin') | |
} | |
) | |
module.exports = plugin |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const Controller = require('./Controller') | |
const plugin = require('./fooPlugin') | |
// create a controller | |
const controller = Controller() | |
// install the plugin | |
controller.installPlugin(plugin) | |
// use the plugin methods | |
controller.plugins.foo.foo() | |
controller.plugins.foo.bar(100, 200) | |
// uninstall the plugin | |
controller.uninstallPlugin('foo') | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const Plugin = (name, construct, destruct) => { | |
if (!name) { | |
throw new Error('Plugin name is required') | |
} | |
if (!construct || typeof construct !== 'object') { | |
throw new Error('Plugin construct object is required') | |
} | |
// ensure a default state object exists | |
if (!('state' in construct)) { | |
construct.state = {} | |
} | |
// ensure that a default methods object exists | |
if (!('methods' in construct)) { | |
construct.methods = {} | |
} | |
const plugin = { | |
__pluginName__: name, | |
__installer__ (pluginContext, controller) { | |
if (!pluginContext || pluginContext.__pluginContext__ !== plugin.__pluginName__) { | |
throw new Error(`Unable to install plugin "${plugin.__pluginName__}" - Invalid plugin context was provided.`) | |
} | |
if (!controller || controller.__VERSION__ !== '1.0.0') { | |
throw new Error(`Unable to install plugin "${plugin.__pluginName__}" - Invalid controller was provided.`) | |
} | |
const stateKeys = Object.keys(construct.state) | |
const methodNames = Object.keys(construct.methods) | |
const validStateKeyTypes = [ | |
'string', | |
'boolean', | |
'number', | |
'object' | |
] | |
stateKeys.forEach(stateKey => { | |
const stateKeyType = typeof construct.state[stateKey] | |
if (!validStateKeyTypes.includes(stateKeyType)) { | |
throw new Error(`Unable to install plugin "${plugin.__pluginName__}" - Invalid state key "${stateKey}" of type "${stateKeyType}" was provided.`) | |
} | |
pluginContext.state[stateKey] = construct.state[stateKey] | |
console.log(`${plugin.__pluginName__} plugin installed state ${stateKey} of type ${stateKeyType}`) | |
}) | |
methodNames.forEach(methodName => { | |
const methodType = typeof construct.methods[methodName] | |
if (methodType !== 'function') { | |
throw new Error(`Unable to install plugin "${plugin.__pluginName__}" - Invalid method "${methodName}" of type "${methodType}" was provided.`) | |
} | |
if (methodName === 'state' || methodName === '__pluginContext__') { | |
throw new Error(`Unable to install plugin "${plugin.__pluginName__}" - Invalid method "${methodName}" with reserved name was provided.`) | |
} | |
const methodWrapper = (...methodArgs) => { | |
construct.methods[methodName](pluginContext, controller, ...methodArgs) | |
} | |
pluginContext[methodName] = methodWrapper | |
console.log(`${plugin.__pluginName__} plugin installed method ${methodName}`) | |
}) | |
if (typeof destruct === 'function') { | |
pluginContext.__uninstall__ = destruct | |
} else { | |
pluginContext.__uninstall__ = (pluginContext, controller) => {} | |
} | |
} | |
} | |
return Object.freeze(plugin) | |
} | |
module.exports = Plugin |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment