Skip to content

Instantly share code, notes, and snippets.

@mattmccray
Created October 20, 2010 19:42
Show Gist options
  • Save mattmccray/637154 to your computer and use it in GitHub Desktop.
Save mattmccray/637154 to your computer and use it in GitHub Desktop.
CoffeeScript and LESS build tool
#
# Web App Bundler Util v1.2
# by M@ McCray
# url http://gist.github.com/637154
#
# I built this for how I like to work, so YMMV.
#
# This script defines one task: 'build'. It will assemble multiple .coffee (or .less)
# files and compile them into a single .js (or .css) file. There are two main ways
# you can use it. Define the target and list all the source files to assemble into
# it. Or you can add special comments in your source that it will look for and
# automatically load (and keep track of, if using --watch). Example:
#
# #!require utils
#
# This will load, and optionally watch for changes to, a utils.coffee file located in the
# same directory as the source file.
#
# Define the bundles to assemble and compile here, using the format:
# "OUTPUT_FILENAME.js|css": [ "MAIN_ENTRY_FILE.coffee|less", ... ]
BUNDLES=
"app.js": "src/coffee/app.coffee"
"theme.css": "src/less/theme.less"
# Default options, if they are supplied on the cmdline
DEFAULT_OPTIONS=
watch: no
compress: no
inplace: no
quiet: no
preserve: no
yuic: "/usr/bin/java -jar $HOME/Library/bin/yuicompressor-2.4.2.jar"
option '-w', '--watch', 'Watch files for change and automatically recompile'
option '-c', '--compress', 'Compress files via YUIC'
option '-q', '--quiet', 'Suppress STDOUT messages'
option '-i', '--inplace', 'Compress files in-place'
option '-p', '--preserve', 'Saves assembled source files'
option '-y', '--yuic [CMD]', 'Command for running YUIC'
# ==============================================================
# = You shouldn't need to change anything beyond this point... =
# ==============================================================
# TODO:
# - If X.coffee isn't found, try X.js (or less/css, case depending)
# - Don't compile required .js files (enclose with backticks?)
# - Support some sort of #!require folder/* (or just folder/ ?)
# - Prevent recursive includes
# - Use UglifyJS for compression
fs = require('fs')
cs = require('coffee-script')
less = require('less')
{exec} = require 'child_process'
class BaseFile
constructor: (filepath, @options)->
@filepath = filepath.trim()
path_info = @filepath.split("/")
@filename = path_info.pop()
@path = path_info.join("/")
path_info = @filename.split('.')
@ext = "."+ path_info.pop()
getPathFor: (file, leaveExt)->
file = file.trim()
source_file = if file[0] == '/' then file[1..] else "#{@path}/#{file}"
unless leaveExt
source_file += @ext unless source_file[-(@ext.length)..] == @ext
source_file
log: ->
console.log( arguments... ) unless @options.quiet
dir: ->
console.dir( arguments... ) unless @options.quiet
err: ->
console.log( arguments... )
class SourceLine
requireParser: /(\#|\/\/)+\!require (.*)/
embedParser: /([^#|.]*)(#|\/\/)+\!embed\((.*)\)(.*)/
base64Parser: /([^#|.]*)(#|\/\/)+\!base64\((.*)\)(.*)/
constructor: (@line, @file) ->
@type = if @line.match(@requireParser)
'require'
else if @line.match(@embedParser)
'embed'
else if @line.match(@base64Parser)
'base64'
else
'string'
@build()
build: ->
switch @type
when 'require'
[src, comment, file] = @line.match(@requireParser)
path = @file.getPathFor(file)
@source_file = SourceFile.findOrCreate path, @file.bundle, @file.options
when 'embed'
match = @line.match(@embedParser)
[src, @pre, comment, file, @post] = match
path = @file.getPathFor(file, yes)
@source_file = SourceFile.findOrCreate path, @file.bundle, @file.options
when 'base64'
[src, comment, file] = @line.match(@base64Parser)
@source_file = file
toString: ->
switch @type
when 'require' then @source_file.toString()
when 'embed' then @source_file.toString(@pre, @post)
when 'base64' then ''
else @line
class SourceFile extends BaseFile
constructor: (filepath, @bundle, options) ->
super filepath, options
@watched= no
@bundles= [@bundle]
@parse()
parse: ->
contents = fs.readFileSync @filepath, 'utf8'
delete @body
@body = []
for line in contents.split "\n"
@body.push( new SourceLine( line, this ) )
@log " : #{@filepath}"
@watch()
this
toString: (pre, post)->
lines = line.toString() for line in @body
content = lines.join "\n"
if pre?
"#{pre}#{content}#{post}"
else
content
fileChanged: (curr, prev)->
changed_keys = []
for key, value of curr
changed_keys.push key if prev[key] != curr[key]
if 'size' in changed_keys
@parse()
for bundle in @bundles
try
bundle.build(@watch)
catch err
@log "Error building bundle #{@bundle.filename}"
@dir err
setTimeout @watch, 1
else
setTimeout @watch, 1
watch: =>
return if not @options.watch or @watched
fs.watchFile @filepath, persistent:true, interval:1000, (curr, prev) =>
fs.unwatchFile @filepath
@watched= no
try
@fileChanged(curr, prev)
catch err
@log err
null
@watched= yes
@cache: {}
@findOrCreate: (filepath, @bundle, options) ->
if filepath of SourceFile.cache
source_file = SourceFile.cache[filepath]
source_file.bundles.push(@bundle) unless @bundle in source_file.bundles
source_file
else
source_file = new SourceFile filepath, @bundle, options
SourceFile.cache[filepath] = source_file
source_file
class Bundle extends BaseFile
constructor: (filepath, sourcelist, options) ->
super filepath, options
@sourcelist = if typeof sourcelist == 'string' then [ sourcelist ] else sourcelist
@sources = []
for source in @sourcelist
@sources.push SourceFile.findOrCreate source, this, @options
@build()
build: (callback)->
source_tar = ""
for source in @sources
source_tar += source.toString() +"\n"
if @ext == '.css'
fs.writeFileSync @filepath.replace(@ext, '.less'), source_tar if @options.preserve
@log @filepath.replace(@ext, '.less') if @options.preserve
less.render source_tar, (err, css) =>
if err?
@err @filepath
@err " ^ #{err.message}"
@err " #{err.extract.join('\n ')}"
else
@log @filepath
fs.writeFileSync @filepath, css
else
fs.writeFileSync @filepath.replace(@ext, '.coffee'), source_tar if @options.preserve
@log @filepath.replace(@ext, '.coffee') if @options.preserve
opts= {}
try
opts.noWrap= yes if @ext == ".#{@filename}"
js = cs.compile source_tar, opts
@log @filepath
fs.writeFileSync @filepath, js
catch err
@err @filepath
@err " ^ #{err}"
if @options.compress
compressed_filename = if @options.inplace then @filepath else @filepath.replace(@ext, ".min#{@ext}")
cmd = "#{@options.yuic} -o '#{compressed_filename}' '#{@filepath}'"
exec cmd, (err, stdout, stderr) =>
if err
@err compressed_filename
@err " ^ #{err}"
else
@log compressed_filename
callback() if callback?
else
callback() if callback?
this
task_build= (options)->
# Assign default options, where applicable
for key, value of DEFAULT_OPTIONS
options[key] = value unless key of options
# Create all the bundles
for target, defs of BUNDLES
new Bundle target, defs, options
task 'build', "Assembles and compiles all sources files", (opts) ->
task_build(opts)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment