As of Firefox 57 (released in November 2017) support for legacy add-on types was removed, including legacy add-ons that embed WebExtensions.

As of Firefox 64 (released in December 2018) support for bootstrapped extensions has been removed, including support for bootstrapped extensions that embed WebExtensions.

Starting in Firefox 51, you can embed a WebExtension in a classic bootstrapped extension or an Add-on SDK add-on.

The embedded WebExtension's files are packaged inside the legacy add-on. The embedded WebExtension doesn't directly share its scope with the embedding legacy add-on, but they can exchange messages using the messaging functions defined in the runtime API.

This means you can migrate a legacy add-on to WebExtensions one piece at a time, and have a fully functioning add-on at every step. In particular, it enables you to migrate stored data from a legacy add-on to a WebExtension, by writing an intermediate hybrid add-on that reads the data using the legacy APIs (for example, simple-prefs or the preferences service) and writes it using the WebExtension APIs (for example, storage).

Together with this guide, we've written two examples showing how to use embedded WebExtensions to help migrate from a legacy add-on type. One shows how to port from a bootstrapped add-on, and the other shows how to port from an SDK add-on.

To embed a WebExtension you'll need Firefox 51 or later. To embed a WebExtension in an SDK add-on, you'll also need jpm 1.2.0.

Firefox 57 drops support for legacy add-on types. If you are currently maintaining an add-on in the legacy add-on format and want to migrate data, publish an update containing an embedded WebExtension as early as possible. If the update is published close to the release date of Firefox 57, the data stored in your add-on will be lost if the user updates Firefox before receiving your add-on update.

Embedding the WebExtension

If the legacy add-on is a bootstrapped extension with an install.rdf, include the property "hasEmbeddedWebExtension" in the RDF, containing the value "true":

<em:hasEmbeddedWebExtension>true</em:hasEmbeddedWebExtension>
If the legacy add-on is an SDK add-on, include the key "hasEmbeddedWebExtension" in the package.json, set to true:
 
"hasEmbeddedWebExtension": true
The WebExtension itself lives in a top-level directory called "webextension" inside the add-on. For example:
 
my-boostrapped-addon/
    chrome/
    webextension/
        manifest.json
        background.js
        ...
    bootstrap.js
    chrome.manifest
    install.rdf
 
my-sdk-addon/
    index.js
    package.json
    webextension/
        manifest.json
        background.js
        ...

Note that the embedded WebExtension must be directly inside the webextension/ directory. It can't be in a subdirectory. This also means that you can't embed more than one WebExtension.

Firefox does not treat the embedded WebExtension as an independent add-on. For this reason you shouldn't specify an add-on ID for it. If you do it will just be ignored.

However, when you've finished migrating the add-on and removed the legacy embedding code, you must include an applications key setting the ID to be the same as the original legacy add-on's ID. In this way addons.mozilla.org will recognize that the WebExtension is an update of the legacy add-on.

Starting the WebExtension

The embedded WebExtension must be explicitly started by the embedding add-on.

If the embedding add-on is a bootstrapped add-on, then the data argument passed to the bootstrap's startup() function will get an extra property webExtension:

// bootstrapped add-on

function startup({webExtension}) {

...

If the embedding add-on is an SDK add-on, it will be able to access a WebExtension object using the sdk/webextension module:

// SDK add-on

const webExtension = require("sdk/webextension");

Either way, this object has a single function, startup(), that returns a Promise. The promise resolves to an object with a single property browser: this contains the runtime APIs that the embedding add-on can use to exchange messages with the embedded WebExtension:

For example:

// bootstrapped add-on

function startup({webExtension}) {
  webExtension.startup().then(api => {
    const {browser} = api;
    browser.runtime.onMessage.addListener(handleMessage);
  });
}
// SDK add-on

const webExtension = require("sdk/webextension");

webExtension.startup().then(api => {
  const {browser} = api;
  browser.runtime.onMessage.addListener(handleMessage);
});

Note that the embedding legacy add-on can't initiate communications: it can receive (and optionally respond to) one-off messages, using onMessage, and can accept connection requests using onConnect.

The promise is rejected if the embedded WebExtension is missing a manifest or if the manifest is invalid. In this case you'll see more details in the Browser Toolbox Console.

Exchanging messages

Once the embedded WebExtension is running, it can exchange messages with the legacy add-on using a subset of the runtime APIs:

Connectionless messaging

To send a single message, the WebExtension can use runtime.sendMessage(). You can omit the extensionId argument, because the browser considers the embedded WebExtension to be part of the embedding add-on:

browser.runtime.sendMessage("message-from-webextension").then(reply => {
  if (reply) {
    console.log("response from legacy add-on: " + reply.content);
  }
});

The embedding add-on can receive (and optionally respond to) this message using the runtime.onMessage object:

// bootstrapped add-on

function startup({webExtension}) {
  // Start the embedded webextension.
  webExtension.startup().then(api => {
    const {browser} = api;
    browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
      if (msg == "message-from-webextension") {
        sendReply({
          content: "reply from legacy add-on"
        });
      }
    });
  });
}

Connection-oriented messaging

To set up a longer-lived connection between the WebExtension and the legacy add-on, the WebExtension can use runtime.connect().

var port = browser.runtime.connect({name: "connection-to-legacy"});

port.onMessage.addListener(function(message) {
  console.log("Message from legacy add-on: " + message.content);
});

The legacy add-on can listen for connection attempts using runtime.onConnect, and both sides can then use the resulting runtime.Port to exchange messages:

function startup({webExtension}) {
  // Start the embedded webextension.
  webExtension.startup().then(api => {
    const {browser} = api;
    browser.runtime.onConnect.addListener((port) => {
      port.postMessage({
        content: "content from legacy add-on"
      });
    });
  });
}

Migrating data from legacy add-ons

One major use for embedded WebExtensions is to migrate an add-on's stored data.

Stored data is a problem for people trying to migrate from legacy add-on types, because the legacy add-ons can't use the WebExtension storage APIs, while WebExtensions can't use the legacy storage APIs. For example, if an SDK add-on uses the SDK's simple-prefs API to store preferences, the WebExtension version won't be able to access that data.

With embedded WebExtensions, you can migrate data by creating an intermediate version of the add-on that embeds a WebExtension. This intermediate version reads the stored data using the legacy APIs, and writes the data using the WebExtension APIs.

We've provided two examples illustrating this pattern: "embedded-webextension-bootstrapped" shows migration from a bootstrapped add-on, while "embedded-webextension-sdk" shows migration from an SDK add-on.

Preferences

An extension that contains an embedded WebExtension can define preferences either in the embedding legacy extension (using, for example, simple-prefs or the preferences service) or in the embedded WebExtension (using options_ui).

If both parts define preferences, then the embedded WebExtension's preferences will override the legacy ones.

If the embedded WebExtension defines preferences, then they will only be initialized after the embedded WebExtension has been started. Until then, the "Preferences" button in "about:addons" will not be shown for the add-on, and the browser will log an error to the Browser Console when "about:addons" is opened.

For this reason, it's important for the embedding extension to start the embedded WebExtension immediately on startup. For a bootstrapped extension, this means you should call webExtension.startup() in the bootstrap startup. For an add-on SDK extension, this means you should call webExtension.startup() in the add-on's entry point (by default, index.js).

If the "about:addons" page is already opened in a tab when the embedded WebExtension is started, the Preferences button will not be visible until the next reload of the "about:addons" page.

Limitations

Debugging

If you have a legacy add-on that embeds a WebExtension, you can't use the new Add-on Debugger to debug the WebExtension. You'll need to use the old debugging workflow, based around the Browser Toolbox.