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,253 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseEnvFile
|
|
3
|
+
} from "./chunk-HFXQGJY3.js";
|
|
4
|
+
import {
|
|
5
|
+
exists,
|
|
6
|
+
listFiles,
|
|
7
|
+
readFile,
|
|
8
|
+
writeFile
|
|
9
|
+
} from "./chunk-3VYXPTYV.js";
|
|
10
|
+
import {
|
|
11
|
+
FileSystemError
|
|
12
|
+
} from "./chunk-5G2DU52U.js";
|
|
13
|
+
|
|
14
|
+
// src/environments/manager.ts
|
|
15
|
+
import { resolve, join } from "path";
|
|
16
|
+
import { statSync } from "fs";
|
|
17
|
+
var KNOWN_ENVIRONMENTS = [
|
|
18
|
+
"development",
|
|
19
|
+
"staging",
|
|
20
|
+
"production",
|
|
21
|
+
"test"
|
|
22
|
+
];
|
|
23
|
+
var ENV_FILE_PATTERNS = [
|
|
24
|
+
".env",
|
|
25
|
+
".env.local",
|
|
26
|
+
".env.development",
|
|
27
|
+
".env.development.local",
|
|
28
|
+
".env.staging",
|
|
29
|
+
".env.staging.local",
|
|
30
|
+
".env.production",
|
|
31
|
+
".env.production.local",
|
|
32
|
+
".env.test",
|
|
33
|
+
".env.test.local",
|
|
34
|
+
".env.ci"
|
|
35
|
+
];
|
|
36
|
+
function extractEnvName(fileName) {
|
|
37
|
+
if (fileName === ".env" || fileName === ".env.local") {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (!fileName.startsWith(".env.")) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
let name = fileName.slice(5);
|
|
44
|
+
if (name.endsWith(".local")) {
|
|
45
|
+
name = name.slice(0, -6);
|
|
46
|
+
if (name === "") return null;
|
|
47
|
+
}
|
|
48
|
+
return name;
|
|
49
|
+
}
|
|
50
|
+
async function listEnvironments(cwd) {
|
|
51
|
+
const baseDir = resolve(cwd ?? process.cwd());
|
|
52
|
+
const environments = [];
|
|
53
|
+
for (const pattern of ENV_FILE_PATTERNS) {
|
|
54
|
+
const absolutePath = join(baseDir, pattern);
|
|
55
|
+
const fileExists = await exists(absolutePath);
|
|
56
|
+
const envName = extractEnvName(pattern);
|
|
57
|
+
let fileSize = 0;
|
|
58
|
+
let lastModified = "";
|
|
59
|
+
let variableCount = 0;
|
|
60
|
+
if (fileExists) {
|
|
61
|
+
try {
|
|
62
|
+
const stat = statSync(absolutePath);
|
|
63
|
+
fileSize = stat.size;
|
|
64
|
+
lastModified = stat.mtime.toISOString();
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const content = await readFile(absolutePath);
|
|
69
|
+
const parsed = parseEnvFile(content, absolutePath);
|
|
70
|
+
variableCount = parsed.vars.length;
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
environments.push({
|
|
75
|
+
name: envName ?? "base",
|
|
76
|
+
fileName: pattern,
|
|
77
|
+
absolutePath,
|
|
78
|
+
exists: fileExists,
|
|
79
|
+
variableCount,
|
|
80
|
+
fileSize,
|
|
81
|
+
lastModified
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
environments.sort((a, b) => {
|
|
85
|
+
const nameOrder = (name) => {
|
|
86
|
+
if (name === "base") return 0;
|
|
87
|
+
const idx = KNOWN_ENVIRONMENTS.indexOf(name);
|
|
88
|
+
return idx >= 0 ? idx + 1 : 100;
|
|
89
|
+
};
|
|
90
|
+
return nameOrder(a.name) - nameOrder(b.name);
|
|
91
|
+
});
|
|
92
|
+
return environments;
|
|
93
|
+
}
|
|
94
|
+
async function validateAllEnvironments(schema, cwd) {
|
|
95
|
+
const baseDir = resolve(cwd ?? process.cwd());
|
|
96
|
+
const results = /* @__PURE__ */ new Map();
|
|
97
|
+
for (const pattern of ENV_FILE_PATTERNS) {
|
|
98
|
+
const absolutePath = join(baseDir, pattern);
|
|
99
|
+
const envName = extractEnvName(pattern);
|
|
100
|
+
if (!await exists(absolutePath)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const content = await readFile(absolutePath);
|
|
105
|
+
const parsed = parseEnvFile(content, absolutePath);
|
|
106
|
+
const vars = {};
|
|
107
|
+
for (const envVar of parsed.vars) {
|
|
108
|
+
vars[envVar.key] = envVar.value;
|
|
109
|
+
}
|
|
110
|
+
const errors = [];
|
|
111
|
+
const warnings = [];
|
|
112
|
+
for (const [key, schemaEntry] of Object.entries(schema)) {
|
|
113
|
+
const r = schemaEntry;
|
|
114
|
+
const rawValue = vars[key];
|
|
115
|
+
const isRequired = schemaEntry.optional !== true && schemaEntry.default === void 0 && r.hasDefault !== true;
|
|
116
|
+
if (isRequired && (rawValue === void 0 || rawValue === "")) {
|
|
117
|
+
errors.push({
|
|
118
|
+
field: key,
|
|
119
|
+
value: rawValue ?? "",
|
|
120
|
+
message: `Required variable "${key}" is missing`,
|
|
121
|
+
hint: `Set "${key}" in ${pattern}`
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (rawValue !== void 0 && r.type === "number") {
|
|
125
|
+
if (Number.isNaN(Number(rawValue))) {
|
|
126
|
+
errors.push({
|
|
127
|
+
field: key,
|
|
128
|
+
value: rawValue,
|
|
129
|
+
message: `"${key}" should be a number but got "${rawValue}"`,
|
|
130
|
+
hint: `Provide a valid number for "${key}"`
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (rawValue !== void 0 && r.type === "boolean") {
|
|
135
|
+
const lower = rawValue.toLowerCase();
|
|
136
|
+
if (!["true", "false", "1", "0", "yes", "no", "on", "off", ""].includes(lower)) {
|
|
137
|
+
errors.push({
|
|
138
|
+
field: key,
|
|
139
|
+
value: rawValue,
|
|
140
|
+
message: `"${key}" should be a boolean but got "${rawValue}"`,
|
|
141
|
+
hint: `Use true/false, 1/0, or yes/no for "${key}"`
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (r.isDeprecated === true && rawValue !== void 0 && rawValue !== "") {
|
|
146
|
+
const msg = r.deprecationMessage;
|
|
147
|
+
warnings.push({
|
|
148
|
+
field: key,
|
|
149
|
+
value: rawValue,
|
|
150
|
+
message: msg ?? `Variable "${key}" is deprecated`,
|
|
151
|
+
code: "DEPRECATED"
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
results.set(envName ?? pattern, {
|
|
156
|
+
valid: errors.length === 0,
|
|
157
|
+
errors,
|
|
158
|
+
warnings
|
|
159
|
+
});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
results.set(envName ?? pattern, {
|
|
163
|
+
valid: false,
|
|
164
|
+
errors: [{
|
|
165
|
+
field: "",
|
|
166
|
+
value: "",
|
|
167
|
+
message: `Failed to read/parse file: ${message}`,
|
|
168
|
+
hint: "Check that the file is a valid .env file."
|
|
169
|
+
}],
|
|
170
|
+
warnings: []
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return results;
|
|
175
|
+
}
|
|
176
|
+
async function switchEnvironment(envName, cwd) {
|
|
177
|
+
const baseDir = resolve(cwd ?? process.cwd());
|
|
178
|
+
const envFile = join(baseDir, `.env.${envName}`);
|
|
179
|
+
if (!await exists(envFile)) {
|
|
180
|
+
throw new FileSystemError(
|
|
181
|
+
`Environment file ".env.${envName}" not found`,
|
|
182
|
+
{
|
|
183
|
+
path: envFile,
|
|
184
|
+
operation: "read",
|
|
185
|
+
hint: `Create a ".env.${envName}" file first, or use "ultraenv env create ${envName}".`
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const content = await readFile(envFile);
|
|
190
|
+
const header = [
|
|
191
|
+
"# ============================================================",
|
|
192
|
+
`# Switched to environment: ${envName}`,
|
|
193
|
+
`# Source: .env.${envName}`,
|
|
194
|
+
`# Generated by ultraenv on ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
195
|
+
"# ============================================================",
|
|
196
|
+
""
|
|
197
|
+
].join("\n");
|
|
198
|
+
const localPath = join(baseDir, ".env.local");
|
|
199
|
+
await writeFile(localPath, header + content);
|
|
200
|
+
const basePath = join(baseDir, ".env");
|
|
201
|
+
if (!await exists(basePath)) {
|
|
202
|
+
const baseHeader = [
|
|
203
|
+
"# ============================================================",
|
|
204
|
+
"# Base environment variables",
|
|
205
|
+
"# Generated by ultraenv",
|
|
206
|
+
"# ============================================================",
|
|
207
|
+
""
|
|
208
|
+
].join("\n");
|
|
209
|
+
await writeFile(basePath, baseHeader);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function getActiveEnvironment(cwd) {
|
|
213
|
+
const baseDir = resolve(cwd ?? process.cwd());
|
|
214
|
+
const localPath = join(baseDir, ".env.local");
|
|
215
|
+
if (!await exists(localPath)) {
|
|
216
|
+
return "base";
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const content = await readFile(localPath);
|
|
220
|
+
const match = content.match(/^#\s*Switched to environment:\s*(\S+)/m);
|
|
221
|
+
if (match !== null && match[1] !== void 0) {
|
|
222
|
+
return match[1];
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
return "base";
|
|
227
|
+
}
|
|
228
|
+
async function discoverEnvironments(cwd) {
|
|
229
|
+
const baseDir = resolve(cwd ?? process.cwd());
|
|
230
|
+
const discovered = [];
|
|
231
|
+
try {
|
|
232
|
+
const files = await listFiles(baseDir, false);
|
|
233
|
+
const knownNames = /* @__PURE__ */ new Set([...KNOWN_ENVIRONMENTS]);
|
|
234
|
+
for (const file of files) {
|
|
235
|
+
if (!file.startsWith(".env.")) continue;
|
|
236
|
+
if (file.endsWith(".local")) continue;
|
|
237
|
+
const name = extractEnvName(file);
|
|
238
|
+
if (name !== null && !knownNames.has(name)) {
|
|
239
|
+
discovered.push(name);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
return discovered.sort();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export {
|
|
248
|
+
listEnvironments,
|
|
249
|
+
validateAllEnvironments,
|
|
250
|
+
switchEnvironment,
|
|
251
|
+
getActiveEnvironment,
|
|
252
|
+
discoverEnvironments
|
|
253
|
+
};
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ParseError
|
|
3
|
+
} from "./chunk-5G2DU52U.js";
|
|
4
|
+
|
|
5
|
+
// src/core/parser.ts
|
|
6
|
+
var NAME_START_RE = /^[A-Za-z_]/;
|
|
7
|
+
var NAME_CHAR_RE = /^[A-Za-z0-9_]/;
|
|
8
|
+
var COMMENT_LINE_RE = /^[ \t]*#/;
|
|
9
|
+
var EXPORT_PREFIX_RE = /^[ \t]*export[ \t]+/i;
|
|
10
|
+
function resolveEscapeSequence(chars, startIndex, filePath, lineNumber) {
|
|
11
|
+
const next = startIndex + 1 < chars.length ? chars[startIndex + 1] : "";
|
|
12
|
+
if (next === "") {
|
|
13
|
+
return { resolved: "\\", charsConsumed: 1 };
|
|
14
|
+
}
|
|
15
|
+
switch (next) {
|
|
16
|
+
case "n":
|
|
17
|
+
return { resolved: "\n", charsConsumed: 2 };
|
|
18
|
+
case "t":
|
|
19
|
+
return { resolved: " ", charsConsumed: 2 };
|
|
20
|
+
case "r":
|
|
21
|
+
return { resolved: "\r", charsConsumed: 2 };
|
|
22
|
+
case "\\":
|
|
23
|
+
return { resolved: "\\", charsConsumed: 2 };
|
|
24
|
+
case '"':
|
|
25
|
+
return { resolved: '"', charsConsumed: 2 };
|
|
26
|
+
/* v8 ignore start */
|
|
27
|
+
case "$":
|
|
28
|
+
return { resolved: "$", charsConsumed: 2 };
|
|
29
|
+
case "/":
|
|
30
|
+
return { resolved: "/", charsConsumed: 2 };
|
|
31
|
+
/* v8 ignore stop */
|
|
32
|
+
case "x":
|
|
33
|
+
case "X": {
|
|
34
|
+
const hex = chars.slice(startIndex + 2, startIndex + 4).join("");
|
|
35
|
+
if (hex.length === 2 && /^[0-9A-Fa-f]{2}$/.test(hex)) {
|
|
36
|
+
const codePoint = parseInt(hex, 16);
|
|
37
|
+
if (codePoint === 0) {
|
|
38
|
+
throw new ParseError("Null character (\\x00) is not allowed in .env values", {
|
|
39
|
+
line: lineNumber,
|
|
40
|
+
filePath,
|
|
41
|
+
column: startIndex
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return { resolved: String.fromCodePoint(codePoint), charsConsumed: 4 };
|
|
45
|
+
}
|
|
46
|
+
throw new ParseError(
|
|
47
|
+
`Invalid hex escape sequence: ${"\\"}x${hex}`,
|
|
48
|
+
{
|
|
49
|
+
line: lineNumber,
|
|
50
|
+
filePath,
|
|
51
|
+
hint: "Hex escapes must be exactly 2 hex digits, e.g. \\x0A for newline."
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
case "u":
|
|
56
|
+
case "U": {
|
|
57
|
+
const hexDigits = chars.slice(startIndex + 2, startIndex + 6).join("");
|
|
58
|
+
if (hexDigits.length === 4 && /^[0-9A-Fa-f]{4}$/.test(hexDigits)) {
|
|
59
|
+
const codePoint = parseInt(hexDigits, 16);
|
|
60
|
+
if (codePoint === 0) {
|
|
61
|
+
throw new ParseError("Null character (\\u0000) is not allowed in .env values", {
|
|
62
|
+
line: lineNumber,
|
|
63
|
+
filePath
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return { resolved: String.fromCodePoint(codePoint), charsConsumed: 6 };
|
|
67
|
+
}
|
|
68
|
+
throw new ParseError(
|
|
69
|
+
`Invalid unicode escape sequence: ${"\\"}u${hexDigits}`,
|
|
70
|
+
{
|
|
71
|
+
line: lineNumber,
|
|
72
|
+
filePath,
|
|
73
|
+
hint: 'Unicode escapes must be exactly 4 hex digits, e.g. \\u0041 for "A".'
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
/* v8 ignore start */
|
|
78
|
+
default: {
|
|
79
|
+
return { resolved: `\\${next}`, charsConsumed: 2 };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function parseDoubleQuotedValue(lines, startLineIndex, startCharIndex, filePath, lineNumber) {
|
|
84
|
+
const parts = [];
|
|
85
|
+
let currentLine = lines[startLineIndex];
|
|
86
|
+
let chars = Array.from(currentLine.slice(startCharIndex));
|
|
87
|
+
let charIndex = 0;
|
|
88
|
+
let lineIndex = startLineIndex;
|
|
89
|
+
while (lineIndex < lines.length) {
|
|
90
|
+
while (charIndex < chars.length) {
|
|
91
|
+
const ch = chars[charIndex];
|
|
92
|
+
if (ch === '"') {
|
|
93
|
+
return {
|
|
94
|
+
value: parts.join(""),
|
|
95
|
+
closed: true,
|
|
96
|
+
extraLines: lineIndex - startLineIndex
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (ch === "\n") {
|
|
100
|
+
parts.push("\n");
|
|
101
|
+
charIndex++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (ch === "\r") {
|
|
105
|
+
parts.push("\r");
|
|
106
|
+
charIndex++;
|
|
107
|
+
if (charIndex < chars.length && chars[charIndex] === "\n") {
|
|
108
|
+
charIndex++;
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (ch === "\\") {
|
|
113
|
+
const resolved = resolveEscapeSequence(
|
|
114
|
+
chars,
|
|
115
|
+
charIndex,
|
|
116
|
+
filePath,
|
|
117
|
+
lineNumber + lineIndex - startLineIndex
|
|
118
|
+
);
|
|
119
|
+
parts.push(resolved.resolved);
|
|
120
|
+
charIndex += resolved.charsConsumed;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
parts.push(ch);
|
|
124
|
+
charIndex++;
|
|
125
|
+
}
|
|
126
|
+
lineIndex++;
|
|
127
|
+
if (lineIndex < lines.length) {
|
|
128
|
+
parts.push("\n");
|
|
129
|
+
currentLine = lines[lineIndex];
|
|
130
|
+
chars = Array.from(currentLine);
|
|
131
|
+
charIndex = 0;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
value: parts.join(""),
|
|
136
|
+
closed: false,
|
|
137
|
+
extraLines: lineIndex - startLineIndex - 1
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function parseSingleQuotedValue(lines, startLineIndex, startCharIndex) {
|
|
141
|
+
const parts = [];
|
|
142
|
+
let currentLine = lines[startLineIndex];
|
|
143
|
+
let chars = Array.from(currentLine.slice(startCharIndex));
|
|
144
|
+
let charIndex = 0;
|
|
145
|
+
let lineIndex = startLineIndex;
|
|
146
|
+
while (lineIndex < lines.length) {
|
|
147
|
+
while (charIndex < chars.length) {
|
|
148
|
+
const ch = chars[charIndex];
|
|
149
|
+
if (ch === "'") {
|
|
150
|
+
return {
|
|
151
|
+
value: parts.join(""),
|
|
152
|
+
closed: true,
|
|
153
|
+
extraLines: lineIndex - startLineIndex
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
parts.push(ch);
|
|
157
|
+
charIndex++;
|
|
158
|
+
}
|
|
159
|
+
lineIndex++;
|
|
160
|
+
if (lineIndex < lines.length) {
|
|
161
|
+
parts.push("\n");
|
|
162
|
+
currentLine = lines[lineIndex];
|
|
163
|
+
chars = Array.from(currentLine);
|
|
164
|
+
charIndex = 0;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
value: parts.join(""),
|
|
169
|
+
closed: false,
|
|
170
|
+
extraLines: lineIndex - startLineIndex - 1
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function parseBacktickQuotedValue(lines, startLineIndex, startCharIndex) {
|
|
174
|
+
const parts = [];
|
|
175
|
+
let currentLine = lines[startLineIndex];
|
|
176
|
+
let chars = Array.from(currentLine.slice(startCharIndex));
|
|
177
|
+
let charIndex = 0;
|
|
178
|
+
let lineIndex = startLineIndex;
|
|
179
|
+
while (lineIndex < lines.length) {
|
|
180
|
+
while (charIndex < chars.length) {
|
|
181
|
+
const ch = chars[charIndex];
|
|
182
|
+
if (ch === "`") {
|
|
183
|
+
return {
|
|
184
|
+
value: parts.join(""),
|
|
185
|
+
closed: true,
|
|
186
|
+
extraLines: lineIndex - startLineIndex
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
parts.push(ch);
|
|
190
|
+
charIndex++;
|
|
191
|
+
}
|
|
192
|
+
lineIndex++;
|
|
193
|
+
if (lineIndex < lines.length) {
|
|
194
|
+
parts.push("\n");
|
|
195
|
+
currentLine = lines[lineIndex];
|
|
196
|
+
chars = Array.from(currentLine);
|
|
197
|
+
charIndex = 0;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
value: parts.join(""),
|
|
202
|
+
closed: false,
|
|
203
|
+
extraLines: lineIndex - startLineIndex - 1
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function parseKey(line, lineIndex, filePath) {
|
|
207
|
+
let i = 0;
|
|
208
|
+
const len = line.length;
|
|
209
|
+
while (i < len && (line[i] === " " || line[i] === " ")) {
|
|
210
|
+
i++;
|
|
211
|
+
}
|
|
212
|
+
const keyStart = i;
|
|
213
|
+
if (i >= len || !NAME_START_RE.test(line[i])) {
|
|
214
|
+
throw new ParseError("Expected a variable name starting with a letter or underscore", {
|
|
215
|
+
line: lineIndex + 1,
|
|
216
|
+
column: i + 1,
|
|
217
|
+
raw: line,
|
|
218
|
+
filePath
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
i++;
|
|
222
|
+
while (i < len && NAME_CHAR_RE.test(line[i])) {
|
|
223
|
+
i++;
|
|
224
|
+
}
|
|
225
|
+
const key = line.slice(keyStart, i);
|
|
226
|
+
return { key, endIndex: i };
|
|
227
|
+
}
|
|
228
|
+
function extractInlineComment(value) {
|
|
229
|
+
let commentStart = -1;
|
|
230
|
+
for (let i = value.length - 1; i >= 0; i--) {
|
|
231
|
+
if (value[i] === " " || value[i] === " ") {
|
|
232
|
+
const rest = value.slice(i + 1);
|
|
233
|
+
if (rest.length > 0 && rest[0] === "#") {
|
|
234
|
+
commentStart = i;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
if (value[i] !== " " && value[i] !== " ") {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (commentStart >= 0) {
|
|
243
|
+
return {
|
|
244
|
+
cleanValue: value.slice(0, commentStart).trimEnd(),
|
|
245
|
+
comment: value.slice(commentStart + 1).trim()
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return { cleanValue: value, comment: "" };
|
|
249
|
+
}
|
|
250
|
+
function parseEnvFile(content, filePath) {
|
|
251
|
+
const resolvedPath = filePath ?? ".env";
|
|
252
|
+
const lines = splitLines(content);
|
|
253
|
+
const vars = [];
|
|
254
|
+
let lineIndex = 0;
|
|
255
|
+
while (lineIndex < lines.length) {
|
|
256
|
+
const rawLine = lines[lineIndex];
|
|
257
|
+
const oneBasedLine = lineIndex + 1;
|
|
258
|
+
if (isEmptyLine(rawLine)) {
|
|
259
|
+
lineIndex++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (COMMENT_LINE_RE.test(rawLine)) {
|
|
263
|
+
lineIndex++;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
let workingLine = rawLine;
|
|
267
|
+
const exportMatch = EXPORT_PREFIX_RE.exec(workingLine);
|
|
268
|
+
if (exportMatch !== null) {
|
|
269
|
+
workingLine = workingLine.slice(exportMatch[0].length);
|
|
270
|
+
}
|
|
271
|
+
if (isEmptyLine(workingLine) || COMMENT_LINE_RE.test(workingLine)) {
|
|
272
|
+
lineIndex++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
let key;
|
|
276
|
+
let afterKeyIndex;
|
|
277
|
+
try {
|
|
278
|
+
const parsed = parseKey(workingLine, lineIndex, resolvedPath);
|
|
279
|
+
key = parsed.key;
|
|
280
|
+
afterKeyIndex = parsed.endIndex;
|
|
281
|
+
} catch {
|
|
282
|
+
lineIndex++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
while (afterKeyIndex < workingLine.length && (workingLine[afterKeyIndex] === " " || workingLine[afterKeyIndex] === " ")) {
|
|
286
|
+
afterKeyIndex++;
|
|
287
|
+
}
|
|
288
|
+
if (afterKeyIndex >= workingLine.length || workingLine[afterKeyIndex] !== "=") {
|
|
289
|
+
const restAfterKey = workingLine.slice(afterKeyIndex).trim();
|
|
290
|
+
if (restAfterKey === "") {
|
|
291
|
+
const varEntry2 = {
|
|
292
|
+
key,
|
|
293
|
+
value: "",
|
|
294
|
+
raw: "",
|
|
295
|
+
source: resolvedPath,
|
|
296
|
+
lineNumber: oneBasedLine,
|
|
297
|
+
comment: ""
|
|
298
|
+
};
|
|
299
|
+
vars.push(varEntry2);
|
|
300
|
+
lineIndex++;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
lineIndex++;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
afterKeyIndex++;
|
|
307
|
+
while (afterKeyIndex < workingLine.length && (workingLine[afterKeyIndex] === " " || workingLine[afterKeyIndex] === " ")) {
|
|
308
|
+
afterKeyIndex++;
|
|
309
|
+
}
|
|
310
|
+
const valueStartIndex = afterKeyIndex;
|
|
311
|
+
if (valueStartIndex >= workingLine.length) {
|
|
312
|
+
const varEntry2 = {
|
|
313
|
+
key,
|
|
314
|
+
value: "",
|
|
315
|
+
raw: "",
|
|
316
|
+
source: resolvedPath,
|
|
317
|
+
lineNumber: oneBasedLine,
|
|
318
|
+
comment: ""
|
|
319
|
+
};
|
|
320
|
+
vars.push(varEntry2);
|
|
321
|
+
lineIndex++;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const firstChar = workingLine[valueStartIndex];
|
|
325
|
+
let value;
|
|
326
|
+
let raw;
|
|
327
|
+
let comment = "";
|
|
328
|
+
let extraLinesConsumed = 0;
|
|
329
|
+
if (firstChar === '"') {
|
|
330
|
+
const result = parseDoubleQuotedValue(
|
|
331
|
+
lines,
|
|
332
|
+
lineIndex,
|
|
333
|
+
valueStartIndex + 1,
|
|
334
|
+
resolvedPath,
|
|
335
|
+
oneBasedLine
|
|
336
|
+
);
|
|
337
|
+
if (!result.closed) {
|
|
338
|
+
throw new ParseError("Unterminated double-quoted string", {
|
|
339
|
+
line: oneBasedLine,
|
|
340
|
+
raw: workingLine,
|
|
341
|
+
filePath: resolvedPath,
|
|
342
|
+
hint: 'Make sure every double-quoted value has a closing ".'
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
value = result.value;
|
|
346
|
+
raw = `"${result.value}"`;
|
|
347
|
+
extraLinesConsumed = result.extraLines;
|
|
348
|
+
const closingQuoteLine = lines[lineIndex + extraLinesConsumed];
|
|
349
|
+
const afterClosingQuote = closingQuoteLine.slice(
|
|
350
|
+
valueStartIndex + 1 + result.value.length + 1
|
|
351
|
+
);
|
|
352
|
+
const trimmedAfter = afterClosingQuote.trim();
|
|
353
|
+
if (trimmedAfter.startsWith("#")) {
|
|
354
|
+
comment = trimmedAfter.slice(1).trim();
|
|
355
|
+
}
|
|
356
|
+
} else if (firstChar === "'") {
|
|
357
|
+
const result = parseSingleQuotedValue(
|
|
358
|
+
lines,
|
|
359
|
+
lineIndex,
|
|
360
|
+
valueStartIndex + 1
|
|
361
|
+
);
|
|
362
|
+
if (!result.closed) {
|
|
363
|
+
throw new ParseError("Unterminated single-quoted string", {
|
|
364
|
+
line: oneBasedLine,
|
|
365
|
+
raw: workingLine,
|
|
366
|
+
filePath: resolvedPath,
|
|
367
|
+
hint: "Make sure every single-quoted value has a closing '."
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
value = result.value;
|
|
371
|
+
raw = `'${result.value}'`;
|
|
372
|
+
extraLinesConsumed = result.extraLines;
|
|
373
|
+
const closingQuoteLine = lines[lineIndex + extraLinesConsumed];
|
|
374
|
+
const afterClosingQuote = closingQuoteLine.slice(
|
|
375
|
+
valueStartIndex + 1 + result.value.length + 1
|
|
376
|
+
);
|
|
377
|
+
const trimmedAfter = afterClosingQuote.trim();
|
|
378
|
+
if (trimmedAfter.startsWith("#")) {
|
|
379
|
+
comment = trimmedAfter.slice(1).trim();
|
|
380
|
+
}
|
|
381
|
+
} else if (firstChar === "`") {
|
|
382
|
+
const result = parseBacktickQuotedValue(
|
|
383
|
+
lines,
|
|
384
|
+
lineIndex,
|
|
385
|
+
valueStartIndex + 1
|
|
386
|
+
);
|
|
387
|
+
if (!result.closed) {
|
|
388
|
+
throw new ParseError("Unterminated backtick-quoted string", {
|
|
389
|
+
line: oneBasedLine,
|
|
390
|
+
raw: workingLine,
|
|
391
|
+
filePath: resolvedPath,
|
|
392
|
+
hint: "Make sure every backtick-quoted value has a closing `."
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
value = result.value;
|
|
396
|
+
raw = `\`${result.value}\``;
|
|
397
|
+
extraLinesConsumed = result.extraLines;
|
|
398
|
+
const closingQuoteLine = lines[lineIndex + extraLinesConsumed];
|
|
399
|
+
const afterClosingQuote = closingQuoteLine.slice(
|
|
400
|
+
valueStartIndex + 1 + result.value.length + 1
|
|
401
|
+
);
|
|
402
|
+
const trimmedAfter = afterClosingQuote.trim();
|
|
403
|
+
if (trimmedAfter.startsWith("#")) {
|
|
404
|
+
comment = trimmedAfter.slice(1).trim();
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
let rawValue = workingLine.slice(valueStartIndex);
|
|
408
|
+
const extracted = extractInlineComment(rawValue);
|
|
409
|
+
value = extracted.cleanValue;
|
|
410
|
+
comment = extracted.comment;
|
|
411
|
+
raw = rawValue;
|
|
412
|
+
}
|
|
413
|
+
const varEntry = {
|
|
414
|
+
key,
|
|
415
|
+
value,
|
|
416
|
+
raw,
|
|
417
|
+
source: resolvedPath,
|
|
418
|
+
lineNumber: oneBasedLine,
|
|
419
|
+
comment
|
|
420
|
+
};
|
|
421
|
+
vars.push(varEntry);
|
|
422
|
+
lineIndex += 1 + extraLinesConsumed;
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
path: resolvedPath,
|
|
426
|
+
vars,
|
|
427
|
+
exists: true
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function splitLines(content) {
|
|
431
|
+
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
432
|
+
return normalized.split("\n");
|
|
433
|
+
}
|
|
434
|
+
function isEmptyLine(line) {
|
|
435
|
+
for (let i = 0; i < line.length; i++) {
|
|
436
|
+
if (line[i] !== " " && line[i] !== " ") {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export {
|
|
444
|
+
parseEnvFile
|
|
445
|
+
};
|