A lightweight, nearly zero-dependency* plugin system for JavaScript classes. Add elaborate plugin support to any class (or any JS object) — with automatic dependency resolution and inheritance support.
* Only dependency is get-symbols, a tiny symbol registry utility.
npm install xtensiblextensible allows you to make your classes extensible so that consumers can do things like:
import YourClass from "your-class";
import yourClassPlugin from "your-class-plugin";
YourClass.addPlugin(yourClassPlugin);Each plugin can only be installed once per class, but can be installed separately on subclasses.
A plugin is a plain object with any combination of these properties:
| Property | Type | Description |
|---|---|---|
provides |
object |
Properties and methods added to the class prototype (instances) or the class itself (under constructor) |
hooks |
object |
Callbacks keyed by hook name, run during lifecycle events |
dependencies |
Plugin[] |
Plugins that must be installed first |
import otherPlugin from "other-plugin";
const myPlugin = {
dependencies: [otherPlugin],
provides: {
greet () {
return `Hi from ${this.constructor.name}`;
},
constructor: {
// Static method!
create (...args) {
return new this(...args);
},
},
},
hooks: {
setup () {
/* runs whenever the class calls the "setup" hook */
},
},
};Use addPlugin() to install one or more plugins on a class.
Dependency plugins are installed automatically.
Duplicate installations are silently skipped.
import { addPlugin } from "xtensible";
addPlugin(MyClass, pluginA, pluginB);You can check whether a plugin is already installed (including on superclasses):
import { hasPlugin } from "xtensible";
hasPlugin(MyClass, pluginA); // true or falseYou can expose addPlugin() as a static method on the class so that your consumers don't need to know about xtensible at all:
import { addPlugin } from "xtensible";
class MyClass {
// ...
static addPlugin (plugin) {
addPlugin(this, plugin);
}
}xtensible plugins can contain one or more of the following:
- Provided properties, loosely inspired from the first-class protocols proposal.
- Lifecycle hooks which allow a class to run code at specific points in its execution lifecycle.
Plugins only using provided members (1) can be installed on any class by simply calling addPlugin(Class, plugin) though classes may want to expose addPlugin() etc as static methods so that consumer don't need to know about xtensible at all.
However, for hooks to make sense, the class needs to actually define when they should run. E.g.:
import { hooks } from "xtensible/hooks";
class MyElement extends HTMLElement {
connectedCallback () {
this[hooks].run("connected", this);
}
}Generally, hooks are useful when a plugin needs to extend what an existing method does or add side effects to it. For example:
- Class creation
- Instance creation
- Common lifecycle events (e.g.
connectedCallbackfor custom elements)
Some commonly useful hooks are:
setup- runs once per classconstructor- When the class constructor is called (sync)constructed- runs after an instance is constructed (including any subclass constructors)
xtensible hooks are an evolution of our earlier blissful-hooks package, adapted to work well for deep class hierarchies (superclass hooks run first)
and support the more elaborate functionality we needed for nude-element.
Hook names are normalized to underscore_case, so all of these are equivalent:
"myHook"; // camelCase
"my-hook"; // kebab-case
"my_hook"; // underscore_casePrefix a hook name with first_ to run it only once per context (class or instance):
hooks: {
first_setup () {
// Only runs the first time setup() is called on this class
},
}Register a "*" hook to run on every hook invocation:
this.hooks.add("*", function (env) {
console.log(`Hook "${env.hookName}" fired`);
});Optionally, xtensible ships with a few tiny plugins that are useful for common use cases.
xtensible is not very opinionated about how you expose your plugin system and does not add any public API to your class by default.
If you want to expose public API for plugins (installedPlugins, hasPlugin(), addPlugin()), you can install the api plugin:
import { addPlugin, pluginsApi } from "xtensible";
class YourClass {
// ...
}
addPlugin(YourClass, pluginsApi);
YourClass.addPlugin(yourClassPlugin); // now works!This also allows consumer subclasses to define a plugins array and have these plugins installed automatically when the subclass is constructed (if a setup hook is defined).
If you find yourself defining a lot of hooks, you can install the $hook plugin to add a convenience instance and class method for running hooks.
import { addPlugin, $hook } from "xtensible";
class YourClass {
constructor () {
// ...
this.$hook("constructor", this);
}
static {
addPlugin(this, $hook);
}
}Supports common hooks for class creation, instance creation, and common lifecycle events:
constructor- When the class constructor is called (sync)constructed- runs after an instance is constructed (including any subclass constructors)setup- runs once per class
The installing class does still need to call the provided constructed() method in its constructor and ideally classes should call setup() to trigger the setup hook earlier than the first instance is constructed.
TBD - Coming soon