-
Notifications
You must be signed in to change notification settings - Fork 786
/
closure.clj
940 lines (812 loc) · 35.5 KB
/
closure.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
; 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
"Compile ClojureScript to JavaScript with optimizations from Google
Closure Compiler producing runnable JavaScript.
The Closure Compiler (compiler.jar) must be on the classpath.
Use the 'build' function for end-to-end compilation.
build = compile -> add-dependencies -> optimize -> output
Two protocols are defined: IJavaScript and Compilable. The
Compilable protocol is satisfied by something which can return one
or more IJavaScripts.
With IJavaScript objects in hand, calling add-dependencies will
produce a sequence of IJavaScript objects which includes all
required dependencies from the Closure library and ClojureScript,
in dependency order. This function replaces the closurebuilder
tool.
The optimize function converts one or more IJavaScripts into a
single string of JavaScript source code using the Closure Compiler
API.
The produced output is either a single string of optimized
JavaScript or a deps file for use during development.
"
(:require [cljs.compiler :as comp]
[cljs.analyzer :as ana]
[clojure.java.io :as io]
[clojure.string :as string])
(:import java.io.File
java.io.BufferedInputStream
java.net.URL
java.util.logging.Level
java.util.jar.JarFile
com.google.common.collect.ImmutableList
com.google.javascript.jscomp.CompilerOptions
com.google.javascript.jscomp.CompilationLevel
com.google.javascript.jscomp.SourceMap$Format
com.google.javascript.jscomp.SourceMap$DetailLevel
com.google.javascript.jscomp.ClosureCodingConvention
com.google.javascript.jscomp.SourceFile
com.google.javascript.jscomp.Result
com.google.javascript.jscomp.JSError
com.google.javascript.jscomp.CommandLineRunner))
(def name-chars (map char (concat (range 48 57) (range 65 90) (range 97 122))))
(defn random-char []
(nth name-chars (.nextInt (java.util.Random.) (count name-chars))))
(defn random-string [length]
(apply str (take length (repeatedly random-char))))
;; 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))
(defn set-options
"TODO: Add any other options that we would like to support."
[opts ^CompilerOptions compiler-options]
(when (contains? opts :pretty-print)
(set! (.prettyPrint compiler-options) (:pretty-print opts)))
(when (contains? opts :print-input-delimiter)
(set! (.printInputDelimiter compiler-options)
(:print-input-delimiter opts))))
(defn 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.)))]
(when (contains? opts :source-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 jar-entry-names* [jar-path]
(with-open [z (java.util.zip.ZipFile. jar-path)]
(doall (map #(.getName %) (enumeration-seq (.entries z))))))
(def jar-entry-names (memoize jar-entry-names*))
(defn find-js-jar
"finds js resources from a given path in a jar file"
[jar-path lib-path]
(doall
(map #(io/resource %)
(filter #(do
(and
(.startsWith % lib-path)
(.endsWith % ".js")))
(jar-entry-names jar-path)))))
(declare to-url)
(defn find-js-fs
"finds js resources from a path on the files system"
[path]
(let [file (io/file path)]
(when (.exists file)
(map to-url (filter #(.endsWith (.getName %) ".js") (file-seq (io/file path)))))))
(defn find-js-classpath
"finds all js files on the classpath matching the path provided"
[path]
(let [process-entry #(if (.endsWith % ".jar")
(find-js-jar % path)
(find-js-fs (str % "/" path)))
cpath-list (let [sysp (System/getProperty "java.class.path" )]
(if (.contains sysp ";")
(string/split sysp #";")
(string/split sysp #":")))]
(doall (reduce #(let [p (process-entry %2)]
(if p (concat %1 p) %1)) [] cpath-list))))
(defn find-js-resources [path]
"finds js resources in a given path on either the file system or
the classpath"
(let [file (io/file path)]
(if (.exists file)
(find-js-fs path)
(find-js-classpath path))))
(defn load-externs
"Externs are JavaScript files which contain empty definitions of
functions which will be provided by the envorinment. 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]}]
(let [filter-cp-js (fn [paths]
(for [p paths u (find-js-classpath p)] u))
filter-js (fn [paths]
(for [p paths u (find-js-resources p)] u))
add-target (fn [ext]
(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 (concat js-sources ups-sources)]
(if use-only-custom-externs
all-sources
(into all-sources (CommandLineRunner/getDefaultExterns))))))
(defn ^com.google.javascript.jscomp.Compiler make-closure-compiler []
(let [compiler (com.google.javascript.jscomp.Compiler.)]
(do (com.google.javascript.jscomp.Compiler/setLoggingLevel Level/WARNING)
compiler)))
(defn report-failure [^Result result]
(let [errors (.errors result)
warnings (.warnings result)]
(doseq [next (seq errors)]
(println "ERROR:" (.toString ^JSError next)))
(doseq [next (seq warnings)]
(println "WARNING:" (.toString ^JSError next)))))
(defn parse-js-ns
"Given the lines from a JavaScript source file, parse the provide
and require statements and return them in a map. Assumes that all
provide and require statements appear before the first function
definition."
[lines]
(letfn [(conj-in [m k v] (update-in m [k] (fn [old] (conj old v))))]
(->> (for [line lines x (string/split line #";")] x)
(map string/trim)
(take-while #(not (re-matches #".*=[\s]*function\(.*\)[\s]*[{].*" %)))
(map #(re-matches #".*goog\.(provide|require)\(['\"](.*)['\"]\)" %))
(remove nil?)
(map #(drop 1 %))
(reduce (fn [m ns]
(if (= (first ns) "require")
(conj-in m :requires (last ns))
(conj-in m :provides (last ns))))
{:requires [] :provides []}))))
;; Protocols for IJavaScript and Compilable
;; ========================================
(defmulti to-url class)
(defmethod to-url File [^File f] (.toURL (.toURI f)))
(defmethod to-url String [s] (to-url (io/file s)))
(defprotocol IJavaScript
(-foreign? [this] "Whether the Javascript represents a foreign
library (a js file that not have any goog.provide statement")
(-url [this] "The URL where this JavaScript is located. Returns nil
when JavaScript exists in memory only.")
(-provides [this] "A list of namespaces that this JavaScript provides.")
(-requires [this] "A list of namespaces that this JavaScript requires.")
(-source [this] "The JavaScript source string."))
(extend-protocol IJavaScript
String
(-foreign? [this] false)
(-url [this] nil)
(-provides [this] (:provides (parse-js-ns (string/split-lines this))))
(-requires [this] (:requires (parse-js-ns (string/split-lines this))))
(-source [this] this)
clojure.lang.IPersistentMap
(-foreign? [this] (:foreign this))
(-url [this] (or (:url this)
(to-url (:file this))))
(-provides [this] (map name (:provides this)))
(-requires [this] (map name (:requires this)))
(-source [this] (if-let [s (:source this)]
s
(slurp (io/reader (-url this))))))
(defrecord JavaScriptFile [foreign ^URL url provides requires]
IJavaScript
(-foreign? [this] foreign)
(-url [this] url)
(-provides [this] provides)
(-requires [this] requires)
(-source [this] (slurp (io/reader url))))
(defn javascript-file [foreign ^URL url provides requires]
(JavaScriptFile. foreign url (map name provides) (map name requires)))
(defn map->javascript-file [m]
(javascript-file (:foreign m)
(to-url (:file m))
(:provides m)
(:requires m)))
(defn read-js
"Read a JavaScript file returning a map of file information."
[f]
(let [source (slurp f)
m (parse-js-ns (string/split-lines source))]
(map->javascript-file (assoc m :file f))))
(defprotocol Compilable
(-compile [this opts] "Returns one or more IJavaScripts."))
(defn build-index
"Index a list of dependencies by namespace and file name. There can
be zero or more namespaces provided per file."
[deps]
(reduce (fn [m next]
(let [provides (:provides next)]
(-> (if (seq provides)
(reduce (fn [m* provide]
(assoc m* provide next))
m
provides)
m)
(assoc (:file next) next))))
{}
deps))
(defn dependency-order-visit
[state ns-name]
(let [file (get state ns-name)]
(if (or (:visited file) (nil? file))
state
(let [state (assoc-in state [ns-name :visited] true)
deps (:requires file)
state (reduce dependency-order-visit state deps)]
(assoc state :order (conj (:order state) file))))))
(defn- pack-string [s]
(if (string? s)
{:provides (-provides s)
:requires (-requires s)
:file (str "from_source_" (gensym) ".clj")
::original s}
s))
(defn- unpack-string [m]
(or (::original m) m))
(defn dependency-order
"Topologically sort a collection of dependencies."
[coll]
(let [state (build-index (map pack-string coll))]
(map unpack-string
(distinct
(:order (reduce dependency-order-visit (assoc state :order []) (keys state)))))))
;; Compile
;; =======
(defn compile-form-seq
"Compile a sequence of forms to a JavaScript source string."
[forms]
(comp/with-core-cljs
(with-out-str
(binding [ana/*cljs-ns* 'cljs.user]
(doseq [form forms]
(comp/emit (ana/analyze (ana/empty-env) form)))))))
(defn output-directory [opts]
(or (:output-dir opts) "out"))
(def compiled-cljs (atom {}))
(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 (.getAbsolutePath (:file m))
js (if (:provides m)
(map->javascript-file m)
(if-let [js (get @compiled-cljs path)]
js
(read-js (:file m))))]
(do (swap! compiled-cljs (fn [old] (assoc old path js)))
js)))
(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 (io/file (output-directory opts) output-file)]
(compiled-file (comp/compile-file file out-file)))
(compile-form-seq (comp/forms-seq file))))
(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 (output-directory opts)]
(map compiled-file
(comp/compile-root src-dir out-dir))))
(defn 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]
(let [out-file (io/file out-dir (path-from-jarfile url))
content (slurp (io/reader url))]
(do (comp/mkdirs out-file)
(spit out-file content)
out-file)))
(defn compile-from-jar
"Compile a file from a jar."
[this {:keys [output-file] :as opts}]
(or (when output-file
(let [out-file (io/file (output-directory opts) output-file)]
(when (.exists out-file)
(compiled-file {:file out-file}))))
(let [file-on-disk (jar-file-to-disk this (output-directory opts))]
(-compile file-on-disk opts))))
(extend-protocol Compilable
File
(-compile [this opts]
(if (.isDirectory this)
(compile-dir this opts)
(compile-file this opts)))
URL
(-compile [this opts]
(case (.getProtocol this)
"file" (-compile (io/file this) opts)
"jar" (compile-from-jar this opts)))
clojure.lang.PersistentList
(-compile [this opts]
(compile-form-seq [this]))
String
(-compile [this opts] (-compile (io/file this) opts))
clojure.lang.PersistentVector
(-compile [this opts] (compile-form-seq this))
)
(comment
;; compile a file in memory
(-compile "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"})
;; compile a project
(-compile (io/file "samples/hello/src") {})
;; compile a project with a custom output directory
(-compile (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))]
{})
)
;; Dependencies
;; ============
;;
;; Find all dependencies from files on the classpath. Eliminates the
;; need for closurebuilder. cljs dependencies will be compiled as
;; needed.
(defn find-url
"Given a string, returns a URL. Attempts to resolve as a classpath-relative
path, then as a path relative to the working directory or a URL string"
[path-or-url]
(or (io/resource path-or-url)
(try (io/as-url path-or-url)
(catch java.net.MalformedURLException e
false))
(io/as-url (io/as-file path-or-url))))
(defn load-foreign-library*
"Given a library spec (a map containing the keys :file
and :provides), returns a map containing :provides, :requires, :file
and :url"
([lib-spec] (load-foreign-library* lib-spec false))
([lib-spec cp-only?]
(let [find-func (if cp-only? io/resource find-url)]
(merge lib-spec {:foreign true
:url (find-func (:file lib-spec))}))))
(def load-foreign-library (memoize load-foreign-library*))
(defn load-library*
"Given a path to a JavaScript library, which is a directory
containing Javascript files, return a list of maps
containing :provides, :requires, :file and :url."
([path] (load-library* path false))
([path cp-only?]
(let [find-func (if cp-only? find-js-classpath find-js-resources)
graph-node (fn [u]
(-> (io/reader u)
line-seq
parse-js-ns
(assoc :url u)))]
(let [js-sources (find-js-resources path)]
(filter #(seq (:provides %)) (map graph-node js-sources))))))
(def load-library (memoize load-library*))
(defn library-dependencies [{libs :libs foreign-libs :foreign-libs
ups-libs :ups-libs ups-flibs :ups-foreign-libs}]
(concat
(mapcat #(load-library % true) ups-libs) ;upstream deps
(mapcat load-library libs)
(mapcat #(load-foreign-library % true) ups-flibs) ;upstream deps
(map load-foreign-library foreign-libs)))
(comment
;; load one library
(load-library* "closure/library/third_party/closure")
;; load all library dependencies
(library-dependencies {:libs ["closure/library/third_party/closure"]})
(library-dependencies {:foreign-libs [{:file "http://example.com/remote.js"
:provides ["my.example"]}]})
(library-dependencies {:foreign-libs [{:file "local/file.js"
:provides ["my.example"]}]})
(library-dependencies {:foreign-libs [{:file "cljs/nodejs_externs.js"
:provides ["my.example"]}]}))
(defn goog-dependencies*
"Create an index of Google dependencies by namespace and file name."
[]
(letfn [(parse-list [s] (when (> (count s) 0)
(-> (.substring s 1 (dec (count s)))
(string/split #"'\s*,\s*'"))))]
(->> (line-seq (io/reader (io/resource "goog/deps.js")))
(map #(re-matches #"^goog\.addDependency\(['\"](.*)['\"],\s*\[(.*)\],\s*\[(.*)\]\);.*" %))
(remove nil?)
(map #(drop 1 %))
(remove #(.startsWith (first %) "../../third_party"))
(map #(hash-map :file (str "goog/"(first %))
:provides (parse-list (second %))
:requires (parse-list (last %))
:group :goog)))))
(def goog-dependencies (memoize goog-dependencies*))
(defn js-dependency-index
"Returns the index for all JavaScript dependencies. Lookup by
namespace or file name."
[opts]
(build-index (concat (goog-dependencies) (library-dependencies opts))))
(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]
(let [index (js-dependency-index opts)]
(loop [requires requires
visited (set requires)
deps #{}]
(if (seq requires)
(let [node (get index (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 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)]
(-compile uri (merge opts {:output-file js-file}))))
(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
depedencies 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]
(let [index (js-dependency-index opts)]
(letfn [(ns->cp [s] (str (string/replace (munge s) \. \/) ".cljs"))
(cljs-deps [coll]
(->> coll
(remove #(contains? index %))
(map #(let [f (ns->cp %)] (hash-map :relative-path f :uri (io/resource f))))
(remove #(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 (-requires js)))]
(recur (into (rest required-files) new-req)
(into visited new-req)
(conj js-deps js)))
(remove nil? js-deps))))))
(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 add-dependencies
"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 [requires (mapcat -requires inputs)
required-cljs (remove (set inputs) (cljs-dependencies opts requires))
required-js (js-dependencies opts (set (concat (mapcat -requires required-cljs) requires)))]
(cons (javascript-file nil (io/resource "goog/base.js") ["goog"] nil)
(dependency-order
(concat (map #(-> (javascript-file (:foreign %)
(or (:url %) (io/resource (:file %)))
(:provides %)
(:requires %))
(assoc :group (:group %))) required-js)
required-cljs
inputs)))))
(comment
;; add dependencies to literal js
(add-dependencies {} "goog.provide('test.app');\ngoog.require('cljs.core');")
(add-dependencies {} "goog.provide('test.app');\ngoog.require('goog.array');")
(add-dependencies {} (str "goog.provide('test.app');\n"
"goog.require('goog.array');\n"
"goog.require('clojure.set');"))
;; add dependencies with external lib
(add-dependencies {:libs ["closure/library/third_party/closure"]}
(str "goog.provide('test.app');\n"
"goog.require('goog.array');\n"
"goog.require('goog.dom.query');"))
;; add dependencies with foreign lib
(add-dependencies {:foreign-libs [{:file "samples/hello/src/hello/core.cljs"
:provides ["example.lib"]}]}
(str "goog.provide('test.app');\n"
"goog.require('example.lib');\n"))
;; add dependencies to a JavaScriptFile record
(add-dependencies {} (javascript-file false
(to-url "samples/hello/src/hello/core.cljs")
["hello.core"]
["goog.array"]))
)
;; 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 (-provides s))] name "cljs/user.js"))
(defmethod javascript-name JavaScriptFile [js] (javascript-name (-url js)))
(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]
(when-let [url (-url js)]
(js-source-file (javascript-name url)
(if (-foreign? js)
(str (build-provides (-provides js)) (slurp url))
(io/input-stream url)))))
(defn optimize
"Use the Closure Compiler to optimize one or more JavaScript files."
[opts & sources]
(let [closure-compiler (make-closure-compiler)
externs (load-externs opts)
compiler-options (make-options opts)
sources (if (= :whitespace (:optimizations opts))
(cons "var CLOSURE_NO_DEPS = true;" sources)
sources)
inputs (map #(js-source-file (javascript-name %) %) sources)
result ^Result (.compile closure-compiler externs inputs compiler-options)]
(if (.success result)
;; compiler.getSourceMap().reset()
(let [source (.toSource closure-compiler)]
(when-let [name (:source-map opts)]
(let [out (io/writer name)]
(.appendTo (.getSourceMap closure-compiler) out name)
(.close out)))
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) {}))
;; optimize a project
(println (->> (-compile "samples/hello/src" {})
(apply add-dependencies {})
(apply optimize {:optimizations :simple :pretty-print true})))
)
;; 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 path-relative-to
"Generate a string which is the path to input relative to base."
[^File base input]
(let [base-path (comp/path-seq (.getCanonicalPath base))
input-path (comp/path-seq (.getCanonicalPath (io/file ^URL (-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
(comp/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 "'" (munge %) "'") coll)))))]
(str "goog.addDependency(\""
(path-relative-to (io/file (output-directory opts) "goog/base.js") input)
"\", ["
(ns-list (-provides input))
"], ["
(ns-list (-requires input))
"]);")))
(defn deps-file
"Return a deps file string for a sequence of inputs."
[opts sources]
(apply str (interpose "\n" (map #(add-dep-string opts %) sources))))
(comment
(path-relative-to (io/file "out/goog/base.js") {:url (to-url "out/cljs/core.js")})
(add-dep-string {} {:url (to-url "out/cljs/core.js") :requires ["goog.string"] :provides ["cljs.core"]})
(deps-file {} [{:url (to-url "out/cljs/core.js") :requires ["goog.string"] :provides ["cljs.core"]}])
)
(defn output-one-file [{:keys [output-to]} js]
(cond (nil? output-to) js
(string? output-to) (spit output-to js)
:else (println js)))
(defn output-deps-file [opts sources]
(output-one-file opts (deps-file opts sources)))
(defn ^String output-path
"Given an IJavaScript which is either in memory or in a jar file,
return the output path for this file relative to the working
directory."
[js]
(if-let [url ^URL (-url js)]
(path-from-jarfile url)
(str (random-string 5) ".js")))
(defn write-javascript
"Write a JavaScript file to disk. Only write if the file does not
already exist. Return IJavaScript for the file on disk."
[opts js]
(let [out-dir (io/file (output-directory opts))
out-name (output-path js)
out-file (io/file out-dir out-name)]
(do (when-not (.exists out-file)
(do (comp/mkdirs out-file)
(spit out-file (-source js))))
{:url (to-url out-file) :requires (-requires js) :provides (-provides js) :group (:group js)})))
(defn source-on-disk
"Ensure that the given JavaScript exists on disk. Write in memory
sources and files contained in jars to the working directory. Return
updated IJavaScript with the new location."
[opts js]
(let [url ^URL (-url js)]
(if (or (not url) (= (.getProtocol url) "jar"))
(write-javascript opts js)
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 (slurp (io/reader (io/resource "goog/base.js")))})
;; 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."
[opts & sources]
(let [disk-sources (map #(source-on-disk opts %) sources)]
(let [goog-deps (io/file (output-directory opts) "goog/deps.js")]
(do (comp/mkdirs goog-deps)
(spit goog-deps (deps-file opts (filter #(= (:group %) :goog) disk-sources)))
(output-deps-file opts (remove #(= (:group %) :goog) disk-sources))))))
(comment
;; output unoptimized alone
(output-unoptimized {} "goog.provide('test');\ngoog.require('cljs.core');\nalert('hello');\n")
;; output unoptimized with all dependencies
(apply output-unoptimized {}
(add-dependencies {}
"goog.provide('test');\ngoog.require('cljs.core');\nalert('hello');\n"))
;; output unoptimized with external library
(apply output-unoptimized {}
(add-dependencies {:libs ["closure/library/third_party/closure"]}
"goog.provide('test');\ngoog.require('cljs.core');\ngoog.require('goog.dom.query');\n"))
;; output unoptimized and write deps file to 'out/test.js'
(output-unoptimized {:output-to "out/test.js"}
"goog.provide('test');\ngoog.require('cljs.core');\nalert('hello');\n")
)
(defn get-upstream-deps*
"returns a merged map containing all upstream dependencies defined by libraries on the classpath"
[]
(let [classloader (. (Thread/currentThread) (getContextClassLoader))
upstream-deps (map #(read-string (slurp %)) (enumeration-seq (. classloader (findResources "deps.cljs"))))]
(doseq [dep upstream-deps]
(println (str "Upstream deps.cljs found on classpath. " dep " This is an EXPERIMENTAL FEATURE and is not guarenteed to remain stable in future versions.")))
(apply merge-with concat upstream-deps)))
(def get-upstream-deps (memoize get-upstream-deps*))
(defn add-header [{:keys [hashbang target]} js]
(if (= :nodejs target)
(str "#!" (or hashbang "/usr/bin/env node") "\n" js)
js))
(defn add-wrapper [{:keys [output-wrapper] :as opts} js]
(if output-wrapper
(str ";(function(){\n" js "\n})();\n")
js))
(defn build
"Given a source which can be compiled, produce runnable JavaScript."
[source opts]
(ana/reset-namespaces!)
(let [opts (if (= :nodejs (:target opts))
(merge {:optimizations :simple} opts)
opts)
ups-deps (get-upstream-deps)
all-opts (assoc opts
:ups-libs (:libs ups-deps)
:ups-foreign-libs (:foreign-libs ups-deps)
:ups-externs (:externs ups-deps))]
(binding [ana/*cljs-static-fns*
(or (and (= (opts :optimizations) :advanced))
(:static-fns opts)
ana/*cljs-static-fns*)
ana/*cljs-warn-on-undeclared*
(true? (opts :warnings))]
(let [compiled (-compile source all-opts)
js-sources (concat
(apply add-dependencies all-opts
(concat (if (coll? compiled) compiled [compiled])
(when (= :nodejs (:target all-opts))
[(-compile (io/resource "cljs/nodejs.cljs") all-opts)])))
(when (= :nodejs (:target all-opts))
[(-compile (io/resource "cljs/nodejscli.cljs") all-opts)]))
optim (:optimizations all-opts)]
(if (and optim (not= optim :none))
(->> js-sources
(apply optimize all-opts)
(add-header all-opts)
(add-wrapper all-opts)
(output-one-file all-opts))
(apply output-unoptimized all-opts js-sources))))))
(comment
(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
)