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.
Starting in Firefox 51, you can embed a WebExtension in a legacy add-on type.
The legacy add-on can be either 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.
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>
true
:"hasEmbeddedWebExtension": true
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 ...
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:
- It can send one-off messages using
runtime.sendMessage()
. - It can set up a connection using
runtime.connect()
.
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.
- In the initial version, an SDK-based add-on reads and writes add-on preferences using the simple-prefs API.
-
In the intermediate version, the SDK add-on starts the embedded WebExtension. The WebExtension then asks the SDK add-on to retrieve the stored data from simple-prefs. The WebExtension then stores the data using the
storage
API.In some cases the intermediate version must keep the data in sync after the initial import. For example, the add-on's preferences UI will still use the old system, so if the user modifies settings here, it will be the old data that is modified. The intermediate add-on must then listen for these changes and send the new data to the embedded WebExtension.
The "embedded-webextension-sdk" example demonstrates this.
- In the final version, the add-on is just a WebExtension, and uses only the storage API.
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.
Limitations
Debugging
If you have a legacy add-on that embeds a WebExtension, you can't use the new Add-on Debugger to debug it. You'll need to use the old debugging workflow, based around the Browser Toolbox.