Mikael's blog

A developers seventh time trying to maintain a blog

Automatic Minification and Bundling with node.js

Scott Guthrie recently wrote about the new minification and bundling process that has been built for ASP.Net 4.5.

I read his blog post, liked what I read and then I though of doing the same thing for my blog. I’ve been looking at minification programs for a while but I’ve put it off so far because I didn’t want to add another step to my manual deployment process. Now I’m thinking I don’t have to have another step. I could do it “automagically”.

The BundleController

/*****************************************
 *   Bundle Controller
 *****************************************
 *   Author:  mikael.lofjard@gmail.com
 *   Website: http://lofjard.se
 *   License: MIT License
 ****************************************/

var BundleController = (function () {
    
  var url = require('url');
  var fs = require('fs');
  var path = require('path');

  var cssmin = require('cssmin');
  var jsmin = require('uglify-js');

  var env = require('../environmentManager').EnvironmentManager;
  var dm = require('../domainManager').DomainManager;

  var config = null;
  var staticFileController = null;
    
  function sortCss(a, b) {
    if (a === 'normalize.css')
      return -1;
    if (b === 'normalize.css')
      return 1;
    if (a === 'shCore.css')
      return -1;
    if (b === 'shCore.css')
      return 1;
    return (a < b) ? -1 : ((a > b) ? 1 : 0);
  }

  function sortScripts(a, b) {
    if (a === 'lofjard.js')
      return -1;
    if (b === 'lofjard.js')
      return 1;
    return (a < b) ? -1 : ((a > b) ? 1 : 0);
  }

  function createCssBundle() {
    var wwwRoots = dm.getAllWwwRoots();

    for (var i in wwwRoots) {
      var cssPath = path.join(wwwRoots[i], config.css.path);
      var bundleFilePath = path.join(cssPath, '_bundled.css');
      console.log('BundleController: CSS bundle path is ' + bundleFilePath);
      try {
        fs.unlinkSync(bundleFilePath);
      } catch (ex) {}

      var files = fs.readdirSync(cssPath);
      files.sort(sortCss);
      var bundleData = '';

      for (var filename in files) {
        console.log(' - Bundling CSS ' + path.join(cssPath, files[filename]));
        var file = fs.readFileSync(path.join(cssPath, files[filename]), 'utf-8');
        bundleData += (file + '\n');
      }

      if (config.css.minimize) {
        bundleData = cssmin.cssmin(bundleData);
      }

      fs.writeFileSync(bundleFilePath, bundleData, 'utf-8');
    }
  }

  function createScriptsBundle() {
    var wwwRoots = dm.getAllWwwRoots();
    for (var i in wwwRoots) {
      var jsPath = path.join(wwwRoots[i], config.scripts.path);
      var bundleFilePath = path.join(jsPath, '_bundled.js');
      console.log('BundleController: JavaScript bundle path is ' + bundleFilePath);

      try {
        fs.unlinkSync(bundleFilePath);
      } catch (ex) {}

      var files = fs.readdirSync(jsPath);
      files.sort(sortScripts);
      var bundleData = '';

      for (var filename in files) {
        console.log(' - Bundling JavaScript ' + path.join(jsPath, files[filename]));
        var file = fs.readFileSync(path.join(jsPath, files[filename]), 'utf-8');
        bundleData += (file + '\n');
      }

      if (config.scripts.minimize) {
        var ast = jsmin.parser.parse(bundleData);
        ast = jsmin.uglify.ast_mangle(ast);
        ast = jsmin.uglify.ast_squeeze(ast);
        bundleData = jsmin.uglify.gen_code(ast);
      }

      fs.writeFileSync(bundleFilePath, bundleData, 'utf-8');
    }
  }

  return {
    init : function (configInit, staticFileControllerInit) {
      config = configInit;
      staticFileController = staticFileControllerInit;
      createCssBundle();
      createScriptsBundle();
    },

    bundleScripts : function (request, response) {
      request.url = path.join(config.scripts.path, '_bundled.js');
      env.info('BundleController: Rewriting request as ' + request.url);
      staticFileController.file(request, response);
      return;
    },

    bundleCss : function (request, response) {
      request.url = path.join(config.css.path, '_bundled.css');
      env.info('BundleController: Rewriting request as ' + request.url);
      staticFileController.file(request, response);
      return;
    }
  };
    
}());

typeof(exports) != 'undefined' ? exports.BundleController = BundleController : null;

Nothing wierd here. It just bundles my scripts in alphabetical order, except for a few scripts and css files that are prioritized. Normalize.css gets top place in the bundling process and so would jQuery if I would host it myself.

I use node-cssmin for CSS minification and UglifyJS for JavaScript minification. None of this is in the production code yet since it was completed about 15 minutes ago and I need to do more testingon the JavaScript side, but on the development server the CSS bundling and minification works like a charm and has cut my CSS size by one third, but most importantly it has replaced 4 http requests with just one and cut the time for fetching css by 3/4.

The BundleController gets its configuration from index.js and it looks like this:

var bundleConfig = {
  scripts: {
    path: '/scripts',
    minimize: true,
    compress: true
  },
  css: {
    path: '/css',
    minimize: true,
    compress: true
  }
}

For now I don’t act on the compress property, but when I find a good way to send gzip files if the client supports it through node-static then at least I already have it configured.

I also added two new routes to my configuration:

routes['blog:/scripts'] = masterController.bundleScripts;
routes['blog:/css'] = masterController.bundleCss;

That’s really all there is to it. If I request lofjard.se/css/normalize.css I get the regular, untouched CSS file, but if I go to lofjard.se/css I get the minified version of all CSS files in the /css directory.

by Mikael Lofjärd
I'm sorry, but comments are not implemented yet.

Sorry, sharing is not available as a feature in your browser.

You can share the link to the page if you want!

URL