Plugins
If you happen to stumble here, this bit of the documentation is under active construction and should not be used at all. Please help us improve the project and the docs. </blockquote>
Create a plugin
API
Here comes the big one, plugins. First, a wish list. I'd like for a plugin to have this API available:
module.exports = {
// This is working right now (highly unstable)
// String
name: 'whatever',
// Object
config: {}
// Function, Array
init: () => {},
// Function, Array
before: () => {},
// Function, Array
after: () => {},
// Function, Array
final: () => {},
// Not working yet but desirable:
// Function (named 'whatever' like the plugin), Object with { name: fn } pairs
router: () => {},
// Function (named 'whatever' like the plugin), Object with { name: fn } pairs
reply: () => {}
};Now, I am not 100% it makes sense to open router and reply right now. I think there are some situations where it'd be really useful, like sending a PDF back for example:
// Send a pdf from server
server(ctx => pdf('./readme.pdf'));But there are many options here and doing it one way might limit some other options. So my first question:
Simple example: database
Why are these useful? Isn't is enough with middleware?
Well no, for instance for database connections it is really useful. Let's say we develop a @server/mongoose and install it with npm install @server/mongoose. Afterwards, just passing the options and we have access to the db through the context:
server({ mongoose: 'url for mongodb (or in .env)' },
get('/', ctx => 'Hello world'),
get('/sales', hasUser, ctx => ctx.db.sales.find({ user: ctx.user.id }))
});This can be applied to anything that has to be connected or configured once initially and later on can be used in the middleware.
Advanced example: sass
It also opens up to new possibilities, let's see a small example with sass. Let's say that we want to make a sass plugin that rebuilds the whole thing on each request for dev and only once on production:
module.exports = {
name: 'sass',
options: {
__root: 'source',
source: {
default: 'style/style.scss',
type: String,
file: true
},
destination: {
default: 'public/style.css',
type: String
}
},
init: async ctx => {
// If `ctx.options.sass.destination` exists and was not generated by @server/sass
// throw an early error and ask for the file to be (re)moved
// Remove the `ctx.options.sass.destination` file
if (ctx.options.env === 'production') {
// Compile everything and store it in `ctx.options.sass.destination`
}
},
// This will only get called in dev+test, since `style.css` will be found in production
before: get('/style.css', async ctx => {
// Reply with the whole `ctx.options.sass.source` compiled dynamically
})
};To use it is really simple. First npm install @server/sass, then if your options are the default ones you won't even need to write any specific javascript for it. Let's say though that we want to change our source file, which is the root option:
server({ sass: './front/style.sass' }, ctx => render('index'));That's it, with the flexibility of plugins you wouldn't need any more code to have a sass plugin.
This is why I think plugins can be really awesome if they are built and documented properly. What plugin would you like to see?
Options
A small exploration about how the options for plugins might look like for a developer of a plugin. Now I've written quite a few and have a better idea of the possibilities and limitations of them. I will be using log as an example. For the simple way with defaults:
plugin.options = {
level: { default: 'info' },
reporter: { default: process.stdout },
__root: 'level'
};There are no mandatory fields, however setting a default is strongly recommended. The last root bit would make both of these usages equivalent when using the plugin log:
server({
log: 'info'
});
server({
log: { level: 'info' }
});The .env is also quite straightforward in this situation thanks to the __root option. Both of these are equivalent as well:
# Single option
LOG=info
# Multiple options (note: cannot do a function here though!)
LOG_LEVEL=infoNow you might want all bells and whistles going on. For instance, let's define the type of the parameter. This will add a small validate function internally:
log.options = {
level: {
default: 'info',
type: String
}
};List of advanced options with their defaults inspired by Mongoose. First, options to use the correct variable:
default: the default to set in case it is not set. Leave it unset and it won't have a value if it is not explicitly set.env: NAME || true: defines the name for that variable in the environment variables. If set to false, it will not accept it through the environment.arg: NAME || true: defines the key of the value for options inserver(OPTIONS). If set to false it will not accept it from the options object (some arguments that MUST only be accepted through the environment).inherit: NAME || false: passing a name, it inherits the value from a global variable by this order of preference: [validate:] specific environment > global environment > specific argument > global argument > specific default > global default.find: FN || false: a function that receives the options passed on the main function, then all of the environment and finally the default. It returns the wanted value.extend: true || false: extend the default value for the unwritten properties with the default props if they are not set. Value passed:{ main: 'a', second: 'b' },{ default: { second: 'c', third: 'd' }, extend: true }=>{ main: 'a', second: 'b', third: 'd' }.
Then you can perform several validations:
required: FN || false: make sure the option is set before proceeding. This doesn't make sense whendefaultis set.type: false: define the type of the variable. A type or an array of types. Will only check the primitive typesBoolean,Number,String,Array,Object. Can be also an array of types.enum: ['a', 'b']: the variable should be within the list.validate: FN || false: defines a function to validate the value. It will accept first the current value, then all the currently set values and must returntruefor a valid value orfalseotherwise. Note: this is done AFTER any of the other specific checks like type check or the enumerate check.
Note: all of the functions described here can be either synchronous or asynchronous by returning a Promise (or using the
asynckeyword).
TODO: check the engine for mongoose to see if it makes sense to extract the validation part.
Routes for Plugins
I have long been wondering whether the routes and reply should be extensible. There are advantages and disadvantages to this.
On one hand, we can stick to more traditional requests workflow: get, post, put, delete and socket seem like a really good scope.
But then, if you think about plugins and the possibilities there is so much more we can do and make a nice abstraction layer.
For example, let's say you are making a LINE bot and there is a LINE plugin for server. This plugin can behave as normal:
server(
..
post('/line', ctx => {
ctx.line.sendMessage('Hello world');
})
);This is the traditional way. But if we don't limit ourselves to that, we could be doing it one abstraction level up where the implementation details are invisible:
const { line } = server.router;
const { message } = server.reply;
server(
line(ctx => {
return message('Hello world');
})
);One of the big issues here would be namespacing. You might want to have a line router but also a line reply, and the same with some of the others I can think right now: sms, email, etc.
In the example above, you might be tempted to name your reply message(), but so might other plugin.
Possible solutions: for the plugins you don't get the router from the main server router, but you get it from the plugin:
const line = require('@server/line');
server(
line.router(ctx => {
return line.reply.message();
})
);This feels too verbose for the otherwise succint server sintax, but right now seems like the best solution.
Another solution would be namespacing the whole server in general. This would also ease one of the pain points I am finding more and more frequently: having to import every route/reply I want to use manually:
const server = require('server');
const { router, reply } = server;
server(
router.get('/', ctx => reply.render(...)),
router.post('/', ctx => reply.json(...))
);While it is a bit more verbose, it cuts down on the import logic (favoring smaller files), makes things more explicit and clear and it's really compatible with this idea of plugins:
const server = require('server');
const { router, reply } = server;
server(
router.get('/', ctx => reply.render(...)),
router.post('/', ctx => reply.json(...)),
router.line(ctx => reply.line(...)),
// OR
router.line(ctx => reply.line.message(...))
);