Zephyrizing

Thoughts on cast-iron seasoning, programming and everything…

Chestnut - Annotated

| Comments

A Note about version numbers

At the time of this writing, there were several newer versions of many of the dependencies used in Chestnut. In particular, the ClojureScript core team had fairly recently released a new version with vastly simplified REPL setup requirements which triggered changes to many of the related ClojureScript tooling libraries (Piggieback, and Weasel especially). So PLEASE! Use the latest version of Chestnut, or if you’re setting up your own project then look up the latest versions on Clojars

On to the Annotating!

As I wrote recently, I recently dove head first into doing web development with Clojure and Clojurescript. Along the way I learned a whole heck of a lot about how to actually set up a Leiningen project to support a nice workflow for such a project. However, most of my new-found knowledge has already been put together into a very nice package called Chestnut. However, at first glance (and second and third glance really) Chestnut projects are complex and intimidating.

This post is an annotated walkthrough of the configuration that a new Chestnut comes with. The best way to follow along would be to start off by running lein new chestnut tour in shell and then exploring the files as I talk about them.

Let’s start by looking at the base directory.

1
2
3
4
5
6
7
8
9
10
11
12
# Directories
- resources
- src
- env

# Files
- LICENSE
- README.md
- code_of_conduct.md
- project.clj
- Procfile
- system.properties

If you’re familiar with Clojure development at all, this should look very familiar to you. But there a few unusual things: the Procfile and system.properties files, and the env folder. The Procfile is a file for letting Heroku know how to run your app, and the contents of it are explained well in the Heroku guide to getting started with Clojure.

https://devcenter.heroku.com/articles/getting-started-with-clojure#define-a-procfile

If we use the awesome [tree][tree] program to visualize some of these directories we can see some interesting stuff. First, src looks pretty standard for Clojure, with the addition of a second tree for cljs files.

1
2
3
4
5
6
7
src
├── clj
│   └── tour
│       └── server.clj
└── cljs
    └── tour
        └── core.cljs

The same is true of the test folder. But what’s really interesting is what is in that new env folder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
env
├── dev
│   ├── clj
│   │   └── tour
│   │       └── dev.clj
│   └── cljs
│       └── tour
│           └── main.cljs
├── prod
│   ├── clj
│   │   └── tour
│   │       └── dev.clj
│   └── cljs
│       └── tour
│           └── main.cljs
└── test
    ├── js
    │   ├── polyfill.js
    │   └── unit-test.js
    └── unit-test.html

What we see is that there are three top-level directories underneath env, and below each of these we see what looks like normal Clojure and Clojurescript source trees. Veeery, interesting.

Now let’s jump and take a look at the project.clj. This is where the heart of the action is. The first is boring normal project meta-data, and we’re going to skip it. The next section is interesting though.

1
2
3
4
  :source-paths ["src/clj"]
  :repl-options {:timeout 200000} ;; Defaults to 30000 (30 seconds)

  :test-paths ["spec/clj"]

The :repl-options is straightforward. It just increases the timeout when launching a REPL. Presumably this is because Chestnut REPL’s are so filled with awesome that they take longer to load ;) The *-paths options are simple too. They just override the default place that Leiningen looks for Clojure source and test files by default.

N.B. The version of Chestnut I generated this with actually has a bug, since it generated “spec/clj” as the test path, but no “spec” folder.

Next come the dependencies and plugins needed by this project.

1
2
3
4
5
6
7
8
9
10
11
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [org.clojure/clojurescript "0.0-2511" :scope "provided"]
                 [ring "1.3.2"]
                 [ring/ring-defaults "0.1.3"]
                 [compojure "1.3.1"]
                 [enlive "1.1.5"]
                 [om "0.8.0-rc1"]
                 [environ "1.0.0"]]

  :plugins [[lein-cljsbuild "1.0.3"]
            [lein-environ "1.0.0"]]

Clojure and Clojurescript are obviously necessary, and when you’re using Clojurescript with Leiningen, you probably want the [lein-cljsbuild][cljsbuild] plugin for compiling your Clojurescript. Ring is the standard Clojure web application’s library. Ring-defaults is a library for providing standard configurations of Ring middleware. Compojure is a routing library built on top of Ring.

I’m not quite sure why the Clojurescript dependency is marked as provided

Enlive is a “a selector-based (à la CSS) templating library for Clojure.” Very cool stuff, and wildly useful for many other HTML processing/producing/consuming tasks than just templating. Unfortunately very under-documented. Om is the only Clojurescript specific dependency in the list, but it’s a pretty cool one. It’s a Clojurescript interface to Facebook’s React.js library.

Finally we have the Environ library, and it’s associated lein-environ plugin. As their README says:

Environ is a Clojure library for managing environment settings from a number of different sources.

However, it is also the basis for a lot of the really neat things that Chestnut does.

Next up, we have some more basic configuration stuff.

1
2
3
  :min-lein-version "2.5.0"

  :uberjar-name "tour.jar"

These two settings are specifically to help with deployment to Heroku which looks for the :min-lein-version key to determine what version of lein to build your app with. Chestnut also changes the uberjar name so that the Procfile can specify a specific filename. Normally, the uberjar name is derived from the project name and the current version. However, this makes it a moving target. Every time you bump your version number you would have to update the Procfile to stay in sync. This way, the uberjar always has one name and the Procfile never needs to change.

Finally, we have the basic cljs build configuration:

1
2
3
4
5
6
7
  :cljsbuild {:builds {:app {:source-paths ["src/cljs"]
                             :compiler {:output-to     "resources/public/js/app.js"
                                        :output-dir    "resources/public/js/out"
                                        :source-map    "resources/public/js/out.js.map"
                                        :preamble      ["react/react.min.js"]
                                        :optimizations :none
                                        :pretty-print  true}}}}

This is remarkably similar to the configuration from the lein-cljsbuild none example project, and a standard setup to enable source-maps.

Profiles

So far, most of what we’ve seen is fairly standard clojure/clojurescript configuration stuff. But one of the things that makes Chestnut awesome is that it makes really good use of the Leiningen profiles feature. In particular, it uses profiles to concisely specify different clojurescript compilation settings and add dependencies that are only needed for development. If this is the first time you’ve heard about Leiningen profiles, you should probably go read about it. The basic summary is this though:

You can place any arbitrary key/value pairs supported by defproject into a given profile and they will be merged into the project map when that profile is activated.

Let’s start with the extra :dev setups. It starts off nice and easy just adding more clojure source and test paths and some extra development-only dependencies.

1
2
3
4
5
6
7
  :profiles {:dev {:source-paths ["env/dev/clj"]
                   :test-paths ["test/clj"]

                   :dependencies [[figwheel "0.2.1-SNAPSHOT"]
                                  [figwheel-sidecar "0.2.1-SNAPSHOT"]
                                  [com.cemerick/piggieback "0.1.3"]
                                  [weasel "0.4.2"]]

Notice that there is a similar entry under the :uberjar profile for :source-paths but that the :test-paths are omitted (since the uberjar is typically for distributing production code).

Figwheel is a very cool project that “pushes ClojureScript code changes to the client.” This enables a very smooth Clojurescript workflow. It’s so seamless in fact that in some ways it makes Clojurescript programming more enjoyable than working with Clojure!

Piggieback provides support for running a ClojureScript REPL on top of an nREPL session. Chas goes into the reasons of why this is a desirable thing in the README, so go check it out if you’re interested.

Finally, Weasel allows your ClojureScript REPL to use WebSockets to communicate with your chosen execution environment. Again, their README has good information about why this might be a thing you want.

The next two sections are pretty much just setup for Piggieback and Figwheel:

1
2
3
4
5
6
7
8
               :repl-options {:init-ns tour.server
                              :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}

               :plugins [[lein-figwheel "0.2.1-SNAPSHOT"]]

               :figwheel {:http-server-root "public"
                          :server-port 3449
                          :css-dirs ["resources/public/css"]}

Then both the dev and uberjar profiles contain an :env map specifying either that :is-dev is true or false. These configurations hook into the Environ library and allow you to specify the value of environment variables directly in the project.clj file. This particular usage should be pretty straightforward; it’s basically a simple switch that allows us to tell in our code whether or not this is a dev build/run or not.

1
               :env {:is-dev true}

The only thing remaining in the dev profile now is the cljsbuild test configuration. Though once again, notice that there is the small addition of the env/dev/cljs path as a source for our previosly configured app ClojureScript build.

1
2
3
4
5
6
7
8
9
               :cljsbuild {:test-commands { "test" ["phantomjs" "env/test/js/unit-test.js" "env/test/unit-test.html"] }
                           :builds {:app {:source-paths ["env/dev/cljs"]}
                                    :test {:source-paths ["src/cljs" "test/cljs"]
                                           :compiler {:output-to     "resources/public/js/app_test.js"
                                                      :output-dir    "resources/public/js/test"
                                                      :source-map    "resources/public/js/test.js.map"
                                                      :preamble      ["react/react.min.js"]
                                                      :optimizations :whitespace
                                                      :pretty-print  false}}}}}

Compared to the :dev profile, the :uberjar is quite simple:

1
2
3
4
5
6
7
8
9
10
         :uberjar {:source-paths ["env/prod/clj"]
                   :hooks [leiningen.cljsbuild]
                   :env {:production true}
                   :omit-source true
                   :aot :all
                   :cljsbuild {:builds {:app
                                        {:source-paths ["env/prod/cljs"]
                                         :compiler
                                         {:optimizations :advanced
                                          :pretty-print false}}}}}})

The main difference is the addition of three pieces of configuration that make this a true production build. The :hooks option causes leiningen to run the leiningen.cljsbuild/activate function to let it hook into the defualt Leiningen tasks.

Then :omit-source simply directs leiningen not to include the clojure source files in the resulting uberjar, and :aot :all causes the Clojure compiler to Ahead-of-time compile all your clojure code. The idea behind both these configs is to make your final uberjar as lean and performant as possible, at the expense of some (probably not needed) flexibility.

So that’s it for Chestnut’s project.clj. It looks intimidating at first, but in the end it is quite approachable when you break it down into it’s component pieces.

Comments