1. Donate
  2. Resume
  3. Blog
  4. Projects
  5. About

Porting Orchestra to ClojureScript

Orchestra is a Clojure library made as a drop-in replacement for clojure.spec.test.alpha, which provides custom instrumentation that validates all aspects of function specs. Now, it also works with ClojureScript. This post covers some pitfalls of porting Clojure projects to ClojureScript, as well as some instruction for those looking to do so while maintaining a respectable amount of sanity.

Consideration: Directory structure

For sake of illustration, imagine you’re porting a hypothetical kitty-ninja Clojure project to ClojureScript. Your directory structure almost certainly looks like this:

.
├── project.clj
├── src
│   └── kitty_ninja
│       ├── core.clj
│       └── meow.clj
└── test
    └── kitty_ninja
        ├── core_test.clj
        └── meow_test.clj

There are three types of Clojure files to deal with, when supporting both Clojure and ClojureScript. There’s the obvious clj and cljs, but also cljc, which represents a file which may be compiled as either Clojure or ClojureScript. When refactoring your shared code from Clojure land to be exposed to ClojureScript, cljc is what will be used. For the most part, code in cljc files can either be Clojure or ClojureScript, but platform-specific bits can also be included using reader conditionals.

The directory structure is thus changed to something like this:

.
├── project.clj
├── src
│   ├── clj
│   │   └── kitty_ninja
│   │       └── core.clj
│   ├── cljc
│   │   └── kitty_ninja
│   │       └── meow.cljc
│   └── cljs
│       └── kitty_ninja_cljs
│           └── core.cljs
└── test
    ├── clj
    │   └── kitty_ninja
    │       └── core_test.clj
    ├── cljc
    │   └── kitty_ninja
    │       └── meow_test.cljc
    └── cljs
        └── kitty_ninja_cljs
            └── core_test.cljs

As is shown, the meow functionality is shared between both Clojure and ClojureScript. Furthermore, its tests are also shared. Aside from that, both Clojure and ClojureScript have their own platform-specific code and tests/entry points as well. More on this later.

Pitfall: Namespaces

The likely first thought, when looking to port the kitty-ninja project to ClojureScript, would be to also have the ClojureScript counterpart be in the kitty-ninja namespace.

This will cause much, much more harm than good.

For this reason, we have cljs.spec instead of clojure.spec for ClojureScript. Similarly, we now have orchestra-cljs.spec.test instead of orchestra.spec.test. Since ClojureScript can actually require Clojure files, and all of its macros are run in Clojure, as well as the class path issues that arise with having src/clj/ and src/cljs/ in your Leiningen :source-paths, just agree from the start that your ClojureScript port will be in a different namespace.

Pitfall: Macros

In ClojureScript, macro expansion is a very distinct phase of compilation, which involves running Clojure on the ClojureScript code. As such, macros need to be in either a clj or cljc file. Part of refactoring kitty-ninja, or any other project, would involve moving the macros which will be shared with ClojureScript to the src/cljc/ directory. Furthermore, any ClojureScript-specific macros may be kept in the src/cljs/ directory. This may be counter-intuitive, since it means there will be cljc files in src/cljs/, but this makes sense in the case where they’re for ClojureScript-only macros.

There is also a pattern, which has largely gone unstated as far I can tell, where a cljs and cljc file can have the same file name and namespace. If the cljs file requires the cljc version, to bring in its macros, then they’ll automatically be available to anyone to requires the cljs file. Here’s an illustration of how that works:

.
└── src
    └── cljs
        └── kitty_ninja_cljs
            ├── core.cljs
            ├── stealth.cljc
            └── stealth.cljs

stealth.cljc:

(ns kitty-ninja-cljs.stealth)

(defmacro purr []
  `(println "purrrr"))

stealth.cljs:

(ns kitty-ninja-cljs.stealth
  (:require-macros [kitty-ninja-cljs.stealth :as st]))

core.cljs:

(ns kitty-ninja-cljs.core
  (:require [kitty-ninja-cljs.stealth :as st]))

(st/purr) ; Macro invocation without having to do require-macros

Pitfall: Cljsbuild profiles

Unlike Leiningen profiles, which are quite flexible, cljsbuild profiles tend toward redundancy. Furthermore, cljsbuild requires a top-level key in the project.clj. As a result, the best setup I’ve found is to provide the base information at the top-level, using a fixed build name, rather than in a shared Leiningen profile. From there, use Leiningen profiles to customize the values you need. Here’s an annotated example from Orchestra:

(defproject ; ... elided a great deal ...

  ; Keep cljs in here so `lein jar` packages the sources
  :source-paths ["src/clj/" "src/cljs/"]

  ; Base cljs setup
  :cljsbuild {:test-commands {"test" ["lein" "doo" "node" "app" "once"]}
              :builds {:app
                       {:source-paths ["src/cljs/"]
                        :compiler
                        {:optimizations :advanced
                         :pretty-print false
                         :parallel-build true
                         :output-dir "target/test"
                         :output-to "target/test.js"}}}}

  ; Custom dev dependencies, sources, and entry point for test running
  :profiles {:dev {:dependencies [[lein-doo "0.1.7"]]
                   :source-paths ["test/clj/" "test/cljc/"]
                   :cljsbuild {:builds {:app
                                        {:source-paths ["test/cljs/" "test/cljc/"]
                                         :compiler
                                         {:main orchestra-cljs.test
                                          :target :nodejs}}}}}})

Consideration: Tests with doo

A combination of doo and ClojureScript’s cljs.test will allow for mostly painless sharing of tests between Clojure and ClojureScript. A word of caution, however, based on my experience:

Use Node for your tests, not Phantom.

There exists an issue on doo for this, but PhantomJS is not as well supported and, in my experience, not nearly as reliable. Fortunately, switching to Node should be as easy as adding :target :nodejs to the cljsbuild profile and changing the lein doo command to use node instead of phantom. See the Orchestra profile above for reference.

Pitfall: Figwheel’s REPL

If you end up using figwheel, which arguably most non-trivial ClojureScript projects do, absolutely consider using rlwrap (available on most Unix-like platforms). By default, fighwheel’s REPL doesn’t provide history (up and down arrows), word/line based editing (like ^W and ^U), or reverse search of previous inputs (using ^R). rlwrap will provide all of these otherwise ubiquitous features for you, without breaking any figwheel functionality.

Wrapping up

ClojureScript’s tooling doesn’t compare to Clojure’s, but it’s certainly a capable platform. For those already working in ClojureScript, be it with React(Native) or anything else, please do consider using spec and instrumentation to aid in your development. For those coming to ClojureScript, from Clojure, hopefully these points will save you some time.


Related posts