Jayesh Bhoot's Ghost Town

How to write a Firefox add-on in ClojureScript

Posted on in

I found writing Firefox add-ons to be a good way to learn the ropes in ClojureScript in a productive manner.

You can find a sample add-on on my GitHub. I won't go through the code here, but will lay out the quirky development process. I will mostly focus on the plumbing - how ClojureScript hands over the compiled JavaScript code to the add-on.

The sample add-on tries to mirror the functionality of the one found in the official tutorial - wrap a border around the pages at mozilla.org.

The addon puts a purple border around the current webpage
The demo add-on puts a purple border around the current webpage

Project Layout

Figuring out a clean project layout took some effort. I am satisfied with the following structure:

project-root
|-- addon  // root for the Firefox add-on
|   |--- manifest.json
|   |--- // add-on artefacts like icons
|   |--- // JS file compiled and placed here by ClojureScript compiler
|-- src  // ClojureScript code
|-- compile-opts.edn  // compilation options provided to ClojureScript compiler
|-- deps.edn

The goal is to isolate the artefacts of the add-on from the ClojureScript code. The add-on gets only its JavaScript from ClojureScript. So, isolating the rest of its artefacts - icons, manifest, etc. - into its own directory works out well. ClojureScript dominates the rest of the project layout.

The only time the ClojureScript compiler touches the addon folder is to put the compiled JavaScript in it.

ClojureScript to JavaScript

The compilation process by default uses :optimizations :none, which produces a set of JavaScript files, with the main file pointing at the others. This does not work here as the add-on does not seem to pick up the secondary files from the main file.

The rest of the :optimizations options - :whitespace, :simple, :advanced - produce a single standalone JavaScript file, which the add-on happily accepts.

However, :optimizations :none is the default because it compiles the fastest, lending a speedy development process. I went with the second fastest - :whitespace - for development purpose, and :advanced to compile a production build.

Two other compilation options helps keep the plumbing short and clean: :target diverts the temporary files produced during compilation to a tmp folder, while :target-to puts the standalone compile JavaScript file directly into the addon folder.

To summarise, we tweak the default compilation process with the following three options to keep life simple:

{:optimizations :whitespace
 :output-dir    "tmp"
 :output-to     "addon/main.js"}

Live reload

Basically, we instruct the ClojureScript compiler to watch over the ClojureScript files and recompile them into JavaScript on change.

# in project-root
clj --main cljs.main \
    --watch "src" \
    --compile-opts "compile-opts.edn" \
    --compile demo.core

In turn, Mozilla's web-ext tool by default watches over the add-on folder and reloads the add-on on detecting a change.

# in addon folder
web-ext run --start-url https://www.mozilla.org/en-US/