<img src="./logo/logo.svg" width="100%">
npm install squint-cljsSquint 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.
> :warning: This project is a work in progress and may still undergo breaking
> changes.
Although it's early days, you're welcome to try out squint and submit issues.
`` shell`
$ mkdir squint-test && cd squint-test
$ npm init -y
$ npm install squint-cljs@latest
Create a .cljs file, e.g. example.cljs:
` clojure
(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)
- The CLJS standard library is replaced with "squint-cljs/core.js", a smaller re-implemented subsetconst x = [...y];
- Keywords are translated into strings
- Maps, sequences and vectors are represented as mutable objects and arrays
- Standard library functions never mutate arguments if the CLJS counterpart do
not do so. Instead, shallow cloning is used to produce new values, a pattern that JS developers
nowadays use all the time: Symbol.iterator
- Most functions return arrays, objects or , not custom data structuresmap
- Functions like , filter, etc. produce lazy iterable values but theirvec
results are not cached. If side effects are used in combination with laziness,
it's recommended to realize the lazy value using on function(def x (js-await y))
boundaries. You can detect re-usage of lazy values by calling
warn-on-lazy-reusage!.
- Supports async/await:. Async functions must be marked^:async
with : (defn ^:async foo []).assoc!
- , dissoc!, conj!, etc. perform in place mutation on objectsassoc
- , dissoc, conj, etc. return a new shallow copy of objectsprintln
- is a synonym for console.logpr-str
- and prn print EDN with the idea that you can paste the output back into your programsMap
- JavaScript s are printed like maps with a #js/Map prefix
- Since JavaScript only supports strings for keys in maps, any data structures used as keys will be stringified
If you are looking for closer ClojureScript semantics, take a look at Cherry 🍒.
- Writing a Cloudflare worker with squint and bun
- Porting a CLJS project to squint
- @nextjournal/clojure-mode
- cljdoc (source)
- Eucalypt: a Reagent clone in squint
- static search index for Tumblr
- wordle
- Zenith: a game developed for the Lisp Game Jame 2024 by Trevor
- hiccupad (source)
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 one
Most collections are iterable already, so seq and iterable will simply{:a 1}
return them; an exception are objects created via , where seq anditerable 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.
#### Memory usage
With respect to memory usage:
- Lazy seqs in squint are built on generators. They do not cache their results, so every time they are consumed, they are re-calculated from scratch.
- Lazy seq function results hold on to their input, so if the input contains resources that should be garbage collected, it is recommended to limit their scope and convert their results to arrays when leaving the scope:
` clojure
(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
#### Warn on Lazy Reusage
Squint can be asked to log warnings when it detects reusage of lazy values by calling warn-on-lazy-reusage!:
lazy.cljs` clojure
(ns lazy)
(warn-on-lazy-reusage!)
(defn lazy-reuser []
(let [a (rest [0 1 2])]
(concat a a)))
(println (mapv inc (lazy-reuser)))
`
When you compile lazy.cljs, you'll see no warnings:`shell`
$ npx squint compile lazy.cljs
[squint] Compiling CLJS file: lazy.cljs
[squint] Wrote file: /tmp/squint-lazy-test/lazy.mjs
You'll see the warnings at runtime:
`shell`
$ node lazy.mjs
Re-use of lazy value Error
at [Symbol.iterator] (file:///tmp/squint-lazy-test/node_modules/squint-cljs/src/squint/core.js:696:15)
at LazyIterable.gen (file:///tmp/squint-lazy-test/node_modules/squint-cljs/src/squint/core.js:1153:14)
at Generator.next (
at Module.mapv (file:///tmp/squint-lazy-test/node_modules/squint-cljs/src/squint/core.js:1076:18)
at file:///tmp/squint-lazy-test/lazy.mjs:7:33
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:647:26)
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5)
[ 2, 3, 2, 3 ]
> [!TIP]
> It is sufficient to call (warn-on-lazy-reusage!) only from your main or entrypoint namespace.
> [!NOTE]
> If you are running code in a browser, look for the warnings in the browser console.
> [!TIP]
> Lazy reusage isn't necessarily incorrect; it's just slower.
Calling warn-on-lazy-reusage! incurs little to no overhead, so, if you wish, you can leave this check in production code.
You can produce JSX syntax using the #jsx tag:
` clojure`
#jsx [:div "Hello"]
produces:
` html`Hello
and outputs the .jsx extension automatically.
You can use Clojure expressions within #jsx expressions:
` clojure`
(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:
` clojure`
(let [x 1]
#jsx [:div
(if (odd? x)
#jsx [:span "Odd"]
#jsx [:span "Even"])])
To pass props, you can use :&:
` clojure`
(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:
` clojure`
(def my-html #html [:div "Hello"])
will set the my-html variable to an HTML object equal to
. To produce a string from the HTML object, call str on it.HTML objects can be nested:
` clojure
(defn my-html [x] #html [:<> [:div "Hello"] x])
(my-html #html [:div "Goodbye"]) ;;=> Html {s: "HelloGoodbye"}
`HTML content is escaped by default:
` clojure
(my-html #html [:div "<>"]) ;;=> Html {s: "Hello<>"}
`You can produce unsafe (unescaped) HTML with
[:$ ...]:` clojure
(str #html
[:<> [:$ ""]
[:html [:head] [:body]]])
;;=> ""
`Using metadata you can modify the tag function, e.g. to use this together with lit-html:
` clojure
(ns my-app
(:require ["lit" :as lit]))#html ^lit/html [:div "Hello"]
`This will produce:
` clojure
lit/html
`See this playground example for a full example.
Asset imports
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]
`Async/await
Squint supports
async/await:` clojure
(defn ^:async foo [] (js/Promise.resolve 10))(def x (js-await (foo)))
(println x) ;;=> 10
`Anonymous functions must have
^:async on the fn symbol or the function's name:` clojure
(^:async fn [] (js-await {}) 3)
`Generator functions
Generator functions must be marked with
^:gen:` clojure
(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:` clojure
(^:gen fn [] (js-yield 1) (js-yield 2))
`See the playground for an example.
Arrow functions
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:` clojure
(fn ^:=> [] 1)
`Defclass
See doc/defclass.md.
JS API
The JavaScript API exposes the
compileString function:` javascript
import { compileString } from 'squint-cljs';const f = eval(compileString("(fn [] 1)"
, {"context": "expr",
"elide-imports": true}
));
console.log(f()); // prints 1
`REPL
Squint has a console repl which can be started with
squint repl.nREPL
A (currently immature!) nREPL implementation can be used on Node.js with:
` clojure
squint nrepl-server :port 1888
`Please try it out and file issues so it can be improved.
$3
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 (or bunx --bun repl) and select the clojure or babashka type
REPL. REPL away!
Truthiness
Squint respect CLJS truth semantics: only
null, undefined and false are non-truthy, 0 and "" are truthy.Macros
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.` clojure
(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.See examples/vite-react for an example project which uses a
squint.edn.Watch
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.Svelte
A svelte pre-processor for squint can be found here.
Vite
See examples/vite-react.
React Native (Expo)
See examples/expo-react-native.
Compile on a server, use in a browser
See examples/babashka/index.clj.
Playground
- Pinball
- Wordle
- React, Preact, Preact + runtime hiccup
- TC39 Records and Tuples
- edn-data
- Immutable-js
- Loading a UMD module
- Vega-lite
- Vue.js
- Tic-tac-toe
- Date-fns
- VanJS
- Mithril
- Rough
- Immer, mutative
- MobX - another example using atoms
- SolidJS
- Web components
- Quill
- defmulti: multimethods in squint as a library
- Game of life
- docx
- snabbdom, Ohm's law
- Idiomorph
- Eucalypt, a Reagent-without-React clone in squint
T-shirt
Buy the t-shirt here!
License
=======
Squint is licensed under the EPL. See epl-v10.html in the root directory for more information.