Small, focused hot reloading for ClojureScript projects using cljs.main.
cljs-reload watches project source roots, detects changed namespaces, finds
their project dependents, and asks the active cljs.repl environment to reload
them in dependency order. It does not provide a build system, web server,
JavaScript package manager, application framework, or alternative REPL
transport.
Shadow-cljs is a fantastic library. In the author's opinion, though, it does too
much for this particular workflow. The author would much rather see the most
important pieces handled by cljs.main, cljs.repl, and small composable
libraries, so that the journey into ClojureScript is easier for newcomers.
The current version is 2026.07.26-2. It supersedes 2026.07.26, which was
published with incorrect license metadata. The project and corrected artifact
are licensed under the MIT License. The implementation targets ClojureScript
1.12.145 and the ordinary browser-connected cljs.main REPL.
Add the library and a development source path to an alias:
{:aliases
{:dev
{:extra-paths ["dev"]
:extra-deps
{org.clojure/clojurescript {:mvn/version "1.12.145"}
org.clojars.hkjels/cljs-reload {:mvn/version "2026.07.26-2"}}
:main-opts
["-m" "cljs.main"
"-co" "dev.cljs.edn"
"-re" "cljs-reload.repl"
"-ro" "cljs-reload.edn"
"-r"]}}}
During checkout-based development, replace the published coordinate with a
:local/root or Git dependency as usual.
dev.cljs.edn contains normal compiler options:
{:optimizations :none
:output-dir "out"
:asset-path "/out"
:source-map true}
cljs-reload.edn contains the browser REPL options plus the few options owned
by this library:
{:source-paths ["src" "dev"]
:poll-ms 100
:debounce-ms 100
:launch-browser true}
The application can be loaded in the same ways it would be in a normal
cljs.main REPL. One convenient choice is dev/user.cljs:
(ns cljs.user
(:require [example.core]))
Alternatively, compile an entry point before the REPL by ending the command
with -c example.core -r. The custom environment is still only a decorator
around the browser REPL, and the compiler environment passed from compilation
is reused.
Start it with:
clojure -M:dev
After the browser connects, editing src/example/a.cljs in this graph:
example.a -> example.b -> example.core
example.unrelated
produces the essential state:
{:changed #{example.a}
:affected #{example.a example.b example.core}
:reload-order [example.a example.b example.core]}
Only namespaces discovered below :source-paths participate in transitive
reload calculation. Library dependencies are never turned into reload targets.
The decorated REPL adds a handful of REPL special forms. They print the JVM-side reload data without installing a second transport or copying it into the browser:
(cljs-reload/state)
(cljs-reload/changed)
(cljs-reload/affected)
(cljs-reload/reload-order)
(cljs-reload/last-reload)
(cljs-reload/last-error)
(cljs-reload/reload!)
The same data is available to JVM-side tooling through
cljs-reload.core/state, changed, affected, reload-order, last-reload,
and last-error.
The complete state includes source paths, source metadata, mtimes, content
hashes, project dependencies and dependents, deleted namespaces, the current
plan, and the last reload or error. Errors are maps, including Throwable->map
data, rather than hidden logger state.
A compile or JavaScript evaluation error is caught by the reload operation. The
watcher remains alive, the browser connection remains open, and the failure is
recorded in last-error. The compiler atom is restored to its state before the
failing file, matching the recovery protection used by ClojureScript's own REPL
special forms. Saving a corrected file creates a new content signature and
tries again.
This library intentionally does not claim Clojure remove-ns or
require :reload semantics.
defonce and other ordinary ClojureScript/JavaScript state are preserved.cljs.repl/load-file.:optimizations :none; optimized
whole-program output is outside the scope of hot reloading.The invariant is narrower and explicit: recompile changed project namespaces and their affected project dependents, then reload the existing sources into the active ClojureScript evaluation environment in dependency order.
The exact extension seam and source-graph tradeoffs are described in
doc/architecture.md.
Run the focused JVM-side and compiler integration tests with:
clojure -M:test
The suite covers direct changes, transitive and branched dependents,
topological order, unrelated namespaces, deletion, rename, compile failure and
recovery, duplicate save events, state inspection, the delegated browser REPL
options, and the real cljs.repl/load-file primitive.
Build the release JAR and its Maven metadata with:
clojure -T:build jar
Every branch push runs the test suite in GitHub Actions. After a successful
test run on main, the workflow builds the release JAR and deploys it to
Clojars with clojure -X:deploy. Configure CLOJARS_USERNAME and
CLOJARS_PASSWORD as repository secrets; the password value must be a Clojars
deploy token.
Copyright © 2026 Henrik Kjerringvåg
Distributed under the MIT License.
Can you improve this documentation?Edit 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 |