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

21. June 2012 12:29

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);
		}
	);
}

Building Twitter Bootstrap With Visual Studio 2010

7. June 2012 12:11

I've been a big fan of the Chirpy Add-In for Visual Studio for a couple of years now. Recently I started work on a project where it made sense to use Twitter Bootstrap as a base set of CSS and JavaScript within an ASP.Net MVC 3 project. Since I needed to adjust the colors, and a few other settings, I figured it would be simple enough. Unfortunately the main .less files use @import directives in order to include the necessary files in the correct order, which Chirpy doesn't seem to support.

I came across another blog post that mentions using dotLess in the same scenario to build the .less files as a pre-build event. I went a slightly different route. Instead of using dotLess, I went with NodeJS and lessc as the compiler, this also allows me to use cssmin as a css minifier within the same build event.

First, you will want to download the latest Bootstrap source files, placing the less and img folders within the same parent. In this case, I used ~/Content/bootstrap/less and ~/Content/bootstrap/img for the less and img content. I placed the js into ~/Scripts/bootstrap.

Second you will want to download and install the latest NodeJS for windows. After that, from a command prompt, you're going to need to use the Node Package Manager (npm) to install the global utilities for less and cssmin from a command prompt.

npm -g install less
npm -g install cssmin

Third, you'll need to setup your Post-build event. Right-click the web project, then select properties. Then bring up the "Build Events" tab. From there, I have the following text in my "Pre-build event command line"

"$(ProjectDir)!PreBuild.cmd" "$(ProjectDir)" "$(DevEnvDir)"

With that in place, I added a "!PreBuild.cmd" batch file into the root of the project with the following content. (note: make sure the file is saved with DOS/ASCII encoding mode text, not UTF. Create it in Notepad if need be)

@echo off
cls

echo.
echo !PreBuild.cmd %1

:: Remove quotes from project path...
SET _projpath=%1
SET _projpath=###%_projpath%###
SET _projpath=%_projpath:"###=%
SET _projpath=%_projpath:###"=%
SET _projpath=%_projpath:###=%

:: Remove quotes from _devenv path
SET _devenv=%2
SET _devenv=###%_devenv%###
SET _devenv=%_devenv:"###=%
SET _devenv=%_devenv:###"=%
SET _devenv=%_devenv:###=%


:: Checkout the files to be built
"%_devenv%tf" checkout /lock:none "%_projpath%Content\bootstrap\css\*.*"


echo.
echo Build bootstrap.less
call lessc "%_projpath%Content\bootstrap\less\bootstrap.less" "%_projpath%Content\bootstrap\css\bootstrap.css"
call cssmin "%_projpath%Content\bootstrap\css\bootstrap.css" > "%_projpath%Content\bootstrap\css\bootstrap.min.css"

echo.
echo Build responsive.less
call lessc "%_projpath%Content\bootstrap\less\responsive.less" "%_projpath%Content\bootstrap\css\responsive.css"
call cssmin "%_projpath%Content\bootstrap\css\responsive.css" > "%_projpath%Content\bootstrap\css\responsive.min.css"

With the above pre-build batch in place, the css directory will be checked out, and the bootstrap css files will be created with a minified version.

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.