Convert wrapped WebExtensions to modern WebExtensions

After legacy WebExtensions had been deprecated in Thunderbird 78, the Thunderbird team provided two so-called wrapper Experiments (the WindowListener Experiment and the BootstrapLoader Experiment), which re-implemented the loading framework of legacy extensions and required only little changes for add-ons to be usable in Thunderbird 78. This mechanism was intended as an intermediate solution.

This document describes how to remove the wrapper Experiment and how to properly convert a legacy extension to a modern WebExtension.

If you need any help, get in touch with the add-on developer community:

{% content-ref url=”../../community.md” %} community.md {% endcontent-ref %}

Converting a wrapped WebExtension into a modern WebExtension will be a complex task: almost all interactions with Thunderbird will need to be re-written to use the new APIs. If these APIs are not yet sufficient for your add-on, you may even need to implement additional Experiment APIs yourself. Don’t worry though: you can find information on all aspects of the migration process below, including links to many advanced topics.

{% hint style=”warning” %} Before working on an update, it is advised to read some information about the WebExtension technology first. Our Extension guide and our “Hello World” Extension Tutorial are good starting points. {% endhint %}

{% hint style=”warning” %} The guide assumes that the background script is loaded as a module. {% endhint %}

Wrapped WebExtensions have a background script similar to the following:

  1. await messenger.WindowListener.registerDefaultPrefs(
  2. "defaults/preferences/prefs.js"
  3. );
  4. await messenger.WindowListener.registerChromeUrl([
  5. ["content", "myaddon", "chrome/content/"],
  6. ["resource", "myaddon", "chrome/"],
  7. ["locale", "myaddon", "en-US", "chrome/locale/en-US/"],
  8. ["locale", "myaddon", "de-DE", "chrome/locale/de-DE/"],
  9. ]);
  10. await messenger.WindowListener.registerOptionsPage(
  11. "chrome://myaddon/content/options.xhtml"
  12. );
  13. await messenger.WindowListener.registerWindow(
  14. "chrome://messenger/content/messengercompose/messengercompose.xhtml",
  15. "chrome://myaddon/content/messengercompose.js"
  16. );
  17. await messenger.WindowListener.startListening();

Step 1: Replace registerDefaultPrefs()

Most legacy extensions stored their preferences in an nsIPrefBranch, and the registerDefaultPrefs() function loaded a JavaScript file with default preference values. An example default preference file could look like this:

  1. pref("extensions.myaddon.enableDebug", false);
  2. pref("extensions.myaddon.retries", 5);
  3. pref("extensions.myaddon.greeting", "Hello");

This file and the associated call to registerDefaultPrefs() can be removed, and the default values must be set in the background script through the LegacyPrefs Experiment:

  1. const DEFAULTS = {
  2. enableDebug: false,
  3. retries: 5,
  4. greeting: "Hello",
  5. }
  6. for (let [prefName, defaultValue] of Object.entries(DEFAULTS)) {
  7. await browser.LegacyPrefs.setDefaultPref(
  8. `extensions.myaddon.${prefName}`,
  9. defaultValue
  10. );
  11. }

We can now use the LegacyPrefs Experiment to access existing preferences, for example the preference entry at extensions.myaddon.enableDebug can be read from any WebExtension script via:

  1. let enableDebug = await browser.LegacyPrefs.getPref("extensions.myaddon.enableDebug");

Modern WebExtension should eventually use browser.storage.local.* for their preferences, but to simplify the conversion process, we will keep using the nsIPrefBranch for now. The very last conversion step will migrate the preferences.

Step 2: Replace registerChromeUrl()

We will keep registering global legacy chrome:// or resource:// URLs, but we will use the LegacyHelper Experiment. Use the registerGlobalUrls() function of the LegacyHelper Experiment instead of the registerChromeUrl() function of the wrapper Experiment. For example:

  1. browser.LegacyHelper.registerGlobalUrls([
  2. ["content", "myaddon", "chrome/content/"],
  3. ["resource", "myaddon", "chrome/"],
  4. ["locale", "myaddon", "en-US", "chrome/locale/en-US/"],
  5. ["locale", "myaddon", "de-DE", "chrome/locale/de-DE/"],
  6. ]);

Step 3: Replace registerOptionsPage()

Modern WebExtensions show their options in an HTML page in a tab or in a frame inside the Add-on Manger. The wrapper APIs instead allowed to register a legacy XUL dialog to be opened when the wrench icon in the add-on card of the Add-on Manger was clicked. This has to be removed to allow that wrench icon to show the standard WebExtension HTML options page.

In this step, we will create a menu entry on the tools menu to open the XUL options dialog via the LegacyHelper Experiment:

  1. browser.menus.create({
  2. id: "oldOptions",
  3. contexts: ["tools_menu"],
  4. title: "Old XUL options dialog",
  5. onclick: () => browser.LegacyHelper.openDialog(
  6. "XulAddonOptions",
  7. "chrome://myaddon/content/options.xhtml"
  8. )
  9. })

This will be removed after the XUL options dialog has been converted to a standard WebExtension HTML options page.

Step 4: Remove the wrapper API

This step will interrupt the main functionality of your add-on. Remove the registration for the wrapper Experiment from manifest.json, remove its implementation and schema files and any usage from your background script. The only remaining working part of your add-on should now be your XUL options dialog.

Please continue at step 5 of the conversion from legacy WebExtensions to modern WebExtensions.