Making extensions is weird
Downloads
Firefox: https://addons.mozilla.org/en-US/firefox/addon/presiding-presto/ Chrome: https://chromewebstore.google.com/detail/presiding-presto/gobmmfglnedbieafinhdmjakneoijngd
Extensions are weird. This weekend I decided to finally make one, and see how hard they actually are. The problem with just using most extension templates is they are either in react :vomit: (Blog learning it coming soon :tm:), or they only support one platform— either chrome or firefox.
That’s fine for most developers, you just choose chrome and call it a day. I use a fork of firefox and most people and speech and debate use chrome(ium), so I just decided to support both chrome and firefox.
Init
Ironically, I started out trying to learn react since I didn’t see svelte supported anywhere in their docs. However, after I realized my lack of react experience made it very hard to work at all, I found that their github contained svelte examples that weren’t documented.
I used those to make the intro project, and started researching how extensions work in the first place.
Diff ./chrome ./firefox
Surprisingly (at least for chrome and firefox), the api is almost the exact same since both browsers support browser.*
and chrome.*
(at least with polyfill). So, developing for both of them is rather similar.
Bun run dev
I decided to use bun for this project because I think it’s always interesting to see what new runtimes offer. The problem with non-npm package managers with extension.js is that internally, it calls npm. Since I’m using nixos and I’m not on my dev computer, I didn’t have npm installed which meant that the dev command errored whenever it ran.
I was able to fix that pretty easily by just running nix-shell -p nodejs_latest
, but just note if you try to use other package managers it will force npm in some cases.
Nixos in it’s purity also complicates a lot of the automated tooling of extension.js. Specifically, trying to use gecko/firefox browsers would either say it created the profile for the extensions (it creates a new one as a kind of sandbox) and just not create it, or crash before it finishes.
I ended up just using brave as the main browser to test the extension, and it help make things a lot easier.
Manifesting the manifest
A big way that cross platform extensions work is because the manifest.json file is pretty much the same between chrome and firefox. So, (for the most part) as long as the extension you make works on chrome it will probably work on firefox with little difference.
The only annoying thing I’ve found with the manifest is host_permissions
on chrome means your review takes way longer even if you just do it for a specific website and not <all_urls>
Extension.js automatically turns all the things you reference in the manifest into the correct paths for example I reference "default_popup": "src/index.html"
which when built ends up as dist/<browser/action/default_popup.html
which is really nice. It also makes sure all the files you reference in your manifest actually exist.
Making an extension
It was honestly pretty easy to make the extension. That’s mostly because last summer I made a full suite of presiding officer tools because I was bored, so I already had the selectors to get the precedence and recency. I used svelte since it has really nice vanilla html syntax with good reactivity. I stored the names as a list which was initialized in the popup
let names: string[] = $state([])
The $state
means that it can be put in html and the html will react to changes in the value of names.
HTML
The design was pretty easy but basic. I used tailwindcss and set a width minimum since otherwise it was really small. Using simple buttons worked, and now the UI looks like this.
I decided to make a separate component for the name box where all the names are displayed just to make things easier. In there I used a bindable rune since I needed the names to be updatable on button clicks in the main page and viewable from the Name Box.
Storage sucks
I noticed that since the browser creates a new popup everytime it opens, the names weren’t saving if you clicked away. Thankfully, browsers provide an api to store data for extensions like mine. I choose to use the browser.storage.sync
api since it was the most documented api.
Nullifying time
I could set data fine, but getting it was the hard part. Originally, I though the page hadn’t loaded correctly which was why browser.storage
was undefined, but I actually just forgot to set the permission in the manifest.
The real problem arose after I figured that out. I could get the data from the sync, but whenever I tried setting it to the data from the sync it just didn’t work. Originally I thought it was how I was setting it, since my typesense said $state([])
is the type of never[]
. That wasn’t the issue though I logged the data, and setting it worked fine. I kept noticing that when I pulled the data from sync it was on object, and I assumed that casting it to an array would fix that. But, I just kept seeing the object type throughout the project as it continued not to work.
Eventually, I used the $inspect(names)
rune to see what the names state was actually set to. I noticed that it turned from what was originally an array to an object whenever I retrieved it from the sync store. I tried handling that, but I couldn’t find a good solution, so I tried to fix the each loop failing to display the names. I thought it was just because the data wasn’t iterable, but upon further inspection I realized this was also because it wasn’t an array. So after a little bit of googling, I found Object.values(object)
which turns an object’s values into an array. This was perfect since the data looked like
{
"0": "First Last",
"1": "First Last",
...
}
Finally, I made the changes, and I had a working display for names!
Final Touches
While testing, I noticed that, even though preset recency should be always prioritized, it was not used when the seating tab was selected. That was really weird to me so I did some logging. It was able to find both the tabs correctly so then I dug deeper. My code for checking each of the recency elements was
for (const personElement of recency) {
const personElementText = personElement.innerText
const personInfo = personElementText.split("\t")
if (personInfo.length != 3) {
return null
}
people.push(personInfo[1])
}
So, when I started logging this data I found something really weird.
Bug catching
Instead of 3 fields like should be there when shown— the code, full name, and preset (always 1), it showed
['\n', '', '', '', '', '', '', '\n', '', '', '', '', '', '', '', 'First Last\n', '', '', '', '', '', '', '\n', '', '', '', '', '', '', '\n', '', '', '', '', '', '', '', 'First Last\n', '', '', '', '', '', '', '\n', '', '', '', '', '', '', '\n', '', '', '', '', '', '', '', '1\n', '', '', '', '', '', '', '\n', '', '', '', '', '', '']
Which is very much not 3 array elements, so I found the failure. But, fixing it didn’t work correctly either. I tried to use a .trim()
on the original data which removes the white space, but only at the start and end. So, while it helped, it was still
['First Last\n', '', '', '', '', '', '', '\n', '', '', '', '', '', '', '\n', '', '', '', '', '', '', '', 'First Last\n', '', '', '', '', '', '', '\n', '', '', '', '', '', '', '\n', '', '', '', '', '', '', '', '1']
At this point, I just applied a simple filter (...split("\t").filter((part) => part.trim() != "")
since the only non-white space data was what I actually needed. And, that worked which finally fixed preset recency not being used.
Favicons aren’t my favorite
The next thing I had to do before shipping my extension off to review was an icon. I suck at drawing so I made a square(ish) shape in inkscape and called it a day. I thought originally that I could just submit a png, but the firefox docs said that you could use an svg instead. So, when I submitted it for review to both mozilla and google, I used an svg.
The problem is that svg files don’t work for icons on chrome. Which meant that now I had to wait for a review which would be rejected since I put the wrong icon in. I fixed this pretty easily though by just converting the svg to a 128x128 png.
Extension/Addon Store Results
It was pretty easy, and it took around 3 days for each version to get approved. The annoying thing with the chrome web store is that if you make an error you can’t replace the version in the queue. But, it wasn’t a bad experience at all with either.