├── .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 |
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 |
4 |
5 |
6 |
21 |
--------------------------------------------------------------------------------
/examples/views/posts/index.eco:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
13 |
14 | <% if @session.user: %>
15 | New Post
16 | <% else: %>
17 | Login
18 | <% end %>
19 |
--------------------------------------------------------------------------------
/examples/views/posts/new.eco:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Name
9 |
10 |
11 |
12 |
13 | Body
14 |
15 |
16 |
17 | Save
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 | Delete
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 |
--------------------------------------------------------------------------------