From c21fe56d59eca3e3176e5b806af91aba98dd8902 Mon Sep 17 00:00:00 2001 From: jed Date: Sun, 26 Sep 2010 11:16:52 +0200 Subject: [PATCH] pulled each app out into its own file. --- README.md | 45 +++++++++++++++++++++ cli.js | 7 ++++ concat.js | 26 ++++++++++++ demo.js | 86 ++++++++++++++++++++++++++++++++++++++++ head.js | 25 ++++++++++++ html.js | 74 ++++++++++++++++++++++++++++++++++ ignore.js | 3 ++ index.js | 29 ++++++++++++++ log.js | 14 +++++++ method.js | 23 +++++++++++ node/addContentLength.js | 24 +++++++++++ node/fs.js | 24 +++++++++++ node/listen.js | 15 +++++++ node/listener.js | 55 +++++++++++++++++++++++++ package.json | 18 +++++++++ render.js | 16 ++++++++ route.js | 41 +++++++++++++++++++ run.js | 18 +++++++++ sleep.js | 14 +++++++ stream.js | 22 ++++++++++ write.js | 5 +++ 21 files changed, 584 insertions(+) create mode 100644 README.md create mode 100644 cli.js create mode 100644 concat.js create mode 100644 demo.js create mode 100644 head.js create mode 100644 html.js create mode 100644 ignore.js create mode 100644 index.js create mode 100644 log.js create mode 100644 method.js create mode 100644 node/addContentLength.js create mode 100644 node/fs.js create mode 100644 node/listen.js create mode 100644 node/listener.js create mode 100644 package.json create mode 100644 render.js create mode 100644 route.js create mode 100644 run.js create mode 100644 sleep.js create mode 100644 stream.js create mode 100644 write.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..734edad --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +(fab) - a streaming javascript framework +======================================== + +(fab) is a lightweight toolkit that makes it easy to build asynchronous web apps. It takes advantage of the flexibility and functional nature of javascript to create a concise "DSL", without pre-compilation or magic scope hackery. + +Here's an example of a "hello world" app: + + module.exports = function( exports, imports ) { + return imports( function( run, node$listen, route, html ) { + with ( html ) return run + + () + ( node$listen, 0xFAB ) + + ( HTML ) + ( BODY, { id: "show" } ) + ( "Hello, " ) + + ( EM ) + ( route, /^\/(\w+)$/ ) + ( route.capture, 0 ) + () + ( "world!" ) + () + + ( "!" ) + () + () + (); + }) + } + +## Getting started + +To install (fab), use [npm](github.com/isaacs/npm): + + npm install fab + +Then, from the (fab) directory in your node folder, launch the `demo.js` example: + + fab demo.js + +Look in the source of the demo to see how the app was built. + +Unfortunately, most of the current (fab) documentation reflects an older and deprecated API. This will be remedied shortly now that the (fab) API has started to gel. To stay updated on the progress of (fab), follow [@fabjs](http://twitter.com/fabjs) on Twitter. \ No newline at end of file diff --git a/cli.js b/cli.js new file mode 100644 index 0000000..aea5e0d --- /dev/null +++ b/cli.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +require( "fab" )( + require( + require( "path" ).join( process.cwd(), process.argv[ 2 ] ) + ) +) \ No newline at end of file diff --git a/concat.js b/concat.js new file mode 100644 index 0000000..4aa4dc5 --- /dev/null +++ b/concat.js @@ -0,0 +1,26 @@ +module.exports = function( exports ) { + return exports( function( write ) { + var buffer = "" + , types = { String: true, Buffer: true }; + + return function read( body, head ) { + // TODO: add timeout to flush? + + if ( head && head.status >= 400 ) { + write( body, head )(); + return function x(){ return x }; + } + + else if ( body && types[ body.constructor.name ] ) buffer += body; + + else { + if ( buffer ) write = write( buffer ); + + buffer = ""; + write = write.apply( undefined, arguments ); + } + + return arguments.length ? read : write; + } + }); +} \ No newline at end of file diff --git a/demo.js b/demo.js new file mode 100644 index 0000000..9c9787a --- /dev/null +++ b/demo.js @@ -0,0 +1,86 @@ +// i'm not a huge fan of the commonJS require spec, but it'll do for now. +module.exports = function( exports, imports ) { + + // imports is an async function that piggybacks on require. + // if you give it a list of string arguments, your callback will + // be called with the results in the same order. if you're feeling + // daring, like we are here, you can omit them and the imports + // function will toString your callback to find what you're looking + // for. a cool hack, but a hack nonetheless. "$" is replaced with + // "/" in module names. + return imports( function + ( run + , node$listen + , route + , ignore + , write + , stream + , html + , head + , node$fs + , sleep + ) { + + // using with for our html module means we can have an awesome DSL for HTML + // templating. run is an app that recursively evaluates the entire stream once. + with ( html ) return run + + // this is blank because we don't need to give run a downstream function, + // since we're not piping anything to stdout. + () + + // this fires up a listener on port 4011 + ( node$listen, 0xFAB ) + + // let's route for static files. route is an app that takes two streams, + // one for matches and one for non-matches. + ( route, /^\/static/ ) + // we matched! + + // the fs module takes one stream: the path name. it's just middleware + // that converts an upstream path name to the contents of the file. + ( node$fs ) + // first, get the current directory + ( __dirname ) + + // then sleep for 500, just because we can + ( sleep, 500 ) + + // then append the pathname + ( head.url.pathname ) + () + () + // we didn't match the /static url, so we keep going. + + // now let's return the front page + ( route, /^\/$/ ) + // since we've with'ed the html app above, each uppercase element + // name here is a reference to html.. there's an app + // for each HTML5 element, and each of these apps is just middleware + // that outputs an open tag, pipes through its contents, and outputs + // a close tag when it's done. + ( HTML ) + ( BODY, { id: "show" } ) + ( "Hello, " ) + + ( EM ) + // this pattern looks for a url that ends with an alphanumeric string + ( route, /^\/(\w+)$/ ) + // looks like a path was provided! here, route.capture outputs + // the captured path segment at the given index. + ( route.capture, 0 ) + () + // we didn't match, so let's output a generic hello. + ( "world" ) + () + + ( "!" ) + () + () + () + + // this is a catchall 404 for any paths that haven't matched. + ( "Not found.", { status: 404 } ) + (); + }) +} \ No newline at end of file diff --git a/head.js b/head.js new file mode 100644 index 0000000..b7aede8 --- /dev/null +++ b/head.js @@ -0,0 +1,25 @@ +module.exports = function( exports ) { + var head = getter( "head" ) + , prop, props = { + method: "", + url: "href protocol host auth hostname port pathname search query hash capture", + headers: "accept acceptCharset acceptEncoding acceptLanguage acceptRanges authorization cacheControl connection cookie contentLength contentType date expect from host ifMatch ifModifiedSince ifNoneMatch ifRange ifUnmodifiedSince maxForwards pragma proxyAuthorization range referer te upgrade userAgent via warning" + }; + + for ( prop in props ) { + var subprops = props[ prop ].split( " " ); + + head[ prop ] = getter( "head." + prop ); + for ( var i = 0, subprop; subprop = subprops[ i++ ]; ) { + head[ prop ][ subprop ] = getter( "head." + prop + "." + subprop ); + } + } + + return exports( head ); + + function writer( val ) { + return new Function( "write", "head", "return write(" + val + ")" ); + } + + function getter( name ){ return writer( writer( name ) ) } +}; \ No newline at end of file diff --git a/html.js b/html.js new file mode 100644 index 0000000..7462020 --- /dev/null +++ b/html.js @@ -0,0 +1,74 @@ +module.exports = function( exports ) { + var tags = [ + +// open +"A ABBR ADDRESS ARTICLE ASIDE AUDIO B BB BDO BLOCKQUOTE BODY BUTTON\ + CANVAS CAPTION CITE CODE COLGROUP DATAGRID DATALIST DD DEL DETAILS\ + DFN DIALOG DIV DL DOCTYPE DT EM EVENTSOURCE FIELDSET FIGURE FOOTER\ + FORM H1 H2 H3 H4 H5 H6 HEAD HEADER HTML I IFRAME INS KBD LABEL\ + LEGEND LI MAP MARK MENU METER NAV NOSCRIPT OBJECT OL OPTGROUP\ + OPTION OUTPUT P PRE PROGRESS Q RP RT RUBY SAMP SCRIPT SECTION\ + SELECT SMALL SPAN STRONG STYLE SUB SUP TABLE TBODY TD TEXTAREA\ + TFOOT TH THEAD TIME TITLE TR UL VAR VIDEO".split( " " ), + +// closed +"AREA BASE BR COL COMMAND EMBED HR IMG INPUT KEYGEN LINK META PARAM\ + SOURCE WBR".split( " " ) + + ]; + + for ( var isVoid = 0, names; names = tags[ isVoid ]; isVoid++ ) { + for ( var i = 0, name; name = names[ i++ ]; ) { + elem[ name ] = elem( name.toLowerCase(), !!isVoid ); + } + } + + elem.DOCTYPE = function( write, dec ) { + return write( "\n" ); + } + + elem.COMMENT = function( write ) { + write = write( "" ); + write = write( obj ); + return read; + } + } + + return exports( elem ); + + function elem( name, isVoid ) { + return function( write, obj ) { + write = write( "<" + name ); + write = attrs( write )( obj )( ">" ); + + if ( isVoid ) return write; + + return function read( arg ) { + if ( !arguments.length ) return write( "" ); + + write = write.apply( undefined, arguments ); + return read; + }; + } + } + + function attrs( write ) { + return function read( obj ) { + for ( var name in obj ) { + write = write( " " )( name )( "=" ); + write = attr( write )( obj[ name ] ); + } + + return write; + } + } + + function attr( write ) { + return function read( value ) { + return write( "\"" )( value )( "\"" ); + } + } +}; \ No newline at end of file diff --git a/ignore.js b/ignore.js new file mode 100644 index 0000000..0c86472 --- /dev/null +++ b/ignore.js @@ -0,0 +1,3 @@ +module.exports = function( exports ) { + return exports( function ignore(){ return ignore } ); +} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..8db050c --- /dev/null +++ b/index.js @@ -0,0 +1,29 @@ +require.paths.unshift( __dirname ); +module.exports = fab; + +function fab( app ){ app( log, imports ) } + +function log( data ) { + if ( data ) process.stdout.write( data ); + return log; +} + +function imports( exports ) { + var libs = [] + , names = 1 in arguments + ? libs.slice.call( arguments, 1 ) + : exports.toString() + .split( /[^\w$]+/, exports.length + !!exports.name + 1 ) + .slice( !!exports.name + 1 ); + + ( function loop() { + var name = names.shift(); + + if ( !name ) exports.apply( undefined, libs ); + + else require( name.replace( /\W/g, "/" ) )( function( lib ) { + libs.push( lib ); + loop(); + }, imports ); + })() +} \ No newline at end of file diff --git a/log.js b/log.js new file mode 100644 index 0000000..be6abff --- /dev/null +++ b/log.js @@ -0,0 +1,14 @@ +module.exports = function( exports ) { + return exports( function( write, msg ) { + if ( msg ) { + console.log( msg ); + return write; + } + + return function read( body ) { + console.log( body ); + write = write.apply( undefined, arguments ); + return arguments.length ? read : write; + } + }) +} \ No newline at end of file diff --git a/method.js b/method.js new file mode 100644 index 0000000..ff26714 --- /dev/null +++ b/method.js @@ -0,0 +1,23 @@ +module.exports = function( exports, imports ) { + return imports( function( stream ) { + var names = "GET PUT POST DELETE HEAD".split( " " ); + + function method( write, name ) { + return stream( function( yes ) { + return stream( function( no ) { + return write( function( write, head, body ) { + return ( head.method == name ? yes : no )( write, head, body ); + })(); + }); + }); + } + + for ( var i = names.length; i--; ) ( function( name ) { + method[ name ] = function( write ) { + return method( write, name ); + } + })( names[ i ] ); + + return exports( method ); + }) +} \ No newline at end of file diff --git a/node/addContentLength.js b/node/addContentLength.js new file mode 100644 index 0000000..6f227ea --- /dev/null +++ b/node/addContentLength.js @@ -0,0 +1,24 @@ +module.exports = function( exports, imports ) { + return imports( function( stream, render ) { + return exports( function( write ) { + return stream( function( upstream ) { + return write( function( write, head, body ) { + var buffered = stream() + , length = 0; + + return upstream( render( function read( body, head ) { + buffered = buffered.apply( this, arguments ); + + if ( !arguments.length ) return buffered( + write( undefined, { headers: { "content-length": length } } ) + ); + + if ( body ) length += body.length; + + return read; + }, head, body ), head, body ); + })(); + }); + }); + }, "stream", "render" ); +} diff --git a/node/fs.js b/node/fs.js new file mode 100644 index 0000000..ec3ee28 --- /dev/null +++ b/node/fs.js @@ -0,0 +1,24 @@ +module.exports = function( exports, imports ) { + var fs = require( "fs" ); + + return imports( function( stream, render, concat, ignore ) { + return exports( function( write ) { + return stream( function( path ) { + return write( function( write, head, body ) { + return stream( function( rest ) { + return path( render( concat( function read( path ) { + fs.createReadStream( path ) + .on( "data", function( body ){ write = write( body ) }) + .on( "end", function(){ rest( write, head, body ) }) + .on( "error", function(){ rest( + write( "File not found: " + path, { status: 404 } ), head, body + )}); + + return ignore; + }, head, body ), head, body), head, body ); + }); + }); + }); + }) + }, "stream", "render", "concat", "ignore" ) +} \ No newline at end of file diff --git a/node/listen.js b/node/listen.js new file mode 100644 index 0000000..4677398 --- /dev/null +++ b/node/listen.js @@ -0,0 +1,15 @@ +module.exports = function( exports, imports ) { + return imports( function( stream, listener ) { + return exports( function( write, port ) { + return stream( function( upstream ) { + upstream( listener( function( listener ) { + require( "http" ) + .createServer( listener ) + .listen( port ); + })) + + return upstream( write ); + }); + }); + }, "stream", "node/listener" ); +} \ No newline at end of file diff --git a/node/listener.js b/node/listener.js new file mode 100644 index 0000000..e7403b5 --- /dev/null +++ b/node/listener.js @@ -0,0 +1,55 @@ +module.exports = function( exports, imports ) { + var url = require( "url" ); + + return imports( function( stream, render ) { + return exports( function( write ) { + return stream( function( upstream ) { + return write( function( req, res ) { + var status = 200 + , headers = {}; + + upstream( render( + read, + + { + method: req.method, + headers: req.headers, + url: url.parse( "//" + req.headers.host + req.url, false, true ) + }, + + function body( write ) { + req.on( "data", write ).on( "end", write ); + } + )); + + function read( body, head ) { + if ( !arguments.length ) res.end(); + + else { + if ( head ) { + if ( "status" in head ) status = head.status; + + if ( "headers" in head ) { + for ( var name in head.headers ) { + headers[ name ] = head.headers[ name ] + } + } + } + + if ( body ) { + if ( headers ) { + res.writeHead( status, headers ); + headers = undefined; + } + + res.write( body ); + } + } + + return read; + } + }); + }) + }) + }, "stream", "render" ) +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..81adcb7 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ "name" : "fab" +, "description" : "a web framework built for steaming" +, "version" : "0.5.1" +, "main" : "./index" +, "bin" : { "fab" : "./cli.js" } +, "author" : "Jed Schmidt " +, "repository" : + { "type" : "git" + , "url" : "http://github.com/jed/fab.git" + } +, "main" : "./index" +, "engines": [ "node" ] +, "licenses" : + [ { "type" : "MIT" + , "url" : "http://github.com/jed/fab/blob/master/LICENSE.txt" + } + ] +} \ No newline at end of file diff --git a/render.js b/render.js new file mode 100644 index 0000000..655da28 --- /dev/null +++ b/render.js @@ -0,0 +1,16 @@ +module.exports = function( exports ) { + return exports( function( write ) { + var args = [].slice.call( arguments ); + + return function read( obj ) { + if ( obj && obj.apply ) { + args[ 0 ] = read; + return obj.apply( undefined, args ); + } + + write = write.apply( undefined, arguments ); + + return arguments.length ? read : write; + } + }); +} \ No newline at end of file diff --git a/route.js b/route.js new file mode 100644 index 0000000..1ce241c --- /dev/null +++ b/route.js @@ -0,0 +1,41 @@ +module.exports = function( exports, imports ) { + var slice = Array.prototype.slice; + + return imports( function( stream ) { + route.capture = function( write, i ) { + return write( function( write, head ) { + var capture = head.url.capture || []; + return write( i >= 0 ? capture[ i ] : capture ); + }); + } + + return exports( route ); + + function route( write, pattern ) { + return stream( function( yes ) { + return stream( function( no ) { + return write( function( write, head, body ) { + var app = no + , url = head.url; + + url.pathname = url.pathname.replace( pattern, function() { + app = yes; + + url.capture = ( url.capture || [] ) + .concat( slice.call( arguments, 1, -2 ) ); + + return ""; + }); + + return app( function read( data ) { + if ( !arguments.length ) return write; + + write = write.apply( this, arguments ); + return read; + }, head, body ); + })(); + }); + }); + } + }, "stream" ); +}; \ No newline at end of file diff --git a/run.js b/run.js new file mode 100644 index 0000000..13cf340 --- /dev/null +++ b/run.js @@ -0,0 +1,18 @@ +module.exports = function( exports, imports ) { + return exports( function run( write, imports ) { + write = write || function x(){ return x } + + return function read( obj ) { + if ( obj && obj.apply ) { + var fn = obj; + + arguments[ 0 ] = write; + + return run( fn.apply( undefined, arguments ) ); + } + + write = write.apply( undefined, arguments ); + return read; + } + }) +} \ No newline at end of file diff --git a/sleep.js b/sleep.js new file mode 100644 index 0000000..79306a2 --- /dev/null +++ b/sleep.js @@ -0,0 +1,14 @@ +module.exports = function( exports, imports ) { + return imports( function( stream ) { + return exports( function( write, duration ) { + return write( function( write ) { + return stream( function( rest ) { + setTimeout( + function(){ return rest( write ) }, + duration || 0 + ); + }); + }); + }); + }, "stream" ); +} \ No newline at end of file diff --git a/stream.js b/stream.js new file mode 100644 index 0000000..46b97a6 --- /dev/null +++ b/stream.js @@ -0,0 +1,22 @@ +module.exports = function( exports ) { + return exports( function( write, queue ) { + queue = queue || []; + + var length = queue.length; + + function drain( write, req ) { + for ( var i = 0; i < length; ) { + write = write.apply( undefined, queue[ i++ ] ); + } + + return write(); + }; + + return function read() { + if ( !arguments.length ) return write ? write( drain ) : drain; + + queue[ length++ ] = arguments; + return read; + } + }) +} \ No newline at end of file diff --git a/write.js b/write.js new file mode 100644 index 0000000..2ee663c --- /dev/null +++ b/write.js @@ -0,0 +1,5 @@ +module.exports = function( exports, imports ) { + return exports( function( write, fn ) { + return write( fn ); + }) +} \ No newline at end of file