(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.string :as gstring] [clojure.string :as string] [clojure.set :refer [difference]] [cljs.core.async :refer [put! chan path [ns] (dev-assert (string? ns)) (aget js/goog.dependencies_.nameToPath ns)) (defn provided? [ns] (aget js/goog.dependencies_.written (name->path ns))) (defn immutable-ns? [name] (or (#{"goog" "cljs.core" "an.existing.path" "dup.base" "far.out" "ns" "someprotopackage.TestPackageTypes" "svgpan.SvgPan" "testDep.bar"} name) (some (partial goog.string/startsWith name) ["goog." "cljs." "clojure." "fake." "proto2."]))) (defn get-requires [ns] (->> ns name->path (aget 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 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] (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)] (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 reload-file* (condp = (utils/host-env?) :node (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)))))) :html (fn [request-url callback] (dev-assert (string? request-url) (not (nil? callback))) (let [deferred (loader/load (add-cache-buster request-url) #js { :cleanupWhenDone true })] (.addCallback deferred #(apply callback [true])) (.addErrback deferred #(apply callback [false])))) :worker (fn [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)))) (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)) (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 (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))))