From c0b501da3d1a35c88336b02d063b2ce0473c205d Mon Sep 17 00:00:00 2001 From: Thomas Heller Date: Tue, 27 Mar 2018 15:53:31 +0200 Subject: [PATCH] quickfix for closure compatibility issue until their next release applies https://dev.clojure.org/jira/browse/CLJS-2694 --- src/main/shadow/build/cljs_bridge.clj | 1 + src/main/shadow/build/cljs_closure.clj | 3095 ++++++++++++++++++++++++ 2 files changed, 3096 insertions(+) create mode 100644 src/main/shadow/build/cljs_closure.clj diff --git a/src/main/shadow/build/cljs_bridge.clj b/src/main/shadow/build/cljs_bridge.clj index 7d440cf2..bd8a0aa8 100644 --- a/src/main/shadow/build/cljs_bridge.clj +++ b/src/main/shadow/build/cljs_bridge.clj @@ -15,6 +15,7 @@ [cljs.env :as cljs-env] [shadow.cljs.util :as util] [shadow.build.ns-form :as ns-form] + [shadow.build.cljs-closure] [shadow.build.cljs-hacks] [shadow.build.data :as data]) (:import (java.io PushbackReader StringReader) diff --git a/src/main/shadow/build/cljs_closure.clj b/src/main/shadow/build/cljs_closure.clj new file mode 100644 index 00000000..6fbdc0c1 --- /dev/null +++ b/src/main/shadow/build/cljs_closure.clj @@ -0,0 +1,3095 @@ +(ns shadow.build.cljs-closure) + +;; temp hack namespace to get the patch applied quickly. +;; https://dev.clojure.org/jira/browse/CLJS-2694 +;; currently required because of a regression in closure that was not released yet +;; https://github.com/google/closure-compiler/issues/2822 + + +; Copyright (c) Rich Hickey. All rights reserved. +; The use and distribution terms for this software are covered by the +; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) +; which can be found in the file epl-v10.html at the root of this distribution. +; By using this software in any fashion, you are agreeing to be bound by +; the terms of this license. +; You must not remove this notice, or any other, from this software. + +(ns cljs.closure + (:refer-clojure :exclude [compile]) + (:require [cljs.util :as util :refer [distinct-by]] + [cljs.core :as cljsm] + [cljs.compiler :as comp] + [cljs.analyzer :as ana] + [cljs.source-map :as sm] + [cljs.env :as env] + [cljs.js-deps :as deps] + [clojure.java.io :as io] + [clojure.set :as set] + [clojure.string :as string] + [clojure.data.json :as json] + [clojure.tools.reader :as reader] + [clojure.tools.reader.reader-types :as readers] + [cljs.module-graph :as module-graph]) + (:import [java.lang ProcessBuilder] + [java.io + File BufferedInputStream BufferedReader + Writer InputStreamReader IOException StringWriter ByteArrayInputStream] + [java.net URL] + [java.util.logging Level] + [java.util List Random] + [java.util.concurrent + TimeUnit LinkedBlockingDeque Executors CountDownLatch] + [com.google.javascript.jscomp CompilerOptions CompilationLevel + CompilerInput CompilerInput$ModuleType DependencyOptions + CompilerOptions$LanguageMode SourceMap$Format + SourceMap$DetailLevel ClosureCodingConvention SourceFile + Result JSError CheckLevel DiagnosticGroups + CommandLineRunner AnonymousFunctionNamingPolicy + JSModule SourceMap VariableMap] + [com.google.javascript.jscomp.deps ModuleLoader$ResolutionMode ModuleNames] + [com.google.javascript.rhino Node] + [java.nio.file Path Paths Files StandardWatchEventKinds WatchKey + WatchEvent FileVisitor FileVisitResult] + [java.nio.charset Charset StandardCharsets] + [com.sun.nio.file SensitivityWatchEventModifier] + [com.google.common.base Throwables])) + +(def name-chars (map char (concat (range 48 57) (range 65 90) (range 97 122)))) + +(defn random-char [] + (nth name-chars (.nextInt (Random.) (count name-chars)))) + +(defn random-string [length] + (apply str (take length (repeatedly random-char)))) + +(defn- sym->var + "Converts a namespaced symbol to a var, loading the requisite namespace if + needed. For use with a function defined under a keyword in opts. The kw and + ex-data arguments are used to form exceptions." + [sym kw ex-data] + (let [ns (namespace sym) + _ (when (nil? ns) + (throw + (ex-info (str kw " symbol " sym " is not fully qualified") + (merge ex-data {kw sym})))) + var-ns (symbol ns)] + (when (not (find-ns var-ns)) + (try + (locking ana/load-mutex + (require var-ns)) + (catch Throwable t + (throw (ex-info (str "Cannot require namespace referred by " kw " value " sym) + (merge ex-data {kw sym}) + t))))) + + (find-var sym))) + +(defn- opts-fn + "Extracts a function from opts, by default expecting a function value, but + converting from a namespaced symbol if needed." + [kw opts] + (when-let [fn-or-sym (kw opts)] + (cond-> fn-or-sym (symbol? fn-or-sym) (sym->var kw {})))) + +;; Closure API +;; =========== + +(defmulti js-source-file (fn [_ source] (class source))) + +(defmethod js-source-file String [^String name ^String source] + (SourceFile/fromCode name source)) + +(defmethod js-source-file File [_ ^File source] + (SourceFile/fromFile source)) + +(defmethod js-source-file BufferedInputStream [^String name ^BufferedInputStream source] + (SourceFile/fromInputStream name source)) + +(def check-level + {:error CheckLevel/ERROR + :warning CheckLevel/WARNING + :off CheckLevel/OFF}) + +(def warning-types + (reduce + (fn [warnings [registered-name group]] + (let [kw (-> registered-name + (string/replace #"[A-Z]" #(str "-" (string/lower-case %))) + keyword)] + (assoc warnings kw group))) + {} + (-> (DiagnosticGroups.) + (.getRegisteredGroups)))) + +(def known-opts + "Set of all known compiler options." + #{:anon-fn-naming-policy :asset-path :cache-analysis :closure-defines :closure-extra-annotations + :closure-warnings :compiler-stats :dump-core :elide-asserts :externs :foreign-libs + :hashbang :language-in :language-out :libs :main :modules :source-map-path :source-map-asset-path + :optimizations :optimize-constants :output-dir :output-to :output-wrapper :parallel-build :preamble + :pretty-print :print-input-delimiter :pseudo-names :recompile-dependents :source-map + :source-map-inline :source-map-timestamp :static-fns :target :verbose :warnings + :emit-constants :ups-externs :ups-foreign-libs :ups-libs :warning-handlers :preloads + :browser-repl :cache-analysis-format :infer-externs :closure-generate-exports :npm-deps + :fn-invoke-direct :checked-arrays :closure-module-roots :rewrite-polyfills :use-only-custom-externs + :watch :watch-error-fn :watch-fn :install-deps :process-shim :rename-prefix :rename-prefix-namespace + :closure-variable-map-in :closure-property-map-in :closure-variable-map-out :closure-property-map-out + :stable-names :ignore-js-module-exts :opts-cache :aot-cache}) + +(def string->charset + {"iso-8859-1" StandardCharsets/ISO_8859_1 + "us-ascii" StandardCharsets/US_ASCII + "utf-16" StandardCharsets/UTF_16 + "utf-16be" StandardCharsets/UTF_16BE + "utf-16le" StandardCharsets/UTF_16LE + "utf-8" StandardCharsets/UTF_8}) + +(defn to-charset [charset] + (cond + (instance? Charset charset) charset + (and (string? charset) + (contains? string->charset (string/lower-case charset))) + (get string->charset (string/lower-case charset)) + :else + (throw + (ex-info + (str "Invalid :closure-output-charset " charset " given, only " + (string/join ", " (keys string->charset)) " supported ") + {})))) + +(defn ^CompilerOptions$LanguageMode lang-key->lang-mode [key] + (case (keyword (string/replace (name key) #"^es" "ecmascript")) + :no-transpile CompilerOptions$LanguageMode/NO_TRANSPILE ;; same mode as input (for language-out only) + :ecmascript3 CompilerOptions$LanguageMode/ECMASCRIPT3 + :ecmascript5 CompilerOptions$LanguageMode/ECMASCRIPT5 + :ecmascript5-strict CompilerOptions$LanguageMode/ECMASCRIPT5_STRICT + :ecmascript6 CompilerOptions$LanguageMode/ECMASCRIPT_2015 ;; (deprecated and remapped) + :ecmascript6-strict CompilerOptions$LanguageMode/ECMASCRIPT_2015 ;; (deprecated and remapped) + :ecmascript-2015 CompilerOptions$LanguageMode/ECMASCRIPT_2015 + :ecmascript6-typed CompilerOptions$LanguageMode/ECMASCRIPT6_TYPED + :ecmascript-2016 CompilerOptions$LanguageMode/ECMASCRIPT_2016 + :ecmascript-2017 CompilerOptions$LanguageMode/ECMASCRIPT_2017 + :ecmascript-next CompilerOptions$LanguageMode/ECMASCRIPT_NEXT)) + +(defn set-options + "TODO: Add any other options that we would like to support." + [opts ^CompilerOptions compiler-options] + (.setModuleResolutionMode compiler-options ModuleLoader$ResolutionMode/NODE) + + (when (contains? opts :pretty-print) + (.setPrettyPrint compiler-options (:pretty-print opts))) + + (when (contains? opts :pseudo-names) + (set! (.generatePseudoNames compiler-options) (:pseudo-names opts))) + + (when (contains? opts :anon-fn-naming-policy) + (let [policy (:anon-fn-naming-policy opts)] + (set! (.anonymousFunctionNaming compiler-options) + (case policy + :off AnonymousFunctionNamingPolicy/OFF + :unmapped AnonymousFunctionNamingPolicy/UNMAPPED + :mapped AnonymousFunctionNamingPolicy/MAPPED + (throw (IllegalArgumentException. (str "Invalid :anon-fn-naming-policy value " policy " - only :off, :unmapped, :mapped permitted"))))))) + + (when-let [lang-key (:language-in opts :ecmascript5)] + (.setLanguageIn compiler-options (lang-key->lang-mode lang-key))) + + (when-let [lang-key (:language-out opts)] + (.setLanguageOut compiler-options (lang-key->lang-mode lang-key))) + + (when (contains? opts :print-input-delimiter) + (set! (.printInputDelimiter compiler-options) + (:print-input-delimiter opts))) + + (when (contains? opts :closure-warnings) + (doseq [[type level] (:closure-warnings opts)] + (. compiler-options + (setWarningLevel (type warning-types) (level check-level))))) + + (when (contains? opts :closure-extra-annotations) + (. compiler-options + (setExtraAnnotationNames (map name (:closure-extra-annotations opts))))) + + (when (contains? opts :closure-module-roots) + (. compiler-options + (setModuleRoots (:closure-module-roots opts)))) + + (when (contains? opts :closure-generate-exports) + (. compiler-options + (setGenerateExports (:closure-generate-exports opts)))) + + (when (contains? opts :rewrite-polyfills) + (. compiler-options + (setRewritePolyfills (:rewrite-polyfills opts)))) + + (when (contains? opts :rename-prefix) + (. compiler-options + (setRenamePrefix (:rename-prefix opts)))) + + (when (contains? opts :rename-prefix-namespace) + (. compiler-options + (setRenamePrefixNamespace (:rename-prefix-namespace opts)))) + + (when (contains? opts :closure-variable-map-in) + (let [var-in (io/file (:closure-variable-map-in opts))] + (when (.exists var-in) + (.setInputVariableMap compiler-options + (VariableMap/load (.getAbsolutePath var-in)))))) + + (when (contains? opts :closure-property-map-in) + (let [prop-in (io/file (:closure-property-map-in opts))] + (when (.exists prop-in) + (.setInputPropertyMap compiler-options + (VariableMap/load (.getAbsolutePath prop-in)))))) + + (. compiler-options + (setOutputCharset (to-charset (:closure-output-charset opts "UTF-8"))) ;; only works > 20160125 Closure Compiler + ) + + compiler-options) + +(defn ^CompilerOptions make-options + "Create a CompilerOptions object and set options from opts map." + [opts] + (let [level (case (:optimizations opts) + :advanced CompilationLevel/ADVANCED_OPTIMIZATIONS + :whitespace CompilationLevel/WHITESPACE_ONLY + :simple CompilationLevel/SIMPLE_OPTIMIZATIONS) + compiler-options (doto (CompilerOptions.) + (.setCodingConvention (ClosureCodingConvention.)))] + (doseq [[key val] (:closure-defines opts)] + (let [key (name key)] + (cond + (string? val) (.setDefineToStringLiteral compiler-options key val) + (number? val) (.setDefineToDoubleLiteral compiler-options key val) + (or (true? val) + (false? val)) (.setDefineToBooleanLiteral compiler-options key val) + :else (println "value for" key "must be string, int, float, or bool")))) + (if-let [extra-annotations (:closure-extra-annotations opts)] + (. compiler-options (setExtraAnnotationNames (map name extra-annotations)))) + (when (:source-map opts) + (if (:modules opts) + ;; name is not actually used by Closure in :modules case, + ;; but we need to provide _something_ for Closure to not + ;; complain + (set! (.sourceMapOutputPath compiler-options) + (str (io/file (util/output-directory opts) + "cljs_modules.map"))) + (set! (.sourceMapOutputPath compiler-options) + (:source-map opts))) + (set! (.sourceMapDetailLevel compiler-options) + SourceMap$DetailLevel/ALL) + (set! (.sourceMapFormat compiler-options) + SourceMap$Format/V3)) + (do + (.setOptionsForCompilationLevel level compiler-options) + (set-options opts compiler-options) + compiler-options))) + +(defn load-externs + "Externs are JavaScript files which contain empty definitions of + functions which will be provided by the environment. Any function in + an extern file will not be renamed during optimization. + + Options may contain an :externs key with a list of file paths to + load. The :use-only-custom-externs flag may be used to indicate that + the default externs should be excluded." + [{:keys [externs use-only-custom-externs target ups-externs infer-externs] :as opts}] + (let [validate (fn validate [p us] + (if (empty? us) + (throw (IllegalArgumentException. + (str "Extern " p " does not exist"))) + us)) + filter-cp-js (fn [paths] + (for [p paths + u (deps/find-js-classpath p)] + u)) + filter-js (fn [paths] + (for [p paths + u (deps/find-js-resources p)] + u)) + add-target (fn [ext] + (cons (io/resource "cljs/externs.js") + (if (= :nodejs target) + (cons (io/resource "cljs/nodejs_externs.js") + (or ext [])) + ext))) + load-js (fn [ext] + (map #(js-source-file (.getFile %) (slurp %)) ext))] + (let [js-sources (-> externs filter-js add-target load-js) + ups-sources (-> ups-externs filter-cp-js load-js) + all-sources (vec (concat js-sources ups-sources))] + (cond-> + (if use-only-custom-externs + all-sources + (into all-sources (CommandLineRunner/getDefaultExterns))) + infer-externs + (conj (js-source-file nil + (io/file (util/output-directory opts) "inferred_externs.js"))))))) + +(defn ^com.google.javascript.jscomp.Compiler make-closure-compiler [] + (let [compiler (com.google.javascript.jscomp.Compiler.)] + (com.google.javascript.jscomp.Compiler/setLoggingLevel Level/WARNING) + compiler)) + +(defn report-failure [^Result result] + (let [errors (.errors result) + warnings (.warnings result)] + (binding [*out* *err*] + (doseq [next (seq errors)] + (println "ERROR:" (.toString ^JSError next))) + (doseq [next (seq warnings)] + (println "WARNING:" (.toString ^JSError next))) + (when (seq errors) + (throw (Exception. "Closure compilation failed")))))) + +;; Protocols for IJavaScript and Compilable +;; ======================================== + + + +(defprotocol ISourceMap + (-source-url [this] "Return the CLJS source url") + (-source-map [this] "Return the CLJS compiler generated JS source mapping")) + +(extend-protocol deps/IJavaScript + + String + (-foreign? [this] false) + (-closure-lib? [this] false) + (-url + ([this] nil) + ([this _] nil)) + (-relative-path + ([this] nil) + ([this _] nil)) + (-provides [this] + (let [{:keys [provides]} (deps/parse-js-ns (string/split-lines this))] + (cond-> provides + (empty? provides) + (conj (util/content-sha this 7))))) + (-requires [this] (:requires (deps/parse-js-ns (string/split-lines this)))) + (-source + ([this] this) + ([this _] this)) + + clojure.lang.IPersistentMap + (-foreign? [this] (:foreign this)) + (-closure-lib? [this] (:closure-lib this)) + (-url + ([this] (deps/-url this nil)) + ([this opts] + (let [[url file] (if-let [url-min (and (#{:advanced :simple} (:optimizations opts)) + (:url-min this))] + [url-min (:file-min this)] + [(:url this) (:file this)])] + (or url (deps/to-url file))))) + (-relative-path + ([this] (deps/-relative-path this nil)) + ([this opts] + (let [file (if-let [file-min (and (#{:advanced :simple} (:optimizations opts)) + (:file-min this))] + file-min + (:file this)) + as-file (io/as-file file)] + (when (and as-file (not (.isAbsolute as-file))) + file)))) + (-provides [this] (map name (:provides this))) + (-requires [this] (map name (:requires this))) + (-source + ([this] (deps/-source this nil)) + ([this opts] + (if-let [s (:source this)] + s + (with-open [reader (io/reader (deps/-url this opts))] + (slurp reader)))))) + +(defrecord JavaScriptFile [foreign ^URL url ^URL source-url provides requires lines source-map] + deps/IJavaScript + (-foreign? [this] foreign) + (-closure-lib? [this] (:closure-lib this)) + (-url [this] url) + (-url [this opts] url) + (-relative-path [this] nil) + (-relative-path [this opts] nil) + (-provides [this] provides) + (-requires [this] requires) + (-source [this] (deps/-source this nil)) + (-source [this opts] + (with-open [reader (io/reader url)] + (slurp reader))) + ISourceMap + (-source-url [this] source-url) + (-source-map [this] source-map)) + +(defn javascript-file + ([foreign ^URL url provides requires] + (javascript-file foreign url nil provides requires nil nil)) + ([foreign ^URL url source-url provides requires lines source-map] + (assert (first provides) (str source-url " does not provide a namespace")) + (JavaScriptFile. foreign url source-url (map name provides) (map name requires) lines source-map))) + +(defn map->javascript-file [m] + (merge + (javascript-file + (:foreign m) + (when-let [f (or (:file m) (:url m))] + (deps/to-url f)) + (when-let [sf (:source-file m)] + (deps/to-url sf)) + (:provides m) + (:requires m) + (:lines m) + (:source-map m)) + (when-let [source-file (:source-file m)] + {:source-file source-file}) + (when-let [out-file (:out-file m)] + {:out-file out-file}) + (when (:closure-lib m) + {:closure-lib true}) + (when-let [ns (:ns m)] + {:ns ns}) + (when (:macros-ns m) + {:macros-ns true}))) + +(defn read-js + "Read a JavaScript file returning a map of file information." + [f] + (let [source (slurp f) + m (deps/parse-js-ns (string/split-lines source))] + (map->javascript-file (assoc m :file f)))) + + +;; Compile +;; ======= + +(defprotocol Inputs + (-paths [this] "Returns the file paths to the source inputs")) + +(extend-protocol Inputs + String + (-paths [this] [(io/file this)]) + File + (-paths [this] [this])) + +(defprotocol Compilable + (-compile [this opts] "Returns one or more IJavaScripts.") + (-find-sources [this opts] "Returns one or more IJavascripts, without compiling them.")) + +(defn compile-form-seq + "Compile a sequence of forms to a JavaScript source string." + ([forms] + (compile-form-seq forms + (when env/*compiler* + (:options @env/*compiler*)))) + ([forms opts] + (comp/with-core-cljs opts + (fn [] + (with-out-str + (binding [ana/*cljs-ns* 'cljs.user] + (doseq [form forms] + (comp/emit (ana/analyze (ana/empty-env) form))))))))) + +(defn compiled-file + "Given a map with at least a :file key, return a map with + {:file .. :provides .. :requires ..}. + + Compiled files are cached so they will only be read once." + [m] + (let [path (.getPath (.toURL ^File (:file m))) + js (if (:provides m) + (map->javascript-file m) + (if-let [js (get-in @env/*compiler* [::compiled-cljs path])] + js + (read-js (:file m))))] + (swap! env/*compiler* update-in [::compiled-cljs] assoc path js) + js)) + +(defn compile + "Given a Compilable, compile it and return an IJavaScript." + [compilable opts] + (-compile compilable opts)) + +(defn find-sources + "Given a Compilable, find sources and return a sequence of IJavaScript." + [compilable opts] + (-find-sources compilable opts)) + +(defn compile-file + "Compile a single cljs file. If no output-file is specified, returns + a string of compiled JavaScript. With an output-file option, the + compiled JavaScript will written to this location and the function + returns a JavaScriptFile. In either case the return value satisfies + IJavaScript." + [^File file {:keys [output-file] :as opts}] + (if output-file + (let [out-file (.toString (io/file (util/output-directory opts) output-file))] + (compiled-file (comp/compile-file file out-file opts))) + (let [path (.getPath ^File file)] + (binding [ana/*cljs-file* path] + (with-open [rdr (io/reader file)] + (compile-form-seq (ana/forms-seq* rdr path))))))) + +(defn compile-dir + "Recursively compile all cljs files under the given source + directory. Return a list of JavaScriptFiles." + [^File src-dir opts] + (let [out-dir (util/output-directory opts)] + (map compiled-file + (comp/compile-root src-dir out-dir opts)))) + +(defn ^String path-from-jarfile + "Given the URL of a file within a jar, return the path of the file + from the root of the jar." + [^URL url] + (last (string/split (.getFile url) #"\.jar!/"))) + +(defn jar-file-to-disk + "Copy a file contained within a jar to disk. Return the created file." + ([url out-dir] + (jar-file-to-disk url out-dir + (when env/*compiler* + (:options @env/*compiler*)))) + ([url out-dir opts] + (let [out-file (io/file out-dir (path-from-jarfile url)) + content (with-open [reader (io/reader url)] + (slurp reader))] + (when (and url (or ana/*verbose* (:verbose opts))) + (util/debug-prn "Copying" (str url) "to" (str out-file))) + (util/mkdirs out-file) + (spit out-file content) + (.setLastModified ^File out-file (util/last-modified url)) + out-file))) + +(defn compile-from-jar + "Compile a file from a jar if necessary. Returns IJavaScript." + [jar-file {:keys [output-file] :as opts}] + (let [out-file (when output-file + (io/file (util/output-directory opts) output-file)) + cacheable (ana/cacheable-files jar-file (util/ext jar-file) opts)] + (when (or (nil? out-file) + (comp/requires-compilation? jar-file out-file opts)) + ;; actually compile from JAR + (if (or (not (:aot-cache opts)) + (not (.canWrite (io/file (System/getProperty "user.home"))))) + (-compile (jar-file-to-disk jar-file (util/output-directory opts) opts) opts) + (let [cache-path (ana/cache-base-path (util/path jar-file) opts)] + (when (comp/requires-compilation? jar-file (:output-file cacheable) opts) + (-compile (jar-file-to-disk jar-file cache-path opts) + (assoc opts :output-dir (util/path cache-path)))) + (doseq [[k ^File f] cacheable] + (when (.exists f) + (let [target (io/file (util/output-directory opts) + (-> (.getAbsolutePath f) + (string/replace (.getAbsolutePath cache-path) "") + (subs 1)))] + (when (and (or ana/*verbose* (:verbose opts)) (= :output-file k)) + (util/debug-prn (str "Copying cached " f " to " target))) + (util/mkdirs target) + (spit target (slurp f)) + (.setLastModified target (util/last-modified jar-file)))))))) + ;; have to call compile-file as it includes more IJavaScript + ;; information than ana/parse-ns for now + (compile-file + (io/file (util/output-directory opts) + (last (string/split (.getPath ^URL jar-file) #"\.jar!/"))) + opts))) + +(defn find-jar-sources [this opts] + [(comp/find-source this)]) + +(extend-protocol Compilable + + File + (-compile [this opts] + (if (.isDirectory this) + (compile-dir this opts) + (compile-file this opts))) + (-find-sources [this _] + (if (.isDirectory this) + (comp/find-root-sources this) + [(comp/find-source this)])) + + URL + (-compile [this opts] + (case (.getProtocol this) + "file" (-compile (io/file this) opts) + "jar" (compile-from-jar this opts))) + (-find-sources [this opts] + (case (.getProtocol this) + "file" (-find-sources (io/file this) opts) + "jar" (find-jar-sources this opts))) + + clojure.lang.PersistentList + (-compile [this opts] + (compile-form-seq [this])) + (-find-sources [this opts] + [(ana/parse-ns [this] opts)]) + + String + (-compile [this opts] (-compile (io/file this) opts)) + (-find-sources [this opts] (-find-sources (io/file this) opts)) + + clojure.lang.Symbol + (-compile [this opts] + (-compile (util/ns->source this) opts)) + (-find-sources [this opts] + (-find-sources (util/ns->source this) opts)) + + clojure.lang.PersistentVector + (-compile [this opts] (compile-form-seq this)) + (-find-sources [this opts] + [(ana/parse-ns this opts)]) + + clojure.lang.IPersistentSet + (-compile [this opts] + (doall (map (comp #(-compile % opts) util/ns->source) this))) + (-find-sources [this opts] + (into [] (mapcat #(-find-sources % opts)) this)) + ) + +(comment + ;; compile a file in memory + (-compile "samples/hello/src/hello/core.cljs" {}) + (-find-sources "samples/hello/src/hello/core.cljs" {}) + ;; compile a file to disk - see file @ 'out/clojure/set.js' + (-compile (io/resource "clojure/set.cljs") {:output-file "clojure/set.js"}) + (-find-sources (io/resource "clojure/set.cljs") {:output-file "clojure/set.js"}) + ;; compile a project + (-compile (io/file "samples/hello/src") {}) + (-find-sources (io/file "samples/hello/src") {}) + ;; compile a project with a custom output directory + (-compile (io/file "samples/hello/src") {:output-dir "my-output"}) + (-find-sources (io/file "samples/hello/src") {:output-dir "my-output"}) + ;; compile a form + (-compile '(defn plus-one [x] (inc x)) {}) + ;; compile a vector of forms + (-compile '[(ns test.app (:require [goog.array :as array])) + (defn plus-one [x] (inc x))] + {}) + + (-find-sources 'cljs.core {}) + ) + +(defn js-dependencies + "Given a sequence of Closure namespace strings, return the list of + all dependencies. The returned list includes all Google and + third-party library dependencies. + + Third-party libraries are configured using the :libs option where + the value is a list of directories containing third-party + libraries." + [opts requires] + (loop [requires requires + visited (set requires) + deps #{}] + (if (seq requires) + (let [node (or (get (@env/*compiler* :js-dependency-index) (first requires)) + (deps/find-classpath-lib (first requires))) + new-req (remove #(contains? visited %) (:requires node))] + (recur (into (rest requires) new-req) + (into visited new-req) + (conj deps node))) + (remove nil? deps)))) + +(comment + ;; find dependencies + (js-dependencies {} ["goog.array"]) + ;; find dependencies in an external library + (js-dependencies {:libs ["closure/library/third_party/closure"]} ["goog.dom.query"]) + ) + +(defn add-core-macros-if-cljs-js + "If a compiled entity is the cljs.js namespace, explicitly + add the cljs.core macros namespace dependency to it." + [compiled] + (cond-> compiled + ;; TODO: IJavaScript :provides :requires should really + ;; always be Vector - David + (= ["cljs.js"] (into [] (map str) (deps/-provides compiled))) + (update-in [:requires] concat ["cljs.core$macros"]))) + +(defn get-compiled-cljs + "Return an IJavaScript for this file. Compiled output will be + written to the working directory." + [opts {:keys [relative-path uri]}] + (let [js-file (comp/rename-to-js relative-path) + compiled (-compile uri (merge opts {:output-file js-file}))] + (add-core-macros-if-cljs-js compiled))) + +(defn cljs-source-for-namespace + "Given a namespace return the corresponding source with either a .cljs or + .cljc extension." + [ns] + (if (= "cljs.core$macros" (str ns)) + (let [relpath "cljs/core.cljc"] + {:relative-path relpath :uri (io/resource relpath) :ext :cljc}) + (let [path (-> (munge ns) (string/replace \. \/)) + relpath (str path ".cljs")] + (if-let [res (io/resource relpath)] + {:relative-path relpath :uri res :ext :cljs} + (let [relpath (str path ".cljc")] + (if-let [res (io/resource relpath)] + {:relative-path relpath :uri res :ext :cljc})))))) + +(defn source-for-namespace + "Given a namespace and compilation environment return the relative path and + uri of the corresponding source regardless of the source language extension: + .cljs, .cljc, .js" + [ns compiler-env] + (let [ns-str (str (comp/munge ns {})) + path (string/replace ns-str \. \/) + relpath (str path ".cljs")] + (if-let [cljs-res (io/resource relpath)] + {:relative-path relpath :uri cljs-res :ext :cljs} + (let [relpath (str path ".cljc")] + (if-let [cljc-res (io/resource relpath)] + {:relative-path relpath :uri cljc-res :ext :cljc} + (let [relpath (str path ".js")] + (if-let [js-res (io/resource relpath)] + {:relative-path relpath :uri js-res :ext :js} + (let [ijs (get-in @compiler-env [:js-dependency-index (str ns)]) + relpath (or (:file ijs) (:url ijs))] + (if-let [js-res (and relpath + ;; try to parse URL, otherwise just return local + ;; resource + (or (and (util/url? relpath) relpath) + (try (URL. relpath) (catch Throwable t)) + (io/resource relpath)))] + {:relative-path relpath :uri js-res :ext :js} + (throw + (IllegalArgumentException. + (str "Namespace " ns " does not exist")))))))))))) + +(defn cljs-dependencies + "Given a list of all required namespaces, return a list of + IJavaScripts which are the cljs dependencies. The returned list will + not only include the explicitly required files but any transitive + dependencies as well. JavaScript files will be compiled to the + working directory if they do not already exist. + + Only load dependencies from the classpath." + [opts requires] + (letfn [(cljs-deps [lib-names] + (->> lib-names + (remove #(or ((@env/*compiler* :js-dependency-index) %) + (deps/find-classpath-lib %))) + (map cljs-source-for-namespace) + (remove (comp nil? :uri))))] + (loop [required-files (cljs-deps requires) + visited (set required-files) + js-deps #{}] + (if (seq required-files) + (let [next-file (first required-files) + js (get-compiled-cljs opts next-file) + new-req (remove #(contains? visited %) (cljs-deps (deps/-requires js)))] + (recur (into (rest required-files) new-req) + (into visited new-req) + (conj js-deps js))) + (disj js-deps nil))))) + +(comment + ;; only get cljs deps + (cljs-dependencies {} ["goog.string" "cljs.core"]) + ;; get transitive deps + (cljs-dependencies {} ["clojure.string"]) + ;; don't get cljs.core twice + (cljs-dependencies {} ["cljs.core" "clojure.string"]) + ) + +(defn find-cljs-dependencies + "Given set of cljs namespace symbols, find IJavaScript objects for the namespaces." + [requires] + (letfn [(cljs-deps [namespaces] + (->> namespaces + (remove #(or ((@env/*compiler* :js-dependency-index) %) + (deps/find-classpath-lib %))) + (map cljs-source-for-namespace) + (remove (comp nil? :uri))))] + (loop [required-files (cljs-deps requires) + visited (set required-files) + cljs-namespaces #{}] + (if (seq required-files) + (let [next-file (first required-files) + ns-info (ana/parse-ns (:uri next-file)) + new-req (remove #(contains? visited %) (cljs-deps (cond-> (deps/-requires ns-info) + (= 'cljs.js (:ns ns-info)) (conj "cljs.core$macros"))))] + (recur (into (rest required-files) new-req) + (into visited new-req) + (conj cljs-namespaces ns-info))) + (disj cljs-namespaces nil))))) + +(defn- constants-filename + "Returns the filename of the constants table." + [opts] + (str (util/output-directory opts) File/separator + (string/replace (str ana/constants-ns-sym) "." File/separator) ".js")) + +(defn- constants-javascript-file + "Returns the constants table as a JavaScriptFile." + [opts] + (let [url (deps/to-url (constants-filename opts))] + (javascript-file nil url [(str ana/constants-ns-sym)] ["cljs.core"]))) + +(defn add-dependencies + "DEPRECATED: Given one or more IJavaScript objects in dependency order, produce + a new sequence of IJavaScript objects which includes the input list + plus all dependencies in dependency order." + [opts & inputs] + (let [inputs (set inputs) + requires (set (mapcat deps/-requires inputs)) + required-cljs (clojure.set/difference (cljs-dependencies opts requires) inputs) + required-js (js-dependencies opts + (into (set (mapcat deps/-requires required-cljs)) requires))] + (cons + (javascript-file nil (io/resource "goog/base.js") ["goog"] nil) + (deps/dependency-order + (concat + (map + (fn [{:keys [type foreign url file provides requires] :as js-map}] + ;; ignore :seed inputs, only for REPL - David + (if (not= :seed type) + (let [url (or url (io/resource file))] + (merge + (javascript-file foreign url provides requires) + js-map)) + js-map)) + required-js) + (when (-> @env/*compiler* :options :emit-constants) + [(constants-javascript-file opts)]) + required-cljs + inputs))))) + +(comment + (alter-var-root #'env/*compiler* (constantly (env/default-compiler-env))) + ;; only get cljs deps + (find-cljs-dependencies ["goog.string" "cljs.core"]) + ;; get transitive deps + (find-cljs-dependencies ["clojure.string"]) + ;; don't get cljs.core twice + (find-cljs-dependencies ["cljs.core" "clojure.string"]) + ) + +(defn- module-entries + "Return the module entries of `compile-opts` as a set." + [compile-opts] + (->> compile-opts :modules vals + (map :entries) + (remove nil?) + (apply concat) + (set))) + +(defn add-dependency-sources + "Given list of IJavaScript objects, produce a new sequence of IJavaScript objects + of all dependencies of inputs." + ([inputs] + (add-dependency-sources inputs + (when env/*compiler* + (:options @env/*compiler*)))) + ([inputs compile-opts] + (let [inputs (set inputs) + requires (set (mapcat deps/-requires inputs)) + module-entries (module-entries compile-opts)] + (into inputs (find-cljs-dependencies (set/union requires module-entries)))))) + +(defn check-unprovided + [inputs] + (let [requires (set (mapcat deps/-requires inputs)) + provided (set (mapcat deps/-provides inputs)) + unprovided (clojure.set/difference requires provided)] + (when (seq unprovided) + (ana/warning :unprovided @env/*compiler* {:unprovided (sort unprovided)})) + inputs)) + +(defn compile-task [^LinkedBlockingDeque deque input-set compiled opts failed] + (loop [ns-info (.pollFirst deque)] + (when (and ns-info (not @failed)) + (let [{:keys [requires]} ns-info + input-set' @input-set + {:keys [compiler-stats verbose]} opts] + (if (every? #(not (contains? input-set' %)) requires) + (do + (try + (swap! compiled conj + (-compile (or (:source-file ns-info) + (:source-forms ns-info)) + ; - ns-info -> ns -> cljs file relpath -> js relpath + (merge opts + {:output-file (comp/rename-to-js + (util/ns->relpath (:ns ns-info)))}))) + (catch Throwable e + (reset! failed e))) + (when-not @failed + (when-let [ns (:ns ns-info)] + (swap! input-set disj ns)) + (recur (.pollFirst deque)))) + (do + (Thread/sleep 10) + (recur ns-info))))))) + +(defn parallel-compile-sources [inputs compiler-stats opts] + (module-graph/validate-inputs inputs) + (let [deque (LinkedBlockingDeque. inputs) + input-set (atom (into #{} (comp (remove nil?) (map :ns)) inputs)) + cnt (+ 2 (.. Runtime getRuntime availableProcessors)) + latch (CountDownLatch. cnt) + es (Executors/newFixedThreadPool cnt) + compiled (atom []) + failed (atom false)] + (dotimes [_ cnt] + (.execute es + (bound-fn [] + (compile-task deque input-set compiled opts failed) + (.countDown latch)))) + (util/measure compiler-stats "Compile sources" (.await latch)) + (.shutdown es) + (when @failed + (throw @failed)) + @compiled)) + +(defn compile-sources + "Takes dependency ordered list of IJavaScript compatible maps from parse-ns + and compiles them." + ([inputs opts] + (compile-sources inputs (:compiler-stats opts) opts)) + ([inputs compiler-stats opts] + (if (:parallel-build opts) + (parallel-compile-sources inputs compiler-stats opts) + (util/measure compiler-stats + "Compile sources" + (binding [comp/*inputs* (zipmap (map :ns inputs) inputs)] + (doall + (for [ns-info inputs] + ; TODO: compile-file calls parse-ns unnecessarily to get ns-info + ; TODO: we could mark dependent namespaces for recompile here + (-compile (or (:source-file ns-info) + (:source-forms ns-info)) + ; - ns-info -> ns -> cljs file relpath -> js relpath + (merge opts {:output-file (comp/rename-to-js (util/ns->relpath (:ns ns-info)))}))))))))) + +(defn add-goog-base + [inputs] + (cons (javascript-file nil (io/resource "goog/base.js") ["goog"] nil) + inputs)) + +(defn add-js-sources + "Given list of IJavaScript objects, add foreign-deps, constants-table + IJavaScript objects to the list." + [inputs opts] + (let [requires (set (mapcat deps/-requires inputs)) + required-js (js-dependencies opts requires)] + (concat + (map + (fn [{:keys [foreign url file provides requires] :as js-map}] + (let [url (or url (io/resource file))] + (merge + (javascript-file foreign url provides requires) + js-map))) + required-js) + (when (-> @env/*compiler* :options :emit-constants) + [(constants-javascript-file opts)]) + inputs))) + +(defn add-preloads + "Add :preloads to a given set of inputs (IJavaScript). Returns a new + list of inputs where the preloaded namespaces and their deps come immediately after + cljs.core or the constants table depending on the optimization setting. Any + files needing copying or compilation will be compiled and/or copied to the + appropiate location." + [inputs opts] + (if-not (:preloads opts) + inputs + (let [pred (fn [x] + (if (:emit-constants opts) + (not= [(str ana/constants-ns-sym)] (:provides x)) + (not= ["cljs.core"] (:provides x)))) + pre (take-while pred inputs) + post (drop-while pred inputs) + preloads (remove nil? + (map + (fn [preload] + (try + (comp/find-source preload) + (catch Throwable t + (util/debug-prn "WARNING: preload namespace" preload "does not exist")))) + (:preloads opts)))] + (distinct-by :provides + (concat pre [(first post)] + (-> (add-dependency-sources preloads opts) + deps/dependency-order + (compile-sources opts) + (add-js-sources opts) + deps/dependency-order) + (next post)))))) + +(comment + (comp/find-sources-root "samples/hello/src") + (find-dependency-sources (find-sources-root "samples/hello/src")) + (find-sources "samples/hello/src")) + +(defn preamble-from-paths [paths] + (when-let [missing (seq (remove io/resource paths))] + (ana/warning :preamble-missing @env/*compiler* {:missing (sort missing)})) + (let [resources (remove nil? (map io/resource paths))] + (str (string/join "\n" (map slurp resources)) "\n"))) + +(defn make-preamble [{:keys [target preamble hashbang]}] + (str (when (and (= :nodejs target) (not (false? hashbang))) + (str "#!" (or hashbang "/usr/bin/env node") "\n")) + (when preamble (preamble-from-paths preamble)))) + +;; Optimize +;; ======== + +(defmulti javascript-name class) + +(defmethod javascript-name URL [^URL url] + (if url (.getPath url) "cljs/user.js")) + +(defmethod javascript-name String [s] + (if-let [name (first (deps/-provides s))] name "cljs/user.js")) + +(defmethod javascript-name JavaScriptFile [js] + (when-let [url (deps/-url js)] + (javascript-name url))) + +(defn build-provides + "Given a vector of provides, builds required goog.provide statements" + [provides] + (apply str (map #(str "goog.provide('" % "');\n") provides))) + +(defmethod js-source-file JavaScriptFile [_ js] + (if-let [url (deps/-url js)] + (js-source-file (javascript-name url) (io/input-stream url)) + (when-let [source (:source js)] + (js-source-file (javascript-name source) source)))) + +(defn ensure-cljs-base-module + "Ensure that compiler :modules map has :cljs-base module with defined + :output-to. If :output-to not provided will default to :output-dir location + and the name of the file will be \"cljs_base.js.\"" + ([modules] + (ensure-cljs-base-module modules + (when env/*compiler* + (:options @env/*compiler*)))) + ([modules opts] + (update-in modules [:cljs-base :output-to] + (fnil io/file + (io/file + (util/output-directory opts) + "cljs_base.js"))))) + +(comment + (ensure-cljs-base-module + {:cljs-base + {:output-to "out/modules/base.js"} + :core + {:output-to "out/modules/core.js" + :entries '#{cljs.core}} + :landing + {:output-to "out/modules/reader.js" + :entries '#{cljs.reader} + :depends-on #{:core}}}) + ) + +(defn compile-loader + "Special compilation pass for cljs.loader namespace. cljs.loader must be + compiled last after all inputs. This is because all inputs must be known and + they must already be sorted in dependency order." + [inputs {:keys [modules] :as opts}] + (when-let [loader (->> inputs + (filter + (fn [input] + (some '#{"cljs.loader" cljs.loader} + (:provides input)))) + first)] + (let [module-uris (module-graph/modules->module-uris modules inputs opts) + module-infos (module-graph/modules->module-infos modules)] + (env/with-compiler-env + (ana/add-consts @env/*compiler* + {'cljs.core/MODULE_URIS module-uris + 'cljs.core/MODULE_INFOS module-infos}) + (-compile (:source-file loader) + (merge opts + {:cache-key (util/content-sha (pr-str module-uris)) + :output-file (comp/rename-to-js (util/ns->relpath (:ns loader)))}))))) + inputs) + +(defn build-modules + "Given a list of IJavaScript sources in dependency order and compiler options + return a dependency sorted list of module name / description tuples. The + module descriptions will be augmented with a :closure-module entry holding + the Closure JSModule. Each module description will also be augmented with + a :foreign-deps vector containing foreign IJavaScript sources in dependency + order." + [sources opts] + (let [sources (map + (fn [js] + (cond + (instance? JavaScriptFile js) + js + (map? js) + (map->JavaScriptFile js) + (string? js) + (merge + (map->javascript-file {:provides (deps/-provides js)}) + {:source js}) + :else js)) + sources) + used (atom #{}) ;; track used inputs to avoid dupes + modules + (reduce + (fn [ret [name {:keys [entries depends-on] :as module-desc}]] + (assert (or (= name :cljs-base) (not (empty? entries))) + (str "Module " name " does not define any :entries")) + (when (:verbose opts) + (util/debug-prn "Building module" name)) + (let [js-module (JSModule. (clojure.core/name name)) + module-sources + (reduce + (fn [ret entry-sym] + (if-let [entries (module-graph/find-sources-for-module-entry entry-sym sources)] + (let [unused (set/difference entries @used)] + (swap! used into entries) + (into ret unused)) + (throw + (IllegalArgumentException. + (str "Could not find matching namespace for " entry-sym))))) + [] entries) + foreign-deps (atom [])] + ;; add inputs to module + (doseq [ijs module-sources] + (when (:verbose opts) + (util/debug-prn " adding entry" (:provides ijs))) + (if-not (deps/-foreign? ijs) + (.add js-module + ^SourceFile (js-source-file (javascript-name ijs) ijs)) + (swap! foreign-deps conj ijs))) + ;; add module dependencies, will always work + ;; since modules are already in dependency order + (doseq [dep depends-on] + (if-let [parent-module (get-in (into {} ret) [dep :closure-module])] + (do + (when (:verbose opts) + (util/debug-prn " module" name "depends on" dep)) + (.addDependency js-module ^JSModule parent-module)) + (throw (IllegalArgumentException. + (str "Parent module " dep " does not exist"))))) + (conj ret + [name (assoc module-desc + :closure-module js-module + :foreign-deps @foreign-deps)]))) + [] (module-graph/sort-modules + (ensure-cljs-base-module + (module-graph/expand-modules (:modules opts) sources) opts)))] + modules)) + +(comment + (build "samples/hello/src" + {:optimizations :none + :output-dir "out" + :output-to "out/hello.js" + :source-map true}) + + (let [modules + (build-modules + [(map->javascript-file + (ana/parse-ns 'cljs.core (io/file "out/cljs/core.js") nil)) + (map->javascript-file + (ana/parse-ns 'cljs.reader (io/file "out/cljs/reader.js") nil))] + {:optimizations :advanced + :output-dir "out" + :cache-analysis true + :modules {:core + {:output-to "out/modules/core.js" + :entries '#{cljs.core}} + :landing + {:output-to "out/modules/reader.js" + :entries '#{cljs.reader} + :depends-on #{:core}}}})] + modules) + ) + +(defn emit-optimized-source-map + "Given a JSON parsed Google Closure JavaScript to JavaScript source map, + the entire list of original IJavaScript sources output a merged JavaScript + to ClojureScript source map file with the given file name. opts should + supply :preamble-line-count and :foreign-deps-line-count if they are + relevant." + [sm-json sources name opts] + (let [closure-source-map (sm/decode-reverse sm-json)] + (loop [sources (seq sources) + relpaths {} + merged (sorted-map-by + (sm/source-compare + (remove nil? + (map (fn [source] + (if-let [^URL source-url (:source-url source)] + (.getPath source-url) + (if-let [^URL url (:url source)] + (.getPath url)))) + sources))))] + (if sources + (let [source (first sources)] + (recur + (next sources) + (let [{:keys [provides source-url]} source] + (if (and provides source-url) + (assoc relpaths + (.getPath ^URL source-url) + (util/ns->relpath (first provides) (util/ext source-url))) + relpaths)) + (if-let [url (:url source)] + (let [path (.getPath ^URL url)] + (if-let [compiled (get-in @env/*compiler* [::comp/compiled-cljs path])] + (if-let [source-url (:source-url source)] + (assoc merged + (.getPath ^URL source-url) + (sm/merge-source-maps + (:source-map compiled) + (get closure-source-map path))) + merged) + (assoc merged path (get closure-source-map path)))) + merged))) + (spit + (io/file name) + (sm/encode merged + {:preamble-line-count (+ (:preamble-line-count opts 0) + (:foreign-deps-line-count opts 0)) + :lines (+ (:lineCount sm-json) + (:preamble-line-count opts 0) + (:foreign-deps-line-count opts 0) + 2) + :file name + :output-dir (util/output-directory opts) + :source-map (:source-map opts) + :source-map-path (:source-map-path opts) + :source-map-timestamp (:source-map-timestamp opts) + :source-map-pretty-print (:source-map-pretty-print opts) + :relpaths relpaths})))))) + +(defn write-variable-maps [^Result result opts] + (let [var-out (:closure-variable-map-out opts)] + (when-let [var-map (and var-out (.-variableMap result))] + (util/mkdirs var-out) + (io/copy (ByteArrayInputStream. (.toBytes var-map)) + (io/file var-out)))) + (let [prop-out (:closure-property-map-out opts)] + (when-let [prop-map (and prop-out (.-propertyMap result))] + (util/mkdirs prop-out) + (io/copy (ByteArrayInputStream. (.toBytes prop-map)) + (io/file prop-out))))) + +(defn optimize-modules + "Use the Closure Compiler to optimize one or more Closure JSModules. Returns + a dependency sorted list of module name and description tuples." + [opts & sources] + ;; the following pre-condition can't be enabled + ;; lein-cljsbuild adds :output-to? + #_{:pre [(and (contains? opts :modules) + (not (contains? opts :output-to)))]} + (assert (= (count (:modules opts)) + (count (into #{} + (map (comp :output-to second) + (:modules opts))))) + "Each :output-to of :modules must be unique") + (let [closure-compiler (make-closure-compiler) + ^List externs (load-externs opts) + compiler-options (make-options opts) + _ (.initOptions closure-compiler compiler-options) + sources (if (= :whitespace (:optimizations opts)) + (cons "var CLOSURE_NO_DEPS = true;" sources) + sources) + modules (build-modules sources opts) + ^List inputs (map (comp :closure-module second) modules) + _ (doseq [^JSModule input inputs] + (.sortInputsByDeps input closure-compiler)) + _ (when (or ana/*verbose* (:verbose opts)) + (util/debug-prn "Applying optimizations" (:optimizations opts) "to" (count sources) "sources")) + ^Result result (.compileModules closure-compiler externs inputs compiler-options) + ^SourceMap source-map (when (:source-map opts) + (.getSourceMap closure-compiler))] + (assert (or (nil? (:source-map opts)) source-map) + "Could not create source maps for modules") + (if (.success result) + (do + (write-variable-maps result opts) + (vec + (for [[name {:keys [output-to closure-module] :as module}] modules] + [name + (merge + (assoc module + :source + (do + (when source-map (.reset source-map)) + (.toSource closure-compiler ^JSModule closure-module))) + (when source-map + (let [sw (StringWriter.) + source-map-name (str output-to ".map.closure")] + (.appendTo source-map sw source-map-name) + {:source-map-json (.toString sw) + :source-map-name source-map-name})))]))) + (report-failure result)))) + +(defn optimize + "Use the Closure Compiler to optimize one or more JavaScript files." + [opts & sources] + (when (or ana/*verbose* (:verbose opts)) + (util/debug-prn "Applying optimizations" (:optimizations opts) "to" (count sources) "sources")) + (let [closure-compiler (make-closure-compiler) + ^List externs (load-externs opts) + compiler-options (make-options opts) + sources (if (= :whitespace (:optimizations opts)) + (cons "var CLOSURE_NO_DEPS = true;" sources) + sources) + ^List inputs (doall + (map + (fn [source] + (let [source (cond-> source + (and (not (record? source)) (map? source)) + map->javascript-file)] + (js-source-file (javascript-name source) source))) + sources)) + ^Result result (util/measure (:compiler-stats opts) + "Optimizing with Google Closure Compiler" + (.compile closure-compiler externs inputs compiler-options))] + (if (.success result) + ;; compiler.getSourceMap().reset() + (do + (write-variable-maps result opts) + (let [source (.toSource closure-compiler)] + (when-let [name (:source-map opts)] + (let [name' (str name ".closure") + sw (StringWriter.) + sm-json-str (do + (.appendTo (.getSourceMap closure-compiler) sw name') + (.toString sw))] + (when (true? (:closure-source-map opts)) + (spit (io/file name') sm-json-str)) + (emit-optimized-source-map + (json/read-str sm-json-str :key-fn keyword) + sources name + (assoc opts + :preamble-line-count + (+ (- (count (.split #"\r?\n" (make-preamble opts) -1)) 1) + (if (:output-wrapper opts) 1 0)))))) + source)) + (report-failure result)))) + +(comment + ;; optimize JavaScript strings + (optimize {:optimizations :whitespace} "var x = 3 + 2; alert(x);") + ;; => "var x=3+2;alert(x);" + (optimize {:optimizations :simple} "var x = 3 + 2; alert(x);") + ;; => "var x=5;alert(x);" + (optimize {:optimizations :advanced} "var x = 3 + 2; alert(x);") + ;; => "alert(5);" + + ;; optimize a ClojureScript form + (optimize {:optimizations :simple} (-compile '(def x 3) {})) + ) + +;; Output +;; ====== +;; +;; The result of a build is always a single string of JavaScript. The +;; build process may produce files on disk but a single string is +;; always output. What this string contains depends on whether the +;; input has been optimized or not. If the :output-to option is set +;; then this string will be written to the specified file. If not, it +;; will be returned. +;; +;; The :output-dir option can be used to set the working directory +;; where any files will be written to disk. By default this directory +;; is 'out'. +;; +;; If inputs are optimized then the output string will be the complete +;; application with all dependencies included. +;; +;; For unoptimized output, the string will be a Closure deps file +;; describing where the JavaScript files are on disk and their +;; dependencies. All JavaScript files will be located in the working +;; directory, including any dependencies from the Closure library. +;; +;; Unoptimized mode is faster because the Closure Compiler is not +;; run. It also makes debugging much simpler because each file is +;; loaded in its own script tag. +;; +;; When working with uncompiled files, you will need to add additional +;; script tags to the hosting HTML file: one which pulls in Closure +;; library's base.js and one which calls goog.require to load your +;; code. See samples/hello/hello-dev.html for an example. + +(defn ^String path-relative-to + "Generate a string which is the path to the input IJavaScript relative + to the specified base file." + [^File base input] + (let [base-path (util/path-seq (.getCanonicalPath base)) + input-path (util/path-seq (.getCanonicalPath (io/file (deps/-url input)))) + count-base (count base-path) + common (count (take-while true? (map #(= %1 %2) base-path input-path))) + prefix (repeat (- count-base common 1) "..")] + (if (= count-base common) + (last input-path) ;; same file + (util/to-path (concat prefix (drop common input-path)) "/")))) + +(defn add-dep-string + "Return a goog.addDependency string for an input." + [opts input] + (letfn [(ns-list [coll] (when (seq coll) (apply str (interpose ", " (map #(str "'" (comp/munge %) "'") coll)))))] + (str "goog.addDependency(\"" + (path-relative-to + (io/file (util/output-directory opts) "goog" "base.js") input) + "\", [" + (ns-list (deps/-provides input)) + "], [" + ;; even under Node.js where runtime require is possible + ;; this is necessary - see CLJS-2151 + (ns-list (cond->> (deps/-requires input) + ;; under Node.js we emit native `require`s for these + (= :nodejs (:target opts)) + (filter (complement ana/node-module-dep?)))) + "]" + (if (deps/-foreign? input) ", {'foreign-lib': true}") + ");\n"))) + +(defn deps-file + "Return a deps file string for a sequence of inputs." + [opts sources] + (apply str (map #(add-dep-string opts %) sources))) + +(comment + (path-relative-to (io/file "out/goog/base.js") {:url (deps/to-url "out/cljs/core.js")}) + (add-dep-string {} {:url (deps/to-url "out/cljs/core.js") :requires ["goog.string"] :provides ["cljs.core"]}) + (deps-file {} [{:url (deps/to-url "out/cljs/core.js") :requires ["goog.string"] :provides ["cljs.core"]}]) + ) + +(defn output-one-file [{:keys [output-to] :as opts} js] + (cond + (nil? output-to) js + + (or (string? output-to) + (util/file? output-to)) + (let [f (io/file output-to)] + (util/mkdirs f) + (spit f js)) + + :else (println js))) + +(defn output-deps-file [opts sources] + (output-one-file opts (deps-file opts sources))) + +(declare foreign-deps-str add-header add-source-map-link) + +(defn preloads + ([syms] + (preloads syms nil)) + ([syms mode] + (letfn [(preload-str [sym] + (str (when (= :browser mode) "document.write('');\n" "\n")))] + (map preload-str syms)))) + +(defn output-main-file + "Output an entry point. In the non-modules case, opts is simply compiler + options. When emitting a module entry point, opts must contain :module-name." + [opts] + (assert (or (not (contains? opts :module-name)) + (get (:modules opts) (:module-name opts))) + (str "Module " (:module-name opts) " does not exist")) + (let [module (get (:modules opts) (:module-name opts)) + asset-path (or (:asset-path opts) + (util/output-directory opts)) + closure-defines (json/write-str (:closure-defines opts))] + (case (:target opts) + :nashorn + (output-one-file + (merge opts + (when module + {:output-to (:output-to module)})) + (add-header opts + (str (when (or (not module) (= :cljs-base (:module-name opts))) + (str "var CLJS_OUTPUT_DIR = \"" asset-path "\";\n" + "load((new java.io.File(new java.io.File(\"" asset-path "\",\"goog\"), \"base.js\")).getPath());\n" + "load((new java.io.File(new java.io.File(\"" asset-path "\",\"goog\"), \"deps.js\")).getPath());\n" + "load((new java.io.File(new java.io.File(new java.io.File(\"" asset-path "\",\"goog\"),\"bootstrap\"),\"nashorn.js\")).getPath());\n" + "load((new java.io.File(\"" asset-path "\",\"cljs_deps.js\")).getPath());\n" + "goog.global.CLOSURE_UNCOMPILED_DEFINES = " closure-defines ";\n" + (apply str (preloads (:preloads opts))))) + (apply str + (map (fn [entry] + (str "goog.require(\"" (comp/munge entry) "\");\n")) + (if-let [entries (when module (:entries module))] + entries + [(:main opts)])))))) + + :nodejs + (output-one-file + (merge opts + (when module + {:output-to (:output-to module)})) + (add-header opts + (str (when (or (not module) (= :cljs-base (:module-name opts))) + (str "var path = require(\"path\");\n" + "try {\n" + " require(\"source-map-support\").install();\n" + "} catch(err) {\n" + "}\n" + "require(path.join(path.resolve(\".\"),\"" asset-path "\",\"goog\",\"bootstrap\",\"nodejs.js\"));\n" + "require(path.join(path.resolve(\".\"),\"" asset-path "\",\"cljs_deps.js\"));\n" + "goog.global.CLOSURE_UNCOMPILED_DEFINES = " closure-defines ";\n" + (apply str (preloads (:preloads opts))))) + (apply str + (map (fn [entry] + (str "goog.require(\"" (comp/munge entry) "\");\n")) + (if-let [entries (when module (:entries module))] + entries + [(:main opts)]))) + "goog.require(\"cljs.nodejscli\");\n"))) + + :webworker + (output-one-file + (merge opts + (when module + {:output-to (:output-to module)})) + (str (when (or (not module) (= :cljs-base (:module-name opts))) + (str "var CLOSURE_BASE_PATH = \"" asset-path "/goog/\";\n" + "var CLOSURE_UNCOMPILED_DEFINES = " closure-defines ";\n" + "var CLOSURE_IMPORT_SCRIPT = (function(global) { return function(src) {global['importScripts'](src); return true;};})(this);\n" + "if(typeof goog == 'undefined') importScripts(\"" asset-path "/goog/base.js\");\n" + "importScripts(\"" asset-path "/cljs_deps.js\");\n" + (apply str (preloads (:preloads opts))))) + (apply str + (map (fn [entry] + (when-not (= "goog" entry) + (str "goog.require(\"" (comp/munge entry) "\");\n"))) + (if-let [entries (when module (:entries module))] + entries + (when-let [main (:main opts)] + [main])))))) + + (output-one-file + (merge opts + (when module + {:output-to (:output-to module)})) + (str (when (or (not module) (= :cljs-base (:module-name opts))) + (str "var CLOSURE_UNCOMPILED_DEFINES = " closure-defines ";\n" + "var CLOSURE_NO_DEPS = true;\n" + "if(typeof goog == \"undefined\") document.write('');\n" + "document.write('');\n" + "document.write('');\n" + "document.write('');\n" + (apply str (preloads (:preloads opts) :browser)))) + (apply str + (map (fn [entry] + (when-not (= "goog" entry) + (str "document.write('');\n"))) + (if-let [entries (when module (:entries module))] + entries + (when-let [main (:main opts)] + [main]))))))))) + +(defn output-modules + "Given compiler options, original IJavaScript sources and a sequence of + module name and module description tuples output module sources to disk. + Modules description must define :output-to and supply :source entry with + the JavaScript source to write to disk." + [opts js-sources modules] + (doseq [[name {:keys [output-to source foreign-deps] :as module-desc}] modules] + (assert (not (nil? output-to)) + (str "Module " name " does not define :output-to")) + (assert (not (nil? source)) + (str "Module " name " did not supply :source")) + (let [fdeps-str (when-not (empty? foreign-deps) + (foreign-deps-str opts foreign-deps)) + sm-name (when (:source-map opts) + (str output-to ".map")) + out-file (io/file output-to)] + (util/mkdirs out-file) + (spit out-file + (as-> source source + (if (= name :cljs-base) + (add-header opts source) + source) + (if fdeps-str + (str fdeps-str "\n" source) + source) + (if sm-name + (add-source-map-link + (assoc opts + :output-to output-to + :source-map sm-name) + source) + source))) + (when (:source-map opts) + (let [sm-json-str (:source-map-json module-desc) + sm-json (json/read-str sm-json-str :key-fn keyword)] + (when (true? (:closure-source-map opts)) + (spit (io/file (:source-map-name module-desc)) sm-json-str)) + (emit-optimized-source-map sm-json js-sources sm-name + (merge opts + {:source-map sm-name + :preamble-line-count + (if (= name :cljs-base) + (+ (- (count (.split #"\r?\n" (make-preamble opts) -1)) 1) + (if (:output-wrapper opts) 1 0)) + 0) + :foreign-deps-line-count + (if fdeps-str + (- (count (.split #"\r?\n" fdeps-str -1)) 1) + 0)}))))))) + +(defn lib-rel-path [{:keys [lib-path url provides] :as ijs}] + (if (nil? lib-path) + (util/ns->relpath (first provides) "js") + (if (.endsWith lib-path ".js") + (util/get-name url) + (let [path (util/path url) + lib-path (util/normalize-path lib-path)] + (subs path (+ (inc (.lastIndexOf path lib-path)) (.length lib-path))))))) + +(defn ^String rel-output-path + "Given a IJavaScript which points to a .js file either in memory, in a jar file, + or is a foreign lib, return the path relative to the output directory." + ([js] + (rel-output-path js + (when env/*compiler* + (:options @env/*compiler*)))) + ([js opts] + (let [url (deps/-url js opts)] + (cond + url + (cond + (deps/-closure-lib? js) (lib-rel-path js) + (deps/-foreign? js) (or (deps/-relative-path js opts) + (util/relative-name url)) + :else (path-from-jarfile url)) + + (string? js) + (str (util/content-sha js 7) ".js") + + :else (str (random-string 5) ".js"))))) + +(defn get-source-files [js-modules opts] + (map (fn [lib] + (let [file (if-let [file-min (and (#{:advanced :simple} (:optimizations opts)) + (:file-min lib))] + file-min + (:file lib))] + (js-source-file file (deps/-source lib)))) + js-modules)) + +(defn make-convert-js-module-options [opts] + (-> opts + (select-keys + [:closure-warnings :closure-extra-annotations :pretty-print + :language-in :language-out :closure-module-roots :rewrite-polyfills]) + (assoc-in [:closure-warnings :non-standard-jsdoc] :off) + (set-options (CompilerOptions.)))) + +(defn module-type->keyword [^CompilerInput$ModuleType module-type] + (case (.name module-type) + "NONE" :none + "GOOG" :goog + "ES6" :es6 + "COMMONJS" :commonjs + "JSON" :json + "IMPORTED_SCRIPT" :imported-script)) + +(defn add-converted-source + [closure-compiler inputs-by-name opts {:keys [file-min file requires] :as ijs}] + (let [processed-file (if-let [min (and (#{:advanced :simple} (:optimizations opts)) + file-min)] + min + file) + processed-file (string/replace processed-file "\\" "/") + ^CompilerInput input (get inputs-by-name processed-file) + ^Node ast-root (.getAstRoot input closure-compiler) + module-name (ModuleNames/fileToModuleName processed-file) + ;; getJsModuleType returns NONE for ES6 files, but getLoadsFlags module returns es6 for those + module-type (or (some-> (.get (.getLoadFlags input) "module") keyword) + (module-type->keyword (.getJsModuleType input)))] + (assoc ijs + :module-type module-type + :source + ;; Add goog.provide/require calls ourselves, not emited by Closure since + ;; https://github.com/google/closure-compiler/pull/2641 + (str + "goog.provide(\"" module-name "\");\n" + (->> (.getRequires input) + ;; v20180204 returns string + ;; next Closure returns DependencyInfo.Require object + (map (fn [i] + (if (string? i) + i + (.getSymbol i)))) + ;; If CJS/ES6 module uses goog.require, goog is added to requires + ;; but this would cause problems with Cljs. + (remove #{"goog"}) + (map (fn [n] + (str "goog.require(\"" n "\");\n"))) + (apply str)) + (.toSource closure-compiler ast-root))))) + +(defn- package-json-entries + "Takes options and returns a sequence with the desired order of package.json + entries for the given :package-json-resolution mode. If no mode is provided, + defaults to :webpack (if no target is set) and :nodejs (if the target is + :nodejs)." + [opts] + {:pre [(or (= (:package-json-resolution opts) :webpack) + (= (:package-json-resolution opts) :nodejs) + (and (sequential? (:package-json-resolution opts)) + (every? string? (:package-json-resolution opts))) + (not (contains? opts :package-json-resolution)))]} + (let [modes {:nodejs ["main"] + :webpack ["browser" "module" "main"]}] + (if-let [mode (:package-json-resolution opts)] + (if (sequential? mode) mode (get modes mode)) + (case (:target opts) + :nodejs (:nodejs modes) + (:webpack modes))))) + +(comment + (= (package-json-entries {}) ["browser" "module" "main"]) + (= (package-json-entries {:package-json-resolution :nodejs}) ["main"]) + (= (package-json-entries {:package-json-resolution :webpack}) ["browser" "module" "main"]) + (= (package-json-entries {:package-json-resolution ["foo" "bar" "baz"]}) ["foo" "bar" "baz"]) + (= (package-json-entries {:target :nodejs}) ["main"]) + (= (package-json-entries {:target :nodejs :package-json-resolution :nodejs}) ["main"]) + (= (package-json-entries {:target :nodejs :package-json-resolution :webpack}) ["browser" "module" "main"]) + (= (package-json-entries {:target :nodejs :package-json-resolution ["foo" "bar"]}) ["foo" "bar"])) + +(defn convert-js-modules + "Takes a list JavaScript modules as an IJavaScript and rewrites them into a Google + Closure-compatible form. Returns list IJavaScript with the converted module + code set as source." + [js-modules opts] + (let [^List externs '() + ^List source-files (get-source-files js-modules opts) + ^CompilerOptions options (doto (make-convert-js-module-options opts) + (.setProcessCommonJSModules true) + (.setLanguageIn (lang-key->lang-mode :ecmascript6)) + (.setLanguageOut (lang-key->lang-mode (:language-out opts :ecmascript3))) + (.setDependencyOptions (doto (DependencyOptions.) + (.setDependencySorting true))) + (.setPackageJsonEntryNames ^List (package-json-entries opts))) + closure-compiler (doto (make-closure-compiler) + (.init externs source-files options)) + _ (.parse closure-compiler) + _ (report-failure (.getResult closure-compiler)) + inputs-by-name (into {} (map (juxt #(.getName %) identity) (vals (.getInputsById closure-compiler))))] + + ;; This will rewrite CommonJS modules + (.whitespaceOnlyPasses closure-compiler) + ;; This will take care of converting ES6 to CJS + ;; Based on language-in setting, this could also handle ES7/8/TypeScript transpilation. + (.transpileAndDontCheck closure-compiler) + + (map (partial add-converted-source + closure-compiler inputs-by-name opts) + js-modules))) + +(defmulti js-transforms + "Takes an IJavaScript with the source code set as source, transforms the + source code and returns an IJavascript with the new code set as source." + (fn [ijs opts] + (:preprocess ijs))) + +(defmethod js-transforms :default [ijs opts] + (ana/warning :unsupported-preprocess-value @env/*compiler* ijs) + ijs) + +(defn write-javascript + "Write or copy a JavaScript file to output directory. Only write if the file + does not already exist. Return IJavaScript for the file on disk at the new + location." + [opts js] + (let [out-dir (io/file (util/output-directory opts)) + out-name (rel-output-path js opts) + out-file (io/file out-dir out-name) + res (or (:url js) (:source-file js)) + js-module? (and res out-dir + (.startsWith (util/path res) (util/path out-dir))) ;; We already Closure processed it and wrote it out + ijs (merge + {:requires (deps/-requires js) + :provides (deps/-provides js) + :group (:group js)} + (when (not js-module?) + {:url (deps/to-url out-file) + :out-file (.toString out-file)}))] + (when (and (not js-module?) + (or (not (.exists out-file)) + (and res (util/changed? out-file res)))) + (when (and res (or ana/*verbose* (:verbose opts))) + (util/debug-prn "Copying" (str res) "to" (str out-file))) + (util/mkdirs out-file) + (spit out-file (deps/-source js)) + (when res + (.setLastModified ^File out-file (util/last-modified res)))) + (if (map? js) + (merge js ijs) + ijs))) + +(defn write-js? + "Returns true if IJavaScript instance needs to be written/copied to output + directory. True when in memory, in a JAR, or if foreign library." + [js] + (try + (let [url ^URL (deps/-url js)] + (or (not url) + (= (.getProtocol url) "jar") + (deps/-closure-lib? js) + (deps/-foreign? js))) + (catch Throwable t + (throw (Exception. (str "Could not write JavaScript " (pr-str js))))))) + +(defn source-on-disk + "Ensure that the given IJavaScript exists on disk in the output directory. + Return updated IJavaScript with the new location if necessary." + [opts js] + (if (write-js? js) + (write-javascript opts js) + ;; always copy original ClojureScript sources to the output directory + ;; when source maps enabled + (let [source-url (:source-url js) + out-file (when-let [ns (and (:source-map opts) + source-url + (first (:provides js)))] + (io/file (io/file (util/output-directory opts)) + (util/ns->relpath ns (util/ext source-url))))] + (when (and out-file source-url + (or (not (.exists ^File out-file)) + (util/changed? (io/file source-url) out-file))) + (do + (when (or ana/*verbose* (:verbose opts)) + (util/debug-prn "Copying" (str source-url) "to" (str out-file))) + (util/mkdirs out-file) + (spit out-file (slurp source-url)) + (.setLastModified ^File out-file (util/last-modified source-url)))) + js))) + +(comment + (write-javascript {} "goog.provide('demo');\nalert('hello');\n") + ;; write something from a jar file to disk + (source-on-disk {} + {:url (io/resource "goog/base.js") + :source (with-open [reader (io/reader (io/resource "goog/base.js"))] + (slurp reader))}) + ;; doesn't write a file that is already on disk + (source-on-disk {} {:url (io/resource "cljs/core.cljs")}) + ) + +(defn output-unoptimized + "Ensure that all JavaScript source files are on disk (not in jars), + write the goog deps file including only the libraries that are being + used and write the deps file for the current project. + + The deps file for the current project will include third-party + libraries." + [{:keys [modules] :as opts} & sources] + ;; this source-on-disk call is currently necessary for REPLs - David + (let [disk-sources (doall + (remove #(= (:group %) :goog) + (map #(source-on-disk opts %) sources))) + goog-deps (io/file (util/output-directory opts) "goog" "deps.js") + main (:main opts) + output-deps #(output-deps-file + (assoc opts :output-to + (str (util/output-directory opts) + File/separator "cljs_deps.js")) + disk-sources)] + (util/mkdirs goog-deps) + (spit goog-deps (slurp (io/resource "goog/deps.js"))) + (when (:debug-inputs opts) + (util/debug-prn "DEBUG: all compiler inputs") + (util/debug-prn (pr-str sources))) + (cond + modules + (let [modules' (module-graph/expand-modules modules sources)] + (output-deps) + (doall + (map + (fn [[module-name _]] + (output-main-file + (merge opts + {:module-name module-name + :modules modules'}))) + modules))) + + (and main (not= :none (:target opts))) + (do + (output-deps) + (output-main-file opts)) + + :else (output-deps-file opts disk-sources)))) + +(defn get-upstream-deps* + "returns a merged map containing all upstream dependencies defined + by libraries on the classpath." + ([] + (get-upstream-deps* (. (Thread/currentThread) (getContextClassLoader)))) + ([classloader] + (let [upstream-deps (map #(read-string (slurp %)) + (enumeration-seq (. classloader (getResources "deps.cljs"))))] + (apply merge-with + (fn [a b] + (if (map? a) + (merge-with #(into #{%1} #{%2}) a b) + (concat a b))) + upstream-deps)))) + +(def get-upstream-deps (memoize get-upstream-deps*)) + +(defn add-header [opts js] + (str (make-preamble opts) js)) + +(defn foreign-deps-str [opts sources] + (letfn [(to-js-str [ijs] + (if-let [url (or (and (#{:advanced :simple} (:optimizations opts)) + (:url-min ijs)) + (:url ijs))] + (slurp url) + (throw (IllegalArgumentException. + (str "Foreign lib " ijs " does not exist")))))] + (str (string/join "\n" (map to-js-str sources)) "\n"))) + +(defn add-wrapper [{:keys [output-wrapper] :as opts} js] + (if output-wrapper + (cond + (fn? output-wrapper) (output-wrapper js) + (string? output-wrapper) (format output-wrapper js) + :else (str ";(function(){\n" js "\n})();\n")) + js)) + +(defn add-source-map-link [{:keys [source-map output-to] :as opts} js] + (if source-map + (if (= output-to :print) + (str js "\n//# sourceMappingURL=" source-map "\n\n") + (str js "\n//# sourceMappingURL=" (path-relative-to (io/file output-to) {:url source-map}) "\n\n")) + js)) + +(defn absolute-path? [path] + (.isAbsolute (io/file path))) + +(defn absolute-parent [path] + (.getParent (.getAbsoluteFile (io/file path)))) + +(defn in-same-dir? + "Checks that path-1 and path-2 are siblings in the same logical directory." + [path-1 path-2] + (= (absolute-parent path-1) + (absolute-parent path-2))) + +(defn same-or-subdirectory-of? + "Checks that path names a file or directory that is the dir or a subdirectory there of." + [dir path] + (let [dir-path (.getAbsolutePath (io/file dir)) + path-path (.getAbsolutePath (io/file path))] + (.startsWith path-path dir-path))) + +(defn check-output-to [{:keys [output-to] :as opts}] + (when (contains? opts :output-to) + (assert (or (string? output-to) + (= :print output-to)) + (format ":output-to %s must specify a file or be :print" + (pr-str output-to)))) + true) + +(defn check-output-dir [{:keys [output-dir] :as opts}] + (when (contains? opts :output-dir) + (assert (string? output-dir) + (format ":output-dir %s must specify a directory" + (pr-str output-dir)))) + true) + +(defn check-source-map + "When :source-map is specified in opts, " + [{:keys [output-to source-map output-dir optimizations] :as opts}] + (when (and (contains? opts :source-map) + (:source-map opts) + (not (= optimizations :none))) + (assert (and (or (contains? opts :output-to) + (contains? opts :modules)) + (contains? opts :output-dir)) + (str ":source-map cannot be specified without also specifying :output-dir " + "and either :output-to or :modules if optimization setting applied")) + (assert (or (nil? (:output-to opts)) (:modules opts) (string? source-map)) + (format (str ":source-map %s must specify a file in the same directory " + "as :output-to %s if optimization setting applied") + (pr-str source-map) + (pr-str output-to))) + (assert (or (nil? (:output-to opts)) (:modules opts) (in-same-dir? source-map output-to)) + (format (str ":source-map %s must specify a file in the same directory as " + ":output-to %s if optimization setting applied") + (pr-str source-map) + (pr-str output-to))) + (assert (or (nil? (:output-to opts)) (:modules opts) (same-or-subdirectory-of? (absolute-parent output-to) output-dir)) + (format (str ":output-dir %s must specify a directory in :output-to's " + "parent %s if optimization setting applied") + (pr-str output-dir) + (pr-str (absolute-parent output-to))))) + (when (and (contains? opts :source-map) + (= optimizations :none)) + (assert (util/boolean? source-map) + (format ":source-map must be true or false when compiling with :optimizations :none but it is: %s" + (pr-str source-map)))) + true) + +(defn check-source-map-path [{:keys [source-map-path] :as opts}] + (when (contains? opts :source-map-path) + (assert (string? source-map-path) + (format ":source-map-path %s must be a directory" + source-map-path)) + (when-not (= (:optimizations opts) :none) + (assert (and (contains? opts :output-to) + (contains? opts :source-map)) + (str ":source-map-path cannot be specified without also specifying " + ":output-to and :source-map if optimization setting applied")))) + true) + +(defn check-output-wrapper [{:keys [output-wrapper optimizations]}] + (assert (not (and output-wrapper (= :whitespace optimizations))) + ":output-wrapper cannot be combined with :optimizations :whitespace")) + +(defn check-node-target [{:keys [target optimizations] :as opts}] + (assert (not (and (= target :nodejs) (= optimizations :whitespace))) + (format ":nodejs target not compatible with :whitespace optimizations")) + (assert (not (and (= target :nodejs) (= optimizations :none) (not (contains? opts :main)))) + (format ":nodejs target with :none optimizations requires a :main entry"))) + +(defn check-preloads [{:keys [preloads optimizations] :as opts}] + (when (and (some? preloads) + (not= preloads '[process.env]) + (not= optimizations :none)) + (binding [*out* *err*] + (println "WARNING: :preloads should only be specified with :none optimizations")))) + +(defn check-cache-analysis-format [{:keys [cache-analysis cache-analysis-format] :as opts}] + (assert (not (and cache-analysis + ((complement #{:edn :transit}) cache-analysis-format) + (not (nil? cache-analysis-format)))) + (format ":cache-analysis format must be :edn or :transit but it is: %s" + (pr-str cache-analysis-format)))) + +(defn check-npm-deps [{:keys [npm-deps]}] + (let [{ups-npm-deps :npm-deps} (get-upstream-deps) + conflicts (filter (fn [[dep v]] + (and (coll? v) (not (contains? npm-deps dep)))) + ups-npm-deps)] + (binding [*out* *err*] + (doseq [[dep versions] conflicts] + (println (str "WARNING: NPM dependency " (name dep) + " conflicts between versions " + (util/conjunction-str versions) + ". Specify a version in :npm-deps or the latest will be installed.")))))) + +(defn foreign-source? [js] + (and (satisfies? deps/IJavaScript js) + (deps/-foreign? js))) + +(defn expand-libs + "EXPERIMENTAL. Given a set of libs expand any entries which only name + directories into a sequence of lib entries for all JS files recursively + found in that directory. All other options will be shared with the original + entry. The computed :provides assumes the specified directory is on the + classpath." + [libs] + (letfn [(prep-path [p root] + (subs (string/replace (subs p 0 (- (count p) 3)) root "") 1)) + (path->provides [p] + (let [p' (string/replace p File/separator ".")] + (cond-> [p'] + (string/includes? p' "_") + (conj (string/replace p' "_" "-"))))) + (expand-lib* [{:keys [file] :as lib}] + (if-not file + [lib] ;; foreign-lib override case - David + (let [root (.getAbsolutePath (io/file file)) + dir (io/file file)] + (if (.isDirectory dir) + (into [] + (comp + (filter #(.endsWith (.getName ^File %) ".js")) + (filter #(not (.isHidden ^File %))) + (map + (fn [^File f] + (let [p (.getPath f) + ap (.getAbsolutePath f)] + (merge lib + {:file p :provides (path->provides (prep-path ap root))}))))) + (file-seq dir)) + [lib]))))] + (into [] (mapcat expand-lib* libs)))) + +(declare index-node-modules) + +(defn compute-upstream-npm-deps + ([] + (compute-upstream-npm-deps + (when env/*compiler* + (:options @env/*compiler*)))) + ([{:keys [npm-deps]}] + (let [{ups-npm-deps :npm-deps} (get-upstream-deps)] + (reduce + (fn [m [dep v]] + (cond-> m + (not (contains? npm-deps dep)) + (assoc dep (if (coll? v) + (last (sort v)) + v)))) + {} ups-npm-deps)))) + +(defn ensure-module-opts [opts] + (update opts :modules + #(ensure-cljs-base-module % opts))) + +(defn shim-process? + [{:keys [target process-shim] :as opts}] + (if (= :nodejs target) + (true? process-shim) + (not (false? process-shim)))) + +(defn normalize-closure-defines [defines] + (into {} + (map (fn [[k v]] + [(if (symbol? k) (str (comp/munge k)) k) v]) + defines))) + +(defn add-implicit-options + [{:keys [optimizations output-dir] + :or {optimizations :none + output-dir "out"} + :as opts}] + (let [opts (cond-> opts + (shim-process? opts) + (-> (update-in [:preloads] (fnil conj []) 'process.env) + (cond-> + (not= :none optimizations) + (update-in [:closure-defines 'process.env/NODE_ENV] (fnil str "production")))) + + (or (:closure-defines opts) (shim-process? opts)) + (update :closure-defines normalize-closure-defines) + + (:browser-repl opts) + (update-in [:preloads] (fnil conj []) 'clojure.browser.repl.preload) + + (and (contains? opts :modules) + (not (contains? opts :stable-names))) + (assoc :stable-names true)) + {:keys [libs foreign-libs externs]} (get-upstream-deps) + emit-constants (or (and (= optimizations :advanced) + (not (false? (:optimize-constants opts)))) + (:optimize-constants opts))] + (cond-> + (-> opts + (assoc + :optimizations optimizations + :output-dir output-dir + :ups-libs libs + :ups-foreign-libs (expand-libs foreign-libs) + :ups-externs externs + :emit-constants emit-constants + :cache-analysis-format (:cache-analysis-format opts :transit)) + (update-in [:preamble] #(into (or % []) ["cljs/imul.js"]))) + + (:target opts) + (assoc-in [:closure-defines (str (comp/munge 'cljs.core/*target*))] + (name (:target opts))) + + (= optimizations :none) + (assoc + :cache-analysis (:cache-analysis opts true) + :source-map (:source-map opts true)) + + (:aot-cache opts) + (assoc :cache-analysis true) + + (= optimizations :advanced) + (cond-> + (not (false? (:static-fns opts))) (assoc :static-fns true) + (not (false? (:optimize-constants opts))) (assoc :optimize-constants true)) + + (nil? (find (:closure-warnings opts) :check-types)) + (assoc-in [:closure-warnings :check-types] :off) + + (nil? (find (:closure-warnings opts) :check-variables)) + (assoc-in [:closure-warnings :check-variables] :off) + + (nil? (:closure-module-roots opts)) + (assoc :closure-module-roots []) + + (nil? (:opts-cache opts)) + (assoc :opts-cache "cljsc_opts.edn") + + (not (contains? opts :aot-cache)) + (assoc :aot-cache false) + + (contains? opts :modules) + (ensure-module-opts) + + (:stable-names opts) + (as-> opts + (let [out-dir (if (true? (:stable-names opts)) + output-dir + (:stable-names opts))] + (merge + {:closure-variable-map-in (io/file out-dir "closure_var.map") + :closure-variable-map-out (io/file out-dir "closure_var.map") + :closure-property-map-in (io/file out-dir "closure_prop.map") + :closure-property-map-out (io/file out-dir "closure_prop.map")} + opts))) + + (nil? (:ignore-js-module-exts opts)) + (assoc :ignore-js-module-exts [".css"])))) + +(defn- alive? [proc] + (try (.exitValue proc) false (catch IllegalThreadStateException _ true))) + +(defn- pipe [^Process proc in ^Writer out] + ;; we really do want system-default encoding here + (with-open [^java.io.Reader in (-> in InputStreamReader. BufferedReader.)] + (loop [buf (char-array 1024)] + (when (alive? proc) + (try + (let [len (.read in buf)] + (when-not (neg? len) + (.write out buf 0 len) + (.flush out))) + (catch IOException e + (when (and (alive? proc) (not (.contains (.getMessage e) "Stream closed"))) + (.printStackTrace e *err*)))) + (recur buf))))) + +(defn maybe-install-node-deps! + [{:keys [npm-deps verbose] :as opts}] + (let [npm-deps (merge npm-deps (compute-upstream-npm-deps opts))] + (when-not (empty? npm-deps) + (let [pkg-json (io/file "package.json")] + (when (or ana/*verbose* verbose) + (util/debug-prn "Installing Node.js dependencies")) + (when-not (.exists pkg-json) + (spit pkg-json "{}")) + (let [proc (-> (ProcessBuilder. + (into (cond->> ["npm" "install" "@cljs-oss/module-deps"] + util/windows? (into ["cmd" "/c"])) + (map (fn [[dep version]] (str (name dep) "@" version))) + npm-deps)) + .start) + is (.getInputStream proc) + iw (StringWriter. (* 16 1024 1024)) + es (.getErrorStream proc) + ew (StringWriter. (* 1024 1024)) + _ (do (.start + (Thread. + (bound-fn [] (pipe proc is iw)))) + (.start + (Thread. + (bound-fn [] (pipe proc es ew))))) + err (.waitFor proc)] + (when (and (not (zero? err)) (not (.isAlive proc))) + (println (str ew))))) + true))) + +(defn node-module-deps + "EXPERIMENTAL: return the foreign libs entries as computed by running + the module-deps package on the supplied JavaScript entry point. Assumes + that the `@cljs-oss/module-deps` NPM package is either locally or globally + installed." + ([entry] + (node-module-deps entry + (when env/*compiler* + (:options @env/*compiler*)))) + ([{:keys [file]} {:keys [target] :as opts}] + (let [main-entries (str "[" (->> (package-json-entries opts) + (map #(str "\"" % "\"")) + (string/join ",")) "]") + code (-> (slurp (io/resource "cljs/module_deps.js")) + (string/replace "JS_FILE" (string/replace file "\\" "\\\\")) + (string/replace "CLJS_TARGET" (str "" (when target (name target)))) + (string/replace "MAIN_ENTRIES" main-entries)) + proc (-> (ProcessBuilder. ["node" "--eval" code]) + .start) + is (.getInputStream proc) + iw (StringWriter. (* 16 1024 1024)) + es (.getErrorStream proc) + ew (StringWriter. (* 1024 1024)) + _ (do (.start + (Thread. + (bound-fn [] (pipe proc is iw)))) + (.start + (Thread. + (bound-fn [] (pipe proc es ew))))) + err (.waitFor proc)] + (if (zero? err) + (into [] + (map (fn [{:strs [file provides]}] file + (merge + {:file file + ;; Just tag everything es6 here, add-converted-source will + ;; ask the real type, CJS/ES6, from Closure. + :module-type :es6} + (when provides + {:provides provides})))) + (next (json/read-str (str iw)))) + (do + (when-not (.isAlive proc) + (binding [*out* *err*] + (println (str ew)))) + []))))) + +(defn node-inputs + "EXPERIMENTAL: return the foreign libs entries as computed by running + the module-deps package on the supplied JavaScript entry points. Assumes + that the `@cljs-oss/module-deps` NPM package is either locally or globally + installed." + ([entries] + (node-inputs entries + (when env/*compiler* + (:options @env/*compiler*)))) + ([entries opts] + (into [] (distinct (mapcat #(node-module-deps % opts) entries))))) + +(defn index-node-modules + ([modules] + (index-node-modules + modules + (when env/*compiler* + (:options @env/*compiler*)))) + ([modules opts] + (let [node-modules (io/file "node_modules")] + (if (and (not (empty? modules)) (.exists node-modules) (.isDirectory node-modules)) + (let [modules (into #{} (map name) modules) + deps-file (io/file (util/output-directory opts) "cljs$node_modules.js") + old-contents (when (.exists deps-file) + (slurp deps-file)) + new-contents (let [sb (StringBuffer.)] + (run! #(.append sb (str "require('" % "');\n")) modules) + (str sb))] + (util/mkdirs deps-file) + (if (or (not= old-contents new-contents) + (nil? env/*compiler*) + (nil? (::transitive-dep-set @env/*compiler*))) + (do + (spit deps-file new-contents) + (let [transitive-js (node-inputs [{:file (.getAbsolutePath deps-file)}] opts)] + (when-not (nil? env/*compiler*) + (swap! env/*compiler* update-in [::transitive-dep-set] + assoc modules transitive-js)) + transitive-js)) + (when-not (nil? env/*compiler*) + (get-in @env/*compiler* [::transitive-dep-set modules])))) + [])))) + +(defn- node-file-seq->libs-spec* + [module-fseq] + (letfn [(package-json? [path] + (boolean (re-find #"node_modules[/\\](@[^/\\]+?[/\\])?[^/\\]+?[/\\]package\.json$" path)))] + (let [pkg-jsons (into {} + (comp + (map #(.getAbsolutePath %)) + (filter package-json?) + (map (fn [path] + [path (json/read-str (slurp path))]))) + module-fseq)] + (into [] + (comp + (map #(.getAbsolutePath %)) + (map (fn [path] + (merge + {:file path + :module-type :es6} + (when-not (package-json? path) + (let [pkg-json-main (some + (fn [[pkg-json-path {:strs [main name]}]] + (when-not (nil? main) + ;; should be the only edge case in + ;; the package.json main field - Antonio + (let [main (cond-> main + (.startsWith main "./") + (subs 2)) + main-path (-> pkg-json-path + (string/replace #"\\" "/") + (string/replace #"package\.json$" "") + (str main))] + (some (fn [candidate] + (when (= candidate (string/replace path #"\\" "/")) + name)) + (cond-> [main-path] + (nil? (re-find #"\.js(on)?$" main-path)) + (into [(str main-path ".js") (str main-path "/index.js") (str main-path ".json")])))))) + pkg-jsons)] + {:provides (let [module-rel-name (-> (subs path (.lastIndexOf path "node_modules")) + (string/replace #"\\" "/") + (string/replace #"node_modules[\\\/]" "")) + provides (cond-> [module-rel-name (string/replace module-rel-name #"\.js(on)?$" "")] + (some? pkg-json-main) + (conj pkg-json-main)) + index-replaced (string/replace module-rel-name #"[\\\/]index\.js(on)?$" "")] + (cond-> provides + (and (boolean (re-find #"[\\\/]index\.js(on)?$" module-rel-name)) + (not (some #{index-replaced} provides))) + (conj index-replaced)))})))))) + module-fseq)))) + +(def node-file-seq->libs-spec (memoize node-file-seq->libs-spec*)) + +(defn index-node-modules-dir + ([] + (index-node-modules-dir + (when env/*compiler* + (:options @env/*compiler*)))) + ([opts] + (let [module-fseq (util/module-file-seq)] + (node-file-seq->libs-spec module-fseq)))) + +(defn preprocess-js + "Given js-module map, apply preprocessing defined by :preprocess value in the map." + [{:keys [preprocess] :as js-module} opts] + (cond + (keyword? preprocess) + (js-transforms js-module opts) + + (symbol? preprocess) + (let [preprocess-var (sym->var preprocess :preprocess {:file (:file js-module)})] + (try + (preprocess-var js-module opts) + (catch Throwable t + (throw (ex-info (str "Error running preprocessing function " preprocess) + {:file (:file js-module) + :preprocess preprocess} + t))))) + + :else + (do + (ana/warning :unsupported-preprocess-value @env/*compiler* js-module) + js-module))) + +(defn- to-absolute-path [^String file-str] + (.getAbsolutePath (io/file file-str))) + +(defn process-js-modules + "Given the current compiler options, converts JavaScript modules to Google + Closure modules and writes them to disk. Adds mapping from original module + namespace to new module namespace to compiler env. Returns modified compiler + options where new modules are passed with :libs option." + [opts] + (let [;; Modules from both :foreign-libs (compiler options) and :ups-foreign-libs (deps.cljs) + ;; are processed together, so that files from both sources can depend on each other. + ;; e.g. commonjs module in :foreign-libs can depend on commonjs module from :ups-foreign-libs. + js-modules (filter :module-type (concat (:foreign-libs opts) (:ups-foreign-libs opts)))] + (if (seq js-modules) + (util/measure (:compiler-stats opts) + "Process JS modules" + (let [_ (when-let [unsupported (first (filter (complement #{:es6 :commonjs}) + (map :module-type js-modules)))] + (ana/warning :unsupported-js-module-type @env/*compiler* unsupported)) + ;; Load all modules - add :source so preprocessing and conversion can access it + js-modules (into [] + (comp + (map (fn [lib] + (let [js (deps/load-foreign-library lib) + url (str (deps/-url js opts))] + (if (and url (some (fn [ext] + (.endsWith url ext)) + (:ignore-js-module-exts opts))) + (do + (when (or ana/*verbose* (:verbose opts)) + (util/debug-prn "Ignoring JS module" url "based on the file extension")) + (assoc js :source "")) + (assoc js :source (deps/-source js opts)))))) + (map (fn [js] + (if (:preprocess js) + (preprocess-js js opts) + js))) + (map (fn [js] + (cond-> (update-in js [:file] to-absolute-path) + (some? (:file-min js)) + (update-in [:file-min] to-absolute-path))))) + js-modules) + js-modules (convert-js-modules js-modules opts)] + ;; Write modules to disk, update compiler state and build new options + (reduce (fn [new-opts {:keys [file module-type] :as ijs}] + (let [ijs (write-javascript opts ijs) + module-name (-> (deps/load-library (:out-file ijs)) first :provides first)] + (swap! env/*compiler* + #(assoc-in % [:js-namespaces module-name] {:module-type module-type})) + (doseq [provide (:provides ijs)] + (swap! env/*compiler* + #(update-in % [:js-module-index] assoc provide {:name module-name + :module-type module-type}))) + (-> new-opts + (update-in [:libs] (comp vec conj) (:out-file ijs)) + ;; js-module might be defined in either, so update both + (update-in [:foreign-libs] + (fn [libs] + (into [] + (remove #(= (to-absolute-path (:file %)) file)) + libs))) + (update-in [:ups-foreign-libs] + (fn [libs] + (into [] + (remove #(= (to-absolute-path (:file %)) (to-absolute-path file))) + libs)))))) + opts js-modules))) + opts))) + +(defn- load-data-reader-file [mappings ^java.net.URL url] + (with-open [rdr (readers/input-stream-push-back-reader (.openStream url))] + (binding [*file* (.getFile url)] + (let [new-mappings (reader/read {:eof nil :read-cond :allow} rdr)] + (when (not (map? new-mappings)) + (throw (ex-info (str "Not a valid data-reader map") + {:url url}))) + (reduce + (fn [m [k v]] + (when (not (symbol? k)) + (throw (ex-info (str "Invalid form in data-reader file") + {:url url + :form k}))) + (when (and (contains? mappings k) + (not= (mappings k) v)) + (throw (ex-info "Conflicting data-reader mapping" + {:url url + :conflict k + :mappings m}))) + (assoc m k v)) + mappings + new-mappings))))) + +(defn get-data-readers* + "returns a merged map containing all data readers defined by libraries + on the classpath." + ([] + (get-data-readers* (. (Thread/currentThread) (getContextClassLoader)))) + ([classloader] + (let [data-reader-urls (enumeration-seq (. classloader (getResources "data_readers.cljc")))] + (reduce load-data-reader-file {} data-reader-urls)))) + +(def get-data-readers (memoize get-data-readers*)) + +(defn load-data-readers! [compiler] + (let [data-readers (get-data-readers) + nses (map (comp symbol namespace) (vals data-readers))] + (swap! compiler update-in [:cljs.analyzer/data-readers] merge (get-data-readers)) + (doseq [ns nses] + (try + (locking ana/load-mutex + (require ns)) + (catch Throwable _))))) + +(defn add-externs-sources [opts] + (cond-> opts + (:infer-externs opts) + (assoc :externs-sources (load-externs (dissoc opts :infer-externs))))) + +(defn handle-js-modules + "Given all Cljs sources (build inputs and dependencies in classpath) + + - index all the node node modules + - process the JS modules (preprocess + convert to Closure JS) + - save js-dependency-index for compilation" + [{:keys [npm-deps target] :as opts} js-sources compiler-env] + ;; Find all the top-level Node packages and their files + (let [top-level (reduce + (fn [acc m] + (reduce (fn [acc p] (assoc acc p m)) acc (:provides m))) + {} + ;; if :npm-deps option is false, node_modules/ dir shouldn't be indexed + (if (not (false? npm-deps)) + (index-node-modules-dir))) + requires (set (mapcat deps/-requires js-sources)) + ;; Select Node files that are required by Cljs code, + ;; and create list of all their dependencies + node-required (set/intersection (set (keys top-level)) requires) + expanded-libs (expand-libs (:foreign-libs opts)) + output-dir (util/output-directory opts) + opts (update opts :foreign-libs + (fn [libs] + (into (if (= target :nodejs) + [] + (index-node-modules node-required)) + (into expanded-libs + (node-inputs (filter (fn [{:keys [module-type]}] + (some? module-type)) + expanded-libs)))))) + ;; If compiler-env doesn't contain JS module info we need to process + ;; modules even if files haven't changed since last compile. + opts (if (or (nil? (:js-namespaces @compiler-env)) + (nil? (:js-module-index @compiler-env)) + (some + (fn [ijs] + (let [dest (io/file output-dir (rel-output-path (assoc ijs :foreign true) opts))] + (util/changed? (deps/-url ijs opts) dest))) + (:foreign-libs opts))) + (process-js-modules opts) + (:options @compiler-env))] + (swap! compiler-env + (fn [cenv] + (-> cenv + ;; we need to also track the whole top level - this is to support + ;; cljs.analyze/analyze-deps, particularly in REPL contexts - David + (merge {:js-dependency-index (deps/js-dependency-index opts)}) + (update-in [:options] merge opts) + (update-in [:node-module-index] (fnil into #{}) + (if (= target :nodejs) + (map str node-required) + (map str (keys top-level))))))) + opts)) + +(defn output-bootstrap [{:keys [target] :as opts}] + (when (and (#{:nodejs :nashorn} target) + (not= (:optimizations opts) :whitespace)) + (let [target-str (name target) + outfile (io/file (util/output-directory opts) + "goog" "bootstrap" (str target-str ".js"))] + (util/mkdirs outfile) + (spit outfile (slurp (io/resource (str "cljs/bootstrap_" target-str ".js"))))))) + +(defn compile-inputs + "Compile inputs and all of their transitive dependencies including JS modules, + libs, and foreign libs. Duplicates the pipeline of build." + [inputs opts] + (env/ensure + (let [sources (-> inputs + (#(map add-core-macros-if-cljs-js %)) + (add-dependency-sources opts)) + opts (handle-js-modules opts sources env/*compiler*) + sources (-> sources + deps/dependency-order + (compile-sources false opts) + (add-js-sources opts) deps/dependency-order + (->> (map #(source-on-disk opts %)) doall))] + sources))) + +(defn compile-ns + "Compiles a namespace and all of its transitive dependencies. + See compile-inputs." + [ns opts] + (compile-inputs (find-sources ns opts) opts)) + +(defn build + "Given compiler options, produce runnable JavaScript. An optional source + parameter may be provided." + ([opts] + (build nil opts)) + ([source opts] + (build source opts + (if-not (nil? env/*compiler*) + env/*compiler* + (env/default-compiler-env + ;; need to dissoc :foreign-libs since we won't know what overriding + ;; foreign libspecs are referring to until after add-implicit-options + ;; - David + (add-externs-sources (dissoc opts :foreign-libs)))))) + ([source opts compiler-env] + (env/with-compiler-env compiler-env + (let [orig-opts opts + opts (add-implicit-options opts) + ;; we want to warn about NPM dep conflicts before installing the modules + _ (when (:install-deps opts) + (check-npm-deps opts) + (swap! compiler-env update-in [:npm-deps-installed?] + (fn [installed?] + (if-not installed? + (maybe-install-node-deps! opts) + installed?)))) + + compiler-stats (:compiler-stats opts) + checked-arrays (or (:checked-arrays opts) + ana/*checked-arrays*) + static-fns? (or (and (= (:optimizations opts) :advanced) + (not (false? (:static-fns opts)))) + (:static-fns opts) + ana/*cljs-static-fns*) + sources (when source + (-find-sources source opts))] + (check-output-to opts) + (check-output-dir opts) + (check-source-map opts) + (check-source-map-path opts) + (check-output-wrapper opts) + (check-node-target opts) + (check-preloads opts) + (check-cache-analysis-format opts) + (swap! compiler-env + #(-> % + (update-in [:options] merge opts) + (assoc :target (:target opts)) + ;; Save the current js-dependency index once we have computed opts + ;; or the analyzer won't be able to find upstream dependencies - Antonio + (assoc :js-dependency-index (deps/js-dependency-index opts)) + ;; Save list of sources for cljs.analyzer/locate-src - Juho Teperi + (assoc :sources sources))) + (binding [comp/*recompiled* (when-not (false? (:recompile-dependents opts)) + (atom #{})) + ana/*checked-arrays* checked-arrays + ana/parse-ns (memoize ana/parse-ns) + ana/*cljs-static-fns* static-fns? + ana/*fn-invoke-direct* (or (and static-fns? + (:fn-invoke-direct opts)) + ana/*fn-invoke-direct*) + *assert* (not= (:elide-asserts opts) true) + ana/*load-tests* (not= (:load-tests opts) false) + ana/*cljs-warnings* + (let [warnings (opts :warnings true)] + (merge + ana/*cljs-warnings* + (if (or (true? warnings) + (false? warnings)) + (zipmap + [:unprovided :undeclared-var + :undeclared-ns :undeclared-ns-form] + (repeat warnings)) + warnings))) + ana/*verbose* (:verbose opts)] + (when ana/*verbose* + (util/debug-prn "Options passed to ClojureScript compiler:" (pr-str opts))) + (let [one-file? (and (:main opts) + (#{:advanced :simple :whitespace} (:optimizations opts))) + source (if one-file? + (let [main (:main opts) + uri (:uri (cljs-source-for-namespace main))] + (assert uri (str "No file for namespace " main " exists")) + uri) + source) + compile-opts (if one-file? + (assoc opts :output-file (:output-to opts)) + opts) + _ (load-data-readers! compiler-env) + ;; reset :js-module-index so that ana/parse-ns called by -find-sources + ;; can find the missing JS modules + js-sources (env/with-compiler-env (dissoc @compiler-env :js-module-index) + (-> (if source + (-find-sources source opts) + (-find-sources (reduce into #{} (map (comp :entries val) (:modules opts))) opts)) + (add-dependency-sources compile-opts))) + opts (handle-js-modules opts js-sources compiler-env) + js-sources (-> js-sources + deps/dependency-order + (compile-sources compiler-stats compile-opts) + (#(map add-core-macros-if-cljs-js %)) + (add-js-sources opts) + (cond-> (= :nodejs (:target opts)) (concat [(-compile (io/resource "cljs/nodejs.cljs") opts)])) + deps/dependency-order + (add-preloads opts) + add-goog-base + (cond-> (= :nodejs (:target opts)) (concat [(-compile (io/resource "cljs/nodejscli.cljs") opts)])) + (->> (map #(source-on-disk opts %)) doall) + (compile-loader opts)) + _ (when (:emit-constants opts) + (comp/emit-constants-table-to-file + (::ana/constant-table @env/*compiler*) + (constants-filename opts))) + _ (when (:infer-externs opts) + (comp/emit-inferred-externs-to-file + (reduce util/map-merge {} + (map (comp :externs second) + (get @compiler-env ::ana/namespaces))) + (str (util/output-directory opts) "/inferred_externs.js"))) + _ (spit (io/file (util/output-directory opts) (:opts-cache opts)) (pr-str orig-opts)) + optim (:optimizations opts) + ret (if (and optim (not= optim :none)) + (do + (when-let [fname (:source-map opts)] + (assert (or (nil? (:output-to opts)) (:modules opts) (string? fname)) + (str ":source-map must name a file when using :whitespace, " + ":simple, or :advanced optimizations with :output-to"))) + (if (:modules opts) + (->> + (util/measure compiler-stats + (str "Optimizing " (count js-sources) " sources") + (apply optimize-modules opts js-sources)) + (output-modules opts js-sources)) + (let [fdeps-str (foreign-deps-str opts + (filter foreign-source? js-sources)) + opts (assoc opts + :foreign-deps-line-count + (- (count (.split #"\r?\n" fdeps-str -1)) 1))] + (->> + (util/measure compiler-stats + (str "Optimizing " (count js-sources) " sources") + (apply optimize opts + (remove foreign-source? js-sources))) + (add-wrapper opts) + (add-source-map-link opts) + (str fdeps-str) + (add-header opts) + (output-one-file opts))))) + (apply output-unoptimized opts js-sources))] + (output-bootstrap opts) + ret)))))) + +(comment + ;; testing modules + (build "samples/hello/src" + {:optimizations :advanced + :output-dir "samples/hello/out" + :source-map true + :modules + {:hello + {:output-to "samples/hello/out/hello.js" + :entries '#{cljs.reader hello.core}}}}) + + (require '[cljs.externs :as externs]) + + (externs/parse-externs + (js-source-file "cljs/externs.js" (io/file "src/main/cljs/cljs/externs.js"))) + ) + +(defn ^File target-file-for-cljs-ns + [ns-sym output-dir] + (util/to-target-file + (util/output-directory {:output-dir output-dir}) + {:ns ns-sym})) + +(defn mark-cljs-ns-for-recompile! + [ns-sym output-dir] + (let [s (target-file-for-cljs-ns ns-sym output-dir)] + (when (.exists s) + (.setLastModified s 5000)))) + +(defn cljs-dependents-for-macro-namespaces + [state namespaces] + (map :name + (let [namespaces-set (set namespaces)] + (filter (fn [x] (not-empty + (set/intersection namespaces-set (-> x :require-macros vals set)))) + (vals (:cljs.analyzer/namespaces @state)))))) + +(defn watch + "Given a source directory, produce runnable JavaScript. Watch the source + directory for changes rebuilding when necessary. Takes the same arguments as + cljs.closure/build in addition to some watch-specific options: + - :watch-fn, a function of no arguments to run after a successful build. May + be a function value or a namespaced symbol identifying a function, + in which case the associated namespace willl be loaded and the + symbol resolved. + - :watch-error-fn, a function receiving the exception of a failed build. May + be a function value or a namespaced symbol, loaded as + with :watch-fn." + ([source opts] + (watch source opts + (if-not (nil? env/*compiler*) + env/*compiler* + (env/default-compiler-env opts)))) + ([source opts compiler-env] + (watch source opts compiler-env nil)) + ([source opts compiler-env quit] + (let [opts (cond-> opts + (= (:verbose opts :not-found) :not-found) + (assoc :verbose true)) + paths (map #(Paths/get (.toURI %)) (-paths source)) + path (first paths) + fs (.getFileSystem path) + srvc (.newWatchService fs)] + (letfn [(buildf [] + (try + (let [start (System/nanoTime)] + (build source opts compiler-env) + (println "... done. Elapsed" + (/ (unchecked-subtract (System/nanoTime) start) 1e9) "seconds") + (flush)) + (when-let [f (opts-fn :watch-fn opts)] + (f)) + (catch Throwable e + (if-let [f (opts-fn :watch-error-fn opts)] + (f e) + (binding [*out* *err*] + (println (Throwables/getStackTraceAsString e))))))) + (watch-all [^Path root] + (Files/walkFileTree root + (reify + FileVisitor + (preVisitDirectory [_ dir _] + (let [^Path dir dir] + (. dir + (register srvc + (into-array [StandardWatchEventKinds/ENTRY_CREATE + StandardWatchEventKinds/ENTRY_DELETE + StandardWatchEventKinds/ENTRY_MODIFY]) + (into-array [SensitivityWatchEventModifier/HIGH])))) + FileVisitResult/CONTINUE) + (postVisitDirectory [_ dir exc] + FileVisitResult/CONTINUE) + (visitFile [_ file attrs] + FileVisitResult/CONTINUE) + (visitFileFailed [_ file exc] + FileVisitResult/CONTINUE))))] + (println "Building ...") + (flush) + (buildf) + (println "Watching paths:" (apply str (interpose ", " paths))) + (doseq [path paths] + (watch-all path)) + (loop [key nil] + (when (and (or (nil? quit) (not @quit)) + (or (nil? key) (. ^WatchKey key reset))) + (let [key (. srvc (poll 300 TimeUnit/MILLISECONDS)) + poll-events-seq (when key (seq (.pollEvents key)))] + (when (and key + (some + (fn [^WatchEvent e] + (let [fstr (.. e context toString)] + (and (or (. fstr (endsWith "cljc")) + (. fstr (endsWith "cljs")) + (. fstr (endsWith "clj")) + (. fstr (endsWith "js"))) + (not (. fstr (startsWith ".#")))))) + poll-events-seq)) + (when-let [clj-files (seq (keep (fn [^WatchEvent e] + (let [ctx (.context e) + fstr (.toString ctx)] + (when (and (or (. fstr (endsWith "cljc")) + (. fstr (endsWith "clj"))) + (not (. fstr (startsWith ".#")))) + ctx))) + poll-events-seq))] + (let [^Path dir (.watchable key) + file-seq (map #(.toFile (.resolve dir %)) clj-files) + nses (map (comp :ns ana/parse-ns) file-seq)] + (doseq [ns nses] + (require ns :reload)) + (doseq [ns (cljs-dependents-for-macro-namespaces compiler-env nses)] + (mark-cljs-ns-for-recompile! ns (:output-dir opts))))) + (println "Change detected, recompiling ...") + (flush) + (buildf)) + (recur key)))))))) + +(comment + (watch "samples/hello/src" + {:optimizations :none + :output-to "samples/hello/out/hello.js" + :output-dir "samples/hello/out" + :cache-analysis true + :source-map true + :verbose true + :watch-fn + (fn [] + (println "Success!"))}) + ) + +;; ============================================================================= +;; Utilities + +;; for backwards compatibility +(defn output-directory [opts] + (util/output-directory opts)) + +(defn parse-js-ns [f] + (deps/parse-js-ns (line-seq (io/reader f)))) + +(defn ^File src-file->target-file + ([src] + (src-file->target-file src + (when env/*compiler* + (:options @env/*compiler*)))) + ([src opts] + (util/to-target-file + (when (:output-dir opts) + (util/output-directory opts)) + (ana/parse-ns src)))) + +(defn ^String src-file->goog-require + ([src] (src-file->goog-require src {:wrap true})) + ([src {:keys [wrap all-provides macros-ns] :as options}] + (let [goog-ns + (case (util/ext src) + ("cljs" "cljc") (let [ns-str (str (comp/munge (:ns (ana/parse-ns src))))] + (cond-> ns-str + (and macros-ns (not (.endsWith ns-str "$macros"))) + (str "$macros"))) + "js" (cond-> (:provides (parse-js-ns src)) + (not all-provides) first) + (throw + (IllegalArgumentException. + (str "Can't create goog.require expression for " src))))] + (if (and (not all-provides) wrap) + (if (:reload options) + (str "goog.require(\"" goog-ns "\", true);") + (str "goog.require(\"" goog-ns "\");")) + (if (vector? goog-ns) + goog-ns + (str goog-ns)))))) + +;; Browser REPL client stuff + +(defn compile-client-js [opts] + (let [copts (select-keys opts [:optimizations :output-dir])] + ;; we're inside the REPL process where cljs.env/*compiler* is already + ;; established, need to construct a new one to avoid mutating the one + ;; the REPL uses + (build + '[(ns clojure.browser.repl.client + (:require [goog.events :as event] + [clojure.browser.repl :as repl])) + (defn start [url] + (event/listen js/window + "load" + (fn [] + (repl/start-evaluator url))))] + copts (env/default-compiler-env copts)))) + +(defn create-client-js-file [opts file-path] + (if-let [cached (io/resource "brepl_client.js")] + cached + (let [file (io/file file-path)] + (when (not (.exists file)) + (spit file (compile-client-js opts))) + file))) + +;; AOTed resources + +(defn aot-cache-core [] + (let [base-path (io/file "src" "main" "cljs" "cljs") + src (io/file base-path "core.cljs") + dest (io/file base-path "core.aot.js") + cache (io/file base-path "core.cljs.cache.aot.edn") + tcache (io/file base-path "core.cljs.cache.aot.json")] + (util/mkdirs dest) + (env/with-compiler-env (env/default-compiler-env {:infer-externs true}) + (comp/compile-file src dest + {:source-map true + :source-map-url "core.js.map" + :output-dir (str "src" File/separator "main" File/separator "cljs")}) + (ana/write-analysis-cache 'cljs.core cache src) + (ana/write-analysis-cache 'cljs.core tcache src)) + (create-client-js-file + {:optimizations :simple + :output-dir "aot_out"} + (io/file "resources" "brepl_client.js")) + (doseq [f (file-seq (io/file "aot_out")) + :when (.isFile f)] + (.delete f)))) + +(comment + (time + (do (aot-cache-core) nil)) + + (time + (do (ana/analyze-file "cljs/core.cljs") nil)) + + (println (build '[(ns hello.core) + (defn ^{:export greet} greet [n] (str "Hola " n)) + (defn ^:export sum [xs] 42)] + {:optimizations :simple :pretty-print true})) + + ;; build a project with optimizations + (build "samples/hello/src" {:optimizations :advanced}) + (build "samples/hello/src" {:optimizations :advanced :output-to "samples/hello/hello.js"}) + ;; open 'samples/hello/hello.html' to see the result in action + + ;; build a project without optimizations + (build "samples/hello/src" {:output-dir "samples/hello/out" :output-to "samples/hello/hello.js"}) + ;; open 'samples/hello/hello-dev.html' to see the result in action + ;; notice how each script was loaded individually + + ;; build unoptimized from raw ClojureScript + (build '[(ns hello.core) + (defn ^{:export greet} greet [n] (str "Hola " n)) + (defn ^:export sum [xs] 42)] + {:output-dir "samples/hello/out" :output-to "samples/hello/hello.js"}) + ;; open 'samples/hello/hello-dev.html' to see the result in action + )