Skip to Content

Creating a useful AngularJS project structure and toolchain

Posted on    18 mins read

About

This article describes in great detail what I learned about setting up AngularJS applications in terms of project structure, tools, dependency management, test automation and code distribution. The result is a seed project that is easily extendable, clearly structured, self-documenting, and comfortable to work with.

Requirements

One of the key points of this setup is that everything that is needed for the application itself to run and the tests to works, and every other relevant task related to developing the application, is pulled in through tool-based dependency management. However, the tools that do the job need a basic global setup – which is not specific to the project itself – available on your development machine: Node.js, the Grunt CLI, and Bower.

The following steps describe how to create this setup on a Linux or Mac OS X system.

Step 1: Installing Node.js

Node.js is the server-side runtime environment for JavaScript based on the V8 engine by Google. We use its package manager, NPM, to pull in external dependencies for our project which we will use during development, like Grunt and its plugins for handling all development-related tasks, Bower for managing external libraries, or Karma for running test cases.

  • Download the latest available version of the source code of Node.js from http://nodejs.org/download. As of this writing, the latest version is 0.10.28, available as node-v0.10.28.tar.gz.
  • Run tar xvfz node-v0.10.28.tar.gz to extract the archive.
  • Run ./configure && make && sudo make install in order to compile and install the software
  • .

Step 2: Installing the Grunt CLI

The Grunt Command Line Interface is a Node.js module which allows us to execute the Grunt tasks of our project on the command line. This way, we can handle every task that is related to the development process of the application – e.g. syntax checking, code style verification, running the unit tests, building a distributable – through a single interface, and we can configure all available tasks in great detail, making Grunt the one-stop assistant for all our project needs (with the notable exception of writing code and tests, which, I’m afraid, still has to be done by us).

The Grunt CLI ships as a Node.js module and is therefore installed using the Node Package Manager, NPM:

npm install -g grunt-cli

The -g switch installs the command line tool globally, that is, to a central location on our system. Grunt needs additional modules to actually do something useful, but these will be installed locally to the project location, as we will see later. The globally installed command line tool, which is invoked as grunt, will then use these local modules to execute the tasks for our project.

Step 3: Installing Bower

The final global requirement we will need is Bower. Bower is to frontend JavaScript libraries what NPM is to backend Node.js libraries. A package and dependency manager that pulls libraries like AngularJS, Restangular, jQuery, Underscore, and whatever else is needed, into our project from remote locations. It frees us from downloading these libraries by hand and putting them into a folder structure of our project manually.

In order to make the command line tool bower available on our system, we install it through NPM just as we did with the Grunt CLI:

npm install -g bower

Creating an initial project structure

With all global requirements installed and in place, we can now start to create our project, putting NPM, Grunt and Bower to use.

Dependency management

The first step is to define the local dependencies of our project – we need several backend libraries (managed through NPM) and several frontend libraries (managed through Bower). Both tools need a JSON configuration file that defines these dependencies. Let’s start with the NPM packages by creating a file package.json in our project root:

package.json

{
  "name": "Example",
  "namelower": "example",
  "version": "0.0.1",
  "description": "An example AngularJS project",
  "readme": "README.md",
  "repository": {
    "type": "git",
    "url": "git@git.example.com:example.git"
  },
  "devDependencies": {
    "grunt": "0.4.2",
    "grunt-contrib-concat":     "0.3.0",
    "grunt-contrib-copy":       "0.5.0",
    "grunt-contrib-jshint":     "0.8.0",
    "grunt-contrib-nodeunit":   "0.3.0",
    "grunt-contrib-uglify":     "0.2.2",
    "grunt-contrib-watch":      "0.5.3",
    "grunt-jsdoc":              "0.5.4",
    "grunt-exec":               "0.4.5",
    "grunt-karma":              "0.8.3",
    "karma":                    "0.12.16",
    "karma-jasmine":            "0.1.5",
    "karma-phantomjs-launcher": "0.1.4"
  },
  "scripts": {
    "postinstall": "bower install"
  }
}

As you can see, these are all development dependencies; none of these packages are needed to run the actual application. We are going to pull in the local grunt module and several of its plugins, and we make Karma, the AngularJS test runner, available. Satisfying these dependencies allows us to use the Grunt command line interface on this project and enables us to run Jasmine test cases against the PhantomJS headless browser.

Note the scripts block: we can use the postinstall statement to make NPM run bower install after installing its modules – this way, we need to execute only one command in order to pull in development and application dependencies.

We also need a configuration file for Bower, again in the root directory of the project, named bower.json:

bower.json

{
    "name": "example",
    "version": "0.0.0",
    "dependencies": {
        "angular":          "1.2.16",
        "angular-route":    "1.2.16",
        "angular-sanitize": "1.2.16",
        "angular-mocks":    "1.2.16",
        "jquery":           "1.8.3",
        "underscore":       "1.6.0",
        "restangular":      "1.4.0"
    },
    "analytics": false
}

These are all dependencies which the application needs to actually run.

Both JSON files belong into version control; however, the libraries they pull in should not be added to a repository; instead, they should be ignored:

~# echo "node_modules/" >> .gitignore
~# echo "bower_components/" >> .gitignore

We can now start to create our very first test and implementation code and see if the setup is able to run the test case.

Setting up the test infrastructure

We start by creating a specification for an ExampleController in a new folder called test:

test/ExampleControllerSpec.js

describe('ExampleController', function() {
    var scope, controller, httpBackend;

    // Initialization of the AngularJS application before each test case
    beforeEach(module('ExampleApp'));

    // Injection of dependencies, $http will be mocked with $httpBackend
    beforeEach(inject(function($rootScope, $controller, $httpBackend) {
        scope = $rootScope;
        controller = $controller;
        httpBackend = $httpBackend;
    }));

    it('should query the webservice', function() {

        // Which HTTP requests do we expect to occur, and how do we response?
        httpBackend.expectGET('/users').respond('[{"name": "First User"}, {"name": "Second User"}]');

        // Starting the controller
        controller('ExampleController', {'$scope': scope });

        // Respond to all HTTP requests
        httpBackend.flush();

        // Triggering the AngularJS digest cycle in order to resolve all promises
        scope.$apply();

        // We expect the controller to put the right value onto the scope
        expect(scope.firstUsername).toEqual('First User');

    });

});

We now need to set up the Karma test runner configuration, which will allow us to run the test case. To do so, we need to create a configuration file for Karma at the root folder of the project, called karma.conf.js:

karma.conf.js

module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',

    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['jasmine'],

    files: [
      'bower_components/jquery/jquery.js',
      'bower_components/angular/angular.js',
      'bower_components/angular-route/angular-route.js',
      'bower_components/angular-sanitize/angular-sanitize.js',
      'bower_components/angular-mocks/angular-mocks.js',
      'bower_components/restangular/dist/restangular.js',
      'bower_components/underscore/underscore.js',
      'bower_components/underscore/underscore.js',
      'test/**/*Spec.js',
      'source/**/*.js'
    ],

    // list of files to exclude
    exclude: [

    ],

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {

    },

    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress'],

    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: false,

    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['PhantomJS'],

    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: true
  });
};

Let’s see if we can run the specification. We don’t expect it to pass, of course, because we have not yet written a single piece of implementation code – however, at this point it is interesting to see if the infrastructure we created so far is already sufficient to run test cases.

If our setup is correct, then we should be able to launch Karma, the AngularJS test runner, as soon as our project dependencies are put in place through NPM. The following commands all need to be executed at the top level of the project folder structure.

~# npm install

This installs all local Node.js dependencies and then runs bower install, which installs the application dependencies. We can now run Karma:

~# ./node_modules/karma/bin/karma start karma.conf.js

Which results in a failing test run, but should demonstrate that the testcase can at least be executed:

INFO [karma]: Karma v0.12.16 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
WARN [watcher]: Pattern "/path/to/project/source/**/*.js" does not match any file.
INFO [PhantomJS 1.9.7 (Linux)]: Connected on socket ipy4Oxm5mNuYz-NrJ_PB with id 14596927
PhantomJS 1.9.7 (Linux) ExampleController should query the webservice FAILED
    Error: [$injector:modulerr] Failed to instantiate module ExampleApp due to:
    Error: [$injector:nomod] Module 'ExampleApp' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.
...
PhantomJS 1.9.7 (Linux): Executed 1 of 1 (1 FAILED) ERROR (0.042 secs / 0.011 secs)

A first implementation

We will now add the implementation code that is neccessary to make the test case pass. Let’s start by adding controller code to source/controllers.js:

source/controllers.js

/**
 * This is an example controller.
 * It triggers the UserdataService and puts the returned value on the scope
 *
 * @see services
 */
var controllers = angular.module('ExampleApp.controllers', [])
    .controller('ExampleController', function ($scope, UserdataService) {

        UserdataService.getFirstUsername().then(function(firstUsername) {
            $scope.firstUsername = firstUsername;
        });

    });

This obviously needs a UserdataService, so let’s implement that, too:

source/services.js

/**
 * Restangular-based data service, fetches user data from the backend
 *
 * @see https://github.com/mgonto/restangular
 */
var services = angular.module('ExampleApp.services', [])
    .factory('UserdataService', ['Restangular', '$q', function UserdataService(Restangular, $q) {
        return {
            /**
             * @function getFirstUsername
             * @returns a Promise that eventually resolves to the username of the first user
             */
            getFirstUsername: function() {
                var firstUsernameDeferred = $q.defer();
                var response = Restangular.one('users').getList().then(function(response) {
                    firstUsernameDeferred.resolve(response[0].name);
                });
                return firstUsernameDeferred.promise;
            }
        };
    }]);

Finally, we can set up the application in source/app.js:

source/app.js

/**
 * Setup of main AngularJS application, with Restangular being defined as a dependency.
 *
 * @see controllers
 * @see services
 */
var app = angular.module('ExampleApp',
    [
        'restangular',
        'ExampleApp.controllers',
        'ExampleApp.services'
    ]
);

Now the testcase runs and passes:

~# ./node_modules/karma/bin/karma start karma.conf.js

INFO [karma]: Karma v0.12.16 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.7 (Linux)]: Connected on socket z_SJLuQxFZkHxLO_Zk5x with id 41252485
PhantomJS 1.9.7 (Linux): Executed 1 of 1 SUCCESS (0.038 secs / 0.022 secs)

Running unit tests is the first of many differents tasks that need to be performed while developing a project. We can configure all these tasks into Grunt, which results in a unified interface for managing our project tasks. In order to set up unit test execution as a Grunt task, we need to describe it in a file named Gruntfile.js in our project root folder:

Gruntfile.js

module.exports = function (grunt) {

  grunt.loadNpmTasks('grunt-karma');

  grunt.initConfig({
    'meta': {
      'jsFilesForTesting': [
        'bower_components/jquery/jquery.js',
        'bower_components/angular/angular.js',
        'bower_components/angular-route/angular-route.js',
        'bower_components/angular-sanitize/angular-sanitize.js',
        'bower_components/angular-mocks/angular-mocks.js',
        'bower_components/restangular/dist/restangular.js',
        'bower_components/underscore/underscore.js',
        'bower_components/underscore/underscore.js',
        'test/**/*Spec.js'
      ]
    },

    'karma': {
      'development': {
        'configFile': 'karma.conf.js',
        'options': {
          'files': [
            '<%= meta.jsFilesForTesting %>',
            'source/**/*.js'
          ],
        }
      }
    }
  });

  grunt.registerTask('test', ['karma:development']);

};

This configures the task karma:development using the grunt-karma plugin, plus an alias test that maps to karma:development. Note that Grunt isn’t just a dumb command executor like a bash script – with its specialized plugins, it understands the tools it runs in its tasks; as you can see, we configure the basic set of files that are needed for a test run in meta.jsFilesForTesting, and then use this set in karma.development.options.files. Later, we are going to define other Karma subtasks (in addition to karma.development), pointing at the basic set of files but including other files than source/**/*js.

Don’t worry if this doesn’t make sense right now; we will take it step by step.

Because now we configure the files set within the Grunt config files, we no longer need it in our karma.conf file:

karma.conf.js

module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',

    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['jasmine'],

    // list of files to exclude
    exclude: [

    ],

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {

    },

    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress'],

    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: false,

    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['PhantomJS'],

    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: true
  });
};

With this, we can start the unit test run using Grunt:

~# grunt test

Note that it doesn’t matter in which subfolder of our project we are while running this command – Grunt gets the paths right for us, another benefit of using Grunt that makes working with our project more comfortable.

Linting code using Grunt

Let’s see if we can find other useful tasks that Grunt can handle for us. An important building block of any useful JavaScript development workflow is linting, i.e., checking our code base for syntax and style errors. The tool of choice here is JSHint, and Grunt makes it easy to integrate it into the workflow:

Gruntfile.js

module.exports = function (grunt) {

  grunt.loadNpmTasks('grunt-karma');
  grunt.loadNpmTasks('grunt-contrib-jshint');

  grunt.initConfig({

    'meta': {
      'jsFilesForTesting': [
        'bower_components/jquery/jquery.js',
        'bower_components/angular/angular.js',
        'bower_components/angular-route/angular-route.js',
        'bower_components/angular-sanitize/angular-sanitize.js',
        'bower_components/angular-mocks/angular-mocks.js',
        'bower_components/restangular/dist/restangular.js',
        'bower_components/underscore/underscore.js',
        'bower_components/underscore/underscore.js',
        'test/**/*Spec.js'
      ]
    },

    'karma': {
      'development': {
        'configFile': 'karma.conf.js',
        'options': {
          'files': [
            '<%= meta.jsFilesForTesting %>',
            'source/**/*.js'
          ],
        }
      },
    },

    'jshint': {
      'beforeconcat': ['source/**/*.js'],
    }

  });

  grunt.registerTask('test', ['karma:development']);

};

With this, running grunt jshint lints our source code files:

~# grunt jshint

Running "jshint:beforeconcat" (jshint) task
>> 3 files lint free.

Done, without errors.

Real developers ship

Until now we have only handled our source files. But that’s probably not the format we want to use when shipping our application to production. A minified single file with all our source code concatenated together is usually the preferred end result.

The goal is to end up with two files in the dist folder of our project root:


bower_components/
bower.json
dist/
    example-0.0.1.js
    example-0.0.1.min.js
.gitignore
Gruntfile.js
node_modules/
karma.conf.js
package.json
source/
test/

As you can see, the file name contains the name and version of our project. Grunt can be configured to derive these values from the package.json file of our project. Then, the concat plugin can be used to generate a non-minified dist file that contains the content of all source files concatenated together, and the uglify plugin then generates a minified version of this file. Let’s look at the changes to our Gruntfile:

Gruntfile.js

module.exports = function (grunt) {

  grunt.loadNpmTasks('grunt-karma');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');

  grunt.initConfig({
    'pkg': grunt.file.readJSON('package.json'),

    'meta': {
      'jsFilesForTesting': [
        'bower_components/jquery/jquery.js',
        'bower_components/angular/angular.js',
        'bower_components/angular-route/angular-route.js',
        'bower_components/angular-sanitize/angular-sanitize.js',
        'bower_components/angular-mocks/angular-mocks.js',
        'bower_components/restangular/dist/restangular.js',
        'bower_components/underscore/underscore.js',
        'bower_components/underscore/underscore.js',
        'test/**/*Spec.js'
      ]
    },

    'karma': {
      'development': {
        'configFile': 'karma.conf.js',
        'options': {
          'files': [
            '<%= meta.jsFilesForTesting %>',
            'source/**/*.js'
          ],
        }
      },
    },

    'jshint': {
      'beforeconcat': ['source/**/*.js'],
    },

    'concat': {
      'dist': {
        'src': ['source/**/*.js'],
        'dest': 'dist/<%= pkg.namelower %>-<%= pkg.version %>.js'
      }
    },

    'uglify': {
      'options': {
        'mangle': false
      },
      'dist': {
        'files': {
          'dist/<%= pkg.namelower %>-<%= pkg.version %>.min.js': ['dist/<%= pkg.namelower %>-<%= pkg.version %>.js']
        }
      }
    },

  });

  grunt.registerTask('test', ['karma:development']);

};

Now we can run both tasks…

~# grunt concat
~# grunt uglify

…and the dist files will be generated.

From single tasks to complete workflows

Another really nice aspect of managing tasks through Grunt is that tasks can be combined into workflows. A very likely workflow during development is to lint and unit test our code, then concatenate and minify it if no errors were detected. Instead of running each of these tasks manually, we can use registerTask to create a new task which executes the other tasks for us:

Gruntfile.js

module.exports = function (grunt) {

  grunt.loadNpmTasks('grunt-karma');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');

  grunt.initConfig({
    'pkg': grunt.file.readJSON('package.json'),

    'meta': {
      'jsFilesForTesting': [
        'bower_components/jquery/jquery.js',
        'bower_components/angular/angular.js',
        'bower_components/angular-route/angular-route.js',
        'bower_components/angular-sanitize/angular-sanitize.js',
        'bower_components/angular-mocks/angular-mocks.js',
        'bower_components/restangular/dist/restangular.js',
        'bower_components/underscore/underscore.js',
        'bower_components/underscore/underscore.js',
        'test/**/*Spec.js'
      ]
    },

    'karma': {
      'development': {
        'configFile': 'karma.conf.js',
        'options': {
          'files': [
            '<%= meta.jsFilesForTesting %>',
            'source/**/*.js'
          ],
        }
      },
    },

    'jshint': {
      'beforeconcat': ['source/**/*.js'],
    },

    'concat': {
      'dist': {
        'src': ['source/**/*.js'],
        'dest': 'dist/<%= pkg.namelower %>-<%= pkg.version %>.js'
      }
    },

    'uglify': {
      'options': {
        'mangle': false
      },
      'dist': {
        'files': {
          'dist/<%= pkg.namelower %>-<%= pkg.version %>.min.js': ['dist/<%= pkg.namelower %>-<%= pkg.version %>.js']
        }
      }
    },

  });

  grunt.registerTask('test', ['karma:development']);
  grunt.registerTask('build',
    [
      'jshint',
      'karma:development',
      'concat',
      'uglify'
    ]);

};

Now, running grunt build will execute this workflow.

Real developers ship tested files

We are not yet using our Grunt-based task management approach to its full potential; now that the most important moving parts are in place, reaping additional benefits is simple. It would be great, for example, to unit test our dist files, too – this way we get additional safety in regards to the stability of our code without extra costs. All that is needed to achieve this is to add two additional Karma subtasks, and to make them part of the build workflow:

Gruntfile.js

module.exports = function (grunt) {

  grunt.loadNpmTasks('grunt-karma');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');

  grunt.initConfig({
    'pkg': grunt.file.readJSON('package.json'),

    'meta': {
      'jsFilesForTesting': [
        'bower_components/jquery/jquery.js',
        'bower_components/angular/angular.js',
        'bower_components/angular-route/angular-route.js',
        'bower_components/angular-sanitize/angular-sanitize.js',
        'bower_components/angular-mocks/angular-mocks.js',
        'bower_components/restangular/dist/restangular.js',
        'bower_components/underscore/underscore.js',
        'bower_components/underscore/underscore.js',
        'test/**/*Spec.js'
      ]
    },

    'karma': {
      'development': {
        'configFile': 'karma.conf.js',
        'options': {
          'files': [
            '<%= meta.jsFilesForTesting %>',
            'source/**/*.js'
          ],
        }
      },
      'dist': {
        'options': {
          'configFile': 'karma.conf.js',
          'files': [
            '<%= meta.jsFilesForTesting %>',
            'dist/<%= pkg.namelower %>-<%= pkg.version %>.js'
          ],
        }
      },
      'minified': {
        'options': {
          'configFile': 'karma.conf.js',
          'files': [
            '<%= meta.jsFilesForTesting %>',
            'dist/<%= pkg.namelower %>-<%= pkg.version %>.min.js'
          ],
        }
      }
    },

    'jshint': {
      'beforeconcat': ['source/**/*.js'],
    },

    'concat': {
      'dist': {
        'src': ['source/**/*.js'],
        'dest': 'dist/<%= pkg.namelower %>-<%= pkg.version %>.js'
      }
    },

    'uglify': {
      'options': {
        'mangle': false
      },
      'dist': {
        'files': {
          'dist/<%= pkg.namelower %>-<%= pkg.version %>.min.js': ['dist/<%= pkg.namelower %>-<%= pkg.version %>.js']
        }
      }
    },

  });

  grunt.registerTask('test', ['karma:development']);
  grunt.registerTask('build',
    [
      'jshint',
      'karma:development',
      'concat',
      'karma:dist',
      'uglify',
      'karma:minified'
    ]);

};

And with this, we have at our fingertips a complete AngularJS workflow: our source code is linted and tested, merged into a distribution file, which is tested, too, and then minified and tested again. Our application is verified and ready to ship with a single command:

~# grunt build

Running "jshint:beforeconcat" (jshint) task
>> 3 files lint free.

Running "karma:development" (karma) task
INFO [karma]: Karma v0.12.16 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.7 (Linux)]: Connected on socket F7fLBEBaNrHk2cU6CYFq with id 89087960
PhantomJS 1.9.7 (Linux): Executed 1 of 1 SUCCESS (0.036 secs / 0.039 secs)

Running "concat:dist" (concat) task
File "dist/example-0.0.1.js" created.

Running "karma:dist" (karma) task
INFO [karma]: Karma v0.12.16 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.7 (Linux)]: Connected on socket jGrx9Gvwj_NL2hdXCYXx with id 25650934
PhantomJS 1.9.7 (Linux): Executed 1 of 1 SUCCESS (0.042 secs / 0.022 secs)

Running "uglify:dist" (uglify) task
File "dist/example-0.0.1.min.js" created.

Running "karma:minified" (karma) task
INFO [karma]: Karma v0.12.16 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.7 (Linux)]: Connected on socket 4X26gG7Ob53ePR4DCYhQ with id 88795558
PhantomJS 1.9.7 (Linux): Executed 1 of 1 SUCCESS (0.038 secs / 0.022 secs)

Done, without errors.

Effortless documentation

With a first complete workflow in place, we can now add other useful steps; for example, generating a JSDoc documentation from our inline source code comments is straight-forward:

First, we add the plugin and task configuration for JSDoc to the Gruntfile, and make the jsdoc task part of our build workflow:

Gruntfile.js

module.exports = function (grunt) {

  grunt.loadNpmTasks('grunt-karma');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-jsdoc');

  grunt.initConfig({
    'pkg': grunt.file.readJSON('package.json'),

    'meta': {
      'jsFilesForTesting': [
        'bower_components/jquery/jquery.js',
        'bower_components/angular/angular.js',
        'bower_components/angular-route/angular-route.js',
        'bower_components/angular-sanitize/angular-sanitize.js',
        'bower_components/angular-mocks/angular-mocks.js',
        'bower_components/restangular/dist/restangular.js',
        'bower_components/underscore/underscore.js',
        'bower_components/underscore/underscore.js',
        'test/**/*Spec.js'
      ]
    },

    'karma': {
      'development': {
        'configFile': 'karma.conf.js',
        'options': {
          'files': [
            '<%= meta.jsFilesForTesting %>',
            'source/**/*.js'
          ],
        }
      },
      'dist': {
        'options': {
          'configFile': 'karma.conf.js',
          'files': [
            '<%= meta.jsFilesForTesting %>',
            'dist/<%= pkg.namelower %>-<%= pkg.version %>.js'
          ],
        }
      },
      'minified': {
        'options': {
          'configFile': 'karma.conf.js',
          'files': [
            '<%= meta.jsFilesForTesting %>',
            'dist/<%= pkg.namelower %>-<%= pkg.version %>.min.js'
          ],
        }
      }
    },

    'jshint': {
      'beforeconcat': ['source/**/*.js'],
    },

    'concat': {
      'dist': {
        'src': ['source/**/*.js'],
        'dest': 'dist/<%= pkg.namelower %>-<%= pkg.version %>.js'
      }
    },

    'uglify': {
      'options': {
        'mangle': false
      },
      'dist': {
        'files': {
          'dist/<%= pkg.namelower %>-<%= pkg.version %>.min.js': ['dist/<%= pkg.namelower %>-<%= pkg.version %>.js']
        }
      }
    },

    'jsdoc': {
      'src': ['source/**/*.js'],
      'options': {
        'destination': 'doc'
      }
    }

  });

  grunt.registerTask('test', ['karma:development']);
  grunt.registerTask('build',
    [
      'jshint',
      'karma:development',
      'concat',
      'karma:dist',
      'uglify',
      'karma:minified',
      'jsdoc'
    ]);

};

If you don’t want the generated documentation to be part of the repository, add the doc folder to .gitignore:

~# echo "doc/" >> .gitignore

Now, generating the documentation is simple:

~# grunt jsdoc

From task management to Continuous Integration

One of the huge benefits of having each project-related task managed by a tool like Grunt is that automation is now simple. And that’s the key to the final step: Setting up the project for Continuous Integration. We will look at what needs to be done to make our project usable with TravisCI and Jenkins, respectively.

Integrating with TravisCI

We need to do some minimal changes to our project to make it able to run on TravisCI. First, let’s create the obligatory .travis.yml file:

.travis.yml

language: node_js
node_js:
  - 0.10
before_install:
  - npm install -g grunt-cli
  - npm install -g bower

As you can see, we declare our project to be a Node.js project. This is because from TravisCI’s point of view, only Node.js scripts (npm install, grunt build, karma etc.) are run for building and testing the project.

Just as we prepared our personal development environment, we need to globally install grunt-cli and bower globally.

This is all we need to put into the .travis.yml file because TravisCI runs npm install and then npm test on Node.js projects by default, after checking them out of version control. However, npm test doesn’t do anything useful yet for our project. Let’s change that by adding a line to our package.json:

package.json

{
  "name": "Example",
  "namelower": "example",
  "version": "0.0.1",
  "description": "An example AngularJS project",
  "readme": "README.md",
  "repository": {
    "type": "git",
    "url": "git@git.example.com:example.git"
  },
  "devDependencies": {
    "grunt": "0.4.2",
    "grunt-contrib-concat":     "0.3.0",
    "grunt-contrib-copy":       "0.5.0",
    "grunt-contrib-jshint":     "0.8.0",
    "grunt-contrib-nodeunit":   "0.3.0",
    "grunt-contrib-uglify":     "0.2.2",
    "grunt-contrib-watch":      "0.5.3",
    "grunt-jsdoc":              "0.5.4",
    "grunt-exec":               "0.4.5",
    "grunt-karma":              "0.8.3",
    "karma":                    "0.12.16",
    "karma-jasmine":            "0.1.5",
    "karma-phantomjs-launcher": "0.1.4"
  },
  "scripts": {
    "postinstall": "bower install",
    "test": "grunt build"
  }
}

Now, TravisCI executing npm test will result in grunt build being run. Should any single step in grunt build fail, then the TravisCI run will fail; if everything succeeds, then TravisCI will consider the run a success, too.

Going further

If you would like to use the project setup described herein as a base template for your own projects, see https://github.com/manuelkiessling/angular-seed-enhanced.