Step inside :cljsbuild

In an ongoing effort to document more of the Clojure ecosystem, I’m going to share with you a day’s headache around the Clojurescript build process. Don’t get me wrong! I adore Clojurescript. However it’s Leiningen configuration is opaque and lends to confusion. What’s worse, the mystery is papered over with calls to lein new and we hope for the best!

No more! I shall rescue you. Here’s a bucket. We’ll save the Titanic yet!

For interested parties, I wanted to build a Clojure/Clojurescript portable library. It was important that the test suite execute in both environments as well as being easy for the programmer to execute.

profile.clj

Here’s the interesting bits of the configuration all in one place.

:plugins [[lein-cljsbuild "1.1.7"]]
:source-paths ["src"]
:test-paths ["test"]
:aliases
{"cljs-test" ["cljsbuild" "test" "unit-tests"]
"test-all" ["do" "clean," "test," "cljsbuild" "once"]
"cljs-auto-test" ["cljsbuild" "auto" "tests"]}
:cljsbuild
{:test-commands {"unit-tests" ["node" "target/unit-tests.js"]}
:builds
{:tests
{:source-paths ["src" "test"]
:notify-command ["node" "target/unit-tests.js"]
:compiler {:output-to "target/unit-tests.js"
:optimizations :none
:target :nodejs
:main your.awesome.code.core-test}}
:production
{:source-paths ["src"]
:compiler {:output-to "target/production.js"
:optimizations :advanced}}}}

:plugins

The cljsbuild project is the central control. It empowers Leiningen with commands like lein cljsbuild test. It handles the compiler through the configuration given here.

:aliases

These serve a duel purpose in my projects. On the one hand they make convenient shortcuts for frequent commands. As importantly, it serves as breadcrumbs for others to understand expectations of how the build tools are configured.

In the first instance, we expose cljs-test as a command to power the test and test-command unit-tests. More on :test-command below, but note that it references cljsbuild and node.

test-all is a set of commands that clean the repository, run the Clojure tests, then compile and run the Clojurescript tests. This is accomplished with the leiningen do command and the :notify-command below.

Finally, cljs-auto-test continuously compiles and runs the tests on every source code change.

:cljsbuild

This is the meat-and-potatoes of most Clojurescript projects, and probably the least understood. I won’t pretend it’s easy (I did spend a day pulling my hair out) but it’s understandable in pieces.

:test-commands

One of the most important lines here, and rarely mentioned. While Clojurescript will happily compile all your code and tests, that code must be run against a real-live Javascript engine. The vector is the shell command to invoke the Javascript engine with your code. The path portion used will match the :output-to key below.

All of this is called by lein cljsbuild test <name>. In this case, the name is unit-tests. This compiles our Clojurescript into Javascript, then runs NodeJS with our newly minted Javascript code.

:builds

I have a pet peeve. This key supports both a vector as input and a map input. This leads to chaos and confusion when you look at other people’s code.

Here I use the map form, and I would encourage others as well to stick to this convention. Although I would love to hear thoughtful benefits of the vector form.

Pet peeve aside, the :builds values contain configurations for the Clojurescript compiler and build configurations. This is how we control where and how our Javascript is generated.

:source-paths

Here we point the compiler to all the folders we want to build from. In the :tests config, we want both the source code and test code. In :production the source code is enough.

:notify-command

It’s a callback when the compiler finishes. It calls a shell command of your choice. Here I call NodeJS to run our generated Javascript+Testcode on every change to the source code so I can see my unit-test results.

:output-to

Once your Javascript is built, where should the compiler put it? This is important, as other tools like NodeJS need to know this location to run our test suite.

When building Single Page Applications and other advanced Clojurescript applications, it is critical you know where to place your generated JS files. Often other tools pick up on output from the Clojurescript compiler.

:optimizations

How aggressive is the code optimization and minification? For :tests I have to avoid using :whitespace as NodeJS complains loudly. In :production I use :advanced to get the smallest deployable.

:output-dir

Often builds and compilations will create extra files. This allows you to specify where those go. This must be unique across the different :builds configurations, otherwise incremental builds will break and worse!

:target :nodejs

Normally, you target a browser. That means you have documents you write to. However, NodeJS lacks these browser features, so this key forces the compiler to generate the raw Javascript for NodeJS consumption.

:main

Usually this is the main entrypoint of your application. In the case of the unit tests, this points to the namespace where my test runner starts the unit tests.

You see, because you send your JS to another Javascript Engine to run tests, it must explicitly call the (run-tests) function somewhere. In this case, it is pointed at the test suite’s main namespace, where the function is called directly in the root.

Extra details

In your test suite namespace, you need two functions: enable-console-print! and run-tests.

(ns your.awesome.code.core-test
(:require [cljs.test :refer [run-tests]]
[your.awesome.code.test-set-a]
[your.awesome.code.test-set-b])
; Turn on console printing. Node can't print to *out* without.(enable-console-print!)
; This must be a root level call for Node to pick it up.
(run-tests 'your.awesome.code.test-set-a
'your.awesome.code.test-set-b)

Compile Onward!

I spent quite some time pouring over the documentation and source files to figure out how all these fit together. Hopefully I saved you a bit of time.

More information

If you’re seeing *print-fn* errors.

An exhaustive list of :compiler options available.

Sample cljsbuild configuration, annotated.