Skip to content
This repository
Browse code

Added support for testing (#36).

* This required that crossovers be moved out of :builds and into the
  top-level :cljsbuild.

* Improved all of the hooks, and added a "lein test" hook.

* Added a PhantomJS test example to the advanced project.

* Ensure that all :compiler :output-dir options are unique (#32).
  • Loading branch information...
commit b8dbbb6d9a575105d6ee67efd8a0f8e565eaacfd 1 parent 92c7dfd
Evan Mezeske authored
4 .gitignore
@@ -7,7 +7,7 @@ cljsbuild-*.*.*
7 7 example-projects/*/*.jar
8 8 example-projects/simple/resources
9 9 example-projects/advanced/resources/public/js
10   -example-projects/*/.clojurescript-output
  10 +example-projects/advanced/resources/private/js
  11 +example-projects/advanced/crossover-cljs
11 12 example-projects/*/classes
12 13 example-projects/*/lib
13   -example-projects/*/src-cljs/example/crossover
2  doc/CROSSOVERS.md
Source Rendered
... ... @@ -1,5 +1,7 @@
1 1 # Sharing Code Between Clojure and ClojureScript
2 2
  3 +**TODO** Fix docs W.R.T. the :crossovers option moving around.
  4 +
3 5 Sharing code with lein-cljsbuild is accomplished via the configuration
4 6 of "crossovers". A crossover specifies a Clojure namespace, the content
5 7 of which should be copied into your ClojureScript project. This can be any
10 example-projects/advanced/phantom/page-repl.js → example-projects/advanced/phantom/repl.js
@@ -6,11 +6,19 @@ if (phantom.args.length != 1) {
6 6 var page = new WebPage();
7 7 var url = phantom.args[0];
8 8
  9 +page.onConsoleMessage = function (message) {
  10 + console.log("App console: " + message);
  11 +};
  12 +
  13 +console.log("Loading URL: " + url);
  14 +
9 15 page.open(url, function (status) {
10 16 if (status != "success") {
11 17 console.log('Failed to open ' + url);
12 18 phantom.exit(1);
13 19 }
14 20
15   - // TODO: Should anything else happen here?
  21 + console.log("Loaded successfully.");
  22 +
  23 + // TODO: Somehow gracefully exit when the REPL is closed?
16 24 });
39 example-projects/advanced/phantom/unit-test.js
... ... @@ -0,0 +1,39 @@
  1 +if (phantom.args.length != 1) {
  2 + console.log('Expected a target URL parameter.');
  3 + phantom.exit(1);
  4 +}
  5 +
  6 +var page = new WebPage();
  7 +var url = phantom.args[0];
  8 +
  9 +page.onConsoleMessage = function (message) {
  10 + console.log("Test console: " + message);
  11 +};
  12 +
  13 +console.log("Loading URL: " + url);
  14 +
  15 +page.open(url, function (status) {
  16 + if (status != "success") {
  17 + console.log('Failed to open ' + url);
  18 + phantom.exit(1);
  19 + }
  20 +
  21 + console.log("Running test.");
  22 +
  23 + var result = page.evaluate(function() {
  24 + return example.test.run();
  25 + });
  26 +
  27 + // NOTE: PhantomJS 1.4.0 has a bug that prevents the exit codes
  28 + // below from eing returned properly. :(
  29 + //
  30 + // http://code.google.com/p/phantomjs/issues/detail?id=294
  31 +
  32 + if (result != 0) {
  33 + console.log("*** Test failed! ***");
  34 + phantom.exit(1);
  35 + }
  36 +
  37 + console.log("Test succeeded.");
  38 + phantom.exit(0);
  39 +});
17 example-projects/advanced/project.clj
@@ -20,22 +20,27 @@
20 20 :repl-listen-port 9000
21 21 :repl-launch-commands
22 22 {"firefox" ["firefox"]
23   - "firefox-naked" ["firefox" "resources/public/html/naked.html"]
24   - "phantom" ["phantomjs" "phantom/page-repl.js"]
25   - "phantom-naked" ["phantomjs" "phantom/page-repl.js" "resources/public/html/naked.html"]}
  23 + "firefox-naked" ["firefox" "resources/private/html/naked.html"]
  24 + "phantom" ["phantomjs" "phantom/repl.js"]
  25 + "phantom-naked" ["phantomjs" "phantom/repl.js" "resources/private/html/naked.html"]}
  26 + :test-commands
  27 + {"unit" ["phantomjs" "phantom/unit-test.js" "resources/private/html/unit-test.html"]}
26 28 ; Configure two separate builds; one with few optimizations for
27 29 ; development/debugging, and one with many optimizations for
28 30 ; production use.
  31 + :crossovers [example.crossover]
29 32 :builds [
30 33 {:source-path "src-cljs"
31 34 :jar true
32   - :crossovers [example.crossover]
33 35 :compiler {:output-to "resources/public/js/main-debug.js"
34 36 :optimizations :whitespace
35 37 :pretty-print true}}
36 38 {:source-path "src-cljs"
37   - :crossovers [example.crossover]
38 39 :compiler {:output-to "resources/public/js/main.js"
39 40 :optimizations :advanced
40   - :pretty-print false}}]}
  41 + :pretty-print false}}
  42 + {:source-path "test-cljs"
  43 + :compiler {:output-to "resources/private/js/unit-test.js"
  44 + :optimizations :whitespace
  45 + :pretty-print true}}]}
41 46 :ring {:handler example.routes/app})
2  ...rojects/advanced/resources/public/html/naked.html → ...ojects/advanced/resources/private/html/naked.html
@@ -2,7 +2,7 @@
2 2 <body>
3 3 This is just a dummy HTML file to connect to the REPL.
4 4 Don't close this window until you're done with the REPL.
5   - <script src="../js/main-debug.js" type="text/javascript"></script>
  5 + <script src="../../public/js/main-debug.js" type="text/javascript"></script>
6 6 <script type="text/javascript">
7 7 example.repl.connect();
8 8 </script>
8 example-projects/advanced/resources/private/html/unit-test.html
... ... @@ -0,0 +1,8 @@
  1 +<html>
  2 + <body>
  3 + This is just a dummy HTML file with which to load the unit tests.
  4 + This file could be changed to include HTML for the tests to use
  5 + during their operation.
  6 + <script src="../js/unit-test.js" type="text/javascript"></script>
  7 + </body>
  8 +</html>
3  example-projects/advanced/src-cljs/example/hello.cljs
@@ -4,3 +4,6 @@
4 4
5 5 (defn ^:export say-hello []
6 6 (js/alert (shared/make-example-text)))
  7 +
  8 +(defn add-some-numbers [& numbers]
  9 + (apply + numbers))
9 example-projects/advanced/test-cljs/example/test.cljs
... ... @@ -0,0 +1,9 @@
  1 +(ns example.test
  2 + (:require [example.test.hello :as hello]))
  3 +
  4 +(def success 0)
  5 +
  6 +(defn ^:export run []
  7 + (.log js/console "Example test started.")
  8 + (hello/run)
  9 + success)
7 example-projects/advanced/test-cljs/example/test/hello.cljs
... ... @@ -0,0 +1,7 @@
  1 +(ns example.test.hello
  2 + (:use [example.hello :only [add-some-numbers]]))
  3 +
  4 +(defn run []
  5 + (assert (= (add-some-numbers 2 2) 4))
  6 + (assert (= (add-some-numbers 1 2 3) 6))
  7 + (assert (= (add-some-numbers 4 5 6) 15)))
182 plugin/src/leiningen/cljsbuild.clj
@@ -5,20 +5,31 @@
5 5 [clojure.pprint :as pprint]
6 6 [clojure.string :as s]
7 7 [fs.core :as fs]
8   - [robert.hooke :as hooke]
9   - [leiningen.compile :as lcompile]
10 8 [leiningen.clean :as lclean]
11   - [leiningen.jar :as ljar]))
  9 + [leiningen.compile :as lcompile]
  10 + [leiningen.jar :as ljar]
  11 + [leiningen.test :as ltest]
  12 + [robert.hooke :as hooke]))
12 13
13 14 (def cljsbuild-dependencies
14 15 '[[cljsbuild "0.1.0"]])
15 16
16   -(def repl-output-dir ".lein-cljsbuild-repl")
17   -(def default-compiler-output-dir ".lein-cljsbuild-compiler")
  17 +(def repl-output-path ".lein-cljsbuild-repl")
  18 +(def crossover-path ".lein-cljsbuild-crossover")
  19 +(def compiler-output-dir-base ".lein-cljsbuild-compiler-")
18 20
19 21 (def default-global-options
20 22 {:repl-launch-commands {}
21   - :repl-listen-port 9000})
  23 + :repl-listen-port 9000
  24 + :test-commands {}
  25 + :crossover-path "crossover-cljs"
  26 + :crossovers []})
  27 +
  28 +; TODO
  29 +; Document the new crossovers beahvior.... :crossover-path
  30 +; Add a :crossover-jar boolean option?
  31 +; Write a "migrating from 0.0.x to 0.1.x" doc... :(
  32 +; TODO
22 33
23 34 (def default-compiler-options
24 35 {:output-to "main.js"
@@ -27,11 +38,9 @@
27 38
28 39 (def default-build-options
29 40 {:source-path "src-cljs"
30   - :crossovers []
31 41 :compiler default-compiler-options})
32 42
33 43 (def exit-success 0)
34   -
35 44 (def exit-failure 1)
36 45
37 46 (defn- printerr [& args]
@@ -41,8 +50,9 @@
41 50 (defn- warn [& args]
42 51 (apply printerr "WARNING:" args))
43 52
  53 +; TODO Dedupliclate this with the docstring below.
44 54 (defn- usage []
45   - (printerr "Usage: lein cljsbuild [once|auto|clean|repl-listen|repl-launch|repl-rhino]"))
  55 + (printerr "Usage: lein cljsbuild [once|auto|clean|test|repl-listen|repl-launch|repl-rhino]"))
46 56
47 57 (declare deep-merge-item)
48 58
@@ -61,62 +71,89 @@
61 71 (map (fn [[k v]] (vec (cons k v)))
62 72 (merge project cljsbuild))))
63 73
64   -(defn- run-local-project [project builds requires form]
  74 +(defn- run-local-project [project crossover-path builds requires form]
65 75 (lcompile/eval-in-project
66 76 {:local-repo-classpath true
67 77 :source-path (:source-path project)
68 78 :extra-classpath-dirs (concat
69 79 (:extra-classpath-dirs project)
70   - (map :source-path builds))
  80 + (map :source-path builds)
  81 + [crossover-path])
71 82 :dependencies (merge-dependencies (:dependencies project))
72 83 :dev-dependencies (:dev-dependencies project)}
73   - form
  84 + `(do
  85 + ~form
  86 + (shutdown-agents))
74 87 nil
75 88 nil
76 89 requires)
77 90 exit-success)
78 91
79   -(defn- run-compiler [project {:keys [builds]} watch?]
80   - (run-local-project project builds
81   - '(require 'cljsbuild.compiler)
  92 +(defn- run-compiler [project {:keys [crossover-path crossovers builds]} watch?]
  93 + (println "Compiling ClojureScript.")
  94 + ; If crossover-path does not exist before eval-in-project is called,
  95 + ; the files it contains won't be classloadable, for some reason.
  96 + (fs/mkdirs crossover-path)
  97 + (run-local-project project crossover-path builds
  98 + '(require 'cljsbuild.compiler 'cljsbuild.crossover 'cljsbuild.util)
82 99 `(do
83   - (println "Compiling ClojureScript.")
84   - (cljsbuild.compiler/in-threads
85   - (fn [opts#]
86   - (cljsbuild.compiler/run-compiler
87   - (:source-path opts#)
88   - (:crossovers opts#)
89   - (:compiler opts#)
90   - ~watch?))
91   - '~builds)
92   - (shutdown-agents))))
93   -
94   -(defn- cleanup-files [project {:keys [builds]}]
95   - (fs/delete-dir repl-output-dir)
96   - (run-local-project project builds
97   - '(require 'cljsbuild.compiler)
98   - `(do
99   - (println "Deleting generated files.")
100   - (cljsbuild.compiler/in-threads
101   - (fn [opts#]
102   - (cljsbuild.compiler/cleanup-files
103   - (:source-path opts#)
104   - (:crossovers opts#)
105   - (:compiler opts#)))
106   - '~builds)
107   - (shutdown-agents))))
108   -
109   -(defn- run-repl-listen [project {:keys [builds repl-listen-port]}]
110   - (run-local-project project builds
  100 + (letfn [(copy-crossovers# []
  101 + (cljsbuild.crossover/copy-crossovers
  102 + ~crossover-path
  103 + '~crossovers))]
  104 + (copy-crossovers#)
  105 + (when ~watch?
  106 + (cljsbuild.util/once-every 1000 "copying crossovers" copy-crossovers#))
  107 + (cljsbuild.util/in-threads
  108 + (fn [opts#]
  109 + (cljsbuild.compiler/run-compiler
  110 + (:source-path opts#)
  111 + ~crossover-path
  112 + (:compiler opts#)
  113 + ~watch?))
  114 + '~builds)))))
  115 +
  116 +(defn- cleanup-files [project {:keys [crossover-path builds]}]
  117 + (println "Deleting ClojureScript-related generated files.")
  118 + (fs/delete-dir repl-output-path)
  119 + (fs/delete-dir crossover-path)
  120 + (run-local-project project crossover-path builds
  121 + '(require 'cljsbuild.clean 'cljsbuild.util)
  122 + `(cljsbuild.util/in-threads
  123 + (fn [opts#]
  124 + (cljsbuild.clean/cleanup-files
  125 + (:compiler opts#)))
  126 + '~builds)))
  127 +
  128 +(defn- run-tests [project {:keys [test-commands builds]} args]
  129 + (when (> (count args) 1)
  130 + (throw (Exception. "Only expected zero or one arguments.")))
  131 + (let [selected-tests (if (empty? args)
  132 + (do
  133 + (println "Running all ClojureScript tests.")
  134 + (vec (vals test-commands)))
  135 + (do
  136 + (println "Running ClojureScript test:" (first args))
  137 + [(test-commands (first args))]))]
  138 + (run-local-project project crossover-path builds
  139 + '(require 'cljsbuild.test)
  140 + `(cljsbuild.test/run-tests ~selected-tests))))
  141 +
  142 +(defn- run-compiler-and-tests [project options args]
  143 + (let [compile-result (run-compiler project options false)]
  144 + (if (not= compile-result exit-success)
  145 + compile-result
  146 + (run-tests project options args))))
  147 +
  148 +(defn- run-repl-listen [project {:keys [crossover-path builds repl-listen-port]}]
  149 + (println (str "Running ClojureScript REPL, listening on port " repl-listen-port "."))
  150 + (run-local-project project crossover-path builds
111 151 '(require 'cljsbuild.repl.listen)
112   - `(do
113   - (println (str "Running REPL, listening on port " ~repl-listen-port "."))
114   - (cljsbuild.repl.listen/run-repl-listen
115   - ~repl-listen-port
116   - ~repl-output-dir)
117   - (shutdown-agents))))
  152 + `(cljsbuild.repl.listen/run-repl-listen
  153 + ~repl-listen-port
  154 + ~repl-output-path)))
118 155
119   -(defn- run-repl-launch [project {:keys [builds repl-listen-port repl-launch-commands]} args]
  156 +(defn- run-repl-launch [project {:keys [crossover-path builds repl-listen-port repl-launch-commands]} args]
120 157 (when (< (count args) 1)
121 158 (throw (Exception. "Must supply a launch command identifier.")))
122 159 (let [launch-name (first args)
@@ -125,22 +162,19 @@
125 162 (when (nil? command-base)
126 163 (throw (Exception. (str "Unknown REPL launch command: " launch-name))))
127 164 (let [command (concat command-base command-args)]
128   - (run-local-project project builds
  165 + (println "Running ClojureScript REPL and launching command:" command)
  166 + (run-local-project project crossover-path builds
129 167 '(require 'cljsbuild.repl.listen)
130   - `(do
131   - (println "Running REPL and launching command:" '~command)
132   - (cljsbuild.repl.listen/run-repl-launch
  168 + `(cljsbuild.repl.listen/run-repl-launch
133 169 ~repl-listen-port
134   - ~repl-output-dir
135   - '~command)
136   - (shutdown-agents))))))
  170 + ~repl-output-path
  171 + '~command)))))
137 172
138   -(defn- run-repl-rhino [project {:keys [builds]}]
139   - (run-local-project project builds
  173 +(defn- run-repl-rhino [project {:keys [crossover-path builds]}]
  174 + (println "Running Rhino-based ClojureScript REPL.")
  175 + (run-local-project project crossover-path builds
140 176 '(require 'cljsbuild.repl.rhino)
141   - `(do
142   - (println "Running Rhino-based REPL.")
143   - (cljsbuild.repl.rhino/run-repl-rhino))))
  177 + `(cljsbuild.repl.rhino/run-repl-rhino)))
144 178
145 179 (defn- set-default-build-options [options]
146 180 (deep-merge default-build-options options))
@@ -152,10 +186,10 @@
152 186 (if (get-in build output-dir-key)
153 187 build
154 188 (assoc-in build output-dir-key
155   - (str default-compiler-output-dir "-" counter))))]
  189 + (str compiler-output-dir-base counter))))]
156 190 (if (apply distinct? (map #(get-in % output-dir-key) builds))
157 191 (assoc options :builds builds)
158   - (throw (Exception. "Compiler :output-dir options must be distinct.")))))
  192 + (throw (Exception. (str "All " output-dir-key " options must be distinct."))))))
159 193
160 194 (defn- set-default-global-options [options]
161 195 (deep-merge default-global-options
@@ -202,6 +236,7 @@ Available commands:
202 236 once Compile the ClojureScript project once.
203 237 auto Automatically recompile when files are modified.
204 238 clean Remove automatically generated files.
  239 + test Run ClojureScript tests.
205 240
206 241 repl-listen Run a REPL that will listen for incoming connections.
207 242 repl-launch Run a REPL and launch a custom command to connect to it.
@@ -215,6 +250,7 @@ Available commands:
215 250 "once" (run-compiler project options false)
216 251 "auto" (run-compiler project options true)
217 252 "clean" (cleanup-files project options)
  253 + "test" (run-compiler-and-tests project options args)
218 254 "repl-listen" (run-repl-listen project options)
219 255 "repl-launch" (run-repl-launch project options args)
220 256 "repl-rhino" (run-repl-rhino project options)
@@ -257,16 +293,26 @@ Available commands:
257 293 (mapcat path-filespecs paths)))
258 294
259 295 (defn compile-hook [task & args]
260   - (cljsbuild (first args) "once")
261   - (apply task args))
  296 + (let [compile-result (apply task args)]
  297 + (if (not= compile-result exit-success)
  298 + compile-result
  299 + (run-compiler (first args) (extract-options (first args)) false))))
  300 +
  301 +(defn test-hook [task & args]
  302 + (let [test-results [(apply task args)
  303 + (run-tests (first args) (extract-options (first args)) [])]]
  304 + (if (every? #(= % exit-success) test-results)
  305 + exit-success
  306 + exit-failure)))
262 307
263 308 (defn clean-hook [task & args]
264   - (cljsbuild (first args) "clean")
265   - (apply task args))
  309 + (apply task args)
  310 + (cleanup-files (first args) (extract-options (first args))))
266 311
267 312 (defn jar-hook [task & [project out-file filespecs]]
268 313 (apply task [project out-file (concat filespecs (get-filespecs project))]))
269 314
270 315 (hooke/add-hook #'lcompile/compile compile-hook)
  316 +(hooke/add-hook #'ltest/test test-hook)
271 317 (hooke/add-hook #'lclean/clean clean-hook)
272 318 (hooke/add-hook #'ljar/write-jar jar-hook)
7 support/src/cljsbuild/clean.clj
... ... @@ -0,0 +1,7 @@
  1 +(ns cljsbuild.clean
  2 + (:require
  3 + [fs.core :as fs]))
  4 +
  5 +(defn cleanup-files [compiler-options]
  6 + (fs/delete (:output-to compiler-options))
  7 + (fs/delete-dir (:output-dir compiler-options)))
157 support/src/cljsbuild/compiler.clj
... ... @@ -1,10 +1,9 @@
1 1 (ns cljsbuild.compiler
2 2 (:use
3   - [clojure.java.io :only [as-url resource]]
4 3 [clj-stacktrace.repl :only [pst+]]
5 4 [cljs.closure :only [build]])
6 5 (:require
7   - [clojure.string :as string]
  6 + [cljsbuild.util :as util]
8 7 [fs.core :as fs]))
9 8
10 9 (def lock (Object.))
@@ -15,45 +14,6 @@
15 14 (apply println args)
16 15 (flush)))
17 16
18   -(defn- join-paths [& paths]
19   - (apply str (interpose "/" paths)))
20   -
21   -(defn- separate [pred coll]
22   - (let [separated (group-by pred coll)]
23   - [(separated true) (separated false)]))
24   -
25   -(defmacro dofor [seq-exprs body-expr]
26   - `(doall (for ~seq-exprs ~body-expr)))
27   -
28   -(defn- fail [& args]
29   - (throw (Exception. (apply str args))))
30   -
31   -(defn- truncate-uri-path [uri n]
32   - (if uri
33   - (let [uri-path (.getPath uri)]
34   - (subs uri-path 0 (- (.length uri-path) n)))
35   - nil))
36   -
37   -(defn- ns-to-path [ns]
38   - (let [underscored (string/replace (str ns) #"-" "_")]
39   - (apply join-paths
40   - (string/split underscored #"\."))))
41   -
42   -(defn- filter-cljs [files types]
43   - (let [ext #(last (string/split % #"\."))]
44   - (filter #(types (ext %)) files)))
45   -
46   -(defn- find-dir-cljs [root files types]
47   - (for [cljs (filter-cljs files types)]
48   - (join-paths root cljs)))
49   -
50   -(defn- find-cljs [dir types]
51   - (let [iter (fs/iterate-dir dir)]
52   - (mapcat
53   - (fn [[root _ files]]
54   - (find-dir-cljs root files types))
55   - iter)))
56   -
57 17 (defn- elapsed [started-at]
58 18 (let [elapsed-us (- (. System (nanoTime)) started-at)]
59 19 (with-precision 2
@@ -79,117 +39,18 @@
79 39 (println-safe " Failed!")
80 40 (pst+ e))))))
81 41
82   -(defn- is-macro-file? [file]
83   - (not (neg? (.indexOf (slurp file) ";*CLJSBUILD-MACRO-FILE*;"))))
84   -
85   -; There is a little bit of madness here to share macros between Clojure
86   -; and ClojureScript. The latter needs a (:require-macros ...) whereas the
87   -; former just wants (:require ...). Thus, we have a ;*CLJSBUILD-REMOVE*;
88   -; conditional comment to allow different code to be used for ClojureScript files.
89   -(defn- filtered-crossover-file [file]
90   - (str
91   - "; DO NOT EDIT THIS FILE! IT WAS AUTOMATICALLY GENERATED BY\n"
92   - "; lein-cljsbuild FROM THE FOLLOWING SOURCE FILE:\n"
93   - "; " file "\n\n"
94   - (string/replace (slurp file) ";*CLJSBUILD-REMOVE*;" "")))
95   -
96   -(defn- crossover-to [cljs-path [from-parent from-resource]]
97   - (let [subpath (string/replace-first
98   - (fs/absolute-path (.getPath from-resource))
99   - (fs/absolute-path from-parent) "")
100   - to-file (fs/normalized-path
101   - (join-paths (fs/absolute-path cljs-path) subpath))]
102   - (string/replace to-file #"\.clj$" ".cljs")))
103   -
104   -(defn- recurse-resource-dir [dir]
105   - (if dir
106   - ; We can't determine the contents of a jar dir. Thus, crossover files
107   - ; in jars cannot be specified recursively; they have to be named file
108   - ; by file.
109   - (if (= (.getProtocol dir) "file")
110   - (let [files (find-cljs (.getPath dir) #{"clj"})]
111   - (map #(as-url (str "file:" %)) files))
112   - [dir])))
113   -
114   -(defn- find-crossover [crossover]
115   - (let [ns-path (ns-to-path crossover)
116   - as-dir (resource ns-path)
117   - dir-parent (truncate-uri-path as-dir (.length ns-path))
118   - recurse-dirs (recurse-resource-dir as-dir)
119   - ns-file-path (str ns-path ".clj")
120   - as-file (resource ns-file-path)
121   - file-parent (truncate-uri-path as-file (.length ns-file-path))
122   - resources (conj
123   - (map vector (repeat dir-parent) recurse-dirs)
124   - [file-parent as-file])]
125   - (when (empty? resources)
126   - (fail "Unable to find crossover: " crossover))
127   - resources))
128   -
129   -(defn- find-crossovers [crossovers]
130   - (distinct
131   - (remove #(nil? (second %))
132   - (mapcat find-crossover crossovers))))
133   -
134   -(defn- crossover-needs-update? [from-resource to-file]
135   - (let [exists (fs/exists? to-file)]
136   - (or
137   - (not exists)
138   - (and
139   - ; We can't determine the mtime for jar resources; they'll just
140   - ; be copied once and that's it.
141   - (= "file" (.getProtocol from-resource))
142   - (> (fs/mod-time (.getPath from-resource)) (fs/mod-time to-file))))))
143   -
144   -(defn- copy-crossovers [cljs-path from-resources]
145   - (let [to-files (map (partial crossover-to cljs-path) from-resources)]
146   - (doseq [dir (distinct (map fs/parent to-files))]
147   - (fs/mkdirs dir))
148   - (dofor [[[_ from-resource] to-file] (zipmap from-resources to-files)]
149   - (when (crossover-needs-update? from-resource to-file)
150   - (spit to-file (filtered-crossover-file from-resource))
151   - ; Mark the file as read-only, to hopefully warn the user not to modify it.
152   - (fs/chmod "-w" to-file)
153   - :updated))))
154   -
155   -(defn in-threads
156   - "Given a seq and a function, applies the function to each item in a different thread
157   -and returns a seq of the results. Launches all the threads at once."
158   - [f s]
159   - (doall (map deref (doall (map #(future (f %)) s)))))
160   -
161   -(defn run-compiler [cljs-path crossovers compiler-options watch?]
  42 +(defn run-compiler [cljs-path crossover-path compiler-options watch?]
162 43 (loop [last-dependency-mtimes {}]
163 44 (let [output-file (:output-to compiler-options)
164 45 output-mtime (if (fs/exists? output-file) (fs/mod-time output-file) 0)
165   - crossover-resources (find-crossovers crossovers)
166   - [crossover-macros crossover-plains] (separate
167   - #(is-macro-file? (second %))
168   - crossover-resources)
169   - ; Filter out non-file macro namespaces, as we can't get the mtime
170   - ; for a namespace that comes from e.g. a jar file.
171   - crossover-macro-files (map #(.getPath %)
172   - (filter #(= "file" (.getProtocol %))
173   - (map second crossover-macros)))
174   - cljs-files (find-cljs cljs-path #{"cljs"})
175   - dependency-mtimes (map fs/mod-time
176   - (concat crossover-macro-files cljs-files))
177   - crossover-updated? (some #{:updated}
178   - (copy-crossovers cljs-path crossover-plains))]
179   - (when (or
180   - (and
181   - (not= last-dependency-mtimes dependency-mtimes)
182   - (some #(< output-mtime %) dependency-mtimes))
183   - crossover-updated?)
  46 + find-cljs #(util/find-files % #{"cljs"})
  47 + dependency-files (mapcat find-cljs [cljs-path crossover-path])
  48 + dependency-mtimes (map fs/mod-time dependency-files)]
  49 + (when
  50 + (and
  51 + (not= last-dependency-mtimes dependency-mtimes)
  52 + (some #(< output-mtime %) dependency-mtimes))
184 53 (compile-cljs cljs-path compiler-options))
185 54 (when watch?
186 55 (Thread/sleep 100)
187 56 (recur dependency-mtimes)))))
188   -
189   -(defn cleanup-files [cljs-path crossovers compiler-options]
190   - (fs/delete (:output-to compiler-options))
191   - (fs/delete-dir (:output-dir compiler-options))
192   - (let [from-resources (find-crossovers crossovers)
193   - to-files (map (partial crossover-to cljs-path) from-resources)]
194   - (doseq [file to-files]
195   - (fs/delete file))))
98 support/src/cljsbuild/crossover.clj
... ... @@ -0,0 +1,98 @@
  1 +(ns cljsbuild.crossover
  2 + (:use
  3 + [clojure.java.io :only [as-url resource]])
  4 + (:require
  5 + [cljsbuild.util :as util]
  6 + [clojure.string :as string]
  7 + [fs.core :as fs]))
  8 +
  9 +(defn- fail [& args]
  10 + (throw (Exception. (apply str args))))
  11 +
  12 +(defn- is-macro-file? [file]
  13 + (not (neg? (.indexOf (slurp file) ";*CLJSBUILD-MACRO-FILE*;"))))
  14 +
  15 +; There is a little bit of madness here to share macros between Clojure
  16 +; and ClojureScript. The latter needs a (:require-macros ...) whereas the
  17 +; former just wants (:require ...). Thus, we have a ;*CLJSBUILD-REMOVE*;
  18 +; conditional comment to allow different code to be used for ClojureScript files.
  19 +(defn- filtered-crossover-file [file]
  20 + (str
  21 + "; DO NOT EDIT THIS FILE! IT WAS AUTOMATICALLY GENERATED BY\n"
  22 + "; lein-cljsbuild FROM THE FOLLOWING SOURCE FILE:\n"
  23 + "; " file "\n\n"
  24 + (string/replace (slurp file) ";*CLJSBUILD-REMOVE*;" "")))
  25 +
  26 +(defn- crossover-to [crossover-path [from-parent from-resource]]
  27 + (let [subpath (string/replace-first
  28 + (fs/absolute-path (.getPath from-resource))
  29 + (fs/absolute-path from-parent) "")
  30 + to-file (fs/normalized-path
  31 + (util/join-paths (fs/absolute-path crossover-path) subpath))]
  32 + (string/replace to-file #"\.clj$" ".cljs")))
  33 +
  34 +(defn- recurse-resource-dir [dir]
  35 + (when dir
  36 + ; We can't determine the contents of a jar dir. Thus, crossover files
  37 + ; in jars cannot be specified recursively; they have to be named file
  38 + ; by file.
  39 + (if (= (.getProtocol dir) "file")
  40 + (let [files (util/find-files (.getPath dir) #{"clj"})]
  41 + (map #(as-url (str "file:" %)) files))
  42 + [dir])))
  43 +
  44 +(defn- truncate-uri-path [uri n]
  45 + (if uri
  46 + (let [uri-path (.getPath uri)]
  47 + (subs uri-path 0 (- (.length uri-path) n)))
  48 + nil))
  49 +
  50 +(defn- ns-to-path [ns]
  51 + (let [underscored (string/replace (str ns) #"-" "_")]
  52 + (apply util/join-paths
  53 + (string/split underscored #"\."))))
  54 +
  55 +(defn- find-crossover [crossover]
  56 + (let [ns-path (ns-to-path crossover)
  57 + as-dir (resource ns-path)
  58 + dir-parent (truncate-uri-path as-dir (.length ns-path))
  59 + recurse-dirs (recurse-resource-dir as-dir)
  60 + ns-file-path (str ns-path ".clj")
  61 + as-file (resource ns-file-path)
  62 + file-parent (truncate-uri-path as-file (.length ns-file-path))
  63 + resources (conj
  64 + (map vector (repeat dir-parent) recurse-dirs)
  65 + [file-parent as-file])]
  66 + (when (empty? resources)
  67 + (fail "Unable to find crossover: " crossover))
  68 + resources))
  69 +
  70 +(defn- find-crossovers [crossovers]
  71 + (distinct
  72 + (remove (fn [[_ file]] (or (nil? file) (is-macro-file? file)))
  73 + (mapcat find-crossover crossovers))))
  74 +
  75 +(defn- crossover-needs-update? [from-resource to-file]
  76 + (let [exists (fs/exists? to-file)]
  77 + (or
  78 + (not exists)
  79 + (and
  80 + ; We can't determine the mtime for jar resources; they'll just
  81 + ; be copied once and that's it.
  82 + (= "file" (.getProtocol from-resource))
  83 + (> (fs/mod-time (.getPath from-resource)) (fs/mod-time to-file))))))
  84 +
  85 +(defn copy-crossovers [crossover-path crossovers]
  86 + (let [from-resources (find-crossovers crossovers)
  87 + to-files (map (partial crossover-to crossover-path) from-resources)]
  88 + (doseq [dir (distinct (map fs/parent to-files))]
  89 + (fs/mkdirs dir))
  90 + (doseq [[[_ from-resource] to-file] (zipmap from-resources to-files)]
  91 + (when (crossover-needs-update? from-resource to-file)
  92 + (let [temp-file (str to-file ".tmp")]
  93 + ; Write a temp file and atomically rename to the real file
  94 + ; to prevent the compiler from reading a half-written file
  95 + (spit temp-file (filtered-crossover-file from-resource))
  96 + (fs/rename temp-file to-file)
  97 + ; Mark the file as read-only, to hopefully warn the user not to modify it.
  98 + (fs/chmod "-w" to-file))))))
15 support/src/cljsbuild/repl/listen.clj
... ... @@ -1,10 +1,10 @@
1 1 (ns cljsbuild.repl.listen
2 2 (:require
3   - [clojure.string :as string]
4   - [clojure.java.shell :as shell]
5 3 [cljs.repl :as repl]
6   - [cljs.repl.browser :as browser])
7   - (:import (java.io InputStreamReader OutputStreamWriter)))
  4 + [cljs.repl.browser :as browser]
  5 + [clojure.string :as string])
  6 + (:import
  7 + (java.io InputStreamReader OutputStreamWriter)))
8 8
9 9 (defn run-repl-listen [port output-dir]
10 10 (let [env (browser/repl-env :port (Integer. port) :working-dir output-dir)]
@@ -18,12 +18,12 @@
18 18 (defn- start-bg-command [command]
19 19 (Thread/sleep 1000)
20 20 (let [process (.exec (Runtime/getRuntime) (into-array command))]
21   - (.close (.getOutputStream process))
  21 + ; TODO: Maybe do something better with output; just stream it out?
22 22 (with-open [stdout (.getInputStream process)
23 23 stderr (.getErrorStream process)]
24 24 (let [[out err]
25   - (for [stream [stdout stderr]]
26   - (apply str (map char (stream-seq (InputStreamReader. stream "UTF-8")))))]
  25 + (for [stream [stdout stderr]]
  26 + (apply str (map char (stream-seq (InputStreamReader. stream "UTF-8")))))]
27 27 {:process process :out out :err err}))))
28 28
29 29 (defn run-repl-launch [port output-dir command]
@@ -45,5 +45,6 @@
45 45 (println (header "Standard error from launched command:"))
46 46 (println err)))
47 47 (catch Exception e
  48 + ; TODO: destroy the process if it was started!
48 49 (binding [*out* *err*]
49 50 (println "Launching command failed:" e))))))
35 support/src/cljsbuild/test.clj
... ... @@ -0,0 +1,35 @@
  1 +(ns cljsbuild.test
  2 + (:require
  3 + [clojure.java.io :as io])
  4 + (:import
  5 + (java.io OutputStreamWriter)))
  6 +
  7 +(defn- pump [reader out]
  8 + (let [buffer (make-array Character/TYPE 1000)]
  9 + (loop [len (.read reader buffer)]
  10 + (when-not (neg? len)
  11 + (.write out buffer 0 len)
  12 + (.flush out)
  13 + (Thread/sleep 100)
  14 + (recur (.read reader buffer))))))
  15 +
  16 +(defn sh [command]
  17 + (let [process (.exec (Runtime/getRuntime) (into-array command))]
  18 + (with-open [out (io/reader (.getInputStream process))
  19 + err (io/reader (.getErrorStream process))]
  20 + (let [pump-out (doto (Thread. #(pump out *out*)) .start)
  21 + pump-err (doto (Thread. #(pump err *err*)) .start)]
  22 + (.join pump-out)
  23 + (.join pump-err))
  24 + (.waitFor process)
  25 + (.exitValue process))))
  26 +
  27 +(defmacro dofor [seq-exprs body-expr]
  28 + `(doall (for ~seq-exprs ~body-expr)))
  29 +
  30 +(defn run-tests [test-commands]
  31 + (let [success (every? #(= % 0)
  32 + (dofor [test-command test-commands]
  33 + (sh test-command)))]
  34 + (when (not success)
  35 + (throw (Exception. "Test failed.")))))
37 support/src/cljsbuild/util.clj
... ... @@ -0,0 +1,37 @@
  1 +(ns cljsbuild.util
  2 + (:require
  3 + [clojure.string :as string]
  4 + [fs.core :as fs]))
  5 +
  6 +(defn join-paths [& paths]
  7 + (apply str (interpose "/" paths)))
  8 +
  9 +(defn filter-by-ext [files types]
  10 + (let [ext #(last (string/split % #"\."))]
  11 + (filter #(types (ext %)) files)))
  12 +
  13 +(defn find-dir-files [root files types]
  14 + (for [files (filter-by-ext files types)]
  15 + (join-paths root files)))
  16 +
  17 +(defn find-files [dir types]
  18 + (let [iter (fs/iterate-dir dir)]
  19 + (mapcat
  20 + (fn [[root _ files]]
  21 + (find-dir-files root files types))
  22 + iter)))
  23 +
  24 +(defn in-threads
  25 + "Given a seq and a function, applies the function to each item in a different thread
  26 +and returns a seq of the results. Launches all the threads at once."
  27 + [f s]
  28 + (doall (map deref (doall (map #(future (f %)) s)))))
  29 +
  30 +(defn once-every [ms desc f]
  31 + (future
  32 + (while true
  33 + (Thread/sleep ms)
  34 + (try
  35 + (f)
  36 + (catch Exception e
  37 + (println (str "Error " desc ": " e)))))))

0 comments on commit b8dbbb6

Please sign in to comment.
Something went wrong with that request. Please try again.