Let’s Write a Web Extension

You might have heard about Mozilla’s WebExtensions, our implementation of a new browser extension API for writing multiprocess-compatible add-ons. Maybe you’ve been wondering what it was about, and how you could use it. Well, I’m here to help! I think the MDN’s WebExtensions docs are a pretty great place to start:

WebExtensions are a new way to write Firefox extensions.

The technology is developed for cross-browser compatibility: to a large extent the API is compatible with the extension API supported by Google Chrome and Opera. Extensions written for these browsers will in most cases run in Firefox with just a few changes. The API is also fully compatible with multiprocess Firefox.

The only thing I would add is that while Mozilla is implementing most of the API that Chrome and Opera support, we’re not restricting ourselves to only that API. Where it makes sense, we will be adding new functionality and talking with other browser makers about implementing it as well. Finally, since the WebExtension API is still under development, it’s probably best if you use Firefox Nightly for this tutorial, so that you get the most up-to-date, standards-compliant behaviour. But keep in mind, this is still experimental technology — things might break!

Starting off

Okay, let’s start with a reasonably simple add-on. We’ll add a button, and when you click it, it will open up one of my favourite sites in a new tab.

The first file we’ll need is a manifest.json, to tell Firefox about our add-on.

{
  "manifest_version": 2,
  "name": "Cat Gifs!",
  "version": "1.0",
  "applications": {
    "gecko": {
      "id": "catgifs@mozilla.org"
    }
  },

  "browser_action": {
    "default_title": "Cat Gifs!"
  }
}

Great! We’re done! Hopefully your code looks a little like this. Of course, we have no idea if it works yet, so let’s install it in Firefox (we’re using Firefox Nightly for the latest implementation). You could try to drag the manifest.json, or the whole directory, onto Firefox, but that really won’t give you what you want.

The directory listing

Installing

To make Firefox recognize your extension as an add-on, you need to give it a zip file which ends in .xpi, so let’s make one of those by first installing 7-Zip, and then typing 7z a catgifs.xpi manifest.json. (If you’re on Mac or Linux, the zip command should be built-in, so just type zip catgifs.xpi manifest.json.) Then you can drag the catgifs.xpi onto Firefox, and it will show you an error because our extension is unsigned.

The first error

We can work around this by going to about:config, typing xpinstall.signatures.required in the search box, double-clicking the entry to set it to false, and then closing that tab. After that, when we drop catgifs.xpi onto Firefox, we get the option to install our new add-on!

It’s important to note that beginning with Firefox 44 (later this year), add-ons will require a signature to be installed on Firefox Beta or Release versions of the browser, so even if you set the preference shown below, you will soon still need to run Firefox Nightly or Developer Edition to follow this tutorial.

Success!!!

Of course, our add-on doesn’t do a whole lot yet.

I click and click, but nothing happens.

So let’s fix that!

Adding features

First, we’ll add the following lines to manifest.json, above the line containing browser_action:

  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },

now, of course, that’s pointing at a background.js file that doesn’t exist yet, so we should create that, too. Let’s paste the following javascript in it:

'use strict';

/*global chrome:false */

chrome.browserAction.setBadgeText({text: '(ツ)'});
chrome.browserAction.setBadgeBackgroundColor({color: '#eae'});

chrome.browserAction.onClicked.addListener(function(aTab) {
  chrome.tabs.create({'url': 'http://chilloutandwatchsomecatgifs.com/', 'active': true});
});

And you should get something that looks like this. Re-create the add-on by typing 7z a catgifs.xpi manifest.json background.js (or zip catgifs.xpi manifest.json background.js), and drop catgifs.xpi onto Firefox again, and now, when we click the button, we should get a new tab! 😄

Cat Gifs!

Automating the build

I don’t know about you, but I ended up typing 7z a catgifs.xpi manifest.json a disappointing number of times, and wondering why my background.js file wasn’t running. Since I know where this blog post is ending up, I know we’re going to be adding a bunch more files, so I think it’s time to add a build script. I hear that the go-to build tool these days is gulp, so I’ll wait here while you go install that, and c’mon back when you’re done. (I needed to install Node, and then gulp twice. I’m not sure why.)

So now that we have gulp installed, we should make a file named gulpfile.js to tell it how to build our add-on.

'use strict';

var gulp = require('gulp');

var files = ['manifest.json', 'background.js'];
var xpiName = 'catgifs.xpi';

gulp.task('default', function () {
  console.log(files, xpiName)
});

Once you have that file looking something like this, you can type gulp, and see output that looks something like this:

Just some command line stuff, nbd.

Now, you may notice that we didn’t actually build the add-on. To do that, we will need to install another package to zip things up. So, type npm install gulp-zip, and then change the gulpfile.js to contain the following:

'use strict';

var gulp = require('gulp');
var zip = require('gulp-zip');

var files = ['manifest.json', 'background.js'];
var xpiName = 'catgifs.xpi';

gulp.task('default', function () {
  gulp.src(files)
    .pipe(zip(xpiName))
    .pipe(gulp.dest('.'));
});

Once your gulpfile.js looks like this, when we run it, it will create the catgifs.xpi (as we can tell by looking at the timestamp, or by deleting it and seeing it get re-created).

Fixing a bug

Now, if you’re like me, you clicked the button a whole bunch of times, to test it out and make sure it’s working, and you might have ended up with a lot of tabs. While this will ensure you remain extra-chill, it would probably be nicer to only have one tab, either creating it, or switching to it if it exists, when we click the button. So let’s go ahead and add that.

Lots and lots of cats.

The first thing we want to do is see if the tab exists, so let’s edit the browserAction.onClicked listener in background.js to contain the following:

chrome.browserAction.onClicked.addListener(function(aTab) {
  chrome.tabs.query({'url': 'http://chilloutandwatchsomecatgifs.com/'}, (tabs) => {
    if (tabs.length === 0) {
      // There is no catgif tab!
      chrome.tabs.create({'url': 'http://chilloutandwatchsomecatgifs.com/', 'active': true});
    } else {
      // Do something here…
    }
  });
});

Huh, that’s weird, it’s always creating a new tab, no matter how many catgifs tabs there are already… It turns out that our add-on doesn’t have permission to see the urls for existing tabs yet which is why it can’t find them, so let’s go ahead and add that by inserting the following code above the browser_action:

  "permissions": [
    "tabs"
  ],

Once your code looks similar to this, re-run gulp to rebuild the add-on, and drag-and-drop to install it, and then when we test it out, ta-da! Only one catgif tab! Of course, if we’re on another tab it doesn’t do anything, so let’s fix that. We can change the else block to contain the following:

      // Do something here…
      chrome.tabs.query({'url': 'http://chilloutandwatchsomecatgifs.com/', 'active': true}, (active) => {
        if (active.length === 0) {
          chrome.tabs.update(tabs[0].id, {'active': true});
        }
      });

Make sure it looks like this, rebuild, re-install, and shazam, it works!

Making it look nice

Well, it works, but it’s not really pretty. Let’s do a couple of things to fix that a bit.

First of all, we can add a custom icon so that our add-on doesn’t look like all the other add-ons that haven’t bothered to set their icons… To do that, we add the following to manifest.json after the manifest_version line:

  "icons": {
    "48": "icon.png",
    "128": "icon128.png"
  },

And, of course, we’ll need to download a pretty picture for our icon, so let’s save a copy of this picture as icon.png, and this one as icon128.png.

We should also have a prettier icon for the button, so going back to the manifest.json, let’s add the following lines in the browser_action block before the default_title:

    "default_icon": {
      "19": "button.png",
      "38": "button38.png"
    },

and save this image as button.png, and this image as button38.png.

Finally, we need to tell our build script about the new files, so change the files line of our gulpfile.js to:

var files = ['manifest.json', 'background.js', '*.png'];

Re-run the build, and re-install the add-on, and we’re done! 😀

New, prettier, icons.

One more thing…

Well, there is another thing we could try to do. I mean, we have an add-on that works beautifully in Firefox, but one of the advantages of the new WebExtension API is that you can run the same add-on (or an add-on with minimal changes) on both Firefox and Chrome. So let’s see what it will take to get this running in both browsers!

We’ll start by launching Chrome, and trying to load the add-on, and see what errors it gives us. To load our extension, we’ll need to go to chrome://extensions/, and check the Developer mode checkbox, as shown below:

Now we’re hackers!

Then we can click the “Load unpacked extension…” button, and choose our directory to load our add-on! Uh-oh, it looks like we’ve got an error.

Close, but not quite.

Since the applications key is required for Firefox, I think we can safely ignore this error. And anyways, the button shows up! And when we click it…

Cats!

So, I guess we’re done! (I used to have a section in here about how to load babel.js, because the version of Chrome I was using didn’t support ES6’s arrow functions, but apparently they’ve upgraded their JavaScript engine, and now everything is good. 😉)

Finally, if you have any questions, or run into any problems following this tutorial, please feel free to leave a comment here, or get in touch with me through email, or on twitter! If you have issues or constructive feedback developing WebExtensions, the team will be listening on the Discourse forum.

About Blake Winton

More articles by Blake Winton…


24 comments

  1. woot

    Cross browser extensions are awesome!

    September 21st, 2015 at 17:25

  2. J. McNair

    Thanks for the clear and fun tutorial. However, I think “standards-compliant” is a bit misleading. On the other hand, “compatible with a well-defined subset of Chrome, Opera, and possibly MS Edge” is a long sentence. On the third hand, it would be great if multiple browser makers are really collaborating on an official standard for extensions written in JS.

    Still, thanks!

    September 21st, 2015 at 17:37

  3. Gabe

    Could you show how to debug the extension also? In chrome you can use dev tools via background.js. What is the parallel in firefox?

    September 21st, 2015 at 20:07

    1. Blake Winton

      That’s a great question! But I think I might leave it for a follow-up post… (I will say that if you look closely at the 8th image, you’ll see a “Debug” button, which should get you most of the way to an answer. )

      September 21st, 2015 at 20:10

  4. Shiv

    Thanks for the very interesting tutorial. Could you please explain the code the you added in the else block ? I downloaded the source and still could not understand it.

    September 21st, 2015 at 22:22

    1. Blake Winton

      Sure!

      the “chrome.tabs.query({‘url’: ‘http://chilloutandwatchsomecatgifs.com/’, ‘active’: true},” part looks for an active tab with the cat gif url. It will call the function we pass in with a possibly-empty array of those tabs.

      The function checks to see if there are no active tabs with that url (“if (active.length === 0) {“), and if there aren’t, it sets the first tab with that url to be active (“chrome.tabs.update(tabs[0].id, {‘active’: true});”).

      Does that help the code make a little more sense?

      September 22nd, 2015 at 06:31

  5. kketch

    Are there plans to include support for chromium’s devtools extension API any time soon ?

    September 22nd, 2015 at 09:09

    1. Blake Winton

      There are definitely plans to support them (https://wiki.mozilla.org/WebExtensions#List_of_APIs_we_will_likely_support_in_the_future) but I don’t know what the timeline is yet. If you find you can’t wait, I’m sure the team would greatly appreciate your help. ;)

      September 22nd, 2015 at 09:13

  6. lv7777

    I’m translated MdN article of webextension.
    and I’m expect to WebExtension.

    September 22nd, 2015 at 09:25

  7. Daniel Lo Nigro

    It’s annoying that you need to package the extension into a .xpi file after doing every single change. Is there a way to load an unpacked extension into Firefox?

    September 23rd, 2015 at 23:49

    1. Blake Winton

      Yep! That’s something we’ve been talking about, and I think we’re even working on it, but I can’t seem to find the bug… What do you think about auto-reloading unpacked extensions when something in the folder changes? Would it be too annoying, or an amazing time-saver?

      September 24th, 2015 at 04:51

      1. Blake Winton

        Found it! And it looks like it even has a patch…

        September 24th, 2015 at 06:44

  8. Scott O’Malley

    Is it possible to use old Firefox API calls in the new web extensions? For example Firefox doesn’t yet support the cookies permission in Chrome/Opera so is it possible say in the background.js to use the functions listed here – https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Cookies

    September 27th, 2015 at 02:54

    1. Blake Winton

      No, since that would make the new API no better than the old API.

      We are hard at work implementing the new API, so the best thing to do would be to wait. Well, actually, the _best_ thing to do would be to help implement the APIs you’re waiting for ;) but the second best thing to do would be to wait. And the third best thing would be to write a Jetpack extension which uses all the old APIs you need until we get around to implementing them in the new API. ;D

      September 27th, 2015 at 09:50

      1. Scott O’Malley

        No worries, I’m using the jetpack extension currently, but I keep running into issues which make things difficult. Namely for some reason my app refuses to work on any other machine save for my development laptop…

        Can you point me to where I can help out implementing the missing API’s? :)

        September 28th, 2015 at 04:32

        1. Blake Winton

          This link will list all the bugs we’re tracking, and this link should be a list of most of the code you’ll need to look at. If you can jump onto irc.mozilla.org, the #webextensions channel should be able to help you with any WebExtension-related problems you run into. :)

          September 28th, 2015 at 07:26

  9. A.J.

    Maybe I’m crazy but I can’t find xpinstall.signatures.required. Followed all the other steps.

    September 29th, 2015 at 10:34

    1. Blake Winton

      Are you using Firefox Nightly? And what happens if you type “signatures” into the search box at the top of about:config? (Also, if you want to email me a screenshot, I might be able to write a longer, more helpful, reply… :)

      September 29th, 2015 at 10:51

  10. Sylos

    Can you comment on the current state of Firefox’s WebExtensions implementation? Is it viable to try to learn the API in Firefox right now, or are there still too many limitations to it?
    I would like to avoid installing Chrome or other Chromium-based browsers, if possible…

    October 1st, 2015 at 09:23

    1. Blake Winton

      It’s coming along. There’s a full (and continually updated) list of the things that don’t work here. I’m porting one of my add-ons to WebExtensions, and the features I’m adding (and testing in Nightly) are going pretty well, I fell.

      October 1st, 2015 at 10:11

  11. Brett Zamir

    It is extremely reassuring to hear you say that “we’re not restricting ourselves to only that API”.

    In the name of harmony though, I really hope you will consider implementing the Node.js API in full, as most of its APIs are not specific to servers but rather to having privileged desktop access as servers do–though even implementing server-specific features could be useful–being able to directly serve your current list of browser tabs, audio being played in the browser or whatever, in a web app, might be some interesting use cases.

    October 4th, 2015 at 13:58

    1. Blake Winton

      You should totally propose that over on the Discourse Forum. I suspect that not all the Node APIs make sense in add-ons, and browsers already have different ways of doing things like child processes, so implementing the full API seems unlikely, but if you make your case, who knows? :D

      Thanks for the idea!

      October 4th, 2015 at 17:16

  12. Paul Dunderdale

    I get “This add-on could not be installed because it appears to be corrupt.” when I drag catgifs.xpi onto Firefox (41.0.1).

    Even tried catgif-extension-step-1 from GitHub

    October 6th, 2015 at 00:42

    1. Blake Winton

      Yep. You’ll need to be using Firefox Nightly or Developer Edition (version 43 or later), since the code to load WebExtensions hasn’t made it all the way to the release version of Firefox yet.

      October 6th, 2015 at 07:06

Comments are closed for this article.