├── .github └── FUNDING.yml ├── deps.edn ├── .gitignore ├── project.clj ├── README.md ├── test └── figwheel │ └── tools │ └── exceptions_test.clj └── src └── figwheel ├── tools ├── exceptions.clj └── heads_up.cljs └── core.cljc /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bhauman] 2 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.10.0"} 3 | org.clojure/clojurescript {:mvn/version "1.10.773"} 4 | org.clojure/data.json {:mvn/version "2.4.0"}}} 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cpcache 2 | out/* 3 | test_out 4 | target 5 | dev/example/except* 6 | 7 | target 8 | .cpcache 9 | *.log 10 | /out 11 | /.repl 12 | .idea 13 | *.iml 14 | .repl 15 | .lein-deps-sum 16 | .lein-failures 17 | .lein-plugins 18 | .lein-repl-history 19 | .nrepl-port 20 | .lein-classpath 21 | \#*\# 22 | .\#* 23 | *.jar 24 | *.class 25 | dev.cljs.edn 26 | figwheel-main.edn 27 | .java-version 28 | .lein-failures 29 | .ruby-version 30 | pom.xml 31 | pom.xml.asc 32 | .rebel_readline_history 33 | 34 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.bhauman/figwheel-core "0.2.21-SNAPSHOT" 2 | :description "Figwheel core provides code reloading facilities for ClojureScript." 3 | :url "https://github.com/bhauman/figwheel-core" 4 | :license {:name "Eclipse Public License - v 1.0" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :scm {:name "git" 7 | :url "https://github.com/bhauman/figwheel-core"} 8 | 9 | :dependencies 10 | [[org.clojure/clojure "1.10.0"] 11 | [org.clojure/clojurescript "1.10.773" :exclusions [commons-codec]] 12 | [org.clojure/data.json "2.4.0"] 13 | ]) 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## figwheel-core 2 | 3 | Contains the core functionality of Figwheel. 4 | 5 | This allows you to get the benefits of figwheel's hot reloading and 6 | feedback cycle without being complected with a server or a REPL 7 | implementation. 8 | 9 | # Usage 10 | 11 | Experts only at this point 12 | 13 | Add `figwheel-core` to your deps and then: 14 | 15 | ``` 16 | clj -m cljs.main -w src -e "(require '[figwheel.core :include-macros true])(figwheel.core/hook-cljs-build)(figwheel.core/start-from-repl)" -r 17 | ``` 18 | 19 | ## License 20 | 21 | Copyright © 2018 Bruce Hauman 22 | 23 | Distributed under the Eclipse Public License either version 1.0 or any 24 | later version. 25 | -------------------------------------------------------------------------------- /test/figwheel/tools/exceptions_test.clj: -------------------------------------------------------------------------------- 1 | (ns figwheel.tools.exceptions-test 2 | (:require 3 | [cljs.build.api :as bapi] 4 | [clojure.string :as string] 5 | [clojure.java.io :as io] 6 | [figwheel.tools.exceptions :refer :all] 7 | [clojure.test :refer [deftest is testing]]) 8 | (:import 9 | [java.util.regex Pattern])) 10 | 11 | #_(remove-ns 'figwheel.tools.exceptions-test) 12 | 13 | ;; ----------------------------- 14 | ;; helpers to capture exceptions 15 | ;; ----------------------------- 16 | 17 | (defn example-test-file! [p code] 18 | (io/make-parents (io/file p)) 19 | (spit p (if (string/starts-with? code "(ns") 20 | (str code) 21 | (str (prn-str '(ns example.except)) code)))) 22 | 23 | (defn fetch-exception [code] 24 | (let [p "dev/example/except.cljs"] 25 | (example-test-file! p code) 26 | (.delete (io/file "target/test_out/example/except.js")) 27 | (try 28 | (bapi/build "dev" {:output-dir "target/test_out" :main "example.except" :output-to "target/test_out/main.js"}) 29 | (catch Throwable e 30 | (Throwable->map e))))) 31 | 32 | (defn fetch-clj-exception [code] 33 | (let [p "dev/example/except.clj"] 34 | (example-test-file! p code) 35 | (try 36 | (load-file p) 37 | (catch Throwable e 38 | (Throwable->map e))))) 39 | 40 | (defn anonymise-ex 41 | "Remove system specific information from exceptions 42 | so that tests produce the same results on different 43 | systems." 44 | [ex-map] 45 | (update ex-map :data dissoc :file)) 46 | 47 | (deftest exception-parsing-test 48 | (is (= {:tag :cljs/analysis-error, 49 | :line 2, 50 | :column 1, 51 | :file "dev/example/except.cljs", 52 | :type 'clojure.lang.ArityException, 53 | :data 54 | {:file "dev/example/except.cljs", 55 | :line 2, 56 | :column 1, 57 | :tag :cljs/analysis-error}} 58 | (dissoc (parse-exception (fetch-exception "(defn)")) :message))) 59 | 60 | (let [ex (parse-exception (fetch-exception "(defn dddd2344)"))] 61 | (is (#{:cljs/general-compile-failure :cljs/analysis-error} 62 | (:tag ex))) 63 | (if (= :cljs/general-compile-failure (:tag ex)) 64 | (is (= {:tag :cljs/general-compile-failure, 65 | :message "Parameter declaration missing", 66 | :line 2, 67 | :column 1, 68 | :file "dev/example/except.cljs", 69 | :type 'java.lang.IllegalArgumentException, 70 | :data 71 | {:source "dev/example/except.cljs", 72 | :line 2, 73 | :column 1, 74 | :phase :macroexpansion, 75 | :symbol 'cljs.core/defn}} 76 | ex)) 77 | (is (= {:tag :cljs/analysis-error, 78 | :message "Parameter declaration missing", 79 | :line 2, 80 | :column 1, 81 | :file "dev/example/except.cljs", 82 | :type 'java.lang.IllegalArgumentException, 83 | :data 84 | {:file "dev/example/except.cljs", 85 | :line 2, 86 | :column 1, 87 | :tag :cljs/analysis-error}} 88 | ex)))) 89 | 90 | (is (= "Wrong number of args (0) passed to" 91 | (some-> (fetch-exception "(defn)") 92 | parse-exception 93 | :message 94 | (string/split #":") 95 | first))) 96 | 97 | (is (= {:tag :tools.reader/eof-reader-exception, 98 | :message 99 | "Unexpected EOF while reading item 1 of list, starting at line 2 and column 1.", 100 | :line 2, 101 | :column 1, 102 | :file "dev/example/except.cljs", 103 | :type 'clojure.lang.ExceptionInfo, 104 | :data 105 | {:type :reader-exception, 106 | :ex-kind :eof, 107 | :line 2, 108 | :col 7}} 109 | (anonymise-ex (parse-exception (fetch-exception "(defn "))))) 110 | 111 | (is (= {:tag :tools.reader/reader-exception, 112 | :message "Unmatched delimiter ).", 113 | :line 2, 114 | :column 2, 115 | :file "dev/example/except.cljs", 116 | :type 'clojure.lang.ExceptionInfo, 117 | :data 118 | {:type :reader-exception, 119 | :ex-kind :reader-error, 120 | :file 121 | (.getCanonicalPath (io/file "dev/example/except.cljs",)) 122 | :line 2, 123 | :col 2}} 124 | (parse-exception (fetch-exception "))")))) 125 | 126 | (is (= {:tag :tools.reader/reader-exception, 127 | :message "No reader function for tag asdf.", 128 | :line 2, 129 | :column 6, 130 | :file "dev/example/except.cljs", 131 | :type 'clojure.lang.ExceptionInfo, 132 | :data 133 | {:type :reader-exception, 134 | :ex-kind :reader-error, 135 | :file (.getCanonicalPath (io/file "dev/example/except.cljs",)) 136 | :line 2, 137 | :col 6}} 138 | (parse-exception (fetch-exception "#asdf {}")))) 139 | 140 | 141 | 142 | (is (= {:tag :clj/compiler-exception, 143 | :message "No reader function for tag asdf", 144 | :line 2, 145 | :column 9, 146 | :file "dev/example/except.clj", 147 | :type 'java.lang.RuntimeException 148 | } 149 | (dissoc (parse-exception (fetch-clj-exception "#asdf {}")) 150 | :data))) 151 | 152 | (is (= {:tag :clj/compiler-exception, 153 | :message "EOF while reading, starting at line 2", 154 | :line 2, 155 | :column 1, 156 | :file "dev/example/except.clj", 157 | :type 'java.lang.RuntimeException} 158 | (dissoc (parse-exception (fetch-clj-exception " (defn")) 159 | :data))) 160 | 161 | (is (= {:tag :cljs/missing-required-ns, 162 | :message 163 | "No such namespace: example.house, could not locate example/house.cljs, example/house.cljc, or JavaScript source providing \"example.house\" in file dev/example/except.cljs", 164 | :line 2, 165 | :column 16, 166 | :file "dev/example/except.cljs", 167 | :type 'clojure.lang.ExceptionInfo, 168 | :data {:tag :cljs/analysis-error}} 169 | (parse-exception 170 | (fetch-exception 171 | "(ns example.except 172 | (:require [example.house])) 173 | ")))) 174 | 175 | ) 176 | 177 | ;; TODO work on spec exceptions 178 | #_(def clj-version 179 | (read-string (string/join "." 180 | (take 2 (string/split (clojure-version) #"\."))))) 181 | 182 | #_(when (>= clj-version 1.9) 183 | 184 | #_(parse-exception (fetch-clj-exception "(defn)")) 185 | 186 | ) 187 | -------------------------------------------------------------------------------- /src/figwheel/tools/exceptions.clj: -------------------------------------------------------------------------------- 1 | (ns figwheel.tools.exceptions 2 | (:require 3 | [clojure.string :as string] 4 | [clojure.java.io :as io]) 5 | (:import 6 | [java.util.regex Pattern])) 7 | 8 | #_(remove-ns 'figwheel.tools.exceptions) 9 | ;; utils 10 | 11 | (defn map-keys [f m] 12 | (into {} (map (fn [[k v]] 13 | (clojure.lang.MapEntry. (f k) v)) m))) 14 | 15 | (defn un-clojure-error-keywords [m] 16 | (map-keys #(if (= "clojure.error" (namespace %)) 17 | (keyword (name %)) 18 | %) 19 | m)) 20 | 21 | (defn relativize-local [path] 22 | (.getPath 23 | (.relativize 24 | (.toURI (io/file (.getCanonicalPath (io/file ".")))) 25 | ;; just in case we get a URL or some such let's change it to a string first 26 | (.toURI (io/file (str path)))))) 27 | 28 | ;; compile time exceptions are syntax errors so we need to break them down into 29 | ;; message line column file 30 | 31 | ;; TODO handle spec errors 32 | 33 | (defn cljs-analysis-ex? [tm] 34 | (some #{:cljs/analysis-error} (keep #(get-in %[:data :tag]) (:via tm)))) 35 | 36 | (defn cljs-missing-required-ns? [tm] 37 | (and (cljs-analysis-ex? tm) 38 | (string? (:cause tm)) 39 | (string/starts-with? (:cause tm) "No such namespace: "))) 40 | 41 | (defn reader-ex? [{:keys [data]}] 42 | (= :reader-exception (:type data))) 43 | 44 | (defn eof-reader-ex? [{:keys [data] :as tm}] 45 | (and (reader-ex? tm) (= :eof (:ex-kind data)))) 46 | 47 | (defn cljs-failed-compiling? [tm] 48 | (some #(.startsWith % "failed compiling file:") (keep :message (:via tm)))) 49 | 50 | (defn clj-compiler-ex? [tm] 51 | (-> tm :via first :type pr-str (= (pr-str 'clojure.lang.Compiler$CompilerException)))) 52 | 53 | (defn clj-spec-error? [tm] 54 | (-> tm :data :clojure.spec.alpha/problems)) 55 | 56 | (defn cljs-no-file-for-namespace? [tm] 57 | (-> tm :via first :data :cljs.repl/error (= :invalid-ns))) 58 | 59 | (defn exception-type? [tm] 60 | (cond 61 | (cljs-no-file-for-namespace? tm) :cljs/no-file-for-namespace 62 | (cljs-missing-required-ns? tm) :cljs/missing-required-ns 63 | (cljs-analysis-ex? tm) :cljs/analysis-error 64 | (eof-reader-ex? tm) :tools.reader/eof-reader-exception 65 | (reader-ex? tm) :tools.reader/reader-exception 66 | (cljs-failed-compiling? tm) :cljs/general-compile-failure 67 | (clj-spec-error? tm) :clj/spec-based-syntax-error 68 | (clj-compiler-ex? tm) :clj/compiler-exception 69 | :else nil)) 70 | 71 | (derive :clj/spec-based-syntax-error :clj/compiler-exception) 72 | 73 | (derive :tools.reader/eof-reader-exception :tools.reader/reader-exception) 74 | 75 | (derive :cljs/missing-required-ns :cljs/analysis-error) 76 | 77 | (defmulti message exception-type?) 78 | 79 | (defmethod message :default [tm] (:cause tm)) 80 | 81 | (defmethod message :tools.reader/reader-exception [tm] 82 | (or 83 | (some-> tm :cause (string/split #"\[line.*\]") second string/trim) 84 | (:cause tm))) 85 | 86 | (defmethod message :clj/spec-based-syntax-error [tm] 87 | (first (string/split-lines (:cause tm)))) 88 | 89 | (defmethod message :cljs/no-file-for-namespace [{:keys [cause] :as tm}] 90 | (when cause 91 | (when-let [ns' (second (re-matches #"^(\S+).*" cause))] 92 | (format "Could not find file for namespace '%s' 93 | this is probably caused by a namespace/filepath miss-match 94 | or a poorly configured classpath." ns')))) 95 | 96 | 97 | (defmulti blame-pos exception-type?) 98 | 99 | (defmethod blame-pos :default [tm]) 100 | 101 | (defmethod blame-pos :cljs/missing-required-ns [{:keys [cause] :as tm}] 102 | (when-let [nmspc (and cause 103 | (second 104 | (re-matches #"No such namespace:\s([^,]+),.*" cause)))] 105 | (when-let [file (-> tm :via first :data :file)] 106 | (let [pat (Pattern/compile (str ".*" nmspc ".*")) 107 | [pre post] 108 | (split-with 109 | #(not (.matches (.matcher pat %))) 110 | (line-seq (io/reader file)))] 111 | (when-not (empty? post) 112 | {:line (inc (count pre)) 113 | :column (inc (.indexOf (first post) nmspc))}))))) 114 | 115 | (defmethod blame-pos :cljs/general-compile-failure [tm] 116 | (-> (some->> tm :via reverse (filter #(or (get-in % [:data :line]) 117 | (get-in % [:data :clojure.error/line]))) 118 | first 119 | :data 120 | un-clojure-error-keywords) 121 | (select-keys [:line :column]))) 122 | 123 | (defmethod blame-pos :cljs/analysis-error [tm] 124 | (select-keys 125 | (some->> tm :via reverse (filter #(get-in % [:data :line])) first :data) 126 | [:line :column])) 127 | 128 | (defmethod blame-pos :tools.reader/eof-reader-exception [tm] 129 | (let [[line column] 130 | (some->> tm :cause (re-matches #".*line\s(\d*)\sand\scolumn\s(\d*).*") 131 | rest)] 132 | (cond-> {} 133 | line (assoc :line (Integer/parseInt line)) 134 | column (assoc :column (Integer/parseInt column))))) 135 | 136 | (defmethod blame-pos :tools.reader/reader-exception [{:keys [data]}] 137 | (let [{:keys [line col]} data] 138 | (cond-> {} 139 | line (assoc :line line) 140 | col (assoc :column col)))) 141 | 142 | (defmethod blame-pos :clj/compiler-exception [tm] 143 | (let [[line column] 144 | (some->> tm :via first :message 145 | (re-matches #"(?s).*\(.*\:(\d+)\:(\d+)\).*") 146 | rest)] 147 | (cond-> {} 148 | line (assoc :line (Integer/parseInt line)) 149 | column (assoc :column (Integer/parseInt column))))) 150 | 151 | ;; return relative path because it isn't lossy 152 | (defmulti source-file exception-type?) 153 | 154 | (defmethod source-file :default [tm]) 155 | 156 | (defn first-file-source [tm] 157 | (some->> tm :via (keep #(get-in % [:data :file])) first str)) 158 | 159 | (defmethod source-file :cljs/general-compile-failure [tm] 160 | (first-file-source tm)) 161 | 162 | (defmethod source-file :cljs/analysis-error [tm] 163 | (first-file-source tm)) 164 | 165 | (defmethod source-file :tools.reader/reader-exception [tm] 166 | (first-file-source tm)) 167 | 168 | (defn correct-file-path [file] 169 | (cond 170 | (nil? file) file 171 | (not (.exists (io/file file))) 172 | (if-let [f (io/resource file)] 173 | (relativize-local (.getPath f)) 174 | file) 175 | :else (relativize-local file))) 176 | 177 | (defmethod source-file :clj/compiler-exception [tm] 178 | (some->> tm :via first :message (re-matches #"(?s).*\(([^:]*)\:.*") second correct-file-path)) 179 | 180 | (defmulti data exception-type?) 181 | 182 | (defmethod data :default [tm] 183 | (un-clojure-error-keywords (or (:data tm) (->> tm :via reverse (keep :data) first)))) 184 | 185 | #_(defmethod data :clj/spec-based-syntax-error [tm] nil) 186 | 187 | (defn ex-type [tm] 188 | (some-> tm :via last :type pr-str symbol)) 189 | 190 | (defn parse-exception [e] 191 | (let [tm (if (instance? Throwable e) (Throwable->map e) e) 192 | tag (exception-type? tm) 193 | msg (message tm) 194 | pos (blame-pos tm) 195 | file (source-file tm) 196 | ex-typ (ex-type tm) 197 | data' (data tm)] 198 | (cond-> (vary-meta {} assoc ::orig-throwable tm) 199 | tag (assoc :tag tag) 200 | msg (assoc :message msg) 201 | pos (merge pos) 202 | file (assoc :file file) 203 | ex-typ (assoc :type ex-typ) 204 | data' (assoc :data data')))) 205 | 206 | #_(parse-exception (figwheel.tools.exceptions-test/fetch-clj-exception "(defn [])")) 207 | 208 | ;; Excerpts 209 | 210 | (defn str-excerpt [code-str start length & [path]] 211 | (cond-> 212 | {:start-line start 213 | :excerpt (->> (string/split-lines code-str) 214 | (drop (dec start)) 215 | (take length) 216 | (string/join "\n"))} 217 | path (assoc :path path))) 218 | 219 | (defn file-excerpt [file start length] 220 | (str-excerpt (slurp file) start length (.getCanonicalPath file))) 221 | 222 | (defn root-source->file-excerpt [{:keys [source-form] :as root-source-info} except-data] 223 | (let [{:keys [source column]} (when (instance? clojure.lang.IMeta source-form) 224 | (meta source-form))] 225 | (cond-> except-data 226 | (and column (> column 1) (= (:line except-data) 1) (:column except-data)) 227 | (update :column #(max 1 (- % (dec column)))) 228 | source (assoc :file-excerpt {:start-line 1 :excerpt source})))) 229 | 230 | (defn add-excerpt 231 | ([parsed] (add-excerpt parsed nil)) 232 | ([{:keys [file line data] :as parsed} code-str] 233 | (cond 234 | (and line file (.isFile (io/file file))) 235 | (let [fex (file-excerpt (io/file file) (max 1 (- line 10)) 20)] 236 | (cond-> parsed 237 | fex (assoc :file-excerpt fex))) 238 | (and line (:root-source-info data)) 239 | (root-source->file-excerpt (:root-source-info data) parsed) 240 | (and line code-str) 241 | (let [str-ex (str-excerpt code-str (max 1 (- line 10)) 20)] 242 | (cond-> parsed 243 | str-ex (assoc :file-excerpt str-ex))) 244 | :else parsed))) 245 | -------------------------------------------------------------------------------- /src/figwheel/tools/heads_up.cljs: -------------------------------------------------------------------------------- 1 | (ns figwheel.tools.heads-up 2 | (:require 3 | [clojure.string :as string] 4 | [goog.string] 5 | [goog.dom.dataset :as data] 6 | [goog.object :as gobj] 7 | [goog.dom :as dom] 8 | [cljs.pprint :as pp]) 9 | (:import [goog Promise])) 10 | 11 | (declare clear cljs-logo-svg) 12 | 13 | ;; cheap hiccup 14 | (defn node [t attrs & children] 15 | (let [e (.createElement js/document (name t))] 16 | (doseq [k (keys attrs)] (.setAttribute e (name k) (get attrs k))) 17 | (doseq [ch children] (.appendChild e ch)) ;; children 18 | e)) 19 | 20 | (defmulti heads-up-event-dispatch (fn [dataset] (.-figwheelEvent dataset))) 21 | (defmethod heads-up-event-dispatch :default [_] {}) 22 | 23 | ;; TODO change this so that clients of this library can register 24 | ;; to catch this event 25 | (defmethod heads-up-event-dispatch "file-selected" [dataset] 26 | #_(socket/send! {:figwheel-event "file-selected" 27 | :file-name (.-fileName dataset) 28 | :file-line (.-fileLine dataset) 29 | :file-column (.-fileColumn dataset)})) 30 | 31 | (defmethod heads-up-event-dispatch "close-heads-up" [dataset] (clear)) 32 | 33 | (defn ancestor-nodes [el] 34 | (iterate (fn [e] (.-parentNode e)) el)) 35 | 36 | (defn get-dataset [el] 37 | (first (keep (fn [x] (when (.. x -dataset -figwheelEvent) (.. x -dataset))) 38 | (take 4 (ancestor-nodes el))))) 39 | 40 | (defn heads-up-onclick-handler [event] 41 | (let [dataset (get-dataset (.. event -target))] 42 | (.preventDefault event) 43 | (when dataset 44 | (heads-up-event-dispatch dataset)))) 45 | 46 | (defn ensure-container [] 47 | (let [cont-id "figwheel-heads-up-container" 48 | content-id "figwheel-heads-up-content-area"] 49 | (if-not (.querySelector js/document (str "#" cont-id)) 50 | (let [el (node :div { :id cont-id 51 | :style 52 | (str "-webkit-transition: all 0.2s ease-in-out;" 53 | "-moz-transition: all 0.2s ease-in-out;" 54 | "-o-transition: all 0.2s ease-in-out;" 55 | "transition: all 0.2s ease-in-out;" 56 | "font-size: 13px;" 57 | "border-top: 1px solid #f5f5f5;" 58 | "box-shadow: 0px 0px 1px #aaaaaa;" 59 | "line-height: 18px;" 60 | "color: #333;" 61 | "font-family: monospace;" 62 | "padding: 0px 10px 0px 70px;" 63 | "position: fixed;" 64 | "bottom: 0px;" 65 | "left: 0px;" 66 | "height: 0px;" 67 | "opacity: 0.0;" 68 | "box-sizing: border-box;" 69 | "z-index: 10000;" 70 | "text-align: left;" 71 | ) })] 72 | (set! (.-onclick el) heads-up-onclick-handler) 73 | (set! (.-innerHTML el) cljs-logo-svg) 74 | (.appendChild el (node :div {:id content-id})) 75 | (-> (.-body js/document) 76 | (.appendChild el)))) 77 | { :container-el (.getElementById js/document cont-id) 78 | :content-area-el (.getElementById js/document content-id) } 79 | )) 80 | 81 | (defn set-style! [{:keys [container-el]} st-map] 82 | (mapv 83 | (fn [[k v]] 84 | (gobj/set (.-style container-el) (name k) v)) 85 | st-map)) 86 | 87 | (defn set-content! [{:keys [content-area-el] :as c} dom-str] 88 | (set! (.-innerHTML content-area-el) dom-str)) 89 | 90 | (defn get-content [{:keys [content-area-el]}] 91 | (.-innerHTML content-area-el)) 92 | 93 | (defn close-link [] 94 | (str "" 103 | "x" 104 | "")) 105 | 106 | (defn display-heads-up [style msg] 107 | (Promise. 108 | (fn [resolve reject] 109 | (let [c (ensure-container)] 110 | (set-style! c (merge { 111 | :paddingTop "10px" 112 | :paddingBottom "10px" 113 | :width "100%" 114 | :minHeight "68px" 115 | :opacity "1.0" } 116 | style)) 117 | (set-content! c msg) 118 | (js/setTimeout (fn [] 119 | (set-style! c {:height "auto"}) 120 | (resolve true)) 121 | 300))))) 122 | 123 | (defn heading 124 | ([s] (heading s "")) 125 | ([s sub-head] 126 | (str "
" 132 | s 133 | " " 137 | sub-head 138 | "
"))) 139 | 140 | (defn file-selector-div [file-name line-number column-number msg] 141 | (str "
" msg "
")) 144 | 145 | (defn format-line [msg {:keys [file line column]}] 146 | (let [msg (goog.string/htmlEscape msg)] 147 | (if (or file line) 148 | (file-selector-div file line column msg) 149 | (str "
" msg "
")))) 150 | 151 | (defn escape [x] 152 | (goog.string/htmlEscape x)) 153 | 154 | (defn pad-line-number [n line-number] 155 | (let [len (count ((fnil str "") line-number))] 156 | (-> (if (< len n) 157 | (apply str (repeat (- n len) " ")) 158 | "") 159 | (str line-number)))) 160 | 161 | (defn inline-error-line [style line-number line] 162 | (str "" "" line-number " " (escape line) "")) 163 | 164 | (defn format-inline-error-line [[typ line-number line]] 165 | (condp = typ 166 | :code-line (inline-error-line "color: #999;" line-number line) 167 | :error-in-code (inline-error-line "color: #ccc; font-weight: bold;" line-number line) 168 | :error-message (inline-error-line "color: #D07D7D;" line-number line) 169 | (inline-error-line "color: #666;" line-number line))) 170 | 171 | (defn pad-line-numbers [inline-error] 172 | (let [max-line-number-length (count (str (reduce max (map second inline-error))))] 173 | (map #(update-in % [1] 174 | (partial pad-line-number max-line-number-length)) inline-error))) 175 | 176 | (defn format-inline-error [inline-error] 177 | (when (not-empty inline-error) 178 | (let [lines (map format-inline-error-line (pad-line-numbers inline-error))] 179 | (str "
"
181 |            (string/join "\n" lines)
182 |            "
")))) 183 | 184 | (def flatten-exception #(take-while some? (iterate :cause %))) 185 | 186 | (defn exception->display-data [{:keys [tag message line column type file data error-inline] :as exception}] 187 | (let [last-message (cond 188 | (and file line) 189 | (str "Please see line " line " of file " file ) 190 | file (str "Please see " file) 191 | :else nil) 192 | data-for-display (when-not (#{"cljs/analysis-error" "tools.reader/eof-reader-exception" "tools.reader/reader-exception"} 193 | tag) 194 | data)] 195 | {:head (condp = tag 196 | "clj/compiler-exception" "Couldn't load Clojure file" 197 | "cljs/missing-required-ns" "Could not Find Namespace" 198 | "cljs/analysis-error" "Could not Analyze" 199 | "tools.reader/eof-reader-exception" "Could not Read" 200 | "tools.reader/reader-exception" "Could not Read" 201 | "cljs/general-compile-failure" "Could not Compile" 202 | "Compile Exception") 203 | :sub-head file 204 | :messages (concat 205 | (map 206 | #(str "
" % "
") 207 | (filter 208 | (complement string/blank?) 209 | [(cond-> "" 210 | type (str (escape type)) 211 | (and type message) (str ": ") 212 | message (str "" (escape message) "")) 213 | (when (and (not (pos? (count error-inline))) 214 | data-for-display) 215 | (str "
"
216 |                           (goog.string/trimRight (with-out-str (pp/pprint data-for-display)))
217 |                           "
")) 218 | (when (pos? (count error-inline)) 219 | (format-inline-error error-inline))])) 220 | (when last-message [(str "
" (escape last-message) "
")])) 221 | :file file 222 | :line line 223 | :column column})) 224 | 225 | #_(defn auto-notify-source-file-line [{:keys [file line column]}] 226 | #_(socket/send! {:figwheel-event "file-selected" 227 | :file-name (str file) 228 | :file-line (str line) 229 | :file-column (str column)})) 230 | 231 | (defn display-exception [exception-data] 232 | (let [{:keys [head 233 | sub-head 234 | messages 235 | last-message 236 | file 237 | line 238 | column]} 239 | (-> exception-data 240 | exception->display-data) 241 | msg (apply str messages)] 242 | (display-heads-up {:backgroundColor "rgba(255, 161, 161, 0.95)"} 243 | (str (close-link) 244 | (heading head sub-head) 245 | (file-selector-div file line column msg))))) 246 | 247 | (defn warning-data->display-data [{:keys [file line column message error-inline] :as warning-data}] 248 | (let [last-message (cond 249 | (and file line) 250 | (str "Please see line " line " of file " file ) 251 | file (str "Please see " file) 252 | :else nil)] 253 | {:head "Compile Warning" 254 | :sub-head file 255 | :messages (concat 256 | (map 257 | #(str "
" % "
") 258 | [(when message 259 | (str "" (escape message) "")) 260 | (when (pos? (count error-inline)) 261 | (format-inline-error error-inline))]) 262 | (when last-message 263 | [(str "
" (escape last-message) "
")])) 264 | :file file 265 | :line line 266 | :column column})) 267 | 268 | (defn display-system-warning [header msg] 269 | (display-heads-up {:backgroundColor "rgba(255, 220, 110, 0.95)" } 270 | (str (close-link) (heading header) 271 | "
" msg "
" 272 | #_(format-line msg {})))) 273 | 274 | (defn display-warning [warning-data] 275 | (let [{:keys [head 276 | sub-head 277 | messages 278 | last-message 279 | file 280 | line 281 | column]} 282 | (-> warning-data 283 | warning-data->display-data) 284 | msg (apply str messages)] 285 | (display-heads-up {:backgroundColor "rgba(255, 220, 110, 0.95)" } 286 | (str (close-link) 287 | (heading head sub-head) 288 | (file-selector-div file line column msg))))) 289 | 290 | (defn format-warning-message [{:keys [message file line column] :as warning-data}] 291 | (cond-> message 292 | line (str " at line " line) 293 | (and line column) (str ", column " column) 294 | file (str " in file " file)) ) 295 | 296 | (defn append-warning-message [{:keys [message file line column] :as warning-data}] 297 | (when message 298 | (let [{:keys [content-area-el]} (ensure-container) 299 | el (dom/createElement "div") 300 | child-count (.-length (dom/getChildren content-area-el))] 301 | (if (< child-count 6) 302 | (do 303 | (set! (.-innerHTML el) 304 | (format-line (format-warning-message warning-data) 305 | warning-data)) 306 | (dom/append content-area-el el)) 307 | (when-let [last-child (dom/getLastElementChild content-area-el)] 308 | (if-let [message-count (data/get last-child "figwheel_count")] 309 | (let [message-count (inc (js/parseInt message-count))] 310 | (data/set last-child "figwheel_count" message-count) 311 | (set! (.-innerHTML last-child) 312 | (str message-count " more warnings have not been displayed ..."))) 313 | (dom/append 314 | content-area-el 315 | (dom/createDom "div" #js {:data-figwheel_count 1 316 | :style "margin-top: 3px; font-weight: bold"} 317 | "1 more warning that has not been displayed ...")))))))) 318 | 319 | (defn timeout* [time-ms] 320 | (Promise. 321 | (fn [resolve _] 322 | (js/setTimeout #(resolve true) time-ms)))) 323 | 324 | (defn clear [] 325 | (let [c (ensure-container)] 326 | (-> (Promise. 327 | (fn [r _] 328 | (set-style! c { :opacity "0.0" }) 329 | (r true))) 330 | (.then (fn [_] (timeout* 300))) 331 | (.then (fn [_] 332 | (set-style! c { :width "auto" 333 | :height "0px" 334 | :minHeight "0px" 335 | :padding "0px 10px 0px 70px" 336 | :borderRadius "0px" 337 | :backgroundColor "transparent" }))) 338 | (.then (fn [_] (timeout* 200))) 339 | (.then (fn [_] (set-content! c "")))))) 340 | 341 | (defn display-loaded-start [] 342 | (display-heads-up {:backgroundColor "rgba(211,234,172,1.0)" 343 | :width "68px" 344 | :height "68px" 345 | :paddingLeft "0px" 346 | :paddingRight "0px" 347 | :borderRadius "35px" } "")) 348 | 349 | (defn flash-loaded [] 350 | (-> (display-loaded-start) 351 | (.then (fn [_] (timeout* 400))) 352 | (.then (fn [_] (clear))))) 353 | 354 | (def cljs-logo-svg 355 | " 356 | 357 | 360 | 361 | 362 | 366 | 367 | 371 | 376 | 377 | 379 | 381 | ") 382 | 383 | ;; ---- bad compile helper ui ---- 384 | 385 | (defn close-bad-compile-screen [] 386 | (when-let [el (js/document.getElementById "figwheelFailScreen")] 387 | (dom/removeNode el))) 388 | 389 | (defn bad-compile-screen [] 390 | (let [body (-> (dom/getElementsByTagNameAndClass "body") 391 | (aget 0))] 392 | (close-bad-compile-screen) 393 | #_(dom/removeChildren body) 394 | (dom/append body 395 | (dom/createDom 396 | "div" 397 | #js {:id "figwheelFailScreen" 398 | :style (str "background-color: rgba(24, 26, 38, 0.95);" 399 | "position: absolute;" 400 | "z-index: 9000;" 401 | "width: 100vw;" 402 | "height: 100vh;" 403 | "top: 0px; left: 0px;" 404 | "font-family: monospace")} 405 | (dom/createDom 406 | "div" 407 | #js {:class "message" 408 | :style (str 409 | "color: #FFF5DB;" 410 | "width: 100vw;" 411 | "margin: auto;" 412 | "margin-top: 10px;" 413 | "text-align: center; " 414 | "padding: 2px 0px;" 415 | "font-size: 13px;" 416 | "position: relative")} 417 | (dom/createDom 418 | "a" 419 | #js {:onclick (fn [e] 420 | (.preventDefault e) 421 | (close-bad-compile-screen)) 422 | :href "javascript:" 423 | :style "position: absolute; right: 10px; top: 10px; color: #666"} 424 | "X") 425 | (dom/createDom "h2" #js {:style "color: #FFF5DB"} 426 | "Figwheel Says: Your code didn't compile.") 427 | (dom/createDom "div" #js {:style "font-size: 12px"} 428 | (dom/createDom "p" #js { :style "color: #D07D7D;"} 429 | "Keep trying. This page will auto-refresh when your code compiles successfully.") 430 | )))))) 431 | -------------------------------------------------------------------------------- /src/figwheel/core.cljc: -------------------------------------------------------------------------------- 1 | (ns figwheel.core 2 | (:require 3 | #?@(:cljs 4 | [[figwheel.tools.heads-up :as heads-up] 5 | [goog.object :as gobj] 6 | [goog.string :as gstring] 7 | [goog.string.format] 8 | [goog.log :as glog]]) 9 | [clojure.set :refer [difference]] 10 | [clojure.string :as string] 11 | #?@(:clj 12 | [[cljs.env :as env] 13 | [cljs.compiler] 14 | [cljs.closure] 15 | [cljs.repl] 16 | [cljs.analyzer :as ana] 17 | [cljs.build.api :as bapi] 18 | [clojure.data.json :as json] 19 | [clojure.java.io :as io] 20 | [clojure.edn :as edn] 21 | [clojure.tools.reader :as redr] 22 | [clojure.tools.reader.edn :as redn] 23 | [clojure.tools.reader.reader-types :as rtypes] 24 | [figwheel.tools.exceptions :as fig-ex]])) 25 | #?(:cljs (:require-macros [figwheel.core])) 26 | (:import #?@(:cljs [[goog.debug Console] 27 | [goog.async Deferred] 28 | [goog Promise] 29 | [goog.events EventTarget Event]]))) 30 | 31 | ;; ------------------------------------------------- 32 | ;; utils 33 | ;; ------------------------------------------------- 34 | 35 | (defn distinct-by [f coll] 36 | (let [seen (volatile! #{})] 37 | (filter #(let [k (f %) 38 | res (not (@seen k))] 39 | (vswap! seen conj k) 40 | res) 41 | coll))) 42 | 43 | (defn map-keys [f coll] 44 | (into {} 45 | (map (fn [[k v]] [(f k) v])) 46 | coll)) 47 | 48 | ;; ------------------------------------------------------ 49 | ;; inline code message formatting 50 | ;; ------------------------------------------------------ 51 | 52 | (def ^:dynamic *inline-code-message-max-column* 80) 53 | 54 | (defn wrap-line [text size] 55 | (re-seq (re-pattern (str ".{1," size "}\\s|.{1," size "}")) 56 | (str (string/replace text #"\n" " ") " "))) 57 | 58 | (defn cross-format [& args] 59 | (apply #?(:clj format :cljs gstring/format) args)) 60 | 61 | ;; TODO this could be more sophisticated 62 | (defn- pointer-message-lines [{:keys [message column]}] 63 | (if (> (+ column (count message)) *inline-code-message-max-column*) 64 | (->> (wrap-line message (- *inline-code-message-max-column* 10)) 65 | (map #(cross-format (str "%" *inline-code-message-max-column* "s") %)) 66 | (cons (cross-format (let [col (dec column)] 67 | (str "%" 68 | (when-not (zero? col) col) 69 | "s%s")) 70 | "" "^---")) 71 | (mapv #(vec (concat [:error-message nil] [%])))) 72 | [[:error-message nil (cross-format 73 | (let [col (dec column)] 74 | (str "%" 75 | (when-not (zero? col) col) 76 | "s%s %s" )) 77 | "" "^---" message)]])) 78 | 79 | (defn inline-message-display-data [{:keys [message line column file-excerpt] :as message-data}] 80 | (when file-excerpt 81 | (let [{:keys [start-line path excerpt]} file-excerpt 82 | lines (map-indexed 83 | (fn [i l] (let [ln (+ i start-line)] 84 | (vector (if (= line ln) :error-in-code :code-line) ln l))) 85 | (string/split-lines excerpt)) 86 | [begin end] (split-with #(not= :error-in-code (first %)) lines)] 87 | (concat 88 | (take-last 5 begin) 89 | (take 1 end) 90 | (pointer-message-lines message-data) 91 | (take 5 (rest end)))))) 92 | 93 | (defn file-line-column [{:keys [file line column]}] 94 | (cond-> "" 95 | file (str "file " file) 96 | line (str " at line " line) 97 | (and line column) (str ", column " column))) 98 | 99 | #?(:cljs 100 | (do 101 | 102 | ;; -------------------------------------------------- 103 | ;; Logging 104 | ;; -------------------------------------------------- 105 | ;; 106 | ;; Levels 107 | ;; goog.debug.Logger.Level.(SEVERE WARNING INFO CONFIG FINE FINER FINEST) 108 | ;; 109 | ;; set level (.setLevel logger goog.debug.Logger.Level.INFO) 110 | ;; disable (.setCapturing log-console false) 111 | 112 | (defonce logger (.call glog/getLogger nil "Figwheel")) 113 | 114 | (defn glog-info [log msg] 115 | (.call glog/info nil log msg)) 116 | 117 | (defn glog-warning [log msg] 118 | (.call glog/warning nil log msg)) 119 | 120 | (defn glog-error [log msg] 121 | (.call glog/error nil log msg)) 122 | 123 | (defn ^:export console-logging [] 124 | (when-not (gobj/get goog.debug.Console "instance") 125 | (let [c (goog.debug.Console.)] 126 | ;; don't display time 127 | (doto (.getFormatter c) 128 | (gobj/set "showAbsoluteTime" false) 129 | (gobj/set "showRelativeTime" false)) 130 | (gobj/set goog.debug.Console "instance" c) 131 | c)) 132 | (when-let [console-instance (gobj/get goog.debug.Console "instance")] 133 | (.setCapturing console-instance true) 134 | true)) 135 | 136 | (defonce log-console (console-logging)) 137 | 138 | ;; -------------------------------------------------- 139 | ;; Cross Platform event dispatch 140 | ;; -------------------------------------------------- 141 | (def ^:export event-target (if (exists? js/document) 142 | js/document 143 | (EventTarget.))) 144 | 145 | (defonce listener-key-map (atom {})) 146 | 147 | (defn unlisten [ky event-name] 148 | (when-let [f (get @listener-key-map ky)] 149 | (.removeEventListener event-target (name event-name) f))) 150 | 151 | (defn listen [ky event-name f] 152 | (unlisten ky event-name) 153 | (.addEventListener event-target (name event-name) f) 154 | (swap! listener-key-map assoc ky f)) 155 | 156 | (defn dispatch-event [event-name data] 157 | (.dispatchEvent 158 | event-target 159 | (doto (if (instance? EventTarget event-target) 160 | (Event. (name event-name) event-target) 161 | (js/Event. (name event-name) event-target)) 162 | (gobj/add "data" (or data {}))))) 163 | 164 | (defn event-data [e] 165 | (gobj/get 166 | (if-let [e (.-event_ e)] e e) 167 | "data")) 168 | 169 | ;; ------------------------------------------------------------ 170 | ;; Global state 171 | ;; ------------------------------------------------------------ 172 | 173 | (goog-define load-warninged-code false) 174 | (goog-define heads-up-display true) 175 | 176 | (defonce state (atom {::reload-state {}})) 177 | 178 | ;; ------------------------------------------------------------ 179 | ;; Heads up display logic 180 | ;; ------------------------------------------------------------ 181 | 182 | ;; TODO could move the state atom and heads up display logic to heads-up display 183 | ;; TODO could probably make it run completely off of events emitted here 184 | 185 | (defn heads-up-display? [] 186 | (and heads-up-display 187 | (not (nil? goog/global.document)))) 188 | 189 | (let [last-reload-timestamp (atom 0) 190 | promise-chain (Promise. (fn [r _] (r true)))] 191 | (defn render-watcher [_ _ o n] 192 | (when (heads-up-display?) 193 | ;; a new reload has arrived 194 | (if-let [ts (when-let [ts (get-in n [::reload-state :reload-started])] 195 | (and (< @last-reload-timestamp ts) ts))] 196 | (let [warnings (not-empty (get-in n [::reload-state :warnings])) 197 | exception (get-in n [::reload-state :exception])] 198 | (reset! last-reload-timestamp ts) 199 | (cond 200 | warnings 201 | (.then promise-chain 202 | (fn [] (let [warn (first warnings)] 203 | (binding [*inline-code-message-max-column* 132] 204 | (.then (heads-up/display-warning (assoc warn :error-inline (inline-message-display-data warn))) 205 | (fn [] 206 | (doseq [w (rest warnings)] 207 | (heads-up/append-warning-message w)))))))) 208 | exception 209 | (.then promise-chain 210 | (fn [] 211 | (binding [*inline-code-message-max-column* 132] 212 | (heads-up/display-exception 213 | (assoc exception :error-inline (inline-message-display-data exception)))))) 214 | :else 215 | (.then promise-chain (fn [] (heads-up/flash-loaded))))))))) 216 | 217 | (add-watch state ::render-watcher render-watcher) 218 | 219 | ;; ------------------------------------------------------------ 220 | ;; Namespace reloading 221 | ;; ------------------------------------------------------------ 222 | 223 | (defn immutable-ns? [ns] 224 | (let [ns (name ns)] 225 | (or (#{"goog" "cljs.core" "cljs.nodejs" 226 | "figwheel.preload" 227 | "figwheel.connect"} ns) 228 | (goog.string/startsWith "clojure." ns) 229 | (goog.string/startsWith "goog." ns)))) 230 | 231 | (defn ns-exists? [ns] 232 | (some? (reduce (fnil gobj/get #js{}) 233 | goog.global (string/split (name ns) ".")))) 234 | 235 | (defn reload-ns? [namespace] 236 | (let [meta-data (meta namespace)] 237 | (and 238 | (not (immutable-ns? namespace)) 239 | (not (:figwheel-no-load meta-data)) 240 | (or 241 | (:figwheel-always meta-data) 242 | (:figwheel-load meta-data) 243 | ;; don't reload it if it doesn't exist 244 | (ns-exists? namespace))))) 245 | 246 | ;; ---------------------------------------------------------------- 247 | ;; TODOS 248 | ;; ---------------------------------------------------------------- 249 | 250 | ;; look at what metadata you are sending when you reload namespaces 251 | 252 | 253 | ;; don't unprovide for things with no-load meta data 254 | ;; look more closely at getting a signal for reloading from the env/compiler 255 | ;; have an interface that just take the current compiler env and returns a list of namespaces to reload 256 | 257 | ;; ---------------------------------------------------------------- 258 | ;; reloading namespaces 259 | ;; ---------------------------------------------------------------- 260 | 261 | (defn call-hooks [hook-key & args] 262 | (let [hooks (keep (fn [[n mdata]] 263 | (when-let [f (get-in mdata [:figwheel-hooks hook-key])] 264 | [n f])) 265 | (:figwheel.core/metadata @state))] 266 | (doseq [[n f] hooks] 267 | (if-let [hook (reduce #(when %1 268 | (gobj/get %1 %2)) 269 | goog.global 270 | (map str (concat (string/split n #"\.") [f])))] 271 | (do 272 | (glog-info logger (str "Calling " (pr-str hook-key) " hook - " n "." f)) 273 | (try 274 | (apply hook args) 275 | (catch js/Error e 276 | (glog-error logger e)))) 277 | (glog-warning logger (str "Unable to find " (pr-str hook-key) " hook - " n "." f)))))) 278 | 279 | (defn ^:export reload-namespaces [namespaces figwheel-meta] 280 | ;; reconstruct serialized data 281 | (let [figwheel-meta (into {} 282 | (map (fn [[k v]] [(name k) v])) 283 | (js->clj figwheel-meta :keywordize-keys true)) 284 | namespaces (map #(with-meta (symbol %) 285 | (get figwheel-meta %)) 286 | namespaces)] 287 | (swap! state #(-> % 288 | (assoc ::metadata figwheel-meta) 289 | (assoc-in [::reload-state :reload-started] (.getTime (js/Date.))))) 290 | (let [to-reload 291 | (when-not (and (not load-warninged-code) 292 | (not-empty (get-in @state [::reload-state :warnings]))) 293 | (filter #(reload-ns? %) namespaces))] 294 | (when-not (empty? to-reload) 295 | (call-hooks :before-load {:namespaces namespaces}) 296 | (js/setTimeout #(dispatch-event :figwheel.before-load {:namespaces namespaces}) 0)) 297 | (doseq [ns to-reload] 298 | ;; goog/require has to be patched by a repl bootstrap 299 | (goog/require (name ns) true)) 300 | (let [after-reload-fn 301 | (fn [] 302 | (try 303 | (when (not-empty to-reload) 304 | (glog-info logger (str "loaded " (pr-str to-reload))) 305 | (call-hooks :after-load {:reloaded-namespaces to-reload}) 306 | (dispatch-event :figwheel.after-load {:reloaded-namespaces to-reload})) 307 | (when-let [not-loaded (not-empty (filter (complement (set to-reload)) namespaces))] 308 | (glog-info logger (str "did not load " (pr-str not-loaded)))) 309 | (finally 310 | (swap! state assoc ::reload-state {}))))] 311 | (if (and (exists? js/figwheel.repl) 312 | (exists? js/figwheel.repl.after_reloads)) 313 | (js/figwheel.repl.after_reloads after-reload-fn) 314 | (js/setTimeout after-reload-fn 100))) 315 | nil))) 316 | 317 | ;; ---------------------------------------------------------------- 318 | ;; compiler warnings 319 | ;; ---------------------------------------------------------------- 320 | 321 | (defn ^:export compile-warnings [warnings] 322 | (when-not (empty? warnings) 323 | (js/setTimeout #(dispatch-event :figwheel.compile-warnings {:warnings warnings}) 0)) 324 | (swap! state update-in [::reload-state :warnings] concat warnings) 325 | (doseq [warning warnings] 326 | (glog-warning logger (str "Compile Warning - " (:message warning) " in " (file-line-column warning))))) 327 | 328 | (defn ^:export compile-warnings-remote [warnings-json] 329 | (compile-warnings (js->clj warnings-json :keywordize-keys true))) 330 | 331 | ;; ---------------------------------------------------------------- 332 | ;; exceptions 333 | ;; ---------------------------------------------------------------- 334 | 335 | (defn ^:export handle-exception [{:keys [file type message] :as exception-data}] 336 | (try 337 | (js/setTimeout #(dispatch-event :figwheel.compile-exception exception-data) 0) 338 | (swap! state #(-> % 339 | (assoc-in [::reload-state :reload-started] (.getTime (js/Date.))) 340 | (assoc-in [::reload-state :exception] exception-data))) 341 | (glog-warning 342 | logger 343 | (cond-> "Compile Exception - " 344 | (or type message) (str (string/join " : " (filter some? [type message]))) 345 | file (str " in " (file-line-column exception-data)))) 346 | (finally 347 | (swap! state assoc-in [::reload-state] {})))) 348 | 349 | (defn ^:export handle-exception-remote [exception-data] 350 | (handle-exception (js->clj exception-data :keywordize-keys true))))) 351 | 352 | #?(:clj 353 | (do 354 | 355 | (def ^:dynamic *config* {:hot-reload-cljs true 356 | :broadcast-reload true 357 | :reload-dependents true}) 358 | 359 | (defn debug-prn [& args] 360 | (binding [*out* *err*] 361 | (apply prn args))) 362 | 363 | (def scratch (atom {})) 364 | 365 | (defonce last-compiler-env (atom {})) 366 | 367 | (defn client-eval [code] 368 | (when-not (string/blank? code) 369 | (cljs.repl/-evaluate 370 | (cond-> cljs.repl/*repl-env* 371 | (:broadcast-reload *config* true) 372 | (assoc :broadcast true)) 373 | "" 1 374 | code))) 375 | 376 | (defn hooks-for-namespace [ns] 377 | (into {} 378 | (keep 379 | (fn [[k v]] 380 | (when-let [hook (first 381 | (filter 382 | (set (keys (:meta v))) 383 | [:before-load :after-load]))] 384 | [hook 385 | (cljs.compiler/munge k)])) 386 | (get-in @cljs.env/*compiler* [:cljs.analyzer/namespaces ns :defs])))) 387 | 388 | (defn find-figwheel-meta [] 389 | (into {} 390 | (comp 391 | (map :ns) 392 | (map (juxt 393 | identity 394 | #(select-keys 395 | (meta %) 396 | [:figwheel-always :figwheel-load :figwheel-no-load :figwheel-hooks]))) 397 | (filter (comp not-empty second)) 398 | (map (fn [[ns m]] 399 | (if (:figwheel-hooks m) 400 | [ns (assoc m :figwheel-hooks (hooks-for-namespace ns))] 401 | [ns m])))) 402 | (:sources @env/*compiler*))) 403 | 404 | (defn in-upper-level? [topo-state current-depth dep] 405 | (some (fn [[_ v]] (and v (v dep))) 406 | (filter (fn [[k v]] (> k current-depth)) topo-state))) 407 | 408 | (defn build-topo-sort [get-deps] 409 | (let [get-deps (memoize get-deps)] 410 | (letfn [(topo-sort-helper* [x depth state] 411 | (let [deps (get-deps x)] 412 | (when-not (empty? deps) (topo-sort* deps depth state)))) 413 | (topo-sort* 414 | ([deps] 415 | (topo-sort* deps 0 (atom (sorted-map)))) 416 | ([deps depth state] 417 | (swap! state update-in [depth] (fnil into #{}) deps) 418 | (doseq [dep deps] 419 | (when (and dep (not (in-upper-level? @state depth dep))) 420 | (topo-sort-helper* dep (inc depth) state))) 421 | (when (= depth 0) 422 | (elim-dups* (reverse (vals @state)))))) 423 | (elim-dups* [[x & xs]] 424 | (if (nil? x) 425 | (list) 426 | (cons x (elim-dups* (map #(clojure.set/difference % x) xs)))))] 427 | topo-sort*))) 428 | 429 | (defn invert-deps [sources] 430 | (apply merge-with concat 431 | {} 432 | (map (fn [{:keys [requires ns]}] 433 | (reduce #(assoc %1 %2 [ns]) {} requires)) 434 | sources))) 435 | 436 | (defn expand-to-dependents [deps] 437 | (reverse (apply concat 438 | ((build-topo-sort (invert-deps (:sources @env/*compiler*))) 439 | deps)))) 440 | 441 | (defn sources-with-paths [files sources] 442 | (let [files (set files)] 443 | (filter 444 | #(when-let [source-file (:source-file %)] 445 | (when (instance? java.io.File source-file) 446 | (files (.getCanonicalPath source-file)))) 447 | sources))) 448 | 449 | (defn js-dependencies-with-file-urls [js-dependency-index] 450 | (distinct-by :url 451 | (filter #(when-let [u (:url %)] 452 | (= "file" (.getProtocol u))) 453 | (vals js-dependency-index)))) 454 | 455 | (defn js-dependencies-with-paths [files js-dependency-index] 456 | (let [files (set files)] 457 | (distinct 458 | (filter 459 | #(when-let [source-file (.getFile (:url %))] 460 | (files source-file)) 461 | (js-dependencies-with-file-urls js-dependency-index))))) 462 | 463 | (defn read-clj-forms [eof file] 464 | (let [reader (rtypes/source-logging-push-back-reader (io/reader file))] 465 | (repeatedly 466 | #(try 467 | (redr/read {:read-cond :allow :features #{:clj} :eof eof} reader) 468 | (catch Throwable t 469 | eof))))) 470 | 471 | (defn parse-clj-ns [file] 472 | (let [eof (Object.) 473 | res (first 474 | (filter #(or 475 | (= eof %) 476 | (and (list? %) 477 | (= (first %) 'ns))) 478 | (read-clj-forms eof file)))] 479 | (when (not= res eof) 480 | (second res)))) 481 | 482 | (defn clj-paths->namespaces [paths] 483 | (->> paths 484 | (map io/file) 485 | (filter #(.isFile %)) 486 | (keep parse-clj-ns) 487 | distinct)) 488 | 489 | (defn figwheel-always-namespaces [figwheel-ns-meta] 490 | (keep (fn [[k v]] (when (:figwheel-always v) k)) 491 | figwheel-ns-meta)) 492 | 493 | (defn sources->namespaces-to-reload [sources] 494 | (let [namespace-syms (map :ns (filter :source-file sources))] 495 | (distinct 496 | (concat 497 | (cond-> namespace-syms 498 | (and (not-empty namespace-syms) 499 | (:reload-dependents *config* true)) 500 | expand-to-dependents) 501 | (map symbol 502 | (mapcat :provides (filter :url sources))))))) 503 | 504 | (defn paths->namespaces-to-reload [paths] 505 | (let [cljs-paths (filter #(or (.endsWith % ".cljs") 506 | (.endsWith % ".cljc")) 507 | paths) 508 | js-paths (filter #(.endsWith % ".js") paths) 509 | clj-paths (filter #(.endsWith % ".clj") paths)] 510 | (distinct 511 | (concat 512 | (sources->namespaces-to-reload 513 | (concat 514 | (when-not (empty? cljs-paths) 515 | (sources-with-paths cljs-paths (:sources @env/*compiler*))) 516 | (when-not (empty? js-paths) 517 | (js-dependencies-with-paths 518 | js-paths 519 | (:js-dependency-index @env/*compiler*))))) 520 | (when-not (empty? clj-paths) 521 | (bapi/cljs-dependents-for-macro-namespaces 522 | env/*compiler* 523 | (clj-paths->namespaces clj-paths))))))) 524 | 525 | (defn require-map [env] 526 | (->> env 527 | :sources 528 | (map (juxt :ns :requires)) 529 | (into {}))) 530 | 531 | (defn changed-dependency-tree? [previous-compiler-env compiler-env] 532 | (not= (require-map previous-compiler-env) (require-map compiler-env))) 533 | 534 | (defn get-sources [ns-sym opts] 535 | (seq 536 | (when-not (ana/node-module-dep? ns-sym) 537 | (when-let [input (try (cljs.repl/ns->input ns-sym opts) 538 | (catch Throwable t nil))] 539 | (if (contains? input :source-file) 540 | (->> (cljs.closure/compile-inputs [input] 541 | (merge {:optimizations :none 542 | :npm-deps false} 543 | opts)) 544 | (remove (comp #{["goog"]} :provides))) 545 | (map #(cljs.closure/source-on-disk opts %) 546 | (cljs.closure/add-js-sources [input] opts))))))) 547 | 548 | (defn add-dependencies-js [ns-sym opts] 549 | (let [sb (StringBuffer.)] 550 | (doseq [source (get-sources ns-sym opts)] 551 | (with-open [rdr (io/reader (:url source))] 552 | (.append sb (cljs.closure/add-dep-string opts source)))) 553 | (.toString sb))) 554 | 555 | (defn all-add-dependencies [ns-syms opts] 556 | (string/join 557 | "\n" 558 | (distinct 559 | (mapcat #(filter 560 | (complement string/blank?) 561 | (string/split-lines %)) 562 | (concat 563 | ;; this is strange because foreign libs aren't being included in add-dependencies above 564 | (let [deps-file (io/file (:output-dir opts "out") "cljs_deps.js")] 565 | (when-let [deps-data (and (.isFile deps-file) (slurp deps-file))] 566 | (when-not (string/blank? deps-data) 567 | [deps-data]))) 568 | (filter 569 | #(string? %) 570 | (keep 571 | #(add-dependencies-js % opts) 572 | ns-syms))))))) 573 | 574 | (defn output-dir [] 575 | (-> @env/*compiler* :options :output-dir (or "out"))) 576 | 577 | (defn root-namespaces [env] 578 | (clojure.set/difference (->> env :sources (mapv :ns) (into #{})) 579 | (->> env :sources (map :requires) (reduce into #{})))) 580 | 581 | ;; TODO since this is the only fn that needs state perhaps isolate 582 | ;; last compiler state here? 583 | (defn all-dependency-code [ns-syms] 584 | (let [last-env (get @last-compiler-env env/*compiler*)] 585 | (when (or (nil? last-env) (changed-dependency-tree? last-env @env/*compiler*)) 586 | (let [roots (root-namespaces @env/*compiler*)] 587 | (all-add-dependencies 588 | roots 589 | (merge 590 | {:output-dir "out" 591 | :optimizations :none} 592 | (:options @env/*compiler*))))))) 593 | 594 | (defn json-write-str [arg] 595 | (try 596 | (json/write-str arg) 597 | (catch Throwable t 598 | (when-let [log (resolve 'figwheel.main.logging/info)] 599 | (log "Can't convert to json!!") 600 | (log (pr-str arg)) 601 | (log (Throwable->map t))) 602 | (json/write-str "")))) 603 | 604 | ;; TODO change this to reload_namespace_remote interface 605 | ;; I think we only need the meta data for the current symbols 606 | ;; better to send objects that hold a namespace and its meta data 607 | ;; and have a function that reassembles this on the other side 608 | ;; this will allow us to add arbitrary data and pehaps change the 609 | ;; serialization in the future 610 | (defn reload-namespace-code [ns-syms] 611 | (str (all-dependency-code ns-syms) 612 | (format "figwheel.core.reload_namespaces(%s,%s)" 613 | (json-write-str (mapv cljs.compiler/munge ns-syms)) 614 | (json-write-str (map-keys cljs.compiler/munge (find-figwheel-meta)))))) 615 | 616 | (defn reload-namespaces [ns-syms] 617 | (let [ns-syms (if (false? (:hot-reload-cljs *config*)) [] ns-syms) 618 | ret (client-eval (reload-namespace-code ns-syms))] 619 | ;; currently we are saveing the value of the compiler env 620 | ;; so that we can detect if the dependency tree changed 621 | (swap! last-compiler-env assoc env/*compiler* @env/*compiler*) 622 | ret)) 623 | 624 | ;; ------------------------------------------------------------- 625 | ;; reload clojure namespaces 626 | ;; ------------------------------------------------------------- 627 | 628 | ;; keep in mind that you need to reload clj namespaces before cljs compiling 629 | (defn reload-clj-namespaces [nses] 630 | (doseq [ns nses] (require ns :reload)) 631 | (when (not-empty nses) 632 | ;; we are going to make internal exceptions behave differently 633 | (try 634 | (let [affected-nses (bapi/cljs-dependents-for-macro-namespaces env/*compiler* nses)] 635 | (doseq [ns affected-nses] 636 | (bapi/mark-cljs-ns-for-recompile! ns (output-dir))) 637 | affected-nses) 638 | (catch Throwable t 639 | (throw (ex-info "Error Figwheel.Core's Clojure File reloading" {::internal true} t)))))) 640 | 641 | (defn reload-clj-files [files] 642 | (reload-clj-namespaces (clj-paths->namespaces files))) 643 | 644 | ;; ------------------------------------------------------------- 645 | ;; warnings 646 | ;; ------------------------------------------------------------- 647 | 648 | (defn str-excerpt [code-str start length & [path]] 649 | (cond-> 650 | {:start-line start 651 | :excerpt (->> (string/split-lines code-str) 652 | (drop (dec start)) 653 | (take length) 654 | (string/join "\n"))} 655 | path (assoc :path path))) 656 | 657 | (defn file-excerpt [file start length] 658 | (str-excerpt (slurp file) start length (.getCanonicalPath file))) 659 | 660 | (defn warning-info [{:keys [warning-type env extra path]}] 661 | (when warning-type 662 | (let [file (and path (io/file path)) 663 | path (if (and (not (string? path)) file) 664 | (str file) 665 | path) 666 | line (:line env) 667 | file-excerpt (when (and file (.isFile file)) 668 | (file-excerpt file (max 1 (- line 10)) 20)) 669 | message (cljs.analyzer/error-message warning-type extra)] 670 | (cond-> {:warning-type warning-type 671 | :line (:line env) 672 | :column (:column env) 673 | :ns (-> env :ns :name)} 674 | message (assoc :message message) 675 | path (assoc :file path) 676 | file-excerpt (assoc :file-excerpt file-excerpt))))) 677 | 678 | (defn warnings->warning-infos [warnings] 679 | (->> warnings 680 | (map warning-info) 681 | not-empty)) 682 | 683 | (defn compiler-warnings-code [warning-infos] 684 | (format "figwheel.core.compile_warnings_remote(%s);" 685 | (json-write-str warning-infos))) 686 | 687 | (defn handle-warnings [warnings] 688 | (when-let [warns (warnings->warning-infos warnings)] 689 | (client-eval (compiler-warnings-code warns)))) 690 | 691 | (comment 692 | 693 | (binding [cljs.env/*compiler* (atom (second (first @last-compiler-env)))] 694 | (let [paths (:paths @scratch)] 695 | (expand-to-dependents (paths->namespaces-to-reload paths)) 696 | #_(sources-with-paths paths (:sources @cljs.env/*compiler*)) 697 | )) 698 | 699 | (def x 700 | (first 701 | (filter (comp cljs.analyzer/*cljs-warnings* :warning-type) (:warnings @scratch)))) 702 | (:warning-data @scratch) 703 | (count (:parsed-warning @scratch)) 704 | 705 | (warnings->warning-infos (:warnings @scratch)) 706 | 707 | (handle-warnings (:warnings @scratch)) 708 | 709 | ) 710 | 711 | ;; ------------------------------------------------------------- 712 | ;; exceptions 713 | ;; ------------------------------------------------------------- 714 | 715 | (defn exception-code [parsed-exception] 716 | (let [parsable-data? 717 | (try (some-> parsed-exception :data pr-str edn/read-string) 718 | (catch Throwable t 719 | false)) 720 | parsed-exception' (cond-> parsed-exception 721 | (not parsable-data?) (dissoc :data))] 722 | (format "figwheel.core.handle_exception_remote(%s);" 723 | (-> (cond-> parsed-exception' 724 | (:tag parsed-exception') 725 | (update :tag #(string/join "/" ((juxt namespace name) %)))) 726 | pr-str 727 | edn/read-string 728 | json-write-str)))) 729 | 730 | (defn handle-exception [exception-o-throwable-map] 731 | (let [{:keys [file line] :as parsed-ex} (fig-ex/parse-exception exception-o-throwable-map) 732 | file-excerpt (when (and file line (.exists (io/file file))) 733 | (file-excerpt (io/file file) (max 1 (- line 10)) 20)) 734 | parsed-ex (cond-> parsed-ex 735 | file-excerpt (assoc :file-excerpt file-excerpt))] 736 | (when parsed-ex 737 | (client-eval 738 | (exception-code parsed-ex))))) 739 | 740 | (comment 741 | (require 'figwheel.tools.exceptions-test) 742 | 743 | (handle-exception (figwheel.tools.exceptions-test/fetch-exception "(defn")) 744 | ) 745 | 746 | 747 | ;; ------------------------------------------------------------- 748 | ;; listening for changes 749 | ;; ------------------------------------------------------------- 750 | 751 | (defn all-sources [compiler-env] 752 | (concat 753 | (filter :source-file (:sources compiler-env)) 754 | (js-dependencies-with-file-urls (:js-dependency-index compiler-env)))) 755 | 756 | (defn source-file [source-o-js-dep] 757 | (let [f (cond 758 | (:url source-o-js-dep) (io/file (.getFile (:url source-o-js-dep))) 759 | (:source-file source-o-js-dep) (:source-file source-o-js-dep))] 760 | (when (instance? java.io.File f) f))) 761 | 762 | (defn sources->modified-map [sources] 763 | (into {} 764 | (comp 765 | (keep source-file) 766 | (map (juxt #(.getCanonicalPath %) #(.lastModified %)))) 767 | sources)) 768 | 769 | (defn sources-modified [compiler-env last-modifieds] 770 | (doall 771 | (keep 772 | (fn [source] 773 | (when-let [file' (source-file source)] 774 | (let [path (.getCanonicalPath file') 775 | last-modified' (.lastModified file') 776 | last-modified (get last-modifieds path 0)] 777 | (when (> last-modified' last-modified) 778 | (vary-meta source assoc ::last-modified last-modified'))))) 779 | (all-sources compiler-env)))) 780 | 781 | (defn sources-modified! [compiler-env last-modified-vol] 782 | (let [modified-sources (sources-modified compiler-env @last-modified-vol)] 783 | (vswap! last-modified-vol merge (sources->modified-map modified-sources)) 784 | modified-sources)) 785 | 786 | (defn start* 787 | ([] (start* *config* env/*compiler* cljs.repl/*repl-env*)) 788 | ([config compiler-env repl-env] 789 | (add-watch 790 | compiler-env 791 | ::watch-hook 792 | (let [last-modified (volatile! (sources->modified-map (all-sources @compiler-env)))] 793 | (fn [_ _ o n] 794 | (let [compile-data (-> n meta ::compile-data)] 795 | (when (and (not= (-> o meta ::compile-data) compile-data) 796 | (not-empty (-> n meta ::compile-data))) 797 | (cond 798 | (and (:finished compile-data) 799 | (not (:exception compile-data))) 800 | (binding [env/*compiler* compiler-env 801 | cljs.repl/*repl-env* repl-env 802 | *config* config] 803 | (let [namespaces 804 | (if (contains? compile-data :changed-files) 805 | (paths->namespaces-to-reload (:changed-files compile-data)) 806 | (->> (sources-modified! @compiler-env last-modified) 807 | (sources->namespaces-to-reload)))] 808 | (when-let [warnings (not-empty (:warnings compile-data))] 809 | (handle-warnings warnings)) 810 | (reload-namespaces namespaces))) 811 | (:exception compile-data) 812 | (binding [env/*compiler* compiler-env 813 | cljs.repl/*repl-env* repl-env 814 | *config* config] 815 | (handle-exception (:exception compile-data))) 816 | ;; next cond 817 | :else nil 818 | )))))))) 819 | 820 | ;; TODO this is still really rough, not quite sure about this yet 821 | (defmacro start-from-repl 822 | ([] 823 | (start*) nil) 824 | ([config] 825 | (start*) 826 | (when config 827 | `(swap! state merge ~config)))) 828 | 829 | (defn stop 830 | ([] (stop env/*compiler*)) 831 | ([compiler-env] (remove-watch compiler-env ::watch-hook))) 832 | 833 | ;; ------------------------------------------------------------- 834 | ;; building 835 | ;; ------------------------------------------------------------- 836 | 837 | (defn notify-on-exception [compiler-env e extra-data] 838 | (doto compiler-env 839 | (swap! vary-meta assoc ::compile-data 840 | {:started (System/currentTimeMillis)}) 841 | (swap! vary-meta update ::compile-data 842 | (fn [x] 843 | (merge (select-keys x [:started]) 844 | extra-data 845 | {:exception e 846 | :finished (System/currentTimeMillis)}))))) 847 | 848 | ;; TODO should handle case of already having changed files 849 | (let [cljs-build cljs.closure/build] 850 | (defn build 851 | ([src opts] 852 | (with-redefs [cljs.closure/build build] 853 | (cljs-build src opts))) 854 | ([src opts compiler-env & [changed-files]] 855 | (assert compiler-env "should have a compiler env") 856 | (let [local-data (volatile! {})] 857 | (binding [cljs.analyzer/*cljs-warning-handlers* 858 | (conj cljs.analyzer/*cljs-warning-handlers* 859 | (fn [warning-type env extra] 860 | (when (warning-type cljs.analyzer/*cljs-warnings*) 861 | (vswap! local-data update :warnings 862 | (fnil conj []) 863 | {:warning-type warning-type 864 | :env env 865 | :extra extra 866 | :path ana/*cljs-file*}))))] 867 | (try 868 | (swap! compiler-env vary-meta assoc ::compile-data {:started (System/currentTimeMillis)}) 869 | (let [res (cljs-build src opts compiler-env)] 870 | (swap! compiler-env 871 | vary-meta 872 | update ::compile-data 873 | (fn [x] 874 | (merge (select-keys x [:started]) 875 | @local-data 876 | (cond-> {:finished (System/currentTimeMillis)} 877 | (some? changed-files) ;; accept empty list here 878 | (assoc :changed-files changed-files))))) 879 | res) 880 | (catch Throwable e 881 | (swap! compiler-env 882 | vary-meta 883 | update ::compile-data 884 | (fn [x] 885 | (merge (select-keys x [:started]) 886 | @local-data 887 | {:exception e 888 | :finished (System/currentTimeMillis)}))) 889 | (throw e)) 890 | (finally 891 | (swap! compiler-env vary-meta assoc ::compile-data {}))))))) 892 | 893 | ;; invasive hook of cljs.closure/build 894 | (defn hook-cljs-closure-build [] 895 | (when (and (= cljs-build cljs.closure/build) (not= build cljs.closure/build)) 896 | (alter-var-root #'cljs.closure/build (fn [_] build)))) 897 | 898 | (defmacro hook-cljs-build [] 899 | (hook-cljs-closure-build) 900 | nil) 901 | ) 902 | 903 | (comment 904 | 905 | (binding [cljs.env/*compiler* cenv] 906 | (add-dependencies-js 'figwheel.core "out")) 907 | 908 | (def cenv (cljs.env/default-compiler-env)) 909 | 910 | (:cljs.analyzer/namespaces @cenv) 911 | 912 | (get-in @cenv [:cljs.analyzer/namespaces 'figwheel.core :defs]) 913 | 914 | #_(clojure.java.shell/sh "rm" "-rf" "out") 915 | (build "src" {:main 'figwheel.core} cenv) 916 | 917 | (binding [cljs.env/*compiler* cenv] 918 | (find-figwheel-meta)) 919 | 920 | (first (cljs.js-deps/load-library* "src")) 921 | 922 | (bapi/cljs-dependents-for-macro-namespaces (atom (first (vals @last-compiler-env))) 923 | '[example.macros]) 924 | 925 | (swap! scratch assoc :require-map2 (require-map (first (vals @last-compiler-env)))) 926 | 927 | (def last-modifieds (volatile! (sources->modified-map (all-sources (first (vals @last-compiler-env)))))) 928 | 929 | (map source-file 930 | (all-sources (first (vals @last-compiler-env)))) 931 | 932 | (let [compile-env (atom (first (vals @last-compiler-env)))] 933 | (binding [env/*compiler* compile-env] 934 | (paths->namespaces-to-reload [(.getCanonicalPath (io/file "src/example/fun_tester.js"))]) 935 | 936 | )) 937 | (secon (:js-dependency-index (first (vals @last-compiler-env)))) 938 | (js-dependencies-with-file-urls (:js-dependency-index (first (vals @last-compiler-env)))) 939 | (filter (complement #(or (.startsWith % "goog") (.startsWith % "proto"))) 940 | (mapcat :provides (vals (:js-dependency-index (first (vals @last-compiler-env)))))) 941 | (map :provides (all-sources (first (vals @last-compiler-env)))) 942 | (sources-last-modified (first (vals @last-compiler-env))) 943 | 944 | 945 | (map source-file ) 946 | 947 | 948 | 949 | 950 | 951 | 952 | (js-dependencies-with-file-urls (:js-dependency-index (first (vals @last-compiler-env)))) 953 | 954 | (distinct (filter #(= "file" (.getProtocol %)) (keep :url (vals )))) 955 | 956 | (def save (:files @scratch)) 957 | 958 | (clj-files->namespaces ["/Users/bhauman/workspace/lein-figwheel/example/src/example/macros.clj"]) 959 | (js-dependencies-with-paths save (:js-dependency-index )) 960 | (namespaces-for-paths ["/Users/bhauman/workspace/lein-figwheel/example/src/example/macros.clj"] 961 | (first (vals @last-compiler-env))) 962 | 963 | (= (-> @scratch :require-map) 964 | (-> @scratch :require-map2) 965 | ) 966 | 967 | (binding [env/*compiler* (atom (first (vals @last-compiler-env)))] 968 | #_(add-dependiencies-js 'example.core (output-dir)) 969 | #_(all-add-dependencies '[example.core figwheel.preload] 970 | (output-dir)) 971 | #_(reload-namespace-code '[example.core]) 972 | (find-figwheel-meta) 973 | ) 974 | 975 | #_(require 'cljs.core) 976 | 977 | (count @last-compiler-env) 978 | (map :requires (:sources (first (vals @last-compiler-env)))) 979 | (expand-to-dependents (:sources (first (vals @last-compiler-env))) '[example.fun-tester]) 980 | 981 | (def scratch (atom {})) 982 | (def comp-env (atom nil)) 983 | 984 | (first (:files @scratch)) 985 | (.getAbsolutePath (:source-file (first (:sources @comp-env)))) 986 | (sources-with-paths (:files @scratch) (:sources @comp-env)) 987 | (invert-deps (:sources @comp-env)) 988 | (expand-to-dependents (:sources @comp-env) '[figwheel.client.utils]) 989 | (clojure.java.shell/sh "touch" "cljs_src/figwheel_helper/core.cljs") 990 | ) 991 | 992 | 993 | ) 994 | 995 | 996 | 997 | 998 | 999 | 1000 | ) 1001 | --------------------------------------------------------------------------------