Skip to content

Instantly share code, notes, and snippets.

@RichardMarks
Created March 11, 2020 23:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RichardMarks/577e07c3a5727a5b686cdee0e0258b1a to your computer and use it in GitHub Desktop.
Save RichardMarks/577e07c3a5727a5b686cdee0e0258b1a to your computer and use it in GitHub Desktop.
Versatile JS Controller/Plugin System
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
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
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')
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