Skip to content

Instantly share code, notes, and snippets.

@millermedeiros
Created May 9, 2012 01:15
Show Gist options
  • Star 31 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save millermedeiros/2640928 to your computer and use it in GitHub Desktop.
Save millermedeiros/2640928 to your computer and use it in GitHub Desktop.
sample node.js build script including RequireJS optimizer (r.js) and copy/delete/filter files
// Combine JS and CSS files
// ---
//
// Make sure you install the npm dependencies
// > cd YOUR_PROJECT_FOLDER
// > npm install
//
// Than run:
// > node build
var _cli = require('commander'),
_minimatch = require('minimatch'),
_wrench = require('wrench'),
_fs = require('fs'),
_path = require('path'),
_requirejs = require('requirejs');
// ========
// SETTINGS
// ========
var DIST_FOLDER = '../../deploy/site/';
var FILE_ENCODING = 'utf-8';
// Configs reused across all the r.js JS optimizations
// reference: https://github.com/jrburke/r.js/blob/master/build/example.build.js
var BASE_JS_SETTINGS = {
logLevel : 1,
baseUrl : 'js',
paths : {
// folders (for brevity)
'jq' : 'lib/jquery',
'mm' : 'lib/millermedeiros',
'amd-utils' : 'lib/amd-utils',
// libs
'jquery' : 'empty:', //load from CDN
'signals' : 'lib/signals',
'crossroads' : 'lib/crossroads',
'hasher' : 'lib/hasher',
// requirejs plugins
'text' : 'lib/require/text',
'json' : 'lib/require/json',
'hbs' : 'lib/require/hbs'
},
inlineText: true,
optimize : 'uglify',
// optimize : 'none',
pragmasOnSave : {
excludeHbs : true,
excludeHbsParser : true
},
hbs : {
disableI18n : true
},
// exclude plugins after build
stubModules : [
'json',
'mdown',
'hbs',
'text'
],
exclude : [],
include :[]
};
// ========
// COMMANDS
// ========
_cli
.command('deploy')
.description('optimize CSS/JS files and copy files to deploy.')
.action(function(){
purgeDeploy();
copyFilesToDeploy();
optimizeJS(optimizeCSS);
});
_cli
.command('optimize-js')
.description('optimize JS files and combine into fewer files to improve page load performance.')
.action(optimizeJS);
_cli
.command('optimize-css')
.description('optimize CSS files and combine into fewer files to improve page load performance.')
.action(optimizeCSS);
// ============================================================================
// TASKS
// ============================================================================
var _optimizeStartTime;
var _nOptimizedModules = 0;
function optimizeJS(cb){
echo('optimizing JS files...');
_optimizeStartTime = Date.now();
uglify('js/lib/require/require.js', _path.join(DIST_FOLDER, 'js/lib/require/require.js'), [
'@license RequireJS 2.1.0 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.',
'Available via the MIT or new BSD license.',
'see: http://github.com/jrburke/requirejs for details'
]);
//jquery only used if local
uglify('js/lib/jquery/jquery.js', _path.join(DIST_FOLDER, 'js/lib/jquery/jquery.js'), [
'jQuery v1.8.2 jquery.com | jquery.org/license'
]);
rjs({
name : 'main',
out : _path.join(DIST_FOLDER, 'js/main.js')
}, function(){
echo('Build date: '+ (new Date()).toUTCString());
echo('optimized %d modules in %d miliseconds.', _nOptimizedModules, Date.now() - _optimizeStartTime);
if (typeof cb === 'function') cb();
});
}
// ---
function optimizeCSS(cb){
_requirejs.optimize({
// optimizeCss : 'standard.keepLines',
optimizeCss : 'standard',
cssIn: 'css/master.css',
out: _path.join(DIST_FOLDER, 'css/master.css')
}, function(response){
console.log(response);
echo('optimized CSS files');
if (typeof cb === 'function') cb();
});
}
// ---
function purgeDeploy(){
echo('deleting old deploy files...');
if (! _fs.existsSync(DIST_FOLDER)) return;
var files = _wrench.readdirSyncRecursive(DIST_FOLDER);
// filter files that shouldn't be deleted
files = filterFiles(files, [
'{**/,}.svn{/**,}'
]);
var stat;
files.forEach(function(path){
path = DIST_FOLDER + path;
stat = _fs.statSync(path);
if( stat.isFile() ) {
_fs.unlinkSync(path);
} else if( stat.isDirectory() ){
if (! _fs.readdirSync(path).length) {
//only delete folder if empty
_fs.rmdirSync(path);
}
}
});
}
function copyFilesToDeploy(){
echo('copying files to deploy...');
var files = _wrench.readdirSyncRecursive('./');
// filter files that shouldn't be copied
files = filterFiles(files, [
'_*',
'.*',
'.DS_Store',
'{**/,}.svn{/**,}',
'node_modules/**',
'tests/**',
'js/**',
'css/**',
'img/tmp/**',
'build.js',
'gui.html',
'package.json',
'README.md',
'update.sh'
]);
// add files that would be excluded by globs above
files.push('.htaccess');
files.forEach(function(path){
//skip directories
if(_fs.statSync(path).isFile() ){
var distPath = _path.join(DIST_FOLDER, path);
safeCreateDir(distPath);
_fs.writeFileSync(distPath, _fs.readFileSync(path));
}
});
_fs.writeFileSync(DIST_FOLDER + 'README.md', "# Do NOT edit these files!\n\nThey are going to be deleted on the next build,\ncheck files inside the 'dev/' folder instead.\n\nLast Build: "+ (new Date()).toUTCString());
}
// =======
// HELPERS
// =======
function echo(var_args){
var args = Array.prototype.slice.call(arguments);
args[0] = ' '+ args[0];
console.log.apply(console, args);
}
function safeCreateDir(path) {
var dir = _path.dirname(path);
if (! _fs.existsSync(dir)) {
_wrench.mkdirSyncRecursive(dir);
}
}
function filterFiles(files, excludes) {
var globOpts = {
matchBase: true,
dot : true
};
excludes = excludes.map(function(val){
//minimatch currently have a bug with star globs (https://github.com/isaacs/minimatch/issues/5)
return _minimatch.makeRe(val, globOpts);
});
files = files.map(function(filePath){
// need to normalize and convert slashes to unix standard
// otherwise the RegExp test will fail on windows
return _path.normalize(filePath).replace(/\\/g, '/');
});
return files.filter(function(filePath){
return ! excludes.some(function(glob){
return glob.test(filePath);
});
});
}
function uglify(srcPath, distPath, licenseArr) {
var
uglyfyJS = require('uglify-js'),
jsp = uglyfyJS.parser,
pro = uglyfyJS.uglify,
ast = jsp.parse( _fs.readFileSync(srcPath, FILE_ENCODING) ),
prepend = licenseArr? '\/**\n * '+ licenseArr.join('\n * ') +'\n */\n' : '';
ast = pro.ast_mangle(ast);
ast = pro.ast_squeeze(ast);
safeCreateDir(distPath);
_fs.writeFileSync(distPath, prepend + pro.gen_code(ast), FILE_ENCODING);
echo('"%s" uglified.', distPath);
}
function rjs(opts, cb){
_requirejs.optimize( mixIn(BASE_JS_SETTINGS, opts), function(){
_nOptimizedModules += 1;
if (typeof cb === 'function') {
cb.apply(null, Array.prototype.slice.call(arguments));
}
});
}
function mixIn(target, objects){
var i = 1,
key, cur;
while(cur = arguments[i++]){
for(key in cur){
if(Object.prototype.hasOwnProperty.call(cur, key)){
target[key] = cur[key];
}
}
}
return target;
}
// ==============
// parse commands
// ==============
_cli.parse(process.argv);
if (!_cli.args.length) {
// show help by default
_cli.parse([process.argv[0], process.argv[1], '-h']);
process.exit(0);
} else {
//warn aboud invalid commands
var validCommands = _cli.commands.map(function(cmd){
return cmd.name;
});
var invalidCommands = _cli.args.filter(function(cmd){
//if command executed it will be an object and not a string
return (typeof cmd === 'string' && validCommands.indexOf(cmd) === -1);
});
if (invalidCommands.length) {
console.log('\n [ERROR] - Invalid command: "%s". See "--help" for a list of available commands.\n', invalidCommands.join(', '));
}
}
{
"private" : true,
"name" : "sample",
"version" : "0.1.0",
"devDependencies" : {
"wrench" : "1.3.4",
"minimatch" : "~0.1",
"commander" : "~0.5",
"requirejs" : "~2.0.4",
"uglify-js" : "~1.2"
},
"engine" : {
"node" : "0.8.x"
}
}
@millermedeiros
Copy link
Author

Note that I'm only using the sync version of the node.js fs methods, that isn't the recommended approach for all the cases, but since it's a build script and it will probably run locally on your machine there is no problem if it blocks the main "thread". If your builds starts taking too long to run you can replace the sync methods with the async versions and run things in parallel, maybe even spawn a diff child_process per CPU core, for my current projects the approach above is working well...

@millermedeiros
Copy link
Author

Be explicit about your dependencies! Future devs trying to build the project might have issues with backward incompatible modules !!

@millermedeiros
Copy link
Author

FYI I created an experimental adapter to run Ant tasks (concat, copy, etc...) from inside node: https://github.com/millermedeiros/node-ant

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment