Writing Node.js scripts with ClojureScript
In the two most recent Lambda Island episodes I covered in-depth how to create command line utilities based on Lumo, how to combine them with third party libraries, and how to deploy them to npmjs.com.
However there’s a different way to create tools with ClojureScript and distribute them through NPM, without relying on Lumo. In this blog post I want to quickly demostrate how to do just that.
To recap, Lumo is a ClojureScript environment based on Node.js, using bootstrapped (self-hosted) ClojureScript. This means the ClojureScript compiler, which is written in Clojure and runs on the JVM, is used to compile itself to JavaScript. This way the JVM is no longer needed, all you need is a JavaScript runtime to compile and run ClojureScript code, which in this case is provided by Node.js. On top of that Lumo uses nexe, so Lumo can be distributed as a single compact and fast executable binary.
With Lumo you can wrap your ClojureScript code in an NPM package, and have it run on the fly. The benefit of this is that there’s no separate compilation step, so you don’t have to fiddle with ClojureScript compiler configuration. You get a fast feedback cycle during development, and your code runs exactly the same way during development as in production (or in this case: someone else’s computer.)
But there are also downsides to this approach. Compiling code and loading dependencies on the fly means the startup time of your app will suffer. It also means you need to have all dependencies available at runtime, instead of at compile time, and you don’t get to benefit from Google Closure’s advanced code optimizations, so all of this contributes to a bigger installed size on disk.
If you’re not afraid to mess with ClojureScript compiler options (which you shouldn’t be), then this is how you do it.
Here’s an exampl script. It turns its arguments into l33t sp34k
;; src/l33t/core.cljs
(ns l33t.core
(:require [clojure.string :as str]
[cljs.nodejs :as nodejs]))
(nodejs/enable-util-print!)
(defn -main [& args]
(-> (str/join " " args)
(str/replace #"cker\b" "xor")
(str/replace #"e|E" "3")
(str/replace #"i|I" "1")
(str/replace #"o|O" "0")
(str/replace #"s|S" "5")
(str/replace #"a|A" "4")
(str/replace #"t|T" "7")
(str/replace #"b|B" "6")
(str/replace #"c|C" "(")
println))
(set! *main-cli-fn* -main)
Now you need a way to compile this to Javascript. You could use Boot, or a small custom build script. I’m going to use lein-cljsbuild.
;; project.clj
(defproject l33t "0.1.0"
:dependencies [[org.clojure/clojure "1.9.0-alpha15"]
[org.clojure/clojurescript "1.9.521"]]
:plugins [[lein-cljsbuild "1.1.5"]]
:cljsbuild {:builds [{:id "prod"
:source-paths ["src"]
:compiler {:main l33t.core
:output-to "package/index.js"
:target :nodejs
:output-dir "target"
;; :externs ["externs.js"]
:optimizations :advanced
:pretty-print true
:parallel-build true}}]})
Notable here is :target :nodejs
, this is necessary to make the final script run on Node. I’m setting :optimizations
to :advanced
, to get the full power of the Google Closure compiler, and to prevent all my code from getting optimized away, I’m setting :main
to l33t.core
, the main namespace.
When you compile this with
lein cljsbuild once prod
It will create package/index.js
. Now create package/package.json
to look like this:
{
"name": "l33t",
"bin": {
"l33t": "index.js"
}
}
So this is what you have now:
.
├── package
│ ├── index.js
│ └── package.json
├── project.clj
└── src
└── l33t
└── core.cljs
Go into package
and do npm link .
so you can test out the package.
$ cd package
$ npm link .
/home/arne/.nvm/versions/node/v6.10.2/bin/l33t -> /home/arne/.nvm/versions/node/v6.10.2/lib/node_modules/l33t/index.js
/home/arne/.nvm/versions/node/v6.10.2/lib/node_modules/l33t -> /home/arne/projects/l33t
$ l33t I am a leet hacker
The script runs, but it doesn’t output anything. The reason is that it tries to find process.argv
, but because of the advanced compilation, this has been munged to something like process.FgI
. Externs to the rescue!
// externs.js
var process = {};
process.argv = [];
Comment out the :externs
line in project.clj
, recompile, and you’re good to go.
$ l33t I am a leet hacker
1 4m 4 l337 h4x0r
Alternatively, you can include cljsjs/nodejs-externs as a dependency, so the whole Node.js API is immediately covered.

Comments ()