Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

[Squint](https://github.com/squint-cljs/squint): Light-weight ClojureScript dialect

## Unreleased

- `defmulti` defines the multimethod with `defonce`, so a REPL or vite HMR module reload keeps the registered methods, like CLJS

## 0.14.206

- Fix [#960](https://github.com/squint-cljs/squint/issues/960): the nREPL server no longer logs requests, responses and eval errors by default. Enable with `--debug` (CLI), `:debug` in squint.edn, or `:debug` (`startServer`)
Expand Down
23 changes: 23 additions & 0 deletions e2e/browser_repl_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,25 @@
(str/includes? (await (.innerHTML page "#plain")) "3px dashed"))
(check "hot swap without page reload" true (await (.evaluate page "!!window.__e2e_no_reload"))))

(defn ^:async check-multimethod-hmr
"defmulti is defonce, like CLJS: hot-swapping the module that defines the
multimethod (shared.cljs) must keep methods registered from other modules
(the defmethod in plain.cljs), not reset to an empty method table."
[page shared-file orig-shared]
(check "multimethod cross-module dispatch" "circle-label"
(await (.textContent page "#mm")))
(.writeFileSync fs shared-file (str/replace orig-shared "(+ 1 2 3)" "(+ 3 4)"))
(await (with-timeout 20000 "shared.cljs hot swap"
(.waitForFunction page "document.querySelector('#reagami').textContent.includes('Shared: 7')")))
;; a stale #mm would still show the old text after a broken swap (the render
;; throws before setting innerHTML), so force a fresh render through the
;; multimethod: click the counter and require the new count to appear
(await (.click page "#plain button"))
(await (with-timeout 20000 "plain render after provider hot swap"
(.waitForFunction page "document.querySelector('#plain').textContent.includes('Counted: 2')")))
(check "multimethod survives provider hot swap" "circle-label"
(await (.textContent page "#mm"))))

;; ------------------------------------------------------------------ run ----

;; Hard safety net: never let the process hang past 2 min (unref'd so a fast
Expand All @@ -134,12 +153,15 @@
macro-file (path/join EXDIR "src" "e2e_macros.cljc")
consumer-file (path/join EXDIR "src" "plain.cljs")
ui-file (path/join EXDIR "src" "ui.cljs")
shared-file (path/join EXDIR "src" "shared.cljs")
orig-macro (.readFileSync fs macro-file "utf8")
orig-consumer (.readFileSync fs consumer-file "utf8")
orig-ui (.readFileSync fs ui-file "utf8")
orig-shared (.readFileSync fs shared-file "utf8")
restore-files! (fn [] (try (.writeFileSync fs macro-file orig-macro)
(.writeFileSync fs consumer-file orig-consumer)
(.writeFileSync fs ui-file orig-ui)
(.writeFileSync fs shared-file orig-shared)
(catch :default _ nil)))
;; safety net: the hard-timeout path calls process.exit and skips the
;; finally below, so restore on exit too (sync, idempotent)
Expand Down Expand Up @@ -170,6 +192,7 @@
(await (check-counter page "reagami (hiccup)" "#reagami"))
(await (check-macro-reload page macro-file consumer-file orig-consumer))
(await (check-after-load page ui-file orig-ui))
(await (check-multimethod-hmr page shared-file orig-shared))
(let [client (await (with-timeout 10000 "nrepl connect" (make-client NREPL-PORT)))
clone (await (with-timeout 10000 "nrepl clone" (nrepl-request client #js {:op "clone"})))
session (some (fn [m] (aget m "new-session")) (js/Array.from clone))
Expand Down
6 changes: 5 additions & 1 deletion examples/browser-repl/src/plain.cljs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
(ns plain
(:require [ui :refer [btn]])
(:require [shared]
[ui :refer [btn]])
(:require-macros [e2e-macros :refer [tag]]))

(defmethod shared/shape-label :circle [_] "circle-label")

;; Plain squint, no framework: build an HTML string with #html, set it as
;; innerHTML, and re-attach the click listener on each render. State is an atom;
;; add-watch re-renders. (The most manual of the three.)
Expand All @@ -14,6 +17,7 @@
[:div {:style (assoc ui/label :color "#0a7d5a")} "plain squint - #html"]
[:div {:style ui/counted} "Counted: " @state]
[:div {:id "macro-tag"} (tag)]
[:div {:id "mm"} (shared/shape-label {:type :circle})]
[:button {:id "plain-btn" :style btn} "Click me!"]])
(.addEventListener (.querySelector el "#plain-btn") "click"
(fn [_] (swap! state inc)))))
Expand Down
4 changes: 3 additions & 1 deletion examples/browser-repl/src/shared.cljs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
(ns shared)

(defn shared-function []
(+ 1 2 3 ))
(+ 1 2 3))

(defmulti shape-label :type)
22 changes: 17 additions & 5 deletions src/squint/compiler_common.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -1835,7 +1835,14 @@ break;}" body)
outer-html? (:outer-html (meta expr))]
(when unsafe-html? (reset! has-dynamic-expr? true))
(if (and (not html?) (:jsx env) (:jsx-runtime env))
(let [single-child? (= 1 (count elts))]
(let [single-child? (= 1 (count elts))
;; on a DOM tag, a literal map value of a well-known attr is a
;; plain-JS contract too: ::js makes cherry emit a JS object
;; literal (no-op for squint). Only literals: dynamic values and
;; component props are left to the user.
attrs (if (and (keyword? tag) (map? attrs) (map? (:style attrs)))
(update attrs :style vary-meta assoc ::js true)
attrs)]
(emit (list (if single-child?
'_jsx '_jsxs)
(cond fragment? "_Fragment"
Expand All @@ -1847,10 +1854,15 @@ break;}" body)
children
(if single-child?
(first elts)
(vec elts))]
(cond-> (or attrs {})
(seq children)
(assoc :children children))))
;; the jsx runtime wants a plain JS children array;
;; ::js makes cherry emit one (no-op for squint)
(with-meta (vec elts) {::js true}))]
;; props are a plain-JS contract for the jsx runtime;
;; ::js makes cherry emit a JS object (no-op for squint)
(-> (cond-> (or attrs {})
(seq children)
(assoc :children children))
(vary-meta assoc ::js true))))
env))
(let [expr-env* (assoc (expr-env env) :unsafe-html unsafe-html?)
ret (str
Expand Down
4 changes: 3 additions & 1 deletion src/squint/internal/multi.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
(contains? opts :hierarchy) (assoc "hierarchy" (:hierarchy opts)))
m (merge (if docstring {:doc docstring} {}) attr-map)
mm-name* (if (seq m) (with-meta mm-name m) mm-name)]
`(def ~mm-name* ~(multi-call "defmulti" [(str mm-name) dispatch-fn opts-js]))))
;; defonce so a module re-run (REPL reload, vite HMR hot swap) keeps the
;; method table and cross-module defmethods alive, like CLJS
`(defonce ~mm-name* ~(multi-call "defmulti" [(str mm-name) dispatch-fn opts-js]))))

(core/defn core-defmethod
"(defmethod multifn dispatch-val [args*] body)"
Expand Down
14 changes: 7 additions & 7 deletions test/squint/multi_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,13 @@
(deftest vector-dispatch-and-remove-test
(t/async done
(-> (eval-repl "
(defmulti conv (fn [from to _] [from to]))
(defmethod conv [:km :m] [_ _ x] (* x 1000))
(defmethod conv [:m :cm] [_ _ x] (* x 100))
(let [before (count (methods conv))
a (conv :km :m 5)
_ (remove-method conv [:m :cm])
after (count (methods conv))]
(defmulti conv2 (fn [from to _] [from to]))
(defmethod conv2 [:km :m] [_ _ x] (* x 1000))
(defmethod conv2 [:m :cm] [_ _ x] (* x 100))
(let [before (count (methods conv2))
a (conv2 :km :m 5)
_ (remove-method conv2 [:m :cm])
after (count (methods conv2))]
[a before after])")
(.then (fn [v] (is (= [5000 2 1] (vec v)))))
(.finally done))))
Expand Down
Loading