@balderdash/sails-edge
Version:
API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
339 lines (272 loc) • 11.8 kB
JavaScript
/**
* Module dependencies
*/
var path = require('path');
var util = require('util');
var _ = require('lodash');
/**
* Adds res.view() method (an enhanced version of res.render) to response object.
* `res.view()` automatically renders the appropriate view based on the calling middleware's source route
* Note: the original function is still accessible via res.render()
*
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
module.exports = function _addResViewMethod(req, res, next) {
var sails = req._sails;
// TODO: don't pass `next` into this method impl to avoid confusing situations.
// i.e. wrap it up:
// ```
// function (req,res,next) { _addResViewMethod(req,res); next(); }
// ```
/**
* res.guessView([locals], [couldNotGuessCb])
*
* @param {Object} locals
* @param {Function} couldNotGuessCb
*/
res.guessView = function (locals, couldNotGuessCb) {
return res.view(locals, function viewReady(err, html) {
// If this is an "implied view doesn't exist" error,
// just serve JSON instead.
if (err && (err.code === 'E_VIEW_INFER' || err.code === 'E_VIEW_FAILED')) {
return (couldNotGuessCb||res.serverError)(err);
}
// But if some other sort of error occurred, call `res.serverError`
else if (err) return res.serverError(err);
// Otherwise we're good, serve the view
return res.send(html);
});
};
/**
* res.view([specifiedPath|locals], [locals])
*
* @param {String} specifiedPath
* -> path to the view file to load (minus the file extension)
* relative to the app's `views` directory
* @param {Object} locals
* -> view locals (data that is accessible in the view template)
* @param {Function} cb_view(err)
* -> called when the view rendering is complete (response is already sent, or in the process)
* (probably should be @api private)
* @api public
*/
res.view = function(/* specifiedPath, locals, cb_view */) {
var specifiedPath = arguments[0];
var locals = arguments[1];
var cb_view = arguments[2];
// sails.log.silly('Running res.view() with arguments:',arguments);
// By default, generate a path to the view using what we know about the controller+action
var relPathToView;
// Ensure req.target is an object, then merge it into req.options
// (this helps ensure backwards compatibility for users who were relying
// on `req.target` in previous versions of Sails)
req.options = _.defaults(req.options, req.target || {});
// Try to guess the view by looking at the controller/action
if (!req.options.view && (req.options.controller || req.options.model)) {
relPathToView = (req.options.controller||req.options.model) + '/' + (req.options.action || 'index');
}
// Use the new view config
else relPathToView = req.options.view;
// Now we have a reasonable guess in `relPathToView`
// If the path to a view was explicitly specified, use that
// Serve the view specified
//
// If first arg is not an obj or function, treat it like the path
if (specifiedPath && !_.isObject(specifiedPath) && !_.isFunction(specifiedPath)) {
relPathToView = specifiedPath;
}
// If the "locals" argument is actually the "specifiedPath"
// give em the old switcheroo
if (!relPathToView && _.isString(arguments[1])) {
relPathToView = arguments[1] || relPathToView;
}
// If first arg ISSSSS AN object, treat it like locals
if (_.isObject(arguments[0])) {
locals = arguments[0];
}
if (_.isFunction(arguments[1])) {
cb_view = arguments[1];
}
// if (_.isFunction(locals)) {
// cb_view = locals;
// locals = {};
// }
// if (_.isFunction(specifiedPath)) {
// cb_view = specifiedPath;
// }
// If a view path cannot be inferred, send back an error instead
if (!relPathToView) {
var err = new Error();
err.name = 'Error in res.view()';
err.type = err.code = 'E_VIEW_INFER';
err.message = 'No path specified, and no path could be inferred from the request context.';
// Prevent endless recursion:
if (req._errorInResView) { return res.send(500); }
req._errorInResView = err;
if (cb_view) { return cb_view(err); }
else return res.serverError(err);
}
// Ensure specifiedPath is a string (important)
relPathToView = '' + relPathToView + '';
// Ensure `locals` is an object
locals = _.isObject(locals) ? locals : {};
// Mixin locals from req.options
if (req.options.locals) {
locals = _.merge(locals, req.options.locals);
}
// Merge with config views locals
if (sails.config.views.locals) {
_.merge(locals, sails.config.views.locals, _.defaults);
}
// If the path was specified, but invalid
// else if (specifiedPath) {
// return res.serverError(new Error('Specified path for view (' + specifiedPath + ') is invalid!'));
// }
// Trim trailing slash
if (relPathToView[(relPathToView.length - 1)] === '/') {
relPathToView = relPathToView.slice(0, -1);
}
// if local `layout` is set to true or unspecified
// fall back to global config
var layout = locals.layout;
if (locals.layout === undefined || locals.layout === true) {
layout = sails.config.views.layout;
}
// Get the view engine name
var engineName = sails.config.views.engine.name || sails.config.views.engine.ext;
// Disable sails built-in layouts for all view engine's except for ejs
if (engineName !== 'ejs') {
layout = false;
}
// Allow `res.locals.layout` to override if it was set:
if (typeof res.locals.layout !== 'undefined') {
layout = res.locals.layout;
}
var pathToViews = sails.config.paths.views;
var absPathToView = path.join(pathToViews, relPathToView);
var absPathToLayout;
var relPathToLayout;
// At this point, layout should be either `false` or a string
if (typeof layout !== 'string') {
layout = false;
}
// Set layout file if enabled (using ejs-locals)
if (layout) {
// Solve relative path to layout from the view itself
// (required by ejs-locals module)
absPathToLayout = path.join(pathToViews, layout);
relPathToLayout = path.relative(path.dirname(absPathToView), absPathToLayout);
// If a layout was specified, set view local so it will be used
res.locals._layoutFile = relPathToLayout;
// sails.log.silly('Using layout at: ', absPathToLayout);
}
// Locals passed in to `res.view()` override app and route locals.
_.each(locals, function(local, key) {
res.locals[key] = local;
});
// Provide access to view metadata in locals
// (for convenience)
res.locals.view = res.locals.view || {
path: relPathToView,
absPath: absPathToView,
pathFromViews: relPathToView,
pathFromApp: path.join(path.relative(sails.config.appPath, sails.config.paths.views), relPathToView),
ext: sails.config.views.engine.ext
};
// In development, provide access to complete path to view via `__dirname` local.
if (sails.config.environment !== 'production') {
res.locals.__dirname = res.locals.__dirname ||
absPathToView + '.' + sails.config.views.engine.ext;
}
// If silly logging is enabled, display some diagnostic information about the res.view() call:
if (specifiedPath) { sails.log.silly('View override argument passed to res.view(): ', specifiedPath); }
sails.log.silly('Serving view at rel path: ', relPathToView);
sails.log.silly('View root: ', sails.config.paths.views);
// Render the view
return res.render(relPathToView, locals, function viewFailedToRender(err, renderedViewStr) {
// Prevent endless recursion:
if (err && req._errorInResView) {
return res.send(500, err);
}
if (err) {
req._errorInResView = err;
// Ensure that if res.serverError() likes to serve views,
// it won't this time because we ran into a view error.
req.wantsJSON = true;
// Enhance the raw Express view error object
// (this error appears when a view is missing)
if (_.isObject(err) && err.view) {
err = _.extend({
message: util.format(
'Could not render view "%s". Tried locating view file @ "%s".%s',
relPathToView,
absPathToView,
(layout ? util.format(' Layout configured as "%s", so tried using layout @ "%s")', layout, absPathToLayout) : '')
),
code: 'E_VIEW_FAILED',
status: 500
}, err);
err.inspect = function () {
return err.message;
};
}
}
// If specified, trigger `res.view()` callback instead of proceeding
if (typeof cb_view === 'function') {
// The provided cb_view callback will receive the error (if there is one)
// as the first argument, and the rendered HTML as the second argument.
return cb_view(err, renderedViewStr);
}
else {
// if a template error occurred, don't rely on any of the Sails request/response methods
// (since they may not exist yet at this point in the request lifecycle.)
if (err) {
//////////////////////////////////////////////////////////////////
// TODO:
// Consider removing this log and deferring to the logging that is
// happening in res.serverError() instead.
// sails.log.error('Error rendering view at ::', absPathToView);
// sails.log.error('with layout located at ::', absPathToLayout);
// sails.log.error(err && err.message);
//
//////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////
// TODO:
// Consider just calling some kind of default error handler fn here
// in order to consolidate the "i dont know wtf i should do w/ this err" logic.
// (keep in mind the `next` we have here is NOT THE SAME as the `next` from
// the point when this error occurred! It is the next from when this
// method was initially attached to the request object in the views hook.)
//
if (res.serverError) {
req.wantsJSON = true;
return res.serverError(err);
}
else if (process.env.NODE_ENV !== 'production') {
return res.json(500, err);
}
else return res.send(500);
//
//////////////////////////////////////////////////////////////////
}
// If verbose logging is enabled, write a log w/ the layout and view that was rendered.
sails.log.verbose('Rendering view: "%s" (located @ "%s")', relPathToView,absPathToView);
layout && sails.log.verbose('• using configured layout:: %s (located @ "%s")', layout, absPathToLayout);
// Finally, send the compiled HTML from the view down to the client
res.send(renderedViewStr);
}
});
};
next();
};
// Express version updates should be closely monitored.
// Express is a "hard" dependency.
//
// While unlikely this will change, it's worth noting that this implementation
// relies on express's private implementation of res.render() here:
// https://github.com/visionmedia/express/blob/master/lib/response.js#L799
//
// To be safe, the version of the Express dependency in package.json will remain locked
// until it can be verified that each subsequent version is compatible. Even patch releases!!