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.