Functional Bytes Clojure, Scala en Java specialist

Figwheel with new Clojure 1.9 CLI tools

Dec 20, 2017 • Arnout Roemers

With the release of Clojure 1.9, Cognitect has added command line tools for Clojure. Using those tools it becomes quite easy to run a Clojure script/application or start a REPL. Next to this, it can also be used to fetch project dependencies, form a classpath with those dependencies and start a script using this classpath.

Because of the latter feature, the new CLI tools seem suitable for building ClojureScript projects, instead of using leiningen or boot. All you need is your own build script, started using the CLI tools. For a recently started ClojureScript on NodeJS project, we set out to find a workable setup for building and developing it using such a build script. A workable setup for us would at least include being able to compile and test the project, having a Figwheel REPL available and being able to connect to it using Emacs CIDER.

This blog post shows our minimal but workable setup.

The deps.edn file

To declare the dependencies of the project, a deps.edn file is required by the CLI tools. A minimal file would have the following contents:

{:deps {org.clojure/clojurescript {:mvn/version "1.9.946"}

 :aliases
 {:repl {:extra-deps
         {;; Figwheel ClojureScript REPL
          com.cemerick/piggieback {:mvn/version "0.2.2"
                                   :exclusions  [com.google.javascript/closure-compiler]}
          figwheel-sidecar        {:mvn/version "0.5.14"
                                   :exclusions  [com.google.javascript/closure-compiler]}

          ;; CIDER compatible nREPL
          cider/cider-nrepl       {:mvn/version "0.15.1"}
          org.clojure/tools.nrepl {:mvn/version "0.2.12"}
          refactor-nrepl          {:mvn/version "2.3.1"}}}}}

The :deps map declares the projects dependencies required to build it. Next to this, there is also the :repl alias, which declares the dependencies required to start a CIDER-compatible Figwheel ClojureScript REPL. If your project does not need CIDER compatibility, you could leave out the cider/cider-nrepl, org.clojure/tools.nrepl and refactor-nrepl dependencies.

When you use the clojure (or clj) CLI tool, it will use the dependencies in the :deps map. If you want the REPL dependencies mixed in as well, you need to supply the :repl alias using the -R option, like so:

$ clj -R:repl <your-script.clj>

The build script

Next is the build script. The goal is to have the script perform a certain task, such as compile, test or figwheel, and thus use the CLI tools like this:

$ clojure build.clj <task> [task args]

For this to work, the following “framework” is created in the build.clj script:

;;; Task dispatching.

(defmulti task first)

(defmethod task :default
  [args]
  (let [all-tasks  (-> task methods (dissoc :default) keys sort)
        interposed (->> all-tasks (interpose ", ") (apply str)))]
    (println "Unknown or missing task. Choose one of:" interposed)
    (System/exit 1)))

;;; Build script entrypoint. This should be the last expression.

(task *command-line-args*)

Now we can define a task method for each of the tasks we want, dispatched on the the first argument to the build script.

Compiling the project

Let’s add the first task for compiling the project. We need to require the ClojureScript compiler API namespace and use it to call the ClojureScript compiler:

(require '[cljs.build.api :as api])

(def source-dir "src")

(def compiler-config {:main          'my-app.core
                      :output-to     "target/my-app/main.js"
                      :output-dir    "target/my-app/main"
                      :target        :nodejs
                      :optimizations :simple
                      :source-map    "target/my-app/main.js.map"})

(defmethod task "compile" [args]
  (api/build source-dir compiler-config))

Easy and effective. Executing clojure build.clj compile will compile the project for us. As our project is a NodeJS project, we would now be able to run it by executing node target/my-app/main.js.

Testing the project

Next, testing our project. For testing a ClojureScript project, one generally uses a different entrypoint into the application - the test runner. The same is true for our NodeJS project. We simply execute the NodeJS project from within our build script using the clojure.java.shell namespace:

(require '[clojure.java.shell :as shell])

(def test-dir "test")

(def test-config {:main          'my-app.test-runner
                  :output-to     "target/test.js"
                  :output-dir    "target/test"
                  :target        :nodejs
                  :optimizations :none
                  :source-map    true})

(defn run-node-tests []
  (let [{:keys [out err]} (shell/sh "node" "target/test.js")]
    (println out err)))

(defn test-once []
  (api/build (api/inputs source-dir test-dir) test-config)
  (run-node-tests)
  (shutdown-agents))

(defmethod task "test" [args]
  (test-once))

Next to a one-off “test” task, it is very convenient to have the project tested after every change in the source code. This workflow can be had by extending the “test” task a bit, using the :watch-fn option of the ClojureScript compiler. On every change, the tests will be re-run, without a new JVM spinning up. The “test” task can be extended as follows:

(defn test-refresh
  (api/watch (api/inputs source-dir test-dir)
             (assoc test-config :watch-fn run-node-tests)))

(defmethod task "test" [[_ type]]
  (case type
    (nil "once") (test-once)
    "watch"      (test-refresh)
    (do (println "Unknown argument to test task:" type)
        (System/exit 1))))

Now you can execute clojure build.clj test watch to start this continuously testing workflow. The one-off test can still be executed by leaving out the watch argument, or using the once argument.

Figwheel REPL-driven development

Now let’s setup a Figwheel REPL task. Luckily for us, the Figwheel project is nicely split up between leiningen-specific stuff and the actual compilation, code reloading and REPL mechanics. The latter is what we need, and is available in the sidecar library. This library compiles the ClojureScript project with Figwheel client related code inserted and starts the Figwheel server. The Figwheel server re-compiles the project on every source code change and signals the client to reload some files after compilation is complete.

The server can start a ClojureScript REPL for you, hooking into the running application. It can also start an nREPL server for you, such that you can connect to it using CIDER, where you can start the ClojureScript REPL yourself. In our setup, we want to be able to choose either one of these “modes”, by optionally supplying an nREPL server port. The “figwheel” task definition then looks as follows:

(require '[figwheel-sidecar.repl-api :as repl-api :refer [cljs-repl]])

(def dev-config (merge compiler-config
                       {:optimizations :none
                        :source-map    true}))

(defmethod task "figwheel" [[_ port]]
  (repl-api/start-figwheel!
   {:figwheel-options (when port
                        {:nrepl-port       (some-> port Long/parseLong)
                         :nrepl-middleware ["cider.nrepl/cider-middleware"
                                            "refactor-nrepl.middleware/wrap-refactor"
                                            "cemerick.piggieback/wrap-cljs-repl"]})
    :all-builds       [{:id           "dev"
                        :figwheel     true
                        :source-paths [source-dir]
                        :compiler     dev-config}]})
  (when-not port
    (cljs-repl)))

If we launch our Figwheel setup as follows, we get a simple REPL in our terminal:

$ clj -R:repl build.clj figwheel

And if we launch as follows, we get an nREPL server, which we can connect to in Emacs by using the cider-connect command. In this CIDER repl, you can call (cljs-repl) yourself, to open the ClojureScript REPL.

$ clojure -R:repl build.clj figwheel <nrepl-port>

Very nice. But, if you try to run our former tasks, such as “compile” or “test”, you will see that those won’t work if you don’t add the -R:repl option. It will complain about not being able to load the figwheel-sidecar.repl-api namespace. We don’t want to add the repl alias for all our tasks, for various reasons. Therefore we have to make our task definitions check whether the required namespaces are available. For this we introduce a small macro:

(defn try-require [ns-sym]
  (try (require ns-sym) true (catch Exception e false)))

(defmacro with-namespaces
  [namespaces & body]
  (if (every? try-require namespaces)
    `(do ~@body)
    `(println "task not available - required dependencies not found")))

Using this macro, we can update our “figwheel” task a bit:

(defmethod task "figwheel" [[_ port]]
  (with-namespaces [figwheel-sidecar.repl-api]
    (figwheel-sidecar.repl-api/start-figwheel!
     {:figwheel-options (when port
                          {:nrepl-port       (some-> port Long/parseLong)
                           :nrepl-middleware ["cider.nrepl/cider-middleware"
                                              "refactor-nrepl.middleware/wrap-refactor"
                                              "cemerick.piggieback/wrap-cljs-repl"]})
      :all-builds       [{:id           "dev"
                          :figwheel     true
                          :source-paths [source-dir]
                          :compiler     dev-config}]})
    (when-not port
      (figwheel-sidecar.repl-api/cljs-repl)))

We can now remove the global require of figwheel-sidecar.repl-api namespace. If we then try to execute the “figwheel” task without the :repl alias, it will show a message that the required namespaces are missing. Other tasks now work again as expected.

Conclusions

Building, testing and developing a ClojureScript application using the new Clojure tools is certainly possible. If anything, it was a nice excercise in how to work with the new tools. Creating your own build script makes it very explicit as to how the project is build and tested, instead of “just add these plugins, and it will probably work”. In other words, in may create insight in how it all works and fits together.

That said, the custom build script implements concepts like tasks, which tools like leiningen and boot have implemented and standardized for you. Simply being able to execute cider-jack-in-clojurescript from within Emacs is easier and works because of those standardized build tools. This is fine, as the new Clojure CLI tools and tools like leiningen and boot can complement each other. Because of this, we will probably see more support for the Clojure tools, and the underlying tools.deps library, in those build tools. Initial examples of such integration are the boot-tools-deps task and lein-tools-deps plugin.

A full build script can be found on github.

As always, have fun!


Clojure - Scala - Java - JavaEE - Datomic - Reagent - Figwheel - HugSQL - JavaScript - Node.js - Maven - SBT - XML - XSD - XSLT - JSON - jQuery - HTML - HTMX - React - Redux - OAuth - REST - GraphQL - ZooKeeper - Kafka - Akka HTTP - PostgreSQL - ElasticSearch - Cassandra - Redis - Mule - RabbitMQ - MQTT - SOAP - Linux - macOS - Git - Scrum - Emacs - Docker - Kubernetes - Ansible - Terraform - Jenkins - GitHub - GitLab - Devops - Raspberry Pi - Event Sourcing - Functional Reactive Programming - Ports and Adapters (Hexagonal)


Functional Bytes, 2013-2024

Boekelo

06 267 145 02

KvK: 59562722

Algemene voorwaarden