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.

@DesertCodeCamp, I am so sorry...

15. May 2010 15:42

Last night, I was working on my presentations for Desert Code Camp, all night.I honestly fell asleep around 5am and didn't wake up until around 3.  I had all my test data setup for MongoDB,  I had utilized a lot of the presentation resources from the MongoSF conference.  Specifically re-tooling the "powerpoint replacement" script from Michael Dirolf.  I've got about 2-3 pages of samples and notes here on that I'd like to cover. 

Around 1am I was going over the node.js stuff for that presentation, amazing how much pain one mistake can bring (entered "kiwi.requires('express/plugins')" instead of just "requires(...)".  The nodejs demo was made with parts from Craig R. Webster's "Getting Started With Node.js", and some parts centering on use of express/haml/sass thanks to the post from How To Node on "Blog rolling with MongoDB express and Node.js".  I had meant to get a bit in on the CommonJS plugin conventions and a some templating with the haml and sass support, there wouldn't be time to tie it in to mongo.

The monday demo for JavaScriptAZ is supposed to be on jQueryUI, but I'm going to use a lot of the material here for the first meeting in June on server-side-javascript.  I'm open to swapping those around if people are interested, so it isn't as far away.

I'm making this post, containing links to those resources I was deriving my presentations from, so the information is at least out there.  The stuff on node.js is harder to track down, as unfortunately the node.js site doesn't link to outside demos much at all, and some of the documentation, even the api docs are hard to follow.  MongoDB's site does better, but there's still a few rough edges in how the JS console (reference client) documentation is organized.  If you have any questions, feel free to email me.

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.