UNPKG

co-assets-compiler

Version:

Assets compiler for CompoundJS

352 lines (313 loc) 9.92 kB
var fs = require('fs') , path = require('path') , utils , $; /** * Assets compilation engine * Waits for requests to public assets folders and compiles * the files if needed * * @constructor */ var AssetsCompiler = module.exports = function AssetsCompiler(compound) { this.compound = compound; this.app = this.compound.app; this.assetDir = this.app.root + '/app/assets'; this.publicDir = this.app.root + '/public'; this.defaultCompilerOptions = { sourceDir: '', destDir: '', }; this.addCompilers(); utils = compound.utils; $ = utils.stylize.$; } AssetsCompiler.prototype.init = function() { /** * Decide which asset directories to watch and which should * be precompiled */ var assetTypes = [{name: 'javascripts', extension: 'js'}, {name:'stylesheets', extension: 'css'}] , self = this; var precompileAssets = []; self.handledAssetTypes = []; assetTypes.forEach(function(assetType) { if (self.app.enabled('merge ' + assetType.name)) { precompileAssets.push(self.getCompiler(assetType.extension).sourceExtension); } else { self.handledAssetTypes.push(assetType.extension); } }); if (precompileAssets.length > 0) { // Compile assets asynchronously process.nextTick(function() { self.precompileAssets(precompileAssets); }); } return function assetsCompiler(req, res, next) { self.handleRequest(req, res, next); }; }; /** * Precompiles assets in /app/assets/coffeescripts and /app/assets/stylesheets * * @param {Array} List of asset types that should be precompiled */ AssetsCompiler.prototype.precompileAssets = function(sourceExtensions) { var self = this; var log = utils.debug; log($('AssetsCompiler').bold + ' ' + $('Precompiling assets:').cyan + ' ' + sourceExtensions.join(', ')); this.compound.utils.recursivelyWalkDir(this.assetDir, function(err, files) { if (err) throw err; files.forEach(function(file) { var match = file.match(new RegExp('^(.*)\/(.*)[.]('+sourceExtensions.join('|')+')$')); if(match) { var source = match[0] , folder = match[1] , fileName = match[2] , extension = match[3] , compiler = self.compilers[extension] , destFolder = folder.replace(self.assetDir+compiler.sourceDir, self.publicDir+compiler.destDir) , dest = destFolder + '/' + fileName + '.' + compiler.destExtension; self.compileAsset(source, dest, compiler, function(err) { if (err) { log($('AssetsCompiler').bold + ' ' + $('Compilation of ' + source.replace(self.assetDir, '') + ' failed: ').red + err); } }); } }); }); }; /** * Listens to /stylesheets and /javascripts requests and * delegates the requests to the static middleware after * conditionally compiling the source files (e.g. coffee) * * @param {Array} List of asset types that should be compiled on request */ AssetsCompiler.prototype.handleRequest = function(req, res, next) { var match , self = this , path = this.publicDir + req.path; if (match = path.match(new RegExp('^(.*)\/(.*)[.]('+this.handledAssetTypes.join('|')+')$'))) { var dest = match[0] , folder = match[1] , fileName = match[2] , extension = match[3] , compiler = this.getCompiler(extension) , sourceFolder = folder.replace(self.publicDir+compiler.destDir, self.assetDir+compiler.sourceDir) , source = sourceFolder + '/' + fileName + '.' + compiler.sourceExtension; if(compiler) { this.compileAsset(source, dest, compiler, function(err) { if (err) { throw new Error('Asset compilation failed: ' + err); } }); } } next(); }; /** * Returns the correct Compiler for the given extension * @param {String} extension */ AssetsCompiler.prototype.getCompiler = function(extension) { var compiler, compilerName; var log = utils.debug; switch (extension) { case 'js': compilerName = this.app.settings.jsEngine || 'coffee'; break; case 'css': compilerName = this.app.settings.cssEngine || 'stylus'; break; default: break; } if(!(compiler = this.compilers[compilerName])) { log($('AssetsCompiler').bold + ' ' + $('Compiler ' + $(compilerName).bold + ' not implemented').red); } return compiler; } /** * Checks for source file, compiles it and saves the * compiled source if the destination file is older than the source * file or if the destination file doesnt exist * * @param {String} filename of asset without extension * @param {String} relative path to folder containing asset * @param {Object} compiler * @return {Boolean} whether a file has been compiled */ AssetsCompiler.prototype.compileAsset = function(sourcePath, destPath, compiler, callback) { var self = this; var log = utils.debug; if (!callback) var callback = function() {}; // options for compiler var options = { sourceDir: sourcePath.match(/(.*)\/|\\/)[1], destDir: destPath.match(/(.*)\//)[1], sourceFileName: sourcePath.match(/([^/\\]+)$/)[1], destFileName: destPath.match(/([^/\\]+)$/)[1] }; if (compiler.options) options = utils.safe_merge(options, compiler.options) // if `sourcePath` doesnt exist, we don't need to compile if (!fs.existsSync(sourcePath)) { return callback(null, false); } // if `destPath` doesnt exist or `sourcePath` is older than `destPath` // => compile! var doCompile = false; if (fs.existsSync(destPath)) { var destStat = fs.statSync(destPath) , sourceStat = fs.statSync(sourcePath); if (sourceStat.mtime > destStat.mtime) { doCompile = true; } } else { doCompile = true; } // special case: always compile if (self.app.enabled('force assets compilation')) doCompile = true; // make sure that the destination path exists // actually compile if (doCompile) { self.compound.utils.ensureDirectoryExists(path.dirname(destPath)); var code = fs.readFileSync(sourcePath).toString(); compiler.render(code, options, function(err, compiledCode) { if(err) { return callback(err); } fs.writeFileSync(destPath, compiledCode); callback(null, true); log($('AssetsCompiler').bold + ' ' + $(options.sourceFileName).cyan + ' => ' + $(options.destFileName).green); }); } else { callback(null, false); } return doCompile; }; AssetsCompiler.prototype.compilers = {}; /** * Adds a new compiler. * @param {String||[String,...]} extensions: string or array of strings that represent * the extensions this compiler handles * @param {Object} options: should contain a render function, and any other options for the compiler */ AssetsCompiler.prototype.add = function(extensions, options) { var self = this; extensions = extensions instanceof Array ? extensions : [extensions]; extensions.forEach(function(extension) { self.compilers[extension] = {}; self.configure(extension, self.defaultCompilerOptions); self.configure(extension, options); }); return this; } /** * Configuers an existing compiler * @param {String} extension: the extension for the compiler to be configured * @param {Object} options: the options to be set on the compiler object */ AssetsCompiler.prototype.configure = function(extension, options) { var compiler = this.compilers[extension], key; if (compiler) { for (key in options) { if (options.hasOwnProperty(key) ) { compiler[key] = options[key]; } } } return this; } /** * Add Available compilers */ AssetsCompiler.prototype.addCompilers = function() { var self = this; this.add('coffee', { render: function(str, options, fn) { var uglify = require('uglify-js'); this.coffee = this.coffee || require('coffee-script'); try { var result = this.coffee.compile(str) if (self.app.enabled('minify')){ var minify = uglify.minify(result, {fromString:true}) result = minify.code; } fn(null, result); } catch (err) { fn(err); } }, sourceDir: '/coffeescripts', destDir: '/javascripts', sourceExtension: 'coffee', destExtension: 'js' }); this.add('sass', { render: function(str, options, fn) { this.sass = this.sass || require('sass'); try { fn(null, this.sass.render(str)); } catch (err) { fn(err); } }, sourceExtension: 'sass', destExtension: 'css' }); this.add('scss', { render: function(str, options, fn){ this.nodeSass = this.nodSass || require('node-sass'); try{ fn(null, this.nodeSass.renderSync({data: str})); }catch (err) { fn(err); } }, sourceExtension: 'scss', destExtension: 'css' }); this.add('less', { render: function(str, options, fn) { this.less = this.less || require('less'); try { var parser = new(this.less.Parser)({ paths: [options.sourceDir] }); parser.parse(str, function (e, tree) { if (e) {throw e;} fn(null, tree.toCSS()); }); } catch (err) { fn(err); } }, sourceExtension: 'less', destExtension: 'css' }); this.add(['stylus', 'styl'], { render: function(str, options, fn) { var style; this.stylus = this.stylus || require('stylus'); options.paths = (options.paths || []).concat([options.sourceDir]); style = this.stylus(str, options); if(this.use) { var use = this.use instanceof Array ? this.use : [this.use]; for(var i = 0; i < use.length; i++) { style.use(use[i]); } } try { style.render(fn); } catch (err) { fn(err); } }, sourceExtension: 'styl', destExtension: 'css' }); }