Skip to content

Commit d99b52c

Browse files
authored
[babashka#38] Add js-interop module (babashka#59)
1 parent fe3cf7f commit d99b52c

File tree

7 files changed

+221
-2
lines changed

7 files changed

+221
-2
lines changed

deps.edn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@
1111
com.github.borkdude/sci
1212
{:git/sha "422bce7925b77cb4175e940d0d1d0f9439b46ad6"}
1313
#_{:local/root "/Users/borkdude/Dropbox/dev/clojure/babashka/sci"}
14-
applied-science/js-interop {:mvn/version "0.2.11"}}}
14+
applied-science/js-interop {:mvn/version "0.3.0"}}}

examples/js-interop/example.cljs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
(ns js-interop
2+
(:require [applied-science.js-interop :as j]))
3+
4+
(def o #js{:x #js {:y 1 :someFn (fn [x] x)}
5+
:a 1 :b 2 :c 3
6+
:someFn (fn [x] x)})
7+
8+
;; Read
9+
(prn (j/get o :x))
10+
;; currently not supported
11+
;; (j/get o .-x "fallback-value")
12+
(prn (j/get-in o [:x :y]))
13+
(prn (j/select-keys o [:a :b :c]))
14+
15+
(let [{:keys [x]} (j/lookup o)] ;; lookup wrapper
16+
(prn x))
17+
18+
;; Destructure
19+
(prn (j/let [^:js {:keys [a b c]} o]
20+
[:a a :b b :c c]))
21+
(def f (j/fn [^:js [n1 n2]] [n1 n2]))
22+
(prn (f #js [1 2]))
23+
(def g (j/fn [^:js {:keys [a b c]}] [a b c]))
24+
(prn (g o))
25+
(j/defn my-fn [^:js {:keys [a b c]}] [a b c])
26+
(prn (my-fn o))
27+
28+
;; Write
29+
(prn (j/assoc! o :a 2))
30+
(prn (j/assoc-in! o [:x :y] 100))
31+
;; currently not yet supported
32+
;; (j/assoc-in! o [.-x .-y] 100)
33+
34+
(prn (j/update! o :a inc))
35+
(prn (j/update-in! o [:x :y] + 10))
36+
37+
;; ;; Call functions
38+
(prn (j/call o :someFn 42))
39+
(prn (j/apply o :someFn #js[42]))
40+
41+
(prn (j/call-in o [:x :someFn] 42))
42+
(prn (j/apply-in o [:x :someFn] #js[42]))
43+
44+
;; ;; Create
45+
(prn (j/obj :a 1 :b 2))
46+
(prn (j/lit {:a 1 :b [2 3 4]}))

interop.cljs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
(ns interop
2+
(:require [applied-science.js-interop :as j]))
3+
4+
(prn (j/get #js{:a 1} :a))

script/nbb_tests.clj

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,35 @@
8383
(tasks/shell {:dir "test-scripts/api-test"} (npm "install"))
8484
(tasks/shell {:dir "test-scripts/api-test"} "node test.mjs"))
8585

86+
(defn normalize-js-interop-output
87+
"Functions print differently in compile vs release."
88+
[s]
89+
(str/replace s #"object\[.*\]" "object[-]"))
90+
91+
(def expected-js-interop-output (normalize-js-interop-output "#js {:y 1, :someFn #object[Function]}
92+
1
93+
#js {:a 1, :b 2, :c 3}
94+
#js {:y 1, :someFn #object[Function]}
95+
[:a 1 :b 2 :c 3]
96+
[1 2]
97+
[1 2 3]
98+
[1 2 3]
99+
#js {:x #js {:y 1, :someFn #object[Function]}, :a 2, :b 2, :c 3, :someFn #object[Function]}
100+
#js {:x #js {:y 100, :someFn #object[Function]}, :a 2, :b 2, :c 3, :someFn #object[Function]}
101+
#js {:x #js {:y 100, :someFn #object[Function]}, :a 3, :b 2, :c 3, :someFn #object[Function]}
102+
#js {:x #js {:y 110, :someFn #object[Function]}, :a 3, :b 2, :c 3, :someFn #object[Function]}
103+
42
104+
42
105+
42
106+
42
107+
#js {:a 1, :b 2}
108+
#js {:a 1, :b #js [2 3 4]}
109+
"))
110+
111+
(deftest js-interop-test
112+
(is (= expected-js-interop-output
113+
(normalize-js-interop-output (nbb* "examples/js-interop/example.cljs")))))
114+
86115
(defn main [& _]
87116
(let [{:keys [:error :fail]} (t/run-tests 'nbb-tests)]
88117
(when (pos? (+ error fail))

shadow-cljs.edn

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
:depends-on #{:nbb_core}}
1818
:nbb_promesa {:init-fn nbb.promesa/init
1919
:depends-on #{:nbb_core}}
20+
21+
:nbb_js_interop {:init-fn nbb.js-interop/init
22+
:depends-on #{:nbb_core}}
2023
:nbb_pprint {:init-fn nbb.pprint/init
2124
:depends-on #{:nbb_core}}}
2225
:build-hooks [(shadow.cljs.build-report/hook

src/nbb/core.cljs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@
9393
(load-module "./nbb_reagent.js" libname as refer rename libspecs))
9494
(promesa.core)
9595
(load-module "./nbb_promesa.js" libname as refer rename libspecs)
96+
(applied-science.js-interop)
97+
(load-module "./nbb_js_interop.js" libname as refer rename libspecs)
9698
(cljs.pprint clojure.pprint)
9799
(load-module "./nbb_pprint.js" libname as refer rename libspecs)
98100
(if (string? libname)
@@ -278,6 +280,13 @@
278280
" msecs"))
279281
ret#))
280282

283+
(defn ^:macro implements?* [_ _ psym x]
284+
;; hardcoded implementation of implements? for js-interop destructure which
285+
;; uses implements?
286+
(case psym
287+
cljs.core/ISeq (implements? ISeq x)
288+
cljs.core/INamed (implements? INamed x)))
289+
281290
(reset! sci-ctx
282291
(sci/init
283292
{:namespaces {'clojure.core {'*print-fn* io/print-fn
@@ -289,7 +298,9 @@
289298
'*command-line-args* command-line-args
290299
'*warn-on-infer* warn-on-infer
291300
'time (sci/copy-var time core-ns)
292-
'system-time (sci/copy-var system-time core-ns)}
301+
'system-time (sci/copy-var system-time core-ns)
302+
'implements? (sci/copy-var implements?* core-ns)
303+
'array (sci/copy-var array core-ns)}
293304
'nbb.core {'load-string (sci/copy-var load-string nbb-ns)
294305
'slurp (sci/copy-var slurp nbb-ns)
295306
'load-file (sci/copy-var load-file nbb-ns)

src/nbb/js_interop.cljs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
(ns nbb.js-interop
2+
(:refer-clojure :exclude [let fn defn spread])
3+
(:require
4+
[applied-science.js-interop :as j]
5+
[applied-science.js-interop.destructure :as d]
6+
[clojure.core :as c]
7+
[nbb.core :as nbb]
8+
[sci.core :as sci]))
9+
10+
(def jns (sci/create-ns 'applied-science.js-interop nil))
11+
12+
(c/defn ^:macro let
13+
"`let` with destructuring that supports js property and array access.
14+
Use ^:js metadata on the binding form to invoke. Eg/
15+
(let [^:js {:keys [a]} obj] …)"
16+
[_ _ bindings & body]
17+
(if (empty? bindings)
18+
`(do ~@body)
19+
`(~'clojure.core/let ~(vec (d/destructure (take 2 bindings)))
20+
(~'applied-science.js-interop/let
21+
~(vec (drop 2 bindings))
22+
~@body))))
23+
24+
(c/defn ^:macro fn
25+
"`fn` with argument destructuring that supports js property and array access.
26+
Use ^:js metadata on binding forms to invoke. Eg/
27+
(fn [^:js {:keys [a]}] …)"
28+
[_ _ & args]
29+
(cons 'clojure.core/fn (d/destructure-fn-args args)))
30+
31+
(c/defn ^:macro defn
32+
"`defn` with argument destructuring that supports js property and array access.
33+
Use ^:js metadata on binding forms to invoke."
34+
[_ _ & args]
35+
(cons 'clojure.core/defn (d/destructure-fn-args args)))
36+
37+
(c/defn litval* [v]
38+
(if (keyword? v)
39+
(cond->> (name v)
40+
(namespace v)
41+
(str (namespace v) "/"))
42+
v))
43+
44+
(declare lit*)
45+
46+
(defn- spread
47+
"For ~@spread values, returns the unwrapped value,
48+
otherwise returns nil."
49+
[x]
50+
(when (and (seq? x)
51+
(= 'clojure.core/unquote-splicing (first x)))
52+
(second x)))
53+
54+
(defn- tagged-sym [tag] (with-meta (gensym (name tag)) {:tag tag}))
55+
56+
(c/defn lit*
57+
"Recursively converts literal Clojure maps/vectors into JavaScript object/array expressions
58+
Options map accepts a :keyfn for custom key conversions."
59+
([x]
60+
(lit* nil x))
61+
([{:as opts
62+
:keys [keyfn valfn env]
63+
:or {keyfn identity
64+
valfn litval*}} x]
65+
(cond (map? x)
66+
(list* 'applied-science.js-interop/obj
67+
(reduce-kv #(conj %1 (keyfn %2) (lit* opts %3)) [] x))
68+
(vector? x)
69+
(if (some spread x)
70+
(c/let [sym (tagged-sym 'js/Array)]
71+
`(c/let [~sym (~'cljs.core/array)]
72+
;; handling the spread operator
73+
~@(for [x'
74+
;; chunk array members into spreads & non-spreads,
75+
;; so that sequential non-spreads can be lumped into
76+
;; a single .push
77+
(->> (partition-by spread x)
78+
(mapcat (clojure.core/fn [x]
79+
(if (spread (first x))
80+
x
81+
(list x)))))]
82+
(if-let [x' (spread x')]
83+
(if false
84+
;; for now disable this optimization
85+
#_(and env (inf/tag-in? env '#{array} x'))
86+
`(.forEach ~x' (c/fn [x#] (.push ~sym x#)))
87+
`(doseq [x# ~(lit* x')] (.push ~sym x#)))
88+
`(.push ~sym ~@(map lit* x'))))
89+
~sym))
90+
(list* 'cljs.core/array (mapv lit* x)))
91+
:else (valfn x))))
92+
93+
(c/defn ^:macro lit
94+
"Recursively converts literal Clojure maps/vectors into JavaScript object/array expressions
95+
(using j/obj and cljs.core/array)"
96+
[_ &env form]
97+
(lit* {:env &env} form))
98+
99+
(def js-interop-namespace
100+
{'get (sci/copy-var j/get jns)
101+
'get-in (sci/copy-var j/get-in jns)
102+
'contains? (sci/copy-var j/contains? jns)
103+
'select-keys (sci/copy-var j/select-keys jns)
104+
'lookup (sci/copy-var j/lookup jns)
105+
'assoc! (sci/copy-var j/assoc! jns)
106+
'assoc-in! (sci/copy-var j/assoc-in! jns)
107+
'update! (sci/copy-var j/update! jns)
108+
'update-in! (sci/copy-var j/update-in! jns)
109+
'extend! (sci/copy-var j/extend! jns)
110+
'push! (sci/copy-var j/push! jns)
111+
'unshift! (sci/copy-var j/unshift! jns)
112+
'call (sci/copy-var j/call jns)
113+
'apply (sci/copy-var j/apply jns)
114+
'call-in (sci/copy-var j/call-in jns)
115+
'apply-in (sci/copy-var j/apply-in jns)
116+
'obj (sci/copy-var j/obj jns)
117+
'let (sci/copy-var let jns)
118+
'fn (sci/copy-var fn jns)
119+
'defn (sci/copy-var defn jns)
120+
'lit (sci/copy-var lit jns)})
121+
122+
(c/defn init []
123+
(nbb/register-plugin!
124+
::js-interop
125+
{:namespaces {'applied-science.js-interop js-interop-namespace}}))
126+

0 commit comments

Comments
 (0)