ClojureScript Build Tooling and JavaScript Dependency Management: Ensuring Repeatable Development Environments
0 reputation · 14 Jul 2025, 18:07 UTC
0 reputation · 14 Jul 2025, 18:07 UTC
The goal is to establish a repeatable ClojureScript development environment that interoperates reliably with JavaScript tooling while preserving hot‑reload functionality and deterministic build outputs.
The key uncertainty lies in whether to declare JavaScript dependencies through the built‑in :npm-deps foreign‑lib mechanism or to delegate resolution entirely to an external tool such as Yarn, and how each option influences cache invalidation, source‑map stability, and version locking across different developer machines.
Which approach yields more deterministic builds when :optimizations and :source‑map settings remain unchanged? How does each method affect the reproducibility of Figwheel Main or shadow‑cljs watch modes? What verification practices can confirm that the choice does not introduce hidden variability in generated assets?
Delegating JavaScript dependency resolution to Yarn or npm with a committed lockfile yields more deterministic builds than the built-in :npm-deps foreign-lib mechanism, assuming :optimizations and :source-map stay fixed. The lockfile pins every direct and transitive version once; :npm-deps re-resolves through the Closure compiler at each build and has no lockfile of its own, so transitive versions can drift between machines and over time.
With :npm-deps, the ClojureScript compiler asks the Closure compiler to resolve npm packages during compilation. Unless you pin every version explicitly in the map, resolution can float, and different developer machines can end up feeding different JavaScript into the same build. That changes generated asset bytes and shifts source-map line numbers, even though your ClojureScript source is identical.
With Yarn or npm, yarn.lock or package-lock.json fixes the entire dependency tree. The node_modules directory becomes effectively immutable between lockfile changes, so the compiler sees byte-identical inputs on every machine. This is the likely explanation for most "works on my machine" build-hash mismatches; the confirmed fact is simply that a lockfile constrains resolution and :npm-deps does not.
:npm-deps works without Node tooling, which is convenient, but you must pin exact versions manually. It can be deterministic in practice, yet nothing prevents a teammate from adding an unpinned dep that resolves differently on their machine.Both systems invalidate caches by content hash, so predictable inputs mean predictable cache behavior. The external-tool approach makes those inputs predictable by construction.
node_modules layouts even from the same lockfile.rm -rf target public/js or your output dirs) and compare SHA-256 hashes of the compiled JS and source maps. They should match.shadow-cljs release :advanced twice with no changes and diff the artifacts; expect byte-for-byte identity.npm ls --json or yarn list --json.One caveat on scope: exact behaviors depend on your compiler and build-tool versions, so treat the verification steps above as the real test for your setup rather than any general claim. If your team cannot standardize on Node tooling at all, pinned :npm-deps is the fallback — but enforce the pins in code review, because nothing else will.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.