Liking cljdoc? Tell your friends :D

cljs-reload

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.

Status

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.

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.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.

Multiple builds in one JVM

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.

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/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.

Errors and recovery

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.

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.
  • CSS refresh success means the browser accepted the DOM swap script. A later HTTP failure loading a stylesheet is handled in the browser by retaining the old link and is not synchronously visible to the JVM reload state.

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, 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.

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