├── .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 | ")
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 |
--------------------------------------------------------------------------------