Created
September 12, 2012 09:04
-
-
Save stephanos/3705409 to your computer and use it in GitHub Desktop.
asset compiler
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
| 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