Build your JS, Less and CSS files via Node.js with Visual Studio

21. June 2012 12:29

The advice below is not what I would recommend today (late 2013), I would suggest using gulp (gulpfile.js) script for building client-side resources, and would do this as a pre-build event, there's also grunt, but I believe that gulp is better. There are plugins for VS (Web Essentials) that can build your .less for you, but they tend to be problematic, inconsistent, and can be outright troublesome.


 

In my previous article, I wanted to use NodeJS to build my .less files as part of my build process in Visual Studio 2010. I've since refined this process slightly. I've now placed my build scripts into the ~/build directory at the root of my web project.

I've also added a package.json file to the solution, so I can make a call to npm install in order to download any required node packages for the build process as well as creating a build.node.js file for the purpose of compiling my less files, as well as minification and merging of files for use elsewhere.

In the future I'd like to expand this to include SASS and CoffeeScript support as well as an npm package wrapper.

Here is an example package.json

{
	"name": "My.Website"
	,"description": "My Website"
	,"version": "0.0.1"
	,"author": "tracker1 (http://tracker1.info/"
	,"dependencies":{
	}
	,"devDependencies": {
		"uglify-js":"1.x.x"
		,"less":"1.x.x"
		,"cssmin":"0.3.x"
		,"async":"0.1.x"
	}
	,"builder":{
		"tasks":[
			{
				"type":"css"
				,"minify":"both"
				,"output":"../Content/css/main"
				,"files":[
					"../Content/bootstrap-less/bootstrap.less"
					,"../Content/bootstrap-less/responsive.less"
					,"../Content/site-less/site.less"
				]
			}
			,{
				"type":"js"
				,"minify":"both"
				,"output":"../Content/js/init"
				,"files":[
					"../Scripts/js-extensions/010-ConsoleStub.js"
					,"../Scripts/browser-extensions/Browser.js"
					,"../Scripts/browser-extensions/init1.js"
					,"../Scripts/browser-extensions/css_browser_selector.js"
					,"../Scripts/browser-extensions/modernizr-2.5.3.js"
					,"../Scripts/browser-extensions/topscroll.js"
				]
			}
			...
		]
	}
}

As you can see, I added a "builder" section with a number of "tasks" right now, the only tasks I am supporting are "js" and "css". The minify option should be either true, false, or "both". The process will create outputfile.(min|full).(css|js) so don't include a file extension on the output path.

My build.cmd file is now as follows, I'm including the TFS commands to checkout my js and css output paths, if you're using git/svn you can comment those lines out.

:: Step up from ~/bin to ~/build directory
cd ..\build

:: Checkout the files to be built
"%VS100COMNTOOLS%\..\IDE\tf" checkout /lock:none "..\Content\css\*.*"
"%VS100COMNTOOLS%\..\IDE\tf" checkout /lock:none "..\Content\js\*.*"

echo.
echo installing package dependancies
call npm install

echo.
echo building min/merge js and css
node build.node.js
echo.

With all of that said, here is my build.node.js file.

var fs = require("fs");
var util = require("util");
var async = require("async");

var less = require("less");
var cssmin = require("cssmin").cssmin;

var jsp = require("uglify-js").parser;
var pro = require("uglify-js").uglify;

var cfg;

main();

function main() {
	var pkg = JSON.parse(fs.readFileSync("package.json"),"utf8");
	cfg = pkg.builder;
	cfg.startDir = process.cwd();
	runTasks();
}

function runTasks() {
	console.log("Building CSS & JS files.");

	//store an array of functions for running each task
	var tasks = [];

	//console.log(JSON.stringify(cfg));

	//input each task definition into a runner.
	cfg.tasks.forEach(function(t){
		tasks.push(function(cb){
			if (t.type == "css") return runCssTask(t,cb);
			if (t.type == "js") return runJsTask(t,cb);
			cb(null,-1); //unrecognized format
		});
	});
	async.series(
		tasks
		,function(err,data) {
			console.log("Finished building CSS & JS files.");
		}
	);
}

function runCssTask(task, cb) {
	//data should be a collection of tree, use tree.toCSS() and tree.toCSS({compress:true}) respectively
	var min = task.minify;
	var full = !task.minify || task.minify === "both";
	
	console.log("Building " + task.output + " css");

	var fx = [];
	task.files.forEach(function(f){
		console.log("Loading " + f);

		fx.push(function(cb){
			var fp = fs.realpathSync(f).replace(/[\\\/]+/g,'/');
			var p = f.replace(/(\/[^\/]+)$/g,'/');

			var src = fs.readFileSync(fp,'utf8');
			var parser = new(less.Parser)({
				paths:[p]
				,filename:fp
			});
			parser.parse(src,function(err,tree){
				if (err) return cb(err,null);
				return cb(null, {"file":f, "css":tree.toCSS()});
			});
		});
	});
	async.series(
		fx
		,function(err,results) {
			if (err) throw err; //don't continue on error

			var m = [];
			var f = [];

			if (results && results.length) {
				results.forEach(function(item){
					if (min) {
						m.push("/*" + item.file + "*/\r\n");
						m.push(cssmin(item.css));
						m.push("\r\n\r\n");
					}
					if (full) {
						f.push("/*" + item.file + "*/\r\n");
						f.push(item.css);
						f.push("\r\n\r\n");
					}
				});
			}

			//write file(s)
			if (min) fs.writeFileSync(task.output + ".min.css", m.join(""), 'utf8');
			if (full) fs.writeFileSync(task.output + ".full.css", f.join(""), 'utf8');

			console.log("css handled for '" + task.output + "' " + results.length);

			cb(null,1);
		}
	)
}

function runJsTask(task, cb) {
	var min = task.minify;
	var full = !task.minify || task.minify === "both";

	console.log("Building " + task.output + " css");

	var fx = [];
	task.files.forEach(function(f){
		fx.push(function(cb){
			console.log("Loading " + f);
			var ret = {"file":f};
			var fp = fs.realpathSync(f).replace(/[\\\/]+/g,'/');
			var p = f.replace(/(\/[^\/]+)$/g,'/');
			
			ret.full = fs.readFileSync(f,'utf8');
			if (min) {
				var ast = jsp.parse(ret.full); //parse code for initial ast
				ast = pro.ast_mangle(ast); //get new ast with mangled names
				ast = pro.ast_squeeze(ast); //get an ast with compression optimizations
				ret.min = pro.gen_code(ast); //get compressed output
			}
			cb(null, ret);
		});
	});
	
	async.series(
		fx
		,function(err,results) {
						if (err) throw err; //don't continue on error

			//data should be a collection of tree, use tree.toCSS() and tree.toCSS({compress:true}) respectively

			var m = [];
			var f = [];

			if (results && results.length) {
				results.forEach(function(item){
					if (min) {
						m.push(";/*" + item.file + "*/\r\n");
						m.push(item.min);
						m.push("\r\n\r\n");
					}
					if (full) {
						f.push(";/*" + item.file + "*/\r\n");
						f.push(item.full);
						f.push("\r\n\r\n");
					}
				});
			}
			
			//write file(s)
			if (min) fs.writeFileSync(task.output + ".min.js", m.join(""), 'utf8');
			if (full) fs.writeFileSync(task.output + ".full.js", f.join(""), 'utf8');

			console.log("js handled for '" + task.output + "' " + results.length);
			cb(null,2);
		}
	);
}
Comments are closed

Tracker1

Michael J. Ryan aka Tracker1

My name is Michael J. Ryan and I've been developing web based applications since the mid 90's.

I am an advanced Web UX developer with a near expert knowledge of JavaScript.