public
Created

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.

  • Download Gist
externs_for_cljs.clj
Clojure
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
(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")
  1. Does this really work? ;-)
  2. Why the hell doesn't gclosure just do this automatically‽
  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.

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

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

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

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

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.