geocsv-js/docs/js/compiled/out/figwheel/client/file_reloading.cljs

564 lines
21 KiB
Clojure

(ns figwheel.client.file-reloading
(:require
[figwheel.client.utils :as utils :refer-macros [dev-assert]]
[goog.Uri :as guri]
[goog.string]
[goog.object :as gobj]
[goog.net.jsloader :as loader]
[goog.html.legacyconversions :as conv]
[goog.string :as gstring]
[clojure.string :as string]
[clojure.set :refer [difference]]
[cljs.core.async :refer [put! chan <! map< close! timeout alts!] :as async])
(:require-macros
[cljs.core.async.macros :refer [go go-loop]])
(:import [goog.async Deferred]))
(declare queued-file-reload)
(defonce figwheel-meta-pragmas (atom {}))
;; you can listen to this event easily like so:
;; document.body.addEventListener("figwheel.js-reload", function (e) {console.log(e.detail);} );
(defn on-jsload-custom-event [url]
(utils/dispatch-custom-event "figwheel.js-reload" url))
;; you can listen to this event easily like so:
;; document.body.addEventListener("figwheel.before-js-reload", function (e) { console.log(e.detail);} );
(defn before-jsload-custom-event [files]
(utils/dispatch-custom-event "figwheel.before-js-reload" files))
;; you can listen to this event easily like so:
;; document.body.addEventListener("figwheel.css-reload", function (e) {console.log(e.detail);} );
(defn on-cssload-custom-event [files]
(utils/dispatch-custom-event "figwheel.css-reload" files))
#_(defn all? [pred coll]
(reduce #(and %1 %2) true (map pred coll)))
(defn namespace-file-map? [m]
(or
(and (map? m)
(string? (:namespace m))
(or (nil? (:file m))
(string? (:file m)))
(= (:type m)
:namespace))
(do
(println "Error not namespace-file-map" (pr-str m))
false)))
;; this assumes no query string on url
(defn add-cache-buster [url]
(dev-assert (string? url))
(.makeUnique (guri/parse url)))
(defn name->path [ns]
(dev-assert (string? ns))
(gobj/get js/goog.dependencies_.nameToPath ns))
(defn provided? [ns]
(gobj/get js/goog.dependencies_.written (name->path ns)))
(defn immutable-ns? [name]
(or (#{"goog" "cljs.core" "cljs.nodejs"} name)
(goog.string/startsWith "clojure." name)
(goog.string/startsWith "goog." name)))
(defn get-requires [ns]
(->> ns
name->path
(gobj/get js/goog.dependencies_.requires)
(gobj/getKeys)
(filter #(not (immutable-ns? %)))
set))
(defonce dependency-data (atom {:pathToName {} :dependents {}}))
(defn path-to-name! [path name]
(swap! dependency-data update-in [:pathToName path] (fnil clojure.set/union #{}) #{name}))
(defn setup-path->name!
"Setup a path to name dependencies map.
That goes from path -> #{ ns-names }"
[]
;; we only need this for dependents
(let [nameToPath (gobj/filter js/goog.dependencies_.nameToPath
(fn [v k o] (gstring/startsWith v "../")))]
(gobj/forEach nameToPath (fn [v k o] (path-to-name! v k)))))
(defn path->name
"returns a set of namespaces defined by a path"
[path]
(get-in @dependency-data [:pathToName path]))
(defn name-to-parent! [ns parent-ns]
(swap! dependency-data update-in [:dependents ns] (fnil clojure.set/union #{}) #{parent-ns}))
(defn setup-ns->dependents!
"This reverses the goog.dependencies_.requires for looking up ns-dependents."
[]
(let [requires (gobj/filter js/goog.dependencies_.requires
(fn [v k o] (gstring/startsWith k "../")))]
(gobj/forEach
requires
(fn [v k _]
(gobj/forEach
v
(fn [v' k' _]
(doseq [n (path->name k)]
(name-to-parent! k' n))))))))
(defn ns->dependents [ns]
(get-in @dependency-data [:dependents ns]))
(defn in-upper-level? [topo-state current-depth dep]
(some (fn [[_ v]] (and v (v dep)))
(filter (fn [[k v]] (> k current-depth)) topo-state)))
(defn build-topo-sort [get-deps]
(let [get-deps (memoize get-deps)]
(letfn [(topo-sort-helper* [x depth state]
(let [deps (get-deps x)]
(when-not (empty? deps) (topo-sort* deps depth state))))
(topo-sort*
([deps]
(topo-sort* deps 0 (atom (sorted-map))))
([deps depth state]
(swap! state update-in [depth] (fnil into #{}) deps)
(doseq [dep deps]
(when (and dep (not (in-upper-level? @state depth dep)))
(topo-sort-helper* dep (inc depth) state)))
(when (= depth 0)
(elim-dups* (reverse (vals @state))))))
(elim-dups* [[x & xs]]
(if (nil? x)
(list)
(cons x (elim-dups* (map #(difference % x) xs)))))]
topo-sort*)))
(defn get-all-dependencies [ns]
(let [topo-sort' (build-topo-sort get-requires)]
(apply concat (topo-sort' (set [ns])))))
(defn get-all-dependents [nss]
(let [topo-sort' (build-topo-sort ns->dependents)]
(filter (comp not immutable-ns?)
(reverse (apply concat (topo-sort' (set nss)))))))
#_(prn "dependents" (get-all-dependents [ "example.core" "figwheel.client.file_reloading" "cljs.core"]))
#_(prn "dependencies" (get-all-dependencies "figwheel.client.file_reloading"))
#_(time (get-all-dependents [ "example.core" "figwheel.client.file_reloading" "cljs.core"]))
(defn unprovide! [ns]
(let [path (name->path ns)]
(gobj/remove js/goog.dependencies_.visited path)
(gobj/remove js/goog.dependencies_.written path)
(gobj/remove js/goog.dependencies_.written (str js/goog.basePath path))))
;; this matches goog behavior in url resolution should actually just
;; use that code
(defn resolve-ns [ns] (str goog/basePath (name->path ns)))
(defn addDependency [path provides requires]
(doseq [prov provides]
(path-to-name! path prov)
(doseq [req requires]
(name-to-parent! req prov))))
(defn figwheel-require [src reload]
;; require is going to be called
(set! (.-require js/goog) figwheel-require)
(when (= reload "reload-all")
(doseq [ns (get-all-dependencies src)] (unprovide! ns)))
(when reload (unprovide! src))
(.require_figwheel_backup_ js/goog src))
(defn bootstrap-goog-base
"Reusable browser REPL bootstrapping. Patches the essential functions
in goog.base to support re-loading of namespaces after page load."
[]
;; The biggest problem here is that clojure.browser.repl might have
;; patched this or might patch this afterward
(when-not js/COMPILED
(set! (.-require_figwheel_backup_ js/goog) (or js/goog.require__ js/goog.require))
;; suppress useless Google Closure error about duplicate provides
(set! (.-isProvided_ js/goog) (fn [name] false))
;; provide cljs.user
(setup-path->name!)
(setup-ns->dependents!)
(set! (.-addDependency_figwheel_backup_ js/goog) js/goog.addDependency)
(set! (.-addDependency js/goog)
(fn [& args]
(apply addDependency args)
(apply (.-addDependency_figwheel_backup_ js/goog) args)))
(goog/constructNamespace_ "cljs.user")
;; we must reuse Closure library dev time dependency management, under namespace
;; reload scenarios we simply delete entries from the correct
;; private locations
(set! (.-CLOSURE_IMPORT_SCRIPT goog/global) queued-file-reload)
(set! (.-require js/goog) figwheel-require)))
(defn patch-goog-base []
(defonce bootstrapped-cljs (do (bootstrap-goog-base) true)))
(def gloader
(cond
(exists? loader/safeLoad)
#(loader/safeLoad (conv/trustedResourceUrlFromString (str %1)) %2)
(exists? loader/load) #(loader/load (str %1) %2)
:else (throw (ex-info "No remote script loading function found." {}))))
(defn reload-file-in-html-env
[request-url callback]
(dev-assert (string? request-url) (not (nil? callback)))
(doto (gloader (add-cache-buster request-url) #js {:cleanupWhenDone true})
(.addCallback #(apply callback [true]))
(.addErrback #(apply callback [false]))))
(def ^:export write-script-tag-import reload-file-in-html-env)
(defn ^:export worker-import-script [request-url callback]
(dev-assert (string? request-url) (not (nil? callback)))
(callback (try
(do (.importScripts js/self (add-cache-buster request-url))
true)
(catch js/Error e
(utils/log :error (str "Figwheel: Error loading file " request-url))
(utils/log :error (.-stack e))
false))))
(defn ^:export create-node-script-import-fn []
(let [node-path-lib (js/require "path")
;; just finding a file that is in the cache so we can
;; figure out where we are
util-pattern (str (.-sep node-path-lib)
(.join node-path-lib "goog" "bootstrap" "nodejs.js"))
util-path (gobj/findKey js/require.cache (fn [v k o] (gstring/endsWith k util-pattern)))
parts (-> (string/split util-path #"[/\\]") pop pop)
root-path (string/join (.-sep node-path-lib) parts)]
(fn [request-url callback]
(dev-assert (string? request-url) (not (nil? callback)))
(let [cache-path (.resolve node-path-lib root-path request-url)]
(gobj/remove (.-cache js/require) cache-path)
(callback (try
(js/require cache-path)
(catch js/Error e
(utils/log :error (str "Figwheel: Error loading file " cache-path))
(utils/log :error (.-stack e))
false)))))))
;; TODO
#_(defn async-fetch-import-script [request-url callback]
(let [base-url (or goog.global.FIGWHEEL_RELOAD_BASE_URL "http://localhost:8081")]
(doto (js/fetch (str base-url "/" request-url))
(.then (fn [r] ))
)))
(def reload-file*
(condp = (utils/host-env?)
:node (create-node-script-import-fn)
:html write-script-tag-import
;; TODO react native reloading not supported internally yet
;:react-native
#_(if (utils/worker-env?)
worker-import-script
async-fetch-import-script)
:worker worker-import-script
(fn [a b] (throw "Reload not defined for this platform"))))
(defn reload-file [{:keys [request-url] :as file-msg} callback]
(dev-assert (string? request-url) (not (nil? callback)))
(utils/debug-prn (str "FigWheel: Attempting to load " request-url))
((or (gobj/get goog.global "FIGWHEEL_IMPORT_SCRIPT") reload-file*)
request-url
(fn [success?]
(if success?
(do
(utils/debug-prn (str "FigWheel: Successfully loaded " request-url))
(apply callback [(assoc file-msg :loaded-file true)]))
(do
(utils/log :error (str "Figwheel: Error loading file " request-url))
(apply callback [file-msg]))))))
;; for goog.require consumption
(defonce reload-chan (chan))
(defonce on-load-callbacks (atom {}))
(defonce dependencies-loaded (atom []))
(defn blocking-load [url]
(let [out (chan)]
(reload-file
{:request-url url}
(fn [file-msg]
(put! out file-msg)
(close! out)))
out))
(defonce reloader-loop
(go-loop []
(when-let [[url opt-source-text] (<! reload-chan)]
(cond
opt-source-text
(js/eval opt-source-text)
url
(let [file-msg (<! (blocking-load url))]
(if-let [callback (get @on-load-callbacks url)]
(callback file-msg)
(swap! dependencies-loaded conj file-msg))))
(recur))))
(defn queued-file-reload
([url] (queued-file-reload url nil))
([url opt-source-text] (put! reload-chan [url opt-source-text])))
(defn require-with-callback [{:keys [namespace] :as file-msg} callback]
(let [request-url (resolve-ns namespace)]
(swap! on-load-callbacks assoc request-url
(fn [file-msg']
(swap! on-load-callbacks dissoc request-url)
(apply callback [(merge file-msg (select-keys file-msg' [:loaded-file]))])))
;; we are forcing reload here
(figwheel-require (name namespace) true)))
(defn figwheel-no-load? [{:keys [namespace] :as file-msg}]
(let [meta-pragmas (get @figwheel-meta-pragmas (name namespace))]
(:figwheel-no-load meta-pragmas)))
(defn ns-exists? [namespace]
(some? (reduce (fnil gobj/get #js{})
goog.global (string/split (name namespace) "."))))
(defn reload-file? [{:keys [namespace] :as file-msg}]
(dev-assert (namespace-file-map? file-msg))
(let [meta-pragmas (get @figwheel-meta-pragmas (name namespace))]
(and
(not (figwheel-no-load? file-msg))
(or
(:figwheel-always meta-pragmas)
(:figwheel-load meta-pragmas)
;; might want to use .-visited here
(provided? (name namespace))
(ns-exists? namespace)))))
(defn js-reload [{:keys [request-url namespace] :as file-msg} callback]
(dev-assert (namespace-file-map? file-msg))
(if (reload-file? file-msg)
(require-with-callback file-msg callback)
(do
(utils/debug-prn (str "Figwheel: Not trying to load file " request-url))
(apply callback [file-msg]))))
(defn reload-js-file [file-msg]
(let [out (chan)]
(js-reload
file-msg
(fn [url]
#_(patch-goog-base)
(put! out url)
(close! out)))
out))
(defn load-all-js-files
"Returns a chanel with one collection of loaded filenames on it."
[files]
(let [out (chan)]
(go-loop [[f & fs] files]
(if-not (nil? f)
(do (put! out (<! (reload-js-file f)))
(recur fs))
(close! out)))
(async/into [] out)))
(defn eval-body [{:keys [eval-body file]} opts]
(when (and eval-body (string? eval-body))
(let [code eval-body]
(try
(utils/debug-prn (str "Evaling file " file))
(utils/eval-helper code opts)
(catch :default e
(utils/log :error (str "Unable to evaluate " file)))))))
(defn expand-files [files]
(let [deps (get-all-dependents (map :namespace files))]
(filter (comp not
(partial re-matches #"figwheel\.connect.*")
:namespace)
(map
(fn [n]
(if-let [file-msg (first (filter #(= (:namespace %) n) files))]
file-msg
{:type :namespace :namespace n}))
deps))))
(defn sort-files [files]
(if (<= (count files) 1) ;; no need to sort if only one
files
(let [keep-files (set (keep :namespace files))]
(filter (comp keep-files :namespace) (expand-files files)))))
(defn get-figwheel-always []
(map (fn [[k v]] {:namespace k :type :namespace})
(filter (fn [[k v]]
(:figwheel-always v)) @figwheel-meta-pragmas)))
(defn reload-js-files [{:keys [before-jsload on-jsload reload-dependents] :as opts}
{:keys [files figwheel-meta recompile-dependents] :as msg}]
(when-not (empty? figwheel-meta)
(reset! figwheel-meta-pragmas figwheel-meta))
(go
(before-jsload files)
(before-jsload-custom-event files)
;; evaluate the eval bodies first
;; for now this is only for updating dependencies
;; we are not handling removals
;; TODO handle removals
(let [eval-bodies (filter #(:eval-body %) files)]
(when (not-empty eval-bodies)
(doseq [eval-body-file eval-bodies]
(eval-body eval-body-file opts))))
(reset! dependencies-loaded (list))
(let [all-files (filter #(and (:namespace %)
(not (:eval-body %))
(not (figwheel-no-load? %)))
files)
;; add in figwheel always
all-files (concat all-files (get-figwheel-always))
all-files (if (or reload-dependents recompile-dependents)
(expand-files all-files)
(sort-files all-files))
;_ (prn "expand-files" (expand-files all-files))
;_ (prn "sort-files" (sort-files all-files))
res' (<! (load-all-js-files all-files))
res (filter :loaded-file res')
files-not-loaded (filter #(not (:loaded-file %)) res')
dependencies-that-loaded (filter :loaded-file @dependencies-loaded)]
(when (not-empty dependencies-that-loaded)
(utils/log :debug "Figwheel: loaded these dependencies")
(utils/log (pr-str (map (fn [{:keys [request-url]}]
(string/replace request-url goog/basePath ""))
(reverse dependencies-that-loaded)))))
(when (not-empty res)
(utils/log :debug "Figwheel: loaded these files")
(utils/log (pr-str (map (fn [{:keys [namespace file]}]
(if namespace
(name->path (name namespace))
file)) res)))
(js/setTimeout #(do
(on-jsload-custom-event res)
(apply on-jsload [res])) 10))
(when (not-empty files-not-loaded)
(utils/log :debug "Figwheel: NOT loading these files ")
(let [{:keys [figwheel-no-load not-required]}
(group-by
(fn [{:keys [namespace]}]
(let [meta-data (get @figwheel-meta-pragmas (name namespace))]
(cond
(nil? meta-data) :not-required
(meta-data :figwheel-no-load) :figwheel-no-load
:else :not-required)))
files-not-loaded)]
(when (not-empty figwheel-no-load)
(utils/log (str "figwheel-no-load meta-data: "
(pr-str (map (comp name->path :namespace) figwheel-no-load)))))
(when (not-empty not-required)
(utils/log (str "not required: " (pr-str (map :file not-required))))))))))
;; CSS reloading
(defn current-links []
(.call (.. js/Array -prototype -slice)
(.getElementsByTagName js/document "link")))
(defn truncate-url [url]
(-> (first (string/split url #"\?"))
(string/replace-first (str (.-protocol js/location) "//") "")
(string/replace-first ".*://" "")
(string/replace-first #"^//" "")
(string/replace-first #"[^\/]*" "")))
(defn matches-file?
[{:keys [file]} link]
(when-let [link-href (.-href link)]
(let [match (string/join "/"
(take-while identity
(map #(if (= %1 %2) %1 false)
(reverse (string/split file "/"))
(reverse (string/split (truncate-url link-href) "/")))))
match-length (count match)
file-name-length (count (last (string/split file "/")))]
(when (>= match-length file-name-length) ;; has to match more than the file name length
{:link link
:link-href link-href
:match-length match-length
:current-url-length (count (truncate-url link-href))}))))
(defn get-correct-link [f-data]
(when-let [res (first
(sort-by
(fn [{:keys [match-length current-url-length]}]
(- current-url-length match-length))
(keep #(matches-file? f-data %)
(current-links))))]
(:link res)))
(defn clone-link [link url]
(let [clone (.createElement js/document "link")]
(set! (.-rel clone) "stylesheet")
(set! (.-media clone) (.-media link))
(set! (.-disabled clone) (.-disabled link))
(set! (.-href clone) (add-cache-buster url))
clone))
(defn create-link [url]
(let [link (.createElement js/document "link")]
(set! (.-rel link) "stylesheet")
(set! (.-href link) (add-cache-buster url))
link))
(defn distinctify [key seqq]
(vals (reduce #(assoc %1 (get %2 key) %2) {} seqq)))
(defn add-link-to-document [orig-link klone finished-fn]
(let [parent (.-parentNode orig-link)]
(if (= orig-link (.-lastChild parent))
(.appendChild parent klone)
(.insertBefore parent klone (.-nextSibling orig-link)))
;; prevent css removal flash
(js/setTimeout #(do
(.removeChild parent orig-link)
(finished-fn))
300)))
(defonce reload-css-deferred-chain (atom (.succeed Deferred)))
(defn reload-css-file [f-data fin]
(if-let [link (get-correct-link f-data)]
(add-link-to-document link (clone-link link (.-href link))
#(fin (assoc f-data :loaded true)))
(fin f-data)))
(defn reload-css-files* [deferred f-datas on-cssload]
(-> deferred
(utils/mapConcatD reload-css-file f-datas)
(utils/liftContD (fn [f-datas' fin]
(let [loaded-f-datas (filter :loaded f-datas')]
(on-cssload-custom-event loaded-f-datas)
(when (fn? on-cssload)
(on-cssload loaded-f-datas)))
(fin)))))
(defn reload-css-files [{:keys [on-cssload]} {:keys [files] :as files-msg}]
(when (utils/html-env?)
(when-let [f-datas (not-empty (distinctify :file files))]
(swap! reload-css-deferred-chain reload-css-files* f-datas on-cssload))))