Squint is a light-weight dialect of ClojureScript with a compiler and standard library.
Squint is not intended as a full replacement for ClojureScript but as a tool to target JS when you need something more light-weight in terms of interop and bundle size. The most significant difference with CLJS is that squint uses only built-in JS data structures. Squint's output is designed to work well with ES modules.
If you want to use squint, but with the normal ClojureScript standard library and data structures, check out cherry.
$ mkdir squint-test && cd squint-test
$ npm init -y
$ npm install squint-cljs@latest
Create a .cljs file, e.g. example.cljs:
(ns example
(:require ["fs" :as fs]
["url" :refer [fileURLToPath]]))
(println (fs/existsSync (fileURLToPath js/import.meta.url)))
(defn foo [{:keys [a b c]}]
(+ a b c))
(println (foo {:a 1 :b 2 :c 3}))
Then compile and run (run does both):
$ npx squint run example.cljs
true
6
Run npx squint --help to see all command line options.
Squint lets you write CLJS syntax but emits small JS output, while still having parts of the CLJS standard library available (ported to mutable data structures, so with caveats). This may work especially well for projects e.g. that you'd like to deploy on CloudFlare workers, node scripts, Github actions, etc. that need the extra performance, startup time and/or small bundle size.
(slides)
"squint-cljs/core.js", a smaller re-implemented subsetconst x = [...y];Symbol.iterator, not custom data structures(def x (await y)). Async functions must be marked
with ^:async: (defn ^:async foo []). The legacy spellings js-await
and js/await are still accepted.assoc!, dissoc!, conj!, etc. perform in place mutation on objectsassoc, dissoc, conj, etc. return a new shallow copy of objectsequiv compares by identity or -equiv, unlike =, which deep-compares
plain data. hash follows equiv: plain mutable objects and arrays hash
by referencepr-str and prn print EDN with the idea that you can paste the output back into your programs
Maps are printed like maps with a #js/Map prefixIf you are looking for closer ClojureScript semantics, take a look at Cherry 🍒.
Solve Advent of Code puzzles with squint here.
Squint does not implement Clojure seqs. Instead it uses the JavaScript iteration protocols to work with collections. What this means in practice is the following:
seq takes a collection and returns an Iterable of that collection, or nil if it's emptyiterable takes a collection and returns an Iterable of that collection, even if it's emptyseqable? can be used to check if you can call either oneMost collections are iterable already, so seq and iterable will simply
return them; an exception are objects created via {:a 1}, where seq and
iterable will return the result of Object.entries.
first, rest, map, reduce et al. call iterable on the collection before
processing, and functions that typically return seqs instead return an array of
the results.
With respect to memory usage:
(js/global.gc)
(println (js/process.memoryUsage))
(defn doit []
(let [x [(-> (new Array 10000000)
(.fill 0)) :foo :bar]
;; Big array `x` is still being held on to by `y`:
y (rest x)]
(println (js/process.memoryUsage))
(vec y)))
(println (doit))
(js/global.gc)
;; Note that big array is garbage collected now:
(println (js/process.memoryUsage))
Run the above program with node --expose-gc ./node_cli mem.cljs
You can produce JSX syntax using the #jsx tag:
#jsx [:div "Hello"]
produces:
<div>Hello</div>
and outputs the .jsx extension automatically.
You can use Clojure expressions within #jsx expressions:
(let [x 1] #jsx [:div (inc x)])
Note that when using a Clojure expression, you escape the JSX context so when you need to return more JSX, use the #jsx once again:
(let [x 1]
#jsx [:div
(if (odd? x)
#jsx [:span "Odd"]
#jsx [:span "Even"])])
To pass props, you can use :&:
(let [props {:a 1}]
#jsx [App {:& props}])
See an example of an application using JSX here (source).
Play with JSX non the playground
Similar to JSX, squint allows you to produce HTML as strings using hiccup notation:
(def my-html #html [:div "Hello"])
will set the my-html variable to an HTML object equal to
<div>Hello</div>. To produce a string from the HTML object, call str on it.
HTML objects can be nested:
(defn my-html [x] #html [:<> [:div "Hello"] x])
(my-html #html [:div "Goodbye"]) ;;=> Html {s: "<div>Hello</div><div>Goodbye</div>"}
HTML content is escaped by default:
(my-html #html [:div "<>"]) ;;=> Html {s: "<div>Hello</div><div><></div>"}
You can produce unsafe (unescaped) HTML with [:$ ...]:
(str #html
[:<> [:$ "<!DOCTYPE html>"]
[:html [:head] [:body]]])
;;=> "<!DOCTYPE html><html><head></head><body></body></html>"
Using metadata you can modify the tag function, e.g. to use this together with lit-html:
(ns my-app
(:require ["lit" :as lit]))
#html ^lit/html [:div "Hello"]
This will produce:
lit/html`<div>Hello</div>`
See this playground example for a full example.
You can do static asset imports like this:
["./test.json$default" :as json :with {:type :json}]
This will produce the following code, which tools like vite can inline at compile time:
import data from "../test.json$default" with { type: "json" };
You can also import other types of assets if using vite with ?raw:
["logo.svg?raw" :as logo]
Squint supports async/await:
(defn ^:async foo [] (js/Promise.resolve 10))
(def x (await (foo)))
(println x) ;;=> 10
Anonymous functions must have ^:async on the fn symbol or the function's name:
(^:async fn [] (await {}) 3)
Generator functions must be marked with ^:gen:
(defn ^:gen foo []
(js-yield 1)
(js-yield* [2 3])
(let [x (inc 3)]
(yield x)))
(vec (foo)) ;;=> [1 2 3 4]
Anonymous functions must have ^:gen on the fn symbol:
(^:gen fn [] (js-yield 1) (js-yield 2))
See the playground for an example.
If for some reason you need to emit arrow functions () => ... rather than
function, you can use :=> metadata on the function expression, fn symbol
or argument vector:
(fn ^:=> [] 1)
See doc/defclass.md.
The JavaScript API exposes the compileString function:
import { compileString } from 'squint-cljs';
const f = eval(compileString("(fn [] 1)"
, {"context": "expr",
"elide-imports": true}
));
console.log(f()); // prints 1
Squint has a console repl which can be started with squint repl.
A (currently immature!) nREPL implementation can be used on Node.js with:
squint nrepl-server :port 1888
Please try it out and file issues so it can be improved.
You can use this together with inf-clojure in emacs as follows:
In a .cljs buffer, type M-x inf-clojure. Then enter the startup command npx squint repl (or bunx --bun repl) and select the clojure or babashka type
REPL. REPL away!
The squint-cljs/vite plugin provides a browser REPL: it hot-reloads compiled
squint in the page and runs an nREPL server that your editor connects to,
evaluating forms in the live page. See
doc/browser-repl.md.
Squint respect CLJS truth semantics: only null, undefined and false are non-truthy, 0 and "" are truthy.
A def with an earmuffed name (*foo*) is always a dynamic var in squint. It compiles to a
mutable box {val: ...}. References read .val, set! assigns it, and
binding saves, sets and restores it in a finally.
(def ^:dynamic *x* 1)
(defn f [] *x*)
(f) ;; => 1
(binding [*x* 2]
(f)) ;; => 2
(set! *x* 3)
(f) ;; => 3
The box object is shared by reference, so a binding is visible across
separately-compiled ESM modules: a module that reads *x* sees a value bound by
another module. Only the box's property is mutated, never the import binding.
To load macros, add a squint.edn file in the root of your project with
{:paths ["src-squint"]} that describes where to find your macro files. Macros
are written in .cljs or .cljc files and are executed using
SCI.
The following searches for a foo/macros.cljc file in the :paths described in squint.edn.
(ns foo (:require-macros [foo.macros :refer [my-macro]]))
(my-macro 1 2 3)
squint.ednIn squint.edn you can describe the following options:
:paths: the source paths to search for files. At the moment, only .cljc and .cljs are supported.:extension: the preferred extension to output, which defaults to .mjs, but can be set to .jsx for React(-like) projects.:copy-resources: a set of keywords that represent file extensions of files
that should be copied over from source paths. E.g. :css, :json. Strings
may also be used which represent regexes which are processed through
re-find.:output-dir: the directory where compiled files will be created,
which defaults to the project root directory.:deps: a map of dependencies from the Clojure ecosystem, in the same format
as deps.edn. Only :git/url (with :git/sha) and :local/root libraries
are supported, no jars yet. Their source directories are resolved via the
clojure CLI (-Spath) and added to :paths. Requires clojure on the
PATH. Example:
{:paths ["src"]
:deps {io.github.cjohansen/replicant {:git/sha "..."}}}
See examples/vite-react for an example project which uses a squint.edn.
See examples/replicant for one that pulls a git dependency via :deps.
Run npx squint watch to watch the source directories described in squint.edn and they will be (re-)compiled whenever they change.
See examples/vite-react for an example project which uses this.
A svelte pre-processor for squint can be found here.
See examples/vite-react.
See examples/expo-react-native.
See examples/babashka/index.clj.
--> -->Buy the t-shirt here!
Squint is licensed under the EPL. See epl-v10.html in the root directory for more information.
Can you improve this documentation? These fine people already did:
Michiel Borkent, Lee Read, Nikita Prokopov, Henrikh Kantuni, Brandon Stubbs, Chris McCormick, Rui Fernando Hayashi, Phil Baker, Shivek Khurana, Dmitri Sotnikov, shaun & Will ActonEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |