Minifying Browserified Angular Modules

If you’ve used Angular in production, you’ve probably learned the importance of annotating your dependencies when using a minifier like Uglify: when parameter names get mangled, Angular’s implicit annotation feature breaks. The ng-annotate package does a great job at alleviating us of this responsibility, but it may need a little hint when you’re using something like Browserify to organize your code as CommonJS modules.

Let’s describe the problem with a little bit of code. First, a simple Angular app authored as a CommonJS module:

var angular = require('angular');

angular
  .module('GreeterApp', [])
  .controller('greeterController', require('./greeter-controller'))
  .factory('greeter', require('./greeter'));

We’ve already installed Angular through npm, so this is all ready to be Browserified. Here’s the Gulp pipeline that does just that:

var
	gulp = require('gulp'),
	browserify = require('browserify'),
	buffer = require('vinyl-buffer'),
	source = require('vinyl-source-stream'),
	ngAnnotate = require('gulp-ng-annotate'),
	uglify = require('gulp-uglify');

gulp.task('default', function() {
  return browserify('./app.js')
    .bundle()
    .pipe(source('app.js'))
    .pipe(buffer())
    .pipe(ngAnnotate())
    .pipe(uglify())
    .pipe(gulp.dest('./dist'));
});

It packages up app.js with Browserify > ng-annotate > Uglify and then drops it into the dist directory.

The greeter-controller.js doesn’t have much to it, either:

module.exports = function($scope, greeter) {
  $scope.greeting = greeter.getGreeting('english');
};

The controller gets an English greeting from the greeter service and puts it in the scope for the view to display. Notice the lack of explicit annotations here; we’re hoping ng-annotate will take care of that for us.

But when we build and run the app, we get the following error in the browser console:

Error: [$injector:unpr] Unknown provider: eProvider <- e <- greeterController
http://errors.angularjs.org/1.4.1/$injector/unpr?p0=eProvider%20%3C-%20e%20%3C-%20greeterController
    at app.js:1
    at app.js:1
    at Object.r [as get] (app.js:1)
    at app.js:1
    at r (app.js:1)
    at Object.i [as invoke] (app.js:1)
    at $get.f.instance (app.js:2)
    at v (app.js:1)
    at s (app.js:1)
    at s (app.js:1)

The documentation page for the error decodes this for us:

This error results from the $injector being unable to resolve a required dependency.

Investigating our ./dist/app.js, we notice the controller has been turned to:

t.exports=function(e,t){e.greeting=t.getGreeting("english")}

No annotations! Angular is looking for a provider for whatever e is, rather than $scope!

What looks like ng-annotate failing to do its job is really just us failing to read the docs very closely. It turns out the CommonJS module pattern is one of those un-common cases that the ng-annotate documentation loudly warns us about:

ng-annotate works by using static analysis to identify common code patterns. There are patterns it does not and never will understand and for those you can use an explicit ngInject annotation instead, see section further down.

The section it refers to gives us a few options that can solve our problem. My favorite is prepending the function declaration with /*@ngInject*/:

module.exports = /*@ngInject*/ function($scope, greeter) {
  $scope.greeting = greeter.getGreeting('english');
};

When we re-run the build, our output includes all of the annotations we were missing:

t.exports=["$scope","greeter",function(e,t){e.greeting=t.getGreeting("english")}]

And all is well.