Skip to content

Instantly share code, notes, and snippets.

@stephanos
Created September 12, 2012 09:04
Show Gist options
  • Select an option

  • Save stephanos/3705409 to your computer and use it in GitHub Desktop.

Select an option

Save stephanos/3705409 to your computer and use it in GitHub Desktop.
asset compiler
console.time("total")
S = require 'string'
os = require 'os'
fs = require 'fs'
sys = require 'sys'
mkdirp = require 'mkdirp'
cluster = require 'cluster'
less = require 'less'
wrench = require 'wrench'
path = require 'path'
crypto = require 'crypto'
requirejs = require 'requirejs'
htmlmini = require 'html-minifier'
_ = require 'underscore'
coffee = require 'coffee-script'
handlebars = require 'handlebars'
class Asseto
constructor: (args = {}) ->
@input = args[2]
@output = args[3]
@module = args[4]
@styleOut = path.join(@output, 'styles')
@scriptOut = path.join(@output, 'scripts')
@pwd = path.dirname(args[1])
@cacheDir = os.tmpDir() + 'asseto/'
@cacheBuild = path.join(@cacheDir, 'build')
############################
##### UTIL
log: (s) ->
#console.log(s)
err: (e) ->
console.error(e)
process.exit(1)
write: (f, data, cb) ->
self = @
@log('writing ' + f)
fn = ->
fs.writeFile(f, data, 'utf8', (e) ->
if(e) then self.err(e)
if(cb) then cb()
)
fdir = path.dirname(f)
if(fdir != @styleOut && fdir != @scriptOut)
@mkdir(path.dirname(f), () -> fn())
else
fn()
read: (f, cb) ->
self = @
#@log("read " + f)
fs.readFile(f, 'utf-8', (e, data) ->
if(e)
self.err(e)
else
cb(data)
)
mkdir: (f, cb) ->
self = @
if(f != '.')
@log("mkdir " + f)
mkdirp(f, (e) ->
if(e) then self.err(e)
if(cb) then cb(f)
)
else
if(cb) then cb(f)
copy: (fin, fout, cb) ->
self = @
@log("copy " + fin + " to " + fout)
self.read(fin, (data) ->
self.mkdir(path.dirname(fout), () ->
self.write(fout, data, cb)
)
)
############################
##### COMPILE
c_style: (f, cb) ->
self = @
@log("less " + path.relative(self.input, f))
dir = path.dirname(f)
fn = path.basename(f)
@read(f, (data) ->
parser = new less.Parser(
filename: fn, paths: [dir]
).parse(data, (e, tree) ->
if (e)
less.writeError(e)
process.exit(1)
else
try
css = tree.toCSS(compress: false, yuicompress: true)
if(cb) then cb(css)
catch e
less.writeError(e)
process.exit(1)
)
)
c_coffee: (f, cb) ->
self = @
@log("coffee " + path.relative(self.input, f))
self.read(f, (data) ->
cb(
try
coffee.compile(data, {'bare': true})
catch e
console.error('"' + path.basename(f) + '"')
throw e
)
)
c_bars: (f, cb) ->
self = @
@log("handlebars " + path.relative(self.input, f))
@read(f, (data) ->
name = path.basename(f).replace('.view', '')
mHtml = htmlmini.minify(data, {collapseWhitespace: true, removeComments: true}).replace(/\\/g, '').replace(/}{/g, '} {')
template = 'define(["handlebars"], function(Handlebars) { return Handlebars.template(' + handlebars.precompile(mHtml) + ')});\n'
cb(template)
)
c_uglify: (fn, data, cb) ->
self = @
hash = crypto.createHash('md5').update(data).digest('hex')
cacheFile = self.cacheDir + fn + '.' + hash
minify = () ->
{uglify, parser} = require 'uglify-js'
ast = parser.parse data
ast = uglify.ast_mangle ast
ast = uglify.ast_squeeze ast
out = uglify.gen_code ast
self.write(cacheFile, out, () ->
cb(out)
)
fs.readFile(cacheFile, 'utf-8', (e, data) ->
if(e)
minify()
else
#@log("using cache " + cacheFile)
cb(data)
)
c_amd: (fpath, cb) ->
self = @
process.chdir(self.pwd)
buildFile = path.join(@cacheBuild, fpath)
buildDir = path.dirname(buildFile)
fn = path.basename(buildFile)
self.read(buildFile, (data) ->
config = JSON.parse(data)
#if (cluster.isMaster)
_.each(config.modules,
(m) ->
m.create = true
#w = cluster.fork({module: m.name})
)
#else
#config.modules = _.filter(config.modules, (m) -> m.name == self.module)
config.baseUrl = '.'
config.dir = buildDir
config.appDir = buildDir
config.useStrict = true
config.optimize = 'none'
config.keepBuildDir = true
console.time("r.js")
requirejs.optimize(config, (report) ->
console.timeEnd("r.js")
self.log(config)
_.each(config.modules, (m) ->
self.copy(m._buildPath, path.join(self.scriptOut, m.name + '.js'))
)
if(cb) then cb(config)
)
)
############################
##### MANAGE
minifier: (files, cb) ->
self = @
fn = () -> self.minifier(_.tail(files), cb)
if(_.isEmpty(files))
if(cb) then cb()
else
f = _.head(files)
self.read(f, (data) ->
fname = path.basename(f)
if(fname != 'test.js' && fname != '.')
self.c_uglify(fname, data, (mdata) ->
fout = path.join(self.scriptOut, fname.replace('.js', '.min.js'))
self.log("minify " + fout)
self.write(fout, mdata)
fn()
)
else
fn()
)
bundler: (fpaths, cb) ->
self = @
fn = () -> self.bundler(_.tail(fpaths), cb)
if(_.isEmpty(fpaths))
if(cb) then cb()
else
fpath = _.head(fpaths)
if(S(fpath).endsWith(".json"))
@log("bundle " + fpath)
console.time("bundle")
self.c_amd(fpath, (config) ->
console.timeEnd("bundle")
outs = []
_.each(config.modules, (m) -> outs.push(m._buildPath))
console.time("minify")
self.minifier(outs, () ->
console.timeEnd("minify")
fn()
)
)
else
fn()
precompiler: (fpaths, cb) ->
self = @
fn = () -> self.precompiler(_.tail(fpaths), cb)
if(_.isEmpty(fpaths))
cb()
else
fpath = _.head(fpaths)
file = path.join(self.input, fpath)
if(S(fpath).endsWith('.less') || S(fpath).endsWith('.css'))
if(path.relative(file, self.input) == '../..')
self.c_style(file, (data) ->
if(S(fpath).contains('.sub'))
fout = path.join(self.cacheBuild, fpath.replace('.sub.less', '.js'))
dn = path.basename(path.join(file, '..')) + '/' + path.basename(fout).replace('.js', '')
dd = "define('" + dn + "', function () {\n" +
"var style = module.exports = document.createElement('style');\n" +
"style.appendChild(document.createTextNode('" + data + "'));\n" +
"});"
# TODO: escape data?
self.write(fout, dd, fn)
else
fout = path.join(self.styleOut, path.basename(file).replace('.less', '.css'))
self.write(fout, data, fn)
)
else
fn()
else if(S(fpath).endsWith('.view'))
fout = path.join(self.cacheBuild, fpath.replace('.view', '.js'))
self.c_bars(file, (data) ->
self.write(fout, data, fn)
)
else if(S(fpath).endsWith('.coffee'))
fout = path.join(self.cacheBuild, fpath.replace('.coffee', '.js'))
self.c_coffee(file, (data) ->
self.write(fout, data, fn)
)
else
self.copy(file, path.join(self.cacheBuild, fpath), fn)
main: (cb) ->
self = @
mkdirp.sync(@styleOut)
mkdirp.sync(@scriptOut)
fn = () ->
self.main1(null, (fpaths) ->
self.main2(cb)
)
@resetCacheDir(() -> fn())
main1: (filter, cb) ->
@log(@input)
fpaths = _.filter(wrench.readdirSyncRecursive(@input), (f) ->
!S(f).endsWith('DS_Store') && S(f).contains('.') && (!filter || filter(f))
)
@log("#1 PRECOMPILING: " + fpaths.length + " files")
console.time("precompile")
@precompiler(fpaths, () ->
console.timeEnd("precompile")
if(cb) then cb(fpaths)
)
main2: (cb) ->
@log("#2 BUNDLING " + if(@module) then ": " + @module else "")
bfpaths = _.filter(fs.readdirSync(@input), (f) -> S(f).contains('build'))
@bundler(bfpaths, () ->
if(cb) then cb()
)
resetCacheDir: (cb) ->
self = @
@mkdir(@cacheDir, () ->
fs.stat(self.cacheDir, (e, stats) ->
if(e)
self.err(e)
else
age = (new Date().getTime()) - (stats.ctime.getTime())
if(age < 1 * 3600000) # x * hours
cb()
else
wrench.rmdirSyncRecursive(self.cacheDir)
self.mkdir(self.cacheDir, () ->
self.resetCacheDir(cb)
)
)
)
#if (cluster.isMaster)
exports.precompilejs = (args, cb) ->
a = new Asseto(args)
a.cacheBuild = a.output
a.main1(
(f) -> !S(f).endsWith('.less'),
() -> if(cb) then cb()
)
exports.bundle = (args) ->
new Asseto(args).main(() ->
console.timeEnd("total")
)
#else
# exports.bundle = (args) ->
# args.push(process.env.module)
# new Asseto(args).main2(() ->
# process.disconnect()
# )
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment