express/lib/application.js
2011-10-07 13:22:55 -07:00

596 lines
13 KiB
JavaScript

/*!
* Express - proto
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var connect = require('connect')
, Router = require('./router')
, view = require('./view')
, toArray = require('./utils').toArray
, methods = Router.methods.concat('del', 'all')
, res = require('./response')
, union = require('./utils').union
, url = require('url')
, utils = connect.utils
, path = require('path')
, extname = path.extname
, join = path.join
, fs = require('fs')
, qs = require('qs');
/**
* Expose `HTTPServer`.
*/
var app = exports = module.exports = {};
/**
* Initialize the server.
*
* @param {Array} middleware
* @api private
*/
app.init = function(middleware){
var self = this;
this.settings = {};
this.engines = {};
this.redirects = {};
this.isCallbacks = {};
this.set('home', '/');
this.set('env', process.env.NODE_ENV || 'development');
this.use(connect.query());
/**
* Assign several locals with the given `obj`,
* or return the locals.
*
* @param {Object} obj
* @return {Object|HTTPServer}
* @api public
*/
this.locals = function(obj){
for (var key in obj) {
self.locals[key] = obj[key];
}
return self;
};
// expose objects to each other
this.use(function(req, res, next){
var charset;
req.query = req.query || {};
res.setHeader('X-Powered-By', 'Express');
req.app = res.app = self;
req.res = res;
res.req = req;
req.next = next;
// charset
if (charset = self.set('charset')) res.charset = charset;
/**
* Assign several locals with the given `obj`,
* or return the locals.
*
* @param {Object} obj
* @return {Object|ServerResponse}
* @api public
*/
res.locals = function(obj){
for (var key in obj) {
res.locals[key] = obj[key];
}
return res;
};
next();
});
// apply middleware
if (middleware) middleware.forEach(self.use.bind(self));
// router
this.routes = new Router(this);
this.__defineGetter__('router', function(){
this.__usedRouter = true;
return self.routes.middleware;
});
// default locals
this.locals.settings = this.settings;
// default development configuration
this.configure('development', function(){
this.enable('hints');
});
// default production configuration
this.configure('production', function(){
this.enable('view cache');
});
// route manipulation methods
methods.forEach(function(method){
self.lookup[method] = function(path){
return self.routes.lookup(method, path);
};
self.match[method] = function(path){
return self.routes.match(method, path);
};
self.remove[method] = function(path){
return self.routes.lookup(method, path).remove();
};
});
// del -> delete
self.lookup.del = self.lookup.delete;
self.match.del = self.match.delete;
self.remove.del = self.remove.delete;
};
/**
* Remove routes matching the given `path`.
*
* @param {Route} path
* @return {Boolean}
* @api public
*/
app.remove = function(path){
return this.routes.lookup('all', path).remove();
};
/**
* Lookup routes defined with a path
* equivalent to `path`.
*
* @param {Stirng} path
* @return {Array}
* @api public
*/
app.lookup = function(path){
return this.routes.lookup('all', path);
};
/**
* Lookup routes matching the given `url`.
*
* @param {Stirng} url
* @return {Array}
* @api public
*/
app.match = function(url){
return this.routes.match('all', url);
};
/**
* Proxy `connect.HTTPServer#use()` to apply settings to
* mounted applications.
*
* @param {String|Function|Server} route
* @param {Function|Server} middleware
* @return {Server} for chaining
* @api public
*/
app.use = function(route, middleware){
var app, home, handle;
if ('string' != typeof route) {
middleware = route, route = '/';
}
// express app
if (middleware.handle && middleware.set) app = middleware;
// restore .app property on req and res
if (app) {
app.route = route;
middleware = function(req, res, next) {
var orig = req.app;
app.handle(req, res, function(err){
req.app = res.app = orig;
next(err);
});
};
}
connect.proto.use.call(this, route, middleware);
// mounted an app, invoke the hook
// and adjust some settings
if (app) {
base = this.set('basepath') || this.route;
if ('/' == base) base = '';
base = base + (app.set('basepath') || app.route);
app.set('basepath', base);
app.parent = this;
if (app.__mounted) app.__mounted.call(app, this);
}
return this;
};
/**
* Assign a callback `fn` which is called
* when this `Server` is passed to `Server#use()`.
*
* Examples:
*
* var app = express.createServer()
* , blog = express.createServer();
*
* blog.mounted(function(parent){
* // parent is app
* // "this" is blog
* });
*
* app.use(blog);
*
* @param {Function} fn
* @return {Server} for chaining
* @api public
*/
app.mounted = function(fn){
this.__mounted = fn;
return this;
};
/**
* Register the given template engine callback `fn`
* as `ext`. For example we may wish to map ".html"
* files to ejs rather than using the ".ejs" extension.
*
* app.register('.html', require('ejs').render);
*
* or
*
* app.register('html', require('ejs').render);
*
* @param {String} ext
* @param {Function} fn
* @return {Server} for chaining
* @api public
*/
app.register = function(ext, fn){
if ('.' != ext[0]) ext = '.' + ext;
this.engines[ext] = fn;
return this;
};
/**
* Map the given param placeholder `name`(s) to the given callback `fn`.
* Register the given view helpers `obj`. This method
* can be called several times to apply additional helpers.
*
* @param {Object} obj
* @return {Server} for chaining
* @api public
*/
app.locals = function(obj){
utils.merge(this._locals, obj);
return this;
};
/**
* Map the given param placeholder `name`(s) to the given callback(s).
*
* Param mapping is used to provide pre-conditions to routes
* which us normalized placeholders. This callback has the same
* signature as regular middleware, for example below when ":userId"
* is used this function will be invoked in an attempt to load the user.
*
* app.param('userId', function(req, res, next, id){
* User.find(id, function(err, user){
* if (err) {
* next(err);
* } else if (user) {
* req.user = user;
* next();
* } else {
* next(new Error('failed to load user'));
* }
* });
* });
*
* Passing a single function allows you to map logic
* to the values passed to `app.param()`, for example
* this is useful to provide coercion support in a concise manner.
*
* The following example maps regular expressions to param values
* ensuring that they match, otherwise passing control to the next
* route:
*
* app.param(function(name, regexp){
* if (regexp instanceof RegExp) {
* return function(req, res, next, val){
* var captures;
* if (captures = regexp.exec(String(val))) {
* req.params[name] = captures;
* next();
* } else {
* next('route');
* }
* }
* }
* });
*
* We can now use it as shown below, where "/commit/:commit" expects
* that the value for ":commit" is at 5 or more digits. The capture
* groups are then available as `req.params.commit` as we defined
* in the function above.
*
* app.param('commit', /^\d{5,}$/);
*
* For more of this useful functionality take a look
* at [express-params](http://github.com/visionmedia/express-params).
*
* @param {String|Array|Function} name
* @param {Function} fn
* @return {Server} for chaining
* @api public
*/
app.param = function(name, fn){
var self = this
, fns = [].slice.call(arguments, 1);
// array
if (Array.isArray(name)) {
name.forEach(function(name){
fns.forEach(function(fn){
self.param(name, fn);
});
});
// param logic
} else if ('function' == typeof name) {
this.routes.param(name);
// single
} else {
if (':' == name[0]) name = name.substr(1);
fns.forEach(function(fn){
self.routes.param(name, fn);
});
}
return this;
};
/**
* Register the given callback `fn` for the given `type`.
*
* @param {String} type
* @param {Function} fn
* @return {Server} for chaining
* @api public
*/
app.is = function(type, fn){
if (!fn) return this.isCallbacks[type];
this.isCallbacks[type] = fn;
return this;
};
/**
* Assign `setting` to `val`, or return `setting`'s value.
* Mounted servers inherit their parent server's settings.
*
* @param {String} setting
* @param {String} val
* @return {Server|Mixed} for chaining, or the setting value
* @api public
*/
app.set = function(setting, val){
if (val === undefined) {
if (this.settings.hasOwnProperty(setting)) {
return this.settings[setting];
} else if (this.parent) {
return this.parent.set(setting);
}
} else {
this.settings[setting] = val;
return this;
}
};
/**
* Check if `setting` is enabled.
*
* @param {String} setting
* @return {Boolean}
* @api public
*/
app.enabled = function(setting){
return !!this.set(setting);
};
/**
* Check if `setting` is disabled.
*
* @param {String} setting
* @return {Boolean}
* @api public
*/
app.disabled = function(setting){
return !this.set(setting);
};
/**
* Enable `setting`.
*
* @param {String} setting
* @return {Server} for chaining
* @api public
*/
app.enable = function(setting){
return this.set(setting, true);
};
/**
* Disable `setting`.
*
* @param {String} setting
* @return {Server} for chaining
* @api public
*/
app.disable = function(setting){
return this.set(setting, false);
};
/**
* Redirect `key` to `url`.
*
* @param {String} key
* @param {String} url
* @return {Server} for chaining
* @api public
*/
app.redirect = function(key, url){
this.redirects[key] = url;
return this;
};
/**
* Configure callback for zero or more envs,
* when no env is specified that callback will
* be invoked for all environments. Any combination
* can be used multiple times, in any order desired.
*
* Examples:
*
* app.configure(function(){
* // executed for all envs
* });
*
* app.configure('stage', function(){
* // executed staging env
* });
*
* app.configure('stage', 'production', function(){
* // executed for stage and production
* });
*
* @param {String} env...
* @param {Function} fn
* @return {Server} for chaining
* @api public
*/
app.configure = function(env, fn){
var envs = 'all'
, args = toArray(arguments);
fn = args.pop();
if (args.length) envs = args;
if ('all' == envs || ~envs.indexOf(this.settings.env)) fn.call(this);
return this;
};
/**
* Delegate `.VERB(...)` calls to `.route(VERB, ...)`.
*/
methods.forEach(function(method){
app[method] = function(path){
if (1 == arguments.length) return this.routes.lookup(method, path);
var args = [method].concat(toArray(arguments));
if (!this.__usedRouter) this.use(this.router);
return this.routes._route.apply(this.routes, args);
}
});
/**
* Special-cased "all" method, applying the given route `path`,
* middleware, and callback to _every_ HTTP method.
*
* @param {String} path
* @param {Function} ...
* @return {Server} for chaining
* @api public
*/
app.all = function(path){
var args = arguments;
if (1 == args.length) return this.routes.lookup('all', path);
methods.forEach(function(method){
if ('all' == method) return;
app[method].apply(this, args);
}, this);
return this;
};
// del -> delete alias
app.del = app.delete;
/**
* Render the given `view` name with `opts`
* and a callback accepting an error and the
* rendered template string.
*
* @param {String} view
* @param {String|Function} opts or fn
* @param {Function} fn
* @api public
*/
app.render = function(view, opts, fn){
var self = this
, options = {}
, engines = this.engines
, root = this.set('views') || process.cwd() + '/views';
// support callback function as second arg
if ('function' == typeof opts) {
fn = opts, opts = null;
}
// merge app.locals
union(options, this.locals);
// merge render() options
if (opts) utils.merge(options, opts);
// join "view engine" if necessary
var ext = extname(view);
if (!ext) view += '.' + (ext = this.set('view engine'));
// pass .cache to the engine
options.cache = this.enabled('view cache');
// when no extension nor "view engine" is given
if (!ext) return fn(new Error('failed to find view "' + view + '"'));
// render
try {
var engine = engines[ext] = engines[ext] || require(ext);
options.filename = join(root, view);
view = fs.readFileSync(options.filename, 'utf8');
engine.render(view, options, fn);
} catch (err) {
fn(err);
}
};