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

Just an example of how my node.js based build scripts usually look like for a "basic" project. Sometimes it can get way more complex than that (than I split it in multiple files, one for each task).

Of course it would be more concise in a DSL created specific for build processes and that already had built-in methods for the trivial stuff (like copying files), but I like the flexibility that JS scripts gives. A helper lib with all the methods available on Ant would be great, if only I had unlimited time and resources...

For my projects a tool like grunt wouldn't help that much since as you can see the only task that I would use is min (uglify) and it's only a few lines of code... Maybe in the future I change my mind and start using it tho.

For more info on how to create your own scripts and the reasoning behind it check my blog post: node.js as a build script.

PS: I've been considering Gradle as a replacement for my build scripts but I never find the time to try it out. Main reason is because of the DSL (looks so clean!) and because it has many features out-of-the-box (including integration with other tools like Jenkins), no need to re-invent the wheel... but I still feel that writing in plain JS end up being more productive and I don't hit as many walls.

@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