Developing Chrome Extensions using Clojurescript

How you may set up your clojurescript project for developing chrome extensions - an example

TL;DR - Of course, you can use Clojurescript to develop Chrome Extensions. So, that’s not the topic of this post. I’ve only documented how you’d set up your project, using the latest versions (as of this post’s publishing) of piggieback and weasel.

Why Weasel? Because, as its author explains

A WebSocket transport is simple and avoids some of the thornier bugs caused by the CrossPageChannel transport, which is used in the standard ClojureScript browser REPL and Austin.

Weasel is very neat in that it uses websocket to connect the Clojurescript-compiler running on the JVM and evaluates any resulting Javascript sent by the compiler, without having to set up an HTTP-server or having to deal with cross-domain scripting issues. For more details, see Weasel’s project page.

So, let’s say you are creating an action with a browser action served using, say, popup.html. Then you’d have the relevant part of your manifest.json thus

  "browser_action": {
      "...": "...",
      "default_popup": "popup.html",
      "...": "..."
  }

Additionally, you may want/have to expose your javascript files in the web_accessible_resources and background sections.

The file popup.html would include your Javascript files, including that generated using Clojurescript. Let’s say your Clojurescript code converts to a single file cljs_out.js, then you’d have

<!-- Other dependency JS files before -->
<script type="text/javascript" src="cljs_out.js"></script>
<!-- Other dependent JS files afterwards -->

For the sake of completeness, the relevant part of the project.clj file might look like the following, although the official sites of piggieback and weasel are the best sources for the latest/correct details.

  (defproject my-extension "0.0.1-SNAPSHOT"
    ;; Stuff
    :dependencies [;; ...
                   [weasel "0.7.0"]
                   ;; ...
                   ]
    ;; Other stuff
    :profiles {:debug-value {
                             :plugins [[lein-cljsbuild "1.1.1"]]
                             :dependencies [[com.cemerick/piggieback "0.2.1"]
                                            [org.clojure/tools.nrepl "0.2.12"]]
                             :nrepl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
                             ;; Good-to-have helpers - available in the Clojure REPL on ``lein repl''
                             :injections [(require '[cemerick.piggieback :as pb]
                                                   '[weasel.repl.websocket :as ws])
                                          (defn browser-repl []
                                            (pb/cljs-repl
                                             (ws/repl-env :ip "0.0.0.0" :port 11000)))] ;; Choose your port
                             }
               }
    :cljsbuild {:builds [{:id "dev"
                          :source-paths ["src"] ;; Assuming your cljs base package starts in src/
                          :compiler {
                                     :output-to "cljs_out.js"
                                     :optimizations :simple ;; Keep it simple, developer. Optimize when releasing in production.
                                     :preamble ["path/to/some-external-library.js" "path/to/another/external-library.js"]
                                     :externs ["..."]
                                     }
                          }]}
    ;; More other stuff
    )

When you run lein repl, you should now have the function browser-repl available. Simply run it.

  user=> (browser-repl)
  << started Weasel server on ws://0.0.0.0:11000 >>
  << waiting for client to connect ...
  ;; You can not evaluate anything here until the browser connects to this instance.
  ;; See below for further steps

In your clojurescript code, you may want to have a helper function to initiate the websocket-based REPL connection.

  (ns my-extension.core
    (:require [;; ...
               [weasel.repl]
               ;; ...
               ]))
  ;; ...
  (defn ^:export ;; You need the ^:export so that you can invoke this function from the browser console
    repl-connect []
    (weasel.repl/connect "ws://localhost:11000" :verbose true)) ;; Remember to use the right port
  ;; ...

That should be mostly it, as far as the setup is concerned. Compile your code. Once you install your extension (in developer mode, you’ll choose to install from the local filesystem, of course.), right-click and choose to inspect your extension. This does two things - not only do you get access to the Javascript console associated with your extension, the popup also does not auto-close if you take away focus. That’s a pretty wasteful thing if it happens - the popup closing on losing focus. You will end up losing your websocket connection, and runtime state, causing heartburn.

In the console, run

my_extension.core.repl_connect();
// Note the change from dashes to underscores, and the namespacing.

If you see a message stating that a websocket connection to port 11000 (or whatever you configured) has been established, you are all set. In the shell (where you’ve run lein repl and then (browser-repl)), you’ll see the following

user=> (browser-repl)
<< started Weasel server on ws://0.0.0.0:9001 >>
<< waiting for client to connect ...  connected! >>
To quit, type: :cljs/quit
nil
cljs.user=>

Type (.log js/console "Hello there!") - you should see that string logged in the browser’s console.

It’s time for a nice coffee break before you start on the next part of the journey. Have your coffee, while I have mine. Until next time!