iammerrick

Hi, I'm Merrick and I love code so much I married it. Can provide proof upon request.

May 10th 2013 - 3 min read

Source Modification With r.js

One of the great principles of Require.js is that you shouldn't need a build step during development, you should simply be able to refresh the browser and see your changes reflected. This does not mean, however, that you should also have to compromise on performance. Often times it makes sense to modify your application's source at build time for performance reasons. This includes minification, annotation, inlining of text dependencies, or even transpiling one source format to another. Require.js offers the r.js build tool to optimize your code for production as well as a robust plugin system. This article will focus on the former, using Angular.js dependency injection annotations and ngmin as an example.

The Problem

In Angular.js a dependency injection system is used to resolve dependencies and provide them at run time. Dependencies use constructor function argument names to match dependencies and provide them at run time. For example:


angular.module('people')
  .controller('MyCtrl', function($scope, $http) {
    // $scope and $http were resolved by name and provided here.
  });

Please see the JavaScript Dependency Injection article for a more detailed explanation.

This approach becomes problematic at the minification phase of a project because when the argument names are mangled they can no longer be properly mapped to dependencies.

angular.module('people')
  .controller('MyCtrl', function(a, b) {
    // WTF is a or b?
  });

For the above reason Angular.js provides a build safe approach for declaring dependencies which involves using strings to annotate dependencies.

angular.module('people')
  .controller('MyCtrl', ['$scope', '$http', function(a, b) {
    // Ok so a is $scope and b is $http.
  }]);

This certainly works and is more akin to how we declare AMD dependencies, but doing these annotations means we duplicate our dependency declarations once in the array annotations and again in the function arguments. Worse, since we don't have the ability to use something like the excellent CommonJS sugar Require.js provides, we are forced to maintain two disparate lists of dependencies and match them up using order instead of variable declarations.

Wouldn't it be great if we could use a tool to perform these annotations for us? Enter ngmin.

ngmin

ngmin is a preprocessor which parses your code for injectable constructor functions and annotates them automatically making your Angular.js code "build safe".

ngmin somefile.js somefile.annotate.js

This command would output "somefile.annotate.js" which would be an annotated version of some file.

As a side note, ngmin also offers a grunt task and a Rails Asset Pipeline plugin.

Using ngmin is well and good and all but we now have an additional step of added complexity for every build we perform. A developer needs to run a concatenator (or dependency tracer), ngmin, and then the minifier. All of this before or after other application specific build tools. To make things worse order matters in many of these cases so running different tasks in parallel becomes difficult.

Enter r.js.

r.js

r.js is the defacto build tool for AMD driven projects and thanks to its extensible callbacks we can perform source modification using things like ngmin. This way developers will run "r.js" causing concatenation, annotation and minification to be taken care of by a single system. This helps reduce complexity in a build system by decreasing the number of cognitive steps to one instead of three.

Solution

r.js offers an excellent build hook "onBuildRead" which is invoked for each module, the return value of this hook will be used for the built file prior to minification. For performance reasons r.js will only invoke this on your bundled modules by default. I recommend setting "normalizeDirDefines" to "all" which means these modifications will be run on all files, not just your bundled modules. The reason I make this recommendation is because I believe you should run your unit tests after the build process and since unit tests are executed against individual modules you will need your source modifications to run against those as well. It is important to remember that tools like UglifyJS, r.js or ngmin aren't flawless.

({
  dir: 'javascripts-built',
  baseUrl: 'javascripts',
  modules: [{
      name: 'MyApplication'
  }],
  normalizeDirDefines: 'all',
  onBuildRead: function (moduleName, path, contents) {
    return require('ngmin').annotate(contents);
  }
})

Now all of "MyApplication" and its child modules will be run through ngmin, and minified afterwards. This means we can unit test those children as well. The combination of "onBuildRead" and "normalizeDirDefines" empowers us to perform testable source modification at build time.