diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e04714b --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/target +/classes +/checkouts +pom.xml +pom.xml.asc +*.jar +*.class +/.lein-* +/.nrepl-port diff --git a/README.md b/README.md new file mode 100644 index 0000000..d6c408d --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# lein-watch + +A Leiningen plugin to watch directories and run tasks automatically. + +## Usage + +Put `[lein-watch "0.1.0-SNAPSHOT"]` into the `:plugins` vector of your project.clj and +add :watch configuration to your project.clj. + +Example configuration (run compile task when .clj files changes) : + + (defproject sample-project + ... + :watch { + :rate 500 ;; check file every 500ms (use 'watchtower' intanally) + :watches { + :compile { + :watch-dirs ["src"] + :file-patterns [#"\.clj"] + :tasks ["compile"]}}} + ...) + +and just run watch task + + $ lein watch + +See [sample-project](http://github.com/runoshun/lein-watch/sample-project) for more complex usage. + +## License + +Distributed under the MIT License. + +Copyright © 2014 runoshun diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..df8aa04 --- /dev/null +++ b/project.clj @@ -0,0 +1,7 @@ +(defproject lein-watch "0.0.1" + :description "A Leiningen plugin to watch directories and run tasks automatically." + :url "http://github.com/runoshun/lein-watch" + :license {:name "MIT" + :url "http://opensource.org/licenses/MIT"} + :dependencies [[watchtower "0.1.1"]] + :eval-in-leiningen true) diff --git a/sample-project/.gitignore b/sample-project/.gitignore new file mode 100644 index 0000000..e04714b --- /dev/null +++ b/sample-project/.gitignore @@ -0,0 +1,9 @@ +/target +/classes +/checkouts +pom.xml +pom.xml.asc +*.jar +*.class +/.lein-* +/.nrepl-port diff --git a/sample-project/README.md b/sample-project/README.md new file mode 100644 index 0000000..06fea3c --- /dev/null +++ b/sample-project/README.md @@ -0,0 +1,9 @@ +# sample-project for lein-watch plugin + +See `project.clj` + +## License + +Distributed under the MIT License. + +Copyright © 2014 FIXME diff --git a/sample-project/doc/intro.md b/sample-project/doc/intro.md new file mode 100644 index 0000000..138fbda --- /dev/null +++ b/sample-project/doc/intro.md @@ -0,0 +1,3 @@ +# Introduction to test-watch + +TODO: write [great documentation](http://jacobian.org/writing/great-documentation/what-to-write/) diff --git a/sample-project/project.clj b/sample-project/project.clj new file mode 100644 index 0000000..f2d0bb1 --- /dev/null +++ b/sample-project/project.clj @@ -0,0 +1,57 @@ +(defproject watch-sample "0.1.0-SNAPSHOT" + :license {:name "Eclipse Public License" + :url "http://www.eclipse.org/legal/epl-v10.html"} + + ;; use 'lein-watch' plugin + :plugins [[lein-watch "0.0.1"] + [lein-garden "0.1.8"]] + :dependencies [[org.clojure/clojure "1.5.1"]] + + ;; profiles used in watch tasks + :profiles {:garden {:source-paths ["src-garden"] + :dependencies [[garden "1.1.5"]]} + :hiccup {:source-paths ["src-hiccup"] + :dependencies [[hiccup "1.0.5"]]}} + + :garden { + :builds [{:id "screen" + :stylesheet test-watch.css/screen + :compiler {:output-to "resources/public/screen.css" + :pretty-print true}}]} + + ;; configuration for 'lein-watch' + :watch { + ;; polling rate in 'ms' (it's directory passed to 'watchtower') + :rate 300 + + ;; watcher definition + :watchers { + ;; run 'lein garden once' when *.clj file under the 'src-garden' changed. + :garden {;; :watch-dirs (required) : vector of string + ;; Put directories that you want watch. + :watch-dirs ["src-garden"] + + ;; :file-patterns (optional) : vector of java.util.regex.Pattern + ;; If file name that changed is matched this patterns, tasks are executed. + ;; otherwise not executed. + ;; default : [#".*"]. + :file-patterns [#"\.clj"] + + ;; :tasks (required) : vector of (string|symbol) + ;; Put tasks that you want executed when file changed. + ;; If a value is string, it is evaluated as a leiningen task. + ;; If a value is symbol, it is called as a function in project context and + ;; is passed changed file as argument. + :tasks ["garden once"] + + ;; profiles (optional) : vector of keyword + ;; profile names used when executing tasks. + ;; default : [] + :profiles [:garden]} + ;; call 'test-watch.hiccup/generate' (defined in 'src-hiccup/test_watch/hiccup.clj') + ;; when *.clj file under the 'src-hiccup' changed. + :hiccup {:watch-dirs ["src-hiccup"] + :profiles [:hiccup] + :file-patterns [#"\.clj"] + :tasks [test-watch.hiccup/generate]}}}) + diff --git a/sample-project/resources/public/index.html b/sample-project/resources/public/index.html new file mode 100644 index 0000000..eff07d8 --- /dev/null +++ b/sample-project/resources/public/index.html @@ -0,0 +1 @@ +
Hello world
\ No newline at end of file diff --git a/sample-project/resources/public/screen.css b/sample-project/resources/public/screen.css new file mode 100644 index 0000000..00fb820 --- /dev/null +++ b/sample-project/resources/public/screen.css @@ -0,0 +1,5 @@ +body { + line-height: 1.5; + font-size: 20px; + font-family: sans-serif; +} \ No newline at end of file diff --git a/sample-project/src-garden/test_watch/css.clj b/sample-project/src-garden/test_watch/css.clj new file mode 100644 index 0000000..6501207 --- /dev/null +++ b/sample-project/src-garden/test_watch/css.clj @@ -0,0 +1,9 @@ +(ns test-watch.css + (:require [garden.def :refer [defstylesheet defstyles]] + [garden.units :refer [px]])) + +(defstyles screen + [:body + {:font-family "sans-serif" + :font-size (px 20) + :line-height 1.5}]) diff --git a/sample-project/src-hiccup/test_watch/hiccup.clj b/sample-project/src-hiccup/test_watch/hiccup.clj new file mode 100644 index 0000000..0d90af0 --- /dev/null +++ b/sample-project/src-hiccup/test_watch/hiccup.clj @@ -0,0 +1,9 @@ +(ns test-watch.hiccup + (:require [hiccup.core :refer [html]])) + +(defn generate [& _] + (spit "resources/public/index.html" + (html [:html + [:head [:link {:rel "stylesheet" :type "text/css" :href "screen.css"}]] + [:body [:div [:span "Hello world"]]]]))) + diff --git a/sample-project/src/test_watch/core.clj b/sample-project/src/test_watch/core.clj new file mode 100644 index 0000000..01bc78b --- /dev/null +++ b/sample-project/src/test_watch/core.clj @@ -0,0 +1,6 @@ +(ns test-watch.core) + +(defn foo + "I don't do a whole lot." + [x] + (println x "Hello, World!")) diff --git a/sample-project/test/test_watch/core_test.clj b/sample-project/test/test_watch/core_test.clj new file mode 100644 index 0000000..de70f6e --- /dev/null +++ b/sample-project/test/test_watch/core_test.clj @@ -0,0 +1,7 @@ +(ns test-watch.core-test + (:require [clojure.test :refer :all] + [test-watch.core :refer :all])) + +(deftest a-test + (testing "FIXME, I fail." + (is (= 0 1)))) diff --git a/src/leiningen/watch.clj b/src/leiningen/watch.clj new file mode 100644 index 0000000..5809a41 --- /dev/null +++ b/src/leiningen/watch.clj @@ -0,0 +1,111 @@ +(ns leiningen.watch + (:require [clojure.string :as string] + [clojure.java.io :as io] + [watchtower.core :as wt] + [leiningen.core.eval :as lein-eval] + [leiningen.core.main :as lein-main] + [leiningen.core.project :as lein-project])) + +(def ^:private default-watcher-options + {:file-patterns [#".*"] + :profiles []}) + +(def ^:private default-global-settings + {:rate 300}) + +(defn- non-nils [coll] (remove nil? coll)) + +(defn- ensure-regex [pat] + (if (instance? java.util.regex.Pattern pat) + pat + (re-pattern pat))) + +(defn- match? [patterns file] + (boolean (first (non-nils (map #(re-find % (str file)) + (map ensure-regex patterns)))))) + +(defn- child? [parent child] + (not (.isAbsolute (.relativize (.toURI (io/file parent)) + (.toURI (io/file child)))))) + +(defn- make-route [dir patterns group] + (fn [file] + (if (and (child? dir file) (match? patterns file)) + group + nil))) + +(defn- make-router [watchers] + (fn [file] + (let [routes (mapcat (fn [group] + (let [options (group watchers) + patterns (-> options :file-patterns) + dirs (-> options :watch-dirs)] + (map (fn [dir] (make-route dir patterns group)) dirs))) + (keys watchers))] + (first (non-nils (map #(% file) routes)))))) + +(defn- separate-ns [sym] + (let [syms (string/split (str sym) #"/")] + (if (= 2 (count syms)) + (symbol (first syms)) + nil))) + +(defn- run-tasks [project watcher file] + (let [project (lein-project/merge-profiles project (:profiles watcher)) + tasks (:tasks watcher)] + (println (str "[lein-watch] file changed : " file)) + (println (str "[lein-watch] run-tasks : " tasks)) + (doseq [task tasks] + (cond + (string? task) (lein-main/resolve-and-apply + project + (string/split (string/replace-first task #"%f" (str file)) #"\s+")) + (symbol task) (let [ns (separate-ns task) + file-str (.getAbsolutePath file) + form (if ns + `(do (require '~ns) (~task ~file-str)) + `(~task ~file-str))] + (lein-eval/eval-in-project project form)))))) + +(defn- ensure-slash [dir] + (if-not (.endsWith dir "/") + (str dir "/") + dir)) + +(defn watch + "Watch directories and run tasks when file changed." + [project & args] + (let [settings (:watch project) + settings (merge default-global-settings settings) + watchers (:watchers settings) + watchers (reduce (fn [m [k v]] (assoc m k (merge default-watcher-options v))) {} watchers) + router (make-router watchers) + dirs (map ensure-slash (mapcat :watch-dirs (vals watchers))) + process-event (fn [files] + (doseq [file files] + (run-tasks project (watchers (router file)) file)))] + (if watchers + (deref + (wt/watcher dirs + (wt/rate (:rate settings)) + (wt/on-change process-event))) + (println "no watcher found.")))) + +(comment +(do + (def project + {:watch + {:watchers + {:garden {:watch-dirs ["src-garden"] + :file-patterns [#".*"] + :tasks ["garden once"]} + :hiccup {:watch-dirs ["src-hiccup"] + :file-patterns [#"\.txt"] + :tasks ["hiccpu once"]}}}}) + + (def router (make-router (-> project :watch :watchers))) + (assert (= (router (io/file "src-garden/foo")) :garden)) + (assert (= (router "src-hiccup/bar") nil)) + (assert (= (router "src-hiccup/bar.txt") :hiccup)) + +))