Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
@emezeske authored
View
4 .gitignore
@@ -7,7 +7,7 @@ cljsbuild-*.*.*
example-projects/*/*.jar
example-projects/simple/resources
example-projects/advanced/resources/public/js
-example-projects/*/.clojurescript-output
+example-projects/advanced/resources/private/js
+example-projects/advanced/crossover-cljs
example-projects/*/classes
example-projects/*/lib
-example-projects/*/src-cljs/example/crossover
View
2  doc/CROSSOVERS.md
@@ -1,5 +1,7 @@
# Sharing Code Between Clojure and ClojureScript
+**TODO** Fix docs W.R.T. the :crossovers option moving around.
+
Sharing code with lein-cljsbuild is accomplished via the configuration
of "crossovers". A crossover specifies a Clojure namespace, the content
of which should be copied into your ClojureScript project. This can be any
View
10 ...le-projects/advanced/phantom/page-repl.js → example-projects/advanced/phantom/repl.js
@@ -6,11 +6,19 @@ if (phantom.args.length != 1) {
var page = new WebPage();
var url = phantom.args[0];
+page.onConsoleMessage = function (message) {
+ console.log("App console: " + message);
+};
+
+console.log("Loading URL: " + url);
+
page.open(url, function (status) {
if (status != "success") {
console.log('Failed to open ' + url);
phantom.exit(1);
}
- // TODO: Should anything else happen here?
+ console.log("Loaded successfully.");
+
+ // TODO: Somehow gracefully exit when the REPL is closed?
});
View
39 example-projects/advanced/phantom/unit-test.js
@@ -0,0 +1,39 @@
+if (phantom.args.length != 1) {
+ console.log('Expected a target URL parameter.');
+ phantom.exit(1);
+}
+
+var page = new WebPage();
+var url = phantom.args[0];
+
+page.onConsoleMessage = function (message) {
+ console.log("Test console: " + message);
+};
+
+console.log("Loading URL: " + url);
+
+page.open(url, function (status) {
+ if (status != "success") {
+ console.log('Failed to open ' + url);
+ phantom.exit(1);
+ }
+
+ console.log("Running test.");
+
+ var result = page.evaluate(function() {
+ return example.test.run();
+ });
+
+ // NOTE: PhantomJS 1.4.0 has a bug that prevents the exit codes
+ // below from eing returned properly. :(
+ //
+ // http://code.google.com/p/phantomjs/issues/detail?id=294
+
+ if (result != 0) {
+ console.log("*** Test failed! ***");
+ phantom.exit(1);
+ }
+
+ console.log("Test succeeded.");
+ phantom.exit(0);
+});
View
17 example-projects/advanced/project.clj
@@ -20,22 +20,27 @@
:repl-listen-port 9000
:repl-launch-commands
{"firefox" ["firefox"]
- "firefox-naked" ["firefox" "resources/public/html/naked.html"]
- "phantom" ["phantomjs" "phantom/page-repl.js"]
- "phantom-naked" ["phantomjs" "phantom/page-repl.js" "resources/public/html/naked.html"]}
+ "firefox-naked" ["firefox" "resources/private/html/naked.html"]
+ "phantom" ["phantomjs" "phantom/repl.js"]
+ "phantom-naked" ["phantomjs" "phantom/repl.js" "resources/private/html/naked.html"]}
+ :test-commands
+ {"unit" ["phantomjs" "phantom/unit-test.js" "resources/private/html/unit-test.html"]}
; Configure two separate builds; one with few optimizations for
; development/debugging, and one with many optimizations for
; production use.
+ :crossovers [example.crossover]
:builds [
{:source-path "src-cljs"
:jar true
- :crossovers [example.crossover]
:compiler {:output-to "resources/public/js/main-debug.js"
:optimizations :whitespace
:pretty-print true}}
{:source-path "src-cljs"
- :crossovers [example.crossover]
:compiler {:output-to "resources/public/js/main.js"
:optimizations :advanced
- :pretty-print false}}]}
+ :pretty-print false}}
+ {:source-path "test-cljs"
+ :compiler {:output-to "resources/private/js/unit-test.js"
+ :optimizations :whitespace
+ :pretty-print true}}]}
:ring {:handler example.routes/app})
View
2  ...advanced/resources/public/html/naked.html → ...dvanced/resources/private/html/naked.html
@@ -2,7 +2,7 @@
<body>
This is just a dummy HTML file to connect to the REPL.
Don't close this window until you're done with the REPL.
- <script src="../js/main-debug.js" type="text/javascript"></script>
+ <script src="../../public/js/main-debug.js" type="text/javascript"></script>
<script type="text/javascript">
example.repl.connect();
</script>
View
8 example-projects/advanced/resources/private/html/unit-test.html
@@ -0,0 +1,8 @@
+<html>
+ <body>
+ This is just a dummy HTML file with which to load the unit tests.
+ This file could be changed to include HTML for the tests to use
+ during their operation.
+ <script src="../js/unit-test.js" type="text/javascript"></script>
+ </body>
+</html>
View
3  example-projects/advanced/src-cljs/example/hello.cljs
@@ -4,3 +4,6 @@
(defn ^:export say-hello []
(js/alert (shared/make-example-text)))
+
+(defn add-some-numbers [& numbers]
+ (apply + numbers))
View
9 example-projects/advanced/test-cljs/example/test.cljs
@@ -0,0 +1,9 @@
+(ns example.test
+ (:require [example.test.hello :as hello]))
+
+(def success 0)
+
+(defn ^:export run []
+ (.log js/console "Example test started.")
+ (hello/run)
+ success)
View
7 example-projects/advanced/test-cljs/example/test/hello.cljs
@@ -0,0 +1,7 @@
+(ns example.test.hello
+ (:use [example.hello :only [add-some-numbers]]))
+
+(defn run []
+ (assert (= (add-some-numbers 2 2) 4))
+ (assert (= (add-some-numbers 1 2 3) 6))
+ (assert (= (add-some-numbers 4 5 6) 15)))
View
182 plugin/src/leiningen/cljsbuild.clj
@@ -5,20 +5,31 @@
[clojure.pprint :as pprint]
[clojure.string :as s]
[fs.core :as fs]
- [robert.hooke :as hooke]
- [leiningen.compile :as lcompile]
[leiningen.clean :as lclean]
- [leiningen.jar :as ljar]))
+ [leiningen.compile :as lcompile]
+ [leiningen.jar :as ljar]
+ [leiningen.test :as ltest]
+ [robert.hooke :as hooke]))
(def cljsbuild-dependencies
'[[cljsbuild "0.1.0"]])
-(def repl-output-dir ".lein-cljsbuild-repl")
-(def default-compiler-output-dir ".lein-cljsbuild-compiler")
+(def repl-output-path ".lein-cljsbuild-repl")
+(def crossover-path ".lein-cljsbuild-crossover")
+(def compiler-output-dir-base ".lein-cljsbuild-compiler-")
(def default-global-options
{:repl-launch-commands {}
- :repl-listen-port 9000})
+ :repl-listen-port 9000
+ :test-commands {}
+ :crossover-path "crossover-cljs"
+ :crossovers []})
+
+; TODO
+; Document the new crossovers beahvior.... :crossover-path
+; Add a :crossover-jar boolean option?
+; Write a "migrating from 0.0.x to 0.1.x" doc... :(
+; TODO
(def default-compiler-options
{:output-to "main.js"
@@ -27,11 +38,9 @@
(def default-build-options
{:source-path "src-cljs"
- :crossovers []
:compiler default-compiler-options})
(def exit-success 0)
-
(def exit-failure 1)
(defn- printerr [& args]
@@ -41,8 +50,9 @@
(defn- warn [& args]
(apply printerr "WARNING:" args))
+; TODO Dedupliclate this with the docstring below.
(defn- usage []
- (printerr "Usage: lein cljsbuild [once|auto|clean|repl-listen|repl-launch|repl-rhino]"))
+ (printerr "Usage: lein cljsbuild [once|auto|clean|test|repl-listen|repl-launch|repl-rhino]"))
(declare deep-merge-item)
@@ -61,62 +71,89 @@
(map (fn [[k v]] (vec (cons k v)))
(merge project cljsbuild))))
-(defn- run-local-project [project builds requires form]
+(defn- run-local-project [project crossover-path builds requires form]
(lcompile/eval-in-project
{:local-repo-classpath true
:source-path (:source-path project)
:extra-classpath-dirs (concat
(:extra-classpath-dirs project)
- (map :source-path builds))
+ (map :source-path builds)
+ [crossover-path])
:dependencies (merge-dependencies (:dependencies project))
:dev-dependencies (:dev-dependencies project)}
- form
+ `(do
+ ~form
+ (shutdown-agents))
nil
nil
requires)
exit-success)
-(defn- run-compiler [project {:keys [builds]} watch?]
- (run-local-project project builds
- '(require 'cljsbuild.compiler)
+(defn- run-compiler [project {:keys [crossover-path crossovers builds]} watch?]
+ (println "Compiling ClojureScript.")
+ ; If crossover-path does not exist before eval-in-project is called,
+ ; the files it contains won't be classloadable, for some reason.
+ (fs/mkdirs crossover-path)
+ (run-local-project project crossover-path builds
+ '(require 'cljsbuild.compiler 'cljsbuild.crossover 'cljsbuild.util)
`(do
- (println "Compiling ClojureScript.")
- (cljsbuild.compiler/in-threads
- (fn [opts#]
- (cljsbuild.compiler/run-compiler
- (:source-path opts#)
- (:crossovers opts#)
- (:compiler opts#)
- ~watch?))
- '~builds)
- (shutdown-agents))))
-
-(defn- cleanup-files [project {:keys [builds]}]
- (fs/delete-dir repl-output-dir)
- (run-local-project project builds
- '(require 'cljsbuild.compiler)
- `(do
- (println "Deleting generated files.")
- (cljsbuild.compiler/in-threads
- (fn [opts#]
- (cljsbuild.compiler/cleanup-files
- (:source-path opts#)
- (:crossovers opts#)
- (:compiler opts#)))
- '~builds)
- (shutdown-agents))))
-
-(defn- run-repl-listen [project {:keys [builds repl-listen-port]}]
- (run-local-project project builds
+ (letfn [(copy-crossovers# []
+ (cljsbuild.crossover/copy-crossovers
+ ~crossover-path
+ '~crossovers))]
+ (copy-crossovers#)
+ (when ~watch?
+ (cljsbuild.util/once-every 1000 "copying crossovers" copy-crossovers#))
+ (cljsbuild.util/in-threads
+ (fn [opts#]
+ (cljsbuild.compiler/run-compiler
+ (:source-path opts#)
+ ~crossover-path
+ (:compiler opts#)
+ ~watch?))
+ '~builds)))))
+
+(defn- cleanup-files [project {:keys [crossover-path builds]}]
+ (println "Deleting ClojureScript-related generated files.")
+ (fs/delete-dir repl-output-path)
+ (fs/delete-dir crossover-path)
+ (run-local-project project crossover-path builds
+ '(require 'cljsbuild.clean 'cljsbuild.util)
+ `(cljsbuild.util/in-threads
+ (fn [opts#]
+ (cljsbuild.clean/cleanup-files
+ (:compiler opts#)))
+ '~builds)))
+
+(defn- run-tests [project {:keys [test-commands builds]} args]
+ (when (> (count args) 1)
+ (throw (Exception. "Only expected zero or one arguments.")))
+ (let [selected-tests (if (empty? args)
+ (do
+ (println "Running all ClojureScript tests.")
+ (vec (vals test-commands)))
+ (do
+ (println "Running ClojureScript test:" (first args))
+ [(test-commands (first args))]))]
+ (run-local-project project crossover-path builds
+ '(require 'cljsbuild.test)
+ `(cljsbuild.test/run-tests ~selected-tests))))
+
+(defn- run-compiler-and-tests [project options args]
+ (let [compile-result (run-compiler project options false)]
+ (if (not= compile-result exit-success)
+ compile-result
+ (run-tests project options args))))
+
+(defn- run-repl-listen [project {:keys [crossover-path builds repl-listen-port]}]
+ (println (str "Running ClojureScript REPL, listening on port " repl-listen-port "."))
+ (run-local-project project crossover-path builds
'(require 'cljsbuild.repl.listen)
- `(do
- (println (str "Running REPL, listening on port " ~repl-listen-port "."))
- (cljsbuild.repl.listen/run-repl-listen
- ~repl-listen-port
- ~repl-output-dir)
- (shutdown-agents))))
+ `(cljsbuild.repl.listen/run-repl-listen
+ ~repl-listen-port
+ ~repl-output-path)))
-(defn- run-repl-launch [project {:keys [builds repl-listen-port repl-launch-commands]} args]
+(defn- run-repl-launch [project {:keys [crossover-path builds repl-listen-port repl-launch-commands]} args]
(when (< (count args) 1)
(throw (Exception. "Must supply a launch command identifier.")))
(let [launch-name (first args)
@@ -125,22 +162,19 @@
(when (nil? command-base)
(throw (Exception. (str "Unknown REPL launch command: " launch-name))))
(let [command (concat command-base command-args)]
- (run-local-project project builds
+ (println "Running ClojureScript REPL and launching command:" command)
+ (run-local-project project crossover-path builds
'(require 'cljsbuild.repl.listen)
- `(do
- (println "Running REPL and launching command:" '~command)
- (cljsbuild.repl.listen/run-repl-launch
+ `(cljsbuild.repl.listen/run-repl-launch
~repl-listen-port
- ~repl-output-dir
- '~command)
- (shutdown-agents))))))
+ ~repl-output-path
+ '~command)))))
-(defn- run-repl-rhino [project {:keys [builds]}]
- (run-local-project project builds
+(defn- run-repl-rhino [project {:keys [crossover-path builds]}]
+ (println "Running Rhino-based ClojureScript REPL.")
+ (run-local-project project crossover-path builds
'(require 'cljsbuild.repl.rhino)
- `(do
- (println "Running Rhino-based REPL.")
- (cljsbuild.repl.rhino/run-repl-rhino))))
+ `(cljsbuild.repl.rhino/run-repl-rhino)))
(defn- set-default-build-options [options]
(deep-merge default-build-options options))
@@ -152,10 +186,10 @@
(if (get-in build output-dir-key)
build
(assoc-in build output-dir-key
- (str default-compiler-output-dir "-" counter))))]
+ (str compiler-output-dir-base counter))))]
(if (apply distinct? (map #(get-in % output-dir-key) builds))
(assoc options :builds builds)
- (throw (Exception. "Compiler :output-dir options must be distinct.")))))
+ (throw (Exception. (str "All " output-dir-key " options must be distinct."))))))
(defn- set-default-global-options [options]
(deep-merge default-global-options
@@ -202,6 +236,7 @@ Available commands:
once Compile the ClojureScript project once.
auto Automatically recompile when files are modified.
clean Remove automatically generated files.
+ test Run ClojureScript tests.
repl-listen Run a REPL that will listen for incoming connections.
repl-launch Run a REPL and launch a custom command to connect to it.
@@ -215,6 +250,7 @@ Available commands:
"once" (run-compiler project options false)
"auto" (run-compiler project options true)
"clean" (cleanup-files project options)
+ "test" (run-compiler-and-tests project options args)
"repl-listen" (run-repl-listen project options)
"repl-launch" (run-repl-launch project options args)
"repl-rhino" (run-repl-rhino project options)
@@ -257,16 +293,26 @@ Available commands:
(mapcat path-filespecs paths)))
(defn compile-hook [task & args]
- (cljsbuild (first args) "once")
- (apply task args))
+ (let [compile-result (apply task args)]
+ (if (not= compile-result exit-success)
+ compile-result
+ (run-compiler (first args) (extract-options (first args)) false))))
+
+(defn test-hook [task & args]
+ (let [test-results [(apply task args)
+ (run-tests (first args) (extract-options (first args)) [])]]
+ (if (every? #(= % exit-success) test-results)
+ exit-success
+ exit-failure)))
(defn clean-hook [task & args]
- (cljsbuild (first args) "clean")
- (apply task args))
+ (apply task args)
+ (cleanup-files (first args) (extract-options (first args))))
(defn jar-hook [task & [project out-file filespecs]]
(apply task [project out-file (concat filespecs (get-filespecs project))]))
(hooke/add-hook #'lcompile/compile compile-hook)
+(hooke/add-hook #'ltest/test test-hook)
(hooke/add-hook #'lclean/clean clean-hook)
(hooke/add-hook #'ljar/write-jar jar-hook)
View
7 support/src/cljsbuild/clean.clj
@@ -0,0 +1,7 @@
+(ns cljsbuild.clean
+ (:require
+ [fs.core :as fs]))
+
+(defn cleanup-files [compiler-options]
+ (fs/delete (:output-to compiler-options))
+ (fs/delete-dir (:output-dir compiler-options)))
View
157 support/src/cljsbuild/compiler.clj
@@ -1,10 +1,9 @@
(ns cljsbuild.compiler
(:use
- [clojure.java.io :only [as-url resource]]
[clj-stacktrace.repl :only [pst+]]
[cljs.closure :only [build]])
(:require
- [clojure.string :as string]
+ [cljsbuild.util :as util]
[fs.core :as fs]))
(def lock (Object.))
@@ -15,45 +14,6 @@
(apply println args)
(flush)))
-(defn- join-paths [& paths]
- (apply str (interpose "/" paths)))
-
-(defn- separate [pred coll]
- (let [separated (group-by pred coll)]
- [(separated true) (separated false)]))
-
-(defmacro dofor [seq-exprs body-expr]
- `(doall (for ~seq-exprs ~body-expr)))
-
-(defn- fail [& args]
- (throw (Exception. (apply str args))))
-
-(defn- truncate-uri-path [uri n]
- (if uri
- (let [uri-path (.getPath uri)]
- (subs uri-path 0 (- (.length uri-path) n)))
- nil))
-
-(defn- ns-to-path [ns]
- (let [underscored (string/replace (str ns) #"-" "_")]
- (apply join-paths
- (string/split underscored #"\."))))
-
-(defn- filter-cljs [files types]
- (let [ext #(last (string/split % #"\."))]
- (filter #(types (ext %)) files)))
-
-(defn- find-dir-cljs [root files types]
- (for [cljs (filter-cljs files types)]
- (join-paths root cljs)))
-
-(defn- find-cljs [dir types]
- (let [iter (fs/iterate-dir dir)]
- (mapcat
- (fn [[root _ files]]
- (find-dir-cljs root files types))
- iter)))
-
(defn- elapsed [started-at]
(let [elapsed-us (- (. System (nanoTime)) started-at)]
(with-precision 2
@@ -79,117 +39,18 @@
(println-safe " Failed!")
(pst+ e))))))
-(defn- is-macro-file? [file]
- (not (neg? (.indexOf (slurp file) ";*CLJSBUILD-MACRO-FILE*;"))))
-
-; There is a little bit of madness here to share macros between Clojure
-; and ClojureScript. The latter needs a (:require-macros ...) whereas the
-; former just wants (:require ...). Thus, we have a ;*CLJSBUILD-REMOVE*;
-; conditional comment to allow different code to be used for ClojureScript files.
-(defn- filtered-crossover-file [file]
- (str
- "; DO NOT EDIT THIS FILE! IT WAS AUTOMATICALLY GENERATED BY\n"
- "; lein-cljsbuild FROM THE FOLLOWING SOURCE FILE:\n"
- "; " file "\n\n"
- (string/replace (slurp file) ";*CLJSBUILD-REMOVE*;" "")))
-
-(defn- crossover-to [cljs-path [from-parent from-resource]]
- (let [subpath (string/replace-first
- (fs/absolute-path (.getPath from-resource))
- (fs/absolute-path from-parent) "")
- to-file (fs/normalized-path
- (join-paths (fs/absolute-path cljs-path) subpath))]
- (string/replace to-file #"\.clj$" ".cljs")))
-
-(defn- recurse-resource-dir [dir]
- (if dir
- ; We can't determine the contents of a jar dir. Thus, crossover files
- ; in jars cannot be specified recursively; they have to be named file
- ; by file.
- (if (= (.getProtocol dir) "file")
- (let [files (find-cljs (.getPath dir) #{"clj"})]
- (map #(as-url (str "file:" %)) files))
- [dir])))
-
-(defn- find-crossover [crossover]
- (let [ns-path (ns-to-path crossover)
- as-dir (resource ns-path)
- dir-parent (truncate-uri-path as-dir (.length ns-path))
- recurse-dirs (recurse-resource-dir as-dir)
- ns-file-path (str ns-path ".clj")
- as-file (resource ns-file-path)
- file-parent (truncate-uri-path as-file (.length ns-file-path))
- resources (conj
- (map vector (repeat dir-parent) recurse-dirs)
- [file-parent as-file])]
- (when (empty? resources)
- (fail "Unable to find crossover: " crossover))
- resources))
-
-(defn- find-crossovers [crossovers]
- (distinct
- (remove #(nil? (second %))
- (mapcat find-crossover crossovers))))
-
-(defn- crossover-needs-update? [from-resource to-file]
- (let [exists (fs/exists? to-file)]
- (or
- (not exists)
- (and
- ; We can't determine the mtime for jar resources; they'll just
- ; be copied once and that's it.
- (= "file" (.getProtocol from-resource))
- (> (fs/mod-time (.getPath from-resource)) (fs/mod-time to-file))))))
-
-(defn- copy-crossovers [cljs-path from-resources]
- (let [to-files (map (partial crossover-to cljs-path) from-resources)]
- (doseq [dir (distinct (map fs/parent to-files))]
- (fs/mkdirs dir))
- (dofor [[[_ from-resource] to-file] (zipmap from-resources to-files)]
- (when (crossover-needs-update? from-resource to-file)
- (spit to-file (filtered-crossover-file from-resource))
- ; Mark the file as read-only, to hopefully warn the user not to modify it.
- (fs/chmod "-w" to-file)
- :updated))))
-
-(defn in-threads
- "Given a seq and a function, applies the function to each item in a different thread
-and returns a seq of the results. Launches all the threads at once."
- [f s]
- (doall (map deref (doall (map #(future (f %)) s)))))
-
-(defn run-compiler [cljs-path crossovers compiler-options watch?]
+(defn run-compiler [cljs-path crossover-path compiler-options watch?]
(loop [last-dependency-mtimes {}]
(let [output-file (:output-to compiler-options)
output-mtime (if (fs/exists? output-file) (fs/mod-time output-file) 0)
- crossover-resources (find-crossovers crossovers)
- [crossover-macros crossover-plains] (separate
- #(is-macro-file? (second %))
- crossover-resources)
- ; Filter out non-file macro namespaces, as we can't get the mtime
- ; for a namespace that comes from e.g. a jar file.
- crossover-macro-files (map #(.getPath %)
- (filter #(= "file" (.getProtocol %))
- (map second crossover-macros)))
- cljs-files (find-cljs cljs-path #{"cljs"})
- dependency-mtimes (map fs/mod-time
- (concat crossover-macro-files cljs-files))
- crossover-updated? (some #{:updated}
- (copy-crossovers cljs-path crossover-plains))]
- (when (or
- (and
- (not= last-dependency-mtimes dependency-mtimes)
- (some #(< output-mtime %) dependency-mtimes))
- crossover-updated?)
+ find-cljs #(util/find-files % #{"cljs"})
+ dependency-files (mapcat find-cljs [cljs-path crossover-path])
+ dependency-mtimes (map fs/mod-time dependency-files)]
+ (when
+ (and
+ (not= last-dependency-mtimes dependency-mtimes)
+ (some #(< output-mtime %) dependency-mtimes))
(compile-cljs cljs-path compiler-options))
(when watch?
(Thread/sleep 100)
(recur dependency-mtimes)))))
-
-(defn cleanup-files [cljs-path crossovers compiler-options]
- (fs/delete (:output-to compiler-options))
- (fs/delete-dir (:output-dir compiler-options))
- (let [from-resources (find-crossovers crossovers)
- to-files (map (partial crossover-to cljs-path) from-resources)]
- (doseq [file to-files]
- (fs/delete file))))
View
98 support/src/cljsbuild/crossover.clj
@@ -0,0 +1,98 @@
+(ns cljsbuild.crossover
+ (:use
+ [clojure.java.io :only [as-url resource]])
+ (:require
+ [cljsbuild.util :as util]
+ [clojure.string :as string]
+ [fs.core :as fs]))
+
+(defn- fail [& args]
+ (throw (Exception. (apply str args))))
+
+(defn- is-macro-file? [file]
+ (not (neg? (.indexOf (slurp file) ";*CLJSBUILD-MACRO-FILE*;"))))
+
+; There is a little bit of madness here to share macros between Clojure
+; and ClojureScript. The latter needs a (:require-macros ...) whereas the
+; former just wants (:require ...). Thus, we have a ;*CLJSBUILD-REMOVE*;
+; conditional comment to allow different code to be used for ClojureScript files.
+(defn- filtered-crossover-file [file]
+ (str
+ "; DO NOT EDIT THIS FILE! IT WAS AUTOMATICALLY GENERATED BY\n"
+ "; lein-cljsbuild FROM THE FOLLOWING SOURCE FILE:\n"
+ "; " file "\n\n"
+ (string/replace (slurp file) ";*CLJSBUILD-REMOVE*;" "")))
+
+(defn- crossover-to [crossover-path [from-parent from-resource]]
+ (let [subpath (string/replace-first
+ (fs/absolute-path (.getPath from-resource))
+ (fs/absolute-path from-parent) "")
+ to-file (fs/normalized-path
+ (util/join-paths (fs/absolute-path crossover-path) subpath))]
+ (string/replace to-file #"\.clj$" ".cljs")))
+
+(defn- recurse-resource-dir [dir]
+ (when dir
+ ; We can't determine the contents of a jar dir. Thus, crossover files
+ ; in jars cannot be specified recursively; they have to be named file
+ ; by file.
+ (if (= (.getProtocol dir) "file")
+ (let [files (util/find-files (.getPath dir) #{"clj"})]
+ (map #(as-url (str "file:" %)) files))
+ [dir])))
+
+(defn- truncate-uri-path [uri n]
+ (if uri
+ (let [uri-path (.getPath uri)]
+ (subs uri-path 0 (- (.length uri-path) n)))
+ nil))
+
+(defn- ns-to-path [ns]
+ (let [underscored (string/replace (str ns) #"-" "_")]
+ (apply util/join-paths
+ (string/split underscored #"\."))))
+
+(defn- find-crossover [crossover]
+ (let [ns-path (ns-to-path crossover)
+ as-dir (resource ns-path)
+ dir-parent (truncate-uri-path as-dir (.length ns-path))
+ recurse-dirs (recurse-resource-dir as-dir)
+ ns-file-path (str ns-path ".clj")
+ as-file (resource ns-file-path)
+ file-parent (truncate-uri-path as-file (.length ns-file-path))
+ resources (conj
+ (map vector (repeat dir-parent) recurse-dirs)
+ [file-parent as-file])]
+ (when (empty? resources)
+ (fail "Unable to find crossover: " crossover))
+ resources))
+
+(defn- find-crossovers [crossovers]
+ (distinct
+ (remove (fn [[_ file]] (or (nil? file) (is-macro-file? file)))
+ (mapcat find-crossover crossovers))))
+
+(defn- crossover-needs-update? [from-resource to-file]
+ (let [exists (fs/exists? to-file)]
+ (or
+ (not exists)
+ (and
+ ; We can't determine the mtime for jar resources; they'll just
+ ; be copied once and that's it.
+ (= "file" (.getProtocol from-resource))
+ (> (fs/mod-time (.getPath from-resource)) (fs/mod-time to-file))))))
+
+(defn copy-crossovers [crossover-path crossovers]
+ (let [from-resources (find-crossovers crossovers)
+ to-files (map (partial crossover-to crossover-path) from-resources)]
+ (doseq [dir (distinct (map fs/parent to-files))]
+ (fs/mkdirs dir))
+ (doseq [[[_ from-resource] to-file] (zipmap from-resources to-files)]
+ (when (crossover-needs-update? from-resource to-file)
+ (let [temp-file (str to-file ".tmp")]
+ ; Write a temp file and atomically rename to the real file
+ ; to prevent the compiler from reading a half-written file
+ (spit temp-file (filtered-crossover-file from-resource))
+ (fs/rename temp-file to-file)
+ ; Mark the file as read-only, to hopefully warn the user not to modify it.
+ (fs/chmod "-w" to-file))))))
View
15 support/src/cljsbuild/repl/listen.clj
@@ -1,10 +1,10 @@
(ns cljsbuild.repl.listen
(:require
- [clojure.string :as string]
- [clojure.java.shell :as shell]
[cljs.repl :as repl]
- [cljs.repl.browser :as browser])
- (:import (java.io InputStreamReader OutputStreamWriter)))
+ [cljs.repl.browser :as browser]
+ [clojure.string :as string])
+ (:import
+ (java.io InputStreamReader OutputStreamWriter)))
(defn run-repl-listen [port output-dir]
(let [env (browser/repl-env :port (Integer. port) :working-dir output-dir)]
@@ -18,12 +18,12 @@
(defn- start-bg-command [command]
(Thread/sleep 1000)
(let [process (.exec (Runtime/getRuntime) (into-array command))]
- (.close (.getOutputStream process))
+ ; TODO: Maybe do something better with output; just stream it out?
(with-open [stdout (.getInputStream process)
stderr (.getErrorStream process)]
(let [[out err]
- (for [stream [stdout stderr]]
- (apply str (map char (stream-seq (InputStreamReader. stream "UTF-8")))))]
+ (for [stream [stdout stderr]]
+ (apply str (map char (stream-seq (InputStreamReader. stream "UTF-8")))))]
{:process process :out out :err err}))))
(defn run-repl-launch [port output-dir command]
@@ -45,5 +45,6 @@
(println (header "Standard error from launched command:"))
(println err)))
(catch Exception e
+ ; TODO: destroy the process if it was started!
(binding [*out* *err*]
(println "Launching command failed:" e))))))
View
35 support/src/cljsbuild/test.clj
@@ -0,0 +1,35 @@
+(ns cljsbuild.test
+ (:require
+ [clojure.java.io :as io])
+ (:import
+ (java.io OutputStreamWriter)))
+
+(defn- pump [reader out]
+ (let [buffer (make-array Character/TYPE 1000)]
+ (loop [len (.read reader buffer)]
+ (when-not (neg? len)
+ (.write out buffer 0 len)
+ (.flush out)
+ (Thread/sleep 100)
+ (recur (.read reader buffer))))))
+
+(defn sh [command]
+ (let [process (.exec (Runtime/getRuntime) (into-array command))]
+ (with-open [out (io/reader (.getInputStream process))
+ err (io/reader (.getErrorStream process))]
+ (let [pump-out (doto (Thread. #(pump out *out*)) .start)
+ pump-err (doto (Thread. #(pump err *err*)) .start)]
+ (.join pump-out)
+ (.join pump-err))
+ (.waitFor process)
+ (.exitValue process))))
+
+(defmacro dofor [seq-exprs body-expr]
+ `(doall (for ~seq-exprs ~body-expr)))
+
+(defn run-tests [test-commands]
+ (let [success (every? #(= % 0)
+ (dofor [test-command test-commands]
+ (sh test-command)))]
+ (when (not success)
+ (throw (Exception. "Test failed.")))))
View
37 support/src/cljsbuild/util.clj
@@ -0,0 +1,37 @@
+(ns cljsbuild.util
+ (:require
+ [clojure.string :as string]
+ [fs.core :as fs]))
+
+(defn join-paths [& paths]
+ (apply str (interpose "/" paths)))
+
+(defn filter-by-ext [files types]
+ (let [ext #(last (string/split % #"\."))]
+ (filter #(types (ext %)) files)))
+
+(defn find-dir-files [root files types]
+ (for [files (filter-by-ext files types)]
+ (join-paths root files)))
+
+(defn find-files [dir types]
+ (let [iter (fs/iterate-dir dir)]
+ (mapcat
+ (fn [[root _ files]]
+ (find-dir-files root files types))
+ iter)))
+
+(defn in-threads
+ "Given a seq and a function, applies the function to each item in a different thread
+and returns a seq of the results. Launches all the threads at once."
+ [f s]
+ (doall (map deref (doall (map #(future (f %)) s)))))
+
+(defn once-every [ms desc f]
+ (future
+ (while true
+ (Thread/sleep ms)
+ (try
+ (f)
+ (catch Exception e
+ (println (str "Error " desc ": " e)))))))

0 comments on commit b8dbbb6

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