Skip to content

Instantly share code, notes, and snippets.

@Chouser
Created June 17, 2013 13:44
Show Gist options
  • Star 70 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save Chouser/5796967 to your computer and use it in GitHub Desktop.
Save Chouser/5796967 to your computer and use it in GitHub Desktop.
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 [clojure.java.io :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))
distinct))
(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 %)))
distinct))
(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
(concat
(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")
@cemerick
Copy link

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

@Chouser
Copy link
Author

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.

@ptaoussanis
Copy link

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

@swannodette
Copy link

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

@micha
Copy link

micha commented Nov 27, 2013

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

@dertseha
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?)

@markjfisher
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 http://www.dotkam.com/2013/07/15/clojurescript-use-any-javascript-library/

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