Organizing ChromeApp Development with Grunt

logo xaxc06

Chrome Apps deliver an experience as capable as a native app, but as safe as a web page. Just like web apps, Chrome Apps are written in HTML5, JavaScript, and CSS. But Chrome Apps look and behave like native apps, and they have native-like capabilities that are much more powerful than those available to web apps.

The Grunt ecosystem is huge and it’s growing every day. With literally hundreds of plugins to choose from, you can use Grunt to automate just about anything with a minimum of effort. Here is what we used to automate our tasks for developing, building and publishing the chrome app all with grunt.

Develop

  • grunt-wiredep: Inject Bower packages into your source code with Grunt. Just run this task after you install a new dependency with bower and it will insert it in areas marked inside your html file.
<!-- bower:js -->
<!-- endbower -->

Here’s what our configuration for the task looks like:

wiredep: {
    app: {
        src: ['<%= config.app %>/index.html'],
        ignorePath: '<%= config.app %>/'
    }
};
  • grunt-usemin: Replaces references from non-optimized scripts, stylesheets and other assets to their optimized version within a set of HTML files (or any templates/views). Just add all your scripts in html inside a build block and get another file with generated scripts/html optimized for deployment.
<!-- build:js js/app.js -->
<script src="js/app.js"></script>
<script src="js/controllers/thing-controller.js"></script>
<script src="js/models/thing-model.js"></script>
<script src="js/views/thing-view.js"></script>
<!-- endbuild -->

Here’s how our usemin configuration looks like:

usemin: {
    options: {
        assetsDirs: ['<%= config.dist %>', '<%= config.dist %>/images']
    },
    html: ['<%= config.dist %>/{,*/}*.html'],
    css: ['<%= config.dist %>/styles/{,*/}*.css']
},
useminPrepare: {
    options: {
        dest: '<%= config.dist %>'
    },
    html: ['<%= config.processed %>/index.html', '<%= config.processed %>/player.html']
}
  • grunt-env, grunt-preprocess and grunt-contrib-copy: Use grunt-env to specify different environment variables for different targets. It works great when coupled with grunt-preprocess which can trim out the code based on the environment. For example, to have different app names for staging and production, you could do the following in `manifest.json:

    {
    "name": "<!-- @echo APP_NAME -->",
    ...
    }

    Here’s the configuration:

// Apply environment variables
env: {
    staging: {
        APP_NAME: 'App Dev'
    },
    prod: {
        APP_NAME: 'App'
    }
},
// Copy files from `app` directory to `processed` directory
copy: {
    preprocess: {
        expand: true,
        cwd: '<%= config.app %>',
        dest: '<%= config.processed %>',
        src: ['**']
    }
},
// Preprocess all js, html and json files inside the `processed` directory
preprocess: {
    inline: {
        src: ['<%= config.processed %>/js/{,*/}*.js', '<%= config.processed %>/*.html', '<%= config.processed %>/*.json'],
        options: {
            inline: true
        }
    }
}
// Make sure code styles are up to par and there are no obvious mistakes
jshint: {
    options: {
        jshintrc: '.jshintrc',
        reporter: require('jshint-stylish')
    },
    all: [
        'Gruntfile.js',
        '<%= config.processed %>/js/{,*/}*.js',
        '!<%= config.processed %>/js/vendor/*',
        'test/spec/{,*/}*.js'
    ]
}
  • grunt-contrib-watch: Run predefined tasks whenever watched file patterns are added, changed or deleted.

    watch: {
    // Run the wiredep task whenever bower.json changes
    bower: {
        files: ['bower.json'],
        tasks: ['wiredep', 'copy:preprocess', 'preprocess']
    },
    // Run preprocess and jshint whenever any js file changes
    js: {
        files: ['<%= config.app %>/js/**/*.js'],
        tasks: ['copy:preprocess', 'preprocess', 'jshint'],
        options: {
            livereload: 45729
        }
    },
    // Run preprocess whenever any css file changes
    styles: {
        files: ['<%= config.app %>/styles/{,*/}*.css'],
        tasks: ['copy:preprocess', 'preprocess'],
        options: {
            livereload: 45729
        }
    },
    // Reload the app on any significant changes
    livereload: {
        tasks: ['copy:preprocess', 'preprocess'],
        options: {
            livereload: 45729
        },
        files: [
            '.tmp/styles/{,*/}*.css',
            '<%= config.app %>/*.html',
            '<%= config.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
            '<%= config.app %>/manifest.json',
            '<%= config.app %>/_locales/{,*/}*.json'
        ]
    }
    }

Build and Deploy

// Merge event page, update build number, exclude the debug script
chromeManifest: {
    dist: {
        options: {
        	// Don't increase the build number automatically
            buildnumber: false,
            background: {
                target: 'js/background.js',
                // Exclude files used for development
                exclude: [
                    'js/chromereload.js'
                ]
            }
        },
        src: '<%= config.processed %>',
        dest: '<%= config.dist %>'
    }
}
  • grunt-bump: Automatically bump version numbers, create tags, commit releases etc.
// Increments the version
bump: {
    options: {
	    // Update the version in package.json, manifest.json and commit the changes
        files: ['package.json', '<%= config.app %>/manifest.json'],
        updateConfigs: [],
        commit: true,
        commitMessage: 'Release v%VERSION%',
        commitFiles: ['package.json', '<%= config.app %>/manifest.json'],
        createTag: false,
        push: false,
        pushTo: 'upstream',
        gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d'
    }
}
// Copmress files inside processed directory and create a new package ready for upload
compress: function (grunt) {
    return {
        dist: {
            options: {
                archive: function () {
                    var manifest = grunt.file.readJSON(grunt.template.process('<%= config.processed %>') + '/manifest.json');
                    return grunt.template.process('<%= config.package %>') + '/App-' + manifest.version + '.zip';
                }
            },
            files: [
                {
                    expand: true,
                    cwd: 'dist/',
                    src: ['**'],
                    dest: ''
                }
            ]
        }
    }
},
// Uploads latest build from package to production/staging
webstore: {
    options: {
        publish: true,
        client_id: "XXXX",
        client_secret: "XXXX"
    },
    "staging": {
        options: {
            appID: "XXXX",
            zip: "<%= config.package %>" // Uploads the most recent zip file from this directory
        }
    }
}

Putting everything together: Lets put all the tasks together into more manageable defaults for our development and deployment.

// Configurable paths
var config = {
    app: 'app',
    dist: 'dist',
    processed: 'processed',
    package: 'package',
    tasks: grunt.cli.tasks
};

// Loads task options from `tasks/options/` and loads tasks defined in `package.json`
// Read more here: https://www.thomasboyt.com/2013/09/01/maintainable-grunt.html
require('load-grunt-config')(grunt, {
    configPath: path.join(process.cwd(), 'tasks/options'),
    init: true,
    config: {
        pkg: grunt.file.readJSON('package.json'),
        manifest: 'manifest',
        config: config
    }
});

var target = grunt.option('target') || 'dev';

// `grunt debug [--target=dev|test|staging|prod]` - Default, run Chrome App on Chrome app container
grunt.registerTask('debug', 'Task for development of the app. Enables live reload', [
    'clean',
    'env:' + target,
    'copy:preprocess',
    'preprocess',
    'watch'
]);

// Creates a production/staging build and archives to zip file
// grunt build [--target=dev|test|staging|prod]
grunt.registerTask('build', 'Creates a production/staging build and archives to zip file', [
    'env:' + target,
    'clean:dist',
    'copy:preprocess',
    'preprocess',
    'chromeManifest:dist',
    'useminPrepare',
    'concat',
    'cssmin',
    'uglify',
    'copy:dist',
    'copy:styles',
    'usemin',
    'compress'
]);

// Publish the application on the web store. Automatically increases the build version, uploads the build and commits the updated files.
// grunt publish [--release=major|minor|patch] [--target=staging|prod] [--setversion=x.x.x]
grunt.registerTask('publish', 'Publish the application on the web store', [
    'bump-only:' + release,
    'build',
    'webstore_upload:' + target,
    'bump-commit'
]);

By the way, if you enjoy movies and tv shows, check out our MovieTabs extension for Chrome to discover new movies and tv shows with every tab.

Published 22 Mar 2015

I build mobile and web applications. Full Stack, Rails, React, Typescript, Kotlin, Swift
Pulkit Goyal on Twitter