pagelet
Version:
pagelet
1,202 lines (1,054 loc) • 33.5 kB
JavaScript
'use strict';
var Formidable = require('formidable').IncomingForm
, fabricate = require('fabricator')
, helpers = require('./helpers')
, debug = require('diagnostics')
, dot = require('dot-component')
, destroy = require('demolish')
, Route = require('routable')
, fuse = require('fusing')
, async = require('async')
, path = require('path')
, url = require('url');
//
// Cache long prototype lookups to increase speed + write shorter code.
//
var slice = Array.prototype.slice;
//
// Methods that needs data buffering.
//
var operations = 'POST, PUT, DELETE, PATCH'.toLowerCase().split(', ');
/**
* Simple helper function to generate some what unique id's for given
* constructed pagelet.
*
* @returns {String}
* @api private
*/
function generator(n) {
if (!n) return Date.now().toString(36).toUpperCase();
return Math.random().toString(36).substring(2, 10).toUpperCase();
}
/**
* A pagelet is the representation of an item, section, column or widget.
* It's basically a small sandboxed application within your application.
*
* @constructor
* @param {Object} options Optional configuration.
* @api public
*/
function Pagelet(options) {
if (!this) return new Pagelet(options);
this.fuse();
options = options || {};
//
// Use the temper instance on Pipe if available.
//
if (options.bigpipe && options.bigpipe._temper) {
options.temper = options.bigpipe._temper;
}
this.writable('_enabled', []); // Contains all enabled pagelets.
this.writable('_disabled', []); // Contains all disable pagelets.
this.writable('_active', null); // Are we active.
this.writable('_req', options.req); // Incoming HTTP request.
this.writable('_res', options.res); // Incoming HTTP response.
this.writable('_params', options.params); // Params extracted from the route.
this.writable('_temper', options.temper); // Attach the Temper instance.
this.writable('_bigpipe', options.bigpipe); // Actual pipe instance.
this.writable('_bootstrap', options.bootstrap); // Reference to bootstrap Pagelet.
this.writable('_append', options.append || false); // Append content client-side.
this.writable('debug', debug('pagelet:'+ this.name)); // Namespaced debug method
//
// Allow overriding the reference to parent pagelet.
// A reference to the parent is normally set on the
// constructor prototype by optimize.
//
if (options.parent) this.writable('_parent', options.parent);
}
fuse(Pagelet, require('eventemitter3'));
/**
* Unique id, useful for internal querying.
*
* @type {String}
* @public
*/
Pagelet.writable('id', null);
/**
* The name of this pagelet so it can checked to see if's enabled. In addition
* to that, it can be injected in to placeholders using this name.
*
* @type {String}
* @public
*/
Pagelet.writable('name', '');
/**
* The HTTP pathname that we should be matching against.
*
* @type {String|RegExp}
* @public
*/
Pagelet.writable('path', null);
/**
* Which HTTP methods should this pagelet accept. It can be a comma
* separated string or an array.
*
* @type {String|Array}
* @public
*/
Pagelet.writable('method', 'GET');
/**
* The default status code that we should send back to the user.
*
* @type {Number}
* @public
*/
Pagelet.writable('statusCode', 200);
/**
* The pagelets that need to be loaded as children of this pagelet.
*
* @type {Object}
* @public
*/
Pagelet.writable('pagelets', {});
/**
* With what kind of generation mode do we need to output the generated
* pagelets. We're supporting 3 different modes:
*
* - sync: Fully render without any fancy flushing of pagelets.
* - async: Render all pagelets async and flush them as fast as possible.
* - pipeline: Same as async but in the specified order.
*
* @type {String}
* @public
*/
Pagelet.writable('mode', 'async');
/**
* Save the location where we got our resources from, this will help us with
* fetching assets from the correct location.
*
* @type {String}
* @public
*/
Pagelet.writable('directory', '');
/**
* The environment that we're running this pagelet in. If this is set to
* `development` It would be verbose.
*
* @type {String}
* @public
*/
Pagelet.writable('env', (process.env.NODE_ENV || 'development').toLowerCase());
/**
* Conditionally load this pagelet. It can also be used as authorization handler.
* If the incoming request is not authorized you can prevent this pagelet from
* showing. The assigned function receives 3 arguments.
*
* - req, the http request that initialized the pagelet
* - list, array of pagelets that will be tried
* - done, a callback function that needs to be called with only a boolean.
*
* ```js
* Pagelet.extend({
* if: function conditional(req, list, done) {
* done(true); // True indicates that the request is authorized for access.
* }
* });
* ```
*
*/
Pagelet.writable('if', null);
/**
* A pagelet has been initialized.
*
* @type {Function}
* @public
*/
Pagelet.writable('initialize', null);
/**
* Remove the DOM element if we are not enabled. This will make it easier to
* create conditional layouts without having to manage the pointless DOM
* elements.
*
* @type {Boolean}
* @public
*/
Pagelet.writable('remove', true);
/**
* List of keys in the data that will be supplied to the client-side script.
* Paths to nested keys can be supplied via dot notation.
*
* @type {Array}
* @public
*/
Pagelet.writable('query', []);
/**
* The location of your view template. But just because you've got a view
* template it doesn't mean we will render it. It depends on how the pagelet is
* called. If it's called from the client side we will only forward the data to
* server.
*
* As a user you need to make sure that your template runs on the client as well
* as on the server side.
*
* @type {String}
* @public
*/
Pagelet.writable('view', null);
/**
* The location of your error template. This template will be rendered when:
*
* 1. We receive an `error` argument from your `get` method.
* 2. Your view throws an error when rendering the template.
*
* If no view has been set it will default to the Pagelet's default error
* template which outputs a small HTML fragment that states the error.
*
* @type {String}
* @public
*/
Pagelet.writable('error', path.join(__dirname, 'error.html'));
/**
* Optional template engine preference. Useful when we detect the wrong template
* engine based on the view's file name. If no engine is provide we will attempt
* to figure out the correct template engine based on the file extension of the
* provided template path.
*
* @type {String}
* @public
*/
Pagelet.writable('engine', '');
/**
* The Style Sheet for this pagelet. The location can be a string or multiple paths
* in an array. It should contain all the CSS that's needed to render this pagelet.
* It doesn't have to be a `CSS` extension as these files are passed through
* `smithy` for automatic pre-processing.
*
* @type {String|Array}
* @public
*/
Pagelet.writable('css', '');
/**
* The JavaScript files needed for this pagelet. The location can be a string or
* multiple paths in an array. This file needs to be included in order for
* this pagelet to function.
*
* @type {String|Array}
* @public
*/
Pagelet.writable('js', '');
/**
* An array with dependencies that your pagelet depends on. This can be CSS or
* JavaScript files/frameworks whatever. It should be an array of strings
* which represent the location of these files.
*
* @type {Array}
* @public
*/
Pagelet.writable('dependencies', []);
/**
* Save the location where we got our resources from, this will help us with
* fetching assets from the correct location. This property is automatically set
* when the you do:
*
* ```js
* Pagelet.extend({}).on(module);
* ```
*
* If you do not use this pattern make sure you set an absolute path the
* directory that the pagelet and all it's resources are in.
*
* @type {String}
* @public
*/
Pagelet.writable('directory', '');
/**
* Reference to parent Pagelet name.
*
* @type {Object}
* @private
*/
Pagelet.writable('_parent', null);
/**
* Set of optimized children Pagelet.
*
* @type {Object}
* @private
*/
Pagelet.writable('_children', {});
/**
* Cataloged dependencies by extension.
*
* @type {Object}
* @private
*/
Pagelet.writable('_dependencies', {});
/**
* Default character set, UTF-8.
*
* @type {String}
* @private
*/
Pagelet.writable('_charset', 'UTF-8');
/**
* Default content type of the Pagelet.
*
* @type {String}
* @private
*/
Pagelet.writable('_contentType', 'text/html');
/**
* Default asynchronous get function. Override to provide specific data to the
* render function.
*
* @param {Function} done Completion callback when we've received data to render.
* @api public
*/
Pagelet.writable('get', function get(done) {
(global.setImmediate || global.setTimeout)(done);
});
/**
* Get parameters that were extracted from the route.
*
* @type {Object}
* @public
*/
Pagelet.readable('params', {
enumerable: false,
get: function params() {
return this._params || this.bootstrap._params || Object.create(null);
}
}, true);
/**
* Report the length of the queue (e.g. amount of children). The length
* is increased with one as the reporting pagelet is part of the queue.
*
* @return {Number} Length of queue.
* @api private
*/
Pagelet.get('length', function length() {
return this._children.length;
});
/**
* Get and initialize a given child Pagelet.
*
* @param {String} name Name of the child pagelet.
* @returns {Array} The pagelet instances.
* @api public
*/
Pagelet.readable('child', function child(name) {
if (Array.isArray(name)) name = name[0];
return (this.has(name) || this.has(name, true) || []).slice(0);
});
/**
* Helper to invoke a specific route with an optionally provided method.
* Useful for serving a pagelet after handling POST requests for example.
*
* @param {String} route Registered path.
* @param {String} method Optional HTTP verb.
* @returns {Pagelet} fluent interface.
*/
Pagelet.readable('serve', function serve(route, method) {
var req = this._req
, res = this._res;
req.method = (method || 'get').toUpperCase();
req.uri = url.parse(route);
this._bigpipe.router(req, res);
return this;
});
/**
* Helper to check if the pagelet has a child pagelet by name, must use
* prototype.name since pagelets are not always constructed yet.
*
* @param {String} name Name of the pagelet.
* @param {String} enabled Make sure that we use the enabled array.
* @returns {Array} The constructors of matching Pagelets.
* @api public
*/
Pagelet.readable('has', function has(name, enabled) {
if (!name) return [];
if (enabled) return this._enabled.filter(function filter(pagelet) {
return pagelet.name === name;
});
var pagelets = this._children
, i = pagelets.length
, pagelet;
while (i--) {
pagelet = pagelets[i][0];
if (
pagelet.prototype && pagelet.prototype.name === name
|| pagelets.name === name
) return pagelets[i];
}
return [];
});
/**
* Render execution flow.
*
* @api private
*/
Pagelet.readable('init', function init() {
var method = this._req.method.toLowerCase()
, pagelet = this;
//
// Only start reading the incoming POST request when we accept the incoming
// method for read operations. Render in a regular mode if we do not accept
// these requests.
//
if (~operations.indexOf(method)) {
var pagelets = this.child(this._req.query._pagelet)
, reader = this.read(pagelet);
this.debug('Processing %s request', method);
async.whilst(function work() {
return !!pagelets.length;
}, function process(next) {
var Child = pagelets.shift()
, child;
if (!(method in Pagelet.prototype)) return next();
child = new Child({ bigpipe: pagelet._bigpipe });
child.conditional(pagelet._req, pagelets, function allowed(accepted) {
if (!accepted) {
if (child.destroy) child.destroy();
return next();
}
reader.before(child[method], child);
});
}, function nothing() {
if (method in pagelet) {
reader.before(pagelet[method], pagelet);
} else {
pagelet._bigpipe[pagelet.mode](pagelet);
}
});
} else {
this._bigpipe[this.mode](this);
}
});
/**
* Start buffering and reading the incoming request.
*
* @returns {Form}
* @api private
*/
Pagelet.readable('read', function read() {
var form = new Formidable
, pagelet = this
, fields = {}
, files = {}
, context
, before;
form.on('progress', function progress(received, expected) {
//
// @TODO if we're not sure yet if we should handle this form, we should only
// buffer it to a predefined amount of bytes. Once that limit is reached we
// need to `form.pause()` so the client stops uploading data. Once we're
// given the heads up, we can safely resume the form and it's uploading.
//
}).on('field', function field(key, value) {
fields[key] = value;
}).on('file', function file(key, value) {
files[key] = value;
}).on('error', function error(err) {
pagelet.capture(err, true);
fields = files = {};
}).on('end', function end() {
form.removeAllListeners();
if (before) {
before.call(context, fields, files);
}
});
/**
* Add a hook for adding a completion callback.
*
* @param {Function} callback
* @returns {Form}
* @api public
*/
form.before = function befores(callback, contexts) {
if (form.listeners('end').length) {
form.resume(); // Resume a possible buffered post.
before = callback;
context = contexts;
return form;
}
callback.call(contexts || context, fields, files);
return form;
};
return form.parse(this._req);
});
//
// !IMPORTANT
//
// These function's & properties should never overridden as we might depend on
// them internally, that's why they are configured with writable: false and
// configurable: false by default.
//
// !IMPORTANT
//
/**
* Discover pagelets that we're allowed to use.
*
* @returns {Pagelet} fluent interface
* @api private
*/
Pagelet.readable('discover', function discover() {
var req = this._req
, res = this._res
, pagelet = this;
//
// We need to do an async map/filter of the pagelets, in order to this as
// efficient as possible we're going to use a reduce.
//
async.reduce(this._children, {
disabled: [],
enabled: []
}, function reduce(memo, children, next) {
children = children.slice(0);
var child, last;
async.whilst(function work() {
return children.length && !child;
}, function work(next) {
var Child = children.shift()
, test = new Child({
bootstrap: pagelet.bootstrap,
bigpipe: pagelet._bigpipe,
res: res,
req: req
});
test.conditional(req, children, function conditionally(accepted) {
if (last && last.destroy) last.destroy();
if (accepted) child = test;
else last = test;
next(!!child);
});
}, function found() {
if (child) memo.enabled.push(child);
else memo.disabled.push(last);
next(undefined, memo);
});
}, function discovered(err, children) {
pagelet._disabled = children.disabled;
pagelet._enabled = children.enabled;
pagelet._enabled.forEach(function initialize(child) {
if ('function' === typeof child.initialize) child.initialize();
});
pagelet.debug('Initialized all allowed pagelets');
pagelet.emit('discover');
});
return this;
});
/**
* Process the pagelet for an async or pipeline based render flow.
*
* @param {String} name Optional name, defaults to pagelet.name.
* @param {Mixed} chunk Content of Pagelet.
* @returns {Bootstrap} Reference to bootstrap Pagelet.
* @api private
*/
Pagelet.readable('write', function write(name, chunk) {
if (!chunk) {
chunk = name;
name = this.name;
}
this.debug('Queueing data chunk');
return this.bootstrap.queue(name, this._parent, chunk);
});
/**
* Close the connection once all pagelets are sent.
*
* @param {Mixed} chunk Fragment of data.
* @returns {Boolean} Closed the connection.
* @api private
*/
Pagelet.readable('end', function end(chunk) {
var pagelet = this;
//
// Write data chunk to the queue.
//
if (chunk) this.write(chunk);
//
// Do not close the connection before all pagelets are send.
//
if (this.bootstrap.length > 0) {
this.debug('Not all pagelets have been written, (%s out of %s)',
this.bootstrap.length, this.length
);
return false;
}
//
// Everything is processed, close the connection and clean up references.
//
this.bootstrap.flush(function close(error) {
if (error) return pagelet.capture(error, true);
pagelet.debug('Closed the connection');
pagelet._res.end();
});
return true;
});
/**
* Set or get the value of the character set, only allows strings.
*
* @type {String}
* @api public
*/
Pagelet.set('charset', function get() {
return this._charset;
}, function set(value) {
if ('string' !== typeof value) return;
return this._charset = value;
});
/**
* The Content-Type of the response. This defaults to text/html with a charset
* preset inherited from the charset property.
*
* @type {String}
* @api public
*/
Pagelet.set('contentType', function get() {
return this._contentType +';charset='+ this._charset;
}, function set(value) {
return this._contentType = value;
});
/**
* Returns reference to bootstrap Pagelet, which could be the Pagelet itself.
* Allows more chaining and valid bootstrap Pagelet references.
*
* @type {String}
* @public
*/
Pagelet.set('bootstrap', function get() {
return !this._bootstrap && this.name === 'bootstrap' ? this : this._bootstrap || {};
}, function set(value) {
if (value && value.name === 'bootstrap') return this._bootstrap = value;
});
/**
* Checks if we're an active Pagelet or if we still need to a do an check
* against the `if` function.
*
* @type {Boolean}
* @private
*/
Pagelet.set('active', function get() {
return 'function' !== typeof this.if // No conditional check needed.
|| this._active !== null && this._active; // Conditional check has been done.
}, function set(value) {
return this._active = !!value;
});
/**
* Helper method that proxies to the redirect of the BigPipe instance.
*
* @param {String} path Redirect URI.
* @param {Number} status Optional status code.
* @param {Object} options Optional options, e.g. caching headers.
* @returns {Pagelet} fluent interface.
* @api public
*/
Pagelet.readable('redirect', function redirect(path, status, options) {
this._bigpipe.redirect(this, path, status, options);
return this;
});
/**
* Proxy to return the compiled server template from Temper.
*
* @param {String} view Absolute path to the templates location.
* @param {Object} data Used to render the server-side template.
* @return {String} Generated HTML.
* @public
*/
Pagelet.readable('template', function template(view, data) {
if ('string' !== typeof view) {
data = view;
view = this.view;
}
return this._temper.fetch(view).server(data || {});
});
/**
* Render takes care of all the data merging and `get` invocation.
*
* Options:
*
* - context: Context on which to call `after`, defaults to pagelet.
* - data: stringified object representation to pass to the client.
* - pagelets: Alternate pagelets to be used when this pagelet is not enabled.
*
* @param {Object} options Add post render functionality.
* @param {Function} fn Completion callback.
* @returns {Pagelet}
* @api private
*/
Pagelet.readable('render', function render(options, fn) {
if ('undefined' === typeof fn) {
fn = options;
options = {};
}
options = options || {};
var framework = this._bigpipe._framework
, compiler = this._bigpipe._compiler
, context = options.context || this
, mode = options.mode || 'async'
, data = options.data || {}
, bigpipe = this._bigpipe
, temper = this._temper
, query = this.query
, pagelet = this
, state = {};
/**
* Write the fragmented data.
*
* @param {String} content The content to respond with.
* @returns {Pagelet}
* @api private
*/
function fragment(content) {
var active = pagelet.active;
if (!active) content = '';
if (mode === 'sync') return fn.call(context, undefined, content);
data.id = data.id || pagelet.id; // Pagelet id.
data.path = data.path || pagelet.path; // Reference to the path.
data.mode = data.mode || pagelet.mode; // Pagelet render mode.
data.remove = active ? false : pagelet.remove; // Remove from DOM.
data.parent = pagelet._parent; // Send parent name along.
data.append = pagelet._append; // Content should be appended.
data.remaining = pagelet.bootstrap.length; // Remaining pagelets number.
data.hash = { // Temper md5's for template ref
error: temper.fetch(pagelet.error).hash.client,
client: temper.fetch(pagelet.view).hash.client
};
fn.call(context, undefined, framework.get('fragment', {
template: content.replace(/<!--(.|\s)*?-->/, ''),
name: pagelet.name,
id: pagelet.id,
state: state,
data: data
}));
return pagelet;
}
return this.conditional(this._req, options.pagelets, function auth(enabled) {
if (!enabled) return fragment('');
//
// Invoke the provided get function and make sure options is an object, from
// which `after` can be called in proper context.
//
pagelet.get(function receive(err, result) {
var view = temper.fetch(pagelet.view).server
, content;
//
// Add some template defaults.
//
result = result || {};
if (!('path' in result)) result.path = pagelet.path;
//
// We've made it this far, but now we have to cross our fingers and HOPE
// that our given template can actually handle the data correctly
// without throwing an error. As the rendering is done synchronously, we
// wrap it in a try/catch statement and hope that an error is thrown
// when the template fails to render the content. If there's an error we
// will process the error template instead.
//
try {
if (err) {
pagelet.debug('Render %s/%s resulted in a error', pagelet.name, pagelet.id, err);
throw err; // Throw so we can capture it again.
}
content = view(result, { html: true });
} catch (e) {
if ('production' !== pagelet.env) {
pagelet.debug('Captured rendering error: %s', e.stack);
}
//
// This is basically fly or die, if the supplied error template throws
// an error while rendering we're basically fucked, your server will
// crash, an angry mob of customers with pitchforks will kick in the
// doors of your office and smear you with peck and feathers for not
// writing a more stable application.
//
if (!pagelet.error) return fn(e);
content = temper.fetch(pagelet.error).server(pagelet.merge(result, {
reason: 'Failed to render: '+ pagelet.name,
env: pagelet.env,
message: e.message,
stack: e.stack,
error: e
}), { html: true });
}
//
// Add queried parts of data, so the client-side script can use it.
//
if ('object' === typeof result && Array.isArray(query) && query.length) {
state = query.reduce(function find(memo, q) {
memo[q] = dot.get(result, q);
return memo;
}, {});
}
fragment(content);
});
});
});
/**
* Authenticate the Pagelet.
*
* @param {Request} req The HTTP request.
* @param {Function} list Array of optional alternate pagelets that take it's place.
* @param {Function} fn The authorized callback.
* @returns {Pagelet}
* @api private
*/
Pagelet.readable('conditional', function conditional(req, list, fn) {
var pagelet = this;
if ('function' !== typeof fn) {
fn = list;
list = [];
}
/**
* Callback for the `pagelet.if` function to see if we're enabled or disabled.
* Use cached value in _active to prevent the same Pagelet being authorized
* multiple times for the same request.
*
* @param {Boolean} value Are we enabled or disabled.
* @api private
*/
function enabled(value) {
fn.call(pagelet, pagelet.active = value || false);
}
if ('boolean' === typeof pagelet._active) {
fn(pagelet.active);
} else if ('function' !== typeof this.if) {
fn(pagelet.active = true);
} else {
if (pagelet.if.length === 2) pagelet.if(req, enabled);
else pagelet.if(req, list, enabled);
}
return pagelet;
});
/**
* Destroy the pagelet and remove all the back references so it can be safely
* garbage collected.
*
* @api public
*/
Pagelet.readable('destroy', destroy([
'_temper', '_bigpipe', '_enabled', '_disabled', '_children'
], {
after: 'removeAllListeners'
}));
/**
* Expose the Pagelet on the exports and parse our the directory. This ensures
* that we can properly resolve all relative assets:
*
* ```js
* Pagelet.extend({
* ..
* }).on(module);
* ```
*
* The use of this function is for convenience and optional. Developers can
* choose to provide absolute paths to files.
*
* @param {Module} module The reference to the module object.
* @returns {Pagelet}
* @api public
*/
Pagelet.on = function on(module) {
var prototype = this.prototype
, dir = prototype.directory = path.dirname(module.filename);
//
// Resolve the view and error templates to ensure
// absolute paths are provided to Temper.
//
if (prototype.error) prototype.error = path.resolve(dir, prototype.error);
if (prototype.view) prototype.view = path.resolve(dir, prototype.view);
return module.exports = this;
};
/**
* Discover all pagelets recursive. Fabricate will create constructable
* instances from the provided value of prototype.pagelets.
*
* @param {String} parent Reference to the parent pagelet name.
* @return {Array} collection of pagelets instances.
* @api public
*/
Pagelet.children = function children(parent, stack) {
var pagelets = this.prototype.pagelets
, log = debug('pagelet:'+ parent);
stack = stack || [];
return fabricate(pagelets, {
source: this.prototype.directory,
recursive: 'string' === typeof pagelets
}).reduce(function each(stack, Pagelet) {
//
// Pagelet could be conditional, simple crawl this function
// again to get the children of each conditional.
//
if (Array.isArray(Pagelet)) return Pagelet.reduce(each, []);
var name = Pagelet.prototype.name;
log('Recursive discovery of child pagelet %s', name);
//
// We need to extend the pagelet if it already has a _parent name reference
// or will accidentally override it. This can happen when you extend a parent
// pagelet with children and alter the parent's name. The extended parent and
// regular parent still point to the same child pagelets. So when we try to
// set the proper parent, these pagelets will override the _parent property
// unless we create a new fresh instance and set it on that instead.
//
if (Pagelet.prototype._parent && name !== parent) {
Pagelet = Pagelet.extend();
}
Pagelet.prototype._parent = parent;
return Pagelet.children(name, stack.concat(Pagelet));
}, stack);
};
/**
* Optimize the prototypes of Pagelets to reduce work when we're actually
* serving the requests via BigPipe.
*
* Options:
* - temper: A custom temper instance we want to use to compile the templates.
*
* @param {Object} options Optimization configuration.
* @param {Function} next Completion callback for async execution.
* @api public
*/
Pagelet.optimize = function optimize(options, done) {
if ('function' === typeof options) {
done = options;
options = {};
}
var stack = []
, Pagelet = this
, bigpipe = options.bigpipe || {}
, transform = options.transform || {}
, temper = options.temper || bigpipe._temper
, before, after;
//
// Check if before listener is found. Add before emit to the stack.
// This async function will be called before optimize.
//
if (bigpipe._events && 'transform:pagelet:before' in bigpipe._events) {
before = bigpipe._events['transform:pagelet:before'].length || 1;
stack.push(function run(next) {
var n = 0;
transform.before(Pagelet, function ran(error, Pagelet) {
if (error || ++n === before) return next(error, Pagelet);
});
});
}
//
// If transform.before was not pushed on the stack, optimizer needs
// to called with a reference to Pagelet.
//
stack.push(!stack.length ? async.apply(optimizer, Pagelet) : optimizer);
//
// Check if after listener is found. Add after emit to the stack.
// This async function will be called after optimize.
//
if (bigpipe._events && 'transform:pagelet:after' in bigpipe._events) {
after = bigpipe._events['transform:pagelet:after'].length || 1;
stack.push(function run(Pagelet, next) {
var n = 0;
transform.after(Pagelet, function ran(error, Pagelet) {
if (error || ++n === after) return next(error, Pagelet);
});
});
}
//
// Run the stack in series. This ensures that before hooks are run
// prior to optimizing and after hooks are ran post optimizing.
//
async.waterfall(stack, done);
/**
* Optimize the pagelet. This function is called by default as part of
* the async stack.
*
* @param {Function} next Completion callback
* @api private
*/
function optimizer(Pagelet, next) {
var prototype = Pagelet.prototype
, method = prototype.method
, status = prototype.status
, router = prototype.path
, name = prototype.name
, view = prototype.view
, log = debug('pagelet:'+ name);
//
// Generate a unique ID used for real time connection lookups.
//
prototype.id = options.id || [0, 1, 1, 1].map(generator).join('-');
//
// Parse the methods to an array of accepted HTTP methods. We'll only accept
// these requests and should deny every other possible method.
//
log('Optimizing pagelet');
if (!Array.isArray(method)) method = method.split(/[\s\,]+?/);
Pagelet.method = method.filter(Boolean).map(function transformation(method) {
return method.toUpperCase();
});
//
// Add the actual HTTP route and available HTTP methods.
//
if (router) {
log('Instantiating router for path %s', router);
Pagelet.router = new Route(router);
}
//
// Prefetch the template if a view is available. The view property is
// mandatory for all pagelets except the bootstrap Pagelet or if the
// Pagelet is just doing a redirect. We can resolve this edge case by
// checking if statusCode is in the 300~ range.
//
if (!view && name !== 'bootstrap' && !(status >= 300 && status < 400)) return next(
new Error('The '+ name +' pagelet should have a .view property.')
);
//
// Resolve the view to ensure the path is correct and prefetch
// the template through Temper.
//
if (view) {
prototype.view = view = path.resolve(prototype.directory, view);
temper.prefetch(view, prototype.engine);
}
//
// Ensure we have a custom error pagelet when we fail to render this fragment.
//
if (prototype.error) {
temper.prefetch(prototype.error, path.extname(prototype.error).slice(1));
}
//
// Map all dependencies to an absolute path or URL.
//
helpers.resolve(Pagelet, ['css', 'js', 'dependencies']);
//
// Find all child pagelets and optimize the found children.
//
async.map(Pagelet.children(name), function map(Child, step) {
if (Array.isArray(Child)) return async.map(Child, map, step);
Child.optimize({
temper: temper,
bigpipe: bigpipe,
transform: {
before: bigpipe.emits && bigpipe.emits('transform:pagelet:before'),
after: bigpipe.emits && bigpipe.emits('transform:pagelet:after')
}
}, step);
}, function optimized(error, children) {
log('optimized all %d child pagelets', children.length);
if (error) return next(error);
//
// Store the optimized children on the prototype, wrapping the Pagelet
// in an array makes it a lot easier to work with conditional Pagelets.
//
prototype._children = children.map(function map(Pagelet) {
return Array.isArray(Pagelet) ? Pagelet : [Pagelet];
});
//
// Always return a reference to the parent Pagelet.
// Otherwise the stack of parents would be infested
// with children returned by this async.map.
//
next(null, Pagelet);
});
}
};
//
// Expose the pagelet.
//
module.exports = Pagelet;