The Chrome Extension Skeleton: Building Modular Extensions with Grunt and Browserify

At Salsita we implement a lot of browser extensions. We create them for our clients, for our own purposes or just for fun. We therefore decided a couple of years about to write a Chrome extension skeleton that provides boilerplate for the common code that every extension tends to need. The resulting skeleton has been a big help in accelerating development of subsequent browser extensions.
skeleton.pngAs time passed, the original skeleton implementation has been slowly obsoleted by changes and improvements in the Chrome Platform APIs. At the same time, we have been working on a lot of Node.js projects and have come to appreciate the use of CommonJS modules designed for reusability and testability. While modularity was a goal even for the initial skeleton implementation, tens of thousands of npm packages can make a big difference when it comes to developer productivity.

When we went to update the skeleton to the latest APIs, we also decided to explore possibilities for coding browser extensions using CommonJS modules. The question was: how do we turn the modules into JavaScript code that browsers can run as extensions? We had already been using a great utility, Browserify, in our web app development projects. It turns out that Browserify, which bundles CommonJS modules into browser-friendly scripts, works perfectly for Chrome extensions as well.

Browserify is available as a Grunt task, so it fit nicely into our existing build system. So how does it all work altogether? Each browser extension needs JavaScript files for several contexts (background script, content scripts, popup windows, etc.). We have corresponding scripts (or rather “entry points”) stored in the code/js directory. These scripts, referenced from  manifest.json or from HTML pages in the extension, use the CommonJS require() function to load other modules. These modules can either be part of the skeleton itself (e.g. the messaging module that I will be writing about next time), modules specific to the extension or any third-party CommonJS module (e.g. installed using npm install). Browserify also allows you to work with non-CommonJS modules like jQuery by using a special shim.

When building the browser extension, the Grunt tasks first checks the JavaScript code for problems using JSHint and then executes the unit test specifications (written with the Mocha testing framework). If all the tests pass, we create subdirectory in the build  directory, copy in the non-JavaScript resources and use the grunt-browserify task to process the JavaScript “entry points”, creating the target JavaScript bundles with all external dependencies included.

For development the above is sufficient, as you can load unpacked extension into the browser to try it. For debugging purposes we can even make use of source maps included in the JavaScript produced by Browserify. For the production version of the extension, we process the JavaScript further, minifying and uglifying it with additional Grunt tasks. When all resources are prepared, we use grunt-crx to create the final package for distribution.

There is one last bonus addition to our build process. All our projects are built on CircleCI, so as the last step of the build procedure we check if we are building on CircleCI (by checking for the presence of the CIRCLECI environment variable). We then archive the packaged browser extension as a build artifact. This way our QA team can go directly to CircleCI to get the latest extension builds.

You can see the complete code for the Chrome extension skeleton on Github. We continue to improve and extend it, and we always appreciate comments and contributions.

Roman Kaspar

Roman Kaspar