Convert legacy WebExtensions to modern WebExtensions
We do not suggest to convert older legacy bootstrapped extensions or legacy overlay extensions (as used in Thunderbird 60) directly to modern WebExtensions. They should first be converted to legacy WebExtensions.
If you need any help, get in touch with the add-on developer community:
Developer CommunityConverting a legacy 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 WebExtension 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.
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.
Please add a background script to your extension, which will be needed during the update process. The guide assumes that the background script is loaded as a module.
Step 1: Dropping the legacy key
The technical conversion from a legacy WebExtension to a modern WebExtension is simple: drop the legacy
key from the manifest.json
file.
Your add-on should now install in current versions of Thunderbird without issues, but it will not yet do anything, because the chrome.manifest
file is no longer read.
Step 2: Replace the chrome.manifest
file
chrome.manifest
fileThe most common entries in the chrome.manifest
file are listed below:
content, resource and locale
These entries registered global URLs used by the extension to access its assets. Use the LegacyHelper Experiment to register content
, resource
and locale
entries. To replicate the entries shown in the previous example, add the following to your background script:
There are no direct equivalents to manifest flags, so add-ons now need to provide their own mechanisms to switch code or resources depending on the runtime environment. Relevant information is accessible through the runtime API.
skin
This entry type is no longer supported, it has to be replaced by a resource://
URL. In the above example we had the following skin
definition:
The skin
folder is a subfolder of /chrome/
, which is already available as a resource://
URL. We can therefore replace all usages of
by
style
This entry is no longer supported, it has to be replaced by the LegacyCSS Experiment. To replicate the style
entry shown in the previous example, add the following to your background script:
All *.xul
files have been renamed to *.xhtml
files in recent versions of Thunderbird! Still using *.xul
files is unsupported and will cause issues.
This should only be a temporary step. After the initial conversion from a style
entry to using the LegacyCSS Experiment, the required styles should be applied by using standard WebExtension theming support.
overlay
This entry type is no longer supported. Replacing it will be the main conversion work, which is described in step 7 and later.
interfaces, component, contract, category
These entries are no longer supported.
Step 3: Replace the /defaults/preferences/
folder
/defaults/preferences/
folderMost legacy extensions stored their preferences in an nsIPrefBranch
, and the /defaults/preferences/
folder contained JavaScript files with default preference values. An example default preference file could look like this:
This file can be removed, and the default values must be set in the background script through the LegacyPrefs Experiment:
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:
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 4: The XUL options dialog
The XUL options dialog is no longer registered, after the legacy
key has been removed from manifest.json
. We will use the LegacyHelper Experiment to open the XUL options dialog via a menu entry in the tools
menu. Add the following to your background script:
This will be removed after the XUL options dialog has been converted to a standard WebExtension HTML options page.
Step 5: Converting locale files
Even though the LegacyHelper Experiment allows to register legacy locales, the technology itself is deprecated: WebExtension HTML pages cannot access DTD or property files. Instead, they use the i18n API to access locales stored in simple JSON files.
The localeConverter.py python script will do most of the work to convert your locale files (DTD and property files) into the new JSON format.
The new locale data can be accessed from any WebExtension script:
Step 6: Converting the XUL options page
Instead of a XUL dialog, WebExtensions use an HTML page for their options page, which will be accessible to the user through the add-on manager. The page is registered in manifest.json
:
JavaScript loaded by that options.html
document can access all WebExtension APIs in the same way as for example the background script.
In this step the old XUL options dialog has to be re-created as an HTML page, using only HTML elements, JavaScript and CSS. It is no longer possible to use XUL elements. Some custom elements and 3rd party libraries to simplify this step can be found in the webext-support repository.
It may help during development, that the old XUL options page can still be opened through the tools
menu.
Localisation
There is no automatic replacement of locale placeholder entities like &myLocaleIdentifier;
in WebExtension HTML files any more. Instead, you can use placeholders like __MSG_myLocaleIdentifier__
in your markup and include the i18n.mjs module and automatically replace all __MSG_*__
locale placeholders on page load.
The script is using the i18n
API to read the modern JSON locale files created in the previous step.
Alternative for preferencesBindings.js
preferencesBindings.js
The legacy XUL options page used a framework to automatically load and save preference values, controlled by the preferencesBindings.js script. That automatism does not exist for HTML option pages. But it is possible to implement a similar mechanism using a data-preference
attribute:
We can loop over all elements which have such an attribute, and load their value from storage. Additionally we can attach an event listener to store the value after the input field has been changed by the user:
Step 7: Find matching WebExtension entry points and WebExtension APIs
Now it's time to find out how your add-on can leverage the existing WebExtension entry points. What UI elements did you use? Do any of the supported WebExtension UI elements fit?
Even if they are not a perfect match, try to replace as many of your legacy UI entry points by WebExtension entry points:
action buttons (normal or menu-typed)
action popus
message display scripts (manipulate/overlay the displayed message)
content tabs
content popup windows\
The goal of this step is to re-create as much of the functionality of your add-on by using only WebExtension technology. Browse through the list of supported WebExtension APIs to see if any of them provide what is needed by your add-on. Check available Web APIs, there is a high chance to find simple replacements for complicated XPCOM calls:
Do not hesitate to ask in our community channels for help.
Step 8: Creating missing UI entry points and APIs as Experiments
If certain crucial features of your add-on cannot be implemented using the available WebExtension APIs or Web APIs, you can create your own Experiment APIs.
As Experiments usually run in the main process and have unrestricted access to any aspect of Thunderbird, they are expected to require updates for each new version of Thunderbird. To reduce the maintenance burden in the future, it is in your own interest to use Experiment APIs only to the extent necessary for the add-on.
Best practice: Try to write APIs that would be useful for a wide range of add-ons, not just the one you're porting. That way, you can later on propose the API you designed for inclusion in Thunderbird, with your add-on serving as the reference implementation. If your APIs become a part of Thunderbird, you no longer need to maintain them as part of the add-on.
A basic description of Experiment APIs can be found in a separate article:
Introducing ExperimentsOverlay methods
Manipulating Thunderbirds UI through Experiments is historically referred to as overlaying. The basic principle of overlaying is to get hold of a native Thunderbird window object and to add or remove DOM elements (or monkey-patch functions living inside that native window to change some behaviour).
Adding or removing DOM elements can be achieved through JavaScript (note that Thunderbird sometimes still uses non-standard XUL elements, which are however slowly replaced by standard HTML elements):
A more detailed explanation of the shown code snippet is beyond the scope of this guide. It is advised to study Thunderbird's code for more details.
Generating complex and nested DOM elements through JavaScript can become cumbersome, and legacy add-ons were able to provide a simple DOM string instead. This is still possible by using the following helper function:
The function supports XUL strings with WebExtension __MSG_*__
locale placeholders. It also supports insertbefore
or insertafter
attributes, to specify where the element should be added. If an existing id
is specified, the element will be added as a child inside the existing element:
A more detailed explanation of the shown code snippet is beyond the scope of this guide. The shown code is taken from the FolderFlags add-on. The Restart Experiment Example is also using this method.
Overlay strategies
In order to add custom UI entry points, the add-on has to manipulate the native window object of all already open windows/tabs and also any window/tab which is opened in the future. The two most common concepts to achieve this are described below.
Detect open windows/tabs through WebExtension APIs
This is the preferred method, since the add-on can leverage existing WebExtension APIs and reduces the amount of code which has to be maintained by the add-on developer. For example, to manipulate all message display tabs, the following code can be used in the WebExtension background script:
The following API implementation is based on the Remove Attachments If Junk Experiment Example:
An example which manipulates the main window using the same strategy is the Restart Experiment Example.
Detect open windows/tabs through the Experiment
If the window of interest is not supported by WebExtension APIs, it is not detectable through WebExtension APIs and the detection code has to live inside an Experiment.
The following example is based on the Activity Manager Experiment Example. Its background script triggers the Experiment to register a global window listener, which manipulates the window of interest:
The implementation of the Experiment could be:
Custom WebExtension events
So far we have only discussed Experiments which perform a direct action inside the Experiment implementation. To move as much code out of the Experiment implementation, we can trigger a standard WebExtension event and let any follow-up action be handled by the WebExtension.
A common use case is a custom button added to Thunderbird's UI through an Experiment. The action which is triggered by clicking on the button should not be handled in the Experiment, but by the WebExtension background script, which has registered a listener for that button being pressed. For this to work we need to define an EventEmitter
in the Experiment:
The boilerplate, which connects the internals of a WebExtension event to the defined EventEmitter
, is the added EventManger
in lines 5-21 of the following example. The glue part to actually trigger the event is
in line 31:
The callback of the EventEmitter
(line 9) has the x
and y
parameters, which are passed through to fire.async()
, transmitting them to the WebExtension:
A working implementation of this example can be found in the Activity Manager Experiment Example.
Step 9: Migrate Preferences
So far we stored our preferences in an nsIPrefBranch
, which could be accessed from WebExtension scripts through the LegacyPrefs Experiment, and from other Experiments directly through the nsIPrefBranch
.
WebExtensions should eventually store their preferences in browser.storage.local.*, which removes the data when the add-on is uninstalled. The user should be able to start fresh by uninstalling and reinstalling an extension, if a specific configuration causes the add-on to malfunction. This is a common pattern, which however does not work for preferences stored in an nsIPrefBranch,
as they are not cleared on add-on uninstall.
The user actually expects that all his data associated with a certain add-on is removed from the Thunderbird profile, when the add-on is removed. An add-on can of course offer import and export functions.
Accessing preferences in custom Experiments
In order to migrate preferences, your custom Experiments may no longer access the nsIPrefBranch
directly, as they later cannot access the migrated values in browser.storage.local.*. All your custom Experiments must be independent of the used storage. The two most common strategies are outlined below.
Passing preferences as function parameters
Consider a simple debug log in an Experiment function, which used to query the extensions.myaddon.enableDebug
preference directly:
The function can receive the debug flag as a parameter:
In the WebExtension script calling that method, we continue (for now) to use the LegacyPrefs Experiment to retrieve the value for the enableDebug
preference before passing it to the Experiment:
Keeping a local preference cache in the Experiment
Cached preferences can be accessed everywhere inside the Experiment implementation. A simple implementation could be:
In the background script we have to monitor the extensions.myaddon.*
preference branch and update the cache if needed. The LegacyPrefs Experiment provides an onChanged
event for that purpose:
Migration strategy
The last step is to move all preferences into browser.storage.local.* and update all WebExtension scripts to no longer use the LegacyPrefs Experiment. The preferences.mjs module can be used as a drop-in replacement for the LegacyPrefs Experiment. Add the following to the top of your background script:
Move the definition of the DEFAULTS
object from the top of your background script into your copy of the preferences.mjs module.
Remove all code which used browser.LegacyPrefs.setDefaultPref()
and update all other calls to access your preferences through the LegacyPrefs Experiment by the matching method of the preferences.mjs module.
The preference caching mechanism for Experiments can be updated as follows:
Wait about 6-12 months after the migration code has been shipped to your users, before removing the migration code and the LegacyPrefs Experiment.
Last updated