@kindgi/dotenv-file
v0.1.6
Published
Dotenv files the way applications read them (dotenv grammar, verified against the version Next.js bundles), expanded (${VAR}), layered (.env < .env.local) and edited losslessly. Used by @kindgi/secrets-dotenv. Zero runtime dependencies.
Readme
@kindgi/dotenv-file
Dotenv files the way applications read them, written the way people edit them. Zero runtime dependencies, no filesystem access — you read and write the files; this package parses, expands, layers and edits their text.
Kindgi often lives inside an existing application and shares its .env
files. When both read the same file they must see the same values, and when
Kindgi writes a key it must not disturb anything else in the file. This
package is where that guarantee lives.
import { readEnvLayers, setKey } from '@kindgi/dotenv-file';
const env = readEnvLayers([
{ source: '.env', contents: await readOrNull('.env') },
{ source: '.env.local', contents: await readOrNull('.env.local') },
]);
env.values.DATABASE_URL; // merged + expanded
env.origin.DATABASE_URL; // '.env.local' — which file supplied it
const { contents } = setKey(existingText, 'ANTHROPIC_API_KEY', key);Reading — parseEnvFile
Grammar is dotenv's parse(), verified input-for-input against
[email protected] (the version Next.js 16 bundles) in
tests/conformance.test.ts — a hand-picked corpus plus 3000 generated
inputs.
KEY=valueorKEY: value; optional leading whitespace andexport.- Keys
[A-Za-z0-9_.-]+. - Unquoted values stop at the first
#and are trimmed. '…',"…",`…`may span lines. Inside double quotes\nand\rbecome newline / carriage return; nothing else is unescaped.- Duplicate keys: last wins.
- Lines dotenv ignores come back as
malformedso a rewrite never drops them.
Results are structured lines (EnvLine) — comments, blanks, malformed
lines and each entry's exact source text — so serializeEnvFile(parseEnvFile(x))
reproduces x (with \n line endings).
One deliberate divergence: dotenv's regex lets a key's separator, or an
empty value's leading whitespace, cross a newline (KEY= followed by a line
that starts with a quote joins the two). Here a key, its separator and the
start of its value are always on one line.
Expanding — expandEnv
$NAME, ${NAME}, ${NAME:-default}, ${NAME-default}, ${NAME:+alt},
${NAME+alt}, and \$ for a literal $. Defaults nest.
Host frameworks don't agree on expansion — Next.js 16 bundles
dotenv-expand 10, Vite uses 12. Kindgi matches both wherever they agree
(verified in tests/conformance.test.ts) and chooses as follows where they
don't:
| Case | Next.js (expand 10) | Vite (expand 12) | Kindgi |
|---|---|---|---|
| ${A}, A defined above | A | A | A |
| A=$B, B defined below | B | "" | B |
| ${A:-d}, A set in the same file | d | A | A |
| ${A-d}, ${A:+x}, ${A+x} | not supported | supported | supported |
| $1 | "" | $1 | $1 |
| shell env vs. a file's own value | shell wins | shell wins | file wins |
The last row is intentional: the files are the source, and
options.env only fills names no file defines (so ${HOME} works).
Nothing reads process.env unless the caller passes it. Undefined
references expand to ""; cycles are cut; both are reported in
diagnostics.
Layering — readEnvLayers
Several files as one view. Layers are ordered lowest precedence first;
a later layer's key wins. Raw values merge first and are expanded once over
the merged view, so a reference in .env to a name defined in .env.local
sees the .env.local value — the same result Next.js produces. Each key
reports its origin; malformed lines, unresolved references and cycles
come back as diagnostics with their source.
Writing — setKey, unsetKey, renderEntry
setKey replaces the last occurrence of a key (the one readers see),
keeping its export prefix, or appends. unsetKey removes every
occurrence. Every other line — comments, hand-written quoting, inline
comments, malformed lines — is kept byte-for-byte.
Values are rendered so they read back exactly, through dotenv and expansion:
[A-Za-z0-9_.,:/@+=%~-]+→ unquoted.- Otherwise double quotes, if the value has no
"and no literal\n/\rsequence (real newlines are written as\n). - Otherwise single quotes, if no
'and no carriage return. - Otherwise backticks, if no
`and no carriage return. - Otherwise
EnvValueNotRepresentableError— never a silently different value.
$ is always written as \$, because expanders substitute $NAME even
inside quotes and \$ is the escape they all undo.
Writes accept only POSIX env names (ENV_KEY_REGEX, [A-Z_][A-Z0-9_]*),
so every key Kindgi writes can also be exported from a shell. Reads accept
everything dotenv does (DOTENV_KEY_REGEX).
