#!/usr/bin/env node // Build Tools Module -- included by build.js. // Copies, symlinks and compresses files into the right locations. // Can also compact & bundle JS/CSS together for distribution. // Copyright (c) 2015 Joseph Huckaby, MIT License. var path = require('path'); var fs = require('fs'); var zlib = require('zlib'); var util = require('util'); var os = require('os'); var cp = require('child_process'); var mkdirp = require('mkdirp'); var async = require('async'); var glob = require('glob'); var UglifyJS = require("uglify-js"); var Tools = require('pixl-tools'); var fileStatSync = function(file) { // no-throw version of fs.statSync var stats = null; try { stats = fs.statSync(file); } catch (err) { return false; } return stats; }; var fileExistsSync = function(file) { // replacement for fs.existsSync which is being removed return !!fileStatSync(file); }; var symlinkFile = exports.symlinkFile = function symlinkFile( old_file, new_file, callback ) { // create symlink to file // Note: 'new_file' is the object that will be created on the filesystem // 'old_file' should already exist, and is the file being pointed to if (new_file.match(/\/$/)) new_file += path.basename(old_file); // if target exists and is not a symlink, skip this try { var stats = fs.lstatSync(new_file); if (!stats.isSymbolicLink()) return callback(); } catch (e) {;} console.log( "Symlink: " + old_file + " --> " + new_file ); try { fs.unlinkSync( new_file ); } catch (e) {;} if (!fileExistsSync( path.dirname(new_file) )) { mkdirp.sync( path.dirname(new_file) ); } // fs.symlink takes a STRING (not a file path per se) as the old (existing) file, // and it needs to be relative from new_file. So we need to resolve some things. var sym_new = path.resolve( new_file ); var sym_old = path.relative( path.dirname(sym_new), path.resolve(old_file) ); fs.symlink( sym_old, sym_new, callback ); }; var copyFile = exports.copyFile = function copyFile( old_file, new_file, callback ) { // copy file if (new_file.match(/\/$/)) new_file += path.basename(old_file); console.log( "Copy: " + old_file + " --> " + new_file ); try { fs.unlinkSync( new_file ); } catch (e) {;} if (!fileExistsSync( path.dirname(new_file) )) { mkdirp.sync( path.dirname(new_file) ); } var inp = fs.createReadStream(old_file); var outp = fs.createWriteStream(new_file); inp.on('end', callback ); inp.pipe( outp ); }; var copyFiles = exports.copyFiles = function copyFiles( src_spec, dest_dir, callback ) { // copy multiple files to destination directory using filesystem globbing dest_dir = dest_dir.replace(/\/$/, ''); glob(src_spec, {}, function (err, files) { // got files if (files && files.length) { async.eachSeries( files, function(src_file, callback) { // foreach file var stats = fileStatSync(src_file); if (stats && stats.isFile()) { copyFile( src_file, dest_dir + '/', callback ); } else callback(); }, callback ); } // got files else { callback( err || new Error("No files found matching: " + src_spec) ); } } ); }; var compressFile = exports.compressFile = function compressFile( src_file, gz_file, callback ) { // gzip compress file console.log( "Compress: " + src_file + " --> " + gz_file ); if (fileExistsSync(gz_file)) { fs.unlinkSync( gz_file ); } var gzip = zlib.createGzip(); var inp = fs.createReadStream( src_file ); var outp = fs.createWriteStream( gz_file ); inp.on('end', callback ); inp.pipe(gzip).pipe(outp); }; var copyCompress = exports.copyCompress = function copyCompress( old_file, new_file, callback ) { // copy file and create gzip version as well if (new_file.match(/\/$/)) new_file += path.basename(old_file); copyFile( old_file, new_file, function(err) { if (err) return callback(err); // Make a compressed copy, so node-static will serve it up to browsers compressFile( old_file, new_file + '.gz', callback ); } ); }; var symlinkCompress = exports.symlinkCompress = function symlinkCompress( old_file, new_file, callback ) { // symlink file and create gzip version as well if (new_file.match(/\/$/)) new_file += path.basename(old_file); symlinkFile( old_file, new_file, function(err) { if (err) return callback(err); // Make a compressed copy, so node-static will serve it up to browsers compressFile( old_file, new_file + '.gz', callback ); } ); }; var deleteFile = exports.deleteFile = function deleteFile( file, callback ) { // delete file console.log( "Delete: " + file ); if (fileExistsSync(file)) { fs.unlink( file, callback ); } else callback(); }; var deleteFiles = exports.deleteFiles = function deleteFiles( spec, callback ) { // delete multiple files using filesystem globbing glob(spec, {}, function (err, files) { // got files if (files && files.length) { async.eachSeries( files, function(file, callback) { // foreach file deleteFile( file, callback ); }, callback ); } // got files else { callback( err ); } } ); }; var chmodFiles = exports.chmodFiles = function chmodFiles( mode, spec, callback ) { // chmod multiple files to specified mode using filesystem globbing glob(spec, {}, function (err, files) { // got files if (files && files.length) { async.eachSeries( files, function(file, callback) { // foreach file fs.chmod( file, mode, callback ); }, callback ); } // got files else { callback( err || new Error("No files found matching: " + spec) ); } } ); }; var copyDir = exports.copyDir = function copyDir( src_dir, dest_dir, exclusive, callback ) { // recursively copy dir and contents, optionally with exclusive mode // symlinks are followed, and the target is copied instead var src_spec = src_dir + '/*'; // exclusive means skip if dest exists (do not replace) if (exclusive && fileExistsSync(dest_dir)) return callback(); mkdirp.sync( dest_dir ); glob(src_spec, {}, function (err, files) { // got files if (files && files.length) { async.eachSeries( files, function(src_file, callback) { // foreach file var stats = fs.statSync(src_file); if (stats.isFile()) { copyFile( src_file, dest_dir + '/', callback ); } else if (stats.isDirectory()) { copyDir( src_file, dest_dir + '/' + path.basename(src_file), exclusive, callback ); } }, callback ); } // got files else { callback( err ); } } ); }; var bundleCompress = exports.bundleCompress = function bundleCompress( args, callback ) { // compress bundle of files var html_file = args.html_file; var html_dir = path.dirname( html_file ); if (!fileExistsSync( path.dirname(args.dest_bundle) )) { mkdirp.sync( path.dirname(args.dest_bundle) ); } var raw_html = fs.readFileSync( html_file, 'utf8' ); var regexp = new RegExp( "\\<\\!\\-\\-\\s+BUILD\\:\\s*"+args.match_key+"_START\\s*\\-\\-\\>([^]+)\\<\\!\\-\\-\\s*BUILD\\:\\s+"+args.match_key+"_END\\s*\\-\\-\\>" ); if (raw_html.match(regexp)) { var files_raw = RegExp.$1; var files = []; files_raw.replace( /\b(src|href)\=[\"\']([^\"\']+)[\"\']/ig, function(m_all, m_g1, m_g2) { files.push( path.join(html_dir, m_g2) ); } ); if (files.length) { console.log("Bundling files: ", files); var raw_output = ''; // optionally add file header if (args.header) { raw_output += args.header.trim() + "\n"; } if (args.uglify) { console.log("Running UglifyJS..."); var result = UglifyJS.minify( files ); if (!result || !result.code) return callback( new Error("Failed to bundle script: Uglify failure") ); raw_output += result.code; } else { for (var idx = 0, len = files.length; idx < len; idx++) { var file = files[idx]; raw_output += fs.readFileSync( file, 'utf8' ) + "\n"; } } // optionally strip source map links // /*# sourceMappingURL=materialdesignicons.min.css.map */ if (args.strip_source_maps) { raw_output = raw_output.replace(/sourceMappingURL\=\S+/g, ""); } // write out our bundle console.log(" --> " + args.dest_bundle); fs.writeFileSync(args.dest_bundle, raw_output); // swap a ref link into a copy of the HTML console.log(" --> " + html_file ); raw_html = raw_html.replace( regexp, args.dest_bundle_tag ); fs.writeFileSync(html_file, raw_html); // now make a compressed version of the bundle compressFile( args.dest_bundle, args.dest_bundle + '.gz', function(err) { if (err) return callback(err); // and compress the final HTML as well compressFile( html_file, html_file + '.gz', callback ); }); } // found files else { callback( new Error("Could not locate any file references: " + args.src_html + ": " + args.match_key) ); } } else { callback( new Error("Could not locate match in HTML source: " + args.src_html + ": " + args.match_key) ); } }; var generateSecretKey = exports.generateSecretKey = function generateSecretKey( args, callback ) { // generate random secret key for a specified JSON config file // use regular expression to preserve natural file format var file = args.file; var key = args.key; var regex = new RegExp('(\\"'+Tools.escapeRegExp(key)+'\\"\\s*\\:\\s*\\")(.*?)(\\")'); var secret_key = Tools.generateUniqueID(32); fs.readFile(file, 'utf8', function(err, text) { if (err) return callback(err); if (!text.match(regex)) return callback( new Error("Could not locate key to replace: " + file + ": " + key) ); text = text.replace(regex, '$1' + secret_key + '$3'); fs.writeFile( file, text, callback ); }); }; var addToServerStartup = exports.addToServerStartup = function addToServerStartup( args, callback ) { // add service to init.d // (RedHat and Ubuntu only -- do nothing on OS X) var src_file = args.src_file; var dest_dir = args.dest_dir; var service_name = args.service_name; var dest_file = dest_dir + '/' + service_name; // shell command that activates service on redhat or ubuntu var cmd = 'chkconfig '+service_name+' on || update-rc.d '+service_name+' defaults'; // skip on os x, and if init dir is missing if (os.platform() == 'darwin') return callback(); if (!fileExistsSync(dest_dir)) return callback( new Error("Cannot locate init.d directory: " + dest_dir) ); if (process.getuid() != 0) return callback( new Error("Must be root to add services to server startup system.") ); // copy file into place copyFile( src_file, dest_file, function(err) { if (err) return callback(err); // must be executable fs.chmod( dest_file, "775", function(err) { if (err) return callback(err); // exec shell command cp.exec( cmd, callback ); } ); } ); }; var printMessage = exports.printMessage = function printMessage( args, callback ) { // output a message to the console // use process.stdout.write because console.log has been redirected to a file. process.stdout.write( "\n" + args.lines.join("\n") + "\n\n" ); };