Compare commits

...

135 Commits
0.0.2 ... 0.2.0

Author SHA1 Message Date
visionmedia
c8dd169ad9 Release 0.2.0 2010-02-03 19:28:31 -08:00
visionmedia
5495fbcd0c Merge branch 'upload' 2010-02-03 19:26:34 -08:00
visionmedia
eb94667ec8 Refactored mergeParam() 2010-02-03 19:26:09 -08:00
visionmedia
cf31355515 Added parseParam() support for name[] etc. Closes #180
PS. this is awesome! because now with WebKit (and opera?) you can have
<input type="file" name="images[]" multiple />
2010-02-03 19:24:33 -08:00
visionmedia
604c359a1c Fixed another spec 2010-02-03 18:45:39 -08:00
visionmedia
dad64b8a3b Fixed specs 2010-02-03 18:45:08 -08:00
visionmedia
ffa432baa2 More mergeParam() specs 2010-02-03 18:42:41 -08:00
visionmedia
5c4a356348 Added more specs 2010-02-03 18:23:31 -08:00
visionmedia
ef949aec20 Specs for key[] with uploads 2010-02-03 18:20:59 -08:00
visionmedia
eaea3f188d Added more mergeParam() specs 2010-02-03 18:14:25 -08:00
visionmedia
818135789b Added multiple file input for testing 2010-02-03 18:08:43 -08:00
visionmedia
2bbe573748 Removed some settings from chat app.
These can be used via EXPRESS_ENV=production
2010-02-03 17:38:55 -08:00
visionmedia
9efb15b267 Both Cache and Session option "reapInterval" may be "reapEvery". Closes #174 2010-02-03 16:59:00 -08:00
visionmedia
5ce1306b75 Merge branch 'cache-lifetime' 2010-02-03 16:57:24 -08:00
visionmedia
75c85663ad use() of the same plugin several time will always use latest options. Closes #176 2010-02-03 16:57:19 -08:00
visionmedia
8c1bcc4c47 Added expiration support to cache api with reaper. Closes #133 2010-02-03 16:27:25 -08:00
visionmedia
200d09c7bd cache Memory.Store#get() utilizing Collection 2010-02-03 16:11:01 -08:00
visionmedia
ef86474830 Added cache Store.Memory#reap() 2010-02-03 16:01:25 -08:00
visionmedia
e2fee9b353 Cache api using Cache instances 2010-02-03 15:57:59 -08:00
visionmedia
542cab1123 Renamed MemoryStore -> Store.Memory 2010-02-03 15:43:29 -08:00
visionmedia
315c05034f Merge branch 'abstract-session' 2010-02-03 15:33:35 -08:00
visionmedia
148d34629b Added abstract session Store. Closes #172 2010-02-03 15:33:29 -08:00
visionmedia
82891ea148 Release 0.1.0 2010-02-03 15:27:58 -08:00
visionmedia
cd1aa92d07 Docs 2010-02-03 15:20:54 -08:00
visionmedia
2a4f3525d9 mime() is no longer global. Renamed to exports.type() 2010-02-03 15:16:29 -08:00
visionmedia
944f99abe0 Added online user count to chat app 2010-02-03 15:10:44 -08:00
visionmedia
e083321a9d Merge branch 'session-count' 2010-02-03 15:05:07 -08:00
visionmedia
a0a1a543e8 Refactored MemoryStore#reap() with Collection 2010-02-03 15:04:52 -08:00
visionmedia
cf74bc655d Added MemoryStore#length() 2010-02-03 15:03:43 -08:00
visionmedia
0587278cda Consistant docs 2010-02-03 14:58:57 -08:00
visionmedia
b333dccc2e Hooks (before / after) pass request as arg
To prevent confusion with the value of "this", either will work
2010-02-03 14:52:45 -08:00
visionmedia
a71201bd55 Session plugin using utils.uid() 2010-02-03 14:30:04 -08:00
visionmedia
cec677062b Merge 2010-02-03 14:26:00 -08:00
visionmedia
327d5b0f88 Readme 2010-02-03 14:20:46 -08:00
visionmedia
20cf8f81a2 Session options in chat sample app 2010-02-03 14:18:40 -08:00
visionmedia
4d5a1b5f4d Fixed sessions when testing 2010-02-03 14:14:31 -08:00
visionmedia
9d5a6f9412 Session docs 2010-02-03 13:51:07 -08:00
visionmedia
7e92012415 Helpers are no longer global. Renamed to utils 2010-02-03 13:14:34 -08:00
visionmedia
6ee83ba8d0 Upgraded node support. Closes #169
Merge branch 'upgrade'
2010-02-03 13:05:42 -08:00
visionmedia
70a99ade67 Removed dirname() helper 2010-02-03 13:03:05 -08:00
visionmedia
634503b8ba Utilizing __dirname 2010-02-03 13:02:32 -08:00
visionmedia
2a4cbb1174 Misc refactoring 2010-02-03 11:45:50 -08:00
visionmedia
ca7d0f3e34 Upgraded JSpec 2010-02-03 11:40:33 -08:00
visionmedia
9cb2645d1c Merge branch 'fix-upload' 2010-02-03 11:35:31 -08:00
visionmedia
acf0a652b7 Added "upload timeout" setting defaulting to 5 seconds 2010-02-03 11:35:23 -08:00
visionmedia
2701dfd80b Fixed incomplete file upload issue. Closes #167
Route now waits until all files have been written / closed
until executing
2010-02-03 11:24:17 -08:00
visionmedia
047499dcd1 More session work 2010-02-01 18:50:34 -08:00
visionmedia
fa37e5a35f Added "session reap threshold" setting 2010-02-01 17:36:18 -08:00
visionmedia
4adbb4971f Added reaper() 2010-02-01 17:32:38 -08:00
visionmedia
8ed99f6bb8 Added session reaping 2010-02-01 17:29:23 -08:00
visionmedia
fbf547d127 Added Session class 2010-02-01 17:05:16 -08:00
visionmedia
f4b4c03cb5 Bulk of memory store based session support complete. 2010-02-01 16:49:47 -08:00
visionmedia
6b9a079bbf Added spec for mock sessions when testing 2010-02-01 15:49:51 -08:00
visionmedia
7238afbf4b Started Session plugin 2010-02-01 15:44:41 -08:00
visionmedia
0186659469 Fixed upload app submit button styling 2010-02-01 15:15:13 -08:00
visionmedia
50d14230f5 Merge branch 'uid' 2010-02-01 15:13:31 -08:00
visionmedia
29fe71c8f7 Implemented uid() for upload tempfiles. Closes #166 2010-02-01 15:13:27 -08:00
visionmedia
5fe033cef1 Added quick uid() helper 2010-02-01 15:10:44 -08:00
visionmedia
6490f0c193 Added aheckmann to contributor list 2010-02-01 13:25:49 -08:00
visionmedia
09ccd043f2 Throw Error 2010-01-29 07:49:11 -08:00
visionmedia
825e8b6f0c fix layout locals. Closes #163 2010-01-28 08:29:32 -08:00
visionmedia
1726dfb3b3 Sample upload app output 2010-01-26 09:01:46 -08:00
visionmedia
8a7ac0a6af Docs 2010-01-25 09:09:29 -08:00
visionmedia
d66ce086d4 Merge branch 'integration' 2010-01-25 09:01:17 -08:00
visionmedia
2bbf6cb8f9 Misc refactoring 2010-01-25 09:01:09 -08:00
visionmedia
4a52e641cf Merge branch 'master' of git://github.com/aheckmann/express into integration 2010-01-25 08:59:59 -08:00
visionmedia
b4c90def81 Merge branch 'integration' 2010-01-25 08:55:45 -08:00
visionmedia
50c0277e7f Merge branch 'master' of git://github.com/gjritter/express into integration 2010-01-25 08:55:37 -08:00
Greg Ritter
5a5f23fc88 Added some additional file extension to mime type mappings. 2010-01-22 21:24:27 -07:00
Aaron Heckmann
ae8f9e1137 bug fix for static.send method. request.halt() didn't return appropriately
Signed-off-by: Aaron Heckmann <aaron.heckmann@gmacfs.com>
2010-01-22 21:13:18 -05:00
visionmedia
2b92e4053b Merge branch 'fix-cookies' 2010-01-22 11:58:34 -08:00
visionmedia
8717e2e970 Fixed cookie paths, defaulting to / now 2010-01-22 11:58:31 -08:00
visionmedia
d0a9b0cb81 Updated libxmljs support. Closes #156
Merge branch 'libxmljs'
2010-01-22 10:55:37 -08:00
visionmedia
844eb5f7e9 Updated libxmljs submodule 2010-01-22 10:55:05 -08:00
visionmedia
41913fd8b9 Fixed two failures due to libxmljs update 2010-01-22 10:54:38 -08:00
visionmedia
2a5ccd8826 Updated libxmljs to v0.2.0 2010-01-22 10:52:57 -08:00
visionmedia
497402311f Merge branch 'master' of git://github.com/gjritter/express 2010-01-22 06:45:27 -08:00
Gregory Ritter
d52090619b Added some additional file extension to mime type mappings. 2010-01-21 22:38:00 -07:00
visionmedia
9d0d6e412b Removed unnecessary require in chat app 2010-01-19 16:18:40 -08:00
visionmedia
9b5605a944 Updated haml submodule 2010-01-19 15:19:30 -08:00
ciaranj
51f4c965b5 Added 'binary' encoding for cached static file responses. 2010-01-19 21:33:40 +00:00
ciaranj
046bee8844 Merge branch 'master' of git://github.com/visionmedia/express 2010-01-18 20:48:56 +00:00
visionmedia
971739089a Using sass property expansion in example app 2010-01-18 11:26:30 -08:00
visionmedia
f962f34e53 Removed sass file we dont need 2010-01-18 11:24:01 -08:00
visionmedia
34a88d029d Fixed query string spec
Failing due to number not being a string
2010-01-18 11:12:51 -08:00
visionmedia
d3b2e6057d make all -> make test 2010-01-18 11:10:08 -08:00
visionmedia
12a58361b4 Misc 2010-01-18 11:06:25 -08:00
visionmedia
0085b0a33b Uploads dont become part of params unless they have length 2010-01-18 11:03:04 -08:00
visionmedia
bf9505a4ce Fixed file upload 2010-01-18 11:01:44 -08:00
visionmedia
4c94457380 Fixed mergeParam() 2010-01-18 10:57:56 -08:00
visionmedia
af313e5e8c Misc work on upload sample app 2010-01-18 10:52:47 -08:00
visionmedia
eb7b96efe1 Added support for multi-part vals that are not files 2010-01-18 10:42:12 -08:00
visionmedia
5fe35bf97e Added multi-part upload support. Closes #155 2010-01-18 10:36:02 -08:00
visionmedia
12bc374ea9 Typo 2010-01-18 09:58:45 -08:00
visionmedia
68c57e08ae Started multi-part upload support 2010-01-18 09:54:50 -08:00
visionmedia
f73c3fc404 Misc styling for sample app 2010-01-18 08:57:14 -08:00
visionmedia
4d926d3840 POST /upload 2010-01-18 08:20:44 -08:00
visionmedia
817ff41b0d Started upload example app 2010-01-18 08:14:17 -08:00
visionmedia
f1614a5946 Moved chat app to examples/chat 2010-01-18 08:05:14 -08:00
visionmedia
17c1207d76 Added style.sass 2010-01-14 19:33:08 -08:00
visionmedia
a92667fdad Bigger docs for benchmarks 2010-01-12 14:23:47 -08:00
visionmedia
f03694e21e Added sass to the benchmarks 2010-01-12 14:20:36 -08:00
visionmedia
83fcf8b594 Better require paths
to pave the way for having a node package manager resolve dependencies
2010-01-12 14:15:18 -08:00
visionmedia
7afb895f1b Merge branch 'sass' 2010-01-12 14:07:02 -08:00
visionmedia
98e566ad69 Finished sass support. Closes #144
Check out my sass.js repo for more information
2010-01-12 14:06:58 -08:00
visionmedia
7d0470d285 Converted sample chat app css to sass 2010-01-12 14:06:04 -08:00
visionmedia
f8879ac5d1 Started converting to sass 2010-01-12 13:58:13 -08:00
visionmedia
9f263e357b Added Sass.js submodule 2010-01-12 13:36:24 -08:00
ciaranj
ae33e7b673 Merge branch 'master' of git://github.com/visionmedia/express
Conflicts:
	lib/express/static.js
2010-01-12 20:56:13 +00:00
visionmedia
b766234b93 Fixed readme example 2010-01-11 18:16:32 -08:00
visionmedia
a2899a0acd Misc styling 2010-01-11 17:06:57 -08:00
visionmedia
43da20a6b3 Misc refactoring 2010-01-11 16:59:06 -08:00
visionmedia
25cf42250a Refactored view contents caching 2010-01-11 16:55:53 -08:00
visionmedia
6b6ec3f19f Added link support to sample chat app 2010-01-11 16:35:22 -08:00
visionmedia
1a2e4342e2 production env caching view contents and static files 2010-01-11 16:25:23 -08:00
visionmedia
1ce1abf216 Static file caching. Closes #136
Boosts performance of static file serving roughly %45
2010-01-11 16:21:55 -08:00
ciaranj
704c933927 Merge branch 'master' of git://github.com/visionmedia/express 2010-01-11 22:11:35 +00:00
visionmedia
8207e7088f Memory cache now allows objects as values 2010-01-11 09:39:16 -08:00
visionmedia
46fdf10c97 "cache views" -> "cache view contents"
This will make way for "cache views" to be the canonical way
to cache view I/O contents AND their rendering
2010-01-11 09:23:29 -08:00
visionmedia
61b82480b0 Added cache plugin to plugins.js 2010-01-11 09:16:44 -08:00
visionmedia
4ce5c7650e Finished memory data-store for cache api. Closes #130
expiration etc coming soon.
2010-01-11 09:09:16 -08:00
visionmedia
965cbf63be Added normalize() 2010-01-11 08:55:56 -08:00
visionmedia
d4d76cda58 Started implemented cache methods 2010-01-11 08:49:12 -08:00
visionmedia
f2d08aa2e3 Added memory store specs 2010-01-11 08:29:56 -08:00
visionmedia
a82b1b9270 Started Store.Memory specs 2010-01-11 08:17:32 -08:00
visionmedia
c1617508c6 Started caching api 2010-01-11 07:59:34 -08:00
ciaranj
e125ae2c48 Merge branch 'master' of git://github.com/visionmedia/express 2010-01-06 18:34:42 +00:00
ciaranj
3f9cbbbffb Merge branch 'master' of git://github.com/visionmedia/express 2010-01-06 09:47:40 +00:00
ciaranj
6f7141a266 Merge branch 'master' of git://github.com/visionmedia/express 2010-01-05 23:04:50 +00:00
ciaranj
99e3130f3c Merge branch 'master' of git://github.com/visionmedia/express 2010-01-04 12:47:40 +00:00
ciaranj
774f96fcb9 Merge branch 'master' of github.com:ciaranj/express 2009-12-30 11:04:33 +00:00
ciaranj
17ab8e81aa Merge branch 'master' of git://github.com/visionmedia/express
Conflicts:
	lib/express/static.js
2009-12-30 11:03:27 +00:00
ciaranj
b02b384f14 Add support for binding to *all* hosts by passing 'null' as the hostname
when 'run-ing' the app
2009-12-22 23:09:16 +00:00
ciaranj
3ec970bb67 Added in a *Very* basic cache (this will ultimately leak memory)
Can be enabled on or off by using set('cache statics')
2009-12-22 22:25:30 +00:00
ciaranj
22e338fbd4 Add support to StaticFile so that it works with non-textual files.
Previously binary files were being interpreted as UTF8 encoded bytestreams
this isn't an ideal for pushing out static files such as images.

I've modified the StaticFile type so that it explicitly declares the encoding
as 'binary' and updated the mechanism used to push back to the client to
also be 'binary aware'.  I'm not happy with my approach as I've violated the
encapsulation of the logic that was previously in the request.respond method, but
without further direction on how that object is moving this was the easiest thing to do
in order to keep moving with my plan to have a full website ;)
2009-12-22 15:15:37 +00:00
49 changed files with 1525 additions and 257 deletions

3
.gitmodules vendored
View File

@@ -7,3 +7,6 @@
[submodule "lib/support/js-oo"]
path = lib/support/js-oo
url = git://github.com/visionmedia/js-oo.git
[submodule "lib/support/sass"]
path = lib/support/sass
url = git://github.com/visionmedia/sass.js.git

View File

@@ -1,4 +1,35 @@
0.2.0 / 2010-02-03
==================
* Added parseParam() support for name[] etc. (allows for file inputs with "multiple" attr) Closes #180
* Added Both Cache and Session option "reapInterval" may be "reapEvery". Closes #174
* Added expiration support to cache api with reaper. Closes #133
* Added cache Store.Memory#reap()
* Added Cache; cache api now uses first class Cache instances
* Added abstract session Store. Closes #172
* Changed; cache Memory.Store#get() utilizing Collection
* Renamed MemoryStore -> Store.Memory
* Fixed use() of the same plugin several time will always use latest options. Closes #176
0.1.0 / 2010-02-03
==================
* Changed; Hooks (before / after) pass request as arg as well as evaluated in their context
* Updated node support to 0.1.27 Closes #169
* Updated dirname(__filename) -> __dirname
* Updated libxmljs support to v0.2.0
* Added session support with memory store / reaping
* Added quick uid() helper
* Added multi-part upload support
* Added Sass.js support / submodule
* Added production env caching view contents and static files
* Added static file caching. Closes #136
* Added cache plugin with memory stores
* Added support to StaticFile so that it works with non-textual files.
* Removed dirname() helper
* Removed several globals (now their modules must be required)
0.0.2 / 2010-01-10
==================

View File

@@ -1,6 +1,8 @@
NODE = node
all: test
init:
@git submodule init && git submodule update
@@ -13,8 +15,13 @@ test-independant: init
test-dependant: init spec/support/libxmljs/libxmljs.node
@$(NODE) spec/node.js dependant
app:
@$(NODE) examples/app.js
app: app-chat
app-chat:
@$(NODE) examples/chat/app.js
app-upload:
@$(NODE) examples/upload/app.js
benchmark:
@$(NODE) benchmarks/collection.js

View File

@@ -11,6 +11,7 @@
* Sexy DSL with robust sinatra-like routing
* High performance
* Session support
* Mime helpers
* Redirection helpers
* Nested parameter parsing
@@ -21,7 +22,7 @@
* Light-weight JavaScript class implementation via js-oo
* Collections and chainable iterators
* ElementCollections / markup parsing via libxmljs and css selector traversal support via css2xpath
* View support (ejs, haml, mustache)
* View support (ejs, haml, sass, etc)
## Installation
@@ -49,16 +50,15 @@ Or with the [gh](http://github.com/visionmedia/gh) utility:
## Examples
require.paths.unshift('lib')
Below is a minimal app example when express is already within your load path.
require('express')
require('express/plugins')
configure(function(){
use(MethodOverride)
use(ContentLength)
use(Redirect)
set('root', dirname(__filename))
enable('cache views')
set('root', __dirname)
})
get('/hello', function(){
@@ -104,7 +104,7 @@ Run individual suites:
...
Express is currently being developed with node --version:
v0.1.24-13-ge2abc5f
v0.1.27
## More Information
@@ -117,6 +117,7 @@ Express is currently being developed with node --version:
* TJ Holowaychuk (visionmedia) &lt;tj@vision-media.ca&gt;
* Ciaran Jessup (ciaranj) &lt;ciaranj@gmail.com&gt;
* Gareth Jones (csausdev) &lt;gareth.jones@sensis.com.au&gt;
* Aaron Heckmann (aheckmann) &lt;aaron.heckmann+github@gmail.com&gt;
## License

View File

@@ -8,20 +8,37 @@ require('express')
print = puts
engine = {
ejs: require('support/ejs/ejs'),
haml: require('support/haml/lib/haml')
ejs: require('ejs'),
haml: require('haml'),
sass: require('sass')
}
options = { locals: { article: { title: 'Foo', body: 'bar' }}}
ejs = ' \n\
<h1><%= article.title %></h1> \n\
<p><%= article.body %></p> \n\
ejs = ' \n\
<div id="primary"> \n\
<div class="block first"> \n\
<h1><%= article.title %></h1> \n\
<p><%= article.body %></p> \n\
</div> \n\
</div> \n\
'
haml = ' \n\
%h1= article.title\n\
%p= article.body \n\
haml = ' \n\
#primary \n\
.block.first \n\
%h1= article.title \n\
%p= article.body \n\
'
sass = ' \n\
red: #ff0000 \n\
body \n\
ul \n\
li \n\
a \n\
:color !red \n\
:list-style none \n\
'
suite('Template Engines', 1000, function(){
@@ -32,4 +49,8 @@ suite('Template Engines', 1000, function(){
benchmark('haml', function(){
engine.haml.render(haml, options)
})
benchmark('sass', function(){
engine.sass.render(sass)
})
})

View File

@@ -4,15 +4,19 @@ require('express')
require('express/plugins')
configure(function(){
var fiveMinutes = 300000,
oneMinute = 60000
use(MethodOverride)
use(ContentLength)
use(CommonLogger)
set('root', dirname(__filename))
enable('cache views')
use(Cookie)
use(Cache, { lifetime: fiveMinutes, reapInterval: oneMinute })
use(Session, { lifetime: fiveMinutes, reapInterval: oneMinute })
set('root', __dirname)
})
var messages = [],
StaticFile = require('express/static').File
utils = require('express/utils')
get('/', function(){
this.redirect('/chat')
@@ -21,13 +25,20 @@ get('/', function(){
get('/chat', function(){
this.render('chat.haml.html', {
locals: {
messages: messages
title: 'Chat',
messages: messages,
name: this.session.name,
usersOnline: Session.store.length()
}
})
})
post('/chat', function(){
messages.push(escape(this.param('message')).replace(/:\)/g, '<img src="http://icons3.iconfinder.netdna-cdn.com/data/icons/ledicons/emoticon_smile.png">'))
this.session.name = this.param('name')
messages
.push(utils.escape(this.param('name')) + ': ' + utils.escape(this.param('message'))
.replace(/(http:\/\/[^\s]+)/g, '<a href="$1" target="express-chat">$1</a>')
.replace(/:\)/g, '<img src="http://icons3.iconfinder.netdna-cdn.com/data/icons/ledicons/emoticon_smile.png">'))
this.halt(200)
})
@@ -44,7 +55,11 @@ get('/chat/messages', function(){
})
get('/public/*', function(file){
this.sendfile(dirname(__filename) + '/public/' + file)
this.sendfile(__dirname + '/public/' + file)
})
get('/*.css', function(file){
this.render(file + '.sass.css', { layout: false })
})
get('/error/view', function(){
@@ -59,4 +74,8 @@ get('/simple', function(){
return 'Hello :)'
})
get('/favicon.ico', function(){
this.halt()
})
run()

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -5,7 +5,7 @@ $(function(){
var message = $('input[name=message]'),
name = $('input[name=name]')
if (message.val())
$.post('/chat', { message: $.trim(name.val()) + ': ' + message.val() }, function(){
$.post('/chat', { name: name.val(), message: message.val() }, function(){
message.val('')
})
else

View File

@@ -5,6 +5,6 @@
%li= msg
%form{ method: 'post' }
%input{ type: 'hidden', name: '_method', value: 'put' }
%input{ type: 'text', name: 'name', value: 'guest' }
%input{ type: 'text', name: 'name', value: name || 'guest' }
%input{ type: 'text', name: 'message' }
%input{ type: 'submit', value: 'Send' }

View File

@@ -0,0 +1,11 @@
%html
%head
%title= title
%script{ src: '/public/javascripts/jquery.js' }
%script{ src: '/public/javascripts/app.js' }
%link{ rel: 'stylesheet', href: '/style.css' }
%body
#wrapper= body
#online
Online:
%strong= usersOnline

View File

@@ -0,0 +1,81 @@
body
:font-family "Helvetica Neue", "Lucida Grande", "Arial"
:font-size 13px
:text-align center
=text-stroke 1px rgba(255, 255, 255, 0.1)
:color #555
h1, h2
:margin 0
:font-size 22px
:color #343434
h1
:text-shadow 1px 2px 2px #ddd
:font-size 60px
img.bubble
:position absolute
:top -25px
:left 120px
#wrapper
:position relative
:margin 100px auto
:width 500px
:text-align left
ul
:margin 0
:padding 0
:max-height 300px
:overflow-x hidden
li
:margin 5px 0
:padding 3px 8px
:list-style none
:border 1px solid #eee
=border-radius 3px
=border-radius 3px
li:hover
:cursor pointer
:color #2E2E2E
input[type=text]
:padding 5px
:border 1px solid #ddd
:outline none
=border-radius 2px
input[type=text]:focus
:border-color #00C3FF
input[type=submit]
=border-radius 2px
=box-shadow 0 1px 2px #ddd
:padding 6px 10px
:border solid 1px #999
:background -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ddd))
:color #333
:text-decoration none
:cursor pointer
:display inline-block
:text-align center
:text-shadow 0px 1px 1px #fff
:line-height 1
input[type=submit]:hover
:background -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#E6E4E4))
input[type=submit]:active
:background -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#c7c7c7))
input[name=name]
:width 80px
a
:color #1ABFF1
=transition-property padding
=transition-duration 0.15s
a:hover
:padding 0 5px
a:hover:before
:content 'visit: '
#online
:font-size 12px

View File

@@ -1,84 +0,0 @@
body {
font-family: "Helvetica Neue", "Lucida Grande", "Arial";
font-size: 13px;
text-align: center;
-webkit-text-stroke: 1px rgba(255, 255, 255, 0.1);
color: #555;
}
h1, h2 {
margin: 0;
font-size: 22px;
color: #343434;
}
h1 {
text-shadow: 1px 2px 2px #ddd;
font-size: 60px;
}
img.bubble {
position: absolute;
top: -25px;
left: 120px;
}
#wrapper {
position: relative;
margin: 100px auto;
width: 500px;
text-align: left;
}
ul {
margin: 0;
padding: 0;
}
ul li {
margin: 5px 0;
padding: 3px 8px;
list-style: none;
border: 1px solid #eee;
-webkit-border-radius: 3px;
-mox-border-radius: 3px;
-webkit-transition-property: color;
-webkit-transition-duration: 0.1s;
}
ul li:hover {
cursor: pointer;
color: #2E2E2E;
}
ul {
max-height: 300px;
overflow-x: hidden;
}
input[type=text] {
padding: 5px;
border: 1px solid #ddd;
outline: none;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
}
input[type=text]:focus {
border-color: #00C3FF;
}
input[type=submit] {
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
-webkit-box-shadow: 0 1px 2px #ddd;
-moz-box-shadow: 0 1px 2px #ddd;
padding: 6px 10px;
border: solid 1px #999;
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ddd));
color: #333;
text-decoration: none;
cursor: pointer;
display: inline-block;
text-align: center;
text-shadow: 0px 1px 1px #fff;
line-height: 1;
}
input[type=submit]:hover {
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#E6E4E4));
}
input[type=submit]:active {
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#c7c7c7));
}
input[name=name] {
width: 80px;
}

36
examples/upload/app.js Normal file
View File

@@ -0,0 +1,36 @@
require.paths.unshift('lib')
require('express')
require('express/plugins')
configure(function(){
use(MethodOverride)
use(ContentLength)
use(CommonLogger)
set('root', __dirname)
})
get('/', function(){
this.redirect('/upload')
})
get('/upload', function(){
this.render('upload.haml.html')
})
post('/upload', function(){
$(this.param('images')).each(function(image){
puts('uploaded ' + image.filename + ' to ' + image.tempfile)
})
this.redirect('/upload')
})
get('/public/*', function(file){
this.sendfile(__dirname + '/public/' + file)
})
get('/*.css', function(file){
this.render(file + '.sass.css', { layout: false })
})
run()

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,8 @@
%html
%head
%title Chat
%title Upload
%script{ src: '/public/javascripts/jquery.js' }
%script{ src: '/public/javascripts/app.js' }
%link{ rel: 'stylesheet', href: '/public/stylesheets/style.css' }
%link{ rel: 'stylesheet', href: '/style.css' }
%body
#wrapper= body

View File

@@ -0,0 +1,59 @@
body
:font-family "Helvetica Neue", "Lucida Grande", "Arial"
:font-size 13px
:text-align center
:-webkit-text-stroke 1px rgba(255, 255, 255, 0.1)
:color #555
h1, h2
:margin 0 0 15px 0
:font-size 22px
:color #343434
h1
:text-shadow 1px 2px 2px #ddd
:font-size 60px
h2
:margin-top 15px
#wrapper
:position relative
:margin 100px auto
:width 500px
:text-align left
input[type=file]
:padding 5px
:border 1px solid #ddd
:outline none
:-webkit-border-radius 2px
:-moz-border-radius 2px
input[type=file]:focus
:border-color #00C3FF
input[type=submit]
:-webkit-border-radius 2px
:-moz-border-radius 2px
:-webkit-box-shadow 0 1px 2px #ddd
:-moz-box-shadow 0 1px 2px #ddd
:padding 6px 10px
:border solid 1px #999
:background -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ddd))
:color #333
:text-decoration none
:cursor pointer
:display inline-block
:text-align center
:text-shadow 0px 1px 1px #fff
:line-height 1
input[type=submit]:hover
:background -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#E6E4E4))
input[type=submit]:active
:background -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#c7c7c7))
input[name=name]
:width 80px
form
.panel
:float left
:width 100%
:margin-bottom 15px

View File

@@ -0,0 +1,19 @@
%h1 Upload
:if typeof images !== 'undefined'
.images
:each img in images
%img{ src: img }
%h2 Singles
%form{ method: 'post', enctype: 'multipart/form-data' }
%input{ type: 'file', name: 'images[0]' }
%input{ type: 'file', name: 'images[1]' }
%input{ type: 'file', name: 'images[2]' }
.panel
%input{ type: 'submit', value: 'Upload' }
%h2 Multiple
%form{ method: 'post', enctype: 'multipart/form-data' }
%input{ type: 'file', name: 'images[]', multiple: 'multiple' }
.panel
%input{ type: 'submit', value: 'Upload' }

View File

@@ -1,3 +1,8 @@
require('support/js-oo/lib/oo')
require('express/core')
var path = require('path')
require.paths.unshift(__dirname + '/support/js-oo/lib')
require.paths.unshift(__dirname + '/support/ejs/lib')
require.paths.unshift(__dirname + '/support/haml/lib')
require.paths.unshift(__dirname + '/support/sass/lib')
require('oo')
require('express/core')

View File

@@ -29,8 +29,7 @@ function callback(fn) {
if (fn === undefined) return
if (fn instanceof Function) return fn
if (fn.length < 4) return Function('a, b, c', 'return a ' + fn + ' b')
if (property.test(fn) ||
method.test(fn)) fn = 'a.' + fn
if (property.test(fn) || method.test(fn)) fn = 'a.' + fn
return Function('a, b, c', 'return ' + fn)
}

View File

@@ -6,11 +6,17 @@ process.mixin(require('express/exceptions'))
process.mixin(require('express/collection'))
process.mixin(require('express/event'))
process.mixin(require('express/request'))
process.mixin(require('express/helpers'))
process.mixin(require('express/plugin'))
process.mixin(require('express/mime'))
process.mixin(require('express/dsl'))
/**
* Module dependencies.
*/
var multipart = require('multipart'),
File = require('file').File,
utils = require('express/utils')
// --- Route
Route = Class({
@@ -77,7 +83,7 @@ Route = Class({
var self = this
this.keys = []
if (path instanceof RegExp) return path
return new RegExp('^' + escapeRegexp(normalizePath(path), '.')
return new RegExp('^' + utils.escapeRegexp(normalizePath(path), '.')
.replace(/\*/g, '(.+)')
.replace(/(\/|\\\.):(\w+)\?/g, function(_, c, key){
self.keys.push(key)
@@ -213,9 +219,51 @@ Server = Class({
require('http')
.createServer(function(request, response){
request.body = ''
request.setBodyEncoding('utf8')
request.addListener('body', function(chunk){ request.body += chunk })
request.addListener('complete', function(){ self.route(request, response) })
request.setBodyEncoding('binary')
if (request.headers['content-type'] &&
request.headers['content-type'].indexOf('multipart/form-data') !== -1) {
var stream = new multipart.Stream(request),
promise = new process.Promise,
pendingFiles = 0
request.params = { post: {}}
promise.timeout(set('upload timeout') || 5000)
stream.addListener('part', function(part){
if (part.filename) {
var file = new File(part.tempfile = '/tmp/express-' + Number(new Date) + utils.uid(), 'w')
++pendingFiles
part.pos = 0
part.addListener('body', function(chunk){
file.write(chunk, part.pos, 'binary').addErrback(function(){
promise.emitError.apply(promise, arguments)
})
part.pos += chunk.length
})
.addListener('complete', function(){
file.close().addCallback(function(){
if (!--pendingFiles)
promise.emitSuccess()
}).addErrback(function(){
promise.emitError.apply(promise, arguments)
})
utils.mergeParam(part.name, part, request.params.post)
})
}
else
part.buf = '',
part
.addListener('body', function(chunk){ part.buf += chunk })
.addListener('complete', function(){
if (part.buf.length)
utils.mergeParam(part.name, part.buf, request.params.post)
})
}).addListener('complete', function(){
promise.addCallback(function(){ self.route(request, response) })
})
}
else
request
.addListener('body', function(chunk){ request.body += chunk })
.addListener('complete', function(){ self.route(request, response) })
})
.listen(this.port, this.host, this.backlog)
puts('Express started at http://' + this.host + ':' + this.port + '/ in ' + Express.environment + ' mode')
@@ -252,7 +300,7 @@ Server = Class({
// --- Express
Express = {
version: '0.0.2',
version: '0.2.0',
config: [],
routes: [],
plugins: [],
@@ -264,6 +312,7 @@ Express = {
configure(function(){
use(require('express/plugins/view').View)
use(require('express/plugins/cache').Cache)
use(require('express/plugins/redirect').Redirect)
use(require('express/plugins/body-decoder').BodyDecoder)
})
@@ -276,3 +325,8 @@ configure('development', function(){
configure('test', function(){
enable('throw exceptions')
})
configure('production', function(){
enable('cache view contents')
enable('cache static files')
})

View File

@@ -69,6 +69,10 @@ exports.disable = function(option) {
exports.run = function() {
configure(Express.environment = process.ENV.EXPRESS_ENV || 'development')
$(Express.plugins).each(function(plugin){
if ('init' in plugin.klass)
plugin.klass.init(plugin.options)
})
Express.server.run.apply(Express.server, arguments)
}
@@ -103,7 +107,7 @@ exports.configure = function(environment, fn) {
if (fn instanceof Function)
return Express.config.push([environment, fn])
if (typeof environment != 'string')
throw 'environment require'
throw new Error('environment required')
for (var i = 0, len = Express.config.length; i < len; ++i)
if (Express.config[i][0] == environment ||
Express.config[i][0] == 'all')

View File

@@ -2,7 +2,7 @@
// Express - ElementCollection - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
/**
* Load libxml support.
* Module dependencies.
*/
var libxml = require('libxmljs')
@@ -126,7 +126,7 @@ ElementCollection = Collection.extend({
*/
prev: function() {
return $([this.at(0).prev_sibling()])
return $([this.at(0).prevSibling()])
},
/**
@@ -137,7 +137,7 @@ ElementCollection = Collection.extend({
*/
next: function() {
return $([this.at(0).next_sibling()])
return $([this.at(0).nextSibling()])
},
/**

View File

@@ -1,136 +1,278 @@
// Express - Mime - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
var types = {
/**
* Module dependencies.
*/
var utils = require('express/utils')
/**
* Mime type lookup table.
*/
exports.types = {
'323' : 'text/h323',
'3gp' : 'video/3gpp',
'a' : 'application/octet-stream',
'acx' : 'application/internet-property-stream',
'ai' : 'application/postscript',
'aif' : 'audio/x-aiff',
'aifc' : 'audio/x-aiff',
'aiff' : 'audio/x-aiff',
'asc' : 'application/pgp-signature',
'asf' : 'video/x-ms-asf',
'asr' : 'video/x-ms-asf',
'asm' : 'text/x-asm',
'asx' : 'video/x-ms-asf',
'atom' : 'application/atom+xml',
'au' : 'audio/basic',
'avi' : 'video/x-msvideo',
'axs' : 'application/olescript',
'bas' : 'text/plain',
'bat' : 'application/x-msdownload',
'bcpio' : 'application/x-bcpio',
'bin' : 'application/octet-stream',
'bmp' : 'image/bmp',
'bz2' : 'application/x-bzip2',
'c' : 'text/x-c',
'cab' : 'application/vnd.ms-cab-compressed',
'cat' : 'application/vnd.ms-pkiseccat',
'cc' : 'text/x-c',
'cdf' : 'application/x-netcdf',
'cer' : 'application/x-x509-ca-cert',
'cgm' : 'image/cgm',
'chm' : 'application/vnd.ms-htmlhelp',
'class' : 'application/octet-stream',
'clp' : 'application/x-msclip',
'cmx' : 'image/x-cmx',
'cod' : 'image/cis-cod',
'com' : 'application/x-msdownload',
'conf' : 'text/plain',
'cpio' : 'application/x-cpio',
'cpp' : 'text/x-c',
'cpt' : 'application/mac-compactpro',
'crd' : 'application/x-mscardfile',
'crl' : 'application/pkix-crl',
'crt' : 'application/x-x509-ca-cert',
'csh' : 'application/x-csh',
'css' : 'text/css',
'csv' : 'text/csv',
'cxx' : 'text/x-c',
'dcr' : 'application/x-director',
'deb' : 'application/x-debian-package',
'der' : 'application/x-x509-ca-cert',
'diff' : 'text/x-diff',
'dir' : 'application/x-director',
'djv' : 'image/vnd.djvu',
'djvu' : 'image/vnd.djvu',
'dll' : 'application/x-msdownload',
'dmg' : 'application/octet-stream',
'dms' : 'application/octet-stream',
'doc' : 'application/msword',
'dot' : 'application/msword',
'dtd' : 'application/xml-dtd',
'dv' : 'video/x-dv',
'dvi' : 'application/x-dvi',
'dxr' : 'application/x-director',
'ear' : 'application/java-archive',
'eml' : 'message/rfc822',
'eps' : 'application/postscript',
'etx' : 'text/x-setext',
'evy' : 'application/envoy',
'exe' : 'application/x-msdownload',
'ez' : 'application/andrew-inset',
'f' : 'text/x-fortran',
'f77' : 'text/x-fortran',
'f90' : 'text/x-fortran',
'fif' : 'application/fractals',
'flr' : 'x-world/x-vrml',
'flv' : 'video/x-flv',
'for' : 'text/x-fortran',
'gem' : 'application/octet-stream',
'gemspec' : 'text/x-script.ruby',
'gif' : 'image/gif',
'gram' : 'application/srgs',
'grxml' : 'application/srgs+xml',
'gtar' : 'application/x-gtar',
'gz' : 'application/x-gzip',
'h' : 'text/x-c',
'hdf' : 'application/x-hdf',
'hh' : 'text/x-c',
'hlp' : 'application/winhlp',
'hqx' : 'application/mac-binhex40',
'hta' : 'application/hta',
'htc' : 'text/x-component',
'htm' : 'text/html',
'html' : 'text/html',
'htt' : 'text/webviewhtml',
'ice' : 'x-conference/x-cooltalk',
'ico' : 'image/vnd.microsoft.icon',
'ics' : 'text/calendar',
'ief' : 'image/ief',
'ifb' : 'text/calendar',
'iges' : 'model/iges',
'igs' : 'model/iges',
'iii' : 'application/x-iphone',
'ins' : 'application/x-internet-signup',
'isp' : 'application/x-internet-signup',
'iso' : 'application/octet-stream',
'jar' : 'application/java-archive',
'java' : 'text/x-java-source',
'jfif' : 'image/pipeg',
'jnlp' : 'application/x-java-jnlp-file',
'jp2' : 'image/jp2',
'jpe' : 'image/jpeg',
'jpeg' : 'image/jpeg',
'jpg' : 'image/jpeg',
'js' : 'application/javascript',
'json' : 'application/json',
'kar' : 'audio/midi',
'latex' : 'application/x-latex',
'lha' : 'application/octet-stream',
'lsf' : 'video/x-la-asf',
'lsx' : 'video/x-la-asf',
'lzh' : 'application/octet-stream',
'log' : 'text/plain',
'm13' : 'application/x-msmediaview',
'm14' : 'application/x-msmediaview',
'm3u' : 'audio/x-mpegurl',
'm4a' : 'audio/mp4a-latm',
'm4b' : 'audio/mp4a-latm',
'm4p' : 'audio/mp4a-latm',
'm4u' : 'video/vnd.mpegurl',
'm4v' : 'video/mp4',
'mac' : 'image/x-macpaint',
'man' : 'text/troff',
'mathml' : 'application/mathml+xml',
'mbox' : 'application/mbox',
'mdb' : 'application/x-msaccess',
'mdoc' : 'text/troff',
'me' : 'text/troff',
'mesh' : 'model/mesh',
'mht' : 'message/rfc822',
'mhtml' : 'message/rfc822',
'mid' : 'audio/midi',
'midi' : 'audio/midi',
'mif' : 'application/vnd.mif',
'mime' : 'message/rfc822',
'mml' : 'application/mathml+xml',
'mng' : 'video/x-mng',
'mny' : 'application/x-msmoney',
'mov' : 'video/quicktime',
'movie' : 'video/x-sgi-movie',
'mp2' : 'video/mpeg',
'mp3' : 'audio/mpeg',
'mp4' : 'video/mp4',
'mp4v' : 'video/mp4',
'mpa' : 'video/mpeg',
'mpe' : 'video/mpeg',
'mpeg' : 'video/mpeg',
'mpg' : 'video/mpeg',
'mpga' : 'audio/mpeg',
'mpp' : 'application/vnd.ms-project',
'mpv2' : 'video/mpeg',
'ms' : 'text/troff',
'msh' : 'model/mesh',
'msi' : 'application/x-msdownload',
'mvb' : 'application/x-msmediaview',
'mxu' : 'video/vnd.mpegurl',
'nc' : 'application/x-netcdf',
'nws' : 'message/rfc822',
'oda' : 'application/oda',
'odp' : 'application/vnd.oasis.opendocument.presentation',
'ods' : 'application/vnd.oasis.opendocument.spreadsheet',
'odt' : 'application/vnd.oasis.opendocument.text',
'ogg' : 'application/ogg',
'p' : 'text/x-pascal',
'p10' : 'application/pkcs10',
'p12' : 'application/x-pkcs12',
'p7b' : 'application/x-pkcs7-certificates',
'p7c' : 'application/x-pkcs7-mime',
'p7m' : 'application/x-pkcs7-mime',
'p7r' : 'application/x-pkcs7-certreqresp',
'p7s' : 'application/x-pkcs7-signature',
'pas' : 'text/x-pascal',
'pbm' : 'image/x-portable-bitmap',
'pct' : 'image/pict',
'pdb' : 'chemical/x-pdb',
'pdf' : 'application/pdf',
'pem' : 'application/x-x509-ca-cert',
'pfx' : 'application/x-pkcs12',
'pgm' : 'image/x-portable-graymap',
'pgn' : 'application/x-chess-pgn',
'pgp' : 'application/pgp-encrypted',
'pic' : 'image/pict',
'pict' : 'image/pict',
'pkg' : 'application/octet-stream',
'pko' : 'application/ynd.ms-pkipko',
'pl' : 'text/x-script.perl',
'pm' : 'text/x-script.perl-module',
'pma' : 'application/x-perfmon',
'pmc' : 'application/x-perfmon',
'pml' : 'application/x-perfmon',
'pmr' : 'application/x-perfmon',
'pmw' : 'application/x-perfmon',
'png' : 'image/png',
'pnm' : 'image/x-portable-anymap',
'pnt' : 'image/x-macpaint',
'pntg' : 'image/x-macpaint',
'pot' : 'application/vnd.ms-powerpoint',
'ppm' : 'image/x-portable-pixmap',
'pps' : 'application/vnd.ms-powerpoint',
'ppt' : 'application/vnd.ms-powerpoint',
'prf' : 'application/pics-rules',
'ps' : 'application/postscript',
'psd' : 'image/vnd.adobe.photoshop',
'pub' : 'application/x-mspublisher',
'py' : 'text/x-script.python',
'qt' : 'video/quicktime',
'qti' : 'image/x-quicktime',
'qtif' : 'image/x-quicktime',
'ra' : 'audio/x-pn-realaudio',
'rake' : 'text/x-script.ruby',
'ram' : 'audio/x-pn-realaudio',
'rar' : 'application/x-rar-compressed',
'ras' : 'image/x-cmu-raster',
'rb' : 'text/x-script.ruby',
'rdf' : 'application/rdf+xml',
'rgb' : 'image/x-rgb',
'rm' : 'application/vnd.rn-realmedia',
'rmi' : 'audio/mid',
'roff' : 'text/troff',
'rpm' : 'application/x-redhat-package-manager',
'rss' : 'application/rss+xml',
'rtf' : 'application/rtf',
'rtx' : 'text/richtext',
'ru' : 'text/x-script.ruby',
's' : 'text/x-asm',
'scd' : 'application/x-msschedule',
'sct' : 'text/scriptlet',
'setpay' : 'application/set-payment-initiation',
'setreg' : 'application/set-registration-initiation',
'sgm' : 'text/sgml',
'sgml' : 'text/sgml',
'sh' : 'application/x-sh',
'shar' : 'application/x-shar',
'sig' : 'application/pgp-signature',
'silo' : 'model/mesh',
'sit' : 'application/x-stuffit',
'skd' : 'application/x-koan',
'skm' : 'application/x-koan',
'skp' : 'application/x-koan',
'skt' : 'application/x-koan',
'smi' : 'application/smil',
'smil' : 'application/smil',
'snd' : 'audio/basic',
'so' : 'application/octet-stream',
'spc' : 'application/x-pkcs7-certificates',
'spl' : 'application/x-futuresplash',
'src' : 'application/x-wais-source',
'sst' : 'application/vnd.ms-pkicertstore',
'stl' : 'application/vnd.ms-pkistl',
'stm' : 'text/html',
'sv4cpio' : 'application/x-sv4cpio',
'sv4crc' : 'application/x-sv4crc',
'svg' : 'image/svg+xml',
'svgz' : 'image/svg+xml',
'swf' : 'application/x-shockwave-flash',
@@ -142,30 +284,60 @@ var types = {
'texi' : 'application/x-texinfo',
'texinfo' : 'application/x-texinfo',
'text' : 'text/plain',
'tgz' : 'application/x-compressed',
'tif' : 'image/tiff',
'tiff' : 'image/tiff',
'torrent' : 'application/x-bittorrent',
'tr' : 'text/troff',
'trm' : 'application/x-msterminal',
'tsv' : 'text/tab-seperated-values',
'txt' : 'text/plain',
'uls' : 'text/iuls',
'ustar' : 'application/x-ustar',
'vcd' : 'application/x-cdlink',
'vcf' : 'text/x-vcard',
'vcs' : 'text/x-vcalendar',
'vrml' : 'model/vrml',
'vxml' : 'application/voicexml+xml',
'war' : 'application/java-archive',
'wav' : 'audio/x-wav',
'wbmp' : 'image/vnd.wap.wbmp',
'wbxml' : 'application/vnd.wap.wbxml',
'wcm' : 'application/vnd.ms-works',
'wdb' : 'application/vnd.ms-works',
'wks' : 'application/vnd.ms-works',
'wma' : 'audio/x-ms-wma',
'wmf' : 'application/x-msmetafile',
'wml' : 'text/vnd.wap.wml',
'wmls' : 'text/vnd.wap.wmlscript',
'wmlsc' : 'application/vnd.wap.wmlscriptc',
'wmv' : 'video/x-ms-wmv',
'wmx' : 'video/x-ms-wmx',
'wps' : 'application/vnd.ms-works',
'wri' : 'application/x-mswrite',
'wrl' : 'model/vrml',
'wrz' : 'x-world/x-vrml',
'wsdl' : 'application/wsdl+xml',
'xaf' : 'x-world/x-vrml',
'xbm' : 'image/x-xbitmap',
'xht' : 'application/xhtml+xml',
'xhtml' : 'application/xhtml+xml',
'xla' : 'application/vnd.ms-excel',
'xlc' : 'application/vnd.ms-excel',
'xlm' : 'application/vnd.ms-excel',
'xls' : 'application/vnd.ms-excel',
'xlt' : 'application/vnd.ms-excel',
'xml' : 'application/xml',
'xof' : 'x-world/x-vrml',
'xpm' : 'image/x-xpixmap',
'xsl' : 'application/xml',
'xslt' : 'application/xslt+xml',
'xul' : 'application/vnd.mozilla.xul+xml',
'xwd' : 'image/x-xwindowdump',
'xyz' : 'chemical/x-xyz',
'yaml' : 'text/yaml',
'yml' : 'text/yaml',
'z' : 'application/x-compress',
'zip' : 'application/zip'
}
@@ -178,10 +350,11 @@ var types = {
* however this can be altered using the 'default mime type'
* setting.
*
* mime('png') // => 'image/png'
* mime('.png') // => 'image/png'
* mime('image.png') // => 'image/png'
* mime('path/to/image.png') // => 'image/png'
* var mime = require('express/mime')
* mime.type('png') // => 'image/png'
* mime.type('.png') // => 'image/png'
* mime.type('image.png') // => 'image/png'
* mime.type('path/to/image.png') // => 'image/png'
*
* @param {string} path
* @return {string}
@@ -189,9 +362,9 @@ var types = {
* @api public
*/
exports.mime = function(path) {
return types[path] ||
types[extname(path)] ||
exports.type = function(path) {
return exports.types[path] ||
exports.types[utils.extname(path)] ||
set('default mime type') ||
'application/octet-stream'
}
}

View File

@@ -3,6 +3,8 @@
/**
* Push _plugin_ with _options_ to the plugin stack.
* If _plugin_ has already been pushed, then it's options
* will override any previously set.
*
* @param {Plugin} plugin
* @param {hash} options
@@ -10,7 +12,12 @@
*/
exports.use = function(plugin, options) {
if ('init' in plugin) plugin.init()
if (Express.environment === 'test' && 'init' in plugin)
plugin.init(options)
$(Express.plugins).each(function(other, i){
if (other.klass === plugin)
delete Express.plugins[i]
})
Express.plugins.push({
klass: plugin,
options: options

View File

@@ -2,7 +2,9 @@
// Express - Plugins - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
process.mixin(require('express/plugins/hooks'))
process.mixin(require('express/plugins/cache'))
process.mixin(require('express/plugins/cookie'))
process.mixin(require('express/plugins/session'))
process.mixin(require('express/plugins/profiler'))
process.mixin(require('express/plugins/common-logger'))
process.mixin(require('express/plugins/content-length'))

View File

@@ -0,0 +1,218 @@
// Express - Cache - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
// --- Cache
var Cache = Class({
/**
* Initialize cache with _key_ and _val_.
*/
init: function(key, val) {
this.key = key
this.val = val
this.created = Number(new Date)
}
})
// --- Store
exports.Store = Class({
/**
* Ensure that the given _key_ is a string.
* Override in subclass to provide data-store specific functionality.
*
* @param {string} key
* @param {string} val
* @api public
*/
set: function(key, val) {
if (typeof key !== 'string') throw new Error(this.name + ' store #set() key must be a string')
},
/**
* Ensure that the given _key_ is a string.
* Override in subclass to provide data-store specific functionality.
*
* @param {string} key
* @api public
*/
get: function(key) {
if (typeof key !== 'string') throw new Error(this.name + 'store #get() key must be a string')
},
/**
* Convert to '[NAME Store]'.
*
* @return {string}
* @api public
*/
toString: function() {
return '[' + this.name + ' Store]'
}
})
// --- Store.Memory
exports.Store.Memory = exports.Store.extend({
/**
* Datastore name.
*/
name: 'Memory',
/**
* Initialize data.
*/
init: function() {
this.data = {}
},
/**
* Set the given _key_ to _val_, returning _val_.
*
* @param {string} key
* @param {string} val
* @return {string}
* @api public
*/
set: function(key, val) {
this.__super__(key, val)
return this.data[key] = new Cache(key, val), val
},
/**
* Get data found matching the given _key_.
*
* Examples:
*
* cache.get('page:front')
* // => '<html>...</html>'
*
* cache.get('page:*')
* // => { 'page:front': '<html>...</html>',
* 'page:users': '<html>...</html>',
* ... }
*
* @param {string} key
* @return {mixed}
* @api public
*/
get: function(key) {
this.__super__(key)
if (key.indexOf('*') === -1)
return this.data[key] instanceof Cache ?
this.data[key].val :
null
var regexp = this.normalize(key)
return $(this.data).reduce({}, function(vals, cache){
if (regexp.test(cache.key))
vals[cache.key] = cache.val
return vals
})
},
/**
* Clear data matching the given _key_.
*
* Examples:
*
* cache.clear('page:front')
* cache.clear('page:*')
*
* @param {string} key
* @api public
*/
clear: function(key) {
if (key.indexOf('*') === -1)
return delete this.data[key]
var regexp = this.normalize(key)
for (var key in this.data)
if (this.data.hasOwnProperty(key))
if (regexp.test(key))
delete this.data[key]
},
/**
* Reap caches older than _ms_.
*
* @param {int} ms
* @api private
*/
reap: function(ms) {
var self = this,
threshold = Number(new Date(Number(new Date) - ms))
$(this.data).each(function(cache){
if (cache.created < threshold)
self.clear(cache.key)
})
},
/**
* Convert the given key matching _pattern_
* into a RegExp.
*
* - * is converted to (.*?)
*
* @param {string} pattern
* @return {regexp}
* @api private
*/
normalize: function(pattern) {
return new RegExp('^' + pattern.replace(/[*]/g, '(.*?)') + '$')
}
})
// --- Cache
exports.Cache = Plugin.extend({
extend: {
/**
* Initialize memory store and start reaper.
*
* Options:
*
* - dataStore constructor name of cache data store, defaults to Store.Memory
* - lifetime lifetime of cache in milliseconds, defaults to one day
* - reapInterval, reapEvery interval in milliseconds in which to reap old caches, defaults to one hour
*
* @param {hash} options
* @api private
*/
init: function(options) {
process.mixin(this, options)
this.store = new (this.dataStore || exports.Store.Memory)(options)
Request.include({ cache: this.store })
this.startReaper()
},
/**
* Start reaper.
*
* @api private
*/
startReaper: function() {
var self = this,
oneDay = 86400000,
oneHour = 3600000
setInterval(function(){
self.store.reap(self.lifetime || oneDay)
}, self.reapInterval || self.reapEvery || oneHour)
}
}
})

View File

@@ -56,12 +56,12 @@ exports.Cookie = Plugin.extend({
*
* Options:
*
* - path: Cookie path, defaults to '/'
* - domain: Tail matched domain name such as 'vision-media.ca' or 'blog.vision-media.ca' etc
* - expires: Date object converted to 'Wdy, DD-Mon-YYYY HH:MM:SS GMT'
* - path Cookie path, defaults to '/'
* - domain Tail matched domain name such as 'vision-media.ca' or 'blog.vision-media.ca' etc
* - expires Date object converted to 'Wdy, DD-Mon-YYYY HH:MM:SS GMT'
* when undefined the cookie will last the duration of a the
* client's session.
* - secure: When true the cookie will be sent by the client only when transfering data via HTTPS
* - secure When true the cookie will be sent by the client only when transfering data via HTTPS
* - httpOnly When true the cookie will be sent to the server only and will not be accessable via
* client-side scripting.
*
@@ -73,6 +73,8 @@ exports.Cookie = Plugin.extend({
*/
cookie: function(name, val, options) {
options = options || {}
options.path = options.path || '/'
return val ?
this.response.cookies.push(exports.compileCookie(name, val, options)) :
this.cookies[name]
@@ -86,13 +88,14 @@ exports.Cookie = Plugin.extend({
on: {
/**
* Parser request cookie data.
* Parse request cookie data.
*/
request: function(event) {
event.request.response.cookies = []
if (event.request.headers.cookie)
event.request.cookies = exports.parseCookie(event.request.headers.cookie)
event.request.cookies = event.request.headers.cookie ?
exports.parseCookie(event.request.headers.cookie) :
{}
},
/**

View File

@@ -50,7 +50,7 @@ exports.Hooks = Plugin.extend({
request: function(event) {
$(before).each(function(fn){
fn.call(event.request)
fn.call(event.request, event.request)
})
},
@@ -60,7 +60,7 @@ exports.Hooks = Plugin.extend({
response: function(event) {
$(after).each(function(fn){
fn.call(event.request)
fn.call(event.request, event.request)
})
}
}

View File

@@ -0,0 +1,205 @@
// Express - Session - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
/**
* Module dependencies.
*/
var utils = require('express/utils')
// --- Session
var Session = Class({
/**
* Initialize session _sid_.
*/
init: function(sid) {
this.id = sid
this.touch()
},
/**
* Update last access time.
*
* @api private
*/
touch: function() {
this.lastAccess = Number(new Date)
}
})
// --- Store
exports.Store = Class({
/**
* Convert to '[NAME Store]'.
*
* @return {string}
* @api public
*/
toString: function() {
return '[' + this.name + ' Store]'
}
})
// --- Store.Memory
exports.Store.Memory = exports.Store.extend({
/**
* Datastore name.
*/
name: 'Memory',
/**
* Initialize in-memory session store.
*/
init: function() {
this.store = {}
},
/**
* Fetch session with the given _sid_ or
* a new Session is returned.
*
* @param {int} sid
* @return {Session}
* @api private
*/
fetch: function(sid) {
return this.store[sid] || new Session(sid)
},
/**
* Commit _session_ data.
*
* @param {Session} session
* @api private
*/
commit: function(session) {
return this.store[session.id] = session
},
/**
* Clear all sessions.
*
* @api public
*/
clear: function() {
this.store = {}
},
/**
* Destroy session using the given _sid_.
*
* @param {int} sid
* @api public
*/
destroy: function(sid) {
delete this.store[sid]
},
/**
* Return the number of sessions currently stored.
*
* @return {int}
* @api public
*/
length: function() {
return $(this.store).length()
},
/**
* Reap sessions older than _ms_.
*
* @param {int} ms
* @api private
*/
reap: function(ms) {
var self = this,
threshold = Number(new Date(Number(new Date) - ms))
$(this.store).each(function(session, sid){
if (session.lastAccess < threshold)
self.destroy(sid)
})
}
})
// --- Cache
exports.Session = Plugin.extend({
extend: {
/**
* Initialize memory store and start reaper.
*
* Options:
*
* - dataStore   constructor name of session data store, defaults to Store.Memory
* - lifetime lifetime of session in milliseconds, defaults to one day
* - reapInterval, reapEvery interval in milliseconds in which to reap old sessions, defaults to one hour
*
* @param {hash} options
* @api private
*/
init: function(options) {
process.mixin(this, options)
this.store = new (this.dataStore || exports.Store.Memory)(options)
this.startReaper()
},
/**
* Start reaper.
*
* @api private
*/
startReaper: function() {
var self = this,
oneDay = 86400000,
oneHour = 3600000
setInterval(function(){
self.store.reap(self.lifetime || oneDay)
}, self.reapInterval || self.reapEvery || oneHour)
}
},
// --- Events
on: {
/**
* Create session id when not found; delegate to store.
*/
request: function(event) {
var sid
if (!(sid = event.request.cookie('sid')))
event.request.cookie('sid', sid = utils.uid(), set('session cookie'))
event.request.session = exports.Session.store.fetch(sid)
event.request.session.touch()
},
/**
* Delegate to store, allowing it to save sessions changes.
*/
response: function(event) {
exports.Session.store.commit(event.request.session)
}
}
})

View File

@@ -1,21 +1,21 @@
// Express - View - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
var posix = require('posix')
/**
* Template content cache.
* Module dependencies.
*/
var cache = {}
var posix = require('posix'),
utils = require('express/utils')
/**
* Supported template engines.
*/
var engine = {
ejs: require('support/ejs/ejs'),
haml: require('support/haml/lib/haml')
ejs: require('ejs'),
haml: require('haml'),
sass: require('sass')
}
// --- View
@@ -57,7 +57,7 @@ exports.View = Plugin.extend({
*
* @param {string} view
* @param {hash} options
* @settings 'views', 'cache views'
* @settings 'views', 'cache view contents'
* @api public
*/
@@ -66,13 +66,13 @@ exports.View = Plugin.extend({
options = options || {},
path = set('views') + '/' + view,
type = path.split('.').slice(-2)[0],
ext = extname(path),
ext = utils.extname(path),
layout = options.layout === undefined ? true : options.layout
self.contentType(ext)
function render(content) {
content = engine[type].render(content, options)
if (layout)
self.render('layout.' + type + '.' + ext, process.mixin(options, {
self.render('layout.' + type + '.' + ext, process.mixin(true, options, {
layout: false,
locals: {
body: content
@@ -81,11 +81,13 @@ exports.View = Plugin.extend({
else
self.halt(200, content)
}
if (set('cache views') && cache[view])
render(cache[view])
if (set('cache view contents') && self.cache.get(path))
render(self.cache.get(path))
else
posix.cat(path).addCallback(function(content){
render(cache[view] = content)
set('cache view contents') ?
render(self.cache.set(path, content)) :
render(content)
}).addErrback(function(e){
throw e
})

View File

@@ -8,6 +8,7 @@
var StaticFile = require('express/static').File,
statusBodies = require('http').STATUS_CODES,
queryString = require('querystring'),
mime = require('express/mime'),
url = require('url')
// --- InvalidStatusCode
@@ -67,11 +68,10 @@ exports.Request = Class({
this.response = response
this.url = url.parse(this.url)
this.url.pathname = exports.normalizePath(this.url.pathname)
this.params = {
get: this.url.query ? queryString.parseQuery(this.url.query) : {},
post: {},
path: {}
}
this.params = this.params || {}
this.params.path = {}
this.params.get = this.url.query ? queryString.parseQuery(this.url.query) : {}
this.params.post = this.params.post || {}
this.plugins = $(Express.plugins).map(function(plugin){
return new plugin.klass(plugin.options)
})
@@ -113,20 +113,19 @@ exports.Request = Class({
/**
* Check if Accept header includes the mime type
* for the given _path_, which calls mime().
* for the given _path_, which calls mime.type().
*
* When no Accept header is present true will be
* returned as stated in the HTTP specification.
*
* @param {string} path
* @return {bool}
* @see mime()
* @api public
*/
accepts: function(path) {
return this.header('accept') ?
this.header('accept').indexOf(mime(path)) !== -1 :
this.header('accept').indexOf(mime.type(path)) !== -1 :
true
},
@@ -187,16 +186,15 @@ exports.Request = Class({
/**
* Set Content-Type header to the mime type
* for the given _path_, which calls mime().
* for the given _path_, which calls mime.type().
*
* @param {string} path
* @return {Request}
* @see mime()
* @api public
*/
contentType: function(path) {
this.header('content-type', mime(path))
this.header('content-type', mime.type(path))
return this
},

View File

@@ -35,19 +35,26 @@ exports.File = Class({
* - Ensures the file exists
* - Ensures the file is a regular file (not FIFO, Socket, etc)
* - Automatically assigns content type
* - Halts with 404 when failing
*
* @param {Request} request
* @settings 'cache static files'
* @api public
*/
send: function(request) {
var file = this.path
var cache, file = this.path
if (set('cache static files') && (cache = request.cache.get(file)))
return request.contentType(cache.type),
request.halt(200, cache.content, 'binary')
path.exists(file, function(exists){
if (!exists) request.halt()
if (!exists) return request.halt()
posix.stat(file).addCallback(function(stats){
if (!stats.isFile()) request.halt()
if (!stats.isFile()) return request.halt()
posix.cat(file, 'binary').addCallback(function(content){
request.contentType(file)
if (set('cache static files'))
request.cache.set(file, { type: file, content: content })
request.halt(200, content, 'binary')
})
})

View File

@@ -1,6 +1,12 @@
// Express - Helpers - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
/**
* Module dependencies.
*/
var queryString = require('querystring')
/**
* JSON aliases.
*/
@@ -9,15 +15,17 @@ JSON.encode = JSON.stringify
JSON.decode = JSON.parse
/**
* Return the directory name of the given _path_.
* Return a unique identifier.
*
* @param {string} path
* @return {string}
* @api public
*/
exports.dirname = function(path) {
return path.split('/').slice(0, -1).join('/')
exports.uid = function() {
var uid = ''
for (var n = 4; n; --n)
uid += (Math.abs((Math.random() * 0xFFFFFFF) | 0)).toString(16)
return uid
}
/**
@@ -92,4 +100,35 @@ exports.toArray = function(arr, offset) {
exports.escapeRegexp = function(string, chars) {
var specials = (chars || '/ . * + ? | ( ) [ ] { } \\').split(' ').join('|\\')
return string.replace(new RegExp('(\\' + specials + ')', 'g'), '\\$1')
}
/**
* Merge param _key_ and _val_ into _params_. Key
* should be a query string key such as 'user[name]',
* and _val_ is it's associated object. The root _params_
* object is returned.
*
* @param {string} key
* @param {mixed} val
* @return {hash}
* @api public
*/
exports.mergeParam = function(key, val, params) {
var orig = params,
keys = key.trim().match(/\w+/g),
array = /\[\]$/.test(key)
$(keys).reduce(queryString.parseQuery(key), function(parts, key, i){
if (i === keys.length - 1)
if (key in params)
params[key] instanceof Array ?
params[key].push(val) :
params[key] = [params[key], val]
else
params[key] = array ? [val] : val
if (!(key in params)) params[key] = {}
params = params[key]
return parts[key]
})
return orig
}

1
lib/support/sass Submodule

Submodule lib/support/sass added at dfc1cd027f

View File

@@ -4,7 +4,7 @@
;(function(){
JSpec = {
version : '3.1.0',
version : '3.2.1',
assert : true,
cache : {},
suites : [],
@@ -1046,7 +1046,7 @@
*/
expect : function(actual) {
assert = function(matcher, args, negate) {
function assert(matcher, args, negate) {
var expected = toArray(args, 1)
matcher.negate = negate
assertion = new JSpec.Assertion(matcher, actual, expected, negate)
@@ -1056,11 +1056,11 @@
return assertion.result
}
to = function(matcher) {
function to(matcher) {
return assert(matcher, arguments, false)
}
not_to = function(matcher) {
function not_to(matcher) {
return assert(matcher, arguments, true)
}
@@ -1692,7 +1692,7 @@
case Number:
case RegExp:
case Function:
state = actual.toString().match(arg.toString())
state = actual.toString().indexOf(arg) !== -1
break
case Object:

View File

@@ -27,12 +27,13 @@ specs = {
independant: [
'core',
'routing',
'helpers',
'utils',
'request',
'mime',
'static',
'collection',
'plugins',
'plugins.cache',
'plugins.view',
'plugins.common-logger',
'plugins.content-length',
@@ -41,6 +42,7 @@ specs = {
'plugins.redirect',
'plugins.hooks',
'plugins.cookie',
'plugins.session',
],
dependant: [
'element-collection'
@@ -62,4 +64,5 @@ switch (process.ARGV[2]) {
run([process.ARGV[2]])
}
Express.environment = 'test'
JSpec.run({ reporter: JSpec.reporters.Terminal, failuresOnly: true }).report()

View File

@@ -1,48 +0,0 @@
describe 'Express'
describe 'toArray()'
describe 'when given an array'
it 'should return the array'
toArray([1,2,3]).should.eql [1,2,3]
end
end
describe 'when given an object with indexed values and length'
it 'should return an array'
var args = -{ return arguments }('foo', 'bar')
toArray(args).should.eql ['foo', 'bar']
end
end
end
describe 'escape()'
it 'should escape html'
escape('<p>this & that').should.eql '&lt;p&gt;this &amp; that'
end
end
describe 'extname()'
it 'should return the a files extension'
extname('image.png').should.eql 'png'
extname('image.large.png').should.eql 'png'
extname('/path/to/image.large.png').should.eql 'png'
end
it 'should return null when not found'
extname('path').should.be_null
extname('/just/a/path').should.be_null
end
end
describe 'dirname()'
it 'should return the directory path'
dirname('/path/to/images/foo.bar.png').should.eql '/path/to/images'
end
end
describe 'basename()'
it 'should return a files basename'
basename('foo/bar/baz.image.png').should.eql 'baz.image.png'
end
end
end

View File

@@ -1,36 +1,42 @@
describe 'Express'
before
mime = require('express/mime')
end
before_each
reset()
end
describe 'mime()'
describe 'when given an extension with leading dot'
it 'should return the associated mime type'
mime('.png').should.eql 'image/png'
describe 'mime'
describe 'type()'
describe 'when given an extension with leading dot'
it 'should return the associated mime type'
mime.type('.png').should.eql 'image/png'
end
end
end
describe 'when given an extension without leading dot'
it 'should return the associated mime type'
mime('png').should.eql 'image/png'
describe 'when given an extension without leading dot'
it 'should return the associated mime type'
mime.type('png').should.eql 'image/png'
end
end
end
describe 'when given a file path'
it 'should return the associated mime type'
mime('/path/to/an/image.png').should.eql 'image/png'
describe 'when given a file path'
it 'should return the associated mime type'
mime.type('/path/to/an/image.png').should.eql 'image/png'
end
end
end
describe 'when given an unknown extension'
it 'should default to the "default mime type" setting'
set('default mime type', 'text/plain')
mime('meow').should.eql 'text/plain'
end
it 'should default to "application/octet-stream" otherwise'
mime('meow').should.eql 'application/octet-stream'
describe 'when given an unknown extension'
it 'should default to the "default mime type" setting'
set('default mime type', 'text/plain')
mime.type('meow').should.eql 'text/plain'
end
it 'should default to "application/octet-stream" otherwise'
mime.type('meow').should.eql 'application/octet-stream'
end
end
end
end

119
spec/spec.plugins.cache.js Normal file
View File

@@ -0,0 +1,119 @@
describe 'Express'
before_each
reset()
use(require('express/plugins/cache').Cache)
cache = require('express/plugins/cache')
end
describe 'Cache'
describe 'Request'
describe '#cache'
it 'should use memory store by default'
get('/item', function(){
return this.cache.toString()
})
get('/item').body.should.eql '[Memory Store]'
end
end
end
end
describe 'cache Store.Memory'
before_each
store = new cache.Store.Memory
end
describe '#toString()'
it 'should return [Memory Store]'
store.toString().should.eql '[Memory Store]'
end
end
describe '#set()'
describe 'given a key and value'
it 'should set the cache data'
store.set('foo', 'bar')
store.get('foo').should.eql 'bar'
end
it 'should override existing data'
store.set('foo', 'bar')
store.set('foo', 'baz')
store.get('foo').should.eql 'baz'
end
it 'should return data'
store.set('foo', 'bar').should.eql 'bar'
end
end
describe 'given an abitrary key'
it 'should throw an error'
-{ store.set({}, 'foo') }.should.throw_error
end
end
describe 'given an abitrary value'
it 'should serialize as JSON'
store.set('user', { name: 'tj' }).should.eql { name: 'tj' }
end
end
end
describe '#get()'
describe 'given a key'
it 'should return cached value'
store.set('foo', 'bar')
store.get('foo').should.eql 'bar'
end
it 'should unserialize JSON data'
store.set('user', { name: 'tj' })
store.get('user').should.eql { name: 'tj' }
end
end
describe 'given wildcards'
it 'should return a set of caches'
store.set('user:1', 'a')
store.set('user:2', 'b')
store.set('foo', 'bar')
store.get('user:*').should.eql { 'user:1': 'a', 'user:2': 'b' }
end
end
end
describe '#clear()'
describe 'given a key'
it 'should delete previous data'
store.set('foo', 'bar')
store.clear('foo')
store.get('foo').should.be_null
end
end
describe 'given wildcards'
it 'should clear a set of caches'
store.set('user:one', '1')
store.set('user:two', '2')
store.clear('user:*')
store.get('user:one').should.be_null
store.get('user:two').should.be_null
end
end
end
describe '#reap()'
it 'should destroy caches older than the given age in milliseconds'
store.set('user:one', '1')
store.data['user:one'].created = Number(new Date) - 300
store.set('user:two', '2')
store.data['user:two'].created = Number(new Date) - 100
store.reap(200)
store.get('user:one').should.be_null
store.get('user:two').should.not.be_null
end
end
end
end

View File

@@ -82,7 +82,7 @@ describe 'Express'
this.cookie('foo', 'bar')
return ''
})
get('/user').headers['set-cookie'].should.eql 'SID=732423sdfs73243; path=/; secure, foo=bar'
get('/user').headers['set-cookie'].should.eql 'SID=732423sdfs73243; path=/; secure, foo=bar; path=/'
end
end
end

View File

@@ -1,4 +1,6 @@
var mime = require('express/mime')
CSSColors = Plugin.extend({
extend: {
init: function() {
@@ -7,7 +9,7 @@ CSSColors = Plugin.extend({
},
on: {
response: function(event) {
if (event.response.headers['content-type'] == mime('css'))
if (event.response.headers['content-type'] == mime.type('css'))
event.response.body = event.response.body.replace('black', '#000')
}
}

View File

@@ -0,0 +1,108 @@
describe 'Express'
before_each
reset()
use(require('express/plugins/cookie').Cookie)
use(Session = require('express/plugins/session').Session)
Session.store.clear()
end
describe 'Session'
describe 'when sid cookie is not present'
it 'should set sid cookie'
get('/login', function(){ return '' })
get('/login').headers['set-cookie'].should.match(/^sid=(\w+);/)
end
end
describe 'when sid cookie is present'
it 'should not set sid'
get('/login', function(){ return '' })
get('/login', { headers: { cookie: 'sid=123' }}).headers.should.not.have_property 'set-cookie'
end
end
describe 'session Store.Memory'
before_each
memory = new (require('express/plugins/session').Store.Memory)
end
it 'should persist'
post('/login', function(){
return this.session.name = 'tj'
})
get('/login', function(){
return this.session.name
})
var headers = { headers: { cookie: 'sid=123' }}
post('/login', headers)
get('/login', headers).status.should.eql 200
get('/login', headers).body.should.eql 'tj'
end
describe '#toString()'
it 'should return [Memory Store]'
memory.toString().should.eql '[Memory Store]'
end
end
describe '#fetch()'
describe 'when the session does not exist'
it 'should return a new Session'
memory.fetch('1').should.have_property 'lastAccess'
end
end
describe 'when the session does exist'
it 'should return the previous session'
memory.commit({ id: '1', same: true })
memory.fetch('1').should.have_property 'same', true
end
end
end
describe '#clear()'
it 'should remove all sessions'
memory.commit({ id: '1' })
memory.commit({ id: '2' })
memory.clear()
memory.should.not.have_property '1'
memory.should.not.have_property '2'
end
end
describe '#length()'
it 'should return the number of session'
memory.commit({ id: '1' })
memory.commit({ id: '2' })
memory.length().should.eql 2
end
end
describe '#destroy()'
it 'should destroy a single session'
memory.commit({ id: '1' })
memory.commit({ id: '2' })
memory.destroy('1')
memory.store.should.not.have_property '1'
memory.store.should.have_property '2'
end
end
describe '#reap()'
it 'should destroy sessions older than the given age in milliseconds'
memory.commit({ id: '1', lastAccess: Number(new Date) - 300 })
memory.commit({ id: '2', lastAccess: Number(new Date) - 250 })
memory.commit({ id: '3', lastAccess: Number(new Date) - 100 })
memory.commit({ id: '4', lastAccess: Number(new Date) })
memory.reap(200)
memory.store.should.not.have_property '1'
memory.store.should.not.have_property '2'
memory.store.should.have_property '3'
memory.store.should.have_property '4'
end
end
end
end
end

View File

@@ -174,7 +174,7 @@ describe 'Express'
it 'should work with a query string'
get('/user', function(){
return this.param('page') || 'First page'
return String(this.param('page') || 'First page')
})
get('/user').body.should.eql 'First page'
get('/user?page=2').body.should.eql '2'

138
spec/spec.utils.js Normal file
View File

@@ -0,0 +1,138 @@
describe 'Express'
before
utils = require('express/utils')
end
describe 'toArray()'
describe 'when given an array'
it 'should return the array'
utils.toArray([1,2,3]).should.eql [1,2,3]
end
end
describe 'when given an object with indexed values and length'
it 'should return an array'
var args = -{ return arguments }('foo', 'bar')
utils.toArray(args).should.eql ['foo', 'bar']
end
end
end
describe 'escape()'
it 'should escape html'
utils.escape('<p>this & that').should.eql '&lt;p&gt;this &amp; that'
end
end
describe 'uid()'
it 'should return a string of random characters'
utils.uid().should.not.eql utils.uid()
utils.uid().length.should.be_greater_than 20
end
end
describe 'extname()'
it 'should return the a files extension'
utils.extname('image.png').should.eql 'png'
utils.extname('image.large.png').should.eql 'png'
utils.extname('/path/to/image.large.png').should.eql 'png'
end
it 'should return null when not found'
utils.extname('path').should.be_null
utils.extname('/just/a/path').should.be_null
end
end
describe 'basename()'
it 'should return a files basename'
utils.basename('foo/bar/baz.image.png').should.eql 'baz.image.png'
end
end
describe 'mergeParam()'
describe 'with empty params'
it 'should merge the given key and value'
params = {}
utils.mergeParam('user[names][first]', 'tj', params)
params.user.names.first.should.eql 'tj'
end
end
describe 'with populated params'
it 'should merge not overwrite'
params = { user: { name: 'tj' }}
utils.mergeParam('user[email]', 'tj@vision-media.ca', params)
params.user.name.should.eql 'tj'
params.user.email.should.eql 'tj@vision-media.ca'
end
end
describe 'with an object as value'
it 'should preserve it'
params = {}
utils.mergeParam('images[]', { name: 1 }, params)
utils.mergeParam('images[]', { name: 2 }, params)
params.images.should.eql [{ name: 1}, { name: 2 }]
end
end
describe 'key[number]'
it 'should merge correctly'
params = { images: { one: 'foo.png' }}
utils.mergeParam('images[0]', 'bar.png', params)
params.images.one.should.eql 'foo.png'
params.images[0].should.eql 'bar.png'
end
end
describe 'key[]'
describe 'with a single value'
it 'should still be an array'
params = {}
utils.mergeParam('images[]', '1', params)
params.images.should.eql ['1']
end
end
describe 'with empty params'
it 'should merge correctly'
params = {}
utils.mergeParam('images[]', '1', params)
utils.mergeParam('images[]', '2', params)
params.images.should.eql ['1', '2']
end
end
describe 'with populated params'
it 'should convert to an array'
params = { images: 'foo.png'}
utils.mergeParam('images[]', '1', params)
params.images.should.eql ['foo.png', '1']
end
end
describe 'with several merges'
it 'should push values'
params = {}
utils.mergeParam('images[]', '1', params)
utils.mergeParam('images[]', '2', params)
utils.mergeParam('images[]', '3', params)
params.images.should.eql ['1', '2', '3']
end
end
describe 'when nested'
it 'should marge correctly'
params = {}
utils.mergeParam('user[tj][images][]', '1', params)
utils.mergeParam('user[tj][images][]', '2', params)
utils.mergeParam('user[tj][images][]', '3', params)
params.user.should.eql { tj: { images: ['1', '2', '3'] }}
end
end
end
end
end