en-route
Version:
Routing for static site generators, build systems and task runners, heavily based on express.js routes but works with file objects. Used by Assemble, Verb, and Template.
171 lines (148 loc) • 3.85 kB
JavaScript
'use strict';
const Emitter = require('events');
const Layer = require('./layer');
/**
* Create a new `Route` with the given pattern, handler functions and options.
*
* ```js
* const fn = file => file.count++;
* const Route = require('en-route').Route;
* const route = new Route('/(.*)', [fn, fn, fn]);
* const file = { path: '/foo', count: 0 };
*
* route.handle(file)
* .then(file => {
* console.log(file.count); // 3
* });
* ```
* @name Route
* @extends {Class} EventEmitter
* @param {string|regex} `pattern`
* @param {function|array} `fns` One or more middleware functions.
* @param {object} `options`
* @api public
*/
class Route extends Emitter {
constructor(pattern, fns, options = {}) {
super();
this.pattern = pattern;
this.options = options;
this.stack = [];
this.layers(this.pattern, fns, this.options);
if (this.options.sync) {
this.handle = handle.bind(null, this);
}
}
/**
* Register one or more handler functions to be called on all
* layers on the route.
*
* ```js
* route.all(function(file) {
* file.data.title = 'Home';
* });
* route.all([
* function(file) {},
* function(file) {}
* ]);
* ```
* @name .all
* @param {function|array} `fns` Handler function or array of handler functions.
* @return {object} Route instance for chaining
* @api public
*/
all(fns) {
return this.layers('/', fns);
}
/**
* Run a middleware stack on the given `file`.
*
* ```js
* route.handle(file)
* .then(file => console.log('File:', file))
* .catch(console.error);
* ```
* @name .handle
* @param {object} `file` File object
* @return {object} Callback that exposes `err` and `file`
* @return {object} Returns a promise with the file object.
* @api public
*/
async handle(file) {
this.status = 'starting';
this.emit('handle', file);
let pending = [];
for (let layer of this.stack) {
this.emit('layer', layer, file);
if (this.options.parallel) {
pending.push(layer.handle(file));
} else {
await layer.handle(file);
}
}
if (this.options.parallel) await Promise.all(pending);
this.status = 'finished';
this.emit('handle', file);
return file;
}
/**
* Push a layer onto the stack for a middleware functions.
*
* ```js
* route.layer(/foo/, file => {
* // do stuff to file
* file.layout = 'default';
* });
* ```
* @name .layer
* @param {string|regex} `pattern` The pattern to use for matching files to determin if they should be handled.
* @param {function|array} `fn` Middleware functions
* @return {object} Route instance for chaining
* @api public
*/
layer(pattern, fn) {
this.stack.push(new Layer(pattern, fn, this.options));
return this;
}
/**
* Push a layer onto the stack for one or more middleware functions.
*
* ```js
* route.layers(/foo/, function);
* route.layers(/bar/, [function, function]);
* ```
* @name .layers
* @param {string|regex} `pattern`
* @param {function|array} `fns` One or more middleware functions
* @return {object} Route instance for chaining
* @api public
*/
layers(pattern, fns) {
for (const fn of arrayify(fns)) this.layer(pattern, fn);
return this;
}
}
/**
* Sync method, used when options.sync is true
*/
function handle(route, file) {
route.status = 'starting';
route.emit('handle', file);
for (let layer of route.stack) {
route.emit('layer', layer, file);
layer.handle(file);
}
route.status = 'finished';
route.emit('handle', file);
return file;
}
/**
* Cast `val` to an array
*/
function arrayify(val) {
return val ? (Array.isArray(val) ? val : [val]) : [];
}
/**
* Expose `Route`
*/
module.exports = Route;