Two of them. The first answers "what is this library", the second answers "how do I build something with it".
node hello/index.js # a service, a consumer, and nothing else
node app/index.js # a small app in the shape these are built in
node app/test.js # and how an app in that shape is testedNo dependencies, node built-ins included. The plugin files import nothing at all --
everything they use arrives through imports -- so the only thing a plugin here depends
on is another plugin. Only the boot imports the plugins, and the tests import the
harness.
Two plugins and a boot. plugin_b provides hello, plugin_a consumes it, and
index.js hands both to Rectify. Thirty seconds, and it is the same thing the top of the
main README walks through.
app/
index.js the boot: gather the plugins, start, drive
test.js the same boot, with the test plugins added
config.js settings, keyed by the service name each plugin provides
plugins/
log/ provides log, consumes only the base class
server/ provides server -- route() is where other plugins attach
time/ consumes server, extends it, provides time
status/ consumes server and time, asks time, provides nothing
A tiny app, kept deliberately unimpressive, so that what it demonstrates is the wiring
rather than the features. There is no HTTP in it: server.request is an in-memory
dispatch, which means the file that would become a real server is one plugin, and nothing
around it would change.
A service is not only functions to call -- it is where other plugins attach. server
registers a route method, and the plugins that consume it add their own routes as they
load:
// plugins/time/plugin.js
export default function setup(imports, register) {
var { Plugin, server, log } = imports;
var plugin = new Plugin("time");
server.route("GET", "/time", function () { // extending another plugin
return { now: now() };
});
register(null, { // and offering its own
time: plugin.api({ now: now, uptime: uptime }),
onDestroy: plugin.unload
});
}
setup.consumes = ["Plugin", "server", "log"];
setup.provides = ["time"];// plugins/status/plugin.js -- asks the time plugin rather than reading a clock
export default function setup(imports, register) {
var { server, time, log } = imports;
server.route("GET", "/status", function () {
return { now: time.now(), uptimeMs: time.uptime(), ok: true };
});
register();
}
setup.consumes = ["app", "server", "time", "log"];
setup.provides = [];Neither file mentions the other's path, and nothing declares that log loads before
server before time before status -- that order falls out of what each one declares.
index.js lists them backwards on purpose. Adding a plugin means adding it to that list,
never finding the right place in it.
Three of the four are built on PluginBase, which is why they
consume "Plugin" and hand plugin.api({...}) to register. status is not, on
purpose: it registers no service, so it has nothing to state a surface for and nothing to
undo. The base class is optional, and easier to believe as optional when something in
front of you is not using it.
Two things shape an app built this way:
- A plugin extends another during its own setup, so a provider must not start doing
its job until the load is finished, or it serves 404s for whichever plugin had not
registered yet. The server waits for the
"ready"its PluginBase instance announces -- which is whyindex.jsnever opens it. - A plugin that provides nothing is normal.
statusexists only to add a route to someone else. It still declaresprovides: []and still callsregister().
A test is a plugin. There is a plugin.test.js beside each plugin, and each one is an
ordinary Rectify plugin that consumes the thing it tests:
// plugins/time/plugin.test.js
import harness from "../../../../harness.js";
var { describe, it, assert } = harness;
export default function setup(imports, register) {
var { time, server } = imports;
describe("time", function () {
it("provides a clock", function () {
assert.ok(typeof time.now === "function");
});
it("added its route to the server", async function () {
var answered = await server.request("GET", "/api/time");
assert.equal(answered.status, 200);
});
});
register();
}
setup.consumes = ["time", "server"];
setup.provides = [];Declaring consumes: ["time"] is what hands it the real service and guarantees it loads
after it. Nothing had to be wired up to make a plugin testable -- the wiring that builds
the app is the wiring that runs the tests.
Loading and running stay two separate moments. describe and it only register a
suite while the app loads; the bodies run afterwards, when the boot asks the harness for
them. That matters because most of what is worth checking is not true yet while a plugin
loads -- the server refuses requests until the app is up. Its own suite leans on exactly
that: the request it fires during setup is asserted, later, to have been refused.
test.js is index.js with the test plugins added to the
list:
var plugins = [
status, time, server, log, rectify.PluginBase,
logTest, serverTest, timeTest, statusTest
];
plugins.config = config;
var app = rectify.build(plugins, { appName: "example-app" });
await app.start();
var results = await harness.run();It also holds the checks no single plugin can make about itself: that the graph came out
whole, that dropping a plugin fails build() before any plugin body runs, and that
swapping one plugin changes the answer and nothing else. Substitution needs no library --
a double is a plugin providing the same name, so it is one entry in the list:
var plugins = [status, frozenTime, server, log, rectify.PluginBase]; // instead of `time`which is also the only way to say exactly what status answers, since the real clock
moves.