Skip to content

Instantly share code, notes, and snippets.

@mhuebert
Last active October 19, 2023 06:09
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mhuebert/a87795a74bf3f3452e6b032f1c7ee25d to your computer and use it in GitHub Desktop.
Save mhuebert/a87795a74bf3f3452e6b032f1c7ee25d to your computer and use it in GitHub Desktop.
clj(s) environment config w/ Shadow-CLJS using a build hook

Objectives/approach

  • Load config conditionally, based on a release flag passed in at the command line. We use juxt/aero to read a static config.edn file, passing in the release flag as aero's :profile.
  • Config should be exposed at runtime (in the browser / cljs) and macro-expansion time (clj). We store config in a plain map, app.env/config. Our build hook, app.build/load-env, updates this using alter-var-root!.
  • Whenever config has changed, shadow-cljs must invalidate its caches so that changes are picked up immediately. We do this in app.build/load-env by putting the current environment in the shadow build state, under [:compiler-options :external-config ::env].

Why this approach?

  • Reading config directly from macros breaks compiler caching - changing config will not cause changes to propagate to the build.
  • :clojure-defines (ie. goog-define) are not exposed in clj, & therefore can't be used by macros. We want one single way to expose config that can be reliably read anywhere.
  • Setting the release flag from the command line makes it easy to integrate this build flow with various deployment scripts/approaches.
(ns app.build
(:require [aero.core :as aero]
[clojure.java.io :as io]
[app.env :as env]
[shadow.cljs.devtools.config :as shadow-config]
[shadow.cljs.devtools.api :as shadow]))
;;;;;;;;;;;;;;;;;;;
;; Build commands
;;
;; these are to be run from the command line, with a release-flag parameter:
;; $ shadow-cljs clj-run app.build/release staging
(defn release
"Build :browser release, with advanced compilation"
([] (release "local"))
([release-flag]
(shadow/release* (-> (shadow-config/get-build! :browser)
;; note, we add ::release-flag to our build-config, we need this later.
(assoc ::release-flag release-flag)) {})))
(defn watch
"Watch the :browser release, reloading on changes."
{:shadow/requires-server true}
([] (watch "local"))
([release-flag]
(shadow/watch (-> (shadow-config/get-build! :browser)
(assoc ::release-flag release-flag)))))
;;;;;;;;;;;;;;;;;;;
;; Reading environment variables
;;
;; We use `juxt/aero` to read a config map, with our `release-flag`
;; passed in as :profile
(defn read-env [release-flag]
(-> (io/resource "config.edn")
(aero/read-config {:profile release-flag})
(assoc :release-flag release-flag)))
(defn load-env
{:shadow.build/stages #{:compile-prepare}}
[{:as build-state
:keys [shadow.build/config]}]
(let [app-env (read-env (-> config ::release-flag keyword))]
(alter-var-root #'env/config (constantly app-env))
(-> build-state
(assoc-in [:compiler-options :external-config ::env] app-env))))
;; example config file, to be read by juxt/aero
{:hostname #profile {:local "localhost"
:staging "staging.my-app.com"
:prod "www.my-app.com"}}
(ns app.env
#?(:cljs (:require-macros [app.env :as env])))
(def config
"Map of environment variables, to be read at runtime."
#?(:cljs (env/get-config-map)
:clj {}))
#?(:clj
(defmacro ^:private get-config-map
"Returns config map at compile time"
[]
config))
{...
:builds {:browser {:target :browser
...
:build-hooks [(app.build/load-env)]}}}
@duncanjbrown
Copy link

Thank you for this! With little Java experience I was tripped up by (io/resource "config.edn"), which apparently expects to find config.edn on the classpath. My config is in the project root so I removed that line and changed the following sexp to (aero/read-config "config.edn" {:profile release-flag}) (aero docs)

@mhuebert
Copy link
Author

mhuebert commented Apr 5, 2020

@duncanjbrown Glad you found it useful, and thanks for the tip.
I recently did another dive into this and extracted out a little lib: https://github.com/mhuebert/shadow-env

@whacked
Copy link

whacked commented Jul 5, 2020

@mhuebert https://github.com/mhuebert/shadow-env is a fantastic library. Thank you so much for making it.

@euporos
Copy link

euporos commented Jun 1, 2021

Really useful example, thanks!

For the record: Closure-defines can be used at compile time, i.e. within macros, by extracting them from the clojurescript env-atom, e.g. like so:

(defn get-closure-define
  [define]
  (get-in @cljs.env/*compiler* [:options :closure-defines define]))

@dudleycraig
Copy link

a bit late tho when I first looked at it I was "meh" ... too much work, tho on trying EVERYTHING else (it's not lost how much MORE effort that was), some reflection, and acceptance, ... it's precisely what was needed, thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment