Some things I learned while porting a Chrome extension to Firefox

I recently switched to Firefox.1 It’s awesome. But, just as with Chrome before it, I found Firefox’s New Tab page kinda useless. So I decided to port over my old Chrome “Launchpad” extension to work in Firefox too.

“Shouldn’t be too hard,” I thought, “Firefox 57 and above support Chrome-style WebExtensions.”

And while the porting process wasn’t a total nightmare, I did get a few unexpected surprises along the way.

In the spirit of ‘open is better’—and because there seems to be very little documentation out there about what you can expect when porting a Chrome extension to Firefox—I figured I’d share what I learned:

Promise-based API vs Callback-based API

When we talk about “WebExtensions”, we’re really talking about an API that lets you write code that integrates with specific parts of the browser (eg: bookmarks, tabs, menus) far more deeply than you’d be allowed to with just generic JavaScript running on a webpage.

Although they look similar, the WebExtension API is not the same as the Chrome extension API.

Probably the first difference you encouter will be that—while both APIs are largely asynchronous—they’re asynchronous in slightly different ways. Chrome API methods expect a callback function, while WebExtension API methods return a Promise onto which you can chain success and failure handlers.

I wanted my “Launchpad” extension to support both Chrome and Firefox using the same source code. So, in my first attempt at supporting both browsers, I wrote a little compatibility layer that translated between the two callback styles:

// In Firefox, the browser API is in the `browser` variable;
// in Chrome, it’s in `chrome`. Even though Firefox includes its
// own chrome->browser alias, we define our own here, to avoid
// unexpected "not defined" errors when accessing grandparent
// methods of the `chrome` alias in Firefox.
var api = (typeof browser !== 'undefined') ? browser : chrome;

// Compatibility layer, to convert promise-based WebExtension-style
// API methods into callback-based Chrome-style API methods.
// eg: call(api.some.api.method, [arg1, arg2], callbackFn);
var call = function call(method, args, cb) {
    var args = args || [];
    if (typeof browser === 'undefined') {
        args.push(cb);
        method.apply(api, args);
    } else {
        var _promise = method.apply(api, args);
        _promise.then(cb, function(err){ console.error(err); });
    }
}

// Example:
call(api.bookmarks.getTree, [], function(tree){
    console.log(tree);
});

In the end, incompatibilities between the two browsers (see below) made it simpler to just fork the source code to run in the two different environments, without a compatibility layer. But maybe the function above will help someone in future!

You can’t use the same manifest.json in both browsers if you want to use the storage.sync API

Both Firefox and Chrome support syncing the user’s settings across multiple devices (assuming they’re logged into the browser with a Firefox Account, or Google Account, respectively).

Browser extensions can use the storage.sync API to save their own data into this synchronised space, which is handy for synchronising settings or content across all of a user’s devices.

Problem is, if you want to use storage.sync in Firefox, you will need to define an ID in your manifest.json file. (I assume Firefox then uses that ID as a key to store your extension’s settings under.) You define an ID in your manifest.json file like this:

"applications": {
    "gecko": {
      "id": "myextension@example.com"
    }
}

Although it’s tempting to use a UUID here, Firefox will raise an error if the ID isn’t of the format [something]@[something-else].

Also, annoyingly, adding the "applications" key to your manifest.json makes it incompatible with Chrome, which refuses to run an extension with an (aparently) malformed manifest.json file.

So, if you want to use storage.sync, you can’t reuse the same manfiest.json file for both the Chrome and Firefox versions of your extension.

Different bookmark tree structure

A beautiful inconsistency that had me stumped for longer than I’d care to admit. In Chrome, chrome.bookmarks.getTree returns a data structure like:

[
  {
    "id": "0",
    "title": "",
    "children": [
      {
        "id": "1",
        "index": 0,
        "parentId": "0",
        "title": "Bookmarks Bar",
        "children": [
          …
        ]
      },
      {
        "id": "492",
        "index": 1,
        "parentId": "0",
        "title": "Other Bookmarks",
        "children": [
          …
        ]
      }
    ]
  }
]

Typically all of your bookmarks will be stored inside the “Bookmarks Bar” folder, which is the tree item with an id of 1. Easy to find!

In Firefox, though, browser.bookmarks.getTree returns something like this:

[
  {
    "id": "root________",
    "title": "",
    "index": 0,
    "type": "folder",
    "children": [
      {
        "id": "menu________",
        "title": "Bookmarks Menu",
        "index": 0,
        "type": "folder",
        "parentId": "root________",
        "children": [
          …
        ]
      },
      {
        "id": "toolbar_____",
        "title": "Bookmarks Toolbar",
        "index": 1,
        "type": "folder",
        "parentId": "root________",
        "children": [
          …
        ]
      },
      {
        "id": "unfiled_____",
        "title": "Other Bookmarks",
        "index": 3,
        "type": "folder",
        "parentId": "root________",
        "children": [
          …
        ]
      },
      {
        "id": "mobile______",
        "title": "Mobile Bookmarks",
        "index": 4,
        "type": "folder",
        "parentId": "root________",
        "children": [
          …
        ]
      }
    ]
  }
]

menu________ LOL. I have a feeling we are dealing, here, with an ID scheme that was chosen a very long time ago, and which everybody now very much regrets. Yay for backwards compatibility!

Oh you want to open that bookmarks folder?

In Chrome, if your extension wants to show the user their bookmarks, you can just programatically open chrome://bookmarks in a new tab for them.

But there’s no equivalent to chrome://bookmarks in Firefox. So, if you want to give the user an easy way to open their bookmarks manager, you can’t. Bummer.

  1. Partly for ethical reasons (anything I can do to support Mozilla’s fight for my online privacy and security right now has to be worth it) and partly for speed reasons (Firefox is much leaner and faster than Chrome, especially on a 6-year-old Mac like mine).