Sokkit
-
Its where yer plugins go, innit?
A simple, unopinionated plugin handler for npm modules.
Discover, load, instantiate and/or execute methods on all of your plugins with
ease - Just decide on a suitable file pattern, or use the mymodule-pluginname
default, and away you go!
Installation
Install via npm, and save it as a dependency for your
project.
npm install sokkit --save
Testing
Install the dev dependencies:
cd node_modules/sokkit/
npm install
Then, run the included test suite:
npm test
Configuration
Require the Sokkit
class, and create an instance.
var Sokkit = require('sokkit');
var sokkit = new Sokkit();
The owning module name will automatically be detected, or you may override this
by supplying a module
option.
var sokkit = new Sokkit({ module: 'mymodule' });
Call the load()
method to discover, and load any plugins.
var plugins = sokkit.load();
Or call it asynchronously, by supplying a callback:
sokkit.load(function(error, plugins) {
});
Failures to detect plugins, or file system failures throw an Error
(in
synchronous mode), or pass the error
parameter to the callback
supplied
(in asynchronous mode).
Actual plugin load failures, however, populate the failed
property, an Array
containing plain objects with name
and error
properties.
if (sokkit.failed.length) {
console.log("The following plugins failed to load:\n",
sokkit.failed.map(function(fail) {
return ' ' + fail.name + ': ' + fail.error;
}).join('\n')
);
}
By default Sokkit
will search the same node_modules
directory that your
module resides in for any other modules named yourmodule-*
, and attempt to
require
them.
You can override the search directory by supplying a path
option:
var dirname = require('path').dirname;
var sokkit = new Sokkit({
path: dirname(require.main.filename) + '/plugins/appname-*'
});
Additionally, you can specify an array of alternate paths, and even include the
default by specifying $DEFAULT
within the array:
var sokkit = new Sokkit({
path: [
dirname(require.main.filename) + '/plugins/appname-*',
dirname(require.main.filename) + '/extras/*.js',
'$DEFAULT'
]
});
Note: Overriding the path automatically overrides the pattern as well (since
you may well need different patterns for different paths).
If you'd prefer to use the default path, but want a different file matching
pattern, just supply a glob compatible
pattern.
var sokkit = new Sokkit({
pattern: 'mymodule-*-{plugin,extension}'
});
Additionally, plugins can be prevented from loading at all, by supplying a
disable
array during construction, and supplying the names of plugins to
prevent:
var sokkit = new Sokkit({
disable: ['plugin1', 'plugin2']
});
Usage
The Sokkit
instance is actually an Array
, well,
sort of.
Once load
has returned (in synchronous mode), or the supplied callback
has
been called (in asynchronous mode), it will contain the module.exports
from
each of the plugins discovered, and loaded.
From there, you can iterate over the loaded plugins, as you would with any
other Array
:
sokkit.forEach(function(plugin) {
});
Or use Array
functions, such as map
, filter
, join
, to perform operations
or aggregate information about your loaded plugins.
If you need access to the actual plugin names, or want to reference a plugin by
name, you can use the plugins
property:
var plugins = sokkit.plugins;
for(var name in plugins) {
if (!plugins.hasOwnProperty(name)) { continue; }
var plugin = plugins[name];
}
Sokkit
is not opinionated, there is no enforced design on your plugin
structure. How your plugins interact with your application is left to the
developer's discretion, but several methods are supplied to assist in those
interactions.
The call
method invokes the method
supplied on every succesfully loaded
plugin with the remaining parameters:
sokkit.call('init', this, config.plugin);
The call
method will return an array, containing the return values of every
successful call made.
An additional property on the returned array, errors
will contain objects with
name
and error
properties, describing details of any plugins that threw
exceptions while processing the request.
The apply
method works exactly the same as call
, except the arguments are
passed as an array:
sokkit.apply('init', [this, config.plugin]);
Plugin naming
Plugins have a name associated, which can be used to reference them when
validating dependencies, enabling, disabling, or accessing via
sokkit.plugins[name]
.
The name is automatically assigned during discovery from the module directory
name, or direct script filename matched by the discovery pattern.
In order to maintain consistency, and prevent app-name-soup, the following
transformations occur when a plugin is discovered.
If the plugin is contained in a single script file, the .js
extension is
removed.
If the plugin is discovered in a module called application-pluginname
, or a
file named application-plugin.js
then the application-
prefix is removed, so
an application using the following plugins:
node_modules/application-plugin1/
node_modules/application-plugin2/
./components/feature1.js
./components/application-feature2.js
Will have the following plugins:
plugin1
plugin2
feature1
feature2
Subsets
Although the Sokkit
instance is a subclass of Array
, you can not manipulate
the contents directly, or use slice()
to obtain a subset of plugins (it will
return an Array
, not a Sokkit
instance). Instead, use subset()
, and
supply an optional function to filter that set, which will result in a
Sokkit
instance containing the required subset of plugins.
var group = sokkit.subset(function(name, plugin) {
return plugin.isGroupMember || name.indexOf('group_name');
});
Subsets do not retain the failed
properties of their parents.
Subsets are also independent plugin lists, they reference the same exports
,
but are unique sets in their own right. This means you can load
plugins,
create two subsets, run instantiate
on each, and maintain two completely
independent sets of plugin instances.
var plugins = new Sokkit().load();
var listA = plugins.subset();
listB = plugins.subset();
listA.instantiate();
listB.instantiate();
console.log(listA[0] === listB[0]);
Instanced plugins
If you prefer modular plugins, the helper function, instantiate
can be used to
treat each plugin as a constructor, and instantiate those objects with the
parameters supplied.
var errors = sokkit.instantiate(this);
sokkit[0] = new sokkit[0](this);
sokkit[1] = new sokkit[1](this);
The returned array will contain objects with name
and error
properties,
listing any plugins that threw exceptions while being instantiated.
Enabling and disabling
If a plugin misbehaves, or is not required, it can be disabled:
sokkit.disable('bad-plugin');
And later re-enabled:
sokkit.enable('good-plugin');
Disabled plugins will no longer appear in the sokkit
array, the plugins
property, appear in any subset
s or be affected by call
, apply
, or
instantiate
.
Plugins disabled during construction, and thereby never actually loaded can be
enabled, just like any other plugin.
Plugin dependency management
In order to remain unopinionated, there is no enforced method of dependency
management between plugins, but there is a helper function to simplify the task,
should you need it.
Create a retrieve
callback to return an array of dependencies for any given
plugin, and pass it to the depend
method to automatically verify dependencies,
disabling any plugins that have not had their dependencies satisfied.
Additionally, this method will return the, now familiar, error array describing
any plugins that have been disabled due to a dependency failure.
var errors = sokkit.depend(function(name, plugin) {
return plugin.requires;
});
So, how does this help me?
Well, it means you can pick a naming scheme, and automatically discover and
work with plugins, using whatever actual plugin API you prefer.
This, in turn, means that your users can, for instance:
npm install yourmodule
npm install yourmodule-plugin1
npm install yourmodule-plugin2
and have those plugins working automatically, right out of the box. No
configuration, or editing of package.json
files to enable them, or ensure
they are loaded/linked correctly.
Alternatively, developers that depend on your module can do exactly the same,
but by specifying plugins as dependencies too, in their package.json
:
{
"name": "superapplication",
"version": "0.1.0",
"author": "Bob",
"description": "the very best application",
"dependencies": {
"yourmodule": "^0.1.0",
"yourmodule-plugin1": "^0.1.0",
"yourmodule-plugin2": "^0.2.0"
}
}
And, when they access your module, the plugins will be available too - all
automatically discovered, loaded and ready to use.
Personally, I like to use this as an automatic aggregator for components within
my own software too - use an array of paths, including some way of picking up
internal plugins.
function MyAPI() {
this.plugins = new Sokkit({
path: [
dirname(require.main.filename) . '/components/**/*.js',
'$DEFAULT'
]
}).load();
}
This style not only allows you to make every component of your system an
effective example plugin, but also forces you, the developer, to consider
encapsulation and separation of concerns within your architecture - and in turn
consider your plugin API, its usability, scalability, etc, from the outset.
Finally
Have fun!