├── .travis.yml
├── .gitignore
├── example
├── .gitignore
├── src
│ ├── cljs
│ │ └── hello
│ │ │ └── core.cljs
│ └── clj
│ │ └── hello
│ │ └── core.clj
├── resources
│ └── public
│ │ └── index-dev.html
├── project.clj
└── README.md
├── project.clj
├── README.md
└── src
└── lively
└── core.cljs
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: clojure
2 | script: ./build.sh
3 |
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | pom.xml
5 | pom.xml.asc
6 | *.jar
7 | *.class
8 | /.lein-*
9 | /.nrepl-port
10 | /.idea
11 | *.iml
12 | cljsbuild.out
13 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | pom.xml
5 | pom.xml.asc
6 | *.jar
7 | *.class
8 | /.lein-*
9 | /.nrepl-port
10 | /.idea
11 | *.iml
12 | /resources/public/js
13 |
--------------------------------------------------------------------------------
/example/src/cljs/hello/core.cljs:
--------------------------------------------------------------------------------
1 | (ns hello.core
2 | (:require [lively.core :as lively]))
3 |
4 | (defn greet []
5 | (js/alert "Hello!"))
6 |
7 | (lively/start "/js/hello.js" {:on-reload #(.log js/console "Reloaded!")})
8 |
--------------------------------------------------------------------------------
/example/src/clj/hello/core.clj:
--------------------------------------------------------------------------------
1 | (ns hello.core
2 | (:require [compojure.core :refer [defroutes]]
3 | [compojure.handler :as handler]
4 | [compojure.route :as route]))
5 |
6 | (defroutes app-routes
7 | (route/resources "/"))
8 |
9 | (def app
10 | (handler/site app-routes))
11 |
--------------------------------------------------------------------------------
/example/resources/public/index-dev.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Lively example
4 |
5 |
6 | Lively example
7 |
8 |
9 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject lively "0.2.2-SNAPSHOT"
2 | :description "ClojureScript live coding with ease"
3 | :url "http://github.com/immoh/lively"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 | :dependencies [[org.clojure/core.async "0.1.346.0-17112a-alpha"]]
7 | :profiles {:dev {:dependencies [[org.clojure/clojure "1.6.0"]
8 | [org.clojure/clojurescript "0.0-3058"]]
9 | :plugins [[lein-cljsbuild "1.0.4"]]}}
10 | :cljsbuild {:builds {:main {:source-paths ["src"]
11 | :compiler {:optimizations :none}}}})
12 |
--------------------------------------------------------------------------------
/example/project.clj:
--------------------------------------------------------------------------------
1 | (defproject hello "0.1.0-SNAPSHOT"
2 | :min-lein-version "2.0.0"
3 | :source-paths ["src/clj"]
4 | :dependencies [[org.clojure/clojure "1.6.0"]
5 | [org.clojure/clojurescript "0.0-2850"]
6 | [compojure "1.2.0"]
7 | [lively "0.2.0"]]
8 | :plugins [[lein-ring "0.8.12"]
9 | [lein-cljsbuild "1.0.3"]]
10 | :ring {:handler hello.core/app
11 | :browser-uri "/index-dev.html"}
12 | :cljsbuild {:builds {:main {:source-paths ["src/cljs"]
13 | :compiler {:output-to "resources/public/js/hello.js"
14 | :output-dir "resources/public/js/out"
15 | :optimizations :none}}}})
16 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # Lively example project
2 |
3 | This is a minimal example project showing [lively](http://github.com/immoh/lively) in action.
4 |
5 |
6 | ## Instructions
7 |
8 |
9 | 1. **Start ClojureScript compiler**
10 |
11 | In terminal:
12 |
13 | ```
14 | lein cljsbuild auto
15 | ```
16 |
17 | 2. **Start the server**
18 |
19 | In another terminal:
20 |
21 | ```
22 | lein ring server
23 | ```
24 |
25 | This should automatically open a web browser, if not, navigate to
26 | [http://localhost:3000/index-dev.html](http://localhost:3000/index-dev.html).
27 |
28 |
29 | 3. **Start hacking**
30 |
31 | Open your favorite editor and start editing ClojureScript files in [src/cljs](http://github.com/immoh/lively/blob/0.1.0/example/src/cljs). Your changes
32 | are automatically reflected in the browser with no need to reload the page!
33 |
34 | For example, try editing the greeting text in [greet](http://github.com/immoh/lively/blob/0.1.0/example/src/cljs/hello/core.cljs#L5) function.
35 | After saving the file, click on the greet button.
36 |
37 |
38 | Copyright © 2014 Immo Heikkinen
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lively [](https://travis-ci.org/immoh/lively)
2 |
3 | ClojureScript live coding with ease
4 |
5 | Lively monitors compiled JavaScript files for changes and reloads them when they
6 | change, so that you don't have to. This creates a development environment where you can edit ClojureScript code and see the changes
7 | immediately in your browser without needing to refresh the page.
8 |
9 | Lively does not compile ClojureScript. (I recommend using [lein-cljsbuild](https://github.com/emezeske/lein-cljsbuild) for that.)
10 |
11 | Lively does not provide web server for serving JavaScript files.
12 |
13 |
14 | ## Installation
15 |
16 | Add the following Leiningen dependency:
17 |
18 | ```clojure
19 | [lively "0.2.1"]
20 | ```
21 |
22 | ## Usage
23 |
24 | Prerequisites:
25 |
26 | * ClojureScript version 0.0-2202 or newer
27 | * ClojureScript is compiled with `:optimizations` set to `:none`
28 | * JavaScript files are loaded over HTTP
29 |
30 | Simply include call to `lively/start` somewhere in your ClojureScript codebase, passing the location of the main JavaScript file
31 | (this is the value of `src` attribute of the `script` tag loading the file in your HTML markup):
32 |
33 | ```clojure
34 | (ns your.app
35 | (:require [lively.core :as lively]))
36 |
37 | (lively/start "/js/hello.js")
38 | ```
39 |
40 | Call to `start` is idempotent, it is safe to call it multiple times.
41 |
42 | The followig options can be passed as an optional options map:
43 |
44 | * `:polling-rate`: Milliseconds to sleep between polls. Defaults to 1000.
45 | * `:on-reload`: Callback function to call after files have been reloaded.
46 |
47 | For example:
48 |
49 | ```clojure
50 | (lively/start "/js/hello.js" {:polling-rate 500
51 | :on-reload (fn [] (.log js/console "Reloaded!"))})
52 | ```
53 |
54 | ## Examples
55 |
56 | * Minimalistic example project can be found in [example](https://github.com/immoh/lively/tree/master/example) directory.
57 | * [Lively Snake Demo](https://github.com/immoh/lively-snake-demo) showcases implementing a snake game using Lively
58 |
59 |
60 | ## How does it work?
61 |
62 | Lively monitors changes in JavaScript files by making consecutive HEAD requests to the server.
63 | When ClojureScript files are compiled, the main JavaScript file is always is generated again and this change is
64 | noticed by Lively. Lively finds out which namespaces have changed by making HEAD requests for each namespace-specific
65 | JavaScript file and reloads ones that have changed.
66 |
67 |
68 | ## License
69 |
70 | Copyright © 2014-2015 Immo Heikkinen
71 |
72 | Distributed under the Eclipse Public License, the same as Clojure.
73 |
--------------------------------------------------------------------------------
/src/lively/core.cljs:
--------------------------------------------------------------------------------
1 | (ns lively.core
2 | (:require [cljs.core.async :as async :refer [! chan close! timeout]]
3 | goog.net.jsloader
4 | goog.net.XhrIo
5 | goog.string
6 | goog.Uri)
7 | (:require-macros [cljs.core.async.macros :refer [go]]))
8 |
9 | (defonce initialized? (atom nil))
10 |
11 | (defn headers-changed? [cache uri headers]
12 | (when-not (= (get @cache uri) headers)
13 | (swap! cache assoc uri headers)))
14 |
15 | (defn put-and-close! [port val]
16 | (go
17 | (>! port val)
18 | (close! port)))
19 |
20 | (defn make-unique [uri]
21 | (.makeUnique (goog.Uri/parse uri)))
22 |
23 | (defn (goog.net.jsloader/load (make-unique uri))
26 | (.addCallbacks (fn [& _] (put-and-close! channel :ok))
27 | (fn [& _] (put-and-close! channel :failed))))
28 | channel))
29 |
30 | (defn pick-headers [target]
31 | (let [headers ["Last-Modified" "Content-Length"]]
32 | (zipmap headers (map #(.getResponseHeader target %) headers))))
33 |
34 | (defn clj (.-requires deps))]
82 | (map (fn [[name path]]
83 | {:name name :uri (resolve-uri path) :requires (set (keys (get requires path)))})
84 | (js->clj (.-nameToPath deps)))))
85 |
86 | (defn get-reloadable-deps []
87 | (let [all-deps (get-all-deps)]
88 | (->> all-deps
89 | (remove immutable?)
90 | (expand-transitive-deps all-deps))))
91 |
92 | ;; Thanks, lein-figwheel!
93 | (defn patch-goog-base []
94 | (set! (.-provide js/goog) (.-exportPath_ js/goog))
95 | (set! (.-CLOSURE_IMPORT_SCRIPT (.-global js/goog)) (fn [file]
96 | (when (.inHtmlDocument_ js/goog)
97 | (goog.net.jsloader/load file)))))
98 |
99 | (defn check-optimization-level []
100 | (when-not (and js/goog (.-dependencies_ js/goog))
101 | (throw (js/Error. "Lively requires that ClojureScript is compiled with :optimizations set to :none"))))
102 |
103 | (defn check-protocol []
104 | (when-not (= "http" (-> js/goog .-basePath goog.Uri/parse .getScheme))
105 | (throw (js/Error. "Lively requires that JavaScript files are loaded over HTTP protocol"))))
106 |
107 | (defn start
108 | "Start polling for changes in compiled JavaScript files and reload them when they change.
109 | Takes location of the main JavaScript file and optionally map of options with following keys:
110 |
111 | :polling-rate Milliseconds to sleep between polls. Defaults to 1000.
112 | :on-reload Callback function to call after files have been reloaded.
113 |
114 | Throws an error if ClojureScript hasn't been compiled with optimization level :none, or
115 | if JavaScript files are not loaded over HTTP.
116 |
117 | Returns nil."
118 | ([main-js-location]
119 | (start main-js-location nil))
120 | ([main-js-location {:keys [polling-rate on-reload]}]
121 | (when-not @initialized?
122 | (reset! initialized? true)
123 | (check-optimization-level)
124 | (check-protocol)
125 | (patch-goog-base)
126 | (go
127 | (let [cljs-deps-uri (resolve-uri "../cljs_deps.js")
128 | main-js-location (if (:success? (> deps
142 | (remove (comp @loaded-cljs-deps :name))
143 | (topo-sort)
144 | (map :uri)
145 | (distinct))
146 | headers-for-uris (zipmap uris (