Build tool: Coffeescript/Node project with multiple components

I'm starting a project at work and was wondering what the best build tool to use would be.

The whole thing is written in CoffeeScript, using AngularJS for the client-side and NodeJS for the server.

There are several components to the app:

  • An iPad app
  • An iPhone app (different functionality from the ipad)
  • A CMS for the apps
  • A NodeJS server

There is tons of shared code between all these, again all written in CoffeeScript.

I'd like a build tool where I can list which app uses what code (much of it shared) and it would build each app's javascript files into a seperate folder.

For example, I would setup a folder called '/compiled/ipad/' which has index.html, and folders for js, css, img, etc. I would list what compiled coffee files I want thrown into /compiled/ipad/js (some of it from /src/shared/*.coffee, some of it from /src/ipad/*.coffee, etc) and what files I want thrown into /compiled/ipad/css. I would want it to be able to easily concatenate files how I want, too.

It would also compile my tests, from /src/test/ipad into /compiled/test/ipad/*.js.

All my client-side unit tests are written using testacular and I'm not sure what I'll write the server-side unit tests in yet.

What build tool/configuration is the best approach here? A Makefile? Something like Grunt? I'm honestly new to the whole build scene.

edit: Decided to go with Browserify. You can find my solution to make it work with Angular here: https://groups.google.com/forum/#!topic/angular/ytoVaikOcCs

Personally I think the drive to write server side code in javascript or coffeescript extends to your build tool chain as well: so stick with using javascript/coffeescript there too. This will allow you to easily automate your server/client tasks from your build tool - I doubt it'd be meaningfully possible with another tool like make (you'd just be writing wrappers around node.js command calls). Suggestions, ordered by structured-ness:

  • node.js : Just roll your build scripts in javascript, and invoke them with node. Akin to shell scripts, I suppose. I don't recommend this route.
  • jake or cake : I'm from the java world, so its not surprising that these kinda remind me of ant. I prefer coffeescript, and hence prefer cake.
  • grunt : I hadn't heard of this before so I can't give much advice. It reminds me of maven, of course...and I can just say...the more structure a build tool tends to enforce the less flexible it can be. Its something of a trade off. As long as you do it 'the build tool' way you can save tons of time. But if you have app specific problems, it can be one royal pain to resolve.

Of course, you can go with some other build tool you are already familiar with from some other language: rake, maven, ant, gradle, etc etc.

I did almost this exact thing all in a Cakefile using node modules as needed.

Set some global variables that are arrays with the path of each file, concatenate those files into a file in the compiled directory that you specify, then compile that one file into js.

For the styles, same thing with the concatenation without the compiling, obviously.

fs = require 'fs'
path = require 'path'
{spawn, exec} = require 'child_process'
parser = require('uglify-js').parser
uglify = require('uglify-js').uglify
cleanCss = require 'clean-css'

coffees = 
 [
  "/src/shared/file1.coffee"
  "/src/shared/file2.coffee"
  "/src/ipad/file1.coffee"
 ]

tests = 
  [
   "/src/ipad/tests.coffee"
  ]

styles = 
 [
  "/src/ipad/styles1.css"
  "/src/shared/styles2.css"
 ]

concatenate = (destinationFile, files, type) ->
  newContents = new Array
  remaining = files.length
  for file, index in files then do (file, index) ->
      fs.readFile file, 'utf8', (err, fileContents) ->
          throw err if err
          newContents[index] = fileContents
          if --remaining is 0
              fs.writeFile destinationFile, newContents.join '\n\n', 'utf8', (err) ->
                throw err if err
              if type is 'styles'
                 minifyCss fileName
              else
                 compileCoffee fileName


 compileCoffee = (file) ->
    exec "coffee -c #{file}", (err) ->
       throw err if err
       # delete coffee file leaving only js
       fs.unlink 'path/specifying/compiled_coffee', (err) -> 
          throw err if err
          minifyJs file

 minifyJs = (file) ->
  fs.readFile f, 'utf8', (err, contents) ->
      ast = parser.parse contents
      ast = uglify.ast_mangle ast 
      ast = uglify.ast_squeeze ast
      minified = uglify.gen_code ast

      writeMinified file, minified

writeMinified = (file, contents) ->
   fs.writeFile file, contents, 'utf8', (err) -> throw err if err  


minifyCss = (file) ->
    fs.readFile file, 'utf8', (err, contents) ->
    throw err if err
    minimized = cleanCss.process contents
    clean = minimized.replace 'app/assets', ''

    fs.writeFile file, clean, 'utf8', (err) ->
        throw err if err


task 'compile_coffees', 'concat, compile, and minify coffees', ->
  concatenate '/compiled/ipad/code.coffee', coffees, 'coffee'

task 'concat_styles', 'concat and minify styles', ->
  concatenate '/compiled/ipad/css/styles.css', styles, 'styles'

task 'compile_tests', 'concat, compile, and minify test', ->
  concatenate '/compiled/ipad/tests.coffee', tests, 'tests'

Now this is roughly what I think you're asking for.

Could definitely be prettier, especially having a separate function for writing the minified contents, but it works.

Not perfect for the styles either because I was using Sass and had other functions before it hit the minified function, but I think you get the idea.

I would put all the shared code into Node.js modules and create a project that looks something like the following:

Project
|~apps/
| |~cms/
| | `-app.js
| |~ipad/
| | `-app.js
| |~iphone/
| | `-app.js
| `~node/
|   `-app.js
|~libs/
| |-module.js
| `-module2.js
|~specs/
| |~cms/
| | `-app.js
| |~ipad/
| | `-app.js
| |~iphone/
| | `-app.js
| `~node/
|   `-app.js
| `~libs/
|   |-module.js
|   `-module2.js
`-Makefile

I would then use something like Browserify (there are others) to make the client-side apps where needed. That way instead of having a build file where you say what you need you actually have real apps importing modules.