Skip to content

Instantly share code, notes, and snippets.

@waterlou
Created October 22, 2012 06:27
Show Gist options
  • Save waterlou/3929970 to your computer and use it in GitHub Desktop.
Save waterlou/3929970 to your computer and use it in GitHub Desktop.
Cakefile for easily compile/watch projects with coffee/less/styles scripts
###
Cakefile for compiling coffee and less
Author: waterlou
v0.1: initial launch
###
fs = require 'fs'
{spawn,exec} = require 'child_process'
try
which = require('which').sync
catch err
if process.platform.match(/^win/)?
console.log 'WARNING: the which module is required for windows\ntry: npm install which'
which = null
###
# Paths for coffeescript and less
# multiple pairs of folders are supported for coffeescripts so that
# you can put client and server script in different folders
# and the coffeescript folder will be checked recursively
# for less script, only one pair of folders and flat tree is supported
###
coffeepaths = [['src', 'lib'], ['scripts_src', 'public/js']]
lesspath = ['less', 'public/css']
styluspath = ['stylus', 'public/css']
coffeeOptions = ["--bare"]
lesscOptions = ['-x'] # minimized
stylusOptions = ['-c'] # compressed
# ANSI Terminal Colors
bold = '\x1b[0;1m'
green = '\x1b[0;32m'
red = '\x1b[0;31m'
blue = '\x1b[0;34m'
yellow = '\x1b[0;33m'
reset = '\x1b[0m'
# ## *log*
#
# **given** string as a message
# **and** string as a color
# **and** optional string as an explaination
# **then** builds a statement and logs to console.
#
log = (message, color, explanation) -> console.log color + message + reset + ' ' + (explanation or '')
task 'watch', 'Watch for all coffeescript and less changes', ->
log "Watching for code changes to:", bold
# watch coffee
for [coffeedir, jsdir] in coffeepaths
watchCoffeeDir coffeedir, jsdir
# watch less
[lessdir, cssdir] = lesspath
addWatchDir lessdir, cssdir, compileLess, isLessFile
walk lessdir, cssdir, (path, destPath, file, isdir) ->
if isdir
addWatchDir "#{path}/#{file}", "#{destPath}/#{file}", compileLess, isLessFile
else if isLessFile file
parts = file.split('.', 1)
compileLess(destPath, path, parts[0], false)
log "Watching #{path}/#{file}", green
addWatch path, "#{path}/#{file}", destPath, compileLess
# watch stylus
[stylusdir, cssdir] = styluspath
addWatchDir stylusdir, cssdir, compileStylus, isStylusFile
walk stylusdir, cssdir, (path, destPath, file, isdir) ->
if isdir
addWatchDir "#{path}/#{file}", "#{destPath}/#{file}", compileStylus, isStylusFile
else if isStylusFile file
parts = file.split('.', 1)
compileStylus(destPath, path, parts[0], false)
log "Watching #{path}/#{file}", green
addWatch path, "#{path}/#{file}", destPath, compileStylus
log "Performed initial compile. Ready and waiting for changes", bold
task 'build', 'Compile all coffeescripts and less scripts', ->
compileAllCoffees()
compileAllLesses()
compileAllStyluses()
task 'build:coffee', 'Compile all coffeescripts to js', ->
compileAllCoffees()
task 'build:less', 'Compile all less scripts to css', ->
compileAllLesses()
task 'build:stylus', 'Compile all stylus scripts to css', ->
compileAllStyluses()
task 'clean', 'remove all generated files', ->
log "Cleaning generated js and css", bold
for [coffeedir, jsdir] in coffeepaths
cleanCoffeeDir coffeedir, jsdir
[lessdir, cssdir] = lesspath
walk lessdir, cssdir, (path, destPath, file, isdir) ->
if not isdir and isLessFile file
parts = file.split('.', 1)
filePath = "#{destPath}/#{parts[0]}.css"
log "Cleaning #{filePath}", yellow
fs.unlink filePath
[stylusdir, cssdir] = styluspath
walk stylusdir, cssdir, (path, destPath, file, isdir) ->
if not isdir and isStylusFile file
parts = file.split('.', 1)
filePath = "#{destPath}/#{parts[0]}.css"
log "Cleaning #{filePath}", yellow
fs.unlink filePath
# run server in debug mode
task 'debug', 'Run development server', ->
process.env.NODE_ENV = 'development'
launch 'node', ['lib/app.js']
# run server in production mode
task 'start', 'Start production server', ->
process.env.NODE_ENV = 'production'
launch 'forever', ['start', 'lib/app.js']
task 'stop', 'Stop production server', ->
launch 'forever', ['stop', 'lib/app.js']
# Cakefile Tasks
#
# ## *docs*
#
# Generate Annotated Documentation
#
# <small>Usage</small>
#
# ```
# cake docs
# ```
task 'docs', 'generate documentation', -> docco()
# ## *test*
#
# Runs your test suite.
#
# <small>Usage</small>
#
# ```
# cake test
# ```
task 'test', 'run tests', -> mocha -> log ":)", green
##### storing watching files, so that won't duplicate
watchList = {}
# unwatch all files
unwatchDirectory = (dirPath) ->
dirObject = watchList[dirPath]
if dirObject
files = Object.keys dirObject
files.forEach (file) ->
fs.unwatchFile file
delete watchList[dirPath]
# check if a file is watching
isWatchingFile = (dirPath, filePath) ->
dirObject = watchList[dirPath]
return true if dirObject and dirObject[filePath]
return false
isWatchingDirectory = (dirPath) ->
return watchList[dirPath]
# watch file
addWatch = (dirPath, filePath, destPath, callback) ->
#util.log "#{dirPath} - #{filePath} - #{destPath}"
watchList[dirPath] = {} if not watchList[dirPath]
watchList[dirPath][filePath] = true # add to list
fs.watchFile filePath, (curr, prev) ->
if +curr.mtime isnt +prev.mtime
filename = filePath.split('/').last().split('.', 1)
log "Updating file #{filePath}", blue
callback destPath, dirPath, filename, false
# watch the dir for new files
addWatchDir = (dirPath, destPath, callback, fileCheck) ->
fs.watchFile dirPath, (curr, prev) ->
if +curr.mtime isnt +prev.mtime
log "Checking folder #{dirPath}", green
fs.readdir dirPath, (err, list) ->
list.forEach (file) ->
path = dirPath + '/' + file
stat = fs.statSync path
if stat and stat.isFile() and fileCheck file
if not isWatchingFile dirPath, path
log "Watching newly created file #{path}", green
filename = path.split('/').last().split('.', 1)
callback destPath, dirPath, filename, false
addWatch dirPath, path, destPath, callback
watchCoffeeDir = (_coffeedir, _jsdir) ->
addWatchDir _coffeedir, _jsdir, compileCoffee, isCoffeeFile
walk _coffeedir, _jsdir, (path, destPath, file, isdir) ->
if isdir
addWatchDir "#{path}/#{file}", "#{destPath}/#{file}", compileCoffee, isCoffeeFile
else if isCoffeeFile file
parts = file.split('.', 1)
compileCoffee(destPath, path, parts[0], false)
log "Watching #{path}/#{file}", green
addWatch path, "#{path}/#{file}", destPath, compileCoffee
cleanCoffeeDir = (_coffeedir, _jsdir) ->
walk _coffeedir, _jsdir, (path, destPath, file, isdir) ->
if not isdir and isCoffeeFile file
parts = file.split('.', 1)
filePath = "#{destPath}/#{parts[0]}.js"
log "Cleaning #{filePath}", yellow
fs.unlink filePath
compileAllLesses = ->
[lessdir, cssdir] = lesspath
compileAllLess cssdir, lessdir
compileAllStyluses = ->
[stylusdir, cssdir] = styluspath
compileAllStylus cssdir, stylusdir
# compile all coffee files, if jsdir/coffeedir is an array, compile all
compileAllCoffees = ->
for [coffeedir, jsdir] in coffeepaths
compileAllCoffee jsdir, coffeedir
# compile all coffee files
compileAllCoffee = (destdir, srcdir) ->
checkFolderExists destdir
options = coffeeOptions.concat ['-c', '-o', destdir, srcdir]
launch "coffee", options
# compile all less files
compileAllLess = (destdir, srcdir) ->
walk srcdir, destdir, (path, destPath, file, isdir) ->
if not isdir and (/(.*)\.(less)/i.test(file))
parts = file.split('.', 1)
exec "mkdir -p #{destPath}"
compileLess(destPath, path, parts[0], true)
# compile all stylus files
compileAllStylus = (destdir, srcdir) ->
walk srcdir, destdir, (path, destPath, file, isdir) ->
if not isdir and (/(.*)\.(styl)/i.test(file))
parts = file.split('.', 1)
exec "mkdir -p #{destPath}"
compileStylus(destPath, path, parts[0], true)
#check if source file is more update than the dest file
isSourceUpdate = (destDir, srcDir, file, ext, destExt) ->
try
srcstat = fs.statSync("#{srcDir}/#{file}.#{ext}")
catch error
return true
try
deststat = fs.statSync("#{destDir}/#{file}.#{destExt}")
catch error
return false
return +srcstat.mtime <= +deststat.mtime
checkFolderExists = (dir) ->
if not fs.existsSync dir
fs.mkdirSync(dir, 0o0755)
#execute command
compileCoffee = (jsdir, coffeedir, file, force) ->
# skip compile file is dest file is more update than the source file
if not force and isSourceUpdate jsdir, coffeedir, file, 'coffee', 'js'
#util.log "Skip compiling #{coffeedir}/#{file}"
return
checkFolderExists jsdir
# exec the command
options = coffeeOptions.concat ['-o', jsdir, "#{coffeedir}/#{file}.coffee"]
launch "coffee", options
compileLess = (cssdir, lessdir, file, force) ->
# skip compile file is dest file is more update than the source file
if not force and isSourceUpdate cssdir, lessdir, file, 'less', 'css'
#util.log "Skip compiling #{lessdir}/#{file}"
return
# exec the command
options = lesscOptions.join(' ')
exec "lessc #{options} #{lessdir}/#{file}.less > #{cssdir}/#{file}.css", (error, stdout, stderr) ->
log stdout, green if stdout
log stderr, red if stderr
compileStylus = (cssdir, stylusdir, file, force) ->
# skip compile file is dest file is more update than the source file
if not force and isSourceUpdate cssdir, stylusdir, file, 'styl', 'css'
#util.log "Skip compiling #{lessdir}/#{file}"
return
# exec the command
options = stylusOptions.join(' ')
exec "stylus #{options} #{stylusdir}/#{file}.styl > #{cssdir}/#{file}.css", (error, stdout, stderr) ->
log stdout, green if stdout
log stderr, red if stderr
# Walk through directory
# optional destdir that will walk through the destdir to the callback as well
walk = (dir, destdir, callback) ->
fs.readdir dir, (err, list) ->
list.forEach (file) ->
path = dir + '/' + file
stat = fs.statSync path
if stat and stat.isDirectory()
callback dir, destdir, file, true
dpath = null
dpath = destdir + '/' + file if destdir
walk path, dpath, callback
else
#console.log path
callback dir, destdir, file, false
# Internal Functions
#
# ## *walk*
#
# **given** string as dir which represents a directory in relation to local directory
# **and** callback as done in the form of (err, results)
# **then** recurse through directory returning an array of files
#
# Examples
#
# ``` coffeescript
# walk 'src', (err, results) -> console.log results
# ```
walkUnused = (dir, done) ->
results = []
fs.readdir dir, (err, list) ->
return done(err, []) if err
pending = list.length
return done(null, results) unless pending
for name in list
file = "#{dir}/#{name}"
try
stat = fs.statSync file
catch err
stat = null
if stat?.isDirectory()
walk file, (err, res) ->
results.push name for name in res
done(null, results) unless --pending
else
results.push file
done(null, results) unless --pending
# ## *launch*
#
# **given** string as a cmd
# **and** optional array and option flags
# **and** optional callback
# **then** spawn cmd with options
# **and** pipe to process stdout and stderr respectively
# **and** on child process exit emit callback if set and status is 0
launch = (cmd, options=[], callback) ->
cmd = which(cmd) if which
app = spawn cmd, options, { customFds: [0,1,2] }
app.on 'exit', (status) -> callback?() if status is 0
# ## *moduleExists*
#
# **given** name for module
# **when** trying to require module
# **and** not found
# **then* print not found message with install helper in red
# **and* return false if not found
moduleExists = (name) ->
try
require name
catch err
log "#{name} required: npm install #{name}", red
false
# ## *mocha*
#
# **given** optional array of option flags
# **and** optional function as callback
# **then** invoke launch passing mocha command
mocha = (options, callback) ->
if moduleExists('mocha')
if typeof options is 'function'
callback = options
options = []
# add coffee directive
options.push '--compilers'
options.push 'coffee:coffee-script'
launch 'mocha', options, callback
# ## *docco*
#
# **given** optional function as callback
# **then** invoke launch passing docco command
docco = (callback) ->
if moduleExists('docco')
walk 'src', (err, files) -> launch 'docco', files, callback
# Helpers
String::endsWith= (str) -> this.substr(this.length - str.length) == str
Array::last= -> this[this.length-1]
# Filetype checker
isCoffeeFile = (file) ->
return (/(.*)\.(coffee)$/i.test(file))
isLessFile = (file) ->
return (/(.*)\.(less)$/i.test(file))
isStylusFile = (file) ->
return (/(.*)\.(styl)$/i.test(file))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment