Created
October 20, 2010 19:42
-
-
Save mattmccray/637154 to your computer and use it in GitHub Desktop.
CoffeeScript and LESS build tool
This file contains hidden or 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
# | |
# 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