sails
Version:
API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
440 lines (363 loc) • 16.8 kB
JavaScript
/**
* Module dependencies
*/
var path = require('path');
var util = require('util');
var _ = require('@sailshq/lodash');
var htmlScriptify = require('./html-scriptify');
/**
* Adds `res.view()` (an enhanced version of res.render) and `res.guessView()` methods 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) {
// - - - - - - - - - - - - - - - - - - - - - - - - - - -
// FUTURE: Completely remove res.guessView()
// - - - - - - - - - - - - - - - - - - - - - - - - - - -
sails.log.warn(
'`res.guessView()` is deprecated in Sails >= v1.0. If you want to continue to use it\n'+
'in your Sails app, please just drop its implementation into a hook.\n'+
' [?] Unsure or need advice? Stop by https://sailsjs.com/support'
);
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);
});
};//</defun:: res.guessView()>
/**
* 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} optionalCb(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, optionalCb */) {
var specifiedPath = arguments[0];
var locals = arguments[1];
var optionalCb = 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.action) {
relPathToView = req.options.action.replace(/\./g, '/');
}
// 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 the second argument is a function, treat it like the callback.
if (_.isFunction(arguments[1])) {
optionalCb = arguments[1];
// In which case if the first argument is a string, it means no locals were specified,
// so set `locals` to an empty dictionary and log a warning.
if (_.isString(arguments[0])) {
sails.log.warn('`res.view` called with (path, cb) signature (using path `' + specifiedPath + '`). You should use `res.view(path, {}, cb)` to render a view without local variables.');
locals = {};
}
}
// if (_.isFunction(locals)) {
// optionalCb = locals;
// locals = {};
// }
// if (_.isFunction(specifiedPath)) {
// optionalCb = 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.code = 'E_VIEW_INFER';
err.type = err.code;// <<TODO remove this
err.message = 'No path specified, and no path could be inferred from the request context.';
// Prevent endless recursion:
if (req._errorInResView) { return res.sendStatus(500); }
req._errorInResView = err;
if (optionalCb) { return optionalCb(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.
// TODO -- replace this _.merge() with a call to the merge-dictionaries module?
if (req.options.locals) {
locals = _.merge(locals, req.options.locals);
}
// Merge with config views locals
if (sails.config.views.locals) {
// Formerly a deep merge: `_.merge(locals, sails.config.views.locals, _.defaults);`
// Now shallow- see https://github.com/balderdashy/sails/issues/3500
_.defaults(locals, sails.config.views.locals);
}
// 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);
}
var pathToViews = sails.config.paths.views;
var absPathToView = path.join(pathToViews, relPathToView);
var absPathToLayout;
var relPathToLayout;
var layout = false;
// Deal with layout options only if there is no custom rendering function in place --
// that is, only if we're using the default EJS layouts.
if (!sails.config.views.getRenderFn) {
layout = locals.layout;
// If local `layout` is set to true or unspecified
// fall back to global config
if (locals.layout === undefined || locals.layout === true) {
layout = sails.config.views.layout;
}
// Allow `res.locals.layout` to override if it was set:
if (typeof res.locals.layout !== 'undefined') {
layout = res.locals.layout;
}
// 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)
if (_.isUndefined(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.extension
};
}
// Set up the `exposeLocalsToBrowser` view helper method
// (unless there is already a local by the same name)
//
// Background:
// -> https://github.com/balderdashy/sails/pull/3522#issuecomment-174242822
if (_.isUndefined(res.locals.exposeLocalsToBrowser)) {
res.locals.exposeLocalsToBrowser = function (options){
if (!_.isObject(options)) { options = {}; }
// Note:
// We get access to locals using a reference obtained via closure--
// and since this view helper won't be used until AFTER the rest of
// the code in THIS file has run, we know any relevant changes to
// `locals` below will be available, since we're referring to the
// same object.
// Note that we include both explicit locals passed to res.view(),
// and implicitly-set locals from `res.locals`. But we exclude
// non-relevant built-in properties like `sails` and `_`, as well
// as experimental properties like `view`.
//
// Also note that we create a new dictionary to avoid tampering.
var relevantLocals = {};
_.each(_.union(_.keys(res.locals), _.keys(locals)), function(localName){
// Explicitly exclude `_locals`, which appears even in explicit locals.
// (FUTURE: longer term, could look into doing this _everywhere_ as an optimization-
// but need to investigate other view engines for potential differences)
if (localName === '_locals') {}
// Explicitly exclude `layout`, since it has special meaning,
// even when it appears even in explicit locals.
else if (localName === 'layout') {}
// Otherwise, use explicit local, if available
else if (locals[localName] !== undefined) {
relevantLocals[localName] = locals[localName];
}
// Otherwise, use the one from res.locals... maybe.
else {
if (localName === '_csrf') {
// Special case for CSRF token
// > If the security hook is disabled, there won't be a CSRF token in the locals.
// > If the hook is enabled but CSRF is disabled for this route, the token will
// > be an empty string. In either of those cases we can just skip it.
if (res.locals._csrf !== undefined && res.locals._csrf !== '') {
relevantLocals._csrf = res.locals._csrf;
}
}
else if (_.contains(['_', 'sails', 'view', 'session', 'req', 'res', '__dirname', '_layoutFile'], localName)) {
// Exclude any other auto-injected implicit locals
}
else if (_.isFunction(res.locals[localName])) {
// Exclude any functions
}
else {
// Otherwise include it!
relevantLocals[localName] = res.locals[localName];
}
}
});//∞
// Return an HTML string which includes a special script tag.
return htmlScriptify({
data: relevantLocals,
keys: options.keys,
namespace: options.namespace,
dontUnescapeOnClient: options.dontUnescapeOnClient
});
};//</defun :: res.locals.exposeLocalsToBrowser()>
}//>-
// Unless this is production, provide access to complete view path to view via `__dirname` local.
if (process.env.NODE_ENV !== 'production') {
res.locals.__dirname =
res.locals.__dirname ||
(absPathToView + '.' + sails.config.views.extension);
}
// 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.status(500).send(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 optionalCb === 'function') {
// The provided optionalCb callback will receive the error (if there is one)
// as the first argument, and the rendered HTML as the second argument.
return optionalCb(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.status(500).send(err);
}
else {return res.sendStatus(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);
if (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);
}
});
};//</defun :: res.view() >
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!!