Liking cljdoc? Tell your friends :D

cljs-reload

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.

Status

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.

Setup

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.

Inspecting and controlling reloads

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.

Errors and recovery

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.

Semantics and limitations

This library intentionally does not claim Clojure remove-ns or require :reload semantics.

  • Code is compiled and evaluated into a live JavaScript runtime. Existing objects and closures can retain references to old function values.
  • Definitions removed from source may remain on the existing JavaScript namespace object.
  • defonce and other ordinary ClojureScript/JavaScript state are preserved.
  • Deleting or renaming a namespace is detected and its project dependents are planned, but the old JavaScript namespace cannot be honestly removed by cljs.repl/load-file.
  • Reloading development output expects :optimizations :none; optimized whole-program output is outside the scope of hot reloading.
  • A namespace dependency cycle is reported as a planning error.

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.

Design

The exact extension seam and source-graph tradeoffs are described in doc/architecture.md.

Development

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.

License

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

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close