CLI tool
squint-cljs/squint avatar
squint-cljs/squint

squint: a ClojureScript dialect that compiles down to plain JavaScript data

Light-weight ClojureScript dialect

915 stars63 forksClojureEPL-1.0

At a glance

What is it?
squint is a light-weight dialect of ClojureScript with its own compiler and standard library, whose defining choice is that it uses only built-in JavaScript data structures, so maps are objects, keywords are strings and the output is ES modules with a small bundle. It targets workers, node scripts and CI jobs rather than replacing ClojureScript outright.
Who is it for?
Adopt squint when you want Clojure syntax in a context where the ClojureScript runtime and its persistent data structures are more weight than the task justifies, such as a Cloudflare worker, a node script or a GitHub action. Do not adopt it if you depend on ClojureScript semantics, since keywords become strings, maps are mutable and equality rules change, and the project points you at Cherry for that.
Can I use it commercially?
Yes, with conditions. EPL-1.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
Is it still maintained?
Yes. The repository last received commits 8 days ago.
What is it written in?
Mainly Clojure, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A dialect with its own compiler and standard library

Squint is a light-weight dialect of ClojureScript with a compiler and standard library. The README is precise about what it is not: squint is not intended as a full replacement for ClojureScript, but as a tool to target JavaScript when you need something more light-weight in terms of interop and bundle size.

The most significant difference with ClojureScript is stated next: squint uses only built-in JavaScript data structures. That single design decision cascades through everything else. There is no PersistentHashMap, no keyword interning, no seq abstraction backed by a custom protocol. A map is an object, a vector is an array, a keyword is a string.

The output is designed to work well with ES modules, which is what makes it usable in modern toolchains without a bundler doing anything clever. And there is a standard library, but it is a smaller re-implemented subset rather than the ClojureScript core, with parts of it ported to mutable data structures and therefore with caveats.

If you want squint with the normal ClojureScript standard library and data structures, the README points to Cherry, a sibling project in the same organisation. The choice between the two is the choice between small output and familiar semantics, and squint's documentation is explicit about which side of that line it sits on.

The implementation language is Clojure, the licence is EPL-1.0, and the homepage is squint-cljs.github.io/squint.

The quickstart is npm install and one command

The setup path is deliberately close to how any node project starts.

shell
$ mkdir squint-test && cd squint-test
$ npm init -y
$ npm install squint-cljs@latest

You then write a .cljs file. The example in the README uses namespace requires with vectors, which is the interop style that squint supports for JavaScript modules.

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}))

Compiling and running is a single command, and run does both.

shell
$ npx squint run example.cljs
true
6

The two lines of output correspond to the two printlns, the existence check of the module URL and the sum. Running npx squint --help lists all the command line options.

What you notice in the source is that the function still destructures a map with :keys and still uses +, and the output is still 6 rather than a string. Clojure syntax survives; the data structures underneath do not.

Where squint is meant to be used

The Why Squint section is short and gives a useful test for whether the project fits your case.

Squint lets you write ClojureScript syntax but emits small JavaScript output, while still having parts of the ClojureScript standard library available, ported to mutable data structures and therefore with caveats. The projects it names as good fits are ones you would like to deploy on Cloudflare Workers, node scripts, GitHub actions, and similar places, where the extra performance, startup time and small bundle size matter.

That is a coherent set of targets and they share a property. Each is a short lived process with a cold start, a small size budget or both, and none of them benefits from persistent data structures that are thrown away at the end of the run.

The articles listed in the README make the same case from practice. There is one on writing a Cloudflare worker with squint and bun, and one on porting a ClojureScript project to squint, which is the more interesting of the two because it is about migration rather than greenfield work.

The projects section shows how much ground the dialect covers in practice: cljdoc for documentation browsing, hyper as a reactive server-rendered web framework for Clojure, weave as a Clojure web framework that compiles client side code with squint, Eucalypt as a Reagent clone in squint, epupp as a browser extension for tampering with web pages, clj-proj for PROJ on the JVM and JavaScript, and a run of small projects including several games, a Minesweeper clone and a Wordle implementation.

The library list is what makes it more than a demo. babashka.cli and babashka.fs support JavaScript, replicant is supported, nexus is supported, and @nextjournal/clojure-mode understands it, which means tooling written for ClojureScript already has an opinion about squint.

The differences that will change your code

The differences list is the most useful section in the README, and it is worth reading line by line before writing anything.

Keywords are translated into strings, which is the change with the widest blast radius, because a keyword used as a map key becomes a plain string key. Maps, sequences and vectors are represented as mutable objects and arrays. Most functions return arrays, objects or Symbol.iterator rather than custom data structures.

Mutation follows a specific split. The bang variants, assoc!, dissoc! and conj!, perform in place mutation on objects. The non-bang versions, assoc, dissoc and conj, return a new shallow copy of objects. And the standard library functions never mutate their arguments when the ClojureScript counterpart does not, using shallow cloning instead, which the README describes as the pattern JavaScript developers use all the time with a spread.

Equality is the subtle one. equiv compares by identity or by -equiv, unlike = which deep compares plain data, and hash follows equiv, so plain mutable objects and arrays hash by reference. In other words, two structurally equal objects are not equal under equiv, which is the opposite of the assumption Clojure programmers carry.

Printing is friendlier. pr-str and prn print EDN with the idea that you can paste the output back into your programs, and JavaScript Maps are printed like maps with a #js/Map prefix.

And the constraint that catches everyone: since JavaScript only supports strings for keys in maps, any data structures used as keys will be stringified.

Async and await, with a marker on the function

Async support is one of the parts of squint that is more complete than you might expect from a dialect.

Async and await are supported, so you can write a def with an await in it. Async functions have to be marked with the metadata ^:async on the definition, and the legacy spellings js-await and js/await are still accepted for code that was written before the marker existed.

clojure
(defn ^:async foo [])

The marker approach is worth pausing on, because it is the opposite of the JavaScript convention. In JavaScript, asyncness is a property of the function declaration and the compiler enforces it. In squint, the marker tells the compiler to treat the function as async, and the fact that it is optional metadata rather than a separate definition form means an unmarked async function is a compile error rather than a runtime surprise.

For the target workloads, this matters. A Cloudflare worker that awaits a fetch, a node script that awaits a subprocess, a GitHub action that awaits an API call: these are the programs where the ClojureScript compilation story is weakest, because the closure-heavy runtime and the async story do not fit together well. Making async a first class part of the dialect is the piece that lets squint handle them at all.

The packaging: a bin, a Node API, a Vite plugin and a playground

The npm package is called squint-cljs and it is an ES module package with side effects set to false, which matters because bundlers use that to drop unused imports.

json
  "name": "squint-cljs",
  "type": "module",
  "sideEffects": false,
  "version": "0.15.212",

The bin entry maps the squint command to node_cli.js, and the files list shows the surface the package ships: the standard library modules such as core.js, string.js and the src/squint directory with set, html, math, walk, multi, record, test and edn, plus node_cli.js, index.js, node-api.js and the Vite plugin files vite.js and vite-common.js.

The exports map makes those entry points explicit, including ./core.js, ./string.js, the individual src/squint modules, ./node-api.js and the vite export. So the compiler is usable as a command line tool, as a Node API for programmatic compilation, and as a Vite plugin for a development server.

The repository is organised in a way that shows the project is developed as a Clojure program with a build script, since build.clj, compile.clj, deps.edn, bb.edn and shadow-cljs.edn all sit at the root. There is a playground directory, and the README links an Advent of Code solver that runs in it, which is a good demonstration that the whole thing works in a browser.

The examples directory is the best catalogue of what squint can target. It includes ajv, babashka, browser-repl, bun, cljs-embed, expo-react-native, express, game-of-life, ink, instantdb, jotai, quickjs, replicant, snake, solid-js, source-maps, tetris, threejs, vite-react, windowjs and wordle, which is a range from server frameworks to terminals to 3D rendering.

The single runtime dependency is chokidar, for watching files, and the funding entry points at a GitHub sponsors page for the maintainer.

Version numbering that reads like a build counter

The release list is unusual and tells you how the project versions itself. The recent tags are v0.15.212 from 2026-10-09, v0.14.211 from 2026-10-02 and v0.14.209 from 2026-09-23.

So the patch position is not a patch counter in the usual sense, it carries the build number, which is why you see 209, 211 and then 212 as the third component across two minor lines. The minor version moves when there is a change worth naming, and everything else accumulates in the last position. The npm package version in the manifest matches, at 0.15.212.

For a consumer, the practical advice is to pin the exact version you tested, because a change in the third component can still be a behavioural change in a library that ships a compiler and a standard library together. The CHANGELOG.md at the root is where the detail lives.

On maintenance, the last push was on 2026-10-15, six days after the v0.15.212 release, and the repository is not archived. The pace of a release every few days, with a build counter in the version, is the signature of a project that publishes continuously rather than on a schedule.

The licence is EPL-1.0, which is the Eclipse Public License, and the text is in the LICENSE file at the root of the repository.

Editorial conclusion

Adopt squint when you want Clojure syntax in a context where the ClojureScript runtime and its persistent data structures are more weight than the task justifies, such as a Cloudflare worker, a node script or a GitHub action. Do not adopt it if you depend on ClojureScript semantics, since keywords become strings, maps are mutable and equality rules change, and the project points you at Cherry for that. Before you start, read the differences list rather than discovering them at runtime, decide whether your keys can be strings since JavaScript only supports string keys in maps, and keep the version numbering in mind, because squint publishes releases such as v0.15.212 where the patch number carries the build.

Frequently asked questions

What is squint and how is it different from ClojureScript?

Squint is a light-weight dialect of ClojureScript with its own compiler and standard library, intended for targeting JavaScript when you need lighter interop and a smaller bundle. It uses only built-in JavaScript data structures, so maps are mutable objects, keywords become strings and the output is designed to work well with ES modules.

How do I run a squint file?

Install the package with npm install squint-cljs@latest, write a .cljs file, and run npx squint run example.cljs, which compiles and runs in one step. Run npx squint --help for all the command line options.

Does squint mutate data the way ClojureScript does?

No. The bang variants such as assoc!, dissoc! and conj! mutate in place, while assoc, dissoc and conj return a new shallow copy, and the standard library functions never mutate their arguments when the ClojureScript counterpart does not, using shallow cloning instead.

What should I use if I want squint but real ClojureScript semantics?

Cherry, the sibling project, keeps the normal ClojureScript standard library and data structures while remaining light-weight. The README points to it as the option when you need closer ClojureScript semantics rather than squint's mutable built-in structures.

What licence is squint released under?

The licence field on the repository says EPL-1.0, with the text in the LICENSE file at the repository root. Releases are published as v-prefixed tags such as v0.15.212, where the last component carries the build number rather than a conventional patch count.

Official sources

  1. License: EPL-1.0
  2. Project website
  3. README
  4. Releases
  5. squint-cljs/squint on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/squint-cljs-squint.svg)](https://hysenlabs.com/projects/squint-cljs-squint)

Was this useful?