Skip to content

Commit

Permalink
shadow: more resilient and correct shadow hook
Browse files Browse the repository at this point in the history
The previous implementation from v2 runs after compiling the cljs files,
since that's the only point in the shadow hooks where the list of
recompiled files is available. This is problematic though. Imagine
adding

  #?(:clj (def foo 1))

and using it in an `e/server` block. Since the cljs compiler runs first
the namespace may not be reloaded on clj and the compiler will fail,
throwing an exception.

The new implementation monkey-patches the shadow compiler in order to
recompile on clj first, fixing the use case above.
  • Loading branch information
xificurC committed May 2, 2024
1 parent 9a183bb commit 944984c
Showing 1 changed file with 22 additions and 20 deletions.
42 changes: 22 additions & 20 deletions src/hyperfiddle/electric/shadow_cljs/hooks_de.clj
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
(ns hyperfiddle.electric.shadow-cljs.hooks-de
(:require [clojure.string :as str]
(:require [shadow.build.compiler]
[hyperfiddle.electric.impl.lang-de2 :as lang]
[hyperfiddle.electric.impl.cljs-analyzer2 :as cljs-ana]))

(let [!first-run? (volatile! true)] ; first run is noop
(defn reload-clj
"When any Electric def is changed, recompile it in both Clojure and ClojureScript
(because the expression may contain e/client and/or e/server). Takes care to prevent
double reloads (i.e. from :require-macros)."
{:shadow.build/stage :compile-finish} [build-state]
(prn ::reload-hook)
(if @!first-run?
(vreset! !first-run? false)
(when (= :dev (:shadow.build/mode build-state))
(let [compiled-keys (-> build-state :shadow.build/build-info :compiled)
cljc-infos (eduction (filter (fn [[_ f]] (str/ends-with? f ".cljc")))
(map #(get (:sources build-state) %)) compiled-keys)]
(doseq [{ns-sym :ns, macro-requires :macro-requires} cljc-infos]
(when (and (not (get macro-requires ns-sym)) (-> ns-sym find-ns meta ::lang/has-edef?))
(prn ::reloading ns-sym)
(swap! lang/!a cljs-ana/purge-ns ns-sym)
(require ns-sym :reload))))))
build-state))
;; Shadow-cljs doesn't expose a way to act before compiling a cljs file.
;; It filters resources in a series of functions, calling `do-compile-cljs-resource` in the end.
;; So we wrap this final step and alter the var.
(defonce original-do-compile-cljs-resource shadow.build.compiler/do-compile-cljs-resource)
(def !built-this-cycle (atom #{})) ; build once per cycle
(defonce first-compile? true) ; on first compile we don't need to recompile
(defn wrapped-do-compile-cljs-resource [state {ns$ :ns :as rc} source]
(swap! lang/!a cljs-ana/purge-ns ns$)
(when (and (not (@!built-this-cycle ns$)) (some-> (find-ns ns$) meta ::lang/has-edef?))
(prn ::recompile-clj ns$)
(require ns$ :reload))
(original-do-compile-cljs-resource state rc source))

(defn reload-clj "On `e/defn` change, recompile Clojure namespace (because the expression
may contain e/client and/or e/server). Prevents double-reloads (e.g. from :require-macros)."
{:shadow.build/stage :compile-finish} [build-state]
(when first-compile?
(alter-var-root #'first-compile? not)
(alter-var-root #'shadow.build.compiler/do-compile-cljs-resource (constantly #'wrapped-do-compile-cljs-resource)))
(reset! !built-this-cycle #{})
build-state)

0 comments on commit 944984c

Please sign in to comment.