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