Using WebExtensions APIs in a “classic” extension

So WebExtensions are the great new way to build Firefox extensions, and soon everybody creating a new extension should be using that over everything else. But what about all the people who already have extensions? How can one be expected to migrate a large extension to WebExtensions and still keep it working? Chances are that you will first spend tons of time rewriting your code, and then even more time responding to complains of your users because that rewrite introduced bugs and unintended changes.

I don’t want to see myself in that hell, a gradual migration is almost always a better idea. So I looked into ways to use WebExtensions APIs from my existing, “classic” extension. And – yes, it works. However, at this point the approach still makes many assumptions and uses internal APIs, so the code example below is merely a proof-of-concept and should be used with caution.

The good news, WebExtensions are currently implemented as a subset of bootstrapped extensions with a very simplistic bootstrap.js. Yay, it is merely creating an Extension instance – I can do that as well! Ok, it’s not quite as simple, and the final code (here based on Add-on SDK but can be done similarly from any extension) looks like this:


let self = require("sdk/self");
let unload = require("sdk/system/unload");
let {Ci, Cu} = require("chrome");

let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
let {Extension, ExtensionPage, Management} = Cu.import("resource://gre/modules/Extension.jsm", {});
let global = this;

function resolveURI(uri)
{
  if (typeof uri == "string")
    uri = Services.io.newURI(uri, null, null);
  if (uri.scheme == "resource")
  {
    let protoHandler = Services.io.getProtocolHandler("resource")
                                  .QueryInterface(Ci.nsIResProtocolHandler);
    return resolveURI(protoHandler.resolveURI(uri));
  }
  else
    return uri;
}

let extension = new Extension({
  id: self.id,
  version: self.version,
  resourceURI: resolveURI(self.data.url(""))
});
extension.startup().then(function()
{
  let context = new ExtensionPage(extension, {type: "background", contentWindow: global});
  let chrome = Management.generateAPIs(extension, context);
  chrome.tabs.query({active: true}, function(tabs)
  {
    chrome.tabs.executeScript(tabs[0].id, {
      code: "chrome.runtime.sendMessage(window.location.href)"
    });
    chrome.runtime.onMessage.addListener(function(message)
    {
      console.error(message);
    })
  });
}).catch(function(e)
{
  console.error(e);
});
unload.when(() => extension.shutdown());

This code will inject a content script into the current tab to retrieve its URL and print it to console (assuming that you have something loaded in the current tab that you are allowed to access and not about:addons for example, otherwise it will throw an obscure exception). Yes, it’s a very complicated way of doing it but I wanted to make sure that the messaging really works. But there clearly more details here worth explaining:

  • The parameter passed to the Extension parameter is fake boostrap data. The interesting part here is resourceURI – that’s where WebExtensions will consider the root of your extension to be, e.g. they will load manifest.json from that directory (make sure to declare "tabs" and "<all_urls>" permissions there). Content script URLs will also be resolved relative to it, so using the data/ directory makes sense for SDK-based extensions.
  • At the moment, resourceURI parameter has to be an nsIURI instance and it has to use the file: or jar: scheme, that’s why I have that relatively complicated resolveURI() function there.
  • ExtensionPage and Management aren’t actually exported by the module I’m importing there, these are internal API and will likely change in future (in fact, I doubt that the Extension class is really stable at this point).

I filed a bunch of bugs to make this simpler and more reliable, these are tracked under bug 1215035. The biggest issue now is backwards compatibility, somehow browser developers tend to forget that extensions cannot support only the latest nightly build. At the very least, the current and previous stable releases of Firefox are supported, often the latest ESR release as well. The first ESR release with WebExtensions support is expected to come out mid-April 2016, and somehow I don’t have the impression that Mozilla would let us wait until then with the WebExtensions adoption (it’s not really a good idea either because bugs about incomplete or faulty APIs need to be filed ASAP).

Comments

  • Noitidart

    Fun entry! It was so much easier to use SDK stuff in classic bootstrap:

    // Import SDK Stuff
    const COMMONJS_URI = ‘resource://gre/modules/commonjs’;
    const { require } = Cu.import(COMMONJS_URI + ‘/toolkit/require.js’, {});
    var child_process = require(‘sdk/system/child_process’);

    Wladimir Palant

    Sure, but that possibility wasn’t there originally – it was added when the SDK matured. I expect the same to happen with WebExtensions.