One of the first features requested for Shadowmute was a browser extension to simplify the workflow of generating mailboxes. It was also clear very early on that the usage between Chrome and Firefox users was split fairly equally. The documentation for Firefox and Chrome extension development is extremely well written and detailed, so I’m not going to delve too deep into that. What I would like to write about is how to develop for both at the same time.
When I first started, I aimed for one platform (Chrome) and figured I would use that as a base for the second (Firefox). This created two codebases in the same repo that were almost identical. The biggest difference between the two was the manifest, the second being the location of the runtime (
It’s worth mentioning the Firefox provides a compatibility layer by aliasing
chrome, but I wanted to be explicit in my extensions.
After the initial version was built in chrome it was minutes before it was fully functional in Firefox. The compatibility between the two for a basic extension is extremely impressive.
For the most part, packaging is simply zipping up the files representing the extension. Each extension’s code referenced a
constants.js file in the
background scripts section. The contents of these settings were replaced with production settings during packaging via a python script. Similarly, updates to permissions in the manifest could be applied at the same time.
This will create your
package.json which will function as the overarching index of the ultimate build system you will end up using.
Linting isn’t absolutely required at this point, but honestly should be set up before building the extension in the first place.
yarn add --dev eslint eslint-config-airbnb-base eslint-plugin-import
This will give you the airbnb base style linting capabilities for your project. I always regret not setting up earlier, no matter when I finally get around to it. Once you have the packages added, update
package.json to include a “scripts” section
In the above example, running
yarn run lint will scan the
src subdirectory where you can put your actual extension code. This has the added effect of allowing IDEs like VS Code to apply linting with minimal setup.
A detour with Webpack
The first avenue I attempted was to use webpack. It was what I had used in the past for react based development. As the name implies, webpack is more targeted at combining several resources into one processed and minified artifact. This is the opposite of what I was aiming to do
After about an hour of messing around, I decided this was probably not the right tool for my needs.
Building with Gulp
yarn add --dev gulp
At the simplest level, the gulp tasks and methods are stored in
gulpfile.js in the root directory of your project.
Building the Manifest
The simplest example is building a manifest. Start by identifying the common aspects of the
manifest.json required by both extensions in
From there adding the portions of the manifest specific to each browser can be added to
Once these are done, start adding logic to
gulpfile.js to assemble a complete manifest. Import some basic required values.
From there import our build time configurable values. These are sourced from the
package.json for the entire project as well as supporting environment variables.
Set up the API information based on the runtime values.
Create a method that accepts
destination as parameters. For example,
browser would be
destination could be
The first defined is the
mergeOverride, which is a dictionary of overwrite fields.
src command loads both manifest components explicitly as a stream of files. The stream of file objects is then piped into
mergeJson operation combines all of the file objects in the stream and squashes them into one file (named
manifest.json). The second parameter allows for a programmatic dictionary to be merged in as well. This is helpful inserting the package version. In the case of an array overwrite, such as in
permissions, it will replace the first elements and leave the remaining elements as they existed in the files. That is why we made sure the first permission was the external host the extensions would be communicating with.
This operation is a plugin from gulp-merge-json package, which will need to be installed separately by your package manager.
While you may be tempted to just try to implement simple functions in the
pipeoperations, it is not a standard function call. The operation you’re trying to implement is probably already an existing plugin.
A similar mechanism can be used to combine source scripts. To start we will import more gulp plugins that we have added in via yarn.
Create a function that first selects the
runtime parent based on the browser.
Next create a stream of all files that live in the
src/background directory, excluding the one named
index.js. We want to hold off on that, as we want to ensure that is the very last file added in the chain.
Create an in-memory file with the contents of the
constants dictionary we want inserted into the code. When we inject it into the stream, we want to prepend it to ensure all subsequent logic has access to those constants.
Finally, add the
src/background/index.js file we had omitted previously to the file stream.
Transform the stream of multiple file objects into a single element named
background.js. The background file is still in the stream, just squashed.
Include every other script in our source package for substitutions. We exclude the background scripts that were previously combined as they still exist in this stream.
Change all of the extension’s code to use the browser-specific runtime location. I had used
__RUNTIME__ in the extension’s code instead of either
browser as it was less likely to collide. Note that this will ruin auto-complete for your IDE.
With the transformations complete, it is time to gather all the remaining assets in the extension including markup, images and fonts.
The final step is to take the stream of file objects and place them into the correct output directory.
Expose the functions
We now have methods to create our manifest, and output the augmented sources. We must expose these tasks in a way for the package manager to call them.
These exports allow for two tasks that can be added into the
We can now run the following in the command line:
$ API_SERVER="https://example.com/api" yarn run firefox
Which will assemble Firefox build package with a custom API server.
A larger example
This is a general overview of how cross browser source packages can create an output for each individual browser. As mentioned at the beginning, this was done for the Shadowmute extensions, which are available on GitHub to illustrate a slightly more complex example.