Miss any of our Open RFC calls?Watch the recordings here! »

tennu-plugins

4.0.0-b4 • Public • Published

Build Status

This is the Plugins subsystem of the Tennu IRC Bot Framework for Node.

This module is a plugin system that handles loading, dependencies, and cross-plugin hooks.

The main facets of this plugin system are:

  • Loading of dependencies
  • Initialization of plugins
  • Plugin roles, where plugins of that role expose a common interface.
  • Plugin hooks, where plugins can hook into each initializing plugin.

While this plugin system was designed for Tennu, you can repurpose this system for your own plugin architecture framework.

When do I want to use this?

Use this if you want to create a plugin based framework in Node.js with a common context for the plugins (e.g. IRC for Tennu).

If you are using a framework that uses this, it's useful to read the sections on PluginFactorys, Plugins, Hooks, and Exports.

Installation

This plugin system targets Node.js v4.x to v6.x, and probably works on later versions of Node.js.

npm install tennu-plugins

Creating a Plugin System

require("tennu-plugins")(systemname: string, context: any)

The systemname is the name of the plugin system, and declares the directory names the plugin loader's use method will look for plugins in. See PluginLoader#use/1 for more.

The context is the first argument passed the plugin initialization functions.

For example, in Tennu, the IRC client is passed to each plugin and plugins are stored in tennu_plugins directories. It is initialized as follows.

const plugins = require("tennu-plugins")("tennu", client)

Initialization and Loading of Plugins

A plugin system comes with two ways to initialize a plugin, PluginSystem#initialize/1 and PluginSystem#use/2.

initialize/1 takes a PluginFactory while use takes an array of strings and a path that the system uses to locate and then load PluginFactorys.

These functions are described in more detail later on in this document.

const channelsPluginFactory = require("./tennu-plugins/channels");
plugins.initialize(channelsPluginFactory).ok();
 
## What's a PluginFactory?
 
A PluginFactory is an object that describes how to create a Plugin. It contains
*static* properties that don'change. PluginFactorys have the following
properties.
 
```javascript
{
    init: Fn(context, imports) -> Plugin,
    name: string
    role: RoleName?,
    requires: [string]?,
    requiresRoles: [RoleName]?,
    // Other properties depending on static hooks
    // See the section on Hooks below for how those work.
}

The name is there so that other plugins can reference the plugin the factory creates. This also means that each plugin must have a unique name, so you cannot have multiple instances of the same plugin.

The init function must return a Plugin. All properties on a Plugin are actually optional, so a plugin could just be an empty object. The context parameter is the context passed in during the creation of the plugin system. The imports parameter is discussed later.

The following is the minimally viable PluginFactory:

{
    name: "bare",
    init (_context, _imports) {
        return {};
    }
}

The requires and requiresRoles properties take a list of plugin names or roles that the plugin requires to be loaded to function properly.

What's a Plugin?

At it's most simple, a plugin is an object that exports properties or defines hooks. A Plugin has the following structure.

{
    hooks: Object<Fn(String, Any)>?,
    staticHooks: Object<Fn(String, Any)>?,
    exports: Any?,
    // Other properties depending on instance hooks
    // See the section on Hooks below for how those work.
}
 
Plugins are created in the `init` function of the PluginFactory, and as such,
have access to the `context` which is basically a free dependency chosen by
the creator of the plugin system. They also have access to imported plugins.
 
The plugin instance can export values for other plugins to use. To do so, these
values must be located on the exports property.
 
The plugin instance can also hook into plugins that require it. To do so, the
properties to hook onto must be defined on the hooks property.
 
## Cross-Plugin Interaction
 
Plugins have two main ways of interacting with each other. The first way, called
`direct dependencies`, allows plugins to export values that are later imported
by other plugins.
 
The second way, called `hooks`, allows a plugin to define
new properties for PluginFactorys and Plugins that have their values passed
back into the defining plugin.
 
### Direct Dependencies
 
The exports property is given to plugins that depend on your plugin through
the init function. For example, let's say we have two plugins, A and B.
Plugin B depends on Plugin A. Plugin A exports a property `exists: true`.
Plugin B loads plugin A and then logs A's `exist` property to the console.
 
```javascript
 
const pluginSystem = require("tennu-plugins")({}, "example");
 
const PluginA = {
    name: "A",
    init: function (_context, _imports) {
        return {
            exports: {exists: true}
        };
    }
}
 
const PluginB = {
    name: "B"
    requires: ["A"],
    init: function (_context, {B}) {
        console.log(B.exists);
        return {
            exports: {}
        };
    },
}
 
pluginSystem.initialize(PluginA);
pluginSystem.initialize(PluginB);
// console will log "true" here.

This is very similar to how Node.js's module system works, except object based instead of file based.

Hooks

At times, a plugin will want to extend the functionality of PluginFactorys and Plugin objects by defining new properties for these objects. Hooks allow for plugins to do so.

A hook is a function that is given the plugin's name and the value on the hook's property.

A plugin defines a hook by creating an object on either its hooks or staticHooks properties where the key is the property to hook into and the value is a function that takes two parameters, the first being the name of the plugin and the second the value that plugin provides for that property. The plugin defining the hook can do whatever it wants with that value. For Plugin hooks, use hooks and for PluginFactory hooks, use staticHooks.

For example, Tennu has a help plugin that hooks into the 'help' property. So, let's say there's a time plugin that wants to use this hook.

The time plugin puts "help" in the dependencies list, and adds a help property to the plugin instance with the help message.

 
// Plugin "help"
{
    name: "help",
    init (client, imports) {
        // ... initialization code.
 
        return {
            // ... other properties
            hooks: {
                help (pluginName, helpobj) {
                    // Does stuff with helpobj
                }
            }
        };
    }
}
 
// Plugin "time"
{
    name: "time",
    init: function (client, imports) {
        // ... initialization code.
 
        return {
            // ... other properties
            help: "Stuff about time."
        }
    }
}

Note: In this example, the plugin name and hook name are the same. This is not a requirement. You can name your hooks whatever you want. You can also have multiple hooks.

Global Hooks

The creator of the plugin system can add hooks that apply to all plugins.

Global hooks should be added before loading any plugins.

To do so, use one of the following functions:

  • addHook(hook: String, fn: Fn(String, Any)) -> Result<(), InstanceHookAlreadyExistsFailure>
  • addStaticHook(hook: String, fn: Fn(String, Any)) -> Result<(), StaticHookAlreadyExistsFailure>

For example, Tennu used to add a 'handlers' global hook.

var plugins = require("tennu-plugins")("tennu", client);
plugins.addHook("handlers", function (pluginName, handlers) {
   client.on(handlers); 
});

Nowadays, Tennu uses a plugin for this behaviour.

Loading Plugins

var plugins = require("tennu-plugins")("tennu", client);
var builtins = ["server", "actions", "help", "user", "channel"];
plugins.use(builtins, __);

Create a list of plugins that you want to use, and then pass them to plugins.use(names: [String], path: String).

The plugin system will then locate and load the plugins in an order such that all dependencies are properly met.

This function returns a Result<undefined, UseFailure>. A UseFailure is a Failure with one of the following failure types:

  • UnmetDependency
  • NoSuchPlugin
  • NoSuchRole (Not yet used)
  • CyclicicDependency (Not yet used)
  • PluginInitializationError
  • RegistryKeyAlreadySet
  • HookAlreadyExists

Locating Plugins

The second parameter to use() is a path. The plugin system will look for the following places for your plugin:

  • %path%/%systemname%_plugins/%pluginname%.js
  • %path%/%systemname%_plugins/%pluginname%/index.js
  • %path%/node_plugins/%systemname%-%pluginname%/

If it cannot find the plugin there, it will then go up the parent directory, and repeat, until it either finds the plugin or is at the root.

If the plugin cannot be found, a NoSuchPlugin failure will be returned.

For example, say you want the plugin "config" for "tennu" starting from "/projects/tennu". The plugin locator will look at the following places until it finds it.

  • /projects/tennu/tennu_plugins/config.js
  • /projects/tennu/tennu_plugins/config/index.js
  • /projects/tennu/node_plugins/tennu-config/
  • /projects/tennu_plugins/config.js
  • /projects/tennu_plugins/config/index.js
  • /projects/node_plugins/tennu-config/
  • /tennu_plugins/config.js
  • /tennu_plugins/config/index.js
  • /node_modules/tennu-config/

Initializing Plugins

Full PluginSystem API

These are the method signatures that a PluginSystem has.

  • addHook(name: string, hook: Fn(String, Any)) -> Result<undefined, InstanceHookAlreadyEixstsFailure>
  • addInstanceHook(name: string, hook: Fn(String, Any) -> Result<undefined, InstanceHookAlreadyExistsFailure>
  • addStaticHook(name: string, hook: Fn(String, Any)) -> Result<undefined, StaticHookAlreadyExistsFailure>
  • isInitializable(plugin: Plugin): InitializationValidation
  • initialize(plugin: Plugin): Result<undefined, InitializeFailure>
  • use(plugins: [String]: Result<undefined, UseFailure)
  • hasPlugin(name: string): boolean
  • hasRole(name: string): boolean
  • getPlugin(name: string): Result<exports, NoSuchPluginFailure>
  • getRole(name: string): Result<exports, NoSuchRoleFailure>

Failure Handling

Any function that has a failure condition returns a Result from r-result.

The failure type can be determined by checking the failureType property of the object. This property contains a symbol accessible from the failures property of this module. For example, the InstanceHookAlreadySet symbol can be found at require("tennu-plugins").failures.InstanceHookAlreadySet.

The failure also has a message property describing the issue

Failures may also have other properties, depending on what failure type it is.

The following subsections are the types of failures.

CannotInitialize

Returned by initialize and use.

Returned when trying to initialize a plugin, but after looking at it's factory object, it was determined not to be possible. The system will not have initialized any plugins if it returns this failure.

The reason for why it cannot be initialized is on the validationFailure property.

The PluginFactory is on the pluginFactory property.

PluginNotAnObject

Returned by initalize and use.

Returned when the init function of a PluginFactory returns something that is not an object.

You should consider the plugin system to be in an indeterminate state at this point, since the init function for a plugin was ran, but it was not fully initialized.

InstanceHookAlreadySet

Returned by initialize, use, addHook and addInstanceHook.

Returned when trying to set an instance hook after calling the init function on the plugin, and the plugin system already has that hook registered to another plugin.

You should consider the plugin system to be in an indeterminate state at this point, since the init function for a plugin was ran, but it was not fully initialized.

The name of the hook is stored on the hook property.

To solve this, figure out which plugin has the hook installed already, and change one of the plugins to use a different hook.

Ideally, no two plugins would ever share the same instance hook (without sharing the same role) though, since that makes them incompatible, both with each other, but also any plugin that depends on the other.

StaticHookAlreadySet

Returned by initialize, use, and addStaticeHook.

Same as InstanceHookAlreadySet, but for static hooks.

// NoSuchPlugin: Symbol("NoSuchPlugin"), // UnmetDependency: Symbol("UnmetDependency"), // InconsistentlyNamedPlugin: Symbol("InconsistentlyNamedPlugin"), // CyclicicDependency: Symbol("CyclicicDependency"),

Keywords

Install

npm i tennu-plugins

DownloadsWeekly Downloads

6

Version

4.0.0-b4

License

ISC

Last publish

Collaborators

  • avatar