Chapter 12

Modules and builds

fs = require 'fs' 





12.1 Server-side modules (on Node.js)

# -- my-trip-to-the-zoo.txt ?-
My trip to the zoo
I went to the Zoo. There were animals.





Listing 12.1 File and directory structure for your blog application

|                                   
|—— app                              
|   |—— controllers                  
|   |   |—— blog.coffee
|   |   |—— controller.coffee
|   |   |—— static.coffee
|   |—— load.coffee
|   |—— models                       
|   |   |—— model.coffee
|   |   |—— post.coffee
|   |—— server.coffee
|   ⌊—— views                        
|       |—— list.coffee
|       |—— post.coffee
|       |—— view.coffee
|—— content                          
|   |—— my-trip-to-the-circus.txt
|   ⌊—— my-trip-to-the-zoo.txt





class Controller
class Blog extends Controller





coffee ?c application.coffee





# -- blog.coffee --





coffee ?c blog.coffee
# ReferenceError: Controller is not defined





require './controller'





# -- blog.coffee ?
require './controller'
class Blog extends Controller





coffee ?c blog.coffee
# ReferenceError: Controller is not defined





# -- controller.coffee ?
class Controller
exports.Controller = Controller





# -- blog.coffee ?-
controller = require './controller'
class Blog extends controller.Controller





# -- blog.coffee -?
{Controller} = require './controller'
class Blog extends Controller





module_a.coffee

w = 3
x = {a: 1}

exports.y = x 
module_b.coffee

z = require('./module_a).y

# z == {a: 1}






{Controller} = require './controller.coffee'





> coffee blog.coffee
# application is running successfully at http://localhost:8080





> coffee ?c blog.coffee
> node blog.js
controller.coffee:2
class Controller
^^^^^
...
SyntaxError: Unexpected reserved word





class Post
  posts = []
  constructor: (@title, @body) ->
    posts.push @
@all: -> posts
exports.Post = Post





{Post} = require './post'
aPost = new Post 'A Post', 'Some content' anotherPost = new Post 'Another Post', 'Some more content' Post.all().length # 2





TheSamePost = require('./post').Post
aThirdPost = new Post 'Three', 'Content three' TheSamePost.all().length
# 3
Post.all().length # 3





makePost = -> 
  class Post
    posts = []
    constructor: (@title, @body) ->
      posts.push @
@all: -> posts
{Post}
exports.makePost = makePost





{Post} = require('./post').makePost()
new Post 'A post', 'Some content'
Other = require ('./post').makePost().Post
Post.all().length
# 1
Other.all().length # 0





> coffee server.coffee





Listing 12.2 server.coffee

http = require 'http'
{load} = require './load'
{Blog} = require './controllers'
load './content'
server = new http.Server()
server.listen '8080', 'localhost'
blog = new Blog server





Listing 12.3 load.coffee

fs = require 'fs'
{Post} = require './models/post'
load = (dir) -> fs.readdir dir, (err, files) -> for file in files when /.*[.]md$/.test file fs.readFile "#{dir}/#{file}", 'utf-8', (err, data) -> [title, content...] = data.split '\n'
new Post title, content.join '\n'
exports.load = load





Listing 12.4 controllers/controller.coffee

class Controller
routes = {}
@route = (path, method) ->
routes[path] = method
constructor: (server) -> server.on 'request', (req, res) => path = require('url').parse(request.url).pathname handlers = [] for route, handler of routes if new RegExp("^#{route}$").test(path) handlers.push handler: handler matches: path.match(new RegExp("^#{route}$")) method = handlers[0]?.handler || 'default'
res.end @[method](req,res,handlers[0]?.matches.slice(1)...)
render: (view) -> @response.writeHead 200, 'Content-Type': 'text/html'
@response.end view.render()
default: (@request, @response) ->
@render render: -> 'unknown'
exports.Controller = Controller





Listing 12.5 controllers/blog.coffee

fs = require 'fs'
{Controller} = require './controller'
{Post} = require '../models'
{views} = require '../views'
class Blog extends Controller
@route '/', 'index' index: (@request, @response) => @posts = Post.all()
@render views 'list', @posts
@route '/([a-zA-Z0-9-]+)', 'show' show: (@request, @response, id) => @post = Post.get id if @post @render views 'post', @post
else ''
exports.Blog = Blog





Listing 12.6 models/model.coffee

class Model                                                      
dirify: (s) -> s.toLowerCase().replace /[^a-zA-Z0-9-]/gi, '-'
exports.Model = Model





Listing 12.7 models/post.coffee

{Model} = require './model'
class Post extends Model posts = [] constructor: (@title, @body) -> throw 'requires title' unless @title super @slug = @dirify @title
posts.push @
@all: -> posts
@get: (slug) -> (post for post in posts when post.slug is slug)[0]
@purge = ->
posts = []
exports.Post = Post





Listing 12.8 views/view.coffee

class View
  render: ->
'Lost?'
wrap: (content) -> """ <!DOCTYPE html> <html dir='ltr' lang='en-US'> <head> <meta http-equiv='Content-Type' content='text/html; charset=utf-8'> <title>Agtron's blog</title> #{content}
"""
exports.View = View





Listing 12.9 views/list.coffee

{View} = require './view'
class List extends View constructor: (@posts) -> render: -> all = (for post in @posts "<li><a href='#{post.slug}'>#{post.title}</a></li>" ).join '' @wrap """ <ul>#{all}</ul>
"""
exports.List = List





Listing 12.10 views/post.coffee

{View} = require './view'
class Post extends View constructor: (@post) -> render: -> @wrap """ <h1>#{@post.title}</h1> <div class='content'> #{@post.body} </div>
"""
exports.Post = Post





|—— utils/
|   |—— string.coffee
|   |—— array.coffee
|   |—— statistics.coffee





{trim, pad} = require 'utils/string'
{remove} = require 'utils/array'
{chebyshev} = require 'utils/stats'





exports.string = require '.utils/string'
exports.array = require '.utils/array'
exports.stats = require '.utils/stats'





{string:{trim,pad},array:{remove},stats:{chebyshev}} = require './utils/index'





{string:{trim,pad},array:{remove},stats:{chebyshev}} = require './utils' 





12.2 Build automation with Cake

> coffee ?c ?o compiled app  





> node compiled/server.js





> node compiled/tests.js





|—— lib                
|   |—— highball.coffee
|   |—— cocktail.coffee
|   |—— julep.coffee
|—— app
|   |—— punch.coffee
|   |—— fizz.coffee
|   |—— flip.coffee
|—— vendor
|   |—— mug.coffee
|   |—— beaker.coffee
|   |—— teacup.coffee
|—— resources
|   |—— reference.csv





> coffee ?c single_file_application.coffee





> cake





task 'build', 'Compile all the CoffeeScript', ->   
  console.log 'Not implemented yet'  





> cake
cake build            # Compile all the CoffeeScript





> cake build
Not implemented yet





{spawn} = require 'child_process'
task 'build', 'Compile all the CoffeeScript', -> coffee = spawn 'coffee', ['-c', '-o', "compiled/app", "app"] coffee.on 'exit', (code) -> console.log 'Build complete'





> cake build
# Build complete 





|—— app                           
|   |—— controllers               
|   |   |—— blog.coffee           
|   |   |—— controller.coffee     
|   |   |—— static.coffee         
|   |—— load.coffee               
|   |—— models                    
|   |   |—— model.coffee          
|   |   |—— post.coffee           
|   |—— server.coffee             
|   |—— views                     
|   |   |—— list.coffee           
|   |   |—— post.coffee           
|   |   |—— view.coffee           
|—— compiled                   
|   |—— controllers            
|   |   |—— blog.js            
|   |   |—— controller.js      
|   |   |—— static.js          
|   |—— load.js                
|   |—— models                 
|   |   |—— model.js           
|   |   |—— post.js            
|   |—— server.js              
|   |—— views                  
|   |   |—— list.js            
|   |   |—— post.js            
|   |   |—— view.js     





task 'test', 'Run all the tests', ->
  console.log 'No tests'





|—— Cakefile               
|—— app
|   |—— # application files are here
|—— spec
|   |—— # test files are here





Listing 12.11 A specification for the Post class

{describe, it} = require 'chromic'        
{Post} = require '../../app/models/post'
describe 'Post', -> post = new Post 'A post', 'with contents'
another = new Post 'Another post', 'with contents'
it 'should return all posts', ->
Post.all().length.shouldBe 2
it 'should return a specific post', -> Post.get(post.slug).shouldBe 'a-post'





Listing 12.12 Part of a Cakefile with build and test tasks

# See listing 12.19 for the complete Cakefile this is part of
compile (directory) = ->
coffee = spawn 'coffee', ['-c', '-o', "compiled/#{directory}", directory]
coffee.on 'exit', (code) ->
console.log 'Build complete'
clean = (path, callback) ->
exec "rm -rf #{path}", -> callback?()
forAllSpecsIn = (dir, fn) -> execFile 'find', [ dir ], (err, stdout, stderr) -> fileList = stdout.split '\n' for file in fileList
fn file if /_spec.js$/.test file
runSpecs = (folder) -> forAllSpecsIn folder, (file) ->
require "./#{file}"
task 'build', 'Compile the application', -> clean 'compiled', -> compile 'app', ->
'Build complete'
task 'test' , 'Run the tests', -> clean 'compiled', -> compile 'app', -> compile 'spec', -> runSpecs 'compiled', -> console.log 'Tests complete'





task 'build', ->
task 'test', -> invoke 'build'





task 'build', ->
console.log 'built'
task 'test', ->
invoke 'build'
task 'deploy', -> invoke 'build' invoke 'test'





> cake deploy
# built
# built





built = false
task 'build', -> return if built built = true





12.3 Client-side modules (in a web browser)

|—— Cakefile               
|   |—— app                                    
|   |   |—— # application files are here       
|   |—— client                                 
|   |   |—— # Put your files here (Scruffy)    
|   |—— spec                                   
|   |   |—— # test files are here  





@app.controllers = do -> 
  controller = do ->
  blog = do ->





@app = loadSomeNefariousProgram()





# -- main.coffee --
{Comments} = require 'comments'
# <rest of module omitted>
# -- comments.coffee --
class Comments
# <rest of module omitted>
exports.Comments = Comments





task 'concatenate', 'Compile multiple CoffeeScript files', ->  





(function() {                       
  var Comments;                     
  Comments = (function() {          
    function Comments() {}          
    return Comments;                
  })();                             
  exports.Comments = Comments;      
}).call(this);
(function() { var Comments; Comments = require('comments').Comments; }).call(this);





defmodule({'main': function (require, exports) {        
  var Comments = require('./comments').Comments;
}});
defmodule({'comments': function (require, exports) { var Comments; Comments = (function() { function Comments() {} return Comments;
})();
exports.Comments = Comments; }});





Listing 12/13 require and defmodule for the browser (lib/modules.coffee)

do ->                 
  modules = {}
  cache = {}
  @require = (raw_name) ->      
    name = raw_name.replace /[^a-z]/gi, ''
    return cache[name].exports if cache[name]
    if modules[name]
      module = exports: {}
      cache[name] = module
      modules[name]((name) ->
        require name
      , module.exports)
      module.exports
else throw "No such module #{name}"
@defmodule = (bundle) -> for own key of bundle modules[key] = bundle[key]





defmodule('comments': function(require, exports) {
  class Comments
  exports.Comments = Comments;
});
defmodule('main': function(require, exports) { var Comments = require('./comments').Comments;
});
require './main'





Listing 12.14 Cake task for client-side modules

task 'build:client', 'build client side stuff with modules', ->
  compiler = require 'coffee-script'
modules = fs.readFileSync "lib/modules.coffee", "utf-8"
modules = compiler.compile modules, bare: true files = fs.readdirSync 'client' source = (for file in files when /\.coffee$/.test file module = file.replace /\.coffee/, ''
fileSource = fs.readFileSync "client/#{file}", "utf-8"
""" defmodule({#{module}: function (require, exports) { #{compiler.compile(fileSource, bare: true)} }}); """
).join '\n\n'
out = modules + '\n\n' + source fs.writeFileSync 'compiled/app/client/application.js'





Listing 12.15 Comments specification (spec/comments_spec.coffee)

{describe, it} = require 'chromic'
{Comments} = require '../../app/client/comments'
describe 'Comments', -> it 'should post a comment to the server', -> requested = false httpRequest = (url) -> requested = url comments = new Comments 'http://the-url', {}, httpRequest comment = 'Hey Agtron. Nice site.' comments.post comment
requested.shouldBe "http://the-url/comments?insert=#{comment}"
it 'should fetch the comments when constructed', -> requested = false httpRequest = (url) -> requested = url comments = new Comments 'http://the-url', {}, httpRequest
requested.shouldBe "http://the-url/comments"
it 'should bind to event on the element', -> comments = new Comments 'http://the-url', {}, -> element = querySelector: -> element
value: 'A comment from Scruffy'
comments.bind element, 'post' postReceived = false comments.post = (comment) -> postReceived = comment element.onpost()
postReceived.shouldBe element.value
it 'should render comments to the page as a list', -> out = innerHTML: (content) -> renderedContent = content comments = new Comments 'http://the-url', out, -> comments.render '["One", "Two", "Three"]' out.innerHTML.shouldBe "<ul><li>One</li><li>Two</li><li>Three"





Listing 12.16 The Comments module (client/comments.coffee)

class Comments
  constructor: (@url, @out, @httpRequest) ->
    @httpRequest "#{@url}/comments", @render
  post: (comment) ->
    @httpRequest "#{@url}/comments?insert=#{comment}", @render
  bind: (element, event) ->
    comment = element.querySelector 'textarea'
    element["on#{event}"] = =>
      @post comment.value
      false
  render: (data) =>
    inLi = (text) -> "<li>#{text}</li>"
    if data isnt ''
      comments = JSON.parse data
      if comments.map?
        formatted = comments.map(inLi).join ''
@out.innerHTML = "<ul>#{formatted}</ul>"
exports.Comments = Comments





12.4 Application deployment

> node compiled/app/server.js





createArtifact = (path, version, callback) ->
  execFile "tar", ["-cvf", "artifact.#{version}.tar", path], (e, d) ->  
callback()
task 'artifact', 'build the artifact', -> version = fs.readFileSync './VERSION', 'utf-8' createArtifact 'compiled', version, -> console.log "done. artifact.#{version}.tar generated"





> cake artifact
# done. artifact.1.tar generated





server.listen '8080', 'localhost'





server.listen '80, 'agtronsblog.com'





if process.env.NODE_ENV is 'production'          
  server.listen '80', 'agtronsblog.com'          
else if process.env.NODE_ENV is 'development'    
  server.listen '8080', 'localhost' 





Listing 12.17 The blog application (server.coffee)

http = require 'http'
{load} = require './load'
{Blog} = require './controllers'
load './content'
config = require('./config')[process.env.NODE_ENV]
server = new http.Server()
server.listen config.port, config.host
blog = new Blog server





Listing 12.18 The blog application configuration file (config.coffee)

config =
  development:
    host: 'localhost'
    port: '8080'
  production:
    host: 'agtronsblog.com'
port: '80'
for key, value of config exports[key] = value





> NODE_ENV=development node compiled/server.js





> NODE_ENV=production node compiled/server.js





task 'production:deploy', 'deploy the application to production' ->
  VERSION = fs.readFileSync('./VERSION', 'utf-8')
  SERVER = require('./app/config').production.host
  clean 'compiled', -> 
    compile 'app', -> 
      copy 'content', 'compiled', -> 
        createArtifact 'compiled', VERSION, ->   
       execFile 'scp', [
         "artifact.#{VERSION}.tar",
         "#{SERVER}:~/."
       ], (err, data) ->
            console.log "Uploaded artifact #{VERSION} to #{SERVER}"





12.5 The final Cakefile

> cake
cake clean                    # delete existing build
cake build                    # run the build
cake test                     # run the tests
cake development:start        # start the application locally





Listing 12.19 The Cakefile for the blog application (Cakefile)

s = require 'fs'
{exec, execFile} = require 'child_process'
buildUtilities = require './build_utilities'
{ clean, compile, copy, createArtifact, runSpecs, runApp
} = buildUtilities.fromDir './'
VERSION = fs.readFileSync('./VERSION', 'utf-8')
task 'clean', 'delete existing build', -> execFile "npm", ["install"], ->
clean "compiled"
task 'build', 'run the build', -> clean 'compiled', -> compile 'app', -> copy 'content', 'compiled', -> createArtifact 'compiled', VERSION, ->
console.log 'Build complete'
task 'test' , 'run the tests', -> clean 'compiled', -> compile 'app', -> compile 'spec', -> runSpecs 'compiled', ->
console.log 'Tests complete'
task "development:start", "start on development", ->
runApp 'development'
SERVER = require('./app/config').production.host
deploy = -> console.log "Deploy..." tarOptions = ["-cvf","artifact.#{VERSION}.tar","compiled"] execFile "tar", tarOptions,(err, data) -> console.log '1. Created artifact' execFile 'scp', [ "artifact.#{VERSION}.tar", "#{SERVER}:~/." ], (err, data) -> console.log '2. Uploaded artifact' exec """ ssh #{SERVER} 'cd ~/; rm -rf compiled; tar -xvf artifact.#{VERSION}.tar; cd ~/compiled; NODE_ENV=production nohup node app/server.js &' & """, (err, data) -> console.log '3. Started server'
console.log 'Done'
task "production:deploy", "deploys the app to production", -> clean 'compiled', -> compile 'app', -> copy 'content', 'compiled', -> createArtifact 'compiled', VERSION, -> deploy()





Listing 12.20 Build utilities (build_utilities.coffee)

fs = require 'fs'
{spawn, exec, execFile, fork} = require 'child_process'
clientCompiled = false
forAllSpecsIn = (dir, fn) -> execFile 'find', [ dir ], (err, stdout, stderr) -> fileList = stdout.split '\n' for file in fileList
fn file if /_spec.js$/.test file
compileClient = (callback) -> return callback() if clientCompiled clientCompiled = true compiler = require 'coffee-script' modules = fs.readFileSync "lib/modules.coffee", "utf-8" modules = compiler.compile modules, bare: true
files = fs.readdirSync 'client'
fs.mkdirSync "compiled/app/client" source = (for file in files when /\.coffee$/.test file module = file.replace /\.coffee/, '' fileSource = fs.readFileSync "client/#{file}", "utf-8" fs.writeFileSync "compiled/app/client/#{module}.js", compiler.compile fileSource """ defmodule({#{module}: function (require, exports) { #{compiler.compile(fileSource, bare: true)} }}); """
).join '\n\n'
out = modules + '\n\n' + source
fs.writeFileSync 'compiled/app/client/application.js', out
callback?()
exports.fromDir = (root) ->
return unless root
compile = (path, callback) ->
coffee = spawn 'coffee', ['-c', '-o', "#{root}compiled/#{path}", path]
coffee.on 'exit', (code, s) -> if code is 0 then compileClient callback
else console.log 'error compiling'
coffee.on 'message', (data) ->
console.log data
createArtifact = (path, version, callback) -> execFile "tar", ["-cvf", "artifact.#{version}.tar", path], (e, d) ->
callback?()
runSpecs = (folder) -> forAllSpecsIn "#{root}#{folder}", (file) ->
require "./#{file}"
clean = (path, callback) ->
exec "rm -r #{root}#{path}", (err) -> callback?()
copy = (src, dst, callback) -> exec "cp -R #{root}#{src} #{root}#{dst}/.", ->
callback?()
runApp = (env) -> exec 'NODE_ENV=#{env} node compiled/app/server.js &', ->
console.log "Running..."
{clean, compile, copy, createArtifact, runSpecs, runApp}





task 'build', 'run the build', ->                 
  clean 'compiled', ->                            
    compile 'app', ->                             
      copy 'content', 'compiled', ->              
        createArtifact 'compiled', VERSION, ->    
          console.log 'Build complete'





build: artifact
artifact: clean compile copy tar ?cvf artifact.tar compiled





task 'build', 'run the build', ->
  clean('compiled')
  .then(compile 'app')              
  .then(copy 'content', 'compiled') 
  .then(createArtifact 'compiled', VERSION)
  .then(-> console.log 'done')
  .run()





task 'build', 'run the build', depends ['clean', 'compile', 'copy'], -> 





12.6 Summary