Fusion.js provides official plugins for a wide variety of tasks, and it's possible to write complex applications without ever writing a single custom plugin. With that said, it's possible you might find that there's no plugin available for a task you're trying to accomplish, or that you don't agree with the opinions of an existing plugin. This section explains the Fusion.js plugin architecture and how to implement various types of plugins.
Create a plugin with createPlugin
:
import {createPlugin} from 'fusion-core';
export default createPlugin({
deps: dependencies,
provides() {
return service;
}
middleware() {
return middleware;
}
});
The createPlugin
function accepts three optional named parameters: deps
, provides
, and middleware
.
deps: Object
- a map of dependenciesprovides: (deps: Object) => T
- receives resolved dependencies as named arguments and returns a servicemiddleware: (deps: Object, service: T) => (ctx: FusionContext, next: () => Promise) => Promise
- receives dependencies and the provided service and returns a middlewarePlugins in Fusion.js exist to encapsulate all code required to address a logical area of concern, regardless of whether the code runs server-side, in the browser, on a per-request basis, on multiple HTTP endpoints, or whether it affects React context.
At the same time, plugins are designed so that dependencies are injectable, and therefore modular and testable.
Examples of areas of concern that a plugin can encapsulate include CSS-in-JS, REST endpoints, RPC, CSRF protection, translations, etc.
Let's review the three parts to a plugin.
A dependency is anything that has a programmatic API that can be consumed by another part of your web application, and that you might reasonably want to mock in a test.
In Fusion.js, dependencies are registered to tokens via app.register
:
// src/main.js
import App from 'fusion-react';
import {LoggerToken} from 'fusion-tokens';
export default () => {
const app = new App();
app.register(LoggerToken, console); // register a logger
return app;
};
Plugins can signal that they depend on something by referencing a token. Let's suppose there's an Example
plugin that looks like this:
// src/plugins/example.js
import {LoggerToken} from 'fusion-tokens';
export default createPlugin({
deps: {logger: LoggerToken},
});
The code above means that the Example
plugin depends on whatever LoggerToken
represents. In other words, the following entry point would throw an error about a missing LoggerToken
dependency:
// src/main.js
import App from 'fusion-react';
import Example from './plugins/example.js';
export default () => {
const app = new App();
app.register(Example);
return app;
};
// throws 'Cannot resolve to a default value of 'undefined' for token: LoggerToken'
The error occurs because we've specified that LoggerToken
is a dependency of the Example
plugin, via its deps
field. To resolve the error, you would then need to register the dependency:
// src/main.js
import App from 'fusion-react';
import {LoggerToken} from 'fusion-tokens';
import Example from './plugins/example.js';
export default () => {
const app = new App();
app.register(Example);
app.register(LoggerToken, console);
return app;
};
// everything's peachy now
Note that nothing depends on Example
, so we don't need to register it to a token. Later, we'll look into how we can make the Example
plugin do something with the logger it depends on.
Also note that we can register dependencies in any order. In this case, we registered console
after Example
even though Example
depends on console
.
One benefit of registering console
as a dependency is that we can mock it in tests, by simply re-registering something else to the LoggerToken
.
// src/__tests__/index.js
import test from 'tape-cup';
import createApp from '../main.js';
test('my test', t => {
const app = createApp();
// override the logger with a mock
app.register(LoggerToken, noopLogger);
// now we can run an end-to-end test without polluting logs
t.end();
});
If we had hard-coded console
everywhere, it would be difficult to mock it in an integration or e2e test.
Plugins can provide a programmatic interface and be registered as dependencies for other plugins via provides
.
// src/plugins/example.js
import {LoggerToken} from 'fusion-tokens';
import {createToken} from 'fusion-core';
export const ExampleToken = createToken('ExampleToken');
export default createPlugin({
provides() {
return {
sayHello() {
console.log('hello world');
},
};
},
});
The example above exports a plugin that resolves to an object with a sayHello
method.
It also exports a token called ExampleToken
, which can be used by other plugins that want to depend on the Example
plugin.
We can expand the Example
plugin above to consume the logger that we registered.
// src/plugins/example.js
import {LoggerToken} from 'fusion-tokens';
import {createToken} from 'fusion-core';
export const ExampleToken = createToken('ExampleToken');
export default createPlugin({
deps: {logger: LoggerToken},
provides({logger}) {
return {
sayHello() {
logger.log('hello world');
},
};
},
});
Similarly, if we wanted to log "hello world"
, we can create another plugin that depends on the Example
plugin:
// src/plugins/foo.js
import {createToken} from 'fusion-core';
import {ExampleToken} from './example.js';
export default createPlugin({
deps: {example: ExampleToken},
provides({example}) {
example.sayHello();
},
});
And then register the dependencies accordingly:
// src/main.js
import App from 'fusion-react';
import {LoggerToken} from 'fusion-tokens';
import Example, {ExampleToken} from './plugins/example.js';
import Foo from './plugins/foo.js';
export default () => {
const app = new App();
app.register(LoggerToken, console);
app.register(ExampleToken, Example);
app.register(Foo);
return app;
};
One of the most common use cases for creating a plugin for an application is to implement HTTP endpoints.
To do so, a plugin would look like this:
// src/api/hello.js
export default createPlugin({
middleware() {
return (ctx, next) => {
if (ctx.method === 'POST' && ctx.path === '/api/hello') {
ctx.body = {greeting: 'hello'};
}
return next();
}
},
}
Just like the provides
method, the middleware
method can receive dependencies as arguments.
For example, let's say we want to inject a logger:
// src/api/hello.js
import {LoggerToken} from 'fusion-tokens';
export default createPlugin({
deps: {console: LoggerToken}
middleware({console}) {
return (ctx, next) => {
if (ctx.method === 'POST' && ctx.path === '/api/hello') {
ctx.body = {greeting: 'hello'};
console.log('hello');
}
return next();
}
}
};
The middleware
method also receives the return value of provides
as its second argument, which allows the middleware to consume the programmatic API that the plugin provides:
// src/plugins/example.js
import {LoggerToken} from 'fusion-tokens';
import {createToken} from 'fusion-core';
export const ExampleToken = createToken('ExampleToken');
export default createPlugin({
deps: {logger: LoggerToken},
provides({logger}) {
return {
sayHello() {
logger.log('hello world');
},
};
},
middleware({logger}, greeter) {
return (ctx, next) => {
greeter.sayHello();
return next();
};
},
});
On the server, the middleware function is a Koa.js middleware, with a few additional Fusion.js-specific properties. A middleware represents the lifecycle of an HTTP request.
On the browser, the middleware function represents the timeline of what happens during page load.
Koa middlewares are functions that receive a ctx
object and a next
function as arguments. The next
function should be called once by the function, and the return value of the function should be a promise.
In a nutshell, the Koa ctx
object has properties for various HTTP values (url
, method
, headers
, etc), and next
is an async function that the middleware is responsible for calling.
In Fusion.js, the next()
call represents the time when virtual DOM rendering happens. Typically, you'll want to run all your logic before that, and simply have a return next()
statement at the end of the function. Even in cases where virtual DOM rendering is not applicable, this pattern is still the simplest way to write a middleware.
In a few more advanced cases, however, you might want to do things after virtual dom rendering. In that case, you can call await next()
instead:
export default createPlugin({
middleware() {
return __NODE__ && async (ctx, next) => {
// this happens before virtual dom rendering
const start = new Date();
await next();
// this happens after virtual rendering, but before the response is sent to the browser
console.log('timing: ', new Date() - start);
}
}
});
The next
function should normally be called once - and only once - per middleware call. We recommend avoiding complex conditional trees to prevent unexpected bugs that could occur when the function inadvertently gets called multiple times (resulting in an error), or cases where it doesn't get called at all.
It's important to keep in mind that the middleware stack will remain in a pending status if you forget to call return next()
or will potentially behave erratically if you break the promise chain (for example, by forgetting to use async/await
or by forgetting to return
in a non-async function). Breaking the promise chain is useful in a few select obscure cases, for example, short-circuiting the stack when dealing with static assets, but can lead to surprising behavior if done inadvertently.
If things appear to hang or give you a blank screen, make sure you called return next()
in your middleware.