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.
-
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). ↩