Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Adding crossover support and examples.

  • Loading branch information...
commit 91e8506585e5a43812c73e1fab747a40a9cda231 1 parent 4128668
@emezeske authored
View
4 .gitignore
@@ -3,3 +3,7 @@ pom.xml
lib
classes/
lein-cljsbuild-*.*.*
+example-projects/*/resources
+example-projects/*/classes
+example-projects/*/lib
+example-projects/*/src-cljs/example/crossover
View
153 README.md
@@ -7,9 +7,12 @@ that your project can depend on a specific version of lein-cljsbuild, fetch
it via `lein deps`, and you don't have to install any special executables into
your `PATH`.
+Also, this plugin has built-in support for seamlessly sharing code between
+your Clojure server-side project and your ClojureScript client-side project.
+
[1]: https://github.com/ibdknox/cljs-watch
-## Installation
+## Installation
You can install the plugin via lein:
@@ -19,7 +22,7 @@ Or by adding lein-cljs to your `project.clj` file in the `:dev-dependencies`
section:
```clojure
-(defproject my-thingie "1.2.3"
+(defproject lein-cljsbuild-example "1.2.3"
:dev-dependencies [[emezeske/lein-cljsbuild "0.0.1"]])
```
@@ -27,13 +30,18 @@ Make sure you pull down the jar file:
$ lein deps
+## Just Give Me a Damned Example Already!
+
+See the `example-projects` directory for a couple of simple examples
+of how to use lein-cljsbuild.
+
## Configuration
The lein-cljsbuild configuration is specified under the `:cljsbuild` section
-of your `project.clj` file:
+of your `project.clj` file. A simple project might look like this:
```clojure
-(defproject my-thingie "1.2.3"
+(defproject lein-cljsbuild-example "1.2.3"
:dev-dependencies [[emezeske/lein-cljsbuild "0.0.1"]]
:cljsbuild {
; The path to the top-level ClojureScript source directory:
@@ -59,6 +67,143 @@ avoids the time-consuming JVM startup for each build:
$ lein cljsbuild auto
+## Sharing Code Between Clojure and ClojureScript
+
+Sharing code with lein-cljsbuild is accomplished via the configuration
+of "crossovers". A crossover specifies a directory in your Clojure project,
+the content of which should be copied into your ClojureScript project. The
+files in the Clojure directory will be monitored and copied over when they are
+modified. Of course, remember that since the files will be used by both Clojure
+and ClojureScript, they will need to only use the subset of features provided by
+both languages.
+
+Assuming that your top-level directory structure looked something like this:
+
+<pre>
+├── src-clj
+│   └── example
+│   ├── core.clj
+│   ├── something.clj
+│   └── crossover
+│      ├── some_stuff.clj
+│      └── some_other_stuff.clj
+└── src-cljs
+    └── example
+    ├── core.cljs
+    ├── whatever.cljs
+    └── util.cljs
+</pre>
+
+And your `project.clj` file looked like this:
+
+```clojure
+(defproject lein-cljsbuild-example "1.2.3"
+ :dev-dependencies [[emezeske/lein-cljsbuild "0.0.1"]]
+ :source-path "src-clj"
+ :cljsbuild {
+ :source-dir "src-cljs"
+ :output-file "war/javascripts/main.js"
+ :optimizations :whitespace
+ :pretty-print true
+ ; Each entry in the :crossovers vector describes a directory
+ ; containing Clojure code that is meant to be used with the
+ ; ClojureScript code as well. Files in :from-dir are copied
+ ; into :to-dir whenever they are modified.
+ :crossovers [{:from-dir "src-clj/example/crossover"
+ :to-dir "src-cljs/example/crossover"}]})
+```
+
+Then lein-cljsbuild would copy files from `src-clj/example/crossover`
+to `src-cljs/example/crossover`, and you'd end up with this:
+
+<pre>
+├── src-clj
+│   └── example
+│   ├── a_file.clj
+│   ├── core.clj
+│   └── crossover
+│      ├── some_stuff.clj
+│      └── some_other_stuff.clj
+└── src-cljs
+    └── example
+    ├── a_different_file.cljs
+    ├── crossover
+    │   ├── some_stuff.cljs
+    │   └── some_other_stuff.cljs
+    ├── whatever.cljs
+    └── util.cljs
+</pre>
+
+With this setup, you would probably want to add `src-cljs/example/crossover`
+to your `.gitignore` file (or equivalent), as its contents are updated automatically
+by lein-cljsbuild.
+
+## Sharing Macros Between Clojure and ClojureScript
+
+In ClojureScript, macros are still written in Clojure, and can not be written
+in the same file as actual ClojureScript code. Also, to use them in a ClojureScript
+namespace, they must be required via `:require-macros` rather than the usual `:require`.
+
+This makes using the crossover feature to share macros between Clojure and ClojureScript
+a bit difficult, but lein-cljsbuild has some special constructs to make it possible.
+
+Three things need to be done to use lein-cljsbuild to share macros.
+
+### 1. Keep Macros in Separate Files
+
+These examples assume that your project uses the `src-clj/example/crossover`
+directory, and that all of the macros are in a file called
+`src-clj/example/crossover/macros.clj`.
+
+### 2. Tell lein-cljsbuild Which Files Contain Macros
+
+Add this magical comment to any crossover files that contain macros:
+
+```clojure
+;*CLJSBUILD-MACRO-FILE*;
+```
+
+This tells lein-cljsbuild to keep the `.clj` file extension when copying
+the files into the ClojureScript directory, instead of changing it to `.cljs`
+as usual.
+
+### 3. Use Black Magic to Require Macros Specially
+
+In any crossover Clojure file, lein-cljsbuild will automatically erase the
+following string (if it appears):
+
+```clojure
+;*CLJSBUILD-REMOVE*;
+```
+
+This magic can be used to generate a `ns` statement that will work in both
+Clojure and ClojureScript:
+
+```clojure
+(ns example.crossover.some_stuff
+ (:require;*CLJSBUILD-REMOVE*;-macros
+ [example.crossover.macros :as macros]))
+```
+
+Thus, after removing comments, Clojure will see:
+
+```clojure
+(ns example.crossover.some_stuff
+ (:require
+ [example.crossover.macros :as macros]))
+```
+
+However, lein-cljsbuild will remove the `;*CLJSBUILD-REMOVE*;` string entirely,
+before copying the file. Thus, ClojureScript will see:
+
+```clojure
+(ns example.crossover.some_stuff
+ (:require-macros
+ [example.crossover.macros :as macros]))
+```
+
+And thus the macros can be shared.
+
## License
Source Copyright © Evan Mezeske, 2011.
View
18 example-projects/advanced/README.md
@@ -0,0 +1,18 @@
+This is an example web application that uses [Ring][1], [Compojure][2],
+and [lein-cljsbuild][3]. It demonstrates the use of lein-cljsbuild to
+build ClojureScript into JavaScript. It also shows how to share code
+between Clojure and ClojureScript, including macros.
+
+To play around with this example project, you will first need
+[Leiningen][4] installed.
+
+Set up and start the server like this:
+
+ lein deps
+ lein cljsbuild once
+ lein ring server
+
+[1]: https://github.com/mmcgrana/ring
+[2]: https://github.com/weavejester/compojure
+[3]: https://github.com/emezeske/lein-cljsbuild
+[4]: https://github.com/technomancy/leiningen
View
15 example-projects/advanced/project.clj
@@ -0,0 +1,15 @@
+(defproject cljsbuild-example-advanced "0.0.1"
+ :description "An advanced example of how to use lein-cljsbuild"
+ :source-path "src-clj"
+ :dependencies [[org.clojure/clojure "1.3.0"]
+ [compojure "0.6.5"]
+ [hiccup "0.3.7"]]
+ :dev-dependencies [[emezeske/lein-cljsbuild "0.0.1"]
+ [lein-ring "0.5.0"]]
+ :cljsbuild {:source-dir "src-cljs"
+ :crossovers [{:from-dir "src-clj/example/crossover"
+ :to-dir "src-cljs/example/crossover"}]
+ :output-file "resources/public/js/main.js"
+ :optimizations :whitespace
+ :pretty-print true}
+ :ring {:handler example.routes/app})
View
6 example-projects/advanced/src-clj/example/crossover/macros.clj
@@ -0,0 +1,6 @@
+;*CLJSBUILD-MACRO-FILE*;
+
+(ns example.crossover.macros)
+
+(defmacro reverse-eval [form]
+ (reverse form))
View
7 example-projects/advanced/src-clj/example/crossover/shared.clj
@@ -0,0 +1,7 @@
+(ns example.crossover.shared
+ (:require;*CLJSBUILD-REMOVE*;-macros
+ [example.crossover.macros :as macros]))
+
+(defn make-example-text []
+ (macros/reverse-eval
+ ("code" "shared " "from the " "Hello " str)))
View
16 example-projects/advanced/src-clj/example/routes.clj
@@ -0,0 +1,16 @@
+(ns example.routes
+ (:use compojure.core
+ example.views
+ [hiccup.middleware :only (wrap-base-url)])
+ (:require [compojure.route :as route]
+ [compojure.handler :as handler]
+ [compojure.response :as response]))
+
+(defroutes main-routes
+ (GET "/" [] (index-page))
+ (route/resources "/")
+ (route/not-found "Page not found"))
+
+(def app
+ (-> (handler/site main-routes)
+ (wrap-base-url)))
View
13 example-projects/advanced/src-clj/example/views.clj
@@ -0,0 +1,13 @@
+(ns example.views
+ (:require
+ [example.crossover.shared :as shared])
+ (:use
+ [hiccup core page-helpers]))
+
+(defn index-page []
+ (html5
+ [:head
+ [:title (shared/make-example-text)]
+ (include-js "/js/main.js")]
+ [:body
+ [:h1 (shared/make-example-text)]]))
View
5 example-projects/advanced/src-cljs/example/hello.cljs
@@ -0,0 +1,5 @@
+(ns example.hello
+ (:require
+ [example.crossover.shared :as shared]))
+
+(js/alert (shared/make-example-text))
17 example-projects/simple/README.md
@@ -0,0 +1,17 @@
+This is an example web application that uses [Ring][1], [Compojure][2],
+and [lein-cljsbuild][3]. It demonstrates the use of lein-cljsbuild to
+build ClojureScript into JavaScript.
+
+To play around with this example project, you will first need
+[Leiningen][4] installed.
+
+Set up and start the server like this:
+
+ lein deps
+ lein cljsbuild once
+ lein ring server
+
+[1]: https://github.com/mmcgrana/ring
+[2]: https://github.com/weavejester/compojure
+[3]: https://github.com/emezeske/lein-cljsbuild
+[4]: https://github.com/technomancy/leiningen
View
13 example-projects/simple/project.clj
@@ -0,0 +1,13 @@
+(defproject cljsbuild-example-simple "0.0.1"
+ :description "A simple example of how to use lein-cljsbuild"
+ :source-path "src-clj"
+ :dependencies [[org.clojure/clojure "1.3.0"]
+ [compojure "0.6.5"]
+ [hiccup "0.3.7"]]
+ :dev-dependencies [[emezeske/lein-cljsbuild "0.0.1"]
+ [lein-ring "0.5.0"]]
+ :cljsbuild {:source-dir "src-cljs"
+ :output-file "resources/public/js/main.js"
+ :optimizations :whitespace
+ :pretty-print true}
+ :ring {:handler example.routes/app})
View
16 example-projects/simple/src-clj/example/routes.clj
@@ -0,0 +1,16 @@
+(ns example.routes
+ (:use compojure.core
+ example.views
+ [hiccup.middleware :only (wrap-base-url)])
+ (:require [compojure.route :as route]
+ [compojure.handler :as handler]
+ [compojure.response :as response]))
+
+(defroutes main-routes
+ (GET "/" [] (index-page))
+ (route/resources "/")
+ (route/not-found "Page not found"))
+
+(def app
+ (-> (handler/site main-routes)
+ (wrap-base-url)))
View
10 example-projects/simple/src-clj/example/views.clj
@@ -0,0 +1,10 @@
+(ns example.views
+ (:use [hiccup core page-helpers]))
+
+(defn index-page []
+ (html5
+ [:head
+ [:title "Hello World"]
+ (include-js "/js/main.js")]
+ [:body
+ [:h1 "Hello World"]]))
View
3  example-projects/simple/src-cljs/example/hello.cljs
@@ -0,0 +1,3 @@
+(ns example.hello)
+
+(js/alert "Hello from ClojureScript!")
View
94 src/cljsbuild/core.clj
@@ -1,30 +1,25 @@
(ns cljsbuild.core
(:require
- [clojure.java.io :as io]
[clojure.string :as string]
+ [clojure.set :as cset]
[clj-stacktrace.repl :as st]
[fs :as fs]
[cljs.closure :as cljsc]))
(def tmpdir "/tmp/clojurescript-output")
-(defn- get-mtime [file]
- (.lastModified (io/file file)))
-
-(defn- filter-cljs [files]
+(defn- filter-cljs [files types]
(let [ext #(last (string/split % #"\."))]
- ; Need to return *.clj as well as *.cljs because ClojureScript
- ; macros are written in Clojure.
- (filter #(#{"clj" "cljs"} (ext %) ) files)))
+ (filter #(types (ext %)) files)))
-(defn- find-dir-cljs [root files]
- (for [cljs (filter-cljs files)] (fs/join root cljs)))
+(defn- find-dir-cljs [root files types]
+ (for [cljs (filter-cljs files types)] (fs/join root cljs)))
-(defn- find-cljs [dir]
+(defn- find-cljs [dir types]
(let [iter (fs/iterdir dir)]
(mapcat
(fn [[root _ files]]
- (find-dir-cljs root files))
+ (find-dir-cljs root files types))
iter)))
(defn- elapsed [started-at]
@@ -35,8 +30,9 @@
(defn- compile-cljs [source-dir output-file optimizations pretty?]
(print (str "Compiling " output-file " from " source-dir "..."))
(flush)
- (when (.exists (io/file tmpdir))
+ (when (fs/exists? tmpdir)
(fs/delete tmpdir))
+ (fs/mkdirs (fs/dirname output-file))
(let [started-at (. System (nanoTime))]
(try
(cljsc/build
@@ -50,15 +46,73 @@
(println " Failed!")
(st/pst+ e)))))
-(defn run-compiler [source-dir output-file optimizations pretty? watch?]
+(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 [from-dir to-dir from-file]
+ (let [abspath (fs/abspath from-file)
+ subpath (string/replace-first
+ (fs/abspath from-file)
+ (fs/abspath from-dir) "")
+ to-file (fs/normpath
+ (fs/join (fs/abspath to-dir) subpath))]
+ (if (is-macro-file? from-file)
+ to-file
+ (string/replace to-file #"\.clj$" ".cljs"))))
+
+(defn- delete-extraneous-files [expected-files to-dir]
+ (let [real-files (map fs/abspath
+ (find-cljs to-dir #{"clj" "cljs"}))
+ extraneous-files (cset/difference
+ (set real-files)
+ (set expected-files))]
+ (for [file extraneous-files]
+ (do
+ (fs/delete file)
+ :updated))))
+
+(defn- copy-crossovers [crossovers]
+ (for [{:keys [from-dir to-dir]} crossovers]
+ (let [from-files (find-cljs from-dir #{"clj"})
+ to-files (map (partial crossover-to from-dir to-dir) from-files)]
+ (fs/mkdirs to-dir)
+ (concat
+ (delete-extraneous-files to-files to-dir)
+ (for [[from-file to-file] (zipmap from-files to-files)]
+ (when
+ (or
+ (not (fs/exists? to-file))
+ (> (fs/mtime from-file) (fs/mtime to-file)))
+ (do
+ (spit to-file (filtered-crossover-file from-file))
+ :updated)))))))
+
+(defn run-compiler [source-dir crossovers output-file optimizations pretty? watch?]
(println "Compiler started.")
(loop [last-input-mtimes {}]
- (let [output-mtime (get-mtime output-file)
- input-files (find-cljs source-dir)
- input-mtimes (map get-mtime input-files)]
- (when (and
- (not= input-mtimes last-input-mtimes)
- (some #(< output-mtime %) input-mtimes))
+ (let [output-mtime (if (fs/exists? output-file) (fs/mtime output-file) 0)
+ ; Need to return *.clj as well as *.cljs because ClojureScript
+ ; macros are written in Clojure.
+ input-files (find-cljs source-dir #{"clj" "cljs"})
+ input-mtimes (map fs/mtime input-files)
+ crossover-updated? (some #{:updated}
+ (flatten (copy-crossovers crossovers)))]
+ (when (or
+ (and
+ (not= last-input-mtimes input-mtimes)
+ (some #(< output-mtime %) input-mtimes))
+ crossover-updated?)
(compile-cljs source-dir output-file optimizations pretty?))
(when watch?
(Thread/sleep 100)
View
3  src/leiningen/cljsbuild.clj
@@ -12,15 +12,18 @@
"once" false
"auto" true)
defaults {:source-dir "src-cljs"
+ :crossovers []
:output-file "main.js"
:optimizations "whitespace"
:pretty-print true}
options (merge defaults (:cljsbuild project))]
(eval-in-project
{:local-repo-classpath true
+ :extra-classpath-dirs [(:source-dir options)]
:dependencies (:dependencies project)}
`(cljsbuild.core/run-compiler
~(:source-dir options)
+ ~(:crossovers options)
~(:output-file options)
(keyword ~(:optimizations options))
~(:pretty-print options)
Please sign in to comment.
Something went wrong with that request. Please try again.