Write squint functions and inline expressions in a ClojureScript project.
Squint operates on JavaScript objects and arrays, so assoc, update-in and
select-keys work on those without converting them with js->clj and
clj->js.
deps.edn:
io.github.squint-cljs/squint-inline {:git/sha "669af4a33086e87232bf16007d9ddefa43185c3f"}The squint core library is a separate npm package. This library declares it in
deps.cljs, so with shadow-cljs install it with:
clojure -M -m shadow.cljs.npm-depsVanilla ClojureScript installs it while compiling when the build sets
:install-deps true. Adding it to package.json by hand works too:
"squint-cljs": "^0.14.206"Require the squint core runtime and the squint-inline.core macro namespace:
(ns app.core
(:require ["squint-cljs/core.js" :as squint-core]
[squint-inline.core :as squint]))Use squint/defn to define a function with a squint body:
(squint/defn public-user [resp]
(-> resp
(select-keys [:id :name])
(assoc :active true)))
(public-user (js/JSON.parse "{\"id\":1,\"name\":\"ada\",\"secret\":\"x\"}"))
;; => #js {:id 1 :name "ada" :active true}Use squint/inline for an expression. Referenced ClojureScript locals are
passed in as arguments:
(let [role "admin"]
(squint/inline (assoc (js/JSON.parse "{\"id\":1}") :role role)))
;; => #js {:id 1 :role "admin"}Require JavaScript libraries in the ClojureScript namespace and use their alias in a squint body:
(ns app.core
(:require ["squint-cljs/core.js" :as squint-core]
["date-fns" :as dfns]
[squint-inline.core :as squint]))
(squint/defn overdue [rows now]
(->> rows
(filter (fn [row] (dfns/isBefore (dfns/parseISO (:due row)) now)))
(map (fn [row]
(assoc row :daysLate (dfns/differenceInDays now (dfns/parseISO (:due row))))))
vec))
(overdue (js/JSON.parse "[{\"id\":1,\"due\":\"2026-01-10\"}]")
(js/Date. "2026-01-20"))
;; => #js [#js {:id 1 :due "2026-01-10" :daysLate 10}]Inside a squint body [1 2 3] is already a JS array and {:a 1} a JS object,
so #js is optional.
A squint function can call another squint function, in the same namespace or through a namespace alias, and can pass it around as a value.
(squint/defn active? [row]
(= "ok" (:status row)))
(squint/defn actives [rows]
(vec (filter active? rows)))Other ClojureScript vars are not available inside a body.
Property access in a squint body works under :advanced without manual ^js
hints.
With shadow-cljs defaults the first squint body adds about 19 KB gzipped.
Squint's core.js is minified but not tree-shaken, so all of it ships once any
squint body is present. A second and a hundredth squint body add almost nothing
on top.
To get it tree-shaken, have shadow-cljs emit real ESM imports and let a JS bundler do the bundling.
;; shadow-cljs.edn
{:builds {:app {:target :esm
:js-options {:js-provider :import}
:modules {:main {:exports {main app.core/-main}}}}}}clojure -M -m shadow.cljs.devtools.cli release app
npx esbuild out/main.js --bundle --minify --format=esm --outfile=out/bundle.jsOne squint/defn calling assoc then costs 1,048 bytes gzipped instead of
19,365, and unused core functions are gone. The bundle is an ES module and runs
in the browser with <script type="module">. For a classic script tag, pass
--format=iife --global-name=app instead.
:js-provider :external does not help. It resolves modules by string at
runtime, so a bundler cannot drop anything.
Vanilla ClojureScript has no equivalent. :target :bundle emits
require("squint-cljs/core.js") rather than a static import, so the bundler
keeps every export and the same single function costs 19,522 bytes gzipped. The
compiler emits require for every npm library with no option to change it. Use
shadow-cljs if bundle size matters.
See examples/esm.
examples/shadow, examples/cljs and
examples/esm run the same source under :advanced.