Skip to content

Instantly share code, notes, and snippets.

@xyhp915
Forked from mhuebert/_doc.md
Created January 14, 2021 14:09
Show Gist options
  • Save xyhp915/f6acdc1512526c751acfc3185b72fd6d to your computer and use it in GitHub Desktop.
Save xyhp915/f6acdc1512526c751acfc3185b72fd6d 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)]}}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment