| title | Dotenv editor (format-preserving) |
|---|
The dotenv editor lets you update .env* files without destroying formatting, comments, ordering, blank lines, or unknown lines, and it can deterministically select a target file across a get-dotenv paths cascade (with optional template bootstrap).
Use this when you want to:
- Update keys in place while preserving the human-edited file layout.
- Keep
.env.localor.env.<env>.localin sync with a secrets source without rewriting the whole file. - Bootstrap a gitignored file from a committed template (e.g.,
.env.local.template→.env.local).
The editor is format-preserving by design: it parses into a document model, applies edits to the model, and renders back to text.
All APIs below are exported from the package root (@karmaniverous/get-dotenv).
High-level entrypoints:
editDotenvText(text, updates, options?)→ string (pure text; no filesystem)editDotenvFile(updates, options)→{ path, createdFromTemplate, changed }(FS adapter; target selection + template bootstrap + in-place edit)
Lower-level building blocks (when you need more control):
parseDotenvDocument(text)→DotenvDocumentapplyDotenvEdits(doc, updates, opts?)→DotenvDocumentrenderDotenvDocument(doc, eolMode?)→ string
Types of interest:
DotenvUpdateMap,DotenvUpdateValueDotenvEditOptions(mode,duplicateKeys,eol, etc.)EditDotenvFileOptions,EditDotenvFileResultDotenvTargetScope(global | env),DotenvTargetPrivacy(public | private)
Use this when you already have the file contents in memory (or when you’re writing a tool that manages IO separately).
import { editDotenvText } from '@karmaniverous/get-dotenv';
const before = [
'# header',
'',
'APP_SETTING=one',
'raw line stays',
'ENV_SETTING=two',
'',
].join('\n');
const after = editDotenvText(before, { APP_SETTING: 'updated', UNUSED: null });
console.log(after);Key behaviors:
- Unknown/unparsed lines are preserved verbatim.
- Ordering is preserved; new keys (merge mode) are appended at the end.
- Inline comment spacing is preserved when updating existing assignment lines.
Updates are expressed as a Record<string, DotenvUpdateValue>.
Supported value types:
string | number | boolean→ written as string tokens (number/booleanare stringified)Record<string, unknown> | unknown[]→ written asJSON.stringify(value)null→ delete matching key lines (defaultnullBehavior: 'delete')undefined→ skip (defaultundefinedBehavior: 'skip')
Important nuance (sync mode):
- In
mode: 'sync', keys that are not present in the update map are deleted, but a key that is present withundefinedcounts as “present” and is not deleted (while also not being updated).
mode: 'merge'(default): update existing keys and append missing keys; do not delete unrelated keys.mode: 'sync': delete assignment/bare-key lines for keys not present in the update map, while preserving comments/unknown lines.
When the same key appears multiple times, you can choose how to update/delete occurrences:
duplicateKeys: 'all'(default)duplicateKeys: 'first'duplicateKeys: 'last'
The editor prefers correctness over minimal diff, but it preserves quote style where it is safe to do so.
Rules of thumb:
- Multiline values are always written using double quotes for correctness.
- Leading/trailing whitespace forces quoting (to avoid silent trimming by dotenv parsers).
- If a line has an inline comment suffix and the new value contains
#, the value is quoted to avoid comment truncation. - Bare-key placeholders like
FOOorBAR # commentare converted into assignments when updated (default separator=), preserving the inline comment.
Rendering can preserve EOLs or normalize them:
eol: 'preserve'(default): detect file EOL and keep it; inserted line breaks use the detected EOL.eol: 'lf'oreol: 'crlf': normalize the entire output.
Trailing newline presence/absence is preserved.
Use this when you want the library to select and update a specific dotenv target file across paths deterministically.
The target is determined by:
paths: string[](directories only; this is the only search surface)scope: 'global' | 'env'privacy: 'public' | 'private'dotenvToken(default:.env)privateToken(default:local)env(and optionaldefaultEnv) whenscope: 'env'
Filename construction:
- public/global:
<dotenvToken> - public/env:
<dotenvToken>.<env> - private/global:
<dotenvToken>.<privateToken> - private/env:
<dotenvToken>.<env>.<privateToken>
By default, selection uses reverse path order (highest precedence wins), matching the get-dotenv overlay model:
searchOrder: 'reverse'(default): checkspathsfrom last → firstsearchOrder: 'forward': checkspathsfrom first → last
If the target file does not exist anywhere under paths, but a template exists at <target>.<templateExtension>, it will be copied to <target> before editing.
templateExtension: 'template'by default, producing patterns like.env.local.template.
If neither the target nor the template exists under any provided path, editDotenvFile throws.
import { editDotenvFile } from '@karmaniverous/get-dotenv';
await editDotenvFile(
{ API_URL: 'https://example.com', UNUSED: null },
{
paths: ['./'],
scope: 'global',
privacy: 'private',
dotenvToken: '.env',
privateToken: 'local',
},
);import { editDotenvFile } from '@karmaniverous/get-dotenv';
await editDotenvFile(
{ FEATURE_FLAG: '1' },
{
paths: ['./'],
scope: 'env',
privacy: 'private',
defaultEnv: 'dev',
},
);- The parser is intentionally conservative: unrecognized/malformed lines are kept as raw text segments rather than being “fixed”.
- Unterminated quoted values are preserved verbatim as raw segments (other keys can still be edited safely).
- For dotenv cascade naming and precedence (reading), see Cascade and precedence.
- For config overlays and dynamic variables (authoring), see Config files and overlays.