Shadow-CLJS replaces cljsbuild for ClojureScript npm interop
Shadow-CLJS makes npm a first-class ClojureScript dependency with automatic externs and incremental builds, speeding UI feedback while requiring care with advanced compilation.
02 Feb 2026, 09:37 UTC

From cljsbuild to npm-first resolution
The classic cljsbuild pipeline required ClojureScript projects to manage JavaScript dependencies outside the build, typically using :foreign-libs or a separate webpack step, and writing externs files by hand to protect symbols under advanced compilation. Small edits often triggered full recompilates, slowing the feedback loop.
Shadow-CLJS moves resolution into shadow-cljs.edn with :npm-deps. The build reads package.json semantics, resolves versions, and generates externs automatically for many packages, removing the manual externs step for development builds.
{:source-paths ["src"] :dependencies [] :builds {:app {:target :browser :modules {:main {:init-fn app/init}} :devtools {:after-load app/reload} :npm-deps {dayjs "^1.11.0"}} :output-dir "public/js" :asset-path "js"}In src/app/core.cljs you can require the package directly:
(ns app.core
(:require [dayjs :as dayjs]))
(defn now [] (dayjs/now))Run the watch task from the project root where shadow-cljs.edn lives. The CLI needs Node and npm on PATH and read access to the project directory.
shadow-cljs watch appExpected checks: the process prints build start messages and a dev server URL. The output directory public/js contains bundled code. Inspecting that directory after a successful build confirms the module was pulled from node_modules and bundled.
Risks: npm resolution is version sensitive. Different Node versions or missing lock files can produce non-reproducible builds across machines. Pin Node with a .nvmrc and commit package-lock.json.
Incremental compilation and hot reload for UI work
Shadow-CLJS tracks source dependencies and recompiles only changed namespaces. The dev server supports hot code reload via :devtools {:after-load ...}. For Reagent or React interop projects this reduces the edit-to-render cycle from seconds to fractions of a second.
Reagent atoms integrate with React rendering while keeping immutable data. A component derefs an atom and re-renders on change. Excessive atom nesting can cause unnecessary re-renders if components are not memoized, so keep state shape flat and co-locate subscriptions.
- Core.async channels provide structured concurrency for JavaScript callbacks.
goblocks and parsers replace nested promises, which pairs well with the fast feedback loop because async logic stays readable during rapid iteration.
Trade-off: advanced compilation and extern safety
Production builds often use :optimizations :advanced for dead-code elimination and symbol mangling. Advanced compilation renames symbols and removes unused code. Direct js/ interop to third-party libraries needs externs or :foreign-libs to prevent runtime errors.
Shadow-CLJS generates externs automatically for many npm packages, but it is not universal. For libraries with dynamic APIs or custom interop, you may still need to declare externs manually or use :foreign-libs. A practical way to check the result is to compile a small project with advanced optimizations and compare source size before and after removing an unused function. Smaller output indicates dead-code elimination is working.
Limitation: automatic extern generation reduces manual work but hides assumptions. If a library ships untyped exports or relies on runtime property access, advanced builds can break silently. Verify production builds with a smoke test that exercises the interop paths.
Use Shadow-CLJS when npm interop and fast incremental feedback are priorities, and budget time for verifying advanced builds and locking Node/npm versions for team reproducibility.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.