♻️ Use rx streams for style dictionary interface

This commit is contained in:
Florian Schroedl 2025-05-09 11:01:09 +02:00 committed by Andrés Moya
parent 9f414b6ecd
commit 31f642ed25
6 changed files with 115 additions and 122 deletions

View file

@ -202,20 +202,20 @@
(build-dictionary [_] (build-dictionary [_]
(let [platform "json" (let [platform "json"
config' (clj->js config)] config' (clj->js config)
(-> (sd. config') build+ (-> (sd. config')
(.buildAllPlatforms platform) (.buildAllPlatforms platform)
(p/then #(.getPlatformTokens ^js % platform)) (p/then #(.getPlatformTokens ^js % platform))
(p/then #(.-allTokens ^js %)))))) (p/then #(.-allTokens ^js %)))]
(rx/from build+))))
(defn resolve-tokens-tree+ (defn resolve-tokens-tree
([tokens-tree get-token] ([tokens-tree get-token]
(resolve-tokens-tree+ tokens-tree get-token (StyleDictionary. default-config))) (resolve-tokens-tree tokens-tree get-token (StyleDictionary. default-config)))
([tokens-tree get-token style-dictionary] ([tokens-tree get-token style-dictionary]
(let [sdict (-> style-dictionary (->> (add-tokens style-dictionary tokens-tree)
(add-tokens tokens-tree) (build-dictionary)
(build-dictionary))] (rx/map #(process-sd-tokens % get-token)))))
(p/fmap #(process-sd-tokens % get-token) sdict))))
(defn sd-token-name [^js sd-token] (defn sd-token-name [^js sd-token]
(.. sd-token -original -name)) (.. sd-token -original -name))
@ -223,12 +223,12 @@
(defn sd-token-uuid [^js sd-token] (defn sd-token-uuid [^js sd-token]
(uuid (.-uuid (.-id ^js sd-token)))) (uuid (.-uuid (.-id ^js sd-token))))
(defn resolve-tokens+ (defn resolve-tokens
[tokens] [tokens]
(let [tokens-tree (ctob/tokens-tree tokens)] (let [tokens-tree (ctob/tokens-tree tokens)]
(resolve-tokens-tree+ tokens-tree #(get tokens (sd-token-name %))))) (resolve-tokens-tree tokens-tree #(get tokens (sd-token-name %)))))
(defn resolve-tokens-interactive+ (defn resolve-tokens-interactive
"Interactive check of resolving tokens. "Interactive check of resolving tokens.
Uses a ids map to backtrace the original token from the resolved StyleDictionary token. Uses a ids map to backtrace the original token from the resolved StyleDictionary token.
@ -241,10 +241,10 @@
this way after the resolving computation we can restore any token, even clashing ones with the same :name path by just looking up that :id in the ids map." this way after the resolving computation we can restore any token, even clashing ones with the same :name path by just looking up that :id in the ids map."
[tokens] [tokens]
(let [{:keys [tokens-tree ids]} (ctob/backtrace-tokens-tree tokens)] (let [{:keys [tokens-tree ids]} (ctob/backtrace-tokens-tree tokens)]
(resolve-tokens-tree+ tokens-tree #(get ids (sd-token-uuid %))))) (resolve-tokens-tree tokens-tree #(get ids (sd-token-uuid %)))))
(defn resolve-tokens-with-errors+ [tokens] (defn resolve-tokens-with-errors [tokens]
(resolve-tokens-tree+ (resolve-tokens-tree
(ctob/tokens-tree tokens) (ctob/tokens-tree tokens)
#(get tokens (sd-token-name %)) #(get tokens (sd-token-name %))
(StyleDictionary. (assoc default-config :log {:verbosity "verbose"})))) (StyleDictionary. (assoc default-config :log {:verbosity "verbose"}))))
@ -346,24 +346,23 @@
(when unknown-tokens (when unknown-tokens
(st/emit! (tokens-of-unknown-type-warning unknown-tokens))) (st/emit! (tokens-of-unknown-type-warning unknown-tokens)))
(try (try
(-> (ctob/get-all-tokens tokens-lib) (->> (ctob/get-all-tokens tokens-lib)
(resolve-tokens-with-errors+) (resolve-tokens-with-errors)
(p/then (fn [_] tokens-lib)) (rx/map (fn [_]
(p/catch (fn [sd-error] tokens-lib))
(let [reference-errors (reference-errors sd-error)] (rx/catch (fn [sd-error]
;; We allow reference errors for the users to resolve in the ui and throw on any other errors (let [reference-errors (reference-errors sd-error)]
(if reference-errors ;; We allow reference errors for the users to resolve in the ui and throw on any other errors
(p/resolved tokens-lib) (if reference-errors
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error sd-error sd-error))))))) (rx/of tokens-lib)
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error sd-error sd-error)))))))
(catch js/Error e (catch js/Error e
(p/rejected (wte/error-ex-info :error.import/style-dictionary-unknown-error "" e)))))))))) (throw (wte/error-ex-info :error.import/style-dictionary-unknown-error "" e))))))))))
;; === Hooks ;; === Hooks
(defonce !tokens-cache (atom nil)) (defonce !tokens-cache (atom nil))
(defonce !theme-tokens-cache (atom nil))
(defn use-resolved-tokens (defn use-resolved-tokens
"The StyleDictionary process function is async, so we can't use resolved values directly. "The StyleDictionary process function is async, so we can't use resolved values directly.
@ -382,19 +381,17 @@
(cond (cond
(nil? tokens) nil (nil? tokens) nil
;; The tokens are already processing somewhere ;; The tokens are already processing somewhere
(p/promise? cached) (-> cached (rx/observable? cached) (rx/sub! cached #(reset! tokens-state %))
(p/then #(reset! tokens-state %))
#_(p/catch js/console.error))
;; Get the cached entry ;; Get the cached entry
(some? cached) (reset! tokens-state cached) (some? cached) (reset! tokens-state cached)
;; No cached entry, start processing ;; No cached entry, start processing
:else (let [promise+ (if interactive? :else (let [resolved-tokens-s (if interactive?
(resolve-tokens-interactive+ tokens) (resolve-tokens-interactive tokens)
(resolve-tokens+ tokens))] (resolve-tokens tokens))]
(swap! cache-atom assoc tokens promise+) (swap! cache-atom assoc tokens resolved-tokens-s)
(p/then promise+ (fn [resolved-tokens] (rx/sub! resolved-tokens-s (fn [resolved-tokens]
(swap! cache-atom assoc tokens resolved-tokens) (swap! cache-atom assoc tokens resolved-tokens)
(reset! tokens-state resolved-tokens))))))) (reset! tokens-state resolved-tokens)))))))
@tokens-state)) @tokens-state))
(defn use-resolved-tokens* (defn use-resolved-tokens*
@ -408,12 +405,12 @@
(mf/with-effect [tokens interactive?] (mf/with-effect [tokens interactive?]
(if (seq tokens) (if (seq tokens)
(let [tpoint (dt/tpoint-ms) (let [tpoint (dt/tpoint-ms)
promise (if interactive? tokens-s (if interactive?
(resolve-tokens-interactive+ tokens) (resolve-tokens-interactive tokens)
(resolve-tokens+ tokens))] (resolve-tokens tokens))]
(->> promise (-> tokens-s
(p/fmap (fn [resolved-tokens] (rx/sub! (fn [resolved-tokens]
(let [elapsed (tpoint)] (let [elapsed (tpoint)]
(l/dbg :hint "use-resolved-tokens*" :elapsed elapsed) (l/dbg :hint "use-resolved-tokens*" :elapsed elapsed)
(reset! state* resolved-tokens)))))) (reset! state* resolved-tokens))))))

View file

@ -45,7 +45,7 @@
(when-let [tokens (some-> (dsh/lookup-file-data state) (when-let [tokens (some-> (dsh/lookup-file-data state)
(get :tokens-lib) (get :tokens-lib)
(ctob/get-active-themes-set-tokens))] (ctob/get-active-themes-set-tokens))]
(->> (rx/from (sd/resolve-tokens+ tokens)) (->> (sd/resolve-tokens tokens)
(rx/mapcat (rx/mapcat
(fn [resolved-tokens] (fn [resolved-tokens]
(let [undo-id (js/Symbol) (let [undo-id (js/Symbol)

View file

@ -185,12 +185,11 @@
(watch [_ state _] (watch [_ state _]
(when-let [tokens-lib (-> (dsh/lookup-file-data state) (when-let [tokens-lib (-> (dsh/lookup-file-data state)
(get :tokens-lib))] (get :tokens-lib))]
(let [tokens (-> (ctob/get-active-themes-set-tokens tokens-lib) (->> (ctob/get-active-themes-set-tokens tokens-lib)
(sd/resolve-tokens+))] (sd/resolve-tokens)
(->> (rx/from tokens) (rx/mapcat (fn [sd-tokens]
(rx/mapcat (fn [sd-tokens] (let [undo-id (js/Symbol)]
(let [undo-id (js/Symbol)] (rx/concat
(rx/concat (rx/of (dwu/start-undo-transaction undo-id :timeout false))
(rx/of (dwu/start-undo-transaction undo-id :timeout false)) (propagate-tokens state sd-tokens)
(propagate-tokens state sd-tokens) (rx/of (dwu/commit-undo-transaction undo-id)))))))))))
(rx/of (dwu/commit-undo-transaction undo-id))))))))))))

View file

@ -36,10 +36,10 @@
[app.util.functions :as uf] [app.util.functions :as uf]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.keyboard :as k] [app.util.keyboard :as k]
[beicon.v2.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
[malli.core :as m] [malli.core :as m]
[malli.error :as me] [malli.error :as me]
[promesa.core :as p]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
;; Schemas --------------------------------------------------------------------- ;; Schemas ---------------------------------------------------------------------
@ -94,14 +94,9 @@
(defn valid-value? [value] (defn valid-value? [value]
(seq (finalize-value value))) (seq (finalize-value value)))
(defn schema-validation->promise [validated]
(if (:errors validated)
(p/rejected validated)
(p/resolved validated)))
;; Component ------------------------------------------------------------------- ;; Component -------------------------------------------------------------------
(defn validate-token-value+ (defn validate-token-value
"Validates token value by resolving the value `input` using `StyleDictionary`. "Validates token value by resolving the value `input` using `StyleDictionary`.
Returns a promise of either resolved tokens or rejects with an error state." Returns a promise of either resolved tokens or rejects with an error state."
[{:keys [value name-value token tokens]}] [{:keys [value name-value token tokens]}]
@ -110,10 +105,10 @@
token-name (if (str/empty? name-value) "__TOKEN_STUDIO_SYSTEM.TEMP" name-value)] token-name (if (str/empty? name-value) "__TOKEN_STUDIO_SYSTEM.TEMP" name-value)]
(cond (cond
(empty? (str/trim value)) (empty? (str/trim value))
(p/rejected {:errors [(wte/get-error-code :error.token/empty-input)]}) (rx/throw {:errors [(wte/get-error-code :error.token/empty-input)]})
(ctob/token-value-self-reference? token-name value) (ctob/token-value-self-reference? token-name value)
(p/rejected {:errors [(wte/get-error-code :error.token/direct-self-reference)]}) (rx/throw {:errors [(wte/get-error-code :error.token/direct-self-reference)]})
:else :else
(let [tokens' (cond-> tokens (let [tokens' (cond-> tokens
@ -122,14 +117,14 @@
:always (update token-name #(ctob/make-token (merge % {:value value :always (update token-name #(ctob/make-token (merge % {:value value
:name token-name :name token-name
:type (:type token)}))))] :type (:type token)}))))]
(-> tokens' (->> tokens'
(sd/resolve-tokens-interactive+) (sd/resolve-tokens-interactive)
(p/then (rx/mapcat
(fn [resolved-tokens] (fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)] (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)]
(cond (cond
resolved-value (p/resolved resolved-token) resolved-value (rx/of resolved-token)
:else (p/rejected {:errors (or errors (wte/get-error-code :error/unknown-error))})))))))))) :else (rx/throw {:errors (or errors (wte/get-error-code :error/unknown-error))}))))))))))
(defn use-debonced-resolve-callback (defn use-debonced-resolve-callback
"Resolves a token values using `StyleDictionary`. "Resolves a token values using `StyleDictionary`.
@ -148,14 +143,14 @@
(js/setTimeout (js/setTimeout
(fn [] (fn []
(when (not (timeout-outdated-cb?)) (when (not (timeout-outdated-cb?))
(-> (validate-token-value+ {:value value (->> (validate-token-value {:value value
:name-value @name-ref :name-value @name-ref
:token token :token token
:tokens tokens}) :tokens tokens})
(p/finally (rx/filter #(not (timeout-outdated-cb?)))
(fn [x err] (rx/subs!
(when-not (timeout-outdated-cb?) callback
(callback (or err x)))))))) callback))))
timeout))))] timeout))))]
debounced-resolver-callback)) debounced-resolver-callback))
@ -442,33 +437,36 @@
;; and the user might have edited a valid form to make it invalid, ;; and the user might have edited a valid form to make it invalid,
;; and press enter before the next validations could return. ;; and press enter before the next validations could return.
(let [final-name (finalize-name @token-name-ref) (let [final-name (finalize-name @token-name-ref)
valid-name?+ (-> (validate-name final-name) schema-validation->promise) valid-name? (try
(not (:errors (validate-name final-name)))
(catch js/Error _ nil))
final-value (finalize-value (mf/ref-val value-ref)) final-value (finalize-value (mf/ref-val value-ref))
final-description @description-ref final-description @description-ref
valid-description?+ (some-> final-description validate-descripion schema-validation->promise)] valid-description? (if final-description
(-> (p/all [valid-name?+ (try
valid-description?+ (not (:errors (validate-descripion final-description)))
(validate-token-value+ {:value final-value (catch js/Error _ nil))
:name-value final-name true)]
:token token (when (and valid-name? valid-description?)
:tokens active-theme-tokens})]) (->> (validate-token-value {:value final-value
(p/finally (fn [result err] :name-value final-name
;; The result should be a vector of all resolved validations :token token
;; We do not handle the error case as it will be handled by the components validations :tokens active-theme-tokens})
(when (and (seq result) (not err)) (rx/subs!
(st/emit! (fn []
(if (ctob/token? token) (st/emit!
(dwtl/update-token (:name token) (if (ctob/token? token)
{:name final-name (dwtl/update-token (:name token)
:value final-value {:name final-name
:description final-description}) :value final-value
:description final-description})
(dwtl/create-token {:name final-name (dwtl/create-token {:name final-name
:type token-type :type token-type
:value final-value :value final-value
:description final-description})) :description final-description}))
(dwtp/propagate-workspace-tokens) (dwtp/propagate-workspace-tokens)
(modal/hide))))))))) (modal/hide)))))))))
on-delete-token on-delete-token
(mf/use-fn (mf/use-fn

View file

@ -30,9 +30,9 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [data (dsh/lookup-file-data state)] (let [data (dsh/lookup-file-data state)]
(->> (rx/from (-> (get data :tokens-lib) (->> (get data :tokens-lib)
(ctob/get-active-themes-set-tokens) (ctob/get-active-themes-set-tokens)
(sd/resolve-tokens+))) (sd/resolve-tokens)
(rx/mapcat #(rx/of (end)))))))) (rx/mapcat #(rx/of (end))))))))
(defn stop-on (defn stop-on

View file

@ -10,8 +10,7 @@
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.main.data.style-dictionary :as sd] [app.main.data.style-dictionary :as sd]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cljs.test :as t :include-macros true] [cljs.test :as t :include-macros true]))
[promesa.core :as p]))
(t/deftest resolve-tokens-test (t/deftest resolve-tokens-test
(t/async (t/async
@ -35,23 +34,23 @@
:value "{borderRadius.sm} * 200000000" :value "{borderRadius.sm} * 200000000"
:type :border-radius})) :type :border-radius}))
(ctob/get-all-tokens))] (ctob/get-all-tokens))]
(-> (sd/resolve-tokens+ tokens) (-> (sd/resolve-tokens tokens)
(p/finally (rx/sub!
(fn [resolved-tokens] (fn [resolved-tokens]
(t/is (= 12 (get-in resolved-tokens ["borderRadius.sm" :resolved-value]))) (t/is (= 12 (get-in resolved-tokens ["borderRadius.sm" :resolved-value])))
(t/is (= "px" (get-in resolved-tokens ["borderRadius.sm" :unit]))) (t/is (= "px" (get-in resolved-tokens ["borderRadius.sm" :unit])))
(t/is (= 24 (get-in resolved-tokens ["borderRadius.md-with-dashes" :resolved-value]))) (t/is (= 24 (get-in resolved-tokens ["borderRadius.md-with-dashes" :resolved-value])))
(t/is (= "px" (get-in resolved-tokens ["borderRadius.md-with-dashes" :unit]))) (t/is (= "px" (get-in resolved-tokens ["borderRadius.md-with-dashes" :unit])))
(t/is (nil? (get-in resolved-tokens ["borderRadius.large" :resolved-value]))) (t/is (nil? (get-in resolved-tokens ["borderRadius.large" :resolved-value])))
(t/is (= :error.token/number-too-large (t/is (= :error.token/number-too-large
(get-in resolved-tokens ["borderRadius.large" :errors 0 :error/code]))) (get-in resolved-tokens ["borderRadius.large" :errors 0 :error/code])))
(t/is (nil? (get-in resolved-tokens ["borderRadius.largePx" :resolved-value]))) (t/is (nil? (get-in resolved-tokens ["borderRadius.largePx" :resolved-value])))
(t/is (= :error.token/number-too-large (t/is (= :error.token/number-too-large
(get-in resolved-tokens ["borderRadius.largePx" :errors 0 :error/code]))) (get-in resolved-tokens ["borderRadius.largePx" :errors 0 :error/code])))
(t/is (nil? (get-in resolved-tokens ["borderRadius.largeFn" :resolved-value]))) (t/is (nil? (get-in resolved-tokens ["borderRadius.largeFn" :resolved-value])))
(t/is (= :error.token/number-too-large (t/is (= :error.token/number-too-large
(get-in resolved-tokens ["borderRadius.largeFn" :errors 0 :error/code]))) (get-in resolved-tokens ["borderRadius.largeFn" :errors 0 :error/code])))
(done)))))))) (done))))))))
(t/deftest process-json-stream-test (t/deftest process-json-stream-test
(t/async (t/async