Compare commits

..

52 Commits
2.2.0 ... 2.3.0

Author SHA1 Message Date
Tj Holowaychuk
658e064220 Release 2.3.0 2011-04-25 09:49:54 -07:00
Tj Holowaychuk
66b472e567 connect >= 1.4.0 2011-04-25 09:32:57 -07:00
Tj Holowaychuk
3d242a607e Fixed caching of views when using several apps. Closes #637
simple fix :)
2011-04-25 09:25:23 -07:00
Tj Holowaychuk
bc68e5e7a3 misc 2011-04-25 09:20:31 -07:00
Tj Holowaychuk
aa6858189c misc refactor 2011-04-23 18:54:49 -07:00
Tj Holowaychuk
06fda62c9e Merge branch 'refactor/route-middleware' 2011-04-22 16:49:04 -07:00
Tj Holowaychuk
5688ea650d Fixed gotcha invoking app.param() callbacks once per route middleware. Closes #638 2011-04-22 16:48:54 -07:00
Tj Holowaychuk
f5c4e9d612 Updated connect submodule 2011-04-22 16:34:42 -07:00
Tj Holowaychuk
b9eda2a59d Updated connect submodule 2011-04-22 16:11:13 -07:00
Tj Holowaychuk
8c168b0971 connect >= 1.3.1 < 2.0.0 2011-04-22 16:11:07 -07:00
Tj Holowaychuk
9c20a50ee2 Added failing test for gotcha 2011-04-22 14:44:08 -07:00
Tj Holowaychuk
eac666574e viewHelpers -> _locals 2011-04-21 08:26:30 -07:00
Tj Holowaychuk
e4c840f6b8 Added res.helpers() as alias of res.locals()
to match app.locals() / app.helpers()
2011-04-20 15:26:22 -07:00
Daniel Shaw
3afbcd0acf JSON and JSONP default to UTF-8 in the same way as HTML. Closes #632.
Signed-off-by: Tj Holowaychuk <tj@vision-media.ca>
2011-04-20 09:00:02 -07:00
Daniel Shaw
8f054dbcaf JSON and JSONP default to UTF-8 in the same way as HTML. Introduces app.set('charset') to set charset default at the application level. Closes #632.
Signed-off-by: Tj Holowaychuk <tj@vision-media.ca>
2011-04-20 09:00:02 -07:00
Tj Holowaychuk
ccc39e5aa2 Fixed partial lookup precedence. Closes #631 2011-04-19 10:23:16 -07:00
Tj Holowaychuk
53d4da2a9c Added failing partial precedence test 2011-04-19 10:19:45 -07:00
Tj Holowaychuk
d9e7153fc9 Renamed "cache views" to "view cache". Closes #628 2011-04-17 16:42:03 -07:00
Tj Holowaychuk
dc02b0d5ae Added options support to res.clearCookie() 2011-04-17 16:37:16 -07:00
Aaron Heckmann
e0bc5711b8 auto set Content-Type in res.attachement
Signed-off-by: Tj Holowaychuk <tj@vision-media.ca>
2011-04-14 13:48:22 -07:00
Tj Holowaychuk
957cf45fa1 Release 2.2.2 2011-04-12 02:44:47 -07:00
Tj Holowaychuk
f4487343df Fixed filename option passing to template engine 2011-04-12 02:43:03 -07:00
Tj Holowaychuk
ca1bdb31e3 Updated jade submodule 2011-04-12 02:41:30 -07:00
Tj Holowaychuk
236a412459 docs 2011-04-11 09:46:28 -07:00
Tj Holowaychuk
759a57bdb6 Added second callback support for res.download() connection errors
since you can no longer respond, it will be helpful to have separate callbacks
2011-04-11 09:44:19 -07:00
Tj Holowaychuk
1abb674a07 temp fix for nodes res.removeHeader() after sent "bug" 2011-04-11 09:14:11 -07:00
Tj Holowaychuk
961146a287 link to express-expose 2011-04-08 17:51:41 -07:00
Tj Holowaychuk
573f940985 fixed example hasMessages
sessions are not always available
2011-04-08 12:44:03 -07:00
Tj Holowaychuk
882916bb2e Updated jade submodule 2011-04-08 12:36:41 -07:00
Tj Holowaychuk
47d1c62732 Updated connect submodule 2011-04-08 12:36:35 -07:00
Tj Holowaychuk
d39293c025 refactor 2011-04-08 12:36:26 -07:00
Tj Holowaychuk
2942dafdfd >= connect 1.2.3 2011-04-05 11:25:16 -07:00
Tj Holowaychuk
4e1aefa5b5 Release 2.2.1 2011-04-04 12:23:37 -07:00
Tj Holowaychuk
89383ecc57 misc refactoring 2011-04-03 14:27:40 -07:00
Tj Holowaychuk
08046f7692 Added better partial() object collection support
only respecting .length is fine in some cases, but
if the object has a length and has holes, it will likely
produce an unexpected result, or an undefined local in the
render call, which may some times be ideal, but most likely not.

this change allows arbitrary objects to be passed to "collection: ",
however unfortunately there is no way to arbitrarily assume the object
is a collection without passing it as the option
2011-04-03 14:27:05 -07:00
Tj Holowaychuk
885fb1fa92 docs 2011-04-01 17:55:26 -07:00
Tj Holowaychuk
6a58c71528 Merge branch 'feature/view-layout-control' 2011-04-01 17:47:11 -07:00
Tj Holowaychuk
371d66ba2a Added layout(path) helper to change the layout within a view. Closes #610 2011-04-01 17:47:03 -07:00
Tj Holowaychuk
25bddf3fb5 added layout control example 2011-04-01 17:39:18 -07:00
Tj Holowaychuk
f6e9fb13f8 Removed "request" and "response" locals
sorry, changed my mind. easy enough to expose these if you want to,
but they are to large by default
2011-04-01 17:25:05 -07:00
Tj Holowaychuk
f0df8434e7 markdown escaping 2011-03-31 08:24:43 -07:00
Tj Holowaychuk
e4d3f239e5 Updated connect submodule 2011-03-30 22:01:16 -07:00
Tj Holowaychuk
bcc22dfa6f Updated connect submodule 2011-03-30 21:59:17 -07:00
Tj Holowaychuk
f614709a01 errorHandler title 2011-03-30 21:59:15 -07:00
Tj Holowaychuk
11250d22c9 misc refactoring 2011-03-30 21:44:21 -07:00
Tj Holowaychuk
4b4de29725 Performance improved with better view caching
the entire View object is now cached in-memory, along with the lookup
logic as well. This increases rendering (with jade at least) by about 260 rps
2011-03-30 21:40:05 -07:00
Tj Holowaychuk
99b49d2718 updated docs 2011-03-30 12:11:22 -07:00
Tj Holowaychuk
6230ec55be more docs 2011-03-30 12:02:14 -07:00
Tj Holowaychuk
0733d3c585 connect 1.2.0 2011-03-30 11:58:26 -07:00
Tj Holowaychuk
6f8370ff0e Updated connect submodule 2011-03-30 11:58:09 -07:00
Tj Holowaychuk
bc244ed07e docs for app.match.VERB() 2011-03-30 11:56:14 -07:00
Tj Holowaychuk
41266aa8e4 docs for app.lookup.VERB() 2011-03-30 11:49:21 -07:00
28 changed files with 526 additions and 153 deletions

View File

@@ -1,11 +1,46 @@
2.3.0 / 2011-04-25
==================
* Added options support to `res.clearCookie()`
* Added `res.helpers()` as alias of `res.locals()`
* Added; json defaults to UTF-8 with `res.send()`. Closes #632. [Daniel * Dependency `connect >= 1.4.0`
* Changed; auto set Content-Type in res.attachement [Aaron Heckmann]
* Renamed "cache views" to "view cache". Closes #628
* Fixed caching of views when using several apps. Closes #637
* Fixed gotcha invoking `app.param()` callbacks once per route middleware.
Closes #638
* Fixed partial lookup precedence. Closes #631
Shaw]
2.2.2 / 2011-04-12
==================
* Added second callback support for `res.download()` connection errors
* Fixed `filename` option passing to template engine
2.2.1 / 2011-04-04
==================
* Added `layout(path)` helper to change the layout within a view. Closes #610
* Fixed `partial()` collection object support.
Previously only anything with `.length` would work.
When `.length` is present one must still be aware of holes,
however now `{ collection: {foo: 'bar'}}` is valid, exposes
`keyInCollection` and `keysInCollection`.
* Performance improved with better view caching
* Removed `request` and `response` locals
* Changed; errorHandler page title is now `Express` instead of `Connect`
2.2.0 / 2011-03-30
==================
* Added `app.lookup.VERB()`, ex `app.lookup.put('/user/:id')`. Closes #606
* Added `app.match.VERB()`, ex `app.match.put('/user/12')`. Closes #606
* Added `app.VERB(path)` as alias of `app.lookup.VERB()`.
* Dependency `connect >= 1.1.6`
* Dependency `connect >= 1.2.0`
2.1.1 / 2011-03-29
==================

View File

@@ -54,6 +54,7 @@ The following are the major contributors of Express (in no specific order).
## More Information
* [express-expose](http://github.com/visionmedia/express-expose) expose objects, functions, modules and more to client-side js with ease
* [express-configure](http://github.com/visionmedia/express-configuration) async configuration support
* [express-messages](http://github.com/visionmedia/express-messages) flash notification rendering helper
* [express-namespace](http://github.com/visionmedia/express-namespace) namespaced route support

View File

@@ -11,7 +11,7 @@ var fs = require('fs')
* Framework version.
*/
var version = '2.2.0';
var version = '2.3.0';
/**
* Add session support.

View File

@@ -232,6 +232,8 @@
<li><a href="#app.error()">error()</a></li>
<li><a href="#app.helpers()">helpers()</a></li>
<li><a href="#app.dynamichelpers()">dynamicHelpers()</a></li>
<li><a href="#app.lookup">lookup</a></li>
<li><a href="#app.match">match</a></li>
<li><a href="#app.mounted()">mounted()</a></li>
<li><a href="#app.register()">register()</a></li>
<li><a href="#app.listen()">listen()</a></li>
@@ -1222,7 +1224,7 @@ res.cookie('rememberme', 'yes', { expires: new Date(Date.now() + 900000), httpOn
<pre><code>res.cookie('rememberme', 'yes', { maxAge: 900000 });
</code></pre>
<p>To parse incoming <em>Cookie</em> headers, use the <em>cookieDecoder</em> middleware, which provides the <em>req.cookies</em> object:</p>
<p>To parse incoming <em>Cookie</em> headers, use the <em>cookieParser</em> middleware, which provides the <em>req.cookies</em> object:</p>
<pre><code>app.use(express.cookieParser());
@@ -1506,21 +1508,30 @@ should call <em>next(err)</em> if it does not wish to deal with the exception:</
<p>Registers static view helpers.</p>
<p> app.helpers({</p>
<pre><code> name: function(first, last){ return first + ', ' + last }
, firstName: 'tj'
, lastName: 'holowaychuk'
<pre><code>app.helpers({
name: function(first, last){ return first + ', ' + last }
, firstName: 'tj'
, lastName: 'holowaychuk'
});
</code></pre>
<p> });</p>
<p>Our view could now utilize the <em>firstName</em> and <em>lastName</em> variables,
as well as the <em>name()</em> function exposed.</p>
<pre><code>&lt;%= name(firstName, lastName) %&gt;
</code></pre>
<p>Express also provides a few locals by default:</p>
<pre><code>- `settings` the app's settings object
- `filename` the view's filename
- `request` the request object
- `response` the response object
- `app` the application itself
</code></pre>
<p>This method is aliased as <em>app.locals()</em>.</p>
<h3 id="app.dynamichelpers()">app.dynamicHelpers(obj)</h3>
<p>Registers dynamic view helpers. Dynamic view helpers
@@ -1540,6 +1551,110 @@ becomes the local variable it is associated with.</p>
<pre><code>&lt;%= session.name %&gt;
</code></pre>
<h3 id="app.lookup">app.lookup</h3>
<p> The <em>app.lookup</em> http methods returns an array of callback functions
associated with the given <em>path</em>.</p>
<p> Suppose we define the following routes:</p>
<pre><code> app.get('/user/:id', function(){});
app.put('/user/:id', function(){});
app.get('/user/:id/:op?', function(){});
</code></pre>
<p> We can utilize this lookup functionality to check which routes
have been defined, which can be extremely useful for higher level
frameworks built on Express.</p>
<pre><code> app.lookup.get('/user/:id');
// =&gt; [Function]
app.lookup.get('/user/:id/:op?');
// =&gt; [Function]
app.lookup.put('/user/:id');
// =&gt; [Function]
app.lookup.all('/user/:id');
// =&gt; [Function, Function]
app.lookup.all('/hey');
// =&gt; []
</code></pre>
<p> To alias <em>app.lookup.VERB()</em>, we can simply invoke <em>app.VERB()</em>
without a callback, as a shortcut, for example the following are
equivalent:</p>
<pre><code> app.lookup.get('/user');
app.get('/user');
</code></pre>
<p> Each function returned has the following properties:</p>
<pre><code> var fn = app.get('/user/:id/:op?')[0];
fn.regexp
// =&gt; /^\/user\/(?:([^\/]+?))(?:\/([^\/]+?))?\/?$/i
fn.keys
// =&gt; ['id', 'op']
fn.path
// =&gt; '/user/:id/:op?'
fn.method
// =&gt; 'GET'
</code></pre>
<h3 id="app.match">app.match</h3>
<p> The <em>app.match</em> http methods return an array of callback functions
which match the given <em>url</em>, which may include a query string etc. This
is useful when you want reflect on which routes have the opportunity to
respond.</p>
<p> Suppose we define the following routes:</p>
<pre><code> app.get('/user/:id', function(){});
app.put('/user/:id', function(){});
app.get('/user/:id/:op?', function(){});
</code></pre>
<p> Our match against <strong>GET</strong> will return two functions, since the <em>:op</em>
in our second route is optional.</p>
<pre><code> app.match.get('/user/1');
// =&gt; [Function, Function]
</code></pre>
<p> This second call returns only the callback for <em>/user/:id/:op?</em>.</p>
<pre><code> app.match.get('/user/23/edit');
// =&gt; [Function]
</code></pre>
<p> We can also use <em>all()</em> to disregard the http method:</p>
<pre><code> app.match.all('/user/20');
// =&gt; [Function, Function, Function]
</code></pre>
<p> Each function matched has the following properties:</p>
<pre><code> var fn = app.match.get('/user/23/edit')[0];
fn.keys
// =&gt; ['id', 'op']
fn.params
// =&gt; { id: '23', op: 'edit' }
fn.method
// =&gt; 'GET'
</code></pre>
<h3 id="app.mounted()">app.mounted(fn)</h3>
<p>Assign a callback <em>fn</em> which is called when this <em>Server</em> is passed to <em>Server#use()</em>.</p>

View File

@@ -165,7 +165,7 @@ For example we can __POST__ some json, and echo the json back using the _bodyPar
app.listen(3000);
Typically we may use a "dumb" placeholder such as "/user/:id" which has no restrictions, however say for example we are limiting a user id to digits, we may use _'/user/:id(\\d+)'_ which will _not_ match unless the placeholder value contains only digits.
Typically we may use a "dumb" placeholder such as "/user/:id" which has no restrictions, however say for example we are limiting a user id to digits, we may use _'/user/:id(\\\\d+)'_ which will _not_ match unless the placeholder value contains only digits.
### Passing Route Control
@@ -816,7 +816,7 @@ Options may also be passed to the internal _fs.createReadStream()_ call, for exa
// handle
});
### res.download(file[, filename[, callback]])
### res.download(file[, filename[, callback[, callback2]]])
Transfer the given _file_ as an attachment with optional alternative _filename_.
@@ -828,12 +828,20 @@ This is equivalent to:
res.attachment(file);
res.sendfile(file);
An optional callback may be supplied as either the second or third argument, which is passed to _res.sendfile()_:
An optional callback may be supplied as either the second or third argument, which is passed to _res.sendfile()_. Within this callback you may still respond, as the header has not been sent.
res.download(path, 'expenses.doc', function(err){
// handle
});
An optional second callback, _callback2_ may be given to allow you to act on connection related errors, however you should not attempt to respond.
res.download(path, function(err){
// error or finished
}, function(err){
// connection related error
});
### res.send(body|status[, headers|status[, status]])
The _res.send()_ method is a high level response utility allowing you to pass
@@ -886,7 +894,7 @@ To parse incoming _Cookie_ headers, use the _cookieParser_ middleware, which pro
// use req.cookies.rememberme
});
### res.clearCookie(name)
### res.clearCookie(name[, options])
Clear cookie _name_ by setting "expires" far in the past.
@@ -1145,9 +1153,7 @@ Express also provides a few locals by default:
- `settings` the app's settings object
- `filename` the view's filename
- `request` the request object
- `response` the response object
- `app` the application itself
- `layout(path)` specify the layout from within a view
This method is aliased as _app.locals()_.
@@ -1168,6 +1174,101 @@ All views would now have _session_ available so that session data can be accesse
<%= session.name %>
### app.lookup
The _app.lookup_ http methods returns an array of callback functions
associated with the given _path_.
Suppose we define the following routes:
app.get('/user/:id', function(){});
app.put('/user/:id', function(){});
app.get('/user/:id/:op?', function(){});
We can utilize this lookup functionality to check which routes
have been defined, which can be extremely useful for higher level
frameworks built on Express.
app.lookup.get('/user/:id');
// => [Function]
app.lookup.get('/user/:id/:op?');
// => [Function]
app.lookup.put('/user/:id');
// => [Function]
app.lookup.all('/user/:id');
// => [Function, Function]
app.lookup.all('/hey');
// => []
To alias _app.lookup.VERB()_, we can simply invoke _app.VERB()_
without a callback, as a shortcut, for example the following are
equivalent:
app.lookup.get('/user');
app.get('/user');
Each function returned has the following properties:
var fn = app.get('/user/:id/:op?')[0];
fn.regexp
// => /^\/user\/(?:([^\/]+?))(?:\/([^\/]+?))?\/?$/i
fn.keys
// => ['id', 'op']
fn.path
// => '/user/:id/:op?'
fn.method
// => 'GET'
### app.match
The _app.match_ http methods return an array of callback functions
which match the given _url_, which may include a query string etc. This
is useful when you want reflect on which routes have the opportunity to
respond.
Suppose we define the following routes:
app.get('/user/:id', function(){});
app.put('/user/:id', function(){});
app.get('/user/:id/:op?', function(){});
Our match against __GET__ will return two functions, since the _:op_
in our second route is optional.
app.match.get('/user/1');
// => [Function, Function]
This second call returns only the callback for _/user/:id/:op?_.
app.match.get('/user/23/edit');
// => [Function]
We can also use _all()_ to disregard the http method:
app.match.all('/user/20');
// => [Function, Function, Function]
Each function matched has the following properties:
var fn = app.match.get('/user/23/edit')[0];
fn.keys
// => ['id', 'op']
fn.params
// => { id: '23', op: 'edit' }
fn.method
// => 'GET'
### app.mounted(fn)
Assign a callback _fn_ which is called when this _Server_ is passed to _Server#use()_.

View File

@@ -237,7 +237,7 @@ app.listen(3000);
<ul>
<li><a href="http://github.com/visionmedia/express-resource">express-resource</a> provides resourceful routing</li>
<li><a href="http://github.com/visionmedia/express-messages">express-messages</a> flash message notification rendering</li>
<li><a href="http://github.com/visionmedia/express-configure">express-configure</a> async configuration support (load settings from redis etc)</li>
<li><a href="http://github.com/visionmedia/express-configuration">express-configure</a> async configuration support (load settings from redis etc)</li>
<li><a href="http://github.com/visionmedia/express-namespace">express-namespace</a> namespaced routing support</li>
</ul>

View File

@@ -0,0 +1,35 @@
// Expose modules in ./support for demo purposes
require.paths.unshift(__dirname + '/../../support');
/**
* Module dependencies.
*/
var express = require('../../lib/express');
var app = express.createServer();
app.set('views', __dirname + '/views');
// set default layout, usually "layout"
app.set('view options', { layout: 'layouts/default' });
// Set our default template engine to "jade"
// which prevents the need for extensions
// (although you can still mix and match)
app.set('view engine', 'ejs');
app.get('/', function(req, res){
res.render('pages/default');
});
app.get('/alternate', function(req, res){
// note that we do not explicitly
// state the layout here, the view does,
// although we could do it here as well.
res.render('pages/alternate');
});
app.listen(3000);
console.log('Express app started on port 3000');

View File

@@ -0,0 +1,6 @@
<html>
<body>
<h1>Alternate Layout</h1>
<%- body %>
</body>
</html>

View File

@@ -0,0 +1,6 @@
<html>
<body>
<h1>Default Layout</h1>
<%- body %>
</body>
</html>

View File

@@ -0,0 +1,2 @@
<% layout('layouts/alternate') %>
<h1>Page</h1>

View File

@@ -0,0 +1 @@
<h1>Page</h1>

View File

@@ -14,7 +14,7 @@ exports.boot = function(app){
// App settings and middleware
function bootApplication(app) {
app.use(express.logger({ format: ':method :url :status' }));
app.use(express.logger(':method :url :status'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser());
@@ -45,6 +45,7 @@ function bootApplication(app) {
},
hasMessages: function(req){
if (!req.session) return false;
return Object.keys(req.session.flash || {}).length;
},

View File

@@ -27,7 +27,7 @@ var exports = module.exports = connect.middleware;
* Framework version.
*/
exports.version = '2.2.0';
exports.version = '2.3.0';
/**
* Shortcut for `new Server(...)`.
@@ -69,3 +69,8 @@ require('./response');
*/
require('./request');
// Error handler title
exports.errorHandler.title = 'Express';

View File

@@ -44,12 +44,13 @@ Server.prototype.__proto__ = connect.HTTPServer.prototype;
Server.prototype.init = function(middleware){
var self = this;
this.cache = {};
this.match = {};
this.lookup = {};
this.settings = {};
this.redirects = {};
this.isCallbacks = {};
this.viewHelpers = {};
this._locals = {};
this.dynamicViewHelpers = {};
this.errorHandlers = [];
@@ -91,12 +92,6 @@ Server.prototype.init = function(middleware){
, app: this
});
// default dynamic locals
this.dynamicHelpers({
request: function(req, res){ return req; }
, response: function(req, res){ return res; }
});
// default development configuration
this.configure('development', function(){
this.enable('hints');
@@ -104,7 +99,7 @@ Server.prototype.init = function(middleware){
// default production configuration
this.configure('production', function(){
this.enable('cache views');
this.enable('view cache');
});
// register error handlers on "listening"
@@ -247,7 +242,7 @@ Server.prototype.register = function(){
Server.prototype.helpers =
Server.prototype.locals = function(obj){
utils.merge(this.viewHelpers, obj);
utils.merge(this._locals, obj);
return this;
};
@@ -456,7 +451,7 @@ Server.prototype.configure = function(env, fn){
// Generate routing methods
function generateMethod(method){
Server.prototype[method] = function(path, fn){
Server.prototype[method] = function(path){
var self = this;
// Lookup
@@ -467,27 +462,10 @@ function generateMethod(method){
}
// Ensure router is mounted
if (!this.__usedRouter) {
this.use(this.router);
}
// Route specific middleware support
if (arguments.length > 2) {
var args = Array.prototype.slice.call(arguments, 1);
fn = args.pop();
(function stack(middleware){
middleware.forEach(function(fn){
if (Array.isArray(fn)) {
stack(fn);
} else {
self[method](path, fn);
}
});
})(args);
}
if (!this.__usedRouter) this.use(this.router);
// Generate the route
this.routes[method](path, fn);
this.routes[method].apply(this, arguments);
return this;
};
return arguments.callee;

View File

@@ -75,10 +75,12 @@ res.send = function(body, headers, status){
}
} else {
if (!this.header('Content-Type')) {
this.charset = this.charset || 'utf-8';
this.contentType('.json');
}
body = JSON.stringify(body);
if (this.req.query.callback && this.app.set('jsonp callback')) {
this.charset = this.charset || 'utf-8';
this.header('Content-Type', 'text/javascript');
body = this.req.query.callback.replace(/[^\w$.]/g, '') + '(' + body + ');';
}
@@ -130,6 +132,7 @@ res.send = function(body, headers, status){
*/
res.sendfile = function(path, options, fn){
var next = this.req.next;
options = options || {};
// support function as second arg
@@ -140,7 +143,7 @@ res.sendfile = function(path, options, fn){
options.path = path;
options.callback = fn;
send(this.req, this, this.req.next, options);
send(this.req, this, next, options);
};
/**
@@ -175,6 +178,7 @@ res.contentType = function(type){
*/
res.attachment = function(filename){
if (filename) this.contentType(filename);
this.header('Content-Disposition', filename
? 'attachment; filename="' + path.basename(filename) + '"'
: 'attachment');
@@ -183,32 +187,41 @@ res.attachment = function(filename){
/**
* Transfer the file at the given `path`, with optional
* `filename` as an attachment and optional callback `fn(err)`.
* `filename` as an attachment and optional callback `fn(err)`,
* and optional `fn2(err)` which is invoked when an error has
* occurred after headers have been sent.
*
* @param {String} path
* @param {String|Function} filename or fn
* @param {Function} fn
* @return {Type}
* @param {Function} fn2
* @api public
*/
res.download = function(path, filename, fn){
res.download = function(path, filename, fn, fn2){
var self = this;
// support callback as second arg
if ('function' == typeof filename) {
fn2 = fn;
fn = filename;
filename = null;
}
// transfer the file
this.attachment(filename || path).sendfile(path, function(err){
if (err) self.removeHeader('Content-Disposition');
if (fn) return fn(err);
var sentHeader = self._header;
if (err) {
self.req.next('ENOENT' == err.code
? null
: err);
if (!sentHeader) self.removeHeader('Content-Disposition');
if (sentHeader) {
fn2 && fn2(err);
} else if (fn) {
fn(err);
} else {
self.req.next(err);
}
} else if (fn) {
fn();
}
});
};
@@ -235,11 +248,15 @@ res.header = function(name, val){
* Clear cookie `name`.
*
* @param {String} name
* @param {Object} options
* @api public
*/
res.clearCookie = function(name){
this.cookie(name, '', { expires: new Date(1) });
res.clearCookie = function(name, options){
var opts = { expires: new Date(1) };
this.cookie(name, '', options
? utils.merge(options, opts)
: opts);
};
/**
@@ -394,7 +411,8 @@ res.local = function(name, val){
* @api public
*/
res.locals = function(obj){
res.locals =
res.helpers = function(obj){
if (obj) {
for (var key in obj) {
this.local(key, obj[key]);

View File

@@ -21,14 +21,6 @@ var path = require('path')
, http = require('http')
, res = http.ServerResponse.prototype;
/**
* Memory cache.
*
* @type Object
*/
var cache = {};
/**
* Expose constructors.
*/
@@ -113,16 +105,39 @@ function renderPartial(res, view, options, parentLocals, parent){
// Collection support
if (collection) {
var len = collection.length
, buf = '';
, buf = ''
, keys
, key
, val;
options.collectionLength = len;
for (var i = 0; i < len; ++i) {
var val = collection[i];
options.firstInCollection = i === 0;
options.indexInCollection = i;
options.lastInCollection = i === len - 1;
object = val;
buf += render();
if ('number' == typeof len || Array.isArray(collection)) {
for (var i = 0; i < len; ++i) {
val = collection[i];
options.firstInCollection = i == 0;
options.indexInCollection = i;
options.lastInCollection = i == len - 1;
object = val;
buf += render();
}
} else {
keys = Object.keys(collection);
len = keys.length;
options.collectionLength = len;
options.collectionKeys = keys;
for (var i = 0; i < len; ++i) {
key = keys[i];
val = collection[key];
options.keyInCollection = key;
options.firstInCollection = i == 0;
options.indexInCollection = i;
options.lastInCollection = i == len - 1;
object = val;
buf += render();
}
}
return buf;
} else {
return render();
@@ -238,10 +253,11 @@ res._render = function(view, opts, fn, parent, sub){
var options = {}
, self = this
, app = this.app
, helpers = app.viewHelpers
, helpers = app._locals
, dynamicHelpers = app.dynamicViewHelpers
, viewOptions = app.set('view options')
, cacheTemplates = app.set('cache views');
, cacheViews = app.enabled('view cache')
, root = app.set('views') || process.cwd() + '/views';
// merge "view options"
if (viewOptions) merge(options, viewOptions);
@@ -258,10 +274,7 @@ res._render = function(view, opts, fn, parent, sub){
// status support
if (options.status) this.statusCode = options.status;
// Defaults
var self = this
, root = app.set('views') || process.cwd() + '/views'
, partial = options.renderPartial
var partial = options.renderPartial
, layout = options.layout;
// Layout support
@@ -284,33 +297,6 @@ res._render = function(view, opts, fn, parent, sub){
// charset option
if (options.charset) this.charset = options.charset;
// Populate view
var orig = view = new View(view, options);
// Try _ prefix ex: ./views/_<name>.jade
if (!view.exists) view = new View(orig.prefixPath, options);
// Try index ex: ./views/user/index.jade
if (!view.exists) view = new View(orig.indexPath, options);
// Try ../<name>/index ex: ../user/index.jade
// when calling partial('user') within the same dir
if (!view.exists && !options.isLayout) view = new View(orig.upIndexPath, options);
// Try root ex: <root>/user.jade
if (!view.exists) view = new View(orig.rootPath, options);
// Try root _ prefix ex: <root>/_user.jade
if (!view.exists && partial) view = new View(view.prefixPath, options);
// Does not exist
if (!view.exists) {
if (app.enabled('hints')) hintAtViewPaths(orig, options);
var err = new Error('failed to locate view "' + orig.view + '"');
err.view = orig;
throw err;
}
// Dynamic helper support
if (false !== options.dynamicHelpers) {
// cache
@@ -336,15 +322,54 @@ res._render = function(view, opts, fn, parent, sub){
return renderPartial(self, path, opts, options, view);
};
// Provide filename to engine
options.filename = view.path;
// cached view
if (app.cache[view]) {
view = app.cache[view];
options.filename = view.path;
// resolve view
} else {
var orig = view = new View(view, options);
// Attempt render
var engine = view.templateEngine
, template = cacheTemplates
? cache[view.path] || (cache[view.path] = engine.compile(view.contents, options))
: engine.compile(view.contents, options)
, str = template.call(options.scope, options);
// Try _ prefix ex: ./views/_<name>.jade
if (partial) {
view = new View(orig.prefixPath, options);
if (!view.exists) view = orig;
}
// Try index ex: ./views/user/index.jade
if (!view.exists) view = new View(orig.indexPath, options);
// Try ../<name>/index ex: ../user/index.jade
// when calling partial('user') within the same dir
if (!view.exists && !options.isLayout) view = new View(orig.upIndexPath, options);
// Try root ex: <root>/user.jade
if (!view.exists) view = new View(orig.rootPath, options);
// Try root _ prefix ex: <root>/_user.jade
if (!view.exists && partial) view = new View(view.prefixPath, options);
// Does not exist
if (!view.exists) {
if (app.enabled('hints')) hintAtViewPaths(orig, options);
var err = new Error('failed to locate view "' + orig.view + '"');
err.view = orig;
throw err;
}
options.filename = view.path;
var engine = view.templateEngine;
view.fn = engine.compile(view.contents, options)
if (cacheViews) app.cache[orig.view] = view;
}
// layout helper
options.layout = function(path){
layout = path;
};
// render
var str = view.fn.call(options.scope, options);
// layout expected
if (layout) {

View File

@@ -17,17 +17,11 @@ var utils = require('../utils')
, stat = fs.statSync;
/**
* Memory cache.
* Require cache.
*/
var cache = {};
/**
* Existance cache.
*/
var exists = {};
/**
* Initialize a new `View` with the given `view` path and `options`.
*
@@ -38,7 +32,6 @@ var exists = {};
var View = exports = module.exports = function View(view, options) {
options = options || {};
// TODO: more caching
this.view = view;
this.root = options.root;
this.relative = false !== options.relative;
@@ -60,16 +53,11 @@ var View = exports = module.exports = function View(view, options) {
*/
View.prototype.__defineGetter__('exists', function(){
var path = this.path;
if (null != exists[path]) {
return exists[path];
} else {
try {
stat(path);
return exists[path] = true;
} catch (err) {
return exists[path] = false;
}
try {
stat(this.path);
return true;
} catch (err) {
return false;
}
});

View File

@@ -1,7 +1,7 @@
{
"name": "express",
"description": "Sinatra inspired web development framework",
"version": "2.2.0",
"version": "2.3.0",
"author": "TJ Holowaychuk <tj@vision-media.ca>",
"contributors": [
{ "name": "TJ Holowaychuk", "email": "tj@vision-media.ca" },
@@ -10,7 +10,7 @@
{ "name": "Guillermo Rauch", "email": "rauchg@gmail.com" }
],
"dependencies": {
"connect": ">= 1.1.6 < 2.0.0",
"connect": ">= 1.4.0 < 2.0.0",
"mime": ">= 0.0.1",
"qs": ">= 0.0.6"
},

View File

@@ -435,8 +435,9 @@ module.exports = {
);
},
'test route middleware': function(){
var app = express.createServer();
'test route middleware': function(beforeExit){
var app = express.createServer()
, calls = 0;
function allow(role) {
return function(req, res, next) {
@@ -460,6 +461,11 @@ module.exports = {
}
}
app.param('user', function(req, res, next, user){
++calls;
next();
});
app.get('/xxx', allow('member'), restrictAge(18), function(req, res){
res.send(200);
});
@@ -471,8 +477,12 @@ module.exports = {
app.get('/tobi', [allow('member')], [[restrictAge(18)]], function(req, res){
res.send(200);
});
app.get('/user/:user', [allow('member')], [[restrictAge(18)]], function(req, res){
res.send(200);
});
['xxx', 'booze', 'tobi'].forEach(function(thing){
['xxx', 'booze', 'tobi', 'user/tj'].forEach(function(thing){
assert.response(app,
{ url: '/' + thing },
{ body: 'Unauthorized', status: 401 });
@@ -483,6 +493,10 @@ module.exports = {
{ url: '/' + thing, headers: { 'X-Role': 'member', 'X-Age': 18 }},
{ body: 'OK', status: 200 });
});
beforeExit(function(){
calls.should.equal(3);
});
},
'test named capture groups': function(){

1
test/fixtures/_foobar.jade vendored Normal file
View File

@@ -0,0 +1 @@
p two

1
test/fixtures/foobar.jade vendored Normal file
View File

@@ -0,0 +1 @@
p one

2
test/fixtures/layout-switch.jade vendored Normal file
View File

@@ -0,0 +1,2 @@
- layout('layouts/alternate')
h1 My Page

1
test/fixtures/layouts/alternate.jade vendored Normal file
View File

@@ -0,0 +1 @@
#alternate!= body

1
test/fixtures/object-item.jade vendored Normal file
View File

@@ -0,0 +1 @@
li #{keyInCollection}: #{item}

View File

@@ -61,7 +61,7 @@ module.exports = {
assert.response(app,
{ url: '/bool' },
{ body: 'true'
, headers: { 'Content-Type': 'application/json' }});
, headers: { 'Content-Type': 'application/json; charset=utf-8' }});
assert.response(app,
{ url: '/html' },
@@ -76,7 +76,7 @@ module.exports = {
{ body: '{"foo":"bar"}'
, status: 201
, headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json; charset=utf-8'
, 'X-Foo': 'baz'
}});
@@ -85,7 +85,7 @@ module.exports = {
{ body: 'test({"foo":"bar"});'
, status: 201
, headers: {
'Content-Type': 'text/javascript'
'Content-Type': 'text/javascript; charset=utf-8'
, 'X-Foo': 'baz'
}});
@@ -93,7 +93,7 @@ module.exports = {
{ url: '/jsonp?callback=baz' },
{ body: 'baz({"foo":"bar"});'
, status: 201, headers: {
'Content-Type': 'text/javascript'
'Content-Type': 'text/javascript; charset=utf-8'
, 'X-Foo': 'baz'
}});
@@ -102,7 +102,7 @@ module.exports = {
{ body: 'invalid({"foo":"bar"});'
, status: 201
, headers: {
'Content-Type': 'text/javascript'
'Content-Type': 'text/javascript; charset=utf-8'
, 'X-Foo': 'baz'
}});
@@ -111,7 +111,7 @@ module.exports = {
{ body: '{"foo":"bar"}'
, status: 201
, headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json; charset=utf-8'
, 'X-Foo': 'baz'
}});
@@ -207,12 +207,14 @@ module.exports = {
assert.response(app,
{ url: '/javascripts/jquery.js' },
{ body: 'whatever'
, headers: { 'Content-Disposition': 'attachment; filename="jquery.js"' }});
, headers: { 'Content-Type': 'application/javascript'
, 'Content-Disposition': 'attachment; filename="jquery.js"' }});
assert.response(app,
{ url: '/style.css' },
{ body: 'some stylezzz'
, headers: { 'Content-Disposition': 'attachment' }});
, headers: { 'Content-Type': 'text/html; charset=utf-8'
, 'Content-Disposition': 'attachment' }});
},
'test #redirect()': function(){
@@ -568,7 +570,7 @@ module.exports = {
var app = express.createServer();
app.get('/', function(req, res){
res.clearCookie('rememberme');
res.clearCookie('rememberme', { path: '/foo' });
res.redirect('/');
});
@@ -576,7 +578,7 @@ module.exports = {
{ url: '/' },
function(res){
res.headers['set-cookie']
.should.eql(['rememberme=; expires=Thu, 01 Jan 1970 00:00:00 GMT']);
.should.eql(['rememberme=; path=/foo; expires=Thu, 01 Jan 1970 00:00:00 GMT']);
});
},

View File

@@ -287,6 +287,19 @@ module.exports = {
{ body: '<cool><p>Welcome</p></cool>' });
},
'test #render() view layout control': function(){
var app = create();
app.set('view engine', 'jade');
app.get('/', function(req, res){
res.render('layout-switch');
});
assert.response(app,
{ url: '/' },
{ body: '<div id="alternate"><h1>My Page</h1></div>' });
},
'test #render() "view engine" with periods in dirname': function(){
var app = create();
app.set('view engine', 'jade');
@@ -358,6 +371,19 @@ module.exports = {
});
},
'test #partial() collection object': function(){
var app = create();
app.get('/', function(req, res){
var items = { 2: 'foo', bar: 'bar' };
res.partial('object-item.jade', { as: 'item', collection: items });
});
assert.response(app,
{ url: '/' },
{ body: '<li>2: foo</li><li>bar: bar</li>' });
},
'test #partial()': function(){
var app = create();
@@ -631,6 +657,14 @@ module.exports = {
assert.response(app,
{ url: '/error' },
{ status: 500 });
app.get('/underscore', function(req, res, next){
res.partial('foobar');
});
assert.response(app,
{ url: '/underscore' },
{ body: '<p>two</p>' });
},
'test #partial() with several calls': function(){