Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Generate an externs.js file for arbitrary JS libraries for use in advanced Google Closure compilation, based on the ClojureScript code that uses the libraries.
(ns n01se.externs-for-cljs
(:require [ :as io]
[cljs.compiler :as comp]
[cljs.analyzer :as ana]))
(defn read-file [file]
(let [eof (Object.)]
(with-open [stream (clojure.lang.LineNumberingPushbackReader. (io/reader file))]
(vec (take-while #(not= % eof)
(repeatedly #(read stream false eof)))))))
(defn file-analysis [file]
(binding [ana/*cljs-ns* 'cljs.user
ana/*cljs-file* file]
(mapv #(ana/analyze (ana/empty-env) %) (read-file file))))
(defn flat-file-analysis [file]
(mapcat #(tree-seq :children :children %)
(file-analysis file)))
(defn get-vars-used [ffa]
(->> ffa
(filter #(and (= (:op %) :var) (-> % :info :ns)))
(map #(-> % :info :name))
(defn var-defined? [sym]
(contains? (:defs (get @ana/namespaces (symbol (namespace sym))))
(symbol (name sym))))
(defn get-undefined-vars [ffa]
(remove var-defined? (get-vars-used ffa)))
(defn externs-for-var [sym]
(if (= "js" (namespace sym))
(format "var %s={};\n" (name sym))
(format "var %s={};\n%s.%s=function(){};\n"
(namespace sym) (namespace sym) (name sym))))
(defn get-interop-used [ffa]
(->> ffa
(filter #(and (= (:op %) :dot)))
(map #(or (:method %) (:field %)))
(defn externs-for-interop [sym]
(format "DummyExternClass.%s=function(){};\n" sym))
(defn externs-for-cljs [file]
(swap! ana/namespaces empty)
(ana/analyze-file "cljs/core.cljs")
(let [ffa (flat-file-analysis file)]
(apply str
(map externs-for-var (get-undefined-vars ffa))
["var DummyExternClass={};\n"]
(map externs-for-interop (get-interop-used ffa))))))
;; (externs-for-cljs "/home/chouser/proj/cljs-example/src/main.cljs")
Copy link

  1. Does this really work? ;-)
  2. Why the hell doesn't gclosure just do this automatically‽

Copy link

Chouser commented Jul 28, 2013

  1. It's sufficient for some input sources, but I've probably missed some cases.
  2. Maybe because in ClojureScript we can tell the difference between interop, vars, and locals more easily than in JavaScript? Not sure.

Copy link

This is terrific - just saved me some serious headache. Much appreciated!

Copy link

It's probably worthwhile to tidy this up and get it into the compiler.

Copy link

micha commented Nov 27, 2013

This is a nice example of using the CLJS analyzer, too. Thanks @Chouser.

Copy link

dertseha commented Dec 6, 2013

I'm a bit lost here; Without knowing ClojureScript (just got it working on my Windows machine) - how do I run this script to generate the externs? (Also: it is meant to create externals for plain .js files, right?)

Copy link

What version of clojurescript was this written/work against? I've just run it on some code of my own and I'm getting:
ClassCastException cljs.analyzer$reify__4241 cannot be cast to clojure.lang.Atom clojure.core/swap! (core.clj:2160)

I'm using 0.0-2202.

Also, for @dertseha there's a blog on using this at

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment