ÆFLASH

Grunt-Browserify 2.x and Browserify-Shim

Much has changed with Grunt and Browserify over the past several months. Unfortuantely, at the time of my last Grunt/Browserify guide, the versions of the plugins I was using were already out of date. Since then, Browserify, grunt-browserify, and browserify-shim have all gone through major version updates, and as the semantic version implies, there are many backwards-incompatible changes. This guide will go over the changes needed to get something like what is described in the previous guide working. It is targeting browserify@3.44.2, grunt-browserify@2.0.3 and browserify-shim@3.4.1, although newer versions will likely work.

Major Changes

The largest change, is that grunt-browserify no longer includes browserify as a dependency -- it is now a peerDependency. You have to npm install both browserify and grunt-browserify. Also, grunt-browserify no longer includes browserify-shim, so you will also have to npm install it.

Much of the configuration now has to go directly in the package.json for everything to work. Unfortunately, the Gruntfile.js can no longer be the single point of configuration for your builds. On the upside, this does give more flexibility in how you can create bundles, and opens up the use of other tools besides Grunt.

Example Configuration

This example project will set up the build for a Backbone/Marionette application that uses jQuery and Lodash, as well as dustjs-linked in for templating. It will also use the dual-bundle strategy of separating the third-party libraries from the main app for faster rebuilds.

package.json

Here are the relevant bits from the package.json:

  "devDependencies": {
    "browserify": "3.44.2",
    "browserify-shim": "3.4.1",
    "grunt": "0.4.4",
    "grunt-browserify": "2.0.3",
    //...
  },
  "dependencies": {
    "dustjs-linkedin": "2.3.4",
    "lodash": "2.4.1",
    //...
  },
  "browser": {
    "jquery": "./app/vendor/jquery-2.1.0.js",
    "lodash": "./node_modules/lodash/dist/lodash.compat.js",
    "backbone": "./app/vendor/backbone-1.1.js",
    "marionette": "./app/vendor/backbone.marionette.js"
  },
  "browserify-shim": "./shims.js"

We have updated the browserify plugins, and are including Dust.js and Lodash from npm. We are also simply keeping static versions of jQuery, Backbone, and Marionette in a vendor/ directory.

What is new here is the browser field. This was created so modules could direct Browserify to alternate versions of packages for use on the browser. It is very similar to the alias feature from previous versions of grunt-browserify. Here we are telling browserify where to find jquery, backbone, and marionette, as well as using the compatibility version of lodash, rather than the node-optimized version.

Also new is the browserify-shim field. Browserify-shim is currently only configurable through the package.json. This is similar to the shims field from previous versions of grunt-browserify. We could have placed the configuration here, but it is more useful to have it in a separate file.

shims.js

module.exports = {
  jquery: {exports: "jQuery"},
  lodash: {exports: "_"},
  backbone: {
    exports: "Backbone",
    depends: {lodash: "underscore", jquery: "jQuery"}
  },
  marionette: {
    exports: "Marionette",
    depends: {lodash: "underscore", jquery: "jQuery", backbone: "Backbone"}
  }
};

The browserify-shim config must be the long form described in the readme, and is just a simple JS module. It defines what each library's global export is, as well as what the dependencies are (if it requires them). We can specify simply "jquery" rathern than "./path/to/jquery-2.1.js" due to the browser field in the package.json. Browserify-shim also takes that field in to account.

Another thing to point out is that browserify-shim is not as fast as it used to be, since it now parses each source file, rather than just appending a footer and a header. There actually is no speed benefit to shimming jQuery and Lodash, since they now support CommonJS out of the box, but I included them here for demonstration purposes. I would say the dual-bundle strategy is even more important, as builds for large applications can take several seconds.

Gruntfile.js

  //...
  var
    shims = require("./shims"),
    sharedModules = Object.keys(shims).concat([
      // place all modules you want in the lib build here
      "dustjs-linkedin"
    ]);

  grunt.initConfig({
    //...
    browserify: {
      lib: {
        files: {
          "tmp/lib.browserify.js": ["app/vendor/lib.js"]
        },
        options: {
          transform: ["browserify-shim"],
          require: sharedModules
        }
      },
      main: {
        files: {
          "tmp/main.browserify.js": ["app/main.js"]
        },
        options: {
          external: sharedModules
        }
      }
    },
    //...
  });

Many things are going on here. First of all, we are requiring the same config file the browserify-shim uses to prevent some duplication. Each of the keys of that file's exports (the names of the modules) will need to be included in the lib build, and marked as external in the main build. We can also add more module names to that list to move them from the main build to the lib build, as we are doing with dustjs-linkedin.

In the lib build, we use the browserify-shim transform, and explicitly require each of the sharedModules. In the main build, we do not use the browserify-shim transform, since it significantly slows things down, and mark each of the sharedModules as external. We also dont use the prelude option described in the previous guide to specify an un-minified module loader. There is now no way to override this, so debugging require()s in the browser will be a bit more cumbersome.

These two bundles will be concatenated together exactly as described in the previous guide. Don't forget the semicolon separator!

All in all, the browserify config is much simpler. However, there is still one more thing we need to do.

app/vendor/lib.js

var
  lodash = require("lodash"),
  jquery = require("jquery"),
  backbone = require("backbone");

require("marionette");

// Backbone and Marionette rely on $, _, and Backbone being in the global
// scope. Call noConflict() after the libs initialize above.
lodash.noConflict();
jquery.noConflict(true);
backbone.noConflict();

In the entry module for the lib build, we need to require each of the shimmed modules in order, and then call noConflict() on each of them to avoid polluting the global window object. This is because when a shimmed module depends on another, browserify-shim will insert a global variable with a reference to that dependency. This might not be a big deal for every app, but it is generally best practice to avoid attatching polluting the global scope, especially if your app wil live with other third-party scripts. It is a bit annoying and inelegant that we have to do this, but browserify-shim does this to ensure maximum compatibility with non-CommonJS-aware libraries. It's just one of the ways the Browserify abstraction leaks.

Conclusion

I hope this guide was helpful to those of you who are having issues getting the build to work as it did with prior versions of these tools. I do think a project set up in this way is in a better place than a pure Grunt approach. For example, since most things are configured through the package.json you can build your app entirely through the command line:

$ browserify app/lib/lib.js app/main.js -t browserify-shim -r lodash > dist/js/app.js

Granted, you lose the speed advantages of a slower lib build and a fast main plus a concatenation, but it opens your app up to powerful analysis tools like colony and disc and anything else that respects the browser field. I would say these benefits offset the drawbacks of losing Gruntfile.js as the single source of configuration.

Happy Browserifying!

code javascript modules commonjs browserify grunt Gruntfile livereload