Programmatically accessing ClojureScripts Externs Inference

TLDR: As of release 1.9.456 ClojureScript can infer externs preventing the clobbering of third party JavaScript libraries. We can retrieve the inferred externs programmatically which allows us to create our own custom externs file.

What are externs?

Lets say you want to use third party Foo library like so:

(defn foobar [arg]
  (let [my-foo (Foo. arg)]
    (.bar my-foo)))

As long as you don't use advanced compilation this will work just fine. But as soon as you build with advanced compilation the above code will fail due to the Google Closure Compiler munging bar into something like K. The Google Closure Compiler has no information about the Foo library so it aggressively munges the bar method.

To fix this you need to create an externs, basically a file which describes all the functions, methods, and classes contained in the third party library. So to use the Foo library you would need have a foo-externs.js containing

function Foo(arg1) {};

Foo.prototype.bar = function() {};

See this blog post for more details about externs and the Google Closure Compiler.

ClojureScript externs inference

With the 1.9.456 release ClojureScript now has Externs inference. ClojureScript now has the ability to infer the needed externs for external libraries and automatically create an externs file for you.

To turn on externs inference specify :infer-externs true in your compiler configuration. This will create an auto generated files inferred_externs.js in your output directory.

Go to the official ClojureScript externs guide for far more information on this new feature.

Programmatically retrieving inferred externs

Inferred externs are a great feature, can we access them programmatically?

For example for this snippet of ClojureScript Code

(defn foo [^js/React.Component c]
  (let [me (.render c)]
    (.woz me)))

Can we get a map of the inferred externs (woz and React.Component.render) like so?

{React {Component {prototype {render {}}}}, Object {woz {}}}

It turns out we can using the ClojureScript Analyzer API. The following code allows us to transform the ClojureScript snippet into a map of the inferred externs.

(require '[cljs.analyzer.api :as ana-api])
(require '[cljs.util :as util])

(defn get-snippet-analysis [cljs-code]
  (let [empty-compiler-env (ana-api/empty-state)
        empty-analyzer-env (ana-api/empty-env)]
    (ana-api/in-cljs-user
      empty-compiler-env
      (ana-api/analyze empty-analyzer-env cljs-code)
      empty-compiler-env)))

(defn get-externs-from-analysis [compiler-state]
  (let [cljs-ns (ana-api/all-ns compiler-state)
        find-ns-metadata #(ana-api/find-ns compiler-state %)]
    (reduce util/map-merge {}
      (map #(:externs (find-ns-metadata %)) cljs-ns))))

(defn get-inferred-externs [cljs-code]
  (let [snippet-analysis (get-snippet-analysis cljs-code)]
    (get-externs-from-analysis snippet-analysis)))

Lets go through what the each of these functions do.

get-snippet-analysis runs the ClojureScript analyzer over our code snippet returning the ClojureScript compilation state atom.

get-externs-from-analysis takes in the compilation state and extracts the externs inference from it. This will work for any ClojureScript analysis like analyze-seq or analyze-file.

get-inferred-externs combines get-snippet-analysis and get-externs-from-analysis for an easy to use function extracting externs from ClojureScript code snippets.

Notice that all we needed to extract the externs was publicly exposed ClojureScript Analyzer API. cljs.util/map-merge is a convenience function that does a deep merge of two maps, transforming a vector of maps:

[{React {Component {prototype {render {}}}}}, {Object {woz {}}}]

into one map

{React {Component {prototype {render {}}}}, Object {woz {}}}

What benefits do we get from grabbing the externs from the ClojureScript analysis? We already get the inferred externs packaged up in the inferred_externs.js file. One use case is annotating the externs to give the Closure Compiler more information. Like adding the return type of React.Component.render for example.

Conclusion

Inferring externs removes much of the busy work that was previously needed to integrate third party JavaScript libraries. Everything that is needed to access them programmatically is contained in the ClojureScript Analyzer API.