项目作者: mhuebert

项目描述 :
cljc environment variables with shadow-cljs
高级语言: Clojure
项目地址: git://github.com/mhuebert/shadow-env.git
创建时间: 2020-02-24T13:37:29Z
项目社区:https://github.com/mhuebert/shadow-env

开源协议:

下载


shadow-env

Version Badge

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

Usage

in a shadow-cljs.edn build:

  1. :build-hooks [(shadow-env.core/hook)]

in your app’s env namespace:

  1. (ns my-app.env
  2. (:refer-clojure :exclude [get])
  3. (:require [shadow-env.core :as env]))
  4. ;; write a Clojure function that returns variables to expose to :clj and :cljs.
  5. ;; the function must accept one variable, the shadow-cljs build-state
  6. ;; (which will be `nil` initially, before compile starts)
  7. #?(:clj
  8. (defn read-env [build-state]
  9. {:common <exposed everywhere>
  10. :clj <exposed to Clojure>
  11. :cljs <exposed to ClojureScript>})
  12. ;; define & link a new var to your reader function.
  13. ;; you must pass a fully qualified symbol here, so syntax-quote (`) is useful.
  14. ;; in this example I use `get` as the name, because we can call Clojure maps
  15. ;; as functions, I like to alias my env namespace as `env`, and (env/get :some-key)
  16. ;; is very readable.
  17. (env/link get `read-env)

usage elsewhere:

  1. (ns my-app.client
  2. (:require [my-app.env :as env]))
  3. (env/get :my/attribute)

How it works

Every time your app recompiles, we

1) re-bind the :clj environment to the var you create using env/link (using alter-var-root), and
2) if the :cljs environment has changed, we invalidate the env’s namespace so that it + its dependents will recompile.

Example usage with aero

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).

  1. #?(:clj
  2. (defn read-env [build-state]
  3. (let [aero-config {:profile (or (System/getenv "ENV") :dev)}
  4. [common
  5. server
  6. secret
  7. client] (->> ["common.edn"
  8. "server.edn"
  9. ".secret.server.edn"
  10. "client.edn"]
  11. (map #(some-> (io/resource %)
  12. (aero/read-config aero-config))))]
  13. {:common common
  14. :clj (merge server secret)
  15. :cljs client})))
  16. (env/link get `read-env)

Dead code elimination (DCE)

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:

  1. (env/link get `read-env)
  2. (defmacro get-static [k]
  3. (clojure.core/get get k))
  4. ;; ClojureScript code can call (get-static :some/key) and the value will be replaced
  5. ;; at compile-time, enabling DCE

Dev

Releases

To tag a version for release:

  1. clj -A:release tag <patch, minor, or major>

To deploy:

  1. clj -A:release