Small, focused hot reloading for ClojureScript projects using cljs.main.
cljs-reload watches project source and CSS roots. It detects changed
namespaces, finds their project dependents, and asks the active cljs.repl
environment to reload them in dependency order. Stylesheet changes refresh the
external CSS links in the connected browser. The library 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.08.22. It adds CSS hot reloading to the namespace
reload workflow. The project is 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.08.22"}}
: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"]
:css-paths ["resources/public/css"]
: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.
Every cljs-reload.repl/ReloadEnv owns an independent reload session. A JVM
process may therefore run multiple browser REPL environments—for example a
public build on port 9000 and an admin build on port 9001—without one setup or
teardown replacing the other's watcher, compiler atom, state, or browser
connection. Each build still needs its own compiler output directory and stock
browser REPL environment.
REPL special forms always inspect and control the session belonging to the REPL
where they were entered. For JVM-side orchestration, retain the environment and
obtain its session with cljs-reload.repl/session; the inspection functions,
reload!, and stop! accept that session as an argument. Their zero-argument
forms continue to address the most recently started session for ordinary
single-build use.
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.
:css-paths is optional and defaults to []. Paths may name directories or
individual .css files. When a configured stylesheet changes, is added, or is
deleted, cljs-reload evaluates a small script through the existing browser REPL
connection. The script replaces every external
link[rel~="stylesheet"][href] with a cache-busted clone and keeps the old
stylesheet active until its replacement loads. All links are refreshed because
there is no portable mapping from a watched filesystem path to its URL in an
arbitrary web server. Inline <style> elements are not changed.
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/changed-css)
(cljs-reload/affected)
(cljs-reload/reload-order)
(cljs-reload/last-reload)
(cljs-reload/last-css-reload)
(cljs-reload/last-error)
(cljs-reload/reload!)
The same data is available to JVM-side tooling through
cljs-reload.core/state, changed, changed-css, affected, reload-order,
last-reload, last-css-reload, and last-error.
The complete state includes source and CSS paths, namespace and stylesheet
metadata, mtimes, content hashes, project dependencies and dependents, deleted
namespaces and stylesheets, the current plan, and the last reload, CSS reload,
or error. Errors are maps, including Throwable->map data, rather than hidden
logger state.
A compile, namespace evaluation, or CSS refresh 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, CSS change/deletion and evaluation recovery,
state inspection, isolated concurrent reload sessions, 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 |