Created
October 22, 2012 06:27
-
-
Save waterlou/3929970 to your computer and use it in GitHub Desktop.
Cakefile for easily compile/watch projects with coffee/less/styles scripts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### | |
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