Setting up Chrome Extensions for use with ES6

First time setup of Chrome Extensions can be painful if you've never done it before. Add to that setting them up for use with ES6 and you can end up spinning your wheels longer than writing code. I recently went through this while creating Reading List, which makes heavy use of ES6 as well as Ramda for the functional work. While Babel setup is fairly easy, the module loading presented some challenges. Having originally gone with SystemJS I faced a lot of difficulty in getting the tests to run. After switching to Webpack, for all the horror stories I had heard about it, the issues I was facing were resolved within the hour.

TLDR: You can see an example of the setup here https://github.com/coreyc/reading-list. It is somewhat barebones - intentionally - as so many JavaScript developers waste their time with tool configuration these days. This setup is meant to get you off the ground ASAP.

We'll step through the setup as follows:

  • Transpiling - Babel
  • ES6 module bundling & loading - Webpack
  • Setting up the Chrome extension
  • Setting up unit tests

Transpiling - Babel

This part is pretty simple. Install the Babel tools we'll need with the command below:

npm install --save-dev babel-preset-es2015 babel-loader babel-register

What does this install? Because Babel can compile several ECMAScript specs we need to install the preset for the version we want to use, in this case ES2015 (ES6). If we wanted ES7 we could install a preset for that too. We also need to install babel-loader so that we can integrate with Webpack. Lastly, babel-register is needed so that we can run our Mocha tests.

Next step is to tell Babel what presets we want to enable. Create a .babelrc config file if you haven't already and add the following:

{
  "presets": ["es2015"]
}

And of course if you want to use ES7 features you would add the ES7 preset to this config.

That takes care of Babel.

ES6 module bundling & loading - Webpack

We'll be using the import / export statements from ES6, formatting our modules as ES6 rather than AMD or CommonJS. Webpack will bundle these modules up for loading in the browser. Install with:

npm install --save-dev webpack webpack-dev-server

Next we need to add a webpack.config.js file to the root of our project. Configure it like so:

module.exports = {
  entry: './src/app.js',
  output: {
    path: __dirname,
    filename: 'bundle.js'
  },
  externals: {
    "ramda": "R"
  },
  module: {
    loaders: [
      {
        test: /.js$/,
        loader: 'babel-loader'
      }
    ]
  },
  devtool: 'source-map'
};

The entry point for our app contains imports of the other files used in the project. It might look something like this:

import * as content from './content'
import * as history from './history'
import * as userPrefs from './userPrefs'

bundle.js is the output of our modules after they've been run through Babel and Webpack. If you have any 3rd party libraries, include them in the externals property so that they won't be included in the bundle. Otherwise all the code for that library will get bundled up and dramatically increase the file size.

From the command line, run the following in order to actually create the bundle and it's source map:

node_modules/.bin/webpack src/app.js -o bundle.js

Now we need to configure our npm run start command so that it does this bundling and serves up the files in one step. Add this to package.json:

"scripts": {
  "start": "node_modules/.bin/webpack src/app.js -o bundle.js && webpack-dev-server"
}

That takes care of Webpack.

Setting up the Chrome extension

Chrome extensions have a config file of their own, manifest.json. Here's the one from my project:

{
  "manifest_version": 2,

  "name": "Today I Read",
  "description": "This extension collects the articles you've read for the day",
  "version": "1.0",

  "browser_action": {
    "default_icon": "books.png",
    "default_popup": "popup.html"
  },
  "permissions": [
    "history",
    "storage"
  ],
  "content_scripts": [
    {
      "matches": ["http://*/*", "https://*/*"],
      "js": ["bundle.js"],
      "run_at": "document_start"
    }
  ]
}

I won't go into too much detail as this config can get really complex, but the main things to know are that you specify the icon, the HTML file you want to run when you click the extension icon, what Chrome API's you need under permissions, then add your content scripts, which are scripts needed by the HTML file you specify. Disclaimer: you can also specify background scripts that run, but I did not make use of these. This setup is not tested for use with background scripts, although they may run just fine.

We take the bundle file output from Webpack and use it as our content script. An important thing to note is that you can specify when this file should run using "run_at". This is especially useful for when you need to use DOM events such as DOMContentLoaded, as extensions seem to block this event from firing. The "run_at" property is useful because it tells the script to execute when you specify, in this case at the start of the document load.

Next we need to add the bundle file to our HTML:

<body>   
  <script src="lib/ramda.min.js"></script>
  <script src="bundle.js"></script>
</body>

A side note here: I had to add the Ramda library to the HTML even though it was specified in the Webpack config as an external library. Not sure if this is the correct way or not, but it works. YMMV.

That takes care of Chrome.

Setting up unit tests

Now we just need to set up our unit tests. If you don't already have mocha installed, run npm install --save-dev mocha . Add this to the "scripts" property of the package.json file:

"test": "mocha --require babel-register ./test/*.spec.js"

Most info you'll find on setup will recommend "test": "mocha --compilers js:babel-core/register <test pattern here>" but this seems to be outdated and the Mocha docs recommend just using --require babel-register. From the docs: "If your ES6 modules have extension .js, you can npm install --save-dev babel-register and use mocha --require babel-register; --compilers is only necessary if you need to specify a file extension."

Run npm run test and watch your tests run. That takes care of unit tests.

Want to continue getting a better grasp of JavaScript and Node.js? Subscribe for more content

No spam ever. Unsubscribe any time.