Knowing this one ClojureScript gotcha will save you hours
Summary: ClojureScript optimizes names by replacing them with shorter ones. Usually, that's a good thing. But it can get carried away. Externs are how you help it know what's unsafe to optimize.
Problem
Here's the situation: you're writing ClojureScript code, compiling it with no optimizations (because it's faster for development). Everything is working great. You compile it with advanced compilation and test it, and things start breaking. Hopefully it's just on your local machine and not on production. What's happening?
Analysis
Surprisingly, this happens a lot. As a Clojure programmer, I'm not used to really having a difference between development and production. It's the same language and everything is available in both environments.
But with ClojureScript, it helps to think of it as a compiled language. There's a real difference between development and production. The difference is that the compiler optimizes the code. In advanced compilation (what you want to do in production), variable names are shortened.
Here's some ClojureScript code and the compiled JavaScript (using
advanced optimizations and pretty printing) that uses the popular
JavaScript library marked
for
parsing Markdown:
ClojureScript
(defn parse-markdown [s]
(js/window.marked s))
JavaScript
function Af(a) {
return window.hc(a);
}
It's nice that the structure of the output is similar to the input. But notice that all of the names in the JavaScript are one or two letters. That's one of the ways a JavaScript file is minified. The compiler makes sure that this all makes sense where it can. And it can for all of the variables and namespaces you define.
But it cannot for variables defined in other JavaScript included in
the page. The compiler has no way of knowing by itself that this
should not be shortened. Look at the ClojureScript code on the left:
it's referring to js/window.marked
. And when it's output, it's called
window.hc
. That's no good and it's no wonder this code doesn't work in
the browser---hc
is not defined.
Solution
Of course, ClojureScript has a solution for this problem: externs.
You can specify a list of files that contain all of the variables that
you use that the compiler should not optimize away. Setting up your
externs is easy. Just add this to your :compiler
options in the
approprate build:
:externs ["externs/marked.js"]
I typically create a directory called externs/
right at the root. Be
sure to add this to your version control system. Then I make a file, in
this case called marked.js
, to put all of the variables I'll need to
access from that library. Inside:
window.marked = function () {};
It's just a JavaScript file, but I don't need to give it the actual
values. This is just for the compiler to know what to look for. See how
window.marked
is given a do-nothing function? It could be any
function, and that's just the shortest way to write one. This tells
the compiler two things:
- Please don't minify this variable.
- You can expect a function there.
If you have to define something that's a number, you would write this:
window.some_number = 1;
You can extend this to any type.
Alternate solution
Because the extern files are just JavaScript, you can normally simply use the JavaScript library as its own externs file. So I'll change the externs entry to point to the file included in the HTML.
:externs ["resources/public/js/marked.min.js"]
This works, but it produces 145 warnings. There is a compiler option
called :externs-validation
that you can turn off to suppress those
warnings.
:closure-warnings {:externs-validation :off}
That also goes in your :compiler
options in the cljsbuild build.
Conclusions
The Google Closure Compiler has some very advanced optimization settings. Since they're too slow to run during development, we typically will turn them off while we're coding and testing things out. However, when we turn them on, stuff can stop working. Most of the time it's due to a missing extern. If code that relies on something outside of ClojureScript stops working, that could be why. Check your externs.
If you're interested in getting started with ClojureScript, I recommend LispCast Single Page Applications with ClojureScript and Om. It uses Om to build an application from the ground up. The course teaches everything you need using animations, exercises, code screencasts, and more. It's the fastest and most effective way to learn to build Om applications.