@karmaniverous/get-dotenv 6.2.4 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/chunks/{AwsRestJsonProtocol-Bq1HE-Ln.mjs → AwsRestJsonProtocol-fYZqn-kW.mjs} +2 -2
- package/dist/chunks/{createCli-BY6_cfZr.mjs → createCli-BnRdfRRL.mjs} +7 -6
- package/dist/chunks/{externalDataInterceptor-CbsdEYa-.mjs → externalDataInterceptor-CILOLqbB.mjs} +2 -2
- package/dist/chunks/{getSSOTokenFromFile-hUSpR7Wf.mjs → getSSOTokenFromFile-BwMkZ_yT.mjs} +1 -1
- package/dist/chunks/{index-C_wqbTwI.mjs → index-0-nP97ri.mjs} +7 -6
- package/dist/chunks/{index-CeCufHlm.mjs → index-70Dm0f1N.mjs} +11 -10
- package/dist/chunks/{index-BPYF6K_G.mjs → index-BhVOypA1.mjs} +9 -8
- package/dist/chunks/{index-cIunyiUQ.mjs → index-CcwT4HJK.mjs} +6 -5
- package/dist/chunks/{index-BpCF5UKx.mjs → index-D8UL3w94.mjs} +6 -5
- package/dist/chunks/{index-Cu7rdyqN.mjs → index-DLNhHC15.mjs} +9 -8
- package/dist/chunks/{index-Dp1Ip6Ra.mjs → index-DWqbxY8Y.mjs} +11 -10
- package/dist/chunks/{index-c7zKtEuy.mjs → index-Du51s-Z0.mjs} +8 -7
- package/dist/chunks/{index-B5JKTBOL.mjs → index-DuSz0ul6.mjs} +8 -7
- package/dist/chunks/{index-DWAtHEA-.mjs → index-OeNCYa8T.mjs} +6 -5
- package/dist/chunks/{index-DyU5pKKi.mjs → index-_FP0whjC.mjs} +6 -5
- package/dist/chunks/{index-BEJFiHMX.mjs → index-o5zJ9PWL.mjs} +15 -15
- package/dist/chunks/{index-Bc3h0a95.mjs → index-r0Me7-sT.mjs} +112 -6
- package/dist/chunks/{loadSso-w1eTVg0O.mjs → loadSso-CLR1fKci.mjs} +8 -7
- package/dist/chunks/{loader-DnhPeGfq.mjs → loader-CePOf74i.mjs} +1 -0
- package/dist/chunks/{parseKnownFiles-B9cDK21V.mjs → parseKnownFiles-BQvmJ0HK.mjs} +1 -1
- package/dist/chunks/readDotenvCascade-DfFkWMjs.mjs +546 -0
- package/dist/chunks/{readMergedOptions-Nt0TR7dX.mjs → readMergedOptions-B7VdLROn.mjs} +62 -272
- package/dist/chunks/{resolveCliOptions-TFRzhB2c.mjs → resolveCliOptions-pgUXHJtj.mjs} +2 -1
- package/dist/chunks/{sdk-stream-mixin-BZoJ5jy9.mjs → sdk-stream-mixin-ecbbBR0l.mjs} +1 -1
- package/dist/chunks/{spawnEnv-CN8a7cNR.mjs → spawnEnv-CQwFu7ZJ.mjs} +2 -1
- package/dist/chunks/{types-DJ-BGABd.mjs → types-CVDR-Sjk.mjs} +1 -1
- package/dist/cli.d.ts +218 -84
- package/dist/cli.mjs +10 -10
- package/dist/cliHost.d.ts +218 -84
- package/dist/cliHost.mjs +8 -7
- package/dist/config.mjs +2 -1
- package/dist/env-overlay.d.ts +304 -2
- package/dist/env-overlay.mjs +38 -1
- package/dist/getdotenv.cli.mjs +10 -10
- package/dist/index.d.ts +703 -86
- package/dist/index.mjs +862 -13
- package/dist/plugins-aws.d.ts +153 -19
- package/dist/plugins-aws.mjs +5 -4
- package/dist/plugins-batch.d.ts +153 -19
- package/dist/plugins-batch.mjs +5 -4
- package/dist/plugins-cmd.d.ts +153 -19
- package/dist/plugins-cmd.mjs +7 -6
- package/dist/plugins-init.d.ts +153 -19
- package/dist/plugins-init.mjs +4 -4
- package/dist/plugins.d.ts +153 -19
- package/dist/plugins.mjs +9 -9
- package/package.json +1 -1
- package/dist/chunks/overlayEnv-Bs2kVayG.mjs +0 -234
package/dist/index.mjs
CHANGED
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
export { c as createCli } from './chunks/createCli-
|
|
2
|
-
|
|
3
|
-
export { b as
|
|
4
|
-
export { d as
|
|
5
|
-
|
|
1
|
+
export { c as createCli } from './chunks/createCli-BnRdfRRL.mjs';
|
|
2
|
+
import { e as resolveGetDotenvOptions, w as writeDotenvFile } from './chunks/readMergedOptions-B7VdLROn.mjs';
|
|
3
|
+
export { G as GetDotenvCli, b as baseRootOptionDefaults, f as defineDynamic, h as defineGetDotenvConfig, d as definePlugin, g as getDotenvCliOptions2Options, i as interpolateDeep, r as readMergedOptions } from './chunks/readMergedOptions-B7VdLROn.mjs';
|
|
4
|
+
export { d as buildSpawnEnv, s as shouldCapture } from './chunks/spawnEnv-CQwFu7ZJ.mjs';
|
|
5
|
+
export { d as defineScripts, g as groupPlugins } from './chunks/types-CVDR-Sjk.mjs';
|
|
6
|
+
import fs from 'fs-extra';
|
|
6
7
|
import 'crypto';
|
|
7
|
-
import 'path';
|
|
8
|
+
import path$1 from 'path';
|
|
8
9
|
import 'url';
|
|
9
10
|
export { z } from 'zod';
|
|
11
|
+
import { nanoid } from 'nanoid';
|
|
12
|
+
import { r as redactObject, m as maybeWarnEntropy } from './chunks/index-r0Me7-sT.mjs';
|
|
13
|
+
export { a as redactDisplay, t as traceChildEnv } from './chunks/index-r0Me7-sT.mjs';
|
|
14
|
+
import { f as readDotenv, d as dotenvExpandAll, a as applyDynamicMap, c as loadAndApplyDynamic } from './chunks/readDotenvCascade-DfFkWMjs.mjs';
|
|
15
|
+
export { g as dotenvExpand, h as dotenvExpandFromProcessEnv } from './chunks/readDotenvCascade-DfFkWMjs.mjs';
|
|
16
|
+
import path from 'node:path';
|
|
10
17
|
import 'dotenv';
|
|
11
|
-
|
|
12
|
-
export { d as dotenvExpand, a as dotenvExpandAll, b as dotenvExpandFromProcessEnv } from './chunks/overlayEnv-Bs2kVayG.mjs';
|
|
13
|
-
import './chunks/loader-DnhPeGfq.mjs';
|
|
18
|
+
import './chunks/loader-CePOf74i.mjs';
|
|
14
19
|
import 'package-directory';
|
|
15
20
|
import 'yaml';
|
|
16
21
|
import './chunks/loadModuleDefault-Dj8B3Stt.mjs';
|
|
17
|
-
import 'nanoid';
|
|
18
22
|
import 'execa';
|
|
19
23
|
import './chunks/helpConfig-CGejgwWW.mjs';
|
|
20
|
-
import './chunks/resolveCliOptions-
|
|
24
|
+
import './chunks/resolveCliOptions-pgUXHJtj.mjs';
|
|
21
25
|
import './chunks/validate-CDl0rE6k.mjs';
|
|
22
26
|
import './plugins-aws.mjs';
|
|
23
27
|
import '@commander-js/extra-typings';
|
|
24
|
-
import './chunks/index-
|
|
28
|
+
import './chunks/index-70Dm0f1N.mjs';
|
|
25
29
|
import 'buffer';
|
|
26
30
|
import 'os';
|
|
27
31
|
import 'node:fs/promises';
|
|
@@ -36,5 +40,850 @@ import 'globby';
|
|
|
36
40
|
import './plugins-init.mjs';
|
|
37
41
|
import 'node:process';
|
|
38
42
|
import 'readline/promises';
|
|
39
|
-
import 'node:path';
|
|
40
43
|
import 'node:url';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Dotenv editor types (format-preserving).
|
|
47
|
+
*
|
|
48
|
+
* Requirements addressed:
|
|
49
|
+
* - Provide a format-preserving dotenv edit surface (pure text pipeline + FS adapter).
|
|
50
|
+
* - Preserve comments/blank lines/unknown lines and separator spacing; support merge vs sync.
|
|
51
|
+
* - Deterministic target selection across getdotenv `paths` with optional template bootstrap.
|
|
52
|
+
*
|
|
53
|
+
* Notes:
|
|
54
|
+
* - The parser intentionally preserves unknown lines verbatim rather than rejecting them.
|
|
55
|
+
* - The editor focuses on `.env`-style KEY=VALUE lines; anything not recognized is preserved as raw text.
|
|
56
|
+
*
|
|
57
|
+
* @packageDocumentation
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* Narrow helper for internal use: detect own properties safely.
|
|
61
|
+
*
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Apply edits to a parsed dotenv document while preserving formatting.
|
|
68
|
+
*
|
|
69
|
+
* Requirements addressed:
|
|
70
|
+
* - Mode: merge vs sync.
|
|
71
|
+
* - Duplicate key strategy: all/first/last.
|
|
72
|
+
* - undefined behavior: skip (default).
|
|
73
|
+
* - null behavior: delete (default).
|
|
74
|
+
* - Quote preservation where safe; upgrade quoting when required (multiline, whitespace safety, inline comment safety).
|
|
75
|
+
*
|
|
76
|
+
* @packageDocumentation
|
|
77
|
+
*/
|
|
78
|
+
const coerceToString = (v) => {
|
|
79
|
+
if (typeof v === 'string')
|
|
80
|
+
return v;
|
|
81
|
+
if (typeof v === 'number')
|
|
82
|
+
return String(v);
|
|
83
|
+
if (typeof v === 'boolean')
|
|
84
|
+
return v ? 'true' : 'false';
|
|
85
|
+
if (v === null || v === undefined)
|
|
86
|
+
return '';
|
|
87
|
+
return JSON.stringify(v);
|
|
88
|
+
};
|
|
89
|
+
const needsQuoted = (value, hasInlineSuffix) => {
|
|
90
|
+
if (value.includes('\n'))
|
|
91
|
+
return true;
|
|
92
|
+
if (value.includes('\r'))
|
|
93
|
+
return true;
|
|
94
|
+
// Preserve correctness: leading/trailing whitespace should be quoted.
|
|
95
|
+
if (/^\s|\s$/.test(value))
|
|
96
|
+
return true;
|
|
97
|
+
// Inline comment safety: if there is a suffix (comment) and the value contains '#', quote it.
|
|
98
|
+
if (hasInlineSuffix && value.includes('#'))
|
|
99
|
+
return true;
|
|
100
|
+
return false;
|
|
101
|
+
};
|
|
102
|
+
const escapeDoubleQuoted = (value) =>
|
|
103
|
+
// Minimal escaping: escape only `"`. (Do not force-escape backslashes.)
|
|
104
|
+
value.replace(/"/g, '\\"');
|
|
105
|
+
const toFileEol = (valueLf, fileEol) => fileEol === '\n' ? valueLf : valueLf.replace(/\n/g, '\r\n');
|
|
106
|
+
const renderValueToken = (args) => {
|
|
107
|
+
const { value, preferQuote, hasInlineSuffix, fileEol } = args;
|
|
108
|
+
const multiline = value.includes('\n');
|
|
109
|
+
if (multiline) {
|
|
110
|
+
// Multiline correctness: use double quotes.
|
|
111
|
+
return {
|
|
112
|
+
token: `"${toFileEol(escapeDoubleQuoted(value), fileEol)}"`,
|
|
113
|
+
quote: '"',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const mustQuote = needsQuoted(value, hasInlineSuffix);
|
|
117
|
+
if (!mustQuote && preferQuote === null)
|
|
118
|
+
return { token: value, quote: null };
|
|
119
|
+
// Try to preserve original quote style when safe.
|
|
120
|
+
if (preferQuote === "'" && !value.includes("'") && !mustQuote) {
|
|
121
|
+
return { token: `'${value}'`, quote: "'" };
|
|
122
|
+
}
|
|
123
|
+
if (preferQuote === "'" && !value.includes("'")) {
|
|
124
|
+
return { token: `'${value}'`, quote: "'" };
|
|
125
|
+
}
|
|
126
|
+
// Prefer double quotes as the general safe fallback.
|
|
127
|
+
return { token: `"${escapeDoubleQuoted(value)}"`, quote: '"' };
|
|
128
|
+
};
|
|
129
|
+
const pickIndexes = (idxs, strategy) => {
|
|
130
|
+
if (idxs.length === 0)
|
|
131
|
+
return [];
|
|
132
|
+
if (strategy === 'first') {
|
|
133
|
+
const first = idxs[0];
|
|
134
|
+
return first === undefined ? [] : [first];
|
|
135
|
+
}
|
|
136
|
+
if (strategy === 'last') {
|
|
137
|
+
const last = idxs[idxs.length - 1];
|
|
138
|
+
return last === undefined ? [] : [last];
|
|
139
|
+
}
|
|
140
|
+
return idxs.slice();
|
|
141
|
+
};
|
|
142
|
+
const segmentEndEol = (seg) => {
|
|
143
|
+
if ('eol' in seg)
|
|
144
|
+
return seg.eol;
|
|
145
|
+
// Raw segments store EOL inside raw; best-effort detect tail.
|
|
146
|
+
const raw = seg.raw;
|
|
147
|
+
if (raw.endsWith('\r\n'))
|
|
148
|
+
return '\r\n';
|
|
149
|
+
if (raw.endsWith('\n'))
|
|
150
|
+
return '\n';
|
|
151
|
+
return '';
|
|
152
|
+
};
|
|
153
|
+
const rebuildAssignmentRaw = (args) => {
|
|
154
|
+
const { seg, valueLf, fileEol } = args;
|
|
155
|
+
const { token, quote } = renderValueToken({
|
|
156
|
+
value: valueLf,
|
|
157
|
+
preferQuote: seg.quote,
|
|
158
|
+
hasInlineSuffix: seg.suffix.length > 0,
|
|
159
|
+
fileEol,
|
|
160
|
+
});
|
|
161
|
+
const line = `${seg.prefix}${seg.key}${seg.separator}${seg.valuePadding}${token}${seg.suffix}`;
|
|
162
|
+
const eol = segmentEndEol(seg);
|
|
163
|
+
const raw = eol ? line + eol : line;
|
|
164
|
+
return { ...seg, raw, value: valueLf, quote, eol };
|
|
165
|
+
};
|
|
166
|
+
const rebuildBareAsAssignment = (args) => {
|
|
167
|
+
const { seg, valueLf, fileEol, defaultSeparator } = args;
|
|
168
|
+
const { token, quote } = renderValueToken({
|
|
169
|
+
value: valueLf,
|
|
170
|
+
preferQuote: null,
|
|
171
|
+
hasInlineSuffix: seg.suffix.length > 0,
|
|
172
|
+
fileEol,
|
|
173
|
+
});
|
|
174
|
+
const separator = defaultSeparator;
|
|
175
|
+
const line = `${seg.prefix}${seg.key}${separator}${token}${seg.suffix}`;
|
|
176
|
+
const eol = segmentEndEol(seg);
|
|
177
|
+
const raw = eol ? line + eol : line;
|
|
178
|
+
return {
|
|
179
|
+
kind: 'assignment',
|
|
180
|
+
raw,
|
|
181
|
+
key: seg.key,
|
|
182
|
+
prefix: seg.prefix,
|
|
183
|
+
separator,
|
|
184
|
+
valuePadding: '',
|
|
185
|
+
quote,
|
|
186
|
+
value: valueLf,
|
|
187
|
+
suffix: seg.suffix,
|
|
188
|
+
eol,
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
const buildNewAssignmentAtEnd = (args) => {
|
|
192
|
+
const { key, valueLf, fileEol, defaultSeparator, endEol } = args;
|
|
193
|
+
const { token, quote } = renderValueToken({
|
|
194
|
+
value: valueLf,
|
|
195
|
+
preferQuote: null,
|
|
196
|
+
hasInlineSuffix: false,
|
|
197
|
+
fileEol,
|
|
198
|
+
});
|
|
199
|
+
const line = `${key}${defaultSeparator}${token}`;
|
|
200
|
+
const raw = endEol ? line + endEol : line;
|
|
201
|
+
return {
|
|
202
|
+
kind: 'assignment',
|
|
203
|
+
raw,
|
|
204
|
+
key,
|
|
205
|
+
prefix: '',
|
|
206
|
+
separator: defaultSeparator,
|
|
207
|
+
valuePadding: '',
|
|
208
|
+
quote,
|
|
209
|
+
value: valueLf,
|
|
210
|
+
suffix: '',
|
|
211
|
+
eol: endEol,
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Apply a set of key/value updates to a parsed dotenv document.
|
|
216
|
+
*
|
|
217
|
+
* @param doc - Parsed dotenv document.
|
|
218
|
+
* @param updates - Key/value update map.
|
|
219
|
+
* @param opts - Editing options.
|
|
220
|
+
* @returns A new {@link DotenvDocument} with edits applied.
|
|
221
|
+
*
|
|
222
|
+
* @public
|
|
223
|
+
*/
|
|
224
|
+
function applyDotenvEdits(doc, updates, opts) {
|
|
225
|
+
const mode = opts?.mode ?? 'merge';
|
|
226
|
+
const duplicateKeys = opts?.duplicateKeys ?? 'all';
|
|
227
|
+
const defaultSeparator = opts?.defaultSeparator ?? '=';
|
|
228
|
+
const segs = doc.segments.slice();
|
|
229
|
+
// Index key-bearing segments.
|
|
230
|
+
const byKey = new Map();
|
|
231
|
+
for (let i = 0; i < segs.length; i++) {
|
|
232
|
+
const s = segs[i];
|
|
233
|
+
if (!s)
|
|
234
|
+
continue;
|
|
235
|
+
if (s.kind === 'assignment' || s.kind === 'bare') {
|
|
236
|
+
const list = byKey.get(s.key) ?? [];
|
|
237
|
+
list.push(i);
|
|
238
|
+
byKey.set(s.key, list);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const deleteIdx = new Set();
|
|
242
|
+
const replace = new Map();
|
|
243
|
+
// Sync deletions: remove any existing key lines not present in update map (own props).
|
|
244
|
+
if (mode === 'sync') {
|
|
245
|
+
for (const [key, idxs] of byKey.entries()) {
|
|
246
|
+
if (!hasOwn(updates, key)) {
|
|
247
|
+
for (const i of idxs)
|
|
248
|
+
deleteIdx.add(i);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Apply update-driven deletes and replacements.
|
|
253
|
+
for (const key of Object.keys(updates)) {
|
|
254
|
+
const v = updates[key];
|
|
255
|
+
const idxsAll = byKey.get(key) ?? [];
|
|
256
|
+
if (v === null) {
|
|
257
|
+
for (const i of pickIndexes(idxsAll, duplicateKeys))
|
|
258
|
+
deleteIdx.add(i);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (v === undefined) {
|
|
262
|
+
// Note: in sync mode, this key counts as present (do not delete); replacements are skipped.
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const valueLf = coerceToString(v);
|
|
266
|
+
for (const i of pickIndexes(idxsAll, duplicateKeys)) {
|
|
267
|
+
const seg = segs[i];
|
|
268
|
+
if (!seg)
|
|
269
|
+
continue;
|
|
270
|
+
if (seg.kind === 'assignment') {
|
|
271
|
+
replace.set(i, rebuildAssignmentRaw({ seg, valueLf, fileEol: doc.fileEol }));
|
|
272
|
+
}
|
|
273
|
+
else if (seg.kind === 'bare') {
|
|
274
|
+
replace.set(i, rebuildBareAsAssignment({
|
|
275
|
+
seg,
|
|
276
|
+
valueLf,
|
|
277
|
+
fileEol: doc.fileEol,
|
|
278
|
+
defaultSeparator,
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Rebuild the segment list, applying deletions and replacements.
|
|
284
|
+
const out = [];
|
|
285
|
+
for (let i = 0; i < segs.length; i++) {
|
|
286
|
+
if (deleteIdx.has(i))
|
|
287
|
+
continue;
|
|
288
|
+
const repl = replace.get(i);
|
|
289
|
+
const original = segs[i];
|
|
290
|
+
if (repl)
|
|
291
|
+
out.push(repl);
|
|
292
|
+
else if (original)
|
|
293
|
+
out.push(original);
|
|
294
|
+
}
|
|
295
|
+
// Merge mode: append missing keys (only when provided with a concrete value).
|
|
296
|
+
if (mode === 'merge') {
|
|
297
|
+
const existingKeys = new Set();
|
|
298
|
+
for (const s of out) {
|
|
299
|
+
if (s.kind === 'assignment' || s.kind === 'bare')
|
|
300
|
+
existingKeys.add(s.key);
|
|
301
|
+
}
|
|
302
|
+
// Use the document EOL when inserting new lines; final newline presence is handled by render().
|
|
303
|
+
const endEol = doc.fileEol;
|
|
304
|
+
for (const key of Object.keys(updates)) {
|
|
305
|
+
if (existingKeys.has(key))
|
|
306
|
+
continue;
|
|
307
|
+
const v = updates[key];
|
|
308
|
+
if (v === undefined)
|
|
309
|
+
continue;
|
|
310
|
+
if (v === null)
|
|
311
|
+
continue;
|
|
312
|
+
const valueLf = coerceToString(v);
|
|
313
|
+
out.push(buildNewAssignmentAtEnd({
|
|
314
|
+
key,
|
|
315
|
+
valueLf,
|
|
316
|
+
fileEol: doc.fileEol,
|
|
317
|
+
defaultSeparator,
|
|
318
|
+
endEol,
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { ...doc, segments: out };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Parse a dotenv file into a format-preserving document model.
|
|
327
|
+
*
|
|
328
|
+
* Requirements addressed:
|
|
329
|
+
* - Preserve comments, blank lines, ordering, and unknown lines verbatim.
|
|
330
|
+
* - Preserve separator spacing around `=`.
|
|
331
|
+
* - Support multiline quoted values (double/single quotes) by grouping physical lines.
|
|
332
|
+
*
|
|
333
|
+
* @packageDocumentation
|
|
334
|
+
*/
|
|
335
|
+
const detectFileEol = (txt) => txt.includes('\r\n') ? '\r\n' : '\n';
|
|
336
|
+
const endsWithNewline$1 = (txt) => txt.endsWith('\n') || txt.endsWith('\r\n');
|
|
337
|
+
const splitLinesWithEol = (txt) => {
|
|
338
|
+
const out = [];
|
|
339
|
+
let start = 0;
|
|
340
|
+
for (let i = 0; i < txt.length; i++) {
|
|
341
|
+
const ch = txt.charAt(i);
|
|
342
|
+
if (ch !== '\n')
|
|
343
|
+
continue;
|
|
344
|
+
const isCrlf = i > 0 && txt.charAt(i - 1) === '\r';
|
|
345
|
+
const line = txt.slice(start, isCrlf ? i - 1 : i);
|
|
346
|
+
out.push({ line, eol: isCrlf ? '\r\n' : '\n' });
|
|
347
|
+
start = i + 1;
|
|
348
|
+
}
|
|
349
|
+
if (start < txt.length) {
|
|
350
|
+
out.push({ line: txt.slice(start), eol: '' });
|
|
351
|
+
}
|
|
352
|
+
else if (txt.length === 0) {
|
|
353
|
+
out.push({ line: '', eol: '' });
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
};
|
|
357
|
+
const normalizeInnerNewlinesToLf = (v) => v.replace(/\r\n/g, '\n');
|
|
358
|
+
const findFirstNonWhitespace = (s) => {
|
|
359
|
+
for (let i = 0; i < s.length; i++) {
|
|
360
|
+
if (!/\s/.test(s.charAt(i)))
|
|
361
|
+
return i;
|
|
362
|
+
}
|
|
363
|
+
return -1;
|
|
364
|
+
};
|
|
365
|
+
const splitInlineCommentUnquoted = (rhs) => {
|
|
366
|
+
// Match a `#` that is preceded by whitespace; keep all whitespace before `#` with the suffix.
|
|
367
|
+
for (let i = 0; i < rhs.length; i++) {
|
|
368
|
+
const ch = rhs.charAt(i);
|
|
369
|
+
if (ch !== '#')
|
|
370
|
+
continue;
|
|
371
|
+
if (i === 0)
|
|
372
|
+
return { value: '', suffix: rhs };
|
|
373
|
+
const prev = rhs.charAt(i - 1);
|
|
374
|
+
if (!/\s/.test(prev))
|
|
375
|
+
continue;
|
|
376
|
+
let j = i;
|
|
377
|
+
while (j > 0 && /\s/.test(rhs.charAt(j - 1)))
|
|
378
|
+
j--;
|
|
379
|
+
return { value: rhs.slice(0, j), suffix: rhs.slice(j) };
|
|
380
|
+
}
|
|
381
|
+
return { value: rhs, suffix: '' };
|
|
382
|
+
};
|
|
383
|
+
const tryParseQuotedValueAcrossLines = (lines, startIndex, startRhs, quote) => {
|
|
384
|
+
// Parse the quoted value beginning at the first quote in startRhs.
|
|
385
|
+
// Returns null if a closing quote is never found (to avoid corrupting segmentation).
|
|
386
|
+
let idx = startIndex;
|
|
387
|
+
let rhs = startRhs;
|
|
388
|
+
const firstQuoteIdx = rhs.indexOf(quote);
|
|
389
|
+
if (firstQuoteIdx < 0)
|
|
390
|
+
return null;
|
|
391
|
+
// Value text begins after the opening quote.
|
|
392
|
+
let cursor = firstQuoteIdx + 1;
|
|
393
|
+
let escaped = false;
|
|
394
|
+
let valueOut = '';
|
|
395
|
+
while (idx < lines.length) {
|
|
396
|
+
for (; cursor < rhs.length; cursor++) {
|
|
397
|
+
const ch = rhs.charAt(cursor);
|
|
398
|
+
if (escaped) {
|
|
399
|
+
valueOut += ch;
|
|
400
|
+
escaped = false;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (ch === '\\') {
|
|
404
|
+
// Preserve backslashes; caller may choose to re-escape on render.
|
|
405
|
+
valueOut += ch;
|
|
406
|
+
escaped = true;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (ch === quote) {
|
|
410
|
+
// Closing quote: suffix is the remainder of this physical line after the closing quote.
|
|
411
|
+
return {
|
|
412
|
+
endIndex: idx,
|
|
413
|
+
value: normalizeInnerNewlinesToLf(valueOut),
|
|
414
|
+
suffix: rhs.slice(cursor + 1),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
valueOut += ch;
|
|
418
|
+
}
|
|
419
|
+
// Move to next physical line; insert a newline into the value.
|
|
420
|
+
idx += 1;
|
|
421
|
+
if (idx >= lines.length)
|
|
422
|
+
break;
|
|
423
|
+
// If the file ended without a newline, we still treat the join as a newline inside the quoted value.
|
|
424
|
+
valueOut += '\n';
|
|
425
|
+
const next = lines[idx];
|
|
426
|
+
if (!next)
|
|
427
|
+
break;
|
|
428
|
+
rhs = next.line;
|
|
429
|
+
cursor = 0;
|
|
430
|
+
escaped = false;
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
};
|
|
434
|
+
const parseKeyPrefix = (line) => {
|
|
435
|
+
// Allow: indentation + optional "export " + KEY
|
|
436
|
+
// KEY must be a shell-ish identifier.
|
|
437
|
+
const m = /^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)(.*)$/.exec(line);
|
|
438
|
+
if (!m)
|
|
439
|
+
return null;
|
|
440
|
+
const prefix = m[1] ?? '';
|
|
441
|
+
const key = m[2] ?? '';
|
|
442
|
+
const rest = m[3] ?? '';
|
|
443
|
+
if (!key)
|
|
444
|
+
return null;
|
|
445
|
+
return { prefix, key, rest };
|
|
446
|
+
};
|
|
447
|
+
/**
|
|
448
|
+
* Parse dotenv text into a document model that preserves formatting.
|
|
449
|
+
*
|
|
450
|
+
* @param text - Dotenv file contents as UTF-8 text.
|
|
451
|
+
* @returns A parsed {@link DotenvDocument}.
|
|
452
|
+
*
|
|
453
|
+
* @public
|
|
454
|
+
*/
|
|
455
|
+
function parseDotenvDocument(text) {
|
|
456
|
+
const fileEol = detectFileEol(text);
|
|
457
|
+
const trailingNewline = endsWithNewline$1(text);
|
|
458
|
+
const lines = splitLinesWithEol(text);
|
|
459
|
+
const segments = [];
|
|
460
|
+
for (let i = 0; i < lines.length; i++) {
|
|
461
|
+
const cur = lines[i];
|
|
462
|
+
if (!cur)
|
|
463
|
+
continue;
|
|
464
|
+
const line = cur.line;
|
|
465
|
+
const eol = cur.eol;
|
|
466
|
+
const parsed = parseKeyPrefix(line);
|
|
467
|
+
if (!parsed) {
|
|
468
|
+
const rawSeg = { kind: 'raw', raw: line + eol };
|
|
469
|
+
segments.push(rawSeg);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const { prefix, key, rest } = parsed;
|
|
473
|
+
const mAssign = /^(\s*=\s*)(.*)$/.exec(rest);
|
|
474
|
+
if (mAssign) {
|
|
475
|
+
const separator = mAssign[1] ?? '=';
|
|
476
|
+
const rhs = mAssign[2] ?? '';
|
|
477
|
+
// Preserve whitespace between separator and first token.
|
|
478
|
+
const nonWs = findFirstNonWhitespace(rhs);
|
|
479
|
+
const valuePadding = nonWs >= 0 ? rhs.slice(0, nonWs) : rhs;
|
|
480
|
+
const tokenStart = nonWs >= 0 ? rhs.charAt(nonWs) : '';
|
|
481
|
+
// Try quoted parsing first (single-line or multiline).
|
|
482
|
+
if (tokenStart === '"' || tokenStart === "'") {
|
|
483
|
+
const quote = tokenStart === '"' ? '"' : "'";
|
|
484
|
+
const parsedQuoted = tryParseQuotedValueAcrossLines(lines, i, rhs, quote);
|
|
485
|
+
if (parsedQuoted) {
|
|
486
|
+
const endIndex = parsedQuoted.endIndex;
|
|
487
|
+
const raw = lines
|
|
488
|
+
.slice(i, endIndex + 1)
|
|
489
|
+
.map((l) => l.line + l.eol)
|
|
490
|
+
.join('');
|
|
491
|
+
const endEol = lines[endIndex]?.eol ?? '';
|
|
492
|
+
const seg = {
|
|
493
|
+
kind: 'assignment',
|
|
494
|
+
raw,
|
|
495
|
+
key,
|
|
496
|
+
prefix,
|
|
497
|
+
separator,
|
|
498
|
+
valuePadding,
|
|
499
|
+
quote,
|
|
500
|
+
value: parsedQuoted.value,
|
|
501
|
+
suffix: parsedQuoted.suffix,
|
|
502
|
+
eol: endEol,
|
|
503
|
+
};
|
|
504
|
+
segments.push(seg);
|
|
505
|
+
i = endIndex;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
// If quote is unclosed, preserve the line verbatim as raw.
|
|
509
|
+
// This avoids accidentally “parsing” malformed lines and losing formatting.
|
|
510
|
+
const rawSeg = { kind: 'raw', raw: line + eol };
|
|
511
|
+
segments.push(rawSeg);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
// Unquoted single-line value: split inline comments.
|
|
515
|
+
const { value, suffix } = splitInlineCommentUnquoted(rhs);
|
|
516
|
+
const seg = {
|
|
517
|
+
kind: 'assignment',
|
|
518
|
+
raw: line + eol,
|
|
519
|
+
key,
|
|
520
|
+
prefix,
|
|
521
|
+
separator,
|
|
522
|
+
valuePadding,
|
|
523
|
+
quote: null,
|
|
524
|
+
value,
|
|
525
|
+
suffix,
|
|
526
|
+
eol,
|
|
527
|
+
};
|
|
528
|
+
segments.push(seg);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
// Bare-key placeholder (KEY or KEY # comment). Only accept whitespace/comment in rest.
|
|
532
|
+
const mBare = /^(\s*)(#.*)?$/.exec(rest);
|
|
533
|
+
if (mBare) {
|
|
534
|
+
const suffix = (mBare[1] ?? '') + (mBare[2] ?? '');
|
|
535
|
+
const seg = {
|
|
536
|
+
kind: 'bare',
|
|
537
|
+
raw: line + eol,
|
|
538
|
+
key,
|
|
539
|
+
prefix,
|
|
540
|
+
suffix,
|
|
541
|
+
eol,
|
|
542
|
+
};
|
|
543
|
+
segments.push(seg);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
// Unknown/unsupported syntax for this line: preserve verbatim.
|
|
547
|
+
const rawSeg = { kind: 'raw', raw: line + eol };
|
|
548
|
+
segments.push(rawSeg);
|
|
549
|
+
}
|
|
550
|
+
return { fileEol, trailingNewline, segments };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Render a parsed dotenv document back to text.
|
|
555
|
+
*
|
|
556
|
+
* Requirements addressed:
|
|
557
|
+
* - Preserve existing EOLs by default; support forcing LF/CRLF.
|
|
558
|
+
* - Preserve trailing newline presence/absence.
|
|
559
|
+
*
|
|
560
|
+
* @packageDocumentation
|
|
561
|
+
*/
|
|
562
|
+
const normalizeEol = (txt, eol) => txt.replace(/\r?\n/g, eol);
|
|
563
|
+
const stripOneFinalNewline = (txt) => {
|
|
564
|
+
if (txt.endsWith('\r\n'))
|
|
565
|
+
return txt.slice(0, -2);
|
|
566
|
+
if (txt.endsWith('\n'))
|
|
567
|
+
return txt.slice(0, -1);
|
|
568
|
+
return txt;
|
|
569
|
+
};
|
|
570
|
+
const endsWithNewline = (txt) => txt.endsWith('\n') || txt.endsWith('\r\n');
|
|
571
|
+
/**
|
|
572
|
+
* Render a {@link DotenvDocument} to text.
|
|
573
|
+
*
|
|
574
|
+
* @param doc - Document to render.
|
|
575
|
+
* @param eolMode - EOL policy (`preserve` | `lf` | `crlf`).
|
|
576
|
+
* @returns Rendered dotenv text.
|
|
577
|
+
*
|
|
578
|
+
* @public
|
|
579
|
+
*/
|
|
580
|
+
function renderDotenvDocument(doc, eolMode = 'preserve') {
|
|
581
|
+
const joined = doc.segments.map((s) => s.raw).join('');
|
|
582
|
+
const targetEol = eolMode === 'crlf' ? '\r\n' : eolMode === 'lf' ? '\n' : doc.fileEol;
|
|
583
|
+
const normalized = eolMode === 'preserve' ? joined : normalizeEol(joined, targetEol);
|
|
584
|
+
// Preserve final newline presence/absence.
|
|
585
|
+
if (doc.trailingNewline) {
|
|
586
|
+
return endsWithNewline(normalized) ? normalized : normalized + targetEol;
|
|
587
|
+
}
|
|
588
|
+
return stripOneFinalNewline(normalized);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* High-level convenience API for editing dotenv text in memory.
|
|
593
|
+
*
|
|
594
|
+
* Requirements addressed:
|
|
595
|
+
* - Pure/text layer: parse → apply edits → render (no FS).
|
|
596
|
+
*
|
|
597
|
+
* @packageDocumentation
|
|
598
|
+
*/
|
|
599
|
+
/**
|
|
600
|
+
* Edit dotenv text with format preservation.
|
|
601
|
+
*
|
|
602
|
+
* @param text - Existing dotenv text.
|
|
603
|
+
* @param updates - Update map of keys to values.
|
|
604
|
+
* @param options - Edit options (merge vs sync, duplicates, null/undefined behavior, EOL policy).
|
|
605
|
+
* @returns Updated dotenv text.
|
|
606
|
+
*
|
|
607
|
+
* @public
|
|
608
|
+
*/
|
|
609
|
+
function editDotenvText(text, updates, options = {}) {
|
|
610
|
+
const doc = parseDotenvDocument(text);
|
|
611
|
+
const edited = applyDotenvEdits(doc, updates, {
|
|
612
|
+
...(options.mode ? { mode: options.mode } : {}),
|
|
613
|
+
...(options.duplicateKeys ? { duplicateKeys: options.duplicateKeys } : {}),
|
|
614
|
+
...(options.undefinedBehavior
|
|
615
|
+
? { undefinedBehavior: options.undefinedBehavior }
|
|
616
|
+
: {}),
|
|
617
|
+
...(options.nullBehavior ? { nullBehavior: options.nullBehavior } : {}),
|
|
618
|
+
...(typeof options.defaultSeparator === 'string'
|
|
619
|
+
? { defaultSeparator: options.defaultSeparator }
|
|
620
|
+
: {}),
|
|
621
|
+
});
|
|
622
|
+
return renderDotenvDocument(edited, options.eol ?? 'preserve');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* @packageDocumentation
|
|
627
|
+
* Deterministic dotenv target selection across a multi-path cascade.
|
|
628
|
+
*
|
|
629
|
+
* This module extracts the “which file should we edit?” logic from the FS-level editor
|
|
630
|
+
* so plugins/tools can reuse the same selection semantics as {@link editDotenvFile}.
|
|
631
|
+
*
|
|
632
|
+
* Notes:
|
|
633
|
+
* - Selection is based on `paths` only (directories), consistent with get-dotenv overlay precedence.
|
|
634
|
+
* - Default search order is reverse (last path wins).
|
|
635
|
+
* - Template discovery is supported via `<target>.<templateExtension>` and returns the template path
|
|
636
|
+
* when the concrete target is missing.
|
|
637
|
+
*/
|
|
638
|
+
const resolveEnvName = (env, defaultEnv) => typeof env === 'string' && env.length > 0
|
|
639
|
+
? env
|
|
640
|
+
: typeof defaultEnv === 'string' && defaultEnv.length > 0
|
|
641
|
+
? defaultEnv
|
|
642
|
+
: undefined;
|
|
643
|
+
/**
|
|
644
|
+
* Build the dotenv filename for a selector (scope × privacy) using the same naming
|
|
645
|
+
* convention as get-dotenv.
|
|
646
|
+
*
|
|
647
|
+
* @param opts - Filename selector options.
|
|
648
|
+
* @returns The filename token (for example, `.env.dev.local`).
|
|
649
|
+
* @throws Error when `scope` is `'env'` and neither `env` nor `defaultEnv` is provided.
|
|
650
|
+
*
|
|
651
|
+
* @public
|
|
652
|
+
*/
|
|
653
|
+
function buildDotenvTargetFilename(opts) {
|
|
654
|
+
const dotenvToken = opts.dotenvToken ?? '.env';
|
|
655
|
+
const privateToken = opts.privateToken ?? 'local';
|
|
656
|
+
const envName = resolveEnvName(opts.env, opts.defaultEnv);
|
|
657
|
+
const parts = [dotenvToken];
|
|
658
|
+
if (opts.scope === 'env') {
|
|
659
|
+
if (!envName) {
|
|
660
|
+
throw new Error(`Unable to resolve env-scoped dotenv filename: env is required.`);
|
|
661
|
+
}
|
|
662
|
+
parts.push(envName);
|
|
663
|
+
}
|
|
664
|
+
if (opts.privacy === 'private')
|
|
665
|
+
parts.push(privateToken);
|
|
666
|
+
return parts.join('.');
|
|
667
|
+
}
|
|
668
|
+
const orderedPaths = (pathsIn, order) => {
|
|
669
|
+
const list = pathsIn.slice();
|
|
670
|
+
return order === 'forward' ? list : list.reverse();
|
|
671
|
+
};
|
|
672
|
+
/**
|
|
673
|
+
* Resolve a deterministic dotenv target across `paths` based on scope/privacy selectors.
|
|
674
|
+
*
|
|
675
|
+
* Selection rules:
|
|
676
|
+
* - Iterates `paths` in the requested search order.
|
|
677
|
+
* - Returns the first existing target file.
|
|
678
|
+
* - Otherwise, returns the first sibling template file (`<target>.<templateExtension>`) when present.
|
|
679
|
+
* - Throws when neither a target nor a template exists anywhere under `paths`.
|
|
680
|
+
*
|
|
681
|
+
* @param opts - Resolver options (paths + selector + fs port).
|
|
682
|
+
* @returns The resolved target path and optional template path.
|
|
683
|
+
*
|
|
684
|
+
* @public
|
|
685
|
+
*/
|
|
686
|
+
async function resolveDotenvTarget(opts) {
|
|
687
|
+
const filename = buildDotenvTargetFilename(opts);
|
|
688
|
+
const templateExtension = opts.templateExtension ?? 'template';
|
|
689
|
+
const searchOrder = opts.searchOrder ?? 'reverse';
|
|
690
|
+
const pathsOrdered = orderedPaths(opts.paths, searchOrder);
|
|
691
|
+
for (const dir of pathsOrdered) {
|
|
692
|
+
const targetPath = path.resolve(dir, filename);
|
|
693
|
+
if (await opts.fs.pathExists(targetPath)) {
|
|
694
|
+
return { targetPath, filename };
|
|
695
|
+
}
|
|
696
|
+
const templatePath = `${targetPath}.${templateExtension}`;
|
|
697
|
+
if (await opts.fs.pathExists(templatePath)) {
|
|
698
|
+
return { targetPath, templatePath, filename };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
throw new Error(`Unable to locate dotenv target "${filename}" under provided paths, and no template was found.`);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* FS-level dotenv editor adapter (target resolution + template bootstrap).
|
|
706
|
+
*
|
|
707
|
+
* Requirements addressed:
|
|
708
|
+
* - Deterministic target selection across getdotenv `paths` only.
|
|
709
|
+
* - Scope axis (global|env) × privacy axis (public|private).
|
|
710
|
+
* - Template bootstrap: copy `<target>.<templateExtension>` to `<target>` when needed.
|
|
711
|
+
* - Edit in place while preserving formatting via the pure text editor.
|
|
712
|
+
*
|
|
713
|
+
* @packageDocumentation
|
|
714
|
+
*/
|
|
715
|
+
const defaultFs = {
|
|
716
|
+
pathExists: async (p) => fs.pathExists(p),
|
|
717
|
+
readFile: async (p) => fs.readFile(p, 'utf-8'),
|
|
718
|
+
writeFile: async (p, contents) => fs.writeFile(p, contents, 'utf-8'),
|
|
719
|
+
copyFile: async (src, dest) => fs.copyFile(src, dest),
|
|
720
|
+
};
|
|
721
|
+
/**
|
|
722
|
+
* Edit a dotenv file selected by scope/privacy across a list of search paths.
|
|
723
|
+
*
|
|
724
|
+
* @param updates - Update map of keys to values.
|
|
725
|
+
* @param options - Target selection options + edit options.
|
|
726
|
+
* @returns An {@link EditDotenvFileResult}.
|
|
727
|
+
*
|
|
728
|
+
* @public
|
|
729
|
+
*/
|
|
730
|
+
async function editDotenvFile(updates, options) {
|
|
731
|
+
const fsPort = options.fs ?? defaultFs;
|
|
732
|
+
const dotenvToken = options.dotenvToken ?? '.env';
|
|
733
|
+
const privateToken = options.privateToken ?? 'local';
|
|
734
|
+
const templateExtension = options.templateExtension ?? 'template';
|
|
735
|
+
const searchOrder = options.searchOrder ?? 'reverse';
|
|
736
|
+
const resolved = await resolveDotenvTarget({
|
|
737
|
+
fs: fsPort,
|
|
738
|
+
paths: options.paths,
|
|
739
|
+
dotenvToken,
|
|
740
|
+
privateToken,
|
|
741
|
+
...(typeof options.env === 'string' && options.env.length > 0
|
|
742
|
+
? { env: options.env }
|
|
743
|
+
: {}),
|
|
744
|
+
...(typeof options.defaultEnv === 'string' && options.defaultEnv.length > 0
|
|
745
|
+
? { defaultEnv: options.defaultEnv }
|
|
746
|
+
: {}),
|
|
747
|
+
scope: options.scope,
|
|
748
|
+
privacy: options.privacy,
|
|
749
|
+
templateExtension,
|
|
750
|
+
searchOrder,
|
|
751
|
+
});
|
|
752
|
+
let createdFromTemplate = false;
|
|
753
|
+
if (resolved.templatePath) {
|
|
754
|
+
// Template bootstrap: copy template as the destination sibling.
|
|
755
|
+
// Only copy if the target does not already exist.
|
|
756
|
+
if (!(await fsPort.pathExists(resolved.targetPath))) {
|
|
757
|
+
await fsPort.copyFile(resolved.templatePath, resolved.targetPath);
|
|
758
|
+
createdFromTemplate = true;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const before = await fsPort.readFile(resolved.targetPath);
|
|
762
|
+
const after = editDotenvText(before, updates, {
|
|
763
|
+
...(options.mode ? { mode: options.mode } : {}),
|
|
764
|
+
...(options.duplicateKeys ? { duplicateKeys: options.duplicateKeys } : {}),
|
|
765
|
+
...(options.undefinedBehavior
|
|
766
|
+
? { undefinedBehavior: options.undefinedBehavior }
|
|
767
|
+
: {}),
|
|
768
|
+
...(options.nullBehavior ? { nullBehavior: options.nullBehavior } : {}),
|
|
769
|
+
...(options.eol ? { eol: options.eol } : {}),
|
|
770
|
+
...(typeof options.defaultSeparator === 'string'
|
|
771
|
+
? { defaultSeparator: options.defaultSeparator }
|
|
772
|
+
: {}),
|
|
773
|
+
});
|
|
774
|
+
const changed = before !== after;
|
|
775
|
+
if (changed) {
|
|
776
|
+
await fsPort.writeFile(resolved.targetPath, after);
|
|
777
|
+
}
|
|
778
|
+
return { path: resolved.targetPath, createdFromTemplate, changed };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function getDotenv(options = {}) {
|
|
782
|
+
// Apply defaults.
|
|
783
|
+
const { defaultEnv, dotenvToken = '.env', dynamicPath, env, excludeDynamic = false, excludeEnv = false, excludeGlobal = false, excludePrivate = false, excludePublic = false, loadProcess = false, log = false, logger = console, outputPath, paths = [], privateToken = 'local', vars = {}, } = await resolveGetDotenvOptions(options);
|
|
784
|
+
// Read .env files.
|
|
785
|
+
const loaded = paths.length
|
|
786
|
+
? await paths.reduce(async (e, p) => {
|
|
787
|
+
const publicGlobal = excludePublic || excludeGlobal
|
|
788
|
+
? Promise.resolve({})
|
|
789
|
+
: readDotenv(path$1.resolve(p, dotenvToken));
|
|
790
|
+
const publicEnv = excludePublic || excludeEnv || (!env && !defaultEnv)
|
|
791
|
+
? Promise.resolve({})
|
|
792
|
+
: readDotenv(path$1.resolve(p, `${dotenvToken}.${env ?? defaultEnv ?? ''}`));
|
|
793
|
+
const privateGlobal = excludePrivate || excludeGlobal
|
|
794
|
+
? Promise.resolve({})
|
|
795
|
+
: readDotenv(path$1.resolve(p, `${dotenvToken}.${privateToken}`));
|
|
796
|
+
const privateEnv = excludePrivate || excludeEnv || (!env && !defaultEnv)
|
|
797
|
+
? Promise.resolve({})
|
|
798
|
+
: readDotenv(path$1.resolve(p, `${dotenvToken}.${env ?? defaultEnv ?? ''}.${privateToken}`));
|
|
799
|
+
const [eResolved, publicGlobalResolved, publicEnvResolved, privateGlobalResolved, privateEnvResolved,] = await Promise.all([
|
|
800
|
+
e,
|
|
801
|
+
publicGlobal,
|
|
802
|
+
publicEnv,
|
|
803
|
+
privateGlobal,
|
|
804
|
+
privateEnv,
|
|
805
|
+
]);
|
|
806
|
+
return {
|
|
807
|
+
...eResolved,
|
|
808
|
+
...publicGlobalResolved,
|
|
809
|
+
...publicEnvResolved,
|
|
810
|
+
...privateGlobalResolved,
|
|
811
|
+
...privateEnvResolved,
|
|
812
|
+
};
|
|
813
|
+
}, Promise.resolve({}))
|
|
814
|
+
: {};
|
|
815
|
+
const outputKey = nanoid();
|
|
816
|
+
const dotenv = dotenvExpandAll({
|
|
817
|
+
...loaded,
|
|
818
|
+
...vars,
|
|
819
|
+
...(outputPath ? { [outputKey]: outputPath } : {}),
|
|
820
|
+
}, { progressive: true });
|
|
821
|
+
// Process dynamic variables. Programmatic option takes precedence over path.
|
|
822
|
+
if (!excludeDynamic) {
|
|
823
|
+
// A2 precedence: programmatic dynamic < dynamicPath (dynamicPath wins when present)
|
|
824
|
+
if (options.dynamic && Object.keys(options.dynamic).length > 0) {
|
|
825
|
+
try {
|
|
826
|
+
applyDynamicMap(dotenv, options.dynamic, env ?? defaultEnv);
|
|
827
|
+
}
|
|
828
|
+
catch {
|
|
829
|
+
throw new Error(`Unable to evaluate dynamic variables.`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// dynamicPath is evaluated even when programmatic `dynamic` is present.
|
|
833
|
+
if (dynamicPath) {
|
|
834
|
+
const absDynamicPath = path$1.resolve(dynamicPath);
|
|
835
|
+
await loadAndApplyDynamic(dotenv, absDynamicPath, env ?? defaultEnv, 'getdotenv-dynamic');
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// Write output file.
|
|
839
|
+
let resultDotenv = dotenv;
|
|
840
|
+
if (outputPath) {
|
|
841
|
+
const outputPathResolved = dotenv[outputKey];
|
|
842
|
+
if (!outputPathResolved)
|
|
843
|
+
throw new Error('Output path not found.');
|
|
844
|
+
const { [outputKey]: _omitted, ...dotenvForOutput } = dotenv;
|
|
845
|
+
await writeDotenvFile(outputPathResolved, dotenvForOutput);
|
|
846
|
+
resultDotenv = dotenvForOutput;
|
|
847
|
+
}
|
|
848
|
+
// Log result.
|
|
849
|
+
if (log) {
|
|
850
|
+
const redactFlag = options.redact ?? false;
|
|
851
|
+
const redactPatterns = options.redactPatterns ?? undefined;
|
|
852
|
+
const redOpts = {};
|
|
853
|
+
if (redactFlag)
|
|
854
|
+
redOpts.redact = true;
|
|
855
|
+
if (redactFlag && Array.isArray(redactPatterns))
|
|
856
|
+
redOpts.redactPatterns = redactPatterns;
|
|
857
|
+
const bag = redactFlag
|
|
858
|
+
? redactObject(resultDotenv, redOpts)
|
|
859
|
+
: { ...resultDotenv };
|
|
860
|
+
logger.log(bag);
|
|
861
|
+
// Entropy warnings: once-per-key-per-run (presentation only)
|
|
862
|
+
const warnEntropyVal = options.warnEntropy ?? true;
|
|
863
|
+
const entropyThresholdVal = options
|
|
864
|
+
.entropyThreshold;
|
|
865
|
+
const entropyMinLengthVal = options
|
|
866
|
+
.entropyMinLength;
|
|
867
|
+
const entropyWhitelistVal = options.entropyWhitelist;
|
|
868
|
+
const entOpts = {};
|
|
869
|
+
if (typeof warnEntropyVal === 'boolean')
|
|
870
|
+
entOpts.warnEntropy = warnEntropyVal;
|
|
871
|
+
if (typeof entropyThresholdVal === 'number')
|
|
872
|
+
entOpts.entropyThreshold = entropyThresholdVal;
|
|
873
|
+
if (typeof entropyMinLengthVal === 'number')
|
|
874
|
+
entOpts.entropyMinLength = entropyMinLengthVal;
|
|
875
|
+
if (Array.isArray(entropyWhitelistVal))
|
|
876
|
+
entOpts.entropyWhitelist = entropyWhitelistVal;
|
|
877
|
+
for (const [k, v] of Object.entries(resultDotenv)) {
|
|
878
|
+
maybeWarnEntropy(k, v, v !== undefined ? 'dotenv' : 'unset', entOpts, (line) => {
|
|
879
|
+
logger.log(line);
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
// Load process.env.
|
|
884
|
+
if (loadProcess)
|
|
885
|
+
Object.assign(process.env, resultDotenv);
|
|
886
|
+
return resultDotenv;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
export { applyDotenvEdits, dotenvExpandAll, editDotenvFile, editDotenvText, getDotenv, maybeWarnEntropy, parseDotenvDocument, redactObject, renderDotenvDocument };
|