Builds the map Cooper's ${...} resolution (CASC.md §7.2) reads
from. Called from Cooper.load_file/2/load_string/2 on every
single call, hit or miss, cached or not -- and again by
Cooper.Cache's own watch_env poll tick, so a .env file edit is
detected the same way a real System.put_env/2 is. Not a separate
pipeline stage callers normally reach for directly, but its own
module (rather than inlined) because the layering rules below are
non-trivial enough to want one place to read, and one place to test,
them.
Layering
Five layers, later winning, Dotenvy.source/2's own convention:
.env.env.<env>--<env>is the explicit:dotenv_envoption, elseCOOPER_ENV(see below) as the real environment,:envand the base.envfile set it -- so.env.dev,.env.staging,.env.testand.env.prodin every Cooper, matchingenv/${COOPER_ENV}.casc. Picking it by the host's own name, as this once did, read.env.developmentbeside documents that readdev..env.localSystem.get_env/0-- the real environment outranks every file.- the
:envoption, if the caller passed one -- always the final, highest-precedence override, on top of every layer above it.
The real environment wins over .env files. A deployment sets
variables in the environment it controls; a file sitting in the working
directory must not silently beat them. This also matches what dotenv
implementations in other ecosystems do by default -- Ruby's and Node's
both decline to overwrite an already-set variable -- and what
twelve-factor configuration expects.
Pass dotenv_override: true for the opposite order, where the files
outrank the real environment. That is occasionally what a developer
wants locally, to shadow something exported in their shell, but it is
a deliberate choice rather than the default.
All of 2-4 are optional -- a missing file is silently skipped, never
a load-time error. .env.<env> in particular is expected to be
absent for every environment but the current one.
All paths are resolved relative to :dotenv_dir, which defaults to
the project root -- deliberately not :root (load_file/2's
config-file directory): .env files live at the project root
regardless of where the CASC file being loaded happens to sit, or
where the application was started from. The project root is the
directory of the running Mix project's mix.exs; in a release, which
has no Mix, RELEASE_ROOT; else the working directory. An absolute
path in :dotenv_files is used as it is.
:env is an override layer, not a replacement. Passing env: %{"FOO" => "bar"} does not isolate resolution from the real
environment or .env files -- it guarantees FOO resolves to
"bar" specifically, while every other ${...} reference still
falls through to .env/.env.local/the real OS environment. There
is currently no option that fully replaces every layer below it; a
test that needs a ${...} reference to resolve to a specific,
guaranteed-deterministic value should give that name an explicit
entry in :env rather than relying on it being otherwise unset.
COOPER_ENV
The one name a document reads the current environment by, in every
Cooper implementation (CASC.md §7.2): import "env/${COOPER_ENV}.casc".
Every layer above is consulted first, so a real COOPER_ENV -- in the
environment, a .env file, or :env -- always wins. Unset or empty,
it falls back to this host's own name for the same thing: MIX_ENV
from those same layers, else the live Mix.env/0 (Mix does not export
MIX_ENV to the OS environment), else -- in a release, which has no
Mix -- Application.compile_env(:cooper, :dotenv_env) if the host app
set config :cooper, dotenv_env: config_env(), else "dev". A fallback value is
mapped onto the names every Cooper uses -- development and local
become dev, testing becomes test, production becomes prod,
anything else is kept -- so a document selects env/prod.casc the same
way whichever host's convention set the variable. A real COOPER_ENV
is used exactly as written.
Enabling
.env file loading (layers 2-4) is on by default; dotenv: false
disables just those three layers -- System.get_env/0 and an
explicit :env still apply either way. :dotenvy is an ordinary
dependency of Cooper: it was an optional one, and the default then
silently read no .env file in any application that had not added it
itself.
Summary
Functions
Resolves the final env map for opts (the same options load_file/2/
load_string/2 take): the .env files layered per the moduledoc
above unless disabled, then System.get_env/0, with :env (if given)
applied last as the final override.
Types
Functions
@spec env(opts()) :: {:ok, %{required(String.t()) => String.t()}} | {:error, Ichor.Error.t()}
Resolves the final env map for opts (the same options load_file/2/
load_string/2 take): the .env files layered per the moduledoc
above unless disabled, then System.get_env/0, with :env (if given)
applied last as the final override.
Pass dotenv_override: true to put the files above the real
environment instead.