Building Robust Cordova Apps with AppBuilder and Grunt

JavaScript task runners provide elegant solutions to a number of common problems for web applications, such as asset minification, CSS preprocessing, code linting, and more. Because AppBuilder Cordova projects are web-based, it’s only natural to want to use the same task runners you use for web apps in your hybrid apps. If you’re using the AppBuilder CLI, you can!

There’s nothing about AppBuilder that precludes the use of any JavaScript task runner, but there are a few conventions that can help you get the best results. In this article I’ll explain the workflow that has worked best for me using my task runner of choice, Grunt, although these workflows can be ported to work with other task runners (e.g. Gulp) fairly easily.

If you’re an AppBuilder user and have been wanting to use Sass, JSHint, and CSSLint, or if you just want a workflow for minifiying your code, then this article is for you. Let’s get started.

If you’re already familiar with Grunt you may want to check out my City Search Challenge app. It’s an AppBuilder project that uses Grunt tasks to implement HTML/CSS/JavaScript linting, HTML/CSS/JavaScript minification, image optimization, and more. Feel free to peruse the project’s Gruntfile and copy and paste to your heart’s desire.

Article Outline

Directory structure

Whether you use Grunt, Gulp, or some other build tool, the most important thing to get right when using a task runner in AppBuilder projects is your directory structure. Here’s the directory structure I recommend, and the one I use for all of my apps:

.
└── my-project
    ├── app
    │   ├── .abproject
    │   ├── App_Resources
    │   │   └── ...
    │   ├── cordova.android.js
    │   ├── cordova.ios.js
    │   ├── cordova.wp8.js
    │   ├── css
    │   │   └── ...
    │   ├── index.html
    │   ├── js
    │   │   └── ...
    │   ├── scss
    │   │   └── ...
    │   └── ...
    ├── Gruntfile.js
    └── package.json

The most important thing here is that the AppBuilder project, in its entirety, resides in a subdirectory of the project’s root. You can name this subdirectory whatever you’d like, but I like using the name app. This is not the structure you get from running appbuilder create hybrid my-project, so it takes a bit of manual file management to get this structure in place.

Start by creating a new directory with your project’s name:

$ mkdir my-project
$ cd my-project

Next, create the AppBuilder project with a name of “app”, which creates the app directory containing the AppBuilder project.

$ appbuilder create hybrid app

Finally, to change your app’s name, navigate into the new app directory and run appbuilder prop set a few times to setup your app’s names and descriptions. Here are the configuration variables I usually set:

$ cd app
$ appbuilder prop set ProjectName "My Project"
$ appbuilder prop set DisplayName "MyProject"
$ appbuilder prop set AppIdentifier "com.tjvantoll.myproject"
$ appbuilder prop set Author "TJ VanToll"
$ appbuilder prop set Description "My amazing app"

From there you’re all set, other than one caveat: you need to remember to run appbuilder commands (e.g. appbuilder simulate, appbuilder deploy, appbuilder livesync, etc.) within the app folder, and not the root directory of the project. That is, if you look at the directory structure below, running appbuilder simulate in the app directory works, but running it in the my-project directory does not.

.
└── my-project  <-- appbuilder commands do NOT work
    ├── app     <-- appbuilder commands work
    │   └── ...
    └── ...

So given this minor hassle why use this structure?

Reason #1

It gives you a place to put files in your repository that you don’t want to be included in your app package (aka your built iOS/Android/Windows Phone app). Fewer files means quicker builds and faster LiveSync times, so it’s worth your time to make sure you’re not including files that your project needs but your app doesn’t. This includes Node resources such as Grunt or Gulp, as well as anything else you’re storing for your project but don’t need in the actual app. For instance I tend to include an assets directory where I store my design files and the screenshots I submit on iTunes Connect and Google Play. I also commonly have a tests directory that contains any unit tests my app uses.

Note: You can alternatively exclude files from app packages by using a .abignore file, but I find having the AppBuilder project in a separate directory is easier to maintain. That is, I commonly forget to maintain the .abignore file, but I remember to keep my app directory slim.

Reason #2

The other AppBuilder clients – specifically the in-browser client, the Windows client, and the Visual Studio extension — all require that the AppBuilder project resides in a subdirectory of the main repository. Therefore using an app subdirectory allows you to share your code across clients.

So to summarize, I recommend placing your AppBuilder project in an app subdirectory in the root of your repository. This is not a requirement, and you can run Grunt or Gulp tasks without this structure, however I’ll be assuming this structure is in place for the rest of the article.

Installing Grunt

Next let’s look at how to get Grunt setup so you’re ready to run tasks. If you haven’t installed Grunt before you’ll need to install its CLI from npm:

$ npm install -g grunt-cli

This gives you a grunt command that you can use to run tasks defined in a Gruntfile (which we’ll get to creating shortly). Next, you need to create a package.json file for your project in the root directory.

.
└── my-project
    ├── app
    │   └── ...
    ├── ...
    └── package.json   <-- here

You can run npm init to create the package.json file, but I find it’s easier to manually create the file as an object with a single "name" property:

{
  "name": "my-project"
}

Next, you’ll need to add Grunt to your project locally, which you can do with the following command:

$ npm install grunt --save-dev

The --save-dev flag tells npm to save this Node dependency to your project’s package.json file (more on why it’s a good idea to do this momentarily). If you look at your package.json file, you’ll see that Grunt is now included as a devDependency:

{
  "name": "my-project",
  "devDependencies": {
    "grunt": "^0.4.5"  <-- here
  }
}

Npm places the actual files Grunt needs to execute in a newly created node_modules directory in your project’s root:

.
└── my-project
    ├── app
    │   └── ...
    ├── node_modules   <-- here
    │   ├── grunt
    │   └── ...
    ├── ...
    └── package.json

It’s common practice to exclude this directory from source control, so you’ll want to add node_modules/ to your .gitignore, or perform the corresponding action using your source control tool of choice.

The fact that node_modules is commonly excluded from source control is the reason it’s important to store your project’s dependencies in your package.json file with the --save-dev flag. With a package.json file, you can rebuild your node_modules directory by running npm install.

Now that you have Grunt installed your next step is to install one of its ~4,000 plugins. As you may have guessed from the plugin count, there an innumerable number of things you can do with Grunt, but let’s start with a common requirement: code linting.

Code linting

Code linting refers to a set of techniques for detecting issues in code and maintaining code quality. The web world has a whole ecosystem of these tools for linting JavaScript, CSS, HTML, and more, but perhaps the most popular one is JSHint.

JSHint

JSHint has a grunt plugin that you can install with the following command:

npm install grunt-contrib-jshint --save-dev

If you take a look back at your package.json you’ll see that grunt-contrib-jshint is now included in addition to Grunt itself:

{
  "name": "my-project",
  "devDependencies": {
    "grunt": "^0.4.5",
    "grunt-contrib-jshint": "^0.10.0" <-- here
  }
}

To actually use the plugin you need to create a Gruntfile.js file. This is the file that Grunt uses as an entry point for tasks run with the grunt command. Create a new file named Gruntfile.js and place it at root of your project.

.
└── my-project
    ├── app
    │   └── ...
    ├── node_modules
    │   ├── grunt
    │   └── ...
    ├── ...
    ├── Gruntfile.js  <-- here
    └── package.json

Next, paste the following code into your Gruntfile.js and save the file:

module.exports = function( grunt ) {
    grunt.initConfig({
        pkg: grunt.file.readJSON( "package.json" ),
        jshint: {
            all: [ "app/js/**/*.js" ]
        },
    });

    grunt.loadNpmTasks( "grunt-contrib-jshint" );

    grunt.registerTask( "default", [ "jshint" ]);
}

Let’s break down what’s happening here. First, the grunt.initConfig() calls sets up the configuration for each Grunt command you use. You pass grunt.initConfig() an object where the keys are the task names (e.g. jshint), and the corresponding values contain an configuration object. For example here’s the config used for JSHint:

jshint: {
    all: [ "app/js/**/*.js" ]
}

This code tells Grunt that the jshint task should have a single target named all. The Grunt CLI allows you to invoke commands using a task:target syntax, so you can invoke this jshint task by running grunt jshint:all.

If there is only one target defined for a task (as there is here), Grunt allows you to omit the target from the command; therefore grunt jshint also works. But it can be even simpler. Remember the last line from the Gruntfile.js?

grunt.registerTask( "default", [ "jshint" ]);

The registerTask() method allows you to define new Grunt tasks, including ones that simply reuse existing tasks. This line tells Grunt to make the default Grunt task (i.e. the one run when you simply type grunt) the jshint task. Because of this configuration, grunt jshint:all, grunt jshint, and grunt are currently synonymous. The power of Grunt is that it gives you a lot of control over how you build and manage tasks to suit your needs.

Before you run this task let’s look at your configuration one more time.

jshint: {
    all: [ "app/js/**/*.js" ]
}

The [ "app/js/**/*.js" ] part is important because it tells the JSHint command which files to operate on. Grunt has an extraordinarily comprehensive set of syntaxes for defining file paths, and things can get fairly complex, but I find the basics to be fairly simple. Here, [ "app/js/**/*.js" ] tells the jshint task to operate on files in the app/js directory, and its subdirectories (because of **/), that end in a .js suffix. As an example here are the files it’ll match:

.
└── my-project
    ├── app
    │   ├── cordova.*.js
    │   ├── css
    │   ├── index.html
    │   ├── js
    │   │   ├── file1.js     <-- here
    │   │   ├── file2.js     <-- here
    │   │   └── moreFiles   
    │   │       └── file3.js <-- here
    │   └── ...
    ├── node_modules
    │   ├── grunt
    │   └── ...
    ├── ...
    ├── Gruntfile.js
    └── package.json

I filter by a js directory because it gives me a place to differentiate between files I want to lint and files I don’t, such as cordova.*.js, and libraries I have installed in my bower_components directory. You’ll want to change the file path to match whatever is appropriate for your project. With this configuration out of the way, you can finally run the task. If all goes well you’ll something like this:

$ grunt jshint
Running "jshint:all" (jshint) task
>> 1 file lint free.

Done, without errors.

If you have errors you’ll see output that looks more like this:

$ grunt jshint
Running "jshint:all" (jshint) task

   app/scripts/app.js
      4 |    var amSelfAware = false
                                    ^ Missing semicolon.

>> 1 error in 1 file
Warning: Task "jshint:all" failed. Use --force to continue.

Aborted due to warnings.

If you don’t like JSHint’s default configuration you can change those by modifying its Grunt configuration. The plugin can also look for a .jshintrc file in your project’s root if you’re into that sort of thing.

Now that you’ve got the basics of Grunt down, here are a few other Grunt linting tasks you may be interested in.

CSSLint

  • Repository: https://github.com/gruntjs/grunt-contrib-csslint
  • Installation: npm install grunt-contrib-csslint --save-dev
  • Example configuration:

    grunt.initConfig({
        ...
        csslint: {
            all: [ "app/css/**/*.css" ]
        }
    });
    
    grunt.loadNpmTasks( "grunt-contrib-csslint" );

JSCS

  • Repository: https://github.com/jscs-dev/grunt-jscs
  • Installation: npm install grunt-jscs --save-dev
  • Example configuration:

    grunt.initConfig({
        ...
        jscs: {
            all: [ "app/js/**/*.js" ],
        }
    });
    
    grunt.loadNpmTasks( "grunt-jscs" );
    

HTML linting

  • Repository: https://github.com/jzaefferer/grunt-html
  • Installation: npm install grunt-html --save-dev
  • Example configuration:

    grunt.initConfig({
        ...
        htmllint: {
            all: [ "app/**/*.html" ]
        }
    });
    
    grunt.loadNpmTasks( "grunt-html" );

Consolidating linting tools

If you choose to use multiple linting tools you may want to setup a task that runs all the tools at once. For example the following sets up a lint task that runs the four tools discussed above:

grunt.registerTask( "lint", [ "jshint", "jscs", "csslint", "htmllint" ]);

Now when you run grunt lint you should see output that looks something like this:

$ grunt lint
Running "jshint:all" (jshint) task
>> 9 files lint free.

Running "jscs:all" (jscs) task
>> 9 files without code style errors.

Running "csslint:all" (csslint) task
>> 1 file lint free.

Running "htmllint:all" (htmllint) task
>> 1 file lint free.

Done, without errors.

Depending on your project, you may want to setup grunt lint as a Git pre-commit hook, or to run it as part of your continuous integration environment of choice. Even if you only run the lint task manually, it’s still powerful to be able to lint all of your code with a single command.

Next, let’s look at some of the other things you might want to do with Grunt in AppBuilder projects.

Sass

I’ve been spoiled by Sass; I have a hard time going back to vanilla CSS without instinctively nesting selectors or creating variables. Luckily with Grunt it’s easy to use Sass in my AppBuilder projects. Here’s what you need to know:

  • Repository: https://github.com/sindresorhus/grunt-sass
  • Installation: npm install grunt-sass --save-dev
  • Example configuration:

    grunt.initConfig({
        ...
        sass: {
            all: {
                files: [{
                    expand: true,
                    cwd: "app/scss/",
                    src: "*.scss",
                    dest: "app/css/",
                    ext: ".css"
                }]
            }
        }
    });
    
    grunt.loadNpmTasks( "grunt-sass" );

This configuration introduces a few more advanced concepts of Grunt file configuration. For more detail on what properties such as expand and cwd do, refer to Grunt’s file documentation, which covers each with a good amount of detail.

This sass task takes all files in the app’s scss directory, aka these:

.
└── my-project
    ├── app
    │   ├── css
    │   │   └── ...
    │   ├── index.html
    │   ├── js
    │   │   └── ...
    │   ├── scss
    │   │   ├── file1.scss     <-- here
    │   │   └── file2.scss     <-- here
    │   │   └── moreFiles   
    │   │       └── file3.scss <-- here
    │   └── ...
    ├── ...
    ├── Gruntfile.js
    └── package.json

…and places them in a generated css directory:

.
└── my-project
    ├── app
    │   ├── css
    │   │   ├── file1.css     <-- here
    │   │   ├── file2.css     <-- here
    │   │   └── moreFiles   
    │   │       └── file3.css <-- here
    │   ├── index.html
    │   ├── js
    │   │   └── ...
    │   ├── scss
    │   │   ├── file1.scss
    │   │   ├── file2.scss
    │   │   └── moreFiles   
    │   │       └── file3.scss
    │   └── ...
    ├── ...
    ├── Gruntfile.js
    └── package.json

This workflow works, but it relies on you running grunt sass every time you change a .scss file, which gets annoying very quickly. Don’t worry though; Grunt has an easy way to automate this.

Watching Files

  • Repository: https://github.com/gruntjs/grunt-contrib-watch
  • Installation: npm install grunt-contrib-watch --save-dev
  • Example configuration:

    grunt.initConfig({
        ...
        watch: {
            sass: {
                files: [ "app/scss/**/*.scss" ],
                tasks: [ "sass" ]
            }
        }
    });
    
    grunt.loadNpmTasks( "grunt-contrib-watch" );

The Grunt watch task packs really powerful functionality into a really simple API. The code above says to watch all .scss files in the app’s scss directory, and to invoke the sass task whenever any of them change. This means you can run grunt watch, and do your development as normal, knowing that every time you change .scss files you can count on Grunt to generate the appropriate .css file automatically.

The watch task isn’t limited to compiling SASS files though; you can put any Grunt task name in the tasks array and Grunt will run them when files in the files array change. This is convenient for any front-end language that needs to be compiled, such as CoffeeScript and TypeScript, as well as other CSS languages that require preprocessing.

LESS, Stylus, and Rework

Not using SASS? No worries, there are Grunt tasks out there for all the major CSS preprocessors, and they work very similarly to the Sass task introduced above.

Minification and optimization

Keeping an app’s size small is important for developers and users alike. For developers, smaller apps means quicker builds and LiveSync times. For users, smaller apps means a shorter download. Let’s look at a few tasks to help optimize your apps.

Creating a distribution directory

All optimization tasks require that you place your optimized files somewhere. For example, if you run your app’s main file is named app.js, and you run it through Uglify… where do you put that minified file? The most common method of handling these conflict is by creating a copy of your project, and to perform optimizations on the copied files only. Let’s look at how to use that workflow with the Grunt copy task.

  • Repository: https://github.com/gruntjs/grunt-contrib-copy
  • Installation: npm install grunt-contrib-copy --save-dev
  • Example configuration:

    grunt.initConfig({
        ...
        copy: {
            all: {
                files: [{
                    expand: true,
                    cwd: "app/",
                    src: [ "**/*" ],
                    dest: "dist/",
                    dot: true
                }]
            }
        }
    });
    
    grunt.loadNpmTasks( "grunt-contrib-copy" );

With this configuration in place, running grunt copy will create a complete copy of the app directory in a new dist directory.

.
└── my-project
    ├── app             <-- takes everything in here
    │   ├── .abproject
    │   └── ...
    ├── dist            <-- and puts it here
    │   ├── .abproject
    │   └── ...
    ├── ...
    ├── Gruntfile.js
    └── package.json

The dist directory gives you a place you can run tasks like Uglify without worrying about messing up your source code. Then, when you’re ready to perform an AppBuilder release build, you can cd into you dist directory, run appbuilder build [ios|android|wp8] --release, and know that your app package contains optimized front-end code. Let’s look at some tasks you can use to perform the optimizations.

Uglify

  • Repository: https://github.com/gruntjs/grunt-contrib-uglify
  • Installation: npm install grunt-contrib-uglify --save-dev
  • Example configuration:

    grunt.initConfig({
        ...
        uglify: {
            all: {
                files: [{
                    expand: true,
                    src: "dist/js/**/*.js"
                }]
            }
        }
    });
    
    grunt.loadNpmTasks( "grunt-contrib-uglify" );

CSS minification

  • Repository: https://github.com/gruntjs/grunt-contrib-cssmin
  • Installation: npm install grunt-contrib-cssmin --save-dev
  • Example configuration:

    grunt.initConfig({
        ...
        cssmin: {
          all: {
              files: [{
                  expand: true,
                  src: "dist/css/*/.css"
              }]
          }
        }
    });
    
    grunt.loadNpmTasks( "grunt-contrib-cssmin" );

HTML minification

  • Repository: https://github.com/gruntjs/grunt-contrib-htmlmin
  • Installation: npm install grunt-contrib-htmlmin --save-dev
  • Example configuration:

    grunt.initConfig({
        ...
        htmlmin: {
            options: {
                removeComments: true,
                collapseWhitespace: true
            },
            all: {
                files: [{
                    expand: true,
                    src: "dist/**/*.html"
                }]
            }
        }
    });
    
    grunt.loadNpmTasks( "grunt-contrib-htmlmin" );

Image optimization

  • Repository: https://github.com/gruntjs/grunt-contrib-imagemin
  • Installation: npm install grunt-contrib-imagemin --save-dev
  • Example configuration:

    grunt.initConfig({
        ...
        imagemin: {
            all: {
                files: [{
                    expand: true,
                    cwd: "dist/img/",
                    src: [ "**/*.{png,gif,jpg}" ],
                    dest: "dist/img/"
                }]
            }
        }
    });
    
    grunt.loadNpmTasks( "grunt-contrib-imagemin" );

Putting it all together

With all these tasks in place I typically define a few additional shortcuts to make my life easier:

grunt.registerTask( "lint", [ "jshint", "jscs", "csslint", "htmllint" ]);
grunt.registerTask( "optimize", [ "sass", "uglify", "cssmin", "htmlmin", "imagemin" ]);
grunt.registerTask( "build", [ "lint", "copy", "optimize" ]);

I usually also set the default task to lint, as it’s the one I run the most frequently:

grunt.registerTask( "default", [ "lint" ]);

One nice feature of Grunt is that you can use these shortcuts to help ensure code quality. For instance, the build task starts by running the lint task; if the lint task fails Grunt aborts the subsequent tasks automatically. Therefore with this shortcut setup I prevent myself from doing builds on bad code.

This is the workflow that works for me but your experiences may vary. There are 4,000+ Grunt plugins, so I’m sure there’s something out there your apps could benefit from. And if not, you can pretty easily create your own plugin plugins to meet your needs. In fact, someone recently built a Grunt plugin for automating AppBuilder builds, which is awesome.

If you’re looking for a concrete example of these Grunt tasks in use, check out my City Search Challenge app on GitHub, particularly the Development documentation, where I lay out the tasks that I use during development. And if you want to learn more about all things AppBuilder, consider coming to TelerikNEXT this May; you’ll get to meet me, as well as the rest of the people behind AppBuilder and the Telerik Platform.

Header image courtesy of Blake Patterson

Comments