UPDATE: Holy cow, they fixed the docs! Sooo, I don't know if it was directly related to this rant, but the quick-start docs now rock! So go forth and ClojureScript!
So you've decided to do your next pet-project in ClojureScript because Rich Hickey is awesome and the Web is awesome, therefore, Clojure on the Web must also be awesome. This article walks you through the many-and-varied steps of trying to get up and running with ClojureScript, by closely following the repo docs.
tl;dr: trolling ahead. Good intro documentation is critical if you don't want to scare off/annoy new users. Follow your own "getting started" docs without assumed specific knowledge and see if it's a nice on-boarding experience for your new community members.
Let's begin our adventure by googling "clojurescript". The most authoritive-looking link is to https://github.com/clojure/clojurescript. The repo doesn't seem to link to a webpage, so we'll assume this is it. The README becomes our first introduction to the world of ClojureScript:
"What is ClojureScript? ClojureScript is a new compiler for Clojure that targets JavaScript. It is designed to emit JavaScript code which is compatible with the advanced compilation mode of the Google Closure optimizing compiler."
Ok, there's the hook: It's new! It's Clojure! Something something about Google Closure! Whatever that is. It sounds like "Clojure", so it must be good!
The next most important piece of information is "Releases and dependency information"; telling us the latest stable release number, and something about "Leiningen" and "Maven". We only want to Learn ClojureScript so let's hope those things are unimportant for now. It's generally safe to assume weirdly-named third-party tools at the top of language repos are package managers. Anyhoo, the next part looks like it's for us: Getting Started.
Getting Started is exactly what we want to do! The first couple of links in this section shows syntax equivelants in JavaScript and Clojure and let us play with it online. That's fun, but figuring out the syntax is very low on our priorities for the moment - we need to be able to actually start a project before we write code, so the the third link on the page looks like a winner: the quick-start guide.
The "Quick Start" guide.
Nice, some direct instructions! First clone the ClojureScript repo somewhere handy and follow the instructions:
git clone https://github.com/clojure/clojurescript.git
cd clojurescript
./script/bootstrap
So we grabbed the repo then executed a random script that does something. Bootstrap things, presumably. It does say it's grabbing Google libraries that ClojureScript depends on. I'm guessing that when we figure out how to start a project we'll see if this needs to be done once per system, or if we have to clone the code for each new project and run it, or what not.
Notable tools
There are a few notable tools included: the first is a Local Clojure REPL. We all know that a REPL is the "Read-eval-print-loop" for entering commands and evaluating them. I guess we want a local Clojure REPL because ClojureScript must rely on it for something. Perhaps there is a remote REPL too? Probably need to go to the Clojure repo for that info. Anyhoo... it tells us to export an environment variable called CLOJURESCRIPT_HOME
(and preferably make it permanent in your bash profile):
export CLOJURESCRIPT_HOME=$PWD
Groovy, now we're cookin'! Now we can start a "properly-classpathed Clojure REPL" ("properly-classpathed" sounds pretty impressive eh?). Let's run it:
./script/repl
This gives us a REPL!
Clojure 1.6.0
user=>
Lets test it out:
user=> (println "Hello World")
Hello World
nil
Running Clojure! Not exactly our goal, but seems like a good first step. Next up, it says "From within this REPL, start a ClojureScript REPL..." and then there is a stack of code that I hope we don't have to memorize and input every time we want to write some code:
(require '[cljs.repl :as repl])
(require '[cljs.repl.rhino :as rhino])
(def env (rhino/repl-env))
(repl/repl env)
The last line kicks off the ClojureScript REPL, and outputs:
user=> (repl/repl env)
To quit, type: :cljs/quit
Error: Namespace "goog.debug.Error" already declared. (goog/base.js#248)
Error: Namespace "goog.asserts" already declared. (goog/base.js#248)
Error: Namespace "goog.string" already declared. (goog/base.js#248)
org.mozilla.javascript.JavaScriptException: Error: Namespace "goog.string.StringBuffer" already declared. (goog/base.js#248)
at goog/base.js:248
at goog/string/stringbuffer.js:19
at bootjs:1
at cljs/core.cljs:10
Error: Namespace "goog.string.StringBuffer" already declared. (goog/base.js#248)
Error: Namespace "goog.debug.Error" already declared. (goog/base.js#248)
Error: Namespace "goog.dom.NodeType" already declared. (goog/base.js#248)
Error: Namespace "goog.string" already declared. (goog/base.js#248)
Error: Namespace "goog.asserts" already declared. (goog/base.js#248)
Error: Namespace "goog.array" already declared. (goog/base.js#248)
Error: Namespace "goog.object" already declared. (goog/base.js#248)
Error: Namespace "goog.string" already declared. (goog/base.js#248)
org.mozilla.javascript.JavaScriptException: Error: Namespace "cljs.core" already declared. (goog/base.js#248)
at goog/base.js:248
at cljs/core.cljs:9
at bootjs:1
at <cljs repl>:126
ClojureScript:cljs.user>
Hmm, maybe that's normal - who knows - it's giving us a different prompt now though, so let's test that one!
ClojureScript:cljs.user> (println "Hello World")
Hello World
nil
Kind of feeling a bit inception-y here, but hey... it's working and we've run some ClojureScript! Next up it says "Local ClojureScript REPL". Hmm. What did we just do then? I'm not sure what that last step was all about then, as it appears we can bypass it entirely and just type:
./script/repljs
To quit, type: :cljs/quit
org.mozilla.javascript.JavaScriptException: Error: Namespace "cljs.core" already declared. (goog/base.js#248)
at goog/base.js:248
at .cljs_repl/cljs/core.js:2
at bootjs:1
at <cljs repl>:126
ClojureScript:cljs.user>
Less errors too! And that prompt looks like what we ended up with at the end of the last step: so I think we can forget that mini-program above and just use this guy! Phew.
Hmm. Then it says "If you have Node.js installed". You know what? I DO have node.js installed....
./script/noderepljs
To quit, type: :cljs/quit
ClojureScript Node.js REPL server listening on 53639
ClojureScript:cljs.user>
Ooookkkay. The same-looking prompt as before, but it also says something about a server on port 53639. I guess we can ignore that for now. The docs say "In this mode, ClojureScript code will be compiled into JavaScript and executed by the JavaScript engine". Sounds good - but maybe we'll leave Node for the moment.
ClojureScript Compiler
So it looks like we're done with REPLing and on to the real deal. Compiling some code from a file: "Compile ClojureScript source files or projects to runnable JavaScript." Boom! Sounds awesome. The compiler is located in the bin
directory:
./bin/cljsc
It says its convenient to set a CLOJURESCRIPT_HOME
environment variable pointing to the ClojureScript root directory and to add $CLOJURESCRIPT_HOME/bin
to your path.
We already did the first part in the "Notable Tools" section, so we should now add the `/bin` path:
export PATH=$CLOJURESCRIPT_HOME/bin:$PATH
And just to make sure things work:
$ cljsc
Usage: cljsc <file-or-dir>
cljsc <file-or-dirv "{:optimizations :advanced}"
It then goes on to say "cljsc
is convenient when a command-line tool is required or when a file or project only needs to be compiled once. While developing, it is much faster to use the build function from the Clojure (not ClojureScript) REPL."
Hmmm, there's a spanner in the works. The compiler was not what we were looking for all along. Perhaps? The Clojure (not ClojureScript) REPL was the first one we tried. Not sure why the ClojureScript REPL is no good, but I guess that's part of the reason they provide all those REPLs. To start the Clojure REPL we had to be in the clojurescript directory and run ./script/repl
. Then paste this:
(require '[cljs.closure :as cljsc])
(doc cljsc/build)
-------------------------
cljs.closure/build
([source opts])
Given a source which can be compiled, produce runnable JavaScript.
Obviously only the first two lines are the code, the stuff under the -------
is the result, so don't go pasting the whole thing in the REPL. I didn't do that, promise.
Using ClojureScript on a Web Page
Well, we didn't end up compiling anything above - so I'm still not really sure what the "build" thing does - but it claims to be "faster when developing" so, that sounds cool. Anyway, compiling stuff is old-school... it's all about using things in a web page these days, so let's get on with it.
"The following example shows the basic steps involved in creating a ClojureScript application and running it from a web page.". Hey hey! That also sounds pretty promising... I'm starting to wonder if we should have just got started here.
"The example assumes that you are working in the ClojureScript root directory." Hmm, strange sounding requirement seeing as we did all the classpath stuff - but hopefully it'll work easily in other directories too.
"In the example below, a function is created which will be called from JavaScript in a web page. The :export metadata ensures that this function name is not minified. The JavaScript function will be available as hello.greet."
Interesting - clojurescript must minify the generated JavaScript by default. The :export
tag stops it munging up the name. I don't know why that's the default - maybe it makes sense, or is standard Clojure. Anyhoo, we'll just have to remember the magic export
thing.
(ns hello)
(defn ^:export greet [n]
(str "Hello " n))
Next up: "Save this code into a file named hello.cljs and then compile it to JavaScript."
From the command-line, you invoke the clsjs
compiler from the "ClojureScript Compiler" section. There's also instructions for the REPL - you just have to do the steps from the last part on the clojure compile: require
-ing the cljsc/build
thing.
Both of them output the compiled hello.js
files.
Unrelated warning: *DO NOT LOOK AT THE JAVASCRIPT*, it will make you cry a little inside. Transpiling is compiling to a similar level of abstraction - so, like compiling your F# code into Scala then running that generated code on your production servers. It works fine, but it doesn't mean Scala coders are going to enjoy reading it.
Anyhoo... Linking that from a html file with alert(hello.greet("ClojureScript"))
runs some ClojureScript! We're on our way!
Running ClojureScript on Node.js
This section is just if you want to run directly from Node, as an alternative to running in the browser. That sounds fun, but let's stick to the browser for now and see if we can't get a better workflow in place. We can always come back to this later.
More about Compiling
"The cljsc tool, and the underlying build function, supports three levels of optimization and a development mode where no optimization is performed and each input JavaScript file is kept separate."
Oh, okay - that's interesting. The initial examples all assumed you were outputing for production, and there's some stuff we can do to make things more obvious while we're getting started. The "hello, world" function probably doesn't need to be production-ready so that sounds like a good idea.
By ommiting the :optimizations
option, the JavaScript will be compiled into the working directory (which defaults to ./out
) and write a dependencies file to hello.js." You need to cd
into the samples/hello
directory for this example, and run:
$ cljsc src '{:main hello}' > hello.js
If you want to do the REPL version then you actually shouldn't be in the samples/hello
dir, but back in the clojurescript root directory so you can run the repl. Then you need to add back all the paths to make up for that:
(cljsc/build "samples/hello/src" {:main hello :output-dir "samples/hello/out" :output-to "samples/hello/hello.js"})
Compiling takes an eternity in web time (like, 10 seconds) because it generates a folder called out
in the clojurescript directory that has a zillion dependencies. There's probably a way to do partial compilation or something like that though so we don't have to generate all the same old stuff, and wait 10 seconds every time we change something.
If you look at the files in samples/hello
, you'll notice it's completely different from the hello.cljs
version above: I think it's more of a complete project. There is a file called core.cljs
which has a bunch of functions, and a folder called foo
that demonstrates how how imports work. Or something.
There are two html files in the folder: hello.html
and hello-dev.html
. The first doesn't work, because the commands above don't have the "{:optimizations :advanced}"
flag. The hello-dev.html
throws the errors:
"goog.require could not find: hello" base.js:466:6
Error: goog.require could not find: hello
In the console, but the program appears to run - so, we'll take it. The production mode adds back in the "optimizations" flag, and does't specify the :main hello
thing - I guess that must be implied in advanced mode.
$ cljsc src '{:optimizations :advanced}' > hello.js
The good news here is that running hello.html
works perfectly, and no errors in the console!
We're done.
Phew, we made it to the end of the Quick Start section! We deserve a beer. I'm still not really sure what most of the pieces are for: the difference between the Clojure REPL, the ClojureScript REPL the Node ClojureScript REPL, what the bootstrap script is for, and so on... and we certainly didn't come out with anything resembling a reasonable workflow.
But it seems like just hacking on the samples/hello
folder with the "production settings" might be a good place to go from here if we actually wanted to start our own ClojureScript project. Who knows. One thing I promise though: if I ever figure any of this out, I'll come back and fix up the docs.
Post Script
Yes, I'm trolling. The problem of terrible intro docs is certainly not limited to ClojureScript: I witnessed a very smart coworker reduced to a quivering mess after spending the day trying to figure out JavaScript package managers. And naturally, he hasn't touched JavaScript since.
There are so many great projects out there, all looking to build up a loyal user base. Putting up barriers to entry is bad for the community and frustrating for new users. So please, fix your Getting Started docs!
One Comment
It’s actually much easier to get started using a template such as Chestnut or Reagent.
For example, all you have to do to get a Reagent project up and running is:
lein new reagent my-app
cd my-app
lein ring server &
lein figwheel
Your app will start up and open a browser page, and once figwheel loads you’ll be able to make changes in the editor and see them reflected live in the browser.