Clojure(Script) environment injection with shadow-cljs live-reload support
I often set up applications to read config from EDN files using something like juxt/aero, and run into a couple minor issues:
- how to get my cljs app to pick up changes in these EDN files without recompiling from scratch
- how to cleanly expose environment to both Clojure and ClojureScript while avoiding secrets being exposed in the wrong places
This small library provides:
- automatic propagation of changes made to environment files on each recompile
- explicit & simple control of what is exposed to ClojureScript vs Clojure
in a shadow-cljs.edn build:
:build-hooks [(shadow-env.core/hook)]
in your app's env namespace:
(ns my-app.env
(:refer-clojure :exclude [get])
(:require [shadow-env.core :as env]))
;; write a Clojure function that returns variables to expose to :clj and :cljs.
;; the function must accept one variable, the shadow-cljs build-state
;; (which will be `nil` initially, before compile starts)
#?(:clj
(defn read-env [build-state]
{:common <exposed everywhere>
:clj <exposed to Clojure>
:cljs <exposed to ClojureScript>})
;; define & link a new var to your reader function.
;; you must pass a fully qualified symbol here, so syntax-quote (`) is useful.
;; in this example I use `get` as the name, because we can call Clojure maps
;; as functions, I like to alias my env namespace as `env`, and (env/get :some-key)
;; is very readable.
(env/link get `read-env)
usage elsewhere:
(ns my-app.client
(:require [my-app.env :as env]))
(env/get :my/attribute)
Every time your app recompiles, we
- re-bind the
:clj
environment to the var you create usingenv/link
(usingalter-var-root
), and - if the
:cljs
environment has changed, we invalidate the env's namespace so that it + its dependents will recompile.
I like to use juxt/aero to read EDN files from disk. I use a handful of different files to separate config that is server-only, client-only, common, and secret (ie. not in source control).
#?(:clj
(defn read-env [build-state]
(let [aero-config {:profile (or (System/getenv "ENV") :dev)}
[common
server
secret
client] (->> ["common.edn"
"server.edn"
".secret.server.edn"
"client.edn"]
(map #(some-> (io/resource %)
(aero/read-config aero-config))))]
{:common common
:clj (merge server secret)
:cljs client})))
(env/link get `read-env)
ClojureScript can remove unused code automatically based on compile-time constants. For this to work, we can't read from our environment map at runtime - instead, we can write a tiny macro that runs at compile-time and reads from our env:
(env/link get `read-env)
(defmacro get-static [k]
(clojure.core/get get k))
;; ClojureScript code can call (get-static :some/key) and the value will be replaced
;; at compile-time, enabling DCE
To tag a version for release:
clj -A:release tag <patch, minor, or major>
To deploy:
clj -A:release