ClojureScript Faux Pas: Forgetting the Extern

When immersing yourself in a new programming language, as in a new culture, it’s important to take time to understand the nuances of its common practices, values, and norms. My recent transition from JavaScript to ClojureScript regularly reminds me of this challenge.

If you haven’t heard of it, ClojureScript is written in Clojure, but compiles to JavaScript using the Google Closure library. ClojureScript includes a core standard library and allows you to write less stateful code, which I often find easier to test. 8th Light Craftsman Andrew Zures provides a great overview for setting up a Clojure project with ClojureScript.

As a newcomer to ClojureScript, I was happy to see how easily I could integrate functions from external JavaScript libraries using the .methodName syntax. In the following example, I use jQuery functions and the waypoints library function in my custom ClojureScript functions.

 1 (ns proj.landing-page
 2   (:require [jayq.core :refer [$]]))
 3 
 4 (defn toggle-logo [logo direction]
 5   (if (= direction "down")
 6     (.fadeIn logo "slow")
 7     (.fadeOut logo "slow")))
 8 
 9 (defn ^:export fade-logo-on-scroll [logo-selector border-selector]
10   (let [logo ($ logo-selector)]
11     (.hide logo)
12     (.waypoint ($ border-selector) (fn [direction]
13       (toggle-logo logo direction)))))

After including these libraries in my local assets folder, the fade-logo-on-scroll function worked as expected in test and development. However, once deployed to staging, the fade-logo-on-scroll function didn’t work, and an “Uncaught TypeError: Undefined is not a function” error message appeared in the browser console.

posts/2014-07-31-clojurescript-faux-pas/01.png

What function is this error message referring to and why the heck is it missing? And why is it missing in staging but not in development? Just above the TypeError, you may have observed that the “zf” function name actually refers to the waypoint function in my ClojureScript function. After compilation and minification, the JavaScript variables are renamed to make the production code much shorter for faster downloads.

In fact, it appears as though the compiler couldn’t find a reference to the .waypoint function in the waypoints JavaScript library. This issue only occurred in staging and production because the project’s settings specified a higher compilation level for production than for test and development. ClojureScript uses Google Closure optimizations so that the compiled JavaScript can load and run faster. Possible optimization levels include :whitespace (removes comments and whitespace), :simple (renames local variables to compress JavaScript), and :advanced. If not specified, the optimization level defaults to simple.

Advanced compilation is more aggressive than the other types of compilation in that it renames symbols to gain performance improvements. Unfortunately, this means that the compilation process breaks references to functions defined outside of the compiled code, such as those in external jQuery libraries, since the renamed function reference doesn’t match the name of the defined function.

To fix this problem, I had to add an extern for the external library method. Google Closure allows for the use of externs. Externs are files that declare the names of external libraries functions that should not be renamed. Google Closure provides extern files for some common libraries—in fact, the project that I was working on already included the jQuery extern, which is why I didn’t see any errors for the jQuery functions.

If you’re using a library that doesn’t have a pre-existing extern file (like the waypoints library in this example), you need to make your own. Thankfully, creating an extern file is easy. In your project’s externs folder, create a new .js file containing a JavaScript function with the same name of the function you want to declare should not be renamed. This function’s body should be empty. For example, I added a file called externs/waypoints.js and included the following declaration:

1 jQuery.prototype.waypoint = function(handler) {};

Next, in the project.clj file, specify this extern file for the :compiler associated with the build using :advanced optimizations. In the following example example, :externs is a vector containing the path to each extern file. By convention, these extern files reside in the project’s externs folder.

 1 :cljsbuild ~(let [compiled-js-path "resources/compiled-assets/proj.js"
 2                   test-command ["bin/speclj" compiled-js-path]]
 3   {:builds {:dev {:source-paths ["src/cljs" "spec/cljs"]
 4                   :compiler {:output-to compiled-js-path
 5                              :optimizations :whitespace
 6                              :pretty-print true}}
 7             :prod {:source-paths ["src/cljs"]
 8                    :compiler {:output-to compiled-js-path
 9                               :optimizations :advanced
10                               :externs ["externs/jquery-1.9.js"
11                                         "externs/waypoints.js"]}}}
12    :test-commands {"test" test-command}}))

Now, when ClojureScript compiles to JavaScript in the prod build, it doesn’t rename the .waypoints function.

To avoid surprises due to differences between compilation optimizations in various environments (i.e., development vs. production), it’s always a good idea to test the production compiler before deploying to staging or production. For example, I used the lein cljsbuild auto prod command to locally build the project using the prod environment’s advanced optimization setting. This helps to avoid some potentially embarrassing ClojureScript blunders.

Interested in 8th Light's services? Let's talk.

Contact Us