Notes on Writing a Web-Extension for Firefox

4 minutes read Jul 7, 2023 Aug 20, 2022

Using React

Don’t use create-react-app if the extension uses a background script. Trying to shoehorn multiple entry points and other configurations needed for writing a web-extension using something like craco is not worth the effort.

Instead write a webpack config with multiple entry points. And to add a plugin for a specific entry point use the chunks option for the plugin if available. You’ll likely want to use HtmlWebpackPlugin, CopyWebpackPlugin, ProvidePlugin, and (maybe) ReactRefreshWebpackPlugin.

Debugging Extension Pages with React Developer Tools (You Cannot)

React Developer Tools does not have permission to access moz-extenison pages and it may not be possible to change that. Modifying the manifest to include <all_urls> or moz-extension://*/* under permissions does not work either. The error message when adding the moz-extension scheme states this is an invalid option and that it ... must either [match the pattern /^(https?|wss?|file|ftp|\*):\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$/, or match the pattern /^file:\/\/\/.*$/], or match the pattern /^resource:\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^about:/].

On Installed (and Updated)

onNotInstalled?

The Web-Extenison API provides a runtime.onInstalled event which is fired when the extension has been installed for the first time or has been updated. This seems pretty handy at first for triggering first run logic however since there is no guaranteed way to know when this event has not fired it’s not very useful for creating first run logic which must run before the extension is initialized.

So what to do? The storage.local API can help out here. Since the extension’s local storage will be wiped on extension removal and will be empty on extension install we can safely assume if the storage is empty this is the first run. And by writing the extension’s version number to storage this same check can determine whether this is the first run after an update.

Event not Firing?

When registering the runtime.onInstalled make sure to call it in the main loop and not in an async function call as you may miss the event. I missed the event consistently in Firefox Developer Edition - 104.0b10 (64-bit) when the event handler was registered in an async function call event when it was the first command.

Window ID and Tab ID Persistence

In Firefox Developer Edition - 105.0b9 (64-bit) window and tab IDs are unstable when:

  • the browser restarts
  • when a window is reopened (tab and window IDs)
  • when a tab is reopened
  • when a tab is moved to another window

Tracking windows and tabs across these ID changes can be done generically using sessions.setWindowValue and sessions.setTabValue respectively to set an extension specific persistent ID. In the move case it may be sufficient to rely on event callbacks like tabs.onDetached and tabs.onAttached.

Window and Tab Data not Cleared on Removal

I have observed in Firefox Developer Edition - 104.0b10 (64-bit) that window and tab session data (sessions.setWindowValue and sessions.setTabValue) is not cleared on extension removal. Since there is no runtime.onRemoved event the next best thing is to clear all tab and window data on first run if the extension will be bothered by old data.

Tab Session Data Disappearing on Move

When a tab is moved from one window to another it looses the session data written using sessions.setTabValue. You’ll need to rewrite the data after you move the tab.

tabs.captureTab Returns Images with Different Dimensions for Active and Inactive Tabs

In Firefox Developer Edition - 116.0b2 (64-bit) calling browser.tabs.captureTab with an unset rect in the ImageDetails param returns an image with dimensions of the visible viewport. Sometimes (ex: when switching to and from a new tab with the bookmarks toolbar conditionally showing) the viewport height appears dependent on whether the tab is active or not. (This can be observed by querying window.innerHeight in a popped out dev console once with the tab active and again with the tab inactive. Or by querying browser.tabs.get and checking .height when the tab is active and again when inactive.) When a tab is inactive the viewport height shrinks by 28 pixels.

I believe this is related to the bookmark bar conditionally being shown on a new tab however I have not dug into this further.