Write cross browser extensions without the tears
Today I want to cover the process of developing browser extensions, from start to finish and submission to the chrome and firefox stores.
According to Apple the new Safari will have better support for extensions, but as for now (Dec. 2021) this is not the case so we will focus on Firefox and Chromium based browsers.
This guide and the examples will be based on Ora, a web extension I wrote for both Firefox and Chrome.
We will not touch on the controversial manifest version 3. We’ll stick to version 2.
The Basics
Let’s dive right in! A browser extensions is basically a series of small “websites”. Logic is written in Javascript, styled in CSS, etc. This also means that we can use Vanilla JS, React, Svelte, whatever you prefer.
There are mainly 3 types of content you need to know.
- Content scripts
These are files that you will be injected into existing tabs of the user. This can beCSS
orJS
files. They live on a tab level. - Background scripts
Here we have files that will run in the background, in a separate thread. They are not bound to any tab/window and have basically the same runtime as the browser itself. These are onlyJS
scripts. - Normal websites for your extension
As we will se later, files inside your extensions will be served as if it was a server, so here you will code you settings page, dashboard and everything else. These areHTML
,CSS
,JS
, images, etc. You name it.
Often this will be an options page, or a dashboard.
Almost everything I learned about browser extensions I learned from the MDN Docs, they are amazing! If you are stuck this should be the first point to look.
The Manifest
The first thing you’ll need to know is a file called manifest.json
.
Here you will define all the contents, permissions, icons, name, etc. It’s the entry point to your app. It’s where your high level configuration lives.
{
"manifest_version": 2,
"name": "Ora",
"version": "0.8.1",
"description": "See how much time you spend on each website and set limits",
}
Icons
Every extensions has an icon. They are defined in the manifest.json
.
icons
is for the general icon, in the settings menu e.g.browser_action
is for the icon you’ll see in the browser interface. Optionally you can specify theme_icons
for dark mode support.
As a general rule you can always specify different sizes, but in my experience the easiest method is to just use a good high res (256 or 512px) png. SVGs support is quite hit or miss, would not recommend it.
Here I chose the same 512px png for both the extension and the interface.
{
"manifest_version": 2,
...
"icons": {
"512": "icons/watch.png"
},
"browser_action": {
"default_icon": {
"512": "icons/watch.png"
},
"default_title": "Ora Dashboard",
"theme_icons": [
{
"light": "./icons/watch.png",
"dark": "./icons/watch-alt.png",
"size": 512
}
]
}
}
Permission
Browser extensions are really powerful, but need to request permissions to unlock that power. Most of them you’ll find on your way but the most common are <all_urls>
for injecting scripts/styles into every page. another common one would be tabs
to be able to get information on all tabs.
Try to keep permissions to the minimum possible and not request stuff you don’t need. Permissions are listed when installing the extensions all at once, unlike on a phone where there is a prompt.
For ora I needed <all_urls>
to inject scripts to block a website when the time limit was reached, tabs
to count the time spent on websites and unlimitedStorage
& storage
for saving all that data.
{
"manifest_version": 2,
...
"permissions": ["<all_urls>", "tabs", "unlimitedStorage", "storage"]
}
Toolchain
We all hate it, bundlers, framework setup, etc. I will try to give you the most basic, painless setup. And by far that is a setup using parcel.js
.
They have official support for browser extensions! And it works really well.
I would write some little guide, but TBH it would just duplicate their docs so I will just point you to the official documentation which explains the steps perfectly.
The cool thing about parcel is that we use the manifest.json
as entry point and just point to the sources we want to use inside of that. Parcel does the rest of the magic.
{
"manifest_version": 2,
...
"background": {
"scripts": ["./src/background/index.ts"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"run_at": "document_start",
"js": ["./src/content/index.ts"]
}
]
}
As you can see above we just use ts
typescript files in our manifest and parcel will compile them for us. And those typescript files can include React, Svelte, whatever framework and it will just work. Amazing 😍
For svelte you’ll need to add the typescript transformer. React works out of the box.
Cross browser support without tears
For this we need to thank the good folks over at Mozilla. They created webextension-polyfill.
import browser from 'webextension-polyfill'
const tabs = await browser.tabs.query({ active: true })
Just like that you will have code that will work in both Chrome and Firefox 99% of the time. This can be used in background scripts, client scripts and standalone pages.
Building
{
...
"scripts": {
"dev": "parcel watch --target dev --no-hmr ./manifest.json",
"build": "parcel build --target prod ./manifest.json",
"dist": "run-s clean build pack:*",
"pack:zip": "zip -r ./ora.zip dist/prod/*",
"pack:ff": "web-ext build -s dist/prod --overwrite-dest"
},
"targets": {
"dev": {
"sourceMap": {
"inline": true,
"inlineSources": true
}
},
"prod": {}
},
}
This is the package.json
of Ora. How is the extension built?
- Parcel bundler builds the raw web files, copies images, etc to the
./dist/prod
folder. - For Chrome we only need to zip it with
zip -r ./ora.zip dist/prod/*
. - For Firefox we use the mozilla own
web-ext
tool withweb-ext build -s dist/prod --overwrite-dest
.
This results into aweb-ext-artifacts/ota-0.8.1.zip
file.
Distributing
Now you can upload the normal zip file to the chrome web store after paying a one time 5$ fee and the web ext artefact to the mozilla add-on page.
Mozilla also requires for minified apps (which this one is) to also submit sources. Just zip the source code (without node_modules) and upload them along the minified one.
Common use cases
I also want to add a few notes on common use cases that every extensions uses.
Communication between content, background and other scripts.
The postMessage
API is the way to go. It’s basically like IPC in the sense that you can send any serializable content from and to any of your scripts while listening for an answer.
Storage / Persistence
Here you have a lot of options. The easiest way that works for most cases is using the storage
API. Here you get access to local
and sync
storage interfaces. local
is.. well local and sync
is synced between browsers if the user is logged in. Choose whatever suits you best.
Another options, which is more powerful for more complex and data hungry is using something like Dexie.js
on top of IndexedDB. This is a full NoSQL kind of local database. A good options for data intensive extensions.
Shortcuts
You can easily add keyboard shortcuts to your browser extension with the commands
entry in the manifest.json
. This has the added benefit that browsers provide an interface where users can customize them.
A little exception for opening the popup with a keyboard shortcut can be achieved with the _execute_browser_action
command.
Final thoughts
A browser extension is very fun project for a free Sunday and I’d recommend everyone to try it out, it’s easier than you might think!