From 959983327e583aa21271706c1752ec307a458c1c Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Fri, 25 Sep 2026 11:22:20 +0200 Subject: [PATCH] feat: take h/expr and defc core names from squint's compiler --- deps.edn | 2 +- src/hyper/component.clj | 86 ++++++++++++++++++---------------- src/hyper/component/bundle.clj | 13 +---- src/hyper/expr.clj | 50 ++++++++++---------- 4 files changed, 75 insertions(+), 76 deletions(-) diff --git a/deps.edn b/deps.edn index 092fa67..28895a5 100644 --- a/deps.edn +++ b/deps.edn @@ -36,7 +36,7 @@ cheshire/cheshire {:mvn/version "6.1.0"} ;; Client-side component compilation (CLJS-dialect -> JS) - io.github.squint-cljs/squint {:mvn/version "0.14.209"} + io.github.squint-cljs/squint {:mvn/version "0.14.210"} ;; Logging com.taoensso/telemere {:mvn/version "1.2.1"}} diff --git a/src/hyper/component.clj b/src/hyper/component.clj index 50797a3..2f14057 100644 --- a/src/hyper/component.clj +++ b/src/hyper/component.clj @@ -54,6 +54,15 @@ :core-alias "$sc" :context :statement}) +(defn- compile-squint* + [source] + (try + (squint/compile* source squint-opts) + (catch Exception e + (throw (ex-info (str "Squint compilation failed: " (ex-message e)) + {:source source} + e))))) + (defn compile-squint "Compile a string of Squint (CLJS-dialect) source to JavaScript. @@ -63,12 +72,7 @@ Throws ex-info with the source attached when compilation fails." [source] - (try - (:body (squint/compile-string* source squint-opts)) - (catch Exception e - (throw (ex-info (str "Squint compilation failed: " (ex-message e)) - {:source source} - e))))) + (:body (compile-squint* source))) (defn- ns-form-source "Build a Squint ns form declaring ES module requires, so alias-qualified @@ -170,11 +174,11 @@ Returns the component name." [name spec] {:pre [(string? name) (str/includes? name "-") (:render spec)]} - (let [requires (parse-requires (:require spec)) - _ (check-alias-conflicts! name requires) - spec (-> spec (dissoc :require) (assoc :requires requires)) - js (compile-squint (registration-source name spec))] - (swap! registry* assoc name (assoc spec :js js)) + (let [requires (parse-requires (:require spec)) + _ (check-alias-conflicts! name requires) + spec (-> spec (dissoc :require) (assoc :requires requires)) + {:keys [body used-core-vars]} (compile-squint* (registration-source name spec))] + (swap! registry* assoc name (assoc spec :js body :core-vars used-core-vars)) name)) ;; --------------------------------------------------------------------------- @@ -450,42 +454,44 @@ {:style/indent 1 :style.cljfmt/indent [[:block 1] [:inner 1]]} [cname & body] - (let [[docstring body] (if (string? (first body)) - [(first body) (rest body)] - [nil body]) - [opts body] (if (map? (first body)) - [(first body) (rest body)] - [{} body]) - requires (parse-requires (:require opts)) - [binding-vec & segments] body - _ (assert (vector? binding-vec) - (str "defc " cname ": expected attribute binding vector, got " - (pr-str binding-vec))) - attr-keys (extract-attrs binding-vec) - _ (assert (seq attr-keys) - (str "defc " cname ": binding vector must use {:keys [...]} form " - "so attribute names are statically visible")) - tag-name (sym->tag cname) - _ (assert (str/includes? tag-name "-") - (str "defc " cname ": tag name \"" tag-name - "\" must contain a hyphen (Custom Elements spec)")) - parsed (parse-segments cname segments) - _ (assert (or (:render parsed) (:mount parsed)) - (str "defc " cname ": a (render ...) or (mount ...) segment is required")) - ns-str (str *ns*) - src (squint-source tag-name attr-keys parsed ns-str requires) - tag-kw (keyword tag-name) + (let [[docstring body] (if (string? (first body)) + [(first body) (rest body)] + [nil body]) + [opts body] (if (map? (first body)) + [(first body) (rest body)] + [{} body]) + requires (parse-requires (:require opts)) + [binding-vec & segments] body + _ (assert (vector? binding-vec) + (str "defc " cname ": expected attribute binding vector, got " + (pr-str binding-vec))) + attr-keys (extract-attrs binding-vec) + _ (assert (seq attr-keys) + (str "defc " cname ": binding vector must use {:keys [...]} form " + "so attribute names are statically visible")) + tag-name (sym->tag cname) + _ (assert (str/includes? tag-name "-") + (str "defc " cname ": tag name \"" tag-name + "\" must contain a hyphen (Custom Elements spec)")) + parsed (parse-segments cname segments) + _ (assert (or (:render parsed) (:mount parsed)) + (str "defc " cname ": a (render ...) or (mount ...) segment is required")) + ns-str (str *ns*) + src (squint-source tag-name attr-keys parsed ns-str requires) + tag-kw (keyword tag-name) ;; Compile at macro-expansion time so Squint errors surface at compile ;; time (with the source attached), but emit the registration into the ;; expansion so it executes at runtime. This keeps defc AOT-safe ;; (expansion-time side effects don't survive AOT compilation) and ;; testable (with-redefs of registry* works). - compiled-js (compile-squint src)] + {compiled-js :body + core-vars :used-core-vars} (compile-squint* src)] `(do (check-alias-conflicts! ~tag-name '~requires) - (swap! registry* assoc ~tag-name {:attrs ~attr-keys - :requires '~requires - :js ~compiled-js}) + (swap! registry* assoc ~tag-name {:attrs ~attr-keys + :requires '~requires + :js ~compiled-js + :core-vars ~core-vars}) (defn ~cname ~@(when docstring [docstring]) [attr-map# & children#] diff --git a/src/hyper/component/bundle.clj b/src/hyper/component/bundle.clj index 24177b1..21823e7 100644 --- a/src/hyper/component/bundle.clj +++ b/src/hyper/component/bundle.clj @@ -10,8 +10,7 @@ REPL redefinition hot-swap the bundle over SSE." (:require [clojure.java.io :as io] [clojure.string :as str] - [hyper.component :as component] - [hyper.expr :as expr])) + [hyper.component :as component])) (defn- runtime-js [] (slurp (io/resource "hyper/component-runtime.js"))) @@ -77,18 +76,10 @@ (reset! bundle-cache* {:key registry :bundle b}) b)))))) -(defonce ^:private core-vars-cache* (atom nil)) - (defn core-vars "Returns the munged squint core names that the registered components use." [] - (let [registry @component/registry* - [cached-registry names] @core-vars-cache*] - (if (identical? registry cached-registry) - names - (let [names (into #{} (mapcat #(expr/core-vars-of "$sc" (:js %))) (vals registry))] - (reset! core-vars-cache* [registry names]) - names)))) + (into #{} (mapcat :core-vars) (vals @component/registry*))) (defn head-script-tag "Hiccup script tag for the components bundle, or nil when no components diff --git a/src/hyper/expr.clj b/src/hyper/expr.clj index bbc2ff2..74b1170 100644 --- a/src/hyper/expr.clj +++ b/src/hyper/expr.clj @@ -237,13 +237,6 @@ core-vars* (atom (sorted-set))) -;; A future squint release adds the used core names to the compiler output, which replaces this regex. -(defn core-vars-of - "Returns the squint core names that js references through alias." - [alias js] - (into (sorted-set) (map second) - (re-seq (re-pattern (str (java.util.regex.Pattern/quote alias) "\\.([A-Za-z_$][\\w$]*)")) js))) - (defn use-core-vars! "Adds names to core-vars*. names is a set of munged squint core names. @@ -253,25 +246,33 @@ (swap! core-vars* into names)) js) +(defn- compile-form* + "Returns {:js ... :used-core-vars ...} for a single (boundary-inferred) form." + [form] + (let [processed (pre-process form) + ;; pr-str, not str, because embedded lazy seqs from the boundary walk would + ;; stringify as "clojure.lang.LazySeq@hash" under str. + {:keys [body used-core-vars]} + (squint/compile* (pr-str processed) + {:elide-imports true + :elide-exports true + :top-level false + :context :expr + :core-alias "hyper_sc" + :macros compiler-macros})] + {:js (-> body + replace-deref + (restore-signal-casing processed) + (str/replace #"\n" " ") + str/trim + (str/replace #";$" "")) + :used-core-vars used-core-vars})) + (defn compile-form "Compile a single (boundary-inferred) form to a Datastar expression string. Public for testing." [form] - (let [processed (pre-process form)] - ;; pr-str, not str — embedded lazy seqs from the boundary walk would - ;; stringify as "clojure.lang.LazySeq@hash" under str. - (-> (squint/compile-string (pr-str processed) - {:elide-imports true - :elide-exports true - :top-level false - :context :expr - :core-alias "hyper_sc" - :macros compiler-macros}) - replace-deref - (restore-signal-casing processed) - (str/replace #"\n" " ") - str/trim - (str/replace #";$" "")))) + (:js (compile-form* form))) ;; --------------------------------------------------------------------------- ;; Runtime splicing @@ -335,8 +336,9 @@ (let [env-locals (set (keys &env)) pairs* (atom []) forms* (mapv #(infer-boundary % env-locals pairs*) forms) - template (join-statements (map compile-form forms*)) - names (core-vars-of "hyper_sc" template) + compiled (map compile-form* forms*) + template (join-statements (map :js compiled)) + names (into #{} (mapcat :used-core-vars) compiled) pairs @pairs* js (if (empty? pairs) template