Writing library for both Clojure and ClojureScript

Clojure 1.7 introduced reader conditionals and standardized way to write Clojure code for various platforms. Last few months ago, when I started to write Rigui, I decided to support both Clojure and ClojureScript. And it turns out to be possible. However, during the development, you need to be aware of a few differences between these two hosts, from tooling, syntax to host natures. That's just way more than reader conditionals.

Host differences

The most important difference between JVM and JavaScript runtime is multithread support. At the moment, almost all JavaScript runtime are event-driven. For most time there is only one thread you can run your code on. And when it comes to multithread, like WebWorker, the platform offers a message-passing API and you don't have any shared memory or data. So features like STM, Agent are not necessary and completely unavailable in this runtime. In case you were using dosync or send! in your Clojure code, you need to add :cljs branch and switch to atom.

#?(:clj (dosync
          (let [b (get (ensure (.-buckets parent)) trigger-time)]
            (alter (.-buckets parent) dissoc trigger-time)
            (ensure b)))
    :cljs (let [b (get @(.-buckets parent) trigger-time)]
            (swap! (.-buckets parent) dissoc trigger-time)
            @b))

Due to the same reason, future and promise (the Clojure promise) are not available in ClojureScript. If you were using a promise as derefable result in API, you need to find other solution in ClojureScript.

Reader conditionals

The Clojure wiki has a brief intro for reader conditionals.

Just to add that if you have a namespace that has completely different implementations, you can write them in different files. For example, platform.clj and platform.cljs. When importing the namespace (require '[platform]'), the reader will look for right file for you.

Syntax differences

There are some minor syntax differences between clj and cljs. To access a field of an object or record, you will need to use (.-fieldName obj). Otherwise cljs will try to call it as a function. Actually this syntax is also supported in clj currently, so I recommend you to switch to this completely in all your clojure code.

Cljs requires explicit macro import. You will need (:require-macros) in your (ns) header, or using :refer-macros in a require form.

Tooling

If you were to write a library for clojurescript, you can use leiningen without plugins. Because cljs libraries are also distributed as jars so you don't need to generate JavaScript files. And in case you need to, cljsbuild is the de-facto plugin for cljs development.

Second, there is no perfect REPL workflow in ClojureScript as in Clojure, to be honest. It's a pain to test your cljs code in Clojure way and browser/node environment. If you have recommendation please tell me.

I thought tests were as pain as REPL until I found doo. doo is a lein plugin made cljs testing as easy as clojure. What you have to do is provide a runner namespace and generate js for this runner. No copy/paste required, no duplicated code. Without doo, you will need to generate JavaScript sources from you clojure test file using cljsbuild, ns by ns, and execute these files one by one. You can find my test runner here and its cljsbuild configuration.

Conclusion

These are my cents about developing library for both clj and cljs. The conclusion is simply it works. In 2016 you can, and you should be encouraged to write Clojure library with cross-platform in mind.