ultraenv 1.0.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/LICENSE +21 -0
- package/README.md +2058 -0
- package/bin/ultraenv.mjs +3 -0
- package/dist/chunk-2USZPWLZ.js +288 -0
- package/dist/chunk-3UV2QNJL.js +270 -0
- package/dist/chunk-3VYXPTYV.js +179 -0
- package/dist/chunk-4XUYMRK5.js +366 -0
- package/dist/chunk-5G2DU52U.js +189 -0
- package/dist/chunk-6KS56D6E.js +172 -0
- package/dist/chunk-AWN6ADV7.js +328 -0
- package/dist/chunk-CHVO6NWI.js +203 -0
- package/dist/chunk-CIFMBJ4H.js +3975 -0
- package/dist/chunk-GC7RXHLA.js +253 -0
- package/dist/chunk-HFXQGJY3.js +445 -0
- package/dist/chunk-IGFVP24Q.js +91 -0
- package/dist/chunk-IKPTKALB.js +78 -0
- package/dist/chunk-JB7RKV3C.js +66 -0
- package/dist/chunk-MNVFG7H4.js +611 -0
- package/dist/chunk-MSXMESFP.js +1910 -0
- package/dist/chunk-N5PAV4NM.js +127 -0
- package/dist/chunk-NBOABPHM.js +158 -0
- package/dist/chunk-OMAOROL4.js +49 -0
- package/dist/chunk-R7PZRSZ7.js +105 -0
- package/dist/chunk-TE7HPLA6.js +73 -0
- package/dist/chunk-TMT5KCO3.js +101 -0
- package/dist/chunk-UEWYFN6A.js +189 -0
- package/dist/chunk-WMHN5RW2.js +128 -0
- package/dist/chunk-XC65ORJ5.js +70 -0
- package/dist/chunk-YMMP4VQL.js +118 -0
- package/dist/chunk-YN2KGTCB.js +33 -0
- package/dist/chunk-YTICOB5M.js +65 -0
- package/dist/chunk-YVWLXFUT.js +107 -0
- package/dist/ci-check-sync-VBMSVWIV.js +48 -0
- package/dist/ci-scan-24MT5XGS.js +41 -0
- package/dist/ci-setup-C2NKEFRD.js +135 -0
- package/dist/ci-validate-7AW24LSQ.js +57 -0
- package/dist/cli/index.cjs +9217 -0
- package/dist/cli/index.d.cts +9 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.js +339 -0
- package/dist/comparator-RDKX3OI7.js +13 -0
- package/dist/completion-MW35C2XO.js +168 -0
- package/dist/config-O5YRQP5Z.js +13 -0
- package/dist/debug-PTPXAF3K.js +131 -0
- package/dist/declaration-LEME4AFZ.js +10 -0
- package/dist/doctor-FZAUPKHS.js +129 -0
- package/dist/envs-compare-5K3HESX5.js +49 -0
- package/dist/envs-create-2XXHXMGA.js +58 -0
- package/dist/envs-list-NQM5252B.js +59 -0
- package/dist/envs-switch-6L2AQYID.js +50 -0
- package/dist/envs-validate-FL73Q76T.js +89 -0
- package/dist/fs-VH7ATUS3.js +31 -0
- package/dist/generator-LFZBMZZS.js +14 -0
- package/dist/git-BZS4DPAI.js +30 -0
- package/dist/help-3XJBXEHE.js +121 -0
- package/dist/index.cjs +12907 -0
- package/dist/index.d.cts +2562 -0
- package/dist/index.d.ts +2562 -0
- package/dist/index.js +3212 -0
- package/dist/init-Y7JQ2KYJ.js +146 -0
- package/dist/install-hook-SKXIV6NV.js +111 -0
- package/dist/json-schema-I26YNQBH.js +10 -0
- package/dist/key-manager-O3G55WPU.js +25 -0
- package/dist/middleware/express.cjs +103 -0
- package/dist/middleware/express.d.cts +115 -0
- package/dist/middleware/express.d.ts +115 -0
- package/dist/middleware/express.js +8 -0
- package/dist/middleware/fastify.cjs +91 -0
- package/dist/middleware/fastify.d.cts +111 -0
- package/dist/middleware/fastify.d.ts +111 -0
- package/dist/middleware/fastify.js +8 -0
- package/dist/module-IDIZPP4M.js +10 -0
- package/dist/protect-NCWPM6VC.js +161 -0
- package/dist/scan-TRLY36TT.js +58 -0
- package/dist/schema/index.cjs +4074 -0
- package/dist/schema/index.d.cts +1244 -0
- package/dist/schema/index.d.ts +1244 -0
- package/dist/schema/index.js +152 -0
- package/dist/sync-TMHMTLH2.js +186 -0
- package/dist/typegen-SQOSXBWM.js +80 -0
- package/dist/validate-IOAM5HWS.js +100 -0
- package/dist/vault-decrypt-U6HJZNBV.js +111 -0
- package/dist/vault-diff-B3ZOQTWI.js +132 -0
- package/dist/vault-encrypt-GUSLCSKS.js +112 -0
- package/dist/vault-init-GUBOTOUL.js +106 -0
- package/dist/vault-rekey-DAHT7JCN.js +132 -0
- package/dist/vault-status-GDLRU2OK.js +90 -0
- package/dist/vault-verify-CD76FJSF.js +102 -0
- package/package.json +106 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseEnvFile
|
|
3
|
+
} from "./chunk-HFXQGJY3.js";
|
|
4
|
+
import {
|
|
5
|
+
readFileSync
|
|
6
|
+
} from "./chunk-3VYXPTYV.js";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_ENV_DIR,
|
|
9
|
+
ENCODING,
|
|
10
|
+
ENVIRONMENT_VARIABLES,
|
|
11
|
+
MAX_INTERPOLATION_DEPTH,
|
|
12
|
+
MAX_VALUE_LENGTH
|
|
13
|
+
} from "./chunk-XC65ORJ5.js";
|
|
14
|
+
import {
|
|
15
|
+
ConfigError,
|
|
16
|
+
FileSystemError,
|
|
17
|
+
InterpolationError,
|
|
18
|
+
isUltraenvError
|
|
19
|
+
} from "./chunk-5G2DU52U.js";
|
|
20
|
+
|
|
21
|
+
// src/core/interpolation.ts
|
|
22
|
+
function parseExpression(expr) {
|
|
23
|
+
if (expr.endsWith("^^")) {
|
|
24
|
+
return {
|
|
25
|
+
varName: expr.slice(0, -2),
|
|
26
|
+
op: "uppercase",
|
|
27
|
+
operand: "",
|
|
28
|
+
useColon: false
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (expr.endsWith(",,")) {
|
|
32
|
+
return {
|
|
33
|
+
varName: expr.slice(0, -2),
|
|
34
|
+
op: "lowercase",
|
|
35
|
+
operand: "",
|
|
36
|
+
useColon: false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const colonIndex = expr.indexOf(":");
|
|
40
|
+
if (colonIndex > 0) {
|
|
41
|
+
const varName = expr.slice(0, colonIndex);
|
|
42
|
+
const rest = expr.slice(colonIndex + 1);
|
|
43
|
+
if (/^-?\d+(:-?\d+)?$/.test(rest)) {
|
|
44
|
+
return {
|
|
45
|
+
varName,
|
|
46
|
+
op: "substring",
|
|
47
|
+
operand: rest,
|
|
48
|
+
useColon: true
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
varName,
|
|
53
|
+
op: "default",
|
|
54
|
+
operand: rest,
|
|
55
|
+
useColon: true
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const dashIndex = expr.indexOf("-");
|
|
59
|
+
if (dashIndex > 0) {
|
|
60
|
+
return {
|
|
61
|
+
varName: expr.slice(0, dashIndex),
|
|
62
|
+
op: "default",
|
|
63
|
+
operand: expr.slice(dashIndex + 1),
|
|
64
|
+
useColon: false
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const plusIndex = expr.indexOf("+");
|
|
68
|
+
if (plusIndex > 0) {
|
|
69
|
+
const varName = expr.slice(0, plusIndex);
|
|
70
|
+
const rest = expr.slice(plusIndex + 1);
|
|
71
|
+
const useColon = rest.startsWith(":");
|
|
72
|
+
return {
|
|
73
|
+
varName,
|
|
74
|
+
op: "alternative",
|
|
75
|
+
/* v8 ignore start */
|
|
76
|
+
operand: useColon ? rest.slice(1) : rest,
|
|
77
|
+
/* v8 ignore stop */
|
|
78
|
+
useColon
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const questionIndex = expr.indexOf("?");
|
|
82
|
+
if (questionIndex > 0) {
|
|
83
|
+
const varName = expr.slice(0, questionIndex);
|
|
84
|
+
const rest = expr.slice(questionIndex + 1);
|
|
85
|
+
const useColon = rest.startsWith(":");
|
|
86
|
+
return {
|
|
87
|
+
varName,
|
|
88
|
+
op: "error",
|
|
89
|
+
/* v8 ignore start */
|
|
90
|
+
operand: useColon ? rest.slice(1) : rest,
|
|
91
|
+
/* v8 ignore stop */
|
|
92
|
+
useColon
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
varName: expr,
|
|
97
|
+
op: "simple",
|
|
98
|
+
operand: "",
|
|
99
|
+
useColon: false
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function lookupVar(varName, env, systemEnv) {
|
|
103
|
+
if (varName in env) {
|
|
104
|
+
return [env[varName], true];
|
|
105
|
+
}
|
|
106
|
+
if (systemEnv !== void 0 && varName in systemEnv) {
|
|
107
|
+
const sysVal = systemEnv[varName];
|
|
108
|
+
if (sysVal !== void 0) {
|
|
109
|
+
return [sysVal, true];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return ["", false];
|
|
113
|
+
}
|
|
114
|
+
function evaluateExpression(parsed, env, systemEnv, expandValue, currentDepth) {
|
|
115
|
+
const [value, isSet] = lookupVar(parsed.varName, env, systemEnv);
|
|
116
|
+
const isNonEmpty = isSet && value !== "";
|
|
117
|
+
switch (parsed.op) {
|
|
118
|
+
case "simple":
|
|
119
|
+
if (!isSet) {
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
case "default": {
|
|
124
|
+
const shouldDefault = parsed.useColon ? !isNonEmpty : !isSet;
|
|
125
|
+
if (shouldDefault) {
|
|
126
|
+
return expandValue(parsed.operand, currentDepth);
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
case "alternative": {
|
|
131
|
+
const shouldAlt = parsed.useColon ? isNonEmpty : isSet;
|
|
132
|
+
if (shouldAlt) {
|
|
133
|
+
return expandValue(parsed.operand, currentDepth);
|
|
134
|
+
}
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
case "error": {
|
|
138
|
+
const shouldError = parsed.useColon ? !isNonEmpty : !isSet;
|
|
139
|
+
if (shouldError) {
|
|
140
|
+
const expandedMsg = expandValue(parsed.operand, currentDepth);
|
|
141
|
+
throw new InterpolationError(
|
|
142
|
+
/* v8 ignore start */
|
|
143
|
+
`Variable "${parsed.varName}" is ${parsed.useColon ? "unset or empty" : "unset"}: ${expandedMsg}`,
|
|
144
|
+
/* v8 ignore stop */
|
|
145
|
+
{
|
|
146
|
+
variable: parsed.varName,
|
|
147
|
+
/* v8 ignore start */
|
|
148
|
+
hint: parsed.operand || `Set the "${parsed.varName}" variable in your .env file.`
|
|
149
|
+
/* v8 ignore stop */
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
case "uppercase":
|
|
156
|
+
return value.toUpperCase();
|
|
157
|
+
case "lowercase":
|
|
158
|
+
return value.toLowerCase();
|
|
159
|
+
case "substring": {
|
|
160
|
+
const parts = parsed.operand.split(":");
|
|
161
|
+
const offset = parts[0] !== void 0 ? parseInt(parts[0], 10) : 0;
|
|
162
|
+
const lengthArg = parts[1] !== void 0 ? parseInt(parts[1], 10) : void 0;
|
|
163
|
+
if (!isSet) return "";
|
|
164
|
+
if (Number.isNaN(offset)) return value;
|
|
165
|
+
const safeOffset = offset < 0 ? Math.max(0, value.length + offset) : offset;
|
|
166
|
+
if (lengthArg !== void 0 && !Number.isNaN(lengthArg)) {
|
|
167
|
+
return value.slice(safeOffset, safeOffset + lengthArg);
|
|
168
|
+
}
|
|
169
|
+
return value.slice(safeOffset);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function resolveStringLength(varName, env, systemEnv) {
|
|
174
|
+
const [value, isSet] = lookupVar(varName, env, systemEnv);
|
|
175
|
+
if (!isSet) return "0";
|
|
176
|
+
return String(value.length);
|
|
177
|
+
}
|
|
178
|
+
function expandVariables(vars, env, options) {
|
|
179
|
+
const maxDepth = options?.maxDepth ?? MAX_INTERPOLATION_DEPTH;
|
|
180
|
+
const systemEnv = options?.systemEnv ?? {};
|
|
181
|
+
const resolvedMap = {};
|
|
182
|
+
const resolving = /* @__PURE__ */ new Set();
|
|
183
|
+
function expandValue(raw, depth) {
|
|
184
|
+
if (depth > maxDepth) {
|
|
185
|
+
throw new InterpolationError(
|
|
186
|
+
`Maximum interpolation depth (${maxDepth}) exceeded`,
|
|
187
|
+
{
|
|
188
|
+
hint: "Check for deeply nested variable references. Consider simplifying your variable definitions."
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const result2 = [];
|
|
193
|
+
let i = 0;
|
|
194
|
+
const len = raw.length;
|
|
195
|
+
while (i < len) {
|
|
196
|
+
const ch = raw[i];
|
|
197
|
+
if (ch === "\\" && i + 1 < len && raw[i + 1] === "$") {
|
|
198
|
+
result2.push("$");
|
|
199
|
+
i += 2;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (ch !== "$") {
|
|
203
|
+
result2.push(ch);
|
|
204
|
+
i++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
i++;
|
|
208
|
+
if (i < len && raw[i] === "{" && i + 1 < len && raw[i + 1] === "#") {
|
|
209
|
+
const closeIdx = raw.indexOf("}", i + 2);
|
|
210
|
+
if (closeIdx !== -1) {
|
|
211
|
+
const varName = raw.slice(i + 2, closeIdx).trim();
|
|
212
|
+
result2.push(resolveStringLength(varName, env, systemEnv));
|
|
213
|
+
i = closeIdx + 1;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
result2.push("$");
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (i < len && raw[i] === "{") {
|
|
220
|
+
const closeIdx = findClosingBrace(raw, i + 1);
|
|
221
|
+
if (closeIdx !== -1) {
|
|
222
|
+
const inner = raw.slice(i + 1, closeIdx);
|
|
223
|
+
const parsed = parseExpression(inner);
|
|
224
|
+
if (resolving.has(parsed.varName)) {
|
|
225
|
+
const chain = Array.from(resolving).concat(parsed.varName).join(" \u2192 ");
|
|
226
|
+
throw new InterpolationError(
|
|
227
|
+
`Circular variable reference detected: ${chain}`,
|
|
228
|
+
{
|
|
229
|
+
variable: parsed.varName,
|
|
230
|
+
circular: true,
|
|
231
|
+
hint: "Break the cycle by removing one of the circular references."
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
resolving.add(parsed.varName);
|
|
236
|
+
if (parsed.varName in vars && !(parsed.varName in resolvedMap)) {
|
|
237
|
+
const refRaw = vars[parsed.varName];
|
|
238
|
+
resolvedMap[parsed.varName] = expandValue(refRaw, depth + 1);
|
|
239
|
+
}
|
|
240
|
+
const expanded = evaluateExpression(
|
|
241
|
+
parsed,
|
|
242
|
+
{ ...resolvedMap, ...vars },
|
|
243
|
+
systemEnv,
|
|
244
|
+
expandValue,
|
|
245
|
+
depth + 1
|
|
246
|
+
);
|
|
247
|
+
result2.push(expanded);
|
|
248
|
+
resolving.delete(parsed.varName);
|
|
249
|
+
i = closeIdx + 1;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
result2.push("${");
|
|
253
|
+
i++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (i < len && /^[A-Za-z_]/.test(raw[i])) {
|
|
257
|
+
let nameEnd = i + 1;
|
|
258
|
+
while (nameEnd < len && /^[A-Za-z0-9_]/.test(raw[nameEnd])) {
|
|
259
|
+
nameEnd++;
|
|
260
|
+
}
|
|
261
|
+
const varName = raw.slice(i, nameEnd);
|
|
262
|
+
if (resolving.has(varName)) {
|
|
263
|
+
const chain = Array.from(resolving).concat(varName).join(" \u2192 ");
|
|
264
|
+
throw new InterpolationError(
|
|
265
|
+
`Circular variable reference detected: ${chain}`,
|
|
266
|
+
{
|
|
267
|
+
variable: varName,
|
|
268
|
+
circular: true,
|
|
269
|
+
hint: "Break the cycle by removing one of the circular references."
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
resolving.add(varName);
|
|
274
|
+
if (varName in vars && !(varName in resolvedMap)) {
|
|
275
|
+
const refRaw = vars[varName];
|
|
276
|
+
resolvedMap[varName] = expandValue(refRaw, depth + 1);
|
|
277
|
+
}
|
|
278
|
+
const [resolvedValue] = lookupVar(varName, { ...resolvedMap, ...vars }, systemEnv);
|
|
279
|
+
result2.push(resolvedValue);
|
|
280
|
+
resolving.delete(varName);
|
|
281
|
+
i = nameEnd;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
result2.push("$");
|
|
285
|
+
}
|
|
286
|
+
return result2.join("");
|
|
287
|
+
}
|
|
288
|
+
const result = {};
|
|
289
|
+
for (const key of Object.keys(vars)) {
|
|
290
|
+
const raw = vars[key];
|
|
291
|
+
result[key] = expandValue(raw, 0);
|
|
292
|
+
}
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
function findClosingBrace(str, startIndex) {
|
|
296
|
+
let depth = 1;
|
|
297
|
+
let i = startIndex;
|
|
298
|
+
const len = str.length;
|
|
299
|
+
while (i < len) {
|
|
300
|
+
const ch = str[i];
|
|
301
|
+
if (ch === "\\" && i + 1 < len) {
|
|
302
|
+
i += 2;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (ch === "{") {
|
|
306
|
+
depth++;
|
|
307
|
+
} else if (ch === "}") {
|
|
308
|
+
depth--;
|
|
309
|
+
if (depth === 0) {
|
|
310
|
+
return i;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
i++;
|
|
314
|
+
}
|
|
315
|
+
return -1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/core/cascade.ts
|
|
319
|
+
import { join, resolve } from "path";
|
|
320
|
+
import { existsSync } from "fs";
|
|
321
|
+
function detectEnvironment(systemEnv) {
|
|
322
|
+
const env = systemEnv ?? process.env;
|
|
323
|
+
for (const varName of ENVIRONMENT_VARIABLES) {
|
|
324
|
+
const value = env[varName];
|
|
325
|
+
if (value !== void 0 && value !== "") {
|
|
326
|
+
return value.toLowerCase().trim();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return "development";
|
|
330
|
+
}
|
|
331
|
+
function detectFileType(fileName) {
|
|
332
|
+
const baseName = fileName.split("/").pop() ?? fileName;
|
|
333
|
+
const baseNameLower = baseName.toLowerCase();
|
|
334
|
+
const fileTypeMap = /* @__PURE__ */ new Map([
|
|
335
|
+
[".env", ".env" /* Env */],
|
|
336
|
+
[".env.local", ".env.local" /* EnvLocal */],
|
|
337
|
+
[".env.development", ".env.development" /* EnvDevelopment */],
|
|
338
|
+
[".env.development.local", ".env.development.local" /* EnvDevelopmentLocal */],
|
|
339
|
+
[".env.test", ".env.test" /* EnvTest */],
|
|
340
|
+
[".env.test.local", ".env.test.local" /* EnvTestLocal */],
|
|
341
|
+
[".env.production", ".env.production" /* EnvProduction */],
|
|
342
|
+
[".env.production.local", ".env.production.local" /* EnvProductionLocal */],
|
|
343
|
+
[".env.staging", ".env.staging" /* EnvStaging */],
|
|
344
|
+
[".env.staging.local", ".env.staging.local" /* EnvStagingLocal */],
|
|
345
|
+
[".env.ci", ".env.ci" /* EnvCI */]
|
|
346
|
+
]);
|
|
347
|
+
return fileTypeMap.get(baseNameLower) ?? null;
|
|
348
|
+
}
|
|
349
|
+
function getStandardCascadeFiles(envDir, environment) {
|
|
350
|
+
const files = [];
|
|
351
|
+
let priority = 0;
|
|
352
|
+
files.push({
|
|
353
|
+
absolutePath: resolve(join(envDir, ".env")),
|
|
354
|
+
fileType: ".env" /* Env */,
|
|
355
|
+
priority: priority++,
|
|
356
|
+
exists: existsSync(resolve(join(envDir, ".env")))
|
|
357
|
+
});
|
|
358
|
+
if (environment !== "test") {
|
|
359
|
+
const localPath = resolve(join(envDir, ".env.local"));
|
|
360
|
+
files.push({
|
|
361
|
+
absolutePath: localPath,
|
|
362
|
+
fileType: ".env.local" /* EnvLocal */,
|
|
363
|
+
priority: priority++,
|
|
364
|
+
exists: existsSync(localPath)
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
const envSpecificPath = resolve(join(envDir, `.env.${environment}`));
|
|
368
|
+
const envFileType = detectFileType(`.env.${environment}`);
|
|
369
|
+
files.push({
|
|
370
|
+
absolutePath: envSpecificPath,
|
|
371
|
+
fileType: envFileType,
|
|
372
|
+
priority: priority++,
|
|
373
|
+
exists: existsSync(envSpecificPath)
|
|
374
|
+
});
|
|
375
|
+
if (environment !== "test") {
|
|
376
|
+
const envLocalPath = resolve(join(envDir, `.env.${environment}.local`));
|
|
377
|
+
const envLocalFileType = detectFileType(`.env.${environment}.local`);
|
|
378
|
+
files.push({
|
|
379
|
+
absolutePath: envLocalPath,
|
|
380
|
+
fileType: envLocalFileType,
|
|
381
|
+
priority: priority++,
|
|
382
|
+
exists: existsSync(envLocalPath)
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return files;
|
|
386
|
+
}
|
|
387
|
+
function resolveCascade(options = {}) {
|
|
388
|
+
const envDir = resolve(options.envDir ?? process.cwd());
|
|
389
|
+
const environment = options.environment ?? detectEnvironment(options.systemEnv);
|
|
390
|
+
const mergeStrategy = options.mergeStrategy ?? "last-wins";
|
|
391
|
+
const includeSystemEnv = options.includeSystemEnv ?? true;
|
|
392
|
+
const systemEnv = options.systemEnv ?? process.env;
|
|
393
|
+
let entries;
|
|
394
|
+
if (options.paths !== void 0 && options.paths.length > 0) {
|
|
395
|
+
entries = options.paths.map((p, idx) => {
|
|
396
|
+
const absPath = resolve(p);
|
|
397
|
+
return {
|
|
398
|
+
absolutePath: absPath,
|
|
399
|
+
fileType: detectFileType(p),
|
|
400
|
+
priority: idx,
|
|
401
|
+
exists: existsSync(absPath)
|
|
402
|
+
};
|
|
403
|
+
});
|
|
404
|
+
} else if (options.files !== void 0 && options.files.length > 0) {
|
|
405
|
+
entries = options.files.map((fileType, idx) => {
|
|
406
|
+
const absPath = resolve(join(envDir, fileType));
|
|
407
|
+
return {
|
|
408
|
+
absolutePath: absPath,
|
|
409
|
+
fileType,
|
|
410
|
+
priority: idx,
|
|
411
|
+
exists: existsSync(absPath)
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
entries = getStandardCascadeFiles(envDir, environment);
|
|
416
|
+
}
|
|
417
|
+
const sorted = [...entries].sort((a, b) => a.priority - b.priority);
|
|
418
|
+
const existingFiles = sorted.filter((entry) => entry.exists);
|
|
419
|
+
const sources = {};
|
|
420
|
+
if (includeSystemEnv) {
|
|
421
|
+
for (const key of Object.keys(systemEnv)) {
|
|
422
|
+
const val = systemEnv[key];
|
|
423
|
+
if (val !== void 0) {
|
|
424
|
+
sources[key] = "system";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
files: sorted,
|
|
430
|
+
environment,
|
|
431
|
+
mergeStrategy,
|
|
432
|
+
includeSystemEnv,
|
|
433
|
+
envDir,
|
|
434
|
+
existingFiles,
|
|
435
|
+
sources
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function mergeCascade(parsedFiles, cascade) {
|
|
439
|
+
const merged = {};
|
|
440
|
+
if (cascade.mergeStrategy === "first-wins") {
|
|
441
|
+
for (const file of parsedFiles) {
|
|
442
|
+
for (const envVar of file.vars) {
|
|
443
|
+
if (!(envVar.key in merged)) {
|
|
444
|
+
merged[envVar.key] = envVar.value;
|
|
445
|
+
cascade.sources[envVar.key] = file.path;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} else if (cascade.mergeStrategy === "last-wins") {
|
|
450
|
+
for (const file of parsedFiles) {
|
|
451
|
+
for (const envVar of file.vars) {
|
|
452
|
+
merged[envVar.key] = envVar.value;
|
|
453
|
+
cascade.sources[envVar.key] = file.path;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
for (const file of parsedFiles) {
|
|
458
|
+
for (const envVar of file.vars) {
|
|
459
|
+
if (envVar.key in merged) {
|
|
460
|
+
throw new ConfigError(
|
|
461
|
+
/* v8 ignore start */
|
|
462
|
+
`Duplicate key "${envVar.key}" found in "${file.path}" (line ${envVar.lineNumber}). Previously defined in "${cascade.sources[envVar.key] ?? "unknown"}".`,
|
|
463
|
+
/* v8 ignore stop */
|
|
464
|
+
{
|
|
465
|
+
field: "mergeStrategy",
|
|
466
|
+
hint: `Either use 'first-wins' or 'last-wins' merge strategy, or rename one of the conflicting variables.`
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
merged[envVar.key] = envVar.value;
|
|
471
|
+
cascade.sources[envVar.key] = file.path;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return merged;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/core/loader.ts
|
|
479
|
+
function resolveLoadOptions(options) {
|
|
480
|
+
const noop = () => {
|
|
481
|
+
};
|
|
482
|
+
return {
|
|
483
|
+
/* v8 ignore start */
|
|
484
|
+
envDir: options?.envDir ?? DEFAULT_ENV_DIR,
|
|
485
|
+
/* v8 ignore stop */
|
|
486
|
+
encoding: options?.encoding ?? ENCODING,
|
|
487
|
+
expandVars: options?.expandVariables ?? true,
|
|
488
|
+
overrideProcessEnv: options?.overrideProcessEnv ?? false,
|
|
489
|
+
maxInterpolationDepth: options?.maxInterpolationDepth ?? MAX_INTERPOLATION_DEPTH,
|
|
490
|
+
maxValueLength: options?.maxValueLength ?? MAX_VALUE_LENGTH,
|
|
491
|
+
mergeStrategy: options?.mergeStrategy ?? "last-wins",
|
|
492
|
+
processEnv: options?.processEnv ?? process.env,
|
|
493
|
+
onError: noop,
|
|
494
|
+
cascade: {
|
|
495
|
+
/* v8 ignore start */
|
|
496
|
+
envDir: options?.envDir ?? DEFAULT_ENV_DIR,
|
|
497
|
+
/* v8 ignore stop */
|
|
498
|
+
mergeStrategy: options?.mergeStrategy ?? "last-wins"
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function load(options) {
|
|
503
|
+
const resolved = resolveLoadOptions(options);
|
|
504
|
+
const result = loadCoreSync(resolved);
|
|
505
|
+
applyToProcessEnv(result.env, resolved);
|
|
506
|
+
return result.env;
|
|
507
|
+
}
|
|
508
|
+
function loadSync(options) {
|
|
509
|
+
return load(options);
|
|
510
|
+
}
|
|
511
|
+
function loadWithResult(options) {
|
|
512
|
+
const startTime = Date.now();
|
|
513
|
+
const resolved = resolveLoadOptions(options);
|
|
514
|
+
const result = loadCoreSync(resolved);
|
|
515
|
+
const hadOverrides = applyToProcessEnv(result.env, resolved);
|
|
516
|
+
return {
|
|
517
|
+
env: result.env,
|
|
518
|
+
metadata: buildMetadata(result.parsed, result.env, startTime, resolved.envDir, hadOverrides),
|
|
519
|
+
parsed: result.parsed
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function loadWithResultSync(options) {
|
|
523
|
+
return loadWithResult(options);
|
|
524
|
+
}
|
|
525
|
+
function loadCoreSync(resolved) {
|
|
526
|
+
const cascade = resolveCascade(resolved.cascade);
|
|
527
|
+
const parsed = [];
|
|
528
|
+
const existingEntries = cascade.existingFiles;
|
|
529
|
+
for (const entry of existingEntries) {
|
|
530
|
+
try {
|
|
531
|
+
const content = readFileSync(entry.absolutePath, resolved.encoding);
|
|
532
|
+
validateContentLengths(content, entry.absolutePath, resolved.maxValueLength);
|
|
533
|
+
const parsedFile = parseEnvFile(content, entry.absolutePath);
|
|
534
|
+
parsed.push(parsedFile);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
resolved.onError(
|
|
537
|
+
isUltraenvError(error) ? error : new FileSystemError(`Failed to read "${entry.absolutePath}"`, {
|
|
538
|
+
path: entry.absolutePath,
|
|
539
|
+
operation: "read",
|
|
540
|
+
cause: error instanceof Error ? error : void 0
|
|
541
|
+
})
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const merged = mergeCascade(parsed, cascade);
|
|
546
|
+
let env;
|
|
547
|
+
if (resolved.expandVars) {
|
|
548
|
+
env = expandVariables(merged, merged, {
|
|
549
|
+
maxDepth: resolved.maxInterpolationDepth,
|
|
550
|
+
systemEnv: resolved.processEnv
|
|
551
|
+
});
|
|
552
|
+
} else {
|
|
553
|
+
env = { ...merged };
|
|
554
|
+
}
|
|
555
|
+
return { env, parsed };
|
|
556
|
+
}
|
|
557
|
+
function applyToProcessEnv(env, resolved) {
|
|
558
|
+
if (!resolved.overrideProcessEnv) return false;
|
|
559
|
+
let hadOverrides = false;
|
|
560
|
+
for (const [key, value] of Object.entries(env)) {
|
|
561
|
+
if (process.env[key] !== value) {
|
|
562
|
+
hadOverrides = true;
|
|
563
|
+
}
|
|
564
|
+
process.env[key] = value;
|
|
565
|
+
}
|
|
566
|
+
return hadOverrides;
|
|
567
|
+
}
|
|
568
|
+
function validateContentLengths(content, filePath, maxValueLength) {
|
|
569
|
+
const lines = content.split("\n");
|
|
570
|
+
for (let i = 0; i < lines.length; i++) {
|
|
571
|
+
const line = lines[i];
|
|
572
|
+
const eqIndex = line.indexOf("=");
|
|
573
|
+
if (eqIndex === -1) continue;
|
|
574
|
+
const trimmed = line.slice(0, eqIndex).trim();
|
|
575
|
+
if (trimmed.startsWith("#")) continue;
|
|
576
|
+
const value = line.slice(eqIndex + 1).trimStart();
|
|
577
|
+
if (value.length > maxValueLength) {
|
|
578
|
+
throw new FileSystemError(
|
|
579
|
+
`Value for variable at line ${i + 1} exceeds maximum length of ${maxValueLength} bytes (${value.length} bytes)`,
|
|
580
|
+
{
|
|
581
|
+
path: filePath,
|
|
582
|
+
operation: "read",
|
|
583
|
+
hint: "Reduce the value length or increase maxValueLength in your ultraenv configuration."
|
|
584
|
+
}
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function buildMetadata(parsed, env, startTime, envDir, hadOverrides) {
|
|
590
|
+
const filesFound = parsed.length;
|
|
591
|
+
const totalVars = Object.keys(env).length;
|
|
592
|
+
return {
|
|
593
|
+
totalVars,
|
|
594
|
+
filesParsed: filesFound,
|
|
595
|
+
filesFound,
|
|
596
|
+
loadTimeMs: Date.now() - startTime,
|
|
597
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
598
|
+
envDir,
|
|
599
|
+
hadOverrides
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export {
|
|
604
|
+
expandVariables,
|
|
605
|
+
resolveCascade,
|
|
606
|
+
mergeCascade,
|
|
607
|
+
load,
|
|
608
|
+
loadSync,
|
|
609
|
+
loadWithResult,
|
|
610
|
+
loadWithResultSync
|
|
611
|
+
};
|