├── .gitignore ├── Cakefile ├── README.md ├── bin └── ace ├── examples ├── app.coffee ├── package.json ├── public │ ├── application.css │ └── test.html ├── spine.coffee └── views │ ├── layout.eco │ ├── posts │ ├── edit.eco │ ├── index.eco │ ├── new.eco │ └── show.eco │ └── user.eco ├── generators ├── app.coffee ├── layout.eco └── package.json ├── lib ├── app.js ├── context.js ├── ext.js ├── fibers.js ├── filter.js ├── format.js ├── helpers.js ├── index.js ├── static.js ├── templates.js └── templates │ ├── coffee.js │ ├── eco.js │ ├── ejs.js │ ├── json.js │ ├── less.js │ ├── mustache.js │ └── stylus.js ├── package.json └── src ├── app.coffee ├── context.coffee ├── ext.coffee ├── fibers.coffee ├── filter.coffee ├── format.coffee ├── helpers.coffee ├── index.coffee ├── static.coffee ├── templates.coffee └── templates ├── coffee.coffee ├── eco.coffee ├── ejs.coffee ├── json.coffee ├── less.coffee ├── mustache.coffee └── stylus.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | {print} = require 'util' 2 | {spawn} = require 'child_process' 3 | 4 | task 'build', 'Build lib/ from src/', -> 5 | coffee = spawn 'coffee', ['-c', '-o', 'lib', 'src'] 6 | coffee.stderr.on 'data', (data) -> 7 | process.stderr.write data.toString() 8 | coffee.stdout.on 'data', (data) -> 9 | print data.toString() 10 | coffee.on 'exit', (code) -> 11 | callback?() if code is 0 12 | 13 | task 'watch', 'Watch src/ for changes', -> 14 | coffee = spawn 'coffee', ['-w', '-c', '-o', 'lib', 'src'] 15 | coffee.stderr.on 'data', (data) -> 16 | process.stderr.write data.toString() 17 | coffee.stdout.on 'data', (data) -> 18 | print data.toString() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ace is [Sinatra](http://www.sinatrarb.com/) for Node; a simple web-server written in CoffeeScript with a straightforward API. 2 | 3 | Every request is wrapped in a [Node Fiber](https://github.com/laverdet/node-fibers), allowing you to program in a synchronous manner without callbacks, but with all the advantages of an asynchronous web-server. 4 | 5 | app.put '/posts/:id', -> 6 | @post = Post.find(+@route.id).wait() 7 | @post.updateAttributes( 8 | name: @params.name, 9 | body: @params.body 10 | ).wait() 11 | @redirect @post 12 | 13 | Ace is built on top of the rock solid [Strata HTTP framework](http://stratajs.org/). 14 | 15 | ##Examples 16 | 17 | You can find an example blog app, including authentication and updating posts, in Ace's [examples directory](https://github.com/maccman/ace/tree/master/examples). 18 | 19 | ##Usage 20 | 21 | Node >= v0.7.3 is required, as well as npm. Ace will run on older versions of Node, but will crash under heavy load due to a bug in V8 (now fixed). 22 | 23 | To install, run: 24 | 25 | npm install -g git://github.com/maccman/ace.git 26 | 27 | 28 | 29 | To generate a new app, run: 30 | 31 | ace new myapp 32 | cd myapp 33 | npm install . 34 | 35 | To serve up an app, run: 36 | 37 | ace 38 | 39 | ##Routing 40 | 41 | In Ace, a route is a HTTP method paired with a URL matching pattern. For example: 42 | 43 | app.get '/users', -> 44 | 'Hello World' 45 | 46 | Anything returned from a routing callback is set as the response body. 47 | 48 | You can also specify a routing pattern, which is available in the callback under the `@route` object. 49 | 50 | app.get '/users/:name', -> 51 | "Hello #{@route.name}" 52 | 53 | POST, PUT and DELETE callbacks are also available, using the `post`, `put` and `del` methods respectively: 54 | 55 | app.post '/users', -> 56 | @user = User.create( 57 | name: @params.name 58 | ).wait() 59 | @redirect "/users/#{@user.id}" 60 | 61 | app.put '/users/:id', -> 62 | @user = User.find(+@route.id).wait() 63 | @user.updateAttributes( 64 | name: @params.name 65 | ).wait() 66 | @redirect "/users/#{@user.id}" 67 | 68 | app.del '/user/:id', -> 69 | @user = User.find(+@route.id).wait() 70 | @user.destroy().wait() 71 | @redirect "/users" 72 | 73 | ##Parameters 74 | 75 | URL encoded forms, multipart uploads and JSON parameters are available via the `@params` object: 76 | 77 | app.post '/posts', -> 78 | @post = Post.create( 79 | name: @params.name, 80 | body: @params.body 81 | ).wait() 82 | 83 | @redirect "/posts/#{@post.id}" 84 | 85 | ##Request 86 | 87 | You can access request information using the `@request` object. 88 | 89 | app.get '/request', -> 90 | result = 91 | protocol: @request.protocol 92 | method: @request.method 93 | remoteAddr: @request.remoteAddr 94 | pathInfo: @request.pathInfo 95 | contentType: @request.contentType 96 | xhr: @request.xhr 97 | host: @request.host 98 | 99 | @json result 100 | 101 | For more information, see [request.js](https://github.com/mjijackson/strata/blob/master/lib/request.js). 102 | 103 | You can access the full request body via `@body`: 104 | 105 | app.get '/body', -> 106 | "You sent: #{@body}" 107 | 108 | You can check to see what the request accepts in response: 109 | 110 | app.get '/users', -> 111 | @users = User.all().wait() 112 | 113 | if @accepts('application/json') 114 | @jsonp @users 115 | else 116 | @eco 'users/list' 117 | 118 | You can also look at the request format (calculated from the URL's extension). This can often give a better indication of what clients are expecting in response. 119 | 120 | app.get '/users', -> 121 | @users = User.all().wait() 122 | 123 | if @format is 'application/json' 124 | @jsonp @users 125 | else 126 | @eco 'users/list' 127 | 128 | Finally you can access the raw `@env` object: 129 | 130 | @env['Warning'] 131 | 132 | ##Responses 133 | 134 | As well as returning the response body as a string from the routing callback, you can set the response attributes directly: 135 | 136 | app.get '/render', -> 137 | @headers['Transfer-Encoding'] = 'chunked' 138 | @contentType = 'text/html' 139 | @status = 200 140 | @body = 'my body' 141 | 142 | You can set the `@headers`, `@status` and `@body` attributes to alter the request's response. 143 | 144 | If you only need to set the status code, you can just return it directly from the routing callback. The properties `@ok`, `@unauthorized` and `@notFound` are aliased to their relevant status codes. 145 | 146 | app.get '/render', -> 147 | # ... 148 | @ok 149 | 150 | ##Static 151 | 152 | By default, if a folder called `public` exists under the app root, its contents will be served up statically. You can configure the path of this folder like so: 153 | 154 | app.set public: './public' 155 | 156 | You can add static assets like stylesheets and images to the `public` folder. 157 | 158 | ##Templates 159 | 160 | Ace includes support for rendering CoffeeScript, Eco, EJS, Less, Mustache and Stylus templates. Simply install the relevant module and the templates will be available to render. 161 | 162 | For example, install the [eco](https://github.com/sstephenson/eco) module and the `@eco` function will be available to you. 163 | 164 | app.get '/users/:name', -> 165 | @name = @route.name 166 | @eco 'user/show' 167 | 168 | The `@eco` function takes a path of the Eco template. By default, this should be located under a folder named `./views`. 169 | The template is rendered in the current context, so you can pass variables to them by setting them locally. 170 | 171 | If a file exists under `./views/layout.*`, then it'll be used as the application's default layout. You can specify a different layout with the `layout` option. 172 | 173 | app.get '/users', -> 174 | @users = User.all().wait() 175 | @mustache 'user/list', layout: 'basic' 176 | 177 | ##JSON 178 | 179 | You can serve up JSON and JSONP with the `@json` and `@jsonp` helpers respectively. 180 | 181 | app.get '/users', -> 182 | @json {status: 'ok'} 183 | 184 | app.get '/users', -> 185 | @users = User.all().wait() 186 | @jsonp @users 187 | 188 | By default `@jsonp` uses the `@params.callback` parameter as the name of its wrapper function. 189 | 190 | ##Fibers 191 | 192 | Every request in Ace is wrapped in a Fiber. This means you can do away with the callback spaghetti that Node applications often descend it. Rather than use callbacks, you can simply pause the current fiber. When the callback returns, the fibers execution continues from where it left off. 193 | 194 | In practice, Ace provides a couple of utility functions for pausing asynchronous functions. Ace adds a `wait()` function to `EventEmitter`. This transforms asynchronous calls on libraries such as [Sequelize](http://sequelizejs.com). 195 | 196 | For example, `save()` is usually an asynchronous call which requires a callback. Here we can just call `save().wait()` and use a synchronous style. 197 | 198 | app.get '/projects', -> 199 | project = Project.build( 200 | name: @params.name 201 | ) 202 | 203 | project.save().wait() 204 | 205 | @sleep(2000) 206 | 207 | "Saved project: #{project.id}" 208 | 209 | This fiber technique also means we can implement functionality like `sleep()` in JavaScript, as in the previous example. 210 | 211 | You can make an existing asynchronous function fiber enabled, by wrapping it with `Function::wait()`. 212 | 213 | syncExists = fs.exists.bind(fs).wait 214 | 215 | if syncExists('./path/to/file') 216 | @sendFile('./path/to/file) 217 | 218 | Fibers are pooled, and by default there's a limit of 100 fibers in the pool. This means that you can serve up to 100 connections simultaneously. After the pool limit is reached, requests are queued. You can increase the pool size like so: 219 | 220 | app.pool.size = 250 221 | 222 | ##Cookies & Session 223 | 224 | Sessions are enabled by default in Ace. You can set and retrieve data stored in the session by using the `@session` object: 225 | 226 | app.get '/login', -> 227 | user = User.find(email: @params.email).wait() 228 | @session.user_id = user.id 229 | @redirect '/' 230 | 231 | You can retrieve cookies via the `@cookie` object, and set them with `@response.setCookie(name, value)`; 232 | 233 | app.get '/login', -> 234 | token = @cookies.rememberMe 235 | # ... 236 | 237 | ##Filters 238 | 239 | Ace supports 'before' filters, callbacks that are executed before route handlers. 240 | 241 | app.before -> 242 | # Before filter 243 | 244 | By default before filters are always executed. You can specify conditions to limit that, such as routes. 245 | 246 | app.before '/users*', -> 247 | 248 | The previous filter will be executed before any routes matching `/users*` are. 249 | 250 | As well as a route, you can specify a object to match the request against: 251 | 252 | app.before method: 'POST', -> 253 | ensureLogin() 254 | 255 | Finally you can specify a conditional function that'll be passed the request's `env`, and should return a boolean indicating whether the filter should be executed or not. 256 | 257 | app.before conditionFunction, -> 258 | 259 | If a filter changes the response status to anything other than 200, then execution will halt. 260 | 261 | app.before -> 262 | if @request.method isnt 'GET' and !@session.user 263 | @head 401 264 | 265 | ##Context 266 | 267 | You can add extra properties to the routing callback context using `context.include()`: 268 | 269 | app.context.include 270 | loggedIn: -> !!@session.user_id 271 | 272 | app.before '/admin*', -> 273 | if @loggedIn() 274 | @ok 275 | else 276 | @redirect '/login' 277 | 278 | The context includes a few utilities methods by default: 279 | 280 | @redirect(url) 281 | @sendFile(path) 282 | @head(status) 283 | 284 | @basicAuth (user, pass) -> 285 | user is 'foo' and pass is 'bar' 286 | 287 | ##Configuration 288 | 289 | Ace includes some sensible default settings, but you can override them using `@set`, passing in an object of names to values. 290 | 291 | @app.set static: true # Serve up file statically from public 292 | sessions: true # Enable sessions 293 | port: 1982 # Server port number 294 | bind: '0.0.0.0' # Bind to host 295 | views: './views' # Path to 'views' dir 296 | public: './public' # Path to 'public' dir 297 | layout: 'layout' # Name of application's default layout 298 | logging: true # Enable logging 299 | 300 | Settings are available on the `@settings` object: 301 | 302 | if app.settings.logging is true 303 | console.log('Logging is enabled') 304 | 305 | ##Middleware 306 | 307 | Middleware sits on the request stack, and gets executed before any of the routes. Using middleware, you can alter the request object such as HTTP headers or the request's path. 308 | 309 | Ace sits on top of [Strata](http://stratajs.org/), so you can use any of the middleware that comes with the framework, or create your own. 310 | 311 | For example, we can use Ace's [methodOverride](https://github.com/mjijackson/strata/blob/master/lib/methodoverride.js) middleware, enabling us to override the request's HTTP method with a `_method` parameter. 312 | 313 | strata = require('ace').strata 314 | app.use strata.methodOverride 315 | 316 | This means we can use HTML forms to send requests other than `GET` or `POST` ones, keeping our application RESTful: 317 | 318 |
319 | 320 | 321 |
322 | 323 | For more information on creating your own middleware, see [Strata's docs](http://stratajs.org/manual/5). 324 | 325 | ##Credits 326 | 327 | Ace was built by [Alex MacCaw](http://alexmaccaw.com) and [Michael Jackson](http://mjijackson.com/). -------------------------------------------------------------------------------- /bin/ace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'), 4 | fs = require('fs'), 5 | App = require('../lib/index').App, 6 | optimist = require('optimist'); 7 | 8 | // Use local ace module if available 9 | if (fs.existsSync('./node_modules/.bin/ace') && !global.local) { 10 | global.local = true; 11 | return require('module')._load('./node_modules/.bin/ace'); 12 | } 13 | 14 | try { 15 | require('coffee-script'); 16 | } catch(e) { } 17 | 18 | var copy = function(from, to){ 19 | var data = fs.readFileSync(from, 'utf8'); 20 | fs.writeFileSync(to, data, 'utf8'); 21 | }; 22 | 23 | var argv = optimist.usage([ 24 | ' usage: ace COMMAND [PATH]', 25 | ' new generate a new application', 26 | ' server start the server', 27 | ].join("\n")) 28 | .alias('p', 'port') 29 | .argv; 30 | 31 | var command = argv._[0]; 32 | var filename = argv._[1]; 33 | 34 | if (filename) { 35 | filename = path.resolve(filename); 36 | } 37 | 38 | if (command === 'new' && filename) { 39 | fs.mkdirSync(filename, 0775); 40 | fs.mkdirSync(filename + '/public', 0775); 41 | fs.mkdirSync(filename + '/views', 0775); 42 | 43 | var generatorPath = path.join( 44 | __dirname, '..', 'generators' 45 | ); 46 | 47 | copy(path.join(generatorPath, 'package.json'), 48 | path.join(filename, 'package.json')); 49 | copy(path.join(generatorPath, 'app.coffee'), 50 | path.join(filename, 'app.coffee')); 51 | copy(path.join(generatorPath, 'layout.eco'), 52 | path.join(filename, 'views', 'layout.eco')); 53 | console.log('Generated: ' + filename); 54 | return; 55 | } 56 | 57 | if ( !filename ) { 58 | try { 59 | filename = require.resolve(path.resolve('app')); 60 | } catch (e) {} 61 | } 62 | 63 | if ( !filename ) { 64 | try { 65 | filename = require.resolve(path.resolve('index')); 66 | } catch (e) {} 67 | } 68 | 69 | if ( !filename ) { 70 | console.log("Ace usage:"); 71 | console.log("\tace new PATH"); 72 | console.log("\tace server PATH"); 73 | return; 74 | } 75 | 76 | process.chdir(path.dirname(filename)); 77 | 78 | var app = new App; 79 | global.app = app; 80 | require(filename); 81 | app.serve({ 82 | port: process.env.PORT || argv.port 83 | }); -------------------------------------------------------------------------------- /examples/app.coffee: -------------------------------------------------------------------------------- 1 | Sequelize = require('sequelize') 2 | strata = require('ace').strata 3 | 4 | sequelize = new Sequelize('mydb', 'root') 5 | Post = sequelize.define('Post', { 6 | name: Sequelize.STRING, 7 | body: Sequelize.TEXT 8 | }, 9 | classMethods: 10 | url: -> '/posts' 11 | instanceMethods: 12 | url: -> '/posts/' + @id 13 | toJSON: -> @values 14 | ) 15 | 16 | Post.sync() 17 | 18 | app.use strata.methodOverride 19 | 20 | app.set credentials: {dragon: 'slayer'} 21 | 22 | app.before -> 23 | if @request.method isnt 'GET' and !@session.user 24 | @redirect '/login' 25 | 26 | app.get '/login', -> 27 | success = @basicAuth (user, pass) -> 28 | @settings.credentials[user] is pass and user 29 | 30 | if success 31 | @session.user = success 32 | @redirect '/' 33 | 34 | app.get '/logout', -> 35 | @session = {} 36 | @redirect '/' 37 | 38 | app.get '/posts', -> 39 | @posts = Post.all().wait() 40 | if @acceptsJSON 41 | @jsonp @posts 42 | else 43 | @eco 'posts/index' 44 | 45 | app.get '/posts/new', -> 46 | @eco 'posts/new' 47 | 48 | app.get '/posts/:id', -> 49 | @post = Post.find(+@route.id).wait() 50 | @eco 'posts/show' 51 | 52 | app.get '/posts/:id/edit', -> 53 | @post = Post.find(+@route.id).wait() 54 | @eco 'posts/edit' 55 | 56 | app.post '/posts', -> 57 | @post = Post.create( 58 | name: @params.name, 59 | body: @params.body 60 | ).wait() 61 | @redirect @post 62 | 63 | app.put '/posts/:id', -> 64 | @post = Post.find(+@route.id).wait() 65 | @post.updateAttributes( 66 | name: @params.name, 67 | body: @params.body 68 | ).wait() 69 | @redirect @post 70 | 71 | app.del '/posts/:id', -> 72 | @post = Post.find(+@route.id).wait() 73 | @post.destroy().wait() 74 | @redirect Post 75 | 76 | app.root '/posts' -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.0.1", 4 | "author": "maccman", 5 | "dependencies": { 6 | "ace": "git://github.com/maccman/ace.git#master", 7 | "coffee-script": "latest", 8 | "eco": "latest", 9 | "sequelize": "latest" 10 | } 11 | } -------------------------------------------------------------------------------- /examples/public/application.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | height: 100%; 4 | width: 100%; 5 | margin: 0; 6 | padding: 0; 7 | overflow: auto; 8 | } 9 | 10 | body { 11 | color: #52585d; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | font: 15px "helvetica neue", helvetica, arial, sans-serif; 15 | background: #f4f4f4; 16 | } 17 | 18 | p { 19 | margin: 0 0 10px 0; 20 | line-height: 1.5em; 21 | } 22 | 23 | a { 24 | color: #2d81c5; 25 | text-shadow: 0 1px 0 #fff; 26 | text-decoration: none; 27 | cursor: pointer; 28 | } 29 | 30 | ::selection { 31 | background: #e0edf8; 32 | text-shadow: none; 33 | } 34 | 35 | hr { 36 | border: 1px solid #dadada; 37 | border-width: 1px 0 0 0; 38 | margin: 10px 0 20px 0; 39 | } 40 | 41 | button, 42 | a.cta { 43 | line-height: 1em; 44 | display: inline-block; 45 | font-size: 13px; 46 | padding: 4px 8px; 47 | border: 1px solid rgba(0,0,0,0.10); 48 | -moz-border-radius: 5px; 49 | -webkit-border-radius: 5px; 50 | border-radius: 5px; 51 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,0.40); 52 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,0.40); 53 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,0.40); 54 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,0.40); 55 | box-shadow: inset 0 1px 0 rgba(255,255,255,0.40); 56 | text-shadow: 0 1px 0 #fff; 57 | color: #464b4f; 58 | font-family: 'Lucida Grande'; 59 | background: #f5f5f5; 60 | background: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 61 | background: -moz-linear-gradient(top, #f5f5f5, #e8e8e8); 62 | background: linear-gradient(top, #f5f5f5, #e8e8e8); 63 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,0.50)), color-stop(0.5, rgba(255,255,255,0.50)), color-stop(0.5, rgba(255,255,255,0.00))), #e8e8e8; 64 | -webkit-box-shadow: inset 0 1px 0 #fff, 0 1px 0 rgba(0,0,0,0.30); 65 | } 66 | 67 | 68 | button.default, 69 | a.cta.default { 70 | border-color: rgba(104,189,244,0.80); 71 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,0.40), 0 1px 5px 0 rgba(104,189,244,0.60); 72 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,0.40), 0 1px 5px 0 rgba(104,189,244,0.60); 73 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,0.40), 0 1px 5px 0 rgba(104,189,244,0.60); 74 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,0.40), 0 1px 5px 0 rgba(104,189,244,0.60); 75 | box-shadow: inset 0 1px 0 rgba(255,255,255,0.40), 0 1px 5px 0 rgba(104,189,244,0.60); 76 | } 77 | 78 | button:active, 79 | a.cta:active { 80 | border-color: rgba(0,0,0,0.30); 81 | -webkit-box-shadow: inset 0 3px 15px rgba(0,0,0,0.30), 0 1px 1px rgba(0,0,0,0.10); 82 | -moz-box-shadow: inset 0 3px 15px rgba(0,0,0,0.30), 0 1px 1px rgba(0,0,0,0.10); 83 | -moz-box-shadow: inset 0 3px 15px rgba(0,0,0,0.30), 0 1px 1px rgba(0,0,0,0.10); 84 | -webkit-box-shadow: inset 0 3px 15px rgba(0,0,0,0.30), 0 1px 1px rgba(0,0,0,0.10); 85 | box-shadow: inset 0 3px 15px rgba(0,0,0,0.30), 0 1px 1px rgba(0,0,0,0.10); 86 | } 87 | 88 | input[type=text], 89 | input[type=url], 90 | textarea { 91 | padding: 3px; 92 | margin: 0; 93 | border: 1px solid rgba(0,0,0,0.25); 94 | -moz-box-shadow: inset 0 1px 2px rgba(0,0,0,0.20); 95 | -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,0.20); 96 | -moz-box-shadow: inset 0 1px 2px rgba(0,0,0,0.20); 97 | -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,0.20); 98 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.20); 99 | } 100 | 101 | input[type=text]:focus, 102 | input[type=url]:focus, 103 | textarea:focus, 104 | select:focus { 105 | outline: none; 106 | border-color: rgba(104,189,244,0.80); 107 | -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,0.20), 0 1px 5px 0 rgba(104,189,244,0.60); 108 | -moz-box-shadow: inset 0 1px 2px rgba(0,0,0,0.20), 0 1px 5px 0 rgba(104,189,244,0.60); 109 | -moz-box-shadow: inset 0 1px 2px rgba(0,0,0,0.20), 0 1px 5px 0 rgba(104,189,244,0.60); 110 | -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,0.20), 0 1px 5px 0 rgba(104,189,244,0.60); 111 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.20), 0 1px 5px 0 rgba(104,189,244,0.60); 112 | } 113 | 114 | textarea { 115 | padding: 5px; 116 | height: 80px; 117 | } 118 | 119 | .right { 120 | float: right; 121 | } 122 | 123 | .left { 124 | float: left; 125 | } 126 | 127 | #app { 128 | background: #fff; 129 | width: 420px; 130 | margin: 40px auto; 131 | -moz-box-shadow: 0 1px 4px rgba(0,0,0,0.40); 132 | -webkit-box-shadow: 0 1px 4px rgba(0,0,0,0.40); 133 | box-shadow: 0 1px 4px rgba(0,0,0,0.40); 134 | -moz-border-radius: 3px; 135 | -webkit-border-radius: 3px; 136 | border-radius: 3px; 137 | background: #000; 138 | background: -webkit-gradient(linear, left top, left bottom, from(#fff), color-stop(0.9, #fff), to(#f6f6f6)); 139 | overflow: hidden; 140 | } 141 | 142 | #app header { 143 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,0.70); 144 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,0.70); 145 | -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,0.70); 146 | -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,0.70); 147 | box-shadow: inset 0 1px 0 rgba(255,255,255,0.70); 148 | background: whiteSmoke; 149 | background: -webkit-gradient(linear, left top, left bottom, from(whiteSmoke), to(#E8E8E8)); 150 | background: -moz-linear-gradient(top, whiteSmoke, #E8E8E8); 151 | background: linear-gradient(top, whiteSmoke, #E8E8E8); 152 | border-bottom: 1px solid #D1D1D1; 153 | height: 35px; 154 | line-height: 35px; 155 | padding: 0 10px; 156 | -webkit-border-radius: 3px 3px 0 0; 157 | } 158 | 159 | #app header h1 { 160 | margin: 0; 161 | font: 20px arial, lucida, helvetica, arial, sans-serif; 162 | font-weight: normal; 163 | text-shadow: 0 1px 0 white; 164 | float: left; 165 | line-height: 36px; 166 | } 167 | 168 | #app article { 169 | padding: 10px; 170 | } 171 | 172 | #app label { 173 | display: block; 174 | margin-bottom: 10px; 175 | } 176 | 177 | #app label span { 178 | display: block; 179 | margin-bottom: 3px; 180 | } -------------------------------------------------------------------------------- /examples/public/test.html: -------------------------------------------------------------------------------- 1 |

TEST

2 | -------------------------------------------------------------------------------- /examples/spine.coffee: -------------------------------------------------------------------------------- 1 | package = require('hem/lib/package') 2 | specs = require('hem/lib/specs') 3 | css = require('hem/lib/css') 4 | 5 | appPackage = package.createPackage( 6 | dependencies: [] 7 | paths: ['./app'] 8 | libs: [] 9 | ) 10 | 11 | specsPackage = specs.createPackage('./specs') 12 | cssPackage = css.createPackage('./css') 13 | 14 | app.get '/application.js', -> 15 | @contentType = 'text/javascript' 16 | appPackage.compile(@settings.minify) 17 | 18 | app.get '/specs.js', -> 19 | @contentType = 'text/javascript' 20 | specsPackage.compile(@settings.minify) 21 | 22 | app.get '/application.css', -> 23 | @stylus 'css/application' 24 | 25 | # app.get '/users', -> 26 | # @users = [] 27 | # @json @users 28 | # 29 | # app.post '/users', -> 30 | # # Create user 31 | # @user = {} 32 | # @json @user 33 | # 34 | # app.put '/users/:id', -> 35 | # @ok 36 | # 37 | # app.del '/users/:id', -> 38 | # @ok -------------------------------------------------------------------------------- /examples/views/layout.eco: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Blog 5 | 6 | 7 | 8 |
9 | <%- @body %> 10 |
11 | 12 | -------------------------------------------------------------------------------- /examples/views/posts/edit.eco: -------------------------------------------------------------------------------- 1 |
2 |

Edit Post

3 |
4 | 5 |
6 |
7 | 8 | 9 | 13 | 14 | 18 | 19 | 20 |
21 |
-------------------------------------------------------------------------------- /examples/views/posts/index.eco: -------------------------------------------------------------------------------- 1 |
2 |

Listing Posts

3 |
4 | 5 |
6 | 13 | 14 | <% if @session.user: %> 15 | New Post 16 | <% else: %> 17 | Login 18 | <% end %> 19 |
-------------------------------------------------------------------------------- /examples/views/posts/new.eco: -------------------------------------------------------------------------------- 1 |
2 |

New Post

3 |
4 | 5 |
6 |
7 | 11 | 12 | 16 | 17 | 18 |
19 |
-------------------------------------------------------------------------------- /examples/views/posts/show.eco: -------------------------------------------------------------------------------- 1 |
2 |

<%= @post.name %>

3 |
4 | 5 |
6 |

<%= @post.body %>

7 | 8 | Back 9 | 10 | <% if @session.user: %> 11 | 12 | Edit 13 | 14 |
15 | 16 | 17 |
18 | 19 | <% end %> 20 |
-------------------------------------------------------------------------------- /examples/views/user.eco: -------------------------------------------------------------------------------- 1 |

Hi <%= @name %>

-------------------------------------------------------------------------------- /generators/app.coffee: -------------------------------------------------------------------------------- 1 | app.get '/', -> 2 | 'Hello World!' -------------------------------------------------------------------------------- /generators/layout.eco: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Application 5 | 6 | 7 |
8 | <%- @body %> 9 |
10 | 11 | -------------------------------------------------------------------------------- /generators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.0.1", 4 | "author": "maccman", 5 | "dependencies": { 6 | "ace": "git://github.com/maccman/ace.git#master", 7 | "coffee-script": "latest", 8 | "eco": "latest" 9 | } 10 | } -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var App, context, fibers, filter, format, fs, method, methods, path, staticFiles, strata, templates, type, 4 | __hasProp = {}.hasOwnProperty, 5 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, 6 | __slice = [].slice; 7 | 8 | fs = require('fs'); 9 | 10 | path = require('path'); 11 | 12 | strata = require('strata'); 13 | 14 | fibers = require('./fibers'); 15 | 16 | context = require('./context'); 17 | 18 | filter = require('./filter'); 19 | 20 | templates = require('./templates'); 21 | 22 | format = require('./format'); 23 | 24 | staticFiles = require('./static'); 25 | 26 | App = (function(_super) { 27 | __extends(App, _super); 28 | 29 | App.prototype.defaults = { 30 | "static": true, 31 | sessions: true, 32 | port: 1982, 33 | bind: '0.0.0.0', 34 | root: process.cwd(), 35 | views: './views', 36 | assets: './assets', 37 | "public": './public', 38 | layout: 'layout', 39 | logging: true 40 | }; 41 | 42 | App.prototype.context = context; 43 | 44 | App.prototype.resolve = templates.resolve; 45 | 46 | function App() { 47 | App.__super__.constructor.call(this); 48 | this.settings = {}; 49 | this.set(this.defaults); 50 | this.settings.layout = this.resolve(this.settings.layout, false); 51 | if (!fs.existsSync(this.settings["public"])) { 52 | this.settings["static"] = false; 53 | } 54 | this.pool = new fibers.Pool; 55 | this.router = new strata.Router; 56 | } 57 | 58 | App.prototype.before = function(conditions, callback) { 59 | if (!callback) { 60 | callback = conditions; 61 | conditions = true; 62 | } 63 | this.beforeFilters || (this.beforeFilters = []); 64 | return this.beforeFilters.push([conditions, callback]); 65 | }; 66 | 67 | App.prototype.rewrite = function(pattern, replacement) { 68 | return this.use(strata.rewrite, pattern, replacement); 69 | }; 70 | 71 | App.prototype.root = function(replacement) { 72 | return this.rewrite('/', replacement); 73 | }; 74 | 75 | App.prototype.route = function(pattern, app, methods) { 76 | return this.router.route(pattern, context.wrap(app, this), methods); 77 | }; 78 | 79 | App.prototype.set = function(key, value) { 80 | var k, v, _results; 81 | 82 | if (typeof key === 'object') { 83 | _results = []; 84 | for (k in key) { 85 | v = key[k]; 86 | _results.push(this.set(k, v)); 87 | } 88 | return _results; 89 | } else { 90 | return this.settings[key] = value; 91 | } 92 | }; 93 | 94 | App.prototype.toApp = function() { 95 | var options, 96 | _this = this; 97 | 98 | this.use(strata.contentType, 'text/html'); 99 | this.use(strata.contentLength); 100 | if (this.settings.logging) { 101 | this.use(strata.commonLogger); 102 | } 103 | if (options = this.settings.sessions) { 104 | if (options === true) { 105 | options = {}; 106 | } 107 | this.use(strata.sessionCookie, options); 108 | } 109 | if (this.settings["static"]) { 110 | this.use(staticFiles, this.settings["public"], ['index.html']); 111 | } 112 | this.use(format); 113 | if (this.beforeFilters) { 114 | this.use(filter, this.beforeFilters, this); 115 | } 116 | this.run(function(env, callback) { 117 | return _this.pool.wrap(_this.router.toApp())(env, callback); 118 | }); 119 | return App.__super__.toApp.apply(this, arguments); 120 | }; 121 | 122 | App.prototype.serve = function(options) { 123 | var key, value; 124 | 125 | if (options == null) { 126 | options = {}; 127 | } 128 | for (key in options) { 129 | value = options[key]; 130 | if (value != null) { 131 | this.settings[key] = value; 132 | } 133 | } 134 | return strata.run(this, this.settings); 135 | }; 136 | 137 | return App; 138 | 139 | })(strata.Builder); 140 | 141 | methods = { 142 | get: ['GET', 'HEAD'], 143 | post: 'POST', 144 | put: 'PUT', 145 | del: 'DELETE', 146 | head: 'HEAD', 147 | options: 'OPTIONS' 148 | }; 149 | 150 | for (type in methods) { 151 | method = methods[type]; 152 | App.prototype[type] = (function(method) { 153 | return function() { 154 | var args; 155 | 156 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 157 | return this.route.apply(this, __slice.call(args).concat([method])); 158 | }; 159 | })(method); 160 | } 161 | 162 | module.exports = App; 163 | 164 | }).call(this); 165 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var Context, strata; 4 | 5 | strata = require('strata'); 6 | 7 | Context = (function() { 8 | Context.include = function(obj) { 9 | var key, value, _results; 10 | 11 | _results = []; 12 | for (key in obj) { 13 | value = obj[key]; 14 | _results.push(this.prototype[key] = value); 15 | } 16 | return _results; 17 | }; 18 | 19 | Context.wrap = function(app, base) { 20 | return function(env, callback) { 21 | var context, err, result; 22 | 23 | context = new Context(env, callback, base); 24 | try { 25 | result = app.call(context, env, callback); 26 | return context.send(result); 27 | } catch (_error) { 28 | err = _error; 29 | return strata.handleError(err, env, callback); 30 | } 31 | }; 32 | }; 33 | 34 | function Context(env, callback, app) { 35 | this.env = env; 36 | this.callback = callback; 37 | this.app = app != null ? app : {}; 38 | this.request = new strata.Request(this.env); 39 | this.response = new strata.Response; 40 | } 41 | 42 | Context.prototype.send = function(result) { 43 | if (result === false) { 44 | return false; 45 | } 46 | if (this.served) { 47 | return false; 48 | } 49 | this.served = true; 50 | if (Array.isArray(result)) { 51 | this.response.status = result[0]; 52 | this.response.headers = result[1]; 53 | this.response.body = result[3]; 54 | } else if (typeof result === 'integer') { 55 | this.response.status = result; 56 | } else if (result instanceof strata.Response) { 57 | this.response = result; 58 | } else if (typeof result === 'function') { 59 | this.response.body = result; 60 | } else if (typeof result === 'string') { 61 | this.response.body = result; 62 | } 63 | return this.callback(this.response.status, this.response.headers, this.response.body); 64 | }; 65 | 66 | Context.prototype.setter = Context.prototype.__defineSetter__; 67 | 68 | Context.prototype.getter = Context.prototype.__defineGetter__; 69 | 70 | Context.prototype.getter('cookies', function() { 71 | return this.request.cookies.bind(this.request).wait(); 72 | }); 73 | 74 | Context.prototype.getter('params', function() { 75 | return this.request.params.bind(this.request).wait(); 76 | }); 77 | 78 | Context.prototype.getter('query', function() { 79 | return this.request.query.bind(this.request).wait(); 80 | }); 81 | 82 | Context.prototype.getter('body', function() { 83 | return this.request.body.bind(this.request).wait(); 84 | }); 85 | 86 | Context.prototype.getter('route', function() { 87 | return this.env.route; 88 | }); 89 | 90 | Context.prototype.getter('settings', function() { 91 | return this.app.settings; 92 | }); 93 | 94 | Context.prototype.getter('session', function() { 95 | var _base; 96 | 97 | return (_base = this.env).session || (_base.session = {}); 98 | }); 99 | 100 | Context.prototype.setter('session', function(value) { 101 | return this.env.session = value; 102 | }); 103 | 104 | Context.prototype.getter('status', function() { 105 | return this.response.status; 106 | }); 107 | 108 | Context.prototype.setter('status', function(value) { 109 | return this.response.status = value; 110 | }); 111 | 112 | Context.prototype.getter('headers', function() { 113 | return this.response.headers; 114 | }); 115 | 116 | Context.prototype.setter('headers', function(value) { 117 | return this.response.headers = value; 118 | }); 119 | 120 | Context.prototype.setter('contentType', function(value) { 121 | return this.response.headers['Content-Type'] = value; 122 | }); 123 | 124 | Context.prototype.setter('body', function(value) { 125 | return this.response.body = value; 126 | }); 127 | 128 | Context.prototype.accepts = function(type) { 129 | return this.request.accepts(type); 130 | }; 131 | 132 | Context.prototype.getter('format', function() { 133 | return this.env.format; 134 | }); 135 | 136 | Context.prototype.getter('acceptsJSON', function() { 137 | var accept, mime; 138 | 139 | mime = 'application/json'; 140 | if (this.env.format === mime) { 141 | return true; 142 | } 143 | accept = this.request.accept || ''; 144 | return accept.indexOf(mime) !== -1; 145 | }); 146 | 147 | return Context; 148 | 149 | })(); 150 | 151 | module.exports = Context; 152 | 153 | }).call(this); 154 | -------------------------------------------------------------------------------- /lib/ext.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var EventEmitter, Fiber, future; 4 | 5 | Fiber = require('fibers'); 6 | 7 | future = require('fibers/future'); 8 | 9 | Function.prototype.wait = function() { 10 | return future.wrap(this).apply(this, arguments).wait(); 11 | }; 12 | 13 | EventEmitter = require('events').EventEmitter; 14 | 15 | EventEmitter.prototype.wait = function(success, failure) { 16 | var fiber; 17 | 18 | if (success == null) { 19 | success = 'success'; 20 | } 21 | if (failure == null) { 22 | failure = 'failure'; 23 | } 24 | fiber = Fiber.current; 25 | this.on(success, function() { 26 | return fiber.run.apply(fiber, arguments); 27 | }); 28 | this.on(failure, function() { 29 | return fiber.throwInto.apply(fiber, arguments); 30 | }); 31 | return Fiber["yield"](); 32 | }; 33 | 34 | }).call(this); 35 | -------------------------------------------------------------------------------- /lib/fibers.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var Fiber, Pool, context, sleep, task; 4 | 5 | Fiber = require('fibers'); 6 | 7 | context = require('./context'); 8 | 9 | task = function(callback) { 10 | return function() { 11 | var args; 12 | 13 | args = arguments; 14 | return Fiber(function() { 15 | return callback.apply(null, args); 16 | }).run(); 17 | }; 18 | }; 19 | 20 | sleep = function(ms) { 21 | var fiber; 22 | 23 | fiber = Fiber.current; 24 | setTimeout(function() { 25 | return fiber.run(); 26 | }, ms); 27 | return Fiber["yield"](); 28 | }; 29 | 30 | Pool = (function() { 31 | function Pool(size) { 32 | if (size == null) { 33 | size = 100; 34 | } 35 | this.queue = []; 36 | this.count = 0; 37 | this.setSize(size); 38 | } 39 | 40 | Pool.prototype.call = function(callback) { 41 | this.queue.push(callback); 42 | if (this.count < this.size) { 43 | this.addFiber(); 44 | } 45 | return this; 46 | }; 47 | 48 | Pool.prototype.wrap = function(callback) { 49 | var _this = this; 50 | 51 | return function() { 52 | var args; 53 | 54 | args = arguments; 55 | return _this.call(function() { 56 | return callback.apply(null, args); 57 | }); 58 | }; 59 | }; 60 | 61 | Pool.prototype.setSize = function(size) { 62 | this._size = size; 63 | if (Fiber.poolSize < this._size) { 64 | return Fiber.poolSize = this._size; 65 | } 66 | }; 67 | 68 | Pool.prototype.__defineGetter__('size', function() { 69 | return this._size; 70 | }); 71 | 72 | Pool.prototype.__defineSetter__('size', Pool.prototype.setSize); 73 | 74 | Pool.prototype.addFiber = function() { 75 | var _this = this; 76 | 77 | return Fiber(function() { 78 | var callback; 79 | 80 | _this.count++; 81 | while (callback = _this.queue.shift()) { 82 | callback(); 83 | } 84 | return _this.count--; 85 | }).run(); 86 | }; 87 | 88 | return Pool; 89 | 90 | })(); 91 | 92 | context.include({ 93 | sleep: sleep 94 | }); 95 | 96 | module.exports = { 97 | task: task, 98 | wrap: task, 99 | sleep: sleep, 100 | Pool: Pool 101 | }; 102 | 103 | }).call(this); 104 | -------------------------------------------------------------------------------- /lib/filter.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var context, passes, passesRoute, strata; 4 | 5 | strata = require('strata'); 6 | 7 | context = require('./context'); 8 | 9 | passesRoute = function(env, route) { 10 | route = strata.Router.compileRoute(route, []); 11 | return route.test(env.pathInfo); 12 | }; 13 | 14 | passes = function(env, conditions) { 15 | var key, request, value; 16 | 17 | if (conditions === true) { 18 | return true; 19 | } else if (typeof conditions === 'function') { 20 | return conditions(env); 21 | } else if (typeof conditions === 'string') { 22 | return passesRoute(env, conditions); 23 | } else if (Array.isArray(conditions)) { 24 | return conditions.some(function(route) { 25 | return passesRoute(env, route); 26 | }); 27 | } else { 28 | request = new strata.Request(env); 29 | for (key in conditions) { 30 | value = conditions[key]; 31 | if (value.test != null) { 32 | if (value.test(request[key])) { 33 | return true; 34 | } 35 | if (value.test(env[key])) { 36 | return true; 37 | } 38 | } else { 39 | if (request[key] === value) { 40 | return true; 41 | } 42 | if (env[key] === value) { 43 | return true; 44 | } 45 | } 46 | } 47 | return false; 48 | } 49 | }; 50 | 51 | module.exports = function(app, filters, base) { 52 | return function(env, callback) { 53 | return app(env, function() { 54 | var conditions, filter, filterCallback, original, proxiedCallback, _i, _len; 55 | 56 | original = arguments; 57 | proxiedCallback = function(status) { 58 | if (this.status === 200) { 59 | return callback.apply(null, original); 60 | } else { 61 | return callback.apply(null, arguments); 62 | } 63 | }; 64 | for (_i = 0, _len = filters.length; _i < _len; _i++) { 65 | filter = filters[_i]; 66 | conditions = filter[0], filterCallback = filter[1]; 67 | if (passes(env, conditions)) { 68 | return context.wrap(filterCallback, base)(env, proxiedCallback); 69 | } 70 | } 71 | return callback.apply(null, original); 72 | }); 73 | }; 74 | }; 75 | 76 | }).call(this); 77 | -------------------------------------------------------------------------------- /lib/format.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var mime, path; 4 | 5 | mime = require('mime'); 6 | 7 | path = require('path'); 8 | 9 | module.exports = function(app, defaultType) { 10 | return function(env, callback) { 11 | var ext, format, pathInfo; 12 | 13 | pathInfo = env.pathInfo; 14 | ext = path.extname(pathInfo); 15 | format = ext ? mime.lookup(ext) : null; 16 | env.format = format; 17 | if (ext) { 18 | env.pathInfo = pathInfo.replace(new RegExp("" + ext + "$"), ''); 19 | } 20 | return app(env, function(status, headers, body) { 21 | env.pathInfo = pathInfo; 22 | return callback(status, headers, body); 23 | }); 24 | }; 25 | }; 26 | 27 | }).call(this); 28 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var basicAuth, context, fs, head, path, redirect, sendFile, strata; 4 | 5 | fs = require('fs'); 6 | 7 | path = require('path'); 8 | 9 | strata = require('strata'); 10 | 11 | context = require('./context'); 12 | 13 | sendFile = function(file, options) { 14 | if (options == null) { 15 | options = {}; 16 | } 17 | if (typeof file === 'string') { 18 | options.filename || (options.filename = path.basename(file)); 19 | file = fs.createReadStream(file); 20 | } 21 | if (options.inline) { 22 | options.disposition = 'inline'; 23 | } 24 | options.disposition || (options.disposition = 'attachment'); 25 | options.type || (options.type = 'application/octet-stream'); 26 | this.headers['Content-Type'] = options.type; 27 | this.headers['Content-Disposition'] = options.disposition; 28 | if (options.filename) { 29 | this.headers['Content-Disposition'] += "; filename=\"" + options.filename + "\""; 30 | } 31 | if (options.lastModified) { 32 | this.headers['Last-Modified'] = options.lastModified; 33 | } 34 | this.headers['Transfer-Encoding'] = 'chunked'; 35 | return this.body = file; 36 | }; 37 | 38 | head = function(status) { 39 | if (status == null) { 40 | status = 200; 41 | } 42 | return this.status = status; 43 | }; 44 | 45 | redirect = function(location, status) { 46 | location = (typeof location.url === "function" ? location.url() : void 0) || location.url || location; 47 | return this.response.redirect(location, status); 48 | }; 49 | 50 | basicAuth = function(callback, realm) { 51 | var auth, creds, pass, result, scheme, unauthorized, user, _ref, _ref1, 52 | _this = this; 53 | 54 | if (realm == null) { 55 | realm = 'Authorization Required'; 56 | } 57 | unauthorized = function() { 58 | _this.status = 401; 59 | _this.contentType = 'text/plain'; 60 | _this.headers['WWW-Authenticate'] = "Basic realm='" + realm + "'"; 61 | _this.body = 'Unauthorized'; 62 | return false; 63 | }; 64 | auth = this.env.httpAuthorization; 65 | if (!auth) { 66 | return unauthorized(); 67 | } 68 | _ref = auth.split(' '), scheme = _ref[0], creds = _ref[1]; 69 | if (scheme.toLowerCase() !== 'basic') { 70 | return this.head(this.badRequest); 71 | } 72 | _ref1 = new Buffer(creds, 'base64').toString().split(':'), user = _ref1[0], pass = _ref1[1]; 73 | if (result = callback.call(this, user, pass)) { 74 | return this.head(this.ok) && result; 75 | } else { 76 | return unauthorized(); 77 | } 78 | }; 79 | 80 | context.include({ 81 | sendFile: sendFile, 82 | head: head, 83 | redirect: redirect, 84 | basicAuth: basicAuth, 85 | ok: 200, 86 | badRequest: 400, 87 | unauthorized: 401, 88 | forbidden: 403, 89 | notFound: 404, 90 | notAcceptable: 406 91 | }); 92 | 93 | module.exports = { 94 | sendFile: sendFile, 95 | head: head, 96 | redirect: redirect, 97 | basicAuth: basicAuth 98 | }; 99 | 100 | }).call(this); 101 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var App, context, helpers, strata, templates; 4 | 5 | require('./ext'); 6 | 7 | strata = require('strata'); 8 | 9 | App = require('./app'); 10 | 11 | context = require('./context'); 12 | 13 | helpers = require('./helpers'); 14 | 15 | templates = require('./templates'); 16 | 17 | module.exports = { 18 | App: App, 19 | context: context, 20 | helpers: helpers, 21 | templates: templates, 22 | strata: strata 23 | }; 24 | 25 | }).call(this); 26 | -------------------------------------------------------------------------------- /lib/static.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var fs, mime, path, sendFile, strata, utils; 4 | 5 | path = require('path'); 6 | 7 | fs = require('fs'); 8 | 9 | mime = require('mime'); 10 | 11 | strata = require('./index'); 12 | 13 | utils = strata.utils; 14 | 15 | sendFile = function(callback, path, stats) { 16 | return callback(200, { 17 | 'Content-Type': mime.lookup(path), 18 | 'Content-Length': stats.size.toString(), 19 | 'Last-Modified': stats.mtime.toUTCString() 20 | }, fs.createReadStream(path)); 21 | }; 22 | 23 | module.exports = function(app, root, index) { 24 | if (typeof root !== 'string') { 25 | throw new strata.Error('Invalid root directory'); 26 | } 27 | if (!fs.existsSync(root)) { 28 | throw new strata.Error("Directory " + root + " does not exist"); 29 | } 30 | if (!fs.statSync(root).isDirectory()) { 31 | throw new strata.Error("" + root + " is not a directory"); 32 | } 33 | if (index && typeof index === 'string') { 34 | index = [index]; 35 | } 36 | return function(env, callback) { 37 | var exists, fullPath, indexPath, pathInfo, stats, _i, _len; 38 | 39 | if (env.requestMethod !== 'GET') { 40 | return app(env, callback); 41 | } 42 | pathInfo = unescape(env.pathInfo); 43 | if (pathInfo.indexOf('..') !== -1) { 44 | return utils.forbidden(env, callback); 45 | } 46 | fullPath = path.join(root, pathInfo); 47 | exists = fs.existsSync(fullPath); 48 | if (!exists) { 49 | return app(env, callback); 50 | } 51 | stats = fs.statSync(fullPath); 52 | if (stats.isFile()) { 53 | return sendFile(callback, fullPath, stats); 54 | } else if (stats.isDirectory() && index) { 55 | for (_i = 0, _len = index.length; _i < _len; _i++) { 56 | indexPath = index[_i]; 57 | indexPath = path.join(fullPath, indexPath); 58 | exists = fs.existsSync(indexPath); 59 | if (exists) { 60 | sendFile(callback, indexPath, stats); 61 | break; 62 | } 63 | } 64 | return app(env, callback); 65 | } else { 66 | return app(env, callback); 67 | } 68 | }; 69 | }; 70 | 71 | }).call(this); 72 | -------------------------------------------------------------------------------- /lib/templates.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var context, name, path, resolve, _i, _len, _ref; 4 | 5 | path = require('path'); 6 | 7 | context = require('./context'); 8 | 9 | _ref = ['coffee', 'eco', 'ejs', 'json', 'less', 'mustache', 'stylus']; 10 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 11 | name = _ref[_i]; 12 | try { 13 | require("./templates/" + name); 14 | } catch (_error) {} 15 | } 16 | 17 | resolve = function(name, defaultPath) { 18 | try { 19 | return require.resolve(name); 20 | } catch (_error) {} 21 | try { 22 | return require.resolve(path.resolve(this.settings.views, name)); 23 | } catch (_error) {} 24 | try { 25 | return require.resolve(path.resolve(this.settings.assets, name)); 26 | } catch (_error) {} 27 | if (defaultPath != null) { 28 | return defaultPath; 29 | } 30 | throw "Cannot find " + name; 31 | }; 32 | 33 | context.include({ 34 | resolve: resolve 35 | }); 36 | 37 | module.exports = { 38 | resolve: resolve 39 | }; 40 | 41 | }).call(this); 42 | -------------------------------------------------------------------------------- /lib/templates/coffee.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var Fiber, coffee, compile, context, fs, path, view; 4 | 5 | Fiber = require('fibers'); 6 | 7 | path = require('path'); 8 | 9 | fs = require('fs'); 10 | 11 | context = require('../context'); 12 | 13 | coffee = require('coffee-script'); 14 | 15 | compile = function(path) { 16 | var fiber; 17 | 18 | fiber = Fiber.current; 19 | fs.readFile(path, 'utf8', function(err, data) { 20 | if (err) { 21 | fiber.throwInto(err); 22 | } 23 | return fiber.run(coffee.compile(data)); 24 | }); 25 | return Fiber["yield"](); 26 | }; 27 | 28 | view = function(name) { 29 | this.contentType = 'text/javascript'; 30 | path = this.resolve(name); 31 | return compile(path); 32 | }; 33 | 34 | context.include({ 35 | coffee: view 36 | }); 37 | 38 | module.exports = compile; 39 | 40 | }).call(this); 41 | -------------------------------------------------------------------------------- /lib/templates/eco.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var Fiber, compile, context, eco, fs, path, view; 4 | 5 | Fiber = require('fibers'); 6 | 7 | path = require('path'); 8 | 9 | fs = require('fs'); 10 | 11 | context = require('../context'); 12 | 13 | eco = require('eco'); 14 | 15 | compile = function(path, context) { 16 | var fiber; 17 | 18 | fiber = Fiber.current; 19 | fs.readFile(path, 'utf8', function(err, data) { 20 | if (err) { 21 | fiber.throwInto(err); 22 | } 23 | return fiber.run(eco.render(data, context)); 24 | }); 25 | return Fiber["yield"](); 26 | }; 27 | 28 | view = function(name, options) { 29 | var layout, result; 30 | 31 | if (options == null) { 32 | options = {}; 33 | } 34 | path = this.resolve(name); 35 | result = compile(path, this); 36 | layout = options.layout; 37 | if (layout == null) { 38 | layout = this.settings.layout; 39 | } 40 | if (layout) { 41 | result = compile(layout, { 42 | body: result 43 | }); 44 | } 45 | return result; 46 | }; 47 | 48 | context.include({ 49 | eco: view 50 | }); 51 | 52 | module.exports = compile; 53 | 54 | }).call(this); 55 | -------------------------------------------------------------------------------- /lib/templates/ejs.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var Fiber, compile, context, ejs, fs, path, view; 4 | 5 | Fiber = require('fibers'); 6 | 7 | path = require('path'); 8 | 9 | fs = require('fs'); 10 | 11 | context = require('../context'); 12 | 13 | ejs = require('ejs'); 14 | 15 | compile = function(path, context) { 16 | var fiber; 17 | 18 | fiber = Fiber.current; 19 | fs.readFile(path, 'utf8', function(err, data) { 20 | if (err) { 21 | fiber.throwInto(err); 22 | } 23 | return fiber.run(ejs.render(data, context)); 24 | }); 25 | return Fiber["yield"](); 26 | }; 27 | 28 | view = function(name) { 29 | path = this.resolve(name); 30 | return compile(path, this); 31 | }; 32 | 33 | context.include({ 34 | ejs: view 35 | }); 36 | 37 | module.exports = compile; 38 | 39 | }).call(this); 40 | -------------------------------------------------------------------------------- /lib/templates/json.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var coffee, compile, context, fs, json, jsonp, path; 4 | 5 | path = require('path'); 6 | 7 | fs = require('fs'); 8 | 9 | context = require('../context'); 10 | 11 | coffee = require('coffee-script'); 12 | 13 | compile = function(object) { 14 | return JSON.stringify(object); 15 | }; 16 | 17 | json = function(object) { 18 | this.contentType = 'application/json'; 19 | return compile(object); 20 | }; 21 | 22 | jsonp = function(object, options) { 23 | var cb, result; 24 | 25 | if (options == null) { 26 | options = {}; 27 | } 28 | cb = options.callback; 29 | cb || (cb = this.params.callback); 30 | result = this.json(object); 31 | if (cb) { 32 | result = "" + cb + "(" + result + ")"; 33 | } 34 | return result; 35 | }; 36 | 37 | context.include({ 38 | json: json, 39 | jsonp: jsonp 40 | }); 41 | 42 | module.exports = compile; 43 | 44 | }).call(this); 45 | -------------------------------------------------------------------------------- /lib/templates/less.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var Fiber, compile, context, fs, less, path, view, _base; 4 | 5 | Fiber = require('fibers'); 6 | 7 | path = require('path'); 8 | 9 | fs = require('fs'); 10 | 11 | context = require('../context'); 12 | 13 | less = require('less'); 14 | 15 | compile = function(path) { 16 | var fiber; 17 | 18 | fiber = Fiber.current; 19 | fs.readFile(path, 'utf8', function(err, data) { 20 | if (err) { 21 | fiber.throwInto(err); 22 | } 23 | return less.render(data, function(err, css) { 24 | if (err) { 25 | fiber.throwInto(err); 26 | } 27 | return fiber.run(css); 28 | }); 29 | }); 30 | return Fiber["yield"](); 31 | }; 32 | 33 | view = function(name) { 34 | this.contentType = 'text/css'; 35 | path = this.resolve(name); 36 | return compile(path); 37 | }; 38 | 39 | (_base = require.extensions)['.less'] || (_base['.less'] = function(module, filename) {}); 40 | 41 | context.include({ 42 | less: view 43 | }); 44 | 45 | module.exports = compile; 46 | 47 | }).call(this); 48 | -------------------------------------------------------------------------------- /lib/templates/mustache.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var Fiber, compile, context, mu, path, view; 4 | 5 | Fiber = require('fibers'); 6 | 7 | path = require('path'); 8 | 9 | mu = require('mu'); 10 | 11 | context = require('../context'); 12 | 13 | compile = function(path, context) { 14 | var fiber; 15 | 16 | fiber = Fiber.current; 17 | fs.readFile(path, 'utf8', function(err, data) { 18 | var buffer, stream; 19 | 20 | if (err) { 21 | fiber.throwInto(err); 22 | } 23 | buffer = ''; 24 | stream = mu.compileText(data)(context); 25 | stream.addListener('data', function(c) { 26 | return buffer += c; 27 | }); 28 | return stream.addListener('end', function() { 29 | return fiber.run(buffer); 30 | }); 31 | }); 32 | return Fiber["yield"](); 33 | }; 34 | 35 | view = function(name, options) { 36 | var layout, result; 37 | 38 | if (options == null) { 39 | options = {}; 40 | } 41 | path = this.resolve(name); 42 | result = compile(path, this); 43 | layout = options.layout; 44 | if (layout == null) { 45 | layout = this.settings.layout; 46 | } 47 | if (layout) { 48 | result = compile(layout, { 49 | body: result 50 | }); 51 | } 52 | return result; 53 | }; 54 | 55 | require.extensions['.mustache'] = function(module, filename) {}; 56 | 57 | require.extensions['.mu'] = function(module, filename) {}; 58 | 59 | context.include({ 60 | mustache: view, 61 | mu: view 62 | }); 63 | 64 | module.exports = compile; 65 | 66 | }).call(this); 67 | -------------------------------------------------------------------------------- /lib/templates/stylus.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var Fiber, compile, context, fs, path, stylus, view, _base; 4 | 5 | Fiber = require('fibers'); 6 | 7 | path = require('path'); 8 | 9 | fs = require('fs'); 10 | 11 | context = require('../context'); 12 | 13 | stylus = require('stylus'); 14 | 15 | compile = function(path) { 16 | var fiber; 17 | 18 | fiber = Fiber.current; 19 | fs.readFile(path, 'utf8', function(err, data) { 20 | if (err) { 21 | fiber.throwInto(err); 22 | } 23 | return stylus.render(data, { 24 | filename: path 25 | }, function(err, css) { 26 | if (err) { 27 | fiber.throwInto(err); 28 | } 29 | return fiber.run(css); 30 | }); 31 | }); 32 | return Fiber["yield"](); 33 | }; 34 | 35 | view = function(name) { 36 | this.contentType = 'text/css'; 37 | path = this.resolve(name); 38 | return compile(path); 39 | }; 40 | 41 | (_base = require.extensions)['.styl'] || (_base['.styl'] = function(module, filename) {}); 42 | 43 | context.include({ 44 | stylus: view, 45 | styl: view 46 | }); 47 | 48 | module.exports = compile; 49 | 50 | }).call(this); 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ace", 3 | "description": "Sinatra for Node", 4 | "version": "0.0.3", 5 | "author": "maccman", 6 | "repository": { 7 | "type" : "git", 8 | "url": "http://github.com/maccman/ace.git" 9 | }, 10 | "main" : "./lib/index.js", 11 | "bin": { "ace": "./bin/ace" }, 12 | "dependencies": { 13 | "strata": "git://github.com/maccman/strata.git", 14 | "fibers": "git://github.com/laverdet/node-fibers.git#master", 15 | "mime": "1.2.4", 16 | "optimist": "0.3.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app.coffee: -------------------------------------------------------------------------------- 1 | fs = require('fs') 2 | path = require('path') 3 | strata = require('strata') 4 | fibers = require('./fibers') 5 | context = require('./context') 6 | filter = require('./filter') 7 | templates = require('./templates') 8 | format = require('./format') 9 | 10 | # N.B. static is a reserved word 11 | staticFiles = require('./static') 12 | 13 | class App extends strata.Builder 14 | defaults: 15 | static: true 16 | sessions: true 17 | port: 1982 18 | bind: '0.0.0.0' 19 | root: process.cwd() 20 | views: './views' 21 | assets: './assets' 22 | public: './public' 23 | layout: 'layout' 24 | logging: true 25 | 26 | context: context 27 | resolve: templates.resolve 28 | 29 | constructor: -> 30 | super() 31 | 32 | @settings = {} 33 | @set @defaults 34 | 35 | @settings.layout = @resolve( 36 | @settings.layout, false 37 | ) 38 | 39 | unless fs.existsSync(@settings.public) 40 | @settings.static = false 41 | 42 | @pool = new fibers.Pool 43 | @router = new strata.Router 44 | 45 | before: (conditions, callback) -> 46 | unless callback 47 | callback = conditions 48 | conditions = true 49 | 50 | @beforeFilters ||= [] 51 | @beforeFilters.push([conditions, callback]) 52 | 53 | rewrite: (pattern, replacement) -> 54 | @use(strata.rewrite, pattern, replacement) 55 | 56 | root: (replacement) -> 57 | @rewrite('/', replacement) 58 | 59 | route: (pattern, app, methods) -> 60 | @router.route( 61 | pattern, 62 | context.wrap(app, this), 63 | methods 64 | ) 65 | 66 | set: (key, value) -> 67 | if typeof key is 'object' 68 | @set(k, v) for k, v of key 69 | else 70 | @settings[key] = value 71 | 72 | toApp: -> 73 | @use(strata.contentType, 'text/html') 74 | 75 | @use(strata.contentLength) 76 | 77 | if @settings.logging 78 | @use(strata.commonLogger) 79 | 80 | if options = @settings.sessions 81 | options = {} if options is true 82 | @use(strata.sessionCookie, options) 83 | 84 | if @settings.static 85 | @use(staticFiles, @settings.public, ['index.html']) 86 | 87 | @use(format) 88 | 89 | if @beforeFilters 90 | @use(filter, @beforeFilters, this) 91 | 92 | @run (env, callback) => 93 | @pool.wrap(@router.toApp())(env, callback) 94 | 95 | super 96 | 97 | serve: (options = {}) -> 98 | for key, value of options 99 | @settings[key] = value if value? 100 | strata.run(this, @settings) 101 | 102 | methods = 103 | get: ['GET', 'HEAD'], 104 | post: 'POST', 105 | put: 'PUT', 106 | del: 'DELETE', 107 | head: 'HEAD', 108 | options: 'OPTIONS' 109 | 110 | for type, method of methods 111 | App::[type] = do (method) -> 112 | (args...) -> @route(args..., method) 113 | 114 | module.exports = App -------------------------------------------------------------------------------- /src/context.coffee: -------------------------------------------------------------------------------- 1 | strata = require('strata') 2 | 3 | class Context 4 | @include: (obj) -> 5 | @::[key] = value for key, value of obj 6 | 7 | @wrap: (app, base) -> 8 | (env, callback) -> 9 | context = new Context(env, callback, base) 10 | try 11 | result = app.call(context, env, callback) 12 | context.send(result) 13 | catch err 14 | strata.handleError(err, env, callback) 15 | 16 | constructor: (@env, @callback, @app = {}) -> 17 | @request = new strata.Request(@env) 18 | @response = new strata.Response 19 | 20 | send: (result) -> 21 | return false if result is false 22 | return false if @served 23 | @served = true 24 | 25 | if Array.isArray(result) 26 | @response.status = result[0] 27 | @response.headers = result[1] 28 | @response.body = result[3] 29 | 30 | else if typeof result is 'integer' 31 | @response.status = result 32 | 33 | else if result instanceof strata.Response 34 | @response = result 35 | 36 | else if typeof result is 'function' 37 | @response.body = result 38 | 39 | else if typeof result is 'string' 40 | @response.body = result 41 | 42 | @callback( 43 | @response.status, 44 | @response.headers, 45 | @response.body 46 | ) 47 | 48 | setter: @::__defineSetter__ 49 | getter: @::__defineGetter__ 50 | 51 | @::getter 'cookies', -> 52 | @request.cookies.bind(@request).wait() 53 | 54 | @::getter 'params', -> 55 | @request.params.bind(@request).wait() 56 | 57 | @::getter 'query', -> 58 | @request.query.bind(@request).wait() 59 | 60 | @::getter 'body', -> 61 | @request.body.bind(@request).wait() 62 | 63 | @::getter 'route', -> 64 | @env.route 65 | 66 | @::getter 'settings', -> 67 | @app.settings 68 | 69 | @::getter 'session', -> 70 | @env.session or= {} 71 | 72 | @::setter 'session', (value) -> 73 | @env.session = value 74 | 75 | @::getter 'status', -> 76 | @response.status 77 | 78 | @::setter 'status', (value) -> 79 | @response.status = value 80 | 81 | @::getter 'headers', -> 82 | @response.headers 83 | 84 | @::setter 'headers', (value) -> 85 | @response.headers = value 86 | 87 | @::setter 'contentType', (value) -> 88 | @response.headers['Content-Type'] = value 89 | 90 | @::setter 'body', (value) -> 91 | @response.body = value 92 | 93 | accepts: (type) -> 94 | @request.accepts(type) 95 | 96 | @::getter 'format', -> 97 | @env.format 98 | 99 | @::getter 'acceptsJSON', -> 100 | mime = 'application/json' 101 | return true if @env.format is mime 102 | 103 | # Check to see if JSON is explicitly 104 | # mentioned in the accept header 105 | accept = @request.accept or '' 106 | accept.indexOf(mime) != -1 107 | 108 | module.exports = Context -------------------------------------------------------------------------------- /src/ext.coffee: -------------------------------------------------------------------------------- 1 | Fiber = require('fibers') 2 | future = require('fibers/future') 3 | 4 | Function::wait = -> 5 | future.wrap(@).apply(@, arguments).wait() 6 | 7 | EventEmitter = require('events').EventEmitter 8 | EventEmitter::wait = (success = 'success', failure = 'failure') -> 9 | fiber = Fiber.current 10 | @on success, -> fiber.run(arguments...) 11 | @on failure, -> fiber.throwInto(arguments...) 12 | Fiber.yield() -------------------------------------------------------------------------------- /src/fibers.coffee: -------------------------------------------------------------------------------- 1 | Fiber = require('fibers') 2 | context = require('./context') 3 | 4 | task = (callback) -> 5 | -> 6 | args = arguments 7 | Fiber -> 8 | callback(args...) 9 | .run() 10 | 11 | sleep = (ms) -> 12 | fiber = Fiber.current 13 | setTimeout -> 14 | fiber.run() 15 | , ms 16 | Fiber.yield() 17 | 18 | class Pool 19 | constructor: (size = 100) -> 20 | @queue = [] 21 | @count = 0 22 | @setSize(size) 23 | 24 | call: (callback) -> 25 | @queue.push(callback) 26 | if @count < @size 27 | @addFiber() 28 | this 29 | 30 | wrap: (callback) -> 31 | => 32 | args = arguments 33 | @call -> 34 | callback(args...) 35 | 36 | setSize: (size) -> 37 | @_size = size 38 | if Fiber.poolSize < @_size 39 | Fiber.poolSize = @_size 40 | 41 | @::__defineGetter__ 'size', -> @_size 42 | @::__defineSetter__ 'size', @::setSize 43 | 44 | # Private 45 | addFiber: -> 46 | Fiber(=> 47 | @count++ 48 | while callback = @queue.shift() 49 | callback() 50 | @count-- 51 | ).run() 52 | 53 | context.include 54 | sleep: sleep 55 | 56 | module.exports = 57 | task: task 58 | wrap: task 59 | sleep: sleep 60 | Pool: Pool 61 | -------------------------------------------------------------------------------- /src/filter.coffee: -------------------------------------------------------------------------------- 1 | strata = require 'strata' 2 | context = require './context' 3 | 4 | passesRoute = (env, route) -> 5 | route = strata.Router.compileRoute(route, []) 6 | return route.test(env.pathInfo) 7 | 8 | passes = (env, conditions) -> 9 | if conditions is true 10 | true 11 | 12 | else if typeof conditions is 'function' 13 | conditions(env) 14 | 15 | else if typeof conditions is 'string' 16 | passesRoute(env, conditions) 17 | 18 | else if Array.isArray(conditions) 19 | conditions.some (route) -> 20 | passesRoute(env, route) 21 | 22 | else 23 | request = new strata.Request(env) 24 | 25 | # Match conditions against the request & env object, 26 | # either by a regex test, or a strict comparision 27 | 28 | for key, value of conditions 29 | if value.test? 30 | return true if value.test(request[key]) 31 | return true if value.test(env[key]) 32 | 33 | else 34 | return true if request[key] is value 35 | return true if env[key] is value 36 | 37 | false 38 | 39 | module.exports = (app, filters, base) -> 40 | (env, callback) -> 41 | app env, -> 42 | original = arguments 43 | 44 | # If the filter returns a status code other 45 | # than 200, then callback with the data 46 | # from the filter, otherwise carry on with 47 | # the original request. 48 | proxiedCallback = (status) -> 49 | if @status is 200 50 | callback(original...) 51 | else 52 | callback(arguments...) 53 | 54 | for filter in filters 55 | [conditions, filterCallback] = filter 56 | 57 | if passes(env, conditions) 58 | return context.wrap(filterCallback, base)(env, proxiedCallback) 59 | 60 | callback(original...) -------------------------------------------------------------------------------- /src/format.coffee: -------------------------------------------------------------------------------- 1 | mime = require('mime') 2 | path = require('path') 3 | 4 | module.exports = (app, defaultType) -> 5 | (env, callback) -> 6 | pathInfo = env.pathInfo 7 | ext = path.extname(pathInfo) 8 | format = if ext then mime.lookup(ext) else null 9 | env.format = format 10 | 11 | # Modify env.pathInfo for downstream apps. 12 | env.pathInfo = pathInfo.replace(new RegExp("#{ext}$"), '') if ext 13 | 14 | app env, (status, headers, body) -> 15 | # Reset env.pathInfo for upstream apps. 16 | env.pathInfo = pathInfo 17 | 18 | callback(status, headers, body) -------------------------------------------------------------------------------- /src/helpers.coffee: -------------------------------------------------------------------------------- 1 | fs = require('fs') 2 | path = require('path') 3 | strata = require('strata') 4 | context = require('./context') 5 | 6 | sendFile = (file, options = {}) -> 7 | if typeof file is 'string' 8 | options.filename or= path.basename(file) 9 | file = fs.createReadStream(file) 10 | 11 | options.disposition = 'inline' if options.inline 12 | options.disposition or= 'attachment' 13 | options.type or= 'application/octet-stream' 14 | 15 | @headers['Content-Type'] = options.type 16 | @headers['Content-Disposition'] = options.disposition 17 | 18 | if options.filename 19 | @headers['Content-Disposition'] += "; filename=\"#{options.filename}\"" 20 | 21 | if options.lastModified 22 | @headers['Last-Modified'] = options.lastModified 23 | 24 | @headers['Transfer-Encoding'] = 'chunked' 25 | 26 | @body = file 27 | 28 | head = (status = 200) -> 29 | @status = status 30 | 31 | redirect = (location, status) -> 32 | location = location.url?() or location.url or location 33 | @response.redirect(location, status) 34 | 35 | basicAuth = (callback, realm = 'Authorization Required') -> 36 | unauthorized = => 37 | @status = 401 38 | @contentType = 'text/plain' 39 | @headers['WWW-Authenticate'] = "Basic realm='#{realm}'" 40 | @body = 'Unauthorized' 41 | false 42 | 43 | auth = @env.httpAuthorization 44 | return unauthorized() unless auth 45 | 46 | [scheme, creds] = auth.split(' ') 47 | return @head(@badRequest) if scheme.toLowerCase() != 'basic' 48 | 49 | [user, pass] = new Buffer(creds, 'base64').toString().split(':') 50 | if result = callback.call(this, user, pass) 51 | @head(@ok) and result 52 | else 53 | unauthorized() 54 | 55 | context.include 56 | sendFile: sendFile 57 | head: head 58 | redirect: redirect 59 | basicAuth: basicAuth 60 | ok: 200 61 | badRequest: 400 62 | unauthorized: 401 63 | forbidden: 403 64 | notFound: 404 65 | notAcceptable: 406 66 | 67 | module.exports = 68 | sendFile: sendFile 69 | head: head 70 | redirect: redirect 71 | basicAuth: basicAuth -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | require('./ext') 2 | 3 | strata = require('strata') 4 | App = require('./app') 5 | context = require('./context') 6 | helpers = require('./helpers') 7 | templates = require('./templates') 8 | 9 | module.exports = 10 | App: App 11 | context: context 12 | helpers: helpers 13 | templates: templates 14 | strata: strata 15 | -------------------------------------------------------------------------------- /src/static.coffee: -------------------------------------------------------------------------------- 1 | path = require('path') 2 | fs = require('fs') 3 | mime = require('mime') 4 | strata = require('./index') 5 | utils = strata.utils 6 | 7 | sendFile = (callback, path, stats) -> 8 | callback 200, 9 | 'Content-Type': mime.lookup(path) 10 | 'Content-Length': stats.size.toString() 11 | 'Last-Modified': stats.mtime.toUTCString() 12 | , fs.createReadStream(path) 13 | 14 | module.exports = (app, root, index) -> 15 | throw new strata.Error('Invalid root directory') if typeof root isnt 'string' 16 | throw new strata.Error("Directory #{root} does not exist") unless fs.existsSync(root) 17 | throw new strata.Error("#{root} is not a directory") unless fs.statSync(root).isDirectory() 18 | index = [ index ] if index and typeof index is 'string' 19 | 20 | (env, callback) -> 21 | unless env.requestMethod is 'GET' 22 | return app(env, callback) 23 | 24 | pathInfo = unescape(env.pathInfo) 25 | 26 | unless pathInfo.indexOf('..') is -1 27 | return utils.forbidden(env, callback) 28 | 29 | fullPath = path.join(root, pathInfo) 30 | 31 | exists = fs.existsSync(fullPath) 32 | return app(env, callback) unless exists 33 | 34 | stats = fs.statSync(fullPath) 35 | 36 | if stats.isFile() 37 | sendFile(callback, fullPath, stats) 38 | 39 | else if stats.isDirectory() and index 40 | for indexPath in index 41 | indexPath = path.join(fullPath, indexPath) 42 | exists = fs.existsSync indexPath 43 | if exists 44 | sendFile callback, indexPath, stats 45 | break 46 | app(env, callback) 47 | 48 | else 49 | app(env, callback) -------------------------------------------------------------------------------- /src/templates.coffee: -------------------------------------------------------------------------------- 1 | path = require('path') 2 | context = require('./context') 3 | 4 | for name in ['coffee', 'eco', 'ejs', 'json', 'less', 'mustache', 'stylus'] 5 | try require("./templates/#{name}") 6 | 7 | resolve = (name, defaultPath) -> 8 | try return require.resolve(name) 9 | try return require.resolve(path.resolve(@settings.views, name)) 10 | try return require.resolve(path.resolve(@settings.assets, name)) 11 | return defaultPath if defaultPath? 12 | throw "Cannot find #{name}" 13 | 14 | context.include 15 | resolve: resolve 16 | 17 | module.exports = 18 | resolve: resolve -------------------------------------------------------------------------------- /src/templates/coffee.coffee: -------------------------------------------------------------------------------- 1 | Fiber = require('fibers') 2 | path = require('path') 3 | fs = require('fs') 4 | context = require('../context') 5 | coffee = require('coffee-script') 6 | 7 | compile = (path) -> 8 | fiber = Fiber.current 9 | fs.readFile path, 'utf8', (err, data) -> 10 | fiber.throwInto(err) if err 11 | 12 | fiber.run coffee.compile(data) 13 | Fiber.yield() 14 | 15 | view = (name) -> 16 | @contentType = 'text/javascript' 17 | path = @resolve(name) 18 | compile(path) 19 | 20 | context.include 21 | coffee: view 22 | 23 | module.exports = compile -------------------------------------------------------------------------------- /src/templates/eco.coffee: -------------------------------------------------------------------------------- 1 | Fiber = require('fibers') 2 | path = require('path') 3 | fs = require('fs') 4 | context = require('../context') 5 | eco = require('eco') 6 | 7 | compile = (path, context) -> 8 | fiber = Fiber.current 9 | fs.readFile path, 'utf8', (err, data) -> 10 | fiber.throwInto(err) if err 11 | 12 | fiber.run eco.render(data, context) 13 | Fiber.yield() 14 | 15 | view = (name, options = {}) -> 16 | path = @resolve(name) 17 | result = compile(path, this) 18 | 19 | layout = options.layout 20 | layout ?= @settings.layout 21 | result = compile(layout, body: result) if layout 22 | 23 | result 24 | 25 | context.include 26 | eco: view 27 | 28 | module.exports = compile -------------------------------------------------------------------------------- /src/templates/ejs.coffee: -------------------------------------------------------------------------------- 1 | Fiber = require('fibers') 2 | path = require('path') 3 | fs = require('fs') 4 | context = require('../context') 5 | ejs = require('ejs') 6 | 7 | compile = (path, context) -> 8 | fiber = Fiber.current 9 | fs.readFile path, 'utf8', (err, data) -> 10 | fiber.throwInto(err) if err 11 | 12 | fiber.run ejs.render(data, context) 13 | Fiber.yield() 14 | 15 | view = (name) -> 16 | path = @resolve(name) 17 | compile(path, this) 18 | 19 | context.include 20 | ejs: view 21 | 22 | module.exports = compile -------------------------------------------------------------------------------- /src/templates/json.coffee: -------------------------------------------------------------------------------- 1 | path = require('path') 2 | fs = require('fs') 3 | context = require('../context') 4 | coffee = require('coffee-script') 5 | 6 | compile = (object) -> 7 | JSON.stringify(object) 8 | 9 | json = (object) -> 10 | @contentType = 'application/json' 11 | compile(object) 12 | 13 | jsonp = (object, options = {}) -> 14 | cb = options.callback 15 | cb or= @params.callback 16 | 17 | result = @json object 18 | result = "#{cb}(#{result})" if cb 19 | result 20 | 21 | context.include 22 | json: json 23 | jsonp: jsonp 24 | 25 | module.exports = compile -------------------------------------------------------------------------------- /src/templates/less.coffee: -------------------------------------------------------------------------------- 1 | Fiber = require('fibers') 2 | path = require('path') 3 | fs = require('fs') 4 | context = require('../context') 5 | less = require('less') 6 | 7 | compile = (path) -> 8 | fiber = Fiber.current 9 | fs.readFile path, 'utf8', (err, data) -> 10 | fiber.throwInto(err) if err 11 | 12 | less.render data, (err, css) -> 13 | fiber.throwInto(err) if err 14 | fiber.run(css) 15 | Fiber.yield() 16 | 17 | view = (name) -> 18 | @contentType = 'text/css' 19 | path = @resolve(name) 20 | compile(path) 21 | 22 | require.extensions['.less'] or= (module, filename) -> 23 | 24 | context.include 25 | less: view 26 | 27 | module.exports = compile 28 | -------------------------------------------------------------------------------- /src/templates/mustache.coffee: -------------------------------------------------------------------------------- 1 | Fiber = require('fibers') 2 | path = require('path') 3 | mu = require('mu') 4 | context = require('../context') 5 | 6 | compile = (path, context) -> 7 | fiber = Fiber.current 8 | fs.readFile path, 'utf8', (err, data) -> 9 | fiber.throwInto(err) if err 10 | buffer = '' 11 | stream = mu.compileText(data)(context) 12 | stream.addListener 'data', (c) -> buffer += c 13 | stream.addListener 'end', -> fiber.run(buffer) 14 | Fiber.yield() 15 | 16 | view = (name, options = {}) -> 17 | path = @resolve(name) 18 | result = compile(path, this) 19 | 20 | layout = options.layout 21 | layout ?= @settings.layout 22 | result = compile(layout, body: result) if layout 23 | 24 | result 25 | 26 | # So require.resolve works correctly 27 | require.extensions['.mustache'] = (module, filename) -> 28 | require.extensions['.mu'] = (module, filename) -> 29 | 30 | context.include 31 | mustache: view 32 | mu: view 33 | 34 | module.exports = compile 35 | -------------------------------------------------------------------------------- /src/templates/stylus.coffee: -------------------------------------------------------------------------------- 1 | Fiber = require('fibers') 2 | path = require('path') 3 | fs = require('fs') 4 | context = require('../context') 5 | stylus = require('stylus') 6 | 7 | compile = (path) -> 8 | fiber = Fiber.current 9 | fs.readFile path, 'utf8', (err, data) -> 10 | fiber.throwInto(err) if err 11 | 12 | stylus.render data, {filename: path}, (err, css) -> 13 | fiber.throwInto(err) if err 14 | fiber.run(css) 15 | Fiber.yield() 16 | 17 | view = (name) -> 18 | @contentType = 'text/css' 19 | path = @resolve(name) 20 | compile(path) 21 | 22 | # So require.resolve works correctly 23 | require.extensions['.styl'] or= (module, filename) -> 24 | 25 | context.include 26 | stylus: view 27 | styl: view 28 | 29 | module.exports = compile 30 | --------------------------------------------------------------------------------