Chrome extension in ClojureScript


Sometimes I need to write/change bunch of code in GAE interactive console and sometimes I need to change build scripts in jenkins tasks. That’s not comfortable to write code in simple browser textarea.

So I decided to create Chrome extension with which I can convert textarea to the code editor (and back) in a few clicks. As an editor I selected Ace because it simple to use and I’d worked with it before. As a language I selected ClojureScript.

For someone who eager: source code of the plugin on github, in the Chrome Web Store.

Developing this extension almost similar to extension in JavaScript, and nearly like ordinary ClojureScript application. But I found a few pitfalls and differences.

ClojureScript compilation

We can’t use :optimizations :none in the Chrome extension, because of goog.require way of loading dependencies. And we should to build separate compiled js files for each background/content/options/etc “pages”. So my cljs-build configuration:

{:builds {:background {:source-paths ["src/textarea_to_code_editor/background/"]
                       :compiler {:output-to "resources/background/main.js"
                                  :output-dir "resources/background/"
                                  :source-map "resources/background/main.js.map"
                                  :optimizations :whitespace
                                  :pretty-print true}}
          :content {:source-paths ["src/textarea_to_code_editor/content/"]
                    :compiler {:output-to "resources/content/main.js"
                               :output-dir "resources/content/"
                               :source-map "resources/content/main.js.map"
                               :optimizations :whitespace
                               :pretty-print true}}}}

If you want to use :optimizations :advanced, you can download externs for Chrome API from github.

Chrome API

From a first look using of Chrome API from ClojureScript is a bit uncomfortable, but with .. macro it looks not worse than in JavaScript. For example, adding listener to runtime messages in js:

chrome.runtime.onMessage.addListener(function(msg){
    console.log(msg);
});

And in ClojureScript:

(.. js/chrome -runtime -onMessage (addListener #(.log js/console %)))

Testing

Because we can’t use Chrome API in tests I created a little function for detecting if it available:

(defn available? [] (aget js/window "chrome"))

And run all extension bootstrapping code inside of (when (available?) ...). So now it’s simple to use with-redefs and with-reset (for mocking code inside of async tests) for mocking Chrome API.

For running test I used clojurescript.test, my config:

{:builds {:test {:source-paths ["src/" "test/"]
                 :compiler {:output-to "target/cljs-test.js"
                            :optimizations :whitespace
                            :pretty-print false}}}
 :test-commands {"test" ["phantomjs" :runner
                         "resources/components/ace-builds/src/ace.js"
                         "resources/components/ace-builds/src/mode-clojure.js"
                         "resources/components/ace-builds/src/mode-python.js"
                         "resources/components/ace-builds/src/theme-monokai.js"
                         "resources/components/ace-builds/src/ext-modelist.js"
                         "target/cljs-test.js"]}}

Benefits

Message passing between the extension background and content parts it’s a little pain, because it’s always turns into huge callback hell. But core.async (and a bit of core.match) can save us, for example, handling messages on content side:

(go-loop []
  (match (<! msg-chan)
    [:populate-context-menu data sender-chan] (h/populate-context-menu! data
                                                                        (:used-modes @storage)
                                                                        sender-chan
                                                                        msg-chan)
    [:clear-context-menu _ _] (h/clear-context-menu!)
    [:update-used-modes mode _] (h/update-used-modes! storage mode)
    [& msg] (println "Unmatched message:" msg))
  (recur))

Sources of content side and backend side helpers for sending/receiving Chrome runtime messages using core.async channels.

Links

Sources on github, extension in the Chrome Web Store.



comments powered by Disqus