To make the migration as safe as possible, in places where it was
used to access environment variables that were absolutely required,
I replaced it with a function that forces the caller to explicitly
enumerate the environment variables they specifically expect,
allows them to access them in a type-safe way, and throws if the
named environment variables aren't set. (This is slightly different
than the previous behaviour, where AppConstants would only log the
missing variable without throwing, but I checked that all these
variables should be set in production.)
This makes it clearer what variables actually need to be set
locally, and which have been forgotten. It also makes the build
simpler, by removing the need to copy the .env-dist file.
This should be safe to apply, since .env-dist already got loaded by
default, just like .env now is. And it is still the case that
actual environment variables overwrite the ones in the .env file.
For non-Next.js setups (e.g. cron jobs or database migrations), I
switched to dotenv-flow. The regular dotenv explicitly avoids
inheritance [1], because it wants environment variables to be
specific to an environment. That was already not the case with most
of our environment variables, so the switch makes sense for us.
Next steps could be to remove unused variables from .env, and
possibly moving variables with local/stage-specific values to
.env.local.example, though that riskier, since environments might
depend on those being present.
[1]
https://www.npmjs.com/package/dotenv#should-i-have-multiple-env-files
This does add another devDependency; we could also simulate this
ourselves by e.g. augmenting the console methods in jest.setup.ts
to throw Errors - however, this gives us a nice error message that
gives some pointers on how to proceed if you do expect the console,
the package is pretty widely used, and is not part of the final
bundle.