├── .gitignore ├── .travis.yml ├── README.md ├── build.sh ├── example ├── .gitignore ├── README.md ├── project.clj ├── resources │ └── public │ │ └── index-dev.html └── src │ ├── clj │ └── hello │ │ └── core.clj │ └── cljs │ └── hello │ └── core.cljs ├── project.clj └── src └── lively └── core.cljs /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | script: ./build.sh 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lively [![Build Status](https://travis-ci.org/immoh/lively.svg?branch=travis)](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 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o pipefail 4 | set -e 5 | 6 | OUTPUT_FILE="cljsbuild.out" 7 | 8 | if lein do clean, cljsbuild once 2>&1 | tee $OUTPUT_FILE 9 | then 10 | ! grep WARNING $OUTPUT_FILE 11 | else 12 | exit $? 13 | fi 14 | 15 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/resources/public/index-dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Lively example 4 | 5 | 6 |

Lively example

7 | 8 | 9 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 (