ClojureScript Compiler Cache vs REPL: Stale Namespace Definition
0 reputation · 06 Feb 2021, 23:07 UTC
0 reputation · 06 Feb 2021, 23:07 UTC
The ClojureScript build pipeline writes a .cljs-cache directory to persist compiled namespaces across builds. When source files change, the compiler may not invalidate stale cache entries unless a clean is performed, leading to the REPL loading an outdated function definition.
In a development workflow that alternates between clj -M -m figwheel.main and the ClojureScript REPL, this can manifest as a function that no longer returns the expected value, yet the REPL still references the old compiled code. The uncertainty lies in whether the compiler automatically detects source modifications or if manual cache invalidation is required for each namespace.
Given that the cache is documented but not explicitly tied to REPL reloading semantics, we need to clarify the expected behavior and potential mitigation steps.
cljsbuild clean or a similar command?34775 reputation · 07 Feb 2021, 00:16 UTC
The ClojureScript compiler does automatically invalidate cache entries when a namespace's source file changes—cache keys incorporate a hash of the source content. However, the REPL will still load a stale definition when:
.clj/.cljc) changed; dependent namespaces are not automatically recompiled.:optimizations :none vs :advanced).You do not need cljsbuild clean for routine source edits. The standard REPL workflow (require 'my.ns :reload-all) forces recompilation of that namespace and its transitive dependencies from source, bypassing the cache for those namespaces.
The compiler writes analysis (.analysis) and JavaScript (.js) artifacts to a cache directory (.cpcache for cljs.main, .shadow-cljs for Shadow-CLJS, target/cljsbuild for lein-cljsbuild). When you require a namespace at the REPL, the toolchain checks the cache first. If a valid entry exists—source hash and compiler options match—it loads the cached JS instead of recompiling. This is by design for speed.
Staleness occurs when the cache looks valid but the running REPL holds an older function object, or when a macro change invalidates downstream namespaces that the cache still considers current.
(require 'your.namespace :reload-all). This recompiles your.namespace and every namespace it depends on, reading source files directly.:reload-all require. If the compiler reports the error, the namespace was recompiled; if the old definition persists, the cache was used..clj/.cljc macro file, restart the host Clojure (JVM) REPL (or run (clojure.tools.namespace.repl/refresh) if using tools.namespace), then :reload-all the consumer namespaces in the ClojureScript REPL..cpcache/your/namespace/) or, more broadly, the entire cache directory (.cpcache, .shadow-cljs, or target/cljsbuild), then restart the ClojureScript REPL.:cache-dir (or Shadow-CLJS :build-id). Stop all but one REPL, clear the shared cache, restart.npx shadow-cljs cljs-repl <build-id> after npx shadow-cljs clean for a full reset, or (shadow.cljs.devtools.api/force-reload 'your.ns) for a single namespace.cljs.repl/load-file and :reload metadata; :reload-all on require works the same way.cljs.repl/require with :reload or :reload-all.Which toolchain are you using (cljs.main, Figwheel Main, Shadow-CLJS, lein-cljsbuild) and are you running multiple REPL sessions against the same cache directory? The answer determines whether step 5 (cache isolation) applies.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 07 Feb 2021, 07:59 UTC
In both cljs.main and shadow-cljs, the compiler records a hash of the source file and the used compiler options. A cached namespace is considered fresh only if that hash matches the current file contents. Therefore, normal edits to a source file automatically mark the cache entry as stale; the next require will trigger recompilation.
What often trips developers is that the REPL’s in‑memory state can still hold the old function object. If a namespace was loaded before the edit, simply calling (require 'my.ns :reload) or (require 'my.ns :reload-all) forces the compiler to recompile from source and replace the in‑memory binding, bypassing the stale cache entry. Macro changes are a special case: the compiler does not automatically recompile dependent namespaces, so a refresh or full REPL restart is required.
In practice, a targeted reload is the least disruptive approach: (require 'my.ns :reload) for a single namespace or (require 'my.ns :reload-all) for its transitive dependencies. If you switch compiler options (e.g., from :optimizations :none to :advanced), the cache key changes and the REPL will fetch the new compiled output automatically.