Thanks for giving!

Dear sponsors,

As we approach Thanksgiving once again, I’m reminded that sustained open source software development, supported by long term sponsors, is not something to take for granted.

I’m genuinely grateful for your ongoing support through GitHub Sponsors. Your contributions make a real difference: my Clojure projects wouldn’t be nearly as polished, maintained, or ambitious without your help.

If you’d like to look back on what happened in open source this past year, you can find an overview here: https://blog.michielborkent.nl/tags/oss-updates.html. The core projects remain clj-kondo, babashka, SCI, scittle, and squint/cherry. Each of them continues to grow in capability and adoption.

I’ve also applied for Clojurists Together again for 2026. If you’re a CT sponsor, a vote in the next long-term funding round would be appreciated.

Here are the main ideas I want to explore in 2026:

  • Clj-kondo: run macros directly from source code
  • Clj-kondo: run exported configs/hooks directly from classpath (instead of having to copy files to a local dir)
  • Squint/Cherry: browser nREPL support
  • Squint/Cherry: source maps
  • Squint: protocolize coll functions so you can extend them to e.g. ImmutableJS or other custom collections
  • Scittle2 (working name): better/faster/smaller version of Scittle using Cherry (in-browser CLJS compiler)
  • Babashka: support CIDER middleware from source directly in bb
  • Babashka: distinguished parallel task output (e.g. colors or prefix)
  • Clj-kondo: add first-class support for Clojure dialects like ClojureDart and Jank
  • Clojure CLI: help improve UX via a new tools working group
  • Clj-kondo: performance improvements for bigger projects

I can't make any promises on hard deadlines, but I definitely intend to work toward realizing the above goals.

Aside from software development, I'm also organizing Babashka Conf 2026 the day before Dutch Clojure Days.

As always, feel free to reach out anytime, whether here, on Clojurians Slack, or by email at michielborkent@gmail.com. I love hearing about what you are doing with my projects. Also if you are struggling with something, let me know. Your feedback and use cases continue to shape the direction of my work.

Here’s to another strong year of Clojure OSS!

Thank you for making this journey possible.

With appreciation,

Michiel Borkent / @borkdude

PS: if you aren't sponsoring, but are interested, here are the main ways to do so:

Sponsor info

Published: 2025-11-26

Tagged: sponsors

OSS updates September and October 2025

In this post I'll give updates about open source I worked on during September and October 2025.

To see previous OSS updates, go here.

Sponsors

I'd like to thank all the sponsors and contributors that make this work possible. Without you, the below projects would not be as mature or wouldn't exist or be maintained at all! So a sincere thank you to everyone who contributes to the sustainability of these projects.

gratitude

Current top tier sponsors:

Open the details section for more info about sponsoring.

Sponsor info

If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. Thank you!

Updates

The summer heat has faded, and autumn is upon us. One big focus for me is preparing my talk for Clojure Conj 2025, titled "Making tools developers actually use". I did a test run of the talk at the Dutch Clojure Meetup. It went a bit too long at 45 minutes, so I have to shrink it almost by half for the Conj. The more I work on the talk the more ideas come up, so it's challenging!

presentation at Dutch Clojure meetup

Of course I spent a ton of time on OSS the past two months as well. Some special mentions:

  • I'm pretty excited by Eucalypt, a remake of Reagent for Squint without React by Chris McCormick. It lets you build UIs with the Reagent API in less than 10kb of gzip'ed JS. The code was initially generated by an LLM, but now work is going into making the code base thoroughly tested and simplified where possible.
  • After studying Eucalypt's code I figured that making an even more minimal Reagent-like by hand would be fun. This is where I came up with Reagami. The API looks like a hybrid between Reagent and Replicant. You can build apps with Reagami starting around 5kb gzip'ed.
  • Edamame got Clojure CLR support thanks to Ambrose Bonnaire-Sergeant.
  • SCI Clojure CLR support is underway. The sci.impl.Reflector code, based on clojure.lang.Reflector was ported to Clojure with the purpose that it would then be easier to translate to Clojure CLR.
  • Cljdoc chose squint for its small bundle sizes and easy migration off of TypeScript towards CLJS
  • Via work on Squint, I found a way to optimize str in ClojureScript (worst case 4x, best case 200x)

Here are updates about the projects/libraries I've worked on in the last two months in detail.

  • babashka: native, fast starting Clojure interpreter for scripting.

    • Bump to clojure 1.12.3
    • #1870: add .addMethod to clojure.lang.MultiFn
    • #1869: add clojure.lang.ITransientCollection for instance? checks
    • #1865: support reify + equals + hashCode on Object
    • Add java.nio.charset.CharsetDecoder, java.nio.charset.CodingErrorAction, java.nio.charset.CharacterCodingException in support of the sfv library
    • Fix nrepl-server completions and lookup op to be compatible with rebel-readline
    • Add clojure.lang.Ref for instance? checks
    • Bump SCI: align unresolved symbol error message with Clojure
    • Use GraalVM 25
    • Bump deps.clj to 1.12.3.1557
    • Change unknown or REPL file path to NO_SOURCE_PATH instead of <expr> since this can cause issues on Windows when checking for absolute file paths
    • #1001: fix encoding issues on Windows in Powershell. Also see this GraalVM issue
    • Fixes around java.security and allowing setting deprecated Cipher suites at runtime. See this commit.
    • Support Windows Git Bash in bash install script
  • SCI: Configurable Clojure/Script interpreter suitable for scripting

    • ClojureCLR support in progress (with Ambrose Bonnaire Sergeant)
  • edamame: configurable EDN and Clojure parser with location metadata and more

    • 1.5.33 (2025-10-28)
    • Add ClojureCLR support (@frenchy64)
  • clj-kondo: static analyzer and linter for Clojure code that sparks joy.

    • Unreleased
    • #2651: resume linting after paren mismatches
    • 2025.10.23
    • #2590: NEW linter: duplicate-key-in-assoc, defaults to :warning
    • #2639: NEW :equals-nil linter to detect (= nil x) or (= x nil) patterns and suggest (nil? x) instead (@conao3)
    • #2633: support new defparkingop macro in core.async alpha
    • #2635: Add :interface flag to :flags set in :java-class-definitions analysis output to distinguish Java interfaces from classes (@hugoduncan)
    • #2636: set global SCI context so hooks can use requiring-resolve etc.
    • #2641: fix linting of def body, no results due to laziness bug
    • #1743: change :not-empty? to only warn on objects that are already seqs
    • Performance optimization for :ns-groups (thanks @severeoverfl0w)
    • Flip :self-requiring-namespace level from :off to :warning
    • 2025.09.22
    • Remove dbg from data_readers.clj since this breaks when using together with CIDER
    • 2025.09.19
    • #1894: support destruct syntax
    • #2624: lint argument types passed to get and get-in (especially to catch swapped arguments to get in threading macros) (@borkdude, @Uthar)
    • #2564: detect calling set with wrong number of arguments
    • #2603: warn on :inline-def with nested deftest
  • squint: CLJS syntax to JS compiler

    • Support passing keyword to mapv
    • Inline identical? calls
    • Clean up emission of paren wrapping
    • Add nat-int?, neg-int?, pos-int? (@eNotchy)
    • Add rand
    • Fix rendering of null and undefined in #html
    • #747: #html escape fix
    • Optimize nested assoc calls, e.g. produced with ->
    • Avoid object spread when object isn't shared (auto-transient)
    • Optimize =, and, and not= even more
    • not= on undefined and false should return true
    • Optimize code produced for assoc, assoc! and get when object argument can be inferred or is type hinted with ^object
    • Optimize str using macro that compiles into template strings + ?? '' for null/undefined
    • Fix #732: take-last should return nil or empty seq for negative numbers
    • #725: keys and vals should work on js/Map
    • Make map-indexed and keep-indexed lazy
    • Compile time optimization for = when using it on numbers, strings or keyword literals
    • Switch = to a deep-equals implementation that works on primitives, objects, Arrays, Maps and Sets
    • Fix #710: add parse-double
    • Fix #714: assoc-in on nil or undefined
    • Fix #714: dissoc on nil or undefined
    • Basic :import-maps support in squint.edn (just literal replacements, prefixes not supported yet)
  • reagami: A minimal zero-deps Reagent-like for Squint and CLJS

    • First releases
  • clerk: Moldable Live Programming for Clojure

    • Support evaluation of quoted regex
    • Support macros defined in notebooks
    • Bump cherry
  • cljs-str

    • More efficient drop-in replacement for CLJS str. This work was already upstreamed into CLJS, so coming near you in the next CLJS release.
  • unused-deps: Find unused deps in a clojure project

    • Support finding unused git deps
  • scittle: Execute Clojure(Script) directly from browser script tags via SCI

    • Fix SCI regression where interop on keywords like (.catch ...) was accidentally munched
  • Nextjournal Markdown

    • Add :start attribute to ordered lists not starting with 1 (@spicyfalafel)
  • cherry: Experimental ClojureScript to ES6 module compiler

    • Bump squint compiler common component and standard library
    • Bump other deps
    • Optimize =, str, not=
    • Support :macros option + :refer so you can use unqualified macros using compiler state (see macro-state-test)
  • deps.clj: A faithful port of the clojure CLI bash script to Clojure

    • Released several versions catching up with the clojure CLI
  • pod-babashka-go-sqlite3: A babashka pod for interacting with sqlite3

    • Add close-connection
    • Fix #38: add get-connection to cache connection
    • Fix potential memory leak
    • Better handling of parent process death by handling EOF of stdin
    • #25: use musl to compile linux binaries to avoid dependency on glibc
  • quickdoc: Quick and minimal API doc generation for Clojure

    • Fix extra newline in codeblock

Contributions to third party projects:

Other projects

These are (some of the) other projects I'm involved with but little to no activity happened in the past month.

Click for more details - [CLI](https://github.com/babashka/cli): Turn Clojure functions into CLIs! - [pod-babashka-fswatcher](https://github.com/babashka/pod-babashka-fswatcher): babashka filewatcher pod - [sci.nrepl](https://github.com/babashka/sci.nrepl): nREPL server for SCI projects that run in the browser - [babashka.nrepl-client](https://github.com/babashka/nrepl-client) - [fs](https://github.com/babashka/fs) - File system utility library for Clojure - [http-server](https://github.com/babashka/http-server): serve static assets - [nbb](https://github.com/babashka/nbb): Scripting in Clojure on Node.js using SCI - [sci.configs](https://github.com/babashka/sci.configs): A collection of ready to be used SCI configs. - [http-client](https://github.com/babashka/http-client): babashka's http-client - [quickblog](https://github.com/borkdude/quickblog): light-weight static blog engine for Clojure and babashka - [process](https://github.com/babashka/process): Clojure library for shelling out / spawning sub-processes - [html](https://github.com/borkdude/html): Html generation library inspired by squint's html tag - [instaparse-bb](https://github.com/babashka/instaparse-bb): Use instaparse from babashka - [sql pods](https://github.com/babashka/babashka-sql-pods): babashka pods for SQL databases - [rewrite-edn](https://github.com/borkdude/rewrite-edn): Utility lib on top of - [rewrite-clj](https://github.com/clj-commons/rewrite-clj): Rewrite Clojure code and edn - [tools-deps-native](https://github.com/babashka/tools-deps-native) and [tools.bbuild](https://github.com/babashka/tools.bbuild): use tools.deps directly from babashka - [bbin](https://github.com/babashka/bbin): Install any Babashka script or project with one command - [qualify-methods](https://github.com/borkdude/qualify-methods) - Initial release of experimental tool to rewrite instance calls to use fully qualified methods (Clojure 1.12 only) - [neil](https://github.com/babashka/neil): A CLI to add common aliases and features to deps.edn-based projects.
- [tools](https://github.com/borkdude/tools): a set of [bbin](https://github.com/babashka/bbin/) installable scripts - [babashka.json](https://github.com/babashka/json): babashka JSON library/adapter - [speculative](https://github.com/borkdude/speculative) - [squint-macros](https://github.com/squint-cljs/squint-macros): a couple of macros that stand-in for [applied-science/js-interop](https://github.com/applied-science/js-interop) and [promesa](https://github.com/funcool/promesa) to make CLJS projects compatible with squint and/or cherry. - [grasp](https://github.com/borkdude/grasp): Grep Clojure code using clojure.spec regexes - [lein-clj-kondo](https://github.com/clj-kondo/lein-clj-kondo): a leiningen plugin for clj-kondo - [http-kit](https://github.com/http-kit/http-kit): Simple, high-performance event-driven HTTP client+server for Clojure. - [babashka.nrepl](https://github.com/babashka/babashka.nrepl): The nREPL server from babashka as a library, so it can be used from other SCI-based CLIs - [jet](https://github.com/borkdude/jet): CLI to transform between JSON, EDN, YAML and Transit using Clojure - [lein2deps](https://github.com/borkdude/lein2deps): leiningen to deps.edn converter - [cljs-showcase](https://github.com/borkdude/cljs-showcase): Showcase CLJS libs using SCI - [babashka.book](https://github.com/babashka/book): Babashka manual - [pod-babashka-buddy](https://github.com/babashka/pod-babashka-buddy): A pod around buddy core (Cryptographic Api for Clojure). - [gh-release-artifact](https://github.com/borkdude/gh-release-artifact): Upload artifacts to Github releases idempotently - [carve](https://github.com/borkdude/carve) - Remove unused Clojure vars - [4ever-clojure](https://github.com/oxalorg/4ever-clojure) - Pure CLJS version of 4clojure, meant to run forever! - [pod-babashka-lanterna](https://github.com/babashka/pod-babashka-lanterna): Interact with clojure-lanterna from babashka - [joyride](https://github.com/BetterThanTomorrow/joyride): VSCode CLJS scripting and REPL (via [SCI](https://github.com/babashka/sci)) - [clj2el](https://borkdude.github.io/clj2el/): transpile Clojure to elisp - [deflet](https://github.com/borkdude/deflet): make let-expressions REPL-friendly! - [deps.add-lib](https://github.com/borkdude/deps.add-lib): Clojure 1.12's add-lib feature for leiningen and/or other environments without a specific version of the clojure CLI

Published: 2025-11-02

Tagged: clojure oss updates

Reagami: a Reagent-like library in less than 100 lines of Squint CLJS

Recently I've been excited about Eucalypt, a Reagent-clone in pure Squint. For those who don't know Squint, it is a CLJS dialect that re-uses JS features as much as possible. Maps and vectors compile directly to objects and arrays for example. The standard library mimics that of Clojure(Script). Collections, although they are mutable in JS, are treated immutably via shallow-copying. You can build small apps in Eucalypt starting at about 9kb gzip. When looking at the code, I wondered if we could make an ultra-simple Reagent-like library with less features for even smaller apps. If I understand Eucalypt correctly, it produces DOM nodes from hiccup and then patches the already mounted DOM nodes using a diffing algorithm. I tried to do this in under 100 lines, in a more basic fashion. I call this library Reagami, a library you can use to fold your state into the DOM!

Let's explain how it works.

Rendering Hiccup to DOM nodes

Below you see the create-node function, which takes hiccup, e.g. [:div "Hello"] and transforms it into a DOM node. I placed comments in the code that help you understand it, hopefully.

(defn- create-node
  ([hiccup] (create-node hiccup false))
  ([hiccup in-svg?]
   (cond
;; we compile primitives directly into TexNodes
     (or (nil? hiccup)
         (string? hiccup)
         (number? hiccup)
         (boolean? hiccup))
     (js/document.createTextNode (str hiccup))
;; vectors indicate a DOM node literal or a function call
     (vector? hiccup)
     (let [[tag & children] hiccup
;; here we pull apart id and class shortcuts like: `:div#my-id.my-class`
           [tag id class] (if (string? tag) (parse-tag tag) [tag])
           classes (when class (.split class "."))
;; hiccup vectors can have an optional map to set attributes of the DOM node
          [attrs children] (if (map? (first children))
                              [(first children) (rest children)]
                              [nil children])
;; SVG needs special handling, SVG nodes need to be created with `createElementNS`, see below
           in-svg? (or in-svg? (= :svg tag))
;; if we encounter a function as the first element, e.g. `[my-component]`, we call it
           node (if (fn? tag)
                  (let [res (apply tag (if attrs
                                         (cons attrs children)
                                         children))]
;; and then we feed the result back into create-node
                    (create-node res in-svg?))
;; so here we create the DOM element, either a normal or an SVG one
                  (let [node (if in-svg?
                               (js/document.createElementNS svg-ns tag)
                               (js/document.createElement tag))]
;; here we iterate over all the children in the hiccup vector
                    (doseq [child children]
;; in squint, vectors are also seqs, but here we want to make sure the result is list-like,
;; e.g. the result from `(for [i xs] [my-component])`
;; if that is the case, then we map create-node over those results
                      (let [child-nodes (if (and (seq? child)
                                                 (not (vector? child)))
                                          (mapv #(create-node % in-svg?) child)
;; else, we have only one node as the result of calling create-node on the child
                                          [(create-node child in-svg?)])]
;; let's add all the child-nodes to the DOM element created from the tag!
                        (doseq [child-node child-nodes]
                          (.appendChild node child-node))))
;; attribute handling: for each key and value in the attribute map:
                    (doseq [[k v] attrs]
                      (let [key-name k]
                        (cond
;; Let's create some CSS if the style attribute is a msp!
                          (and (= "style" key-name) (map? v))
                          (doseq [[k v] v]
                            (aset (.-style node) k v))
;; if the attribute starts with "on", then we add an event handler
                          (.startsWith key-name "on")
;; we support :on-click, :on-mouse-down, :onMouseDown, etc.
                          (let [event (-> (subs key-name 2)
                                          (.replaceAll "-" "")
                                          (.toLowerCase))]
                            (.addEventListener node event v))
;; if the attribute false is false or nil, we ignore it
                          :else (when v
                                  (.setAttribute node key-name (str v))))))
;; classes parsed from the tag, e.g. `:div.my-class1.my-class2`
                    (let [class-list (.-classList node)]
                      (doseq [clazz classes]
                        (.add class-list clazz)))
;; finally we handle the id parsed from the tag, e.g. `:div#my-id`
                    (when id
                      (set! node -id id))
                    node))]
       node)
     :else
     (throw (do
              (js/console.error "Invalid hiccup:" hiccup)
              (js/Error. (str "Invalid hiccup: " hiccup)))))))

That's it! Pretty simple.

Patching the DOM

If we simply just re-insert the generated hiccup DOM nodes into the DOM every time something changes, then we have several problems. E.g. when we are typing in an input field and we insert a new input field, we lose focus. Also we lose event handlers, although this isn't really an issue if we re-generate them via the hiccup-to-DOM function above. A better approach is to cleverly patch the DOM and re-use existing DOM nodes when possible.

The patch function takes a parent node, some child nodes and tries to re-use or insert the children. There are several libraries that do this far more intelligently than the below function. E.g. Idiomorph is such a library. One problem I encountered with Idiomorph is that it doesn't preserve event listeners. There's also Snabbdom, a small but effective vDOM library that you could use. But where's the fun in that, if you can make your own naive patch function. Here we go! When we render a component for the first time, Reagami calls patch with the root node (usually something like (js/document.querySelector "#app")) and the rendered hiccup as the only child. Read along with the comments again.

(defn- patch [parent new-children]
;; First we take a look at the current children of the root node, already mounted in the DOM
  (let [old-children (.-childNodes parent)]
;; If the amount of children isn't the same, we just give up and replace all the children with the new children.
;; This typically happens on first render or when elements disappear due to conditionals in the hiccup code.
;; More mature patching libraries do much more clever things here
;; but I've found this works well enough for the basic use cases I tested.
    (if (not= (count old-children) (count new-children))
      (parent.replaceChildren.apply parent new-children)
;; When the number of children is the same we compare them one by one.
      (doseq [[old new] (mapv vector old-children new-children)]
        (cond
          (and old new (= (.-nodeName old) (.-nodeName new)))
;; Only when the node names are the same, we consider it the same node.
;; If it's Text node, we need to copy over the text content.
          (if (= 3 (.-nodeType old))
            (let [txt (.-textContent new)]
              (set! (.-textContent old) txt))
            (let [new-attributes (.-attributes new)
                  old-attributes (.-attributes old)]
;; Here we just set all the attribues from the new node to the old node.
              (doseq [attr new-attributes]
                (.setAttribute old (.-name attr) (.-value attr)))
              (doseq [attr old-attributes]
;; When an existing attribute in the old node, doesn't occur in the new node, we remove it
                (when-not (.hasAttribute new (.-name attr))
                  (.removeAttribute old (.-name attr))))
;; Then we recursively descend into the children of the new node.
              (when-let [new-children (.-childNodes new)]
                (patch old new-children))))
;; If the node names aren't the same, we replace the node completely with the new node.
          :else (.replaceChild parent new old))))))

Putting it all together

Reagami's only public function is called render which is just:

(defn render [root hiccup]
  (let [new-node (create-node hiccup)]
    (patch root [new-node])))

With this single function, you can write a small reactive app, if we combine it with atom and add-watch. Here's an example of this:

(ns my-app
  (:require ["https://esm.sh/reagami@0.0.8" :as reagami]))

(def state (atom {:counter 0}))

(defn my-component []
  [:div
   [:div "Counted: " (:counter @state)]
   [:button {:on-click #(swap! state update :counter inc)}
    "Click me!"]])

(defn render []
  (reagami/render (js/document.querySelector "#app") [my-component]))

(add-watch state ::render (fn [_ _ _ _]
                            (render)))

(render)

(Open this example on the Squint playground)

That's it: a Reagent-like library in less than 100 lines of code!

Further ideas:

  • Blending hiccup and patching so we don't even need to create DOM nodes for stuff that hasn't changed
  • Or remember the hiccup we generated on the previous change, compare that since that's more efficient? (I believe Eucalypt does this).
  • Better patching by looking at IDs or keys

Examples

Here are some examples you can play with on the Squint playground:

Published: 2025-10-24

Tagged: clojure reagent clojurescript squint

Archive