cljs-dev 2025-11-10

@borkdude re: squint, is the idea that you just compile yr stuff, squint.js gets prepended and that's it? You can pass that through minifier if you want? Or are you just gzipping w/o minificiation?

@dnolen almost. here's an example of what happens when you compile squint: https://squint-cljs.github.io/squint/?src=gzip%3AH4sIAAAAAAAAE9NISU3LU0jLz1eITozlUlDQSCwuzk9WSFSwSlKwStbUBAAoQM30IAAAAA%3D%3D&repl=false so:

(defn foo [a]
  (assoc a :b :c))
compiles to :
import * as squint_core from "squint-cljs/core.js";
var foo = function (a) {
  return squint_core.assoc(a, "b", "c");
};

export { foo };
And :require also compiles to import etc. So it emits ES6. You can then process that ES6 with a bundler like esbuild to minify it (like most JS projects do). With vite you get some more UX around that but that's the essence.

there's also clojure.string and clojure.set in the standard library

(require '[clojure.string :as str])
(str/join "," [1,2,3])
=>
import * as squint_core from "squint-cljs/core.js";
import * as str from "squint-cljs/src/squint/string.js";
str.join(",", [1, 2, 3]);

Was just playing around with it, pretty cool - I guess there are some functionality gaps that you have to be mindful of.

like collections as fns, complex keys, hash / eq - stuff like that

yes. {} are just plain objects, so all keys are stringified. js/Map is supported with assoc etc too though which preserves keys, but JS not supporting hash/eq stuff is kind of a mismatch with that collections as fns is currently supported in callsites, like:

(#{:a :b :c} :c)
works. it's statically detected that this should be a lookup. also (keep #{:a :b :c} [:a :d :e :b]) works since many HOFs turn their argument into a function if it isn't already one. Similar for keywords (which are just strings in squint):
(map :foo [{:foo 1}])
The collection as function call could benefit from some more inference though

right so manual transform - also no protocols?

protocols do exist but the language itself isn't extensible yet. I'm thinking about doing that, so you can plug in immutable js and make it first class for example

e.g. this code works:

(defprotocol IDude
  (dude [_]))

(deftype Dude []
  IDude (dude [_] :dude))

(dude (->Dude.))

but assoc etc aren't protocolized yet

I'm not being critical, just thinking about code size stuff - re: :lite-mode everything does actually work - could get rid of more things but undesirable

code size is one aspect of squint. being first class JS is another. you can write libs in squint, publish them to NPM without re-publishing the core library every time

oh yeah, wasn't implying otherwise - re: only code size

btw, there is also cherry, which is basically the same as squint, but it uses the regular CLJS data structures. you can get some scripts going very fast, but the output size won't be optimal due to how GC + shadow produced the ESM core lib. The baseline is 44kb gzip for (prn :foo)

playground is here: https://squint-cljs.github.io/cherry/ (takes a while to load due to CDN stuff)

the transparent bit w/ JS stuff is neat - but the fidelity gap makes it hard to dual publish Node & Maven

not saying the fidelity gap needs closing

that's where reader conditionals come in. e.g. reagami can be directly used from JS or Squint, but also directly from CLJS (from source). https://github.com/nextjournal/clojure-mode/ is another library that is published in this way. Compiled squint for JS consumers, CLJS from source for CLJS consumers

you do need to put in the effort to make it compatible. clojure-mode was first written in CLJS. It took a day to make it also work in squint

(it's a plugin for code-mirror)

I do like the async / generator support - that's a bit of work

people requested to use clojure-mode from JS. for a few years there wasn't an answer to this. but squint closed the gap

squint also supports async/await

but that won't be compatible with CLJS

unless with some macros, perhaps

(defn ^:async foo []
  (try (js-await ...) (catch ...))

oh that was my other question - so you punt on the expression problem w/ async/await?

people can use it out of convenience, they don't have to

you need to generate closures to preserve the semantics

yes, squint does that

so your await might be in the wrong place

(unless I'm missing something). can you give an example?

I mean any time you introduce a closure and the await is inside the wrong fn

give me an example and I'll see whether squint does it correctly or not

do you close over loop variables to fix loop/recur when you use JS iteration underneath?

what would be a trivial example

ask chatgpt for an example ;)

loop/recur is an easy one - you have to make a fn to capture the loop var

ok, but not sure what you mean with JS iteration. what does it have to do with async/await?

stuff like this (or (let [a (+ 1 (await c))] a))

I mean once you come w/ one, there's an infinite number of these 🙂

in the the case of (or ...) it will introduce let and the temp bindings

so probably just (or ... (and ...)) where you await is enough

oh you just make all closures async in an async context?

well, only the closures that serve as wrappers for let, etc

I notice the closure in the return statement shouldn't have to be async so could optimize that

but currently it's like this

right you're combinding w/ the :context parameter when doing this

👍 1

but I guess this approach doesn't for work yield? or maybe that would create more steps than the user would expect?

I mean I see squint supports it in theory, but I'm confused as to how that could work

let's see. do you have an example? :)

(brb, rehearing my talk for the last time...)

exactly the same the same as await, no difference

welll, yield can only be in statement position so that's a bit easier I guess? this isn't valid for example.

function* foo() {
  return 1 + yield 2;
}

yeah, that's not valid in JS either

yeah, the thing I was missing was yield* - I stopped paying attention to generators a long time ago.

so yield* doesn't need to be in statement position?

oh that's interesting

oh I think I got it. I just have to wrap yield in some parens to fix this

yep, that fixes it

a few tweaks on master, trivial reagami program is looking more like 13.5K brotli - that's pretty good, 7.4% of that is reagami.

🎉 4