@xlameiro/env-typegen 0.1.2 → 0.1.4
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/CHANGELOG.md +18 -0
- package/README.md +97 -54
- package/dist/cli.js +1448 -169
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1716 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +537 -1
- package/dist/index.d.ts +537 -1
- package/dist/index.js +1704 -151
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import
|
|
2
|
+
import path6 from 'path';
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
4
|
import { readFile, mkdir, writeFile } from 'fs/promises';
|
|
5
5
|
import { format } from 'prettier';
|
|
6
|
-
import { green } from 'picocolors';
|
|
6
|
+
import { green, red, yellow } from 'picocolors';
|
|
7
|
+
import { parseArgs } from 'util';
|
|
7
8
|
|
|
8
9
|
// src/parser/comment-parser.ts
|
|
9
10
|
var VALID_ENV_VAR_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -19,28 +20,73 @@ var VALID_ENV_VAR_TYPES = /* @__PURE__ */ new Set([
|
|
|
19
20
|
function isEnvVarType(value) {
|
|
20
21
|
return VALID_ENV_VAR_TYPES.has(value);
|
|
21
22
|
}
|
|
23
|
+
function applyTypeAnnotation(state, content) {
|
|
24
|
+
const typeStr = content.slice("@type ".length).trim();
|
|
25
|
+
if (isEnvVarType(typeStr)) state.annotatedType = typeStr;
|
|
26
|
+
}
|
|
27
|
+
function applyEnumAnnotation(state, content) {
|
|
28
|
+
const values = content.slice("@enum ".length).trim().split(",").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
29
|
+
if (values.length > 0) state.enumValues = values;
|
|
30
|
+
}
|
|
31
|
+
function applyMinAnnotation(state, content) {
|
|
32
|
+
const num = Number(content.slice("@min ".length).trim());
|
|
33
|
+
if (Number.isFinite(num)) state.minConstraint = num;
|
|
34
|
+
}
|
|
35
|
+
function applyMaxAnnotation(state, content) {
|
|
36
|
+
const num = Number(content.slice("@max ".length).trim());
|
|
37
|
+
if (Number.isFinite(num)) state.maxConstraint = num;
|
|
38
|
+
}
|
|
39
|
+
function applyRuntimeAnnotation(state, content) {
|
|
40
|
+
const scope = content.slice("@runtime ".length).trim();
|
|
41
|
+
if (scope === "server" || scope === "client" || scope === "edge") {
|
|
42
|
+
state.runtime = scope;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function processAnnotationContent(state, content) {
|
|
46
|
+
const trimmed = content.trim();
|
|
47
|
+
if (trimmed === "@required") {
|
|
48
|
+
state.isRequired = true;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (trimmed === "@secret") {
|
|
52
|
+
state.isSecret = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (trimmed === "@optional") return;
|
|
56
|
+
if (content.startsWith("@description ")) {
|
|
57
|
+
state.description = content.slice("@description ".length).trim();
|
|
58
|
+
} else if (content.startsWith("@type ")) {
|
|
59
|
+
applyTypeAnnotation(state, content);
|
|
60
|
+
} else if (content.startsWith("@enum ")) {
|
|
61
|
+
applyEnumAnnotation(state, content);
|
|
62
|
+
} else if (content.startsWith("@min ")) {
|
|
63
|
+
applyMinAnnotation(state, content);
|
|
64
|
+
} else if (content.startsWith("@max ")) {
|
|
65
|
+
applyMaxAnnotation(state, content);
|
|
66
|
+
} else if (content.startsWith("@runtime ")) {
|
|
67
|
+
applyRuntimeAnnotation(state, content);
|
|
68
|
+
} else if (state.description === void 0 && trimmed.length > 0) {
|
|
69
|
+
state.description = trimmed;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
22
72
|
function parseCommentBlock(lines) {
|
|
23
|
-
|
|
24
|
-
let description;
|
|
25
|
-
let isRequired = false;
|
|
73
|
+
const state = { isRequired: false };
|
|
26
74
|
for (const line of lines) {
|
|
27
|
-
|
|
28
|
-
if (content.startsWith("@description ")) {
|
|
29
|
-
description = content.slice("@description ".length).trim();
|
|
30
|
-
} else if (content.startsWith("@type ")) {
|
|
31
|
-
const typeStr = content.slice("@type ".length).trim();
|
|
32
|
-
if (isEnvVarType(typeStr)) {
|
|
33
|
-
annotatedType = typeStr;
|
|
34
|
-
}
|
|
35
|
-
} else if (content.trim() === "@required") {
|
|
36
|
-
isRequired = true;
|
|
37
|
-
} else if (content.trim() === "@optional") ; else if (description === void 0 && content.trim().length > 0) {
|
|
38
|
-
description = content.trim();
|
|
39
|
-
}
|
|
75
|
+
processAnnotationContent(state, line.replace(/^#\s*/, "").trimEnd());
|
|
40
76
|
}
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
77
|
+
let constraints;
|
|
78
|
+
if (state.minConstraint !== void 0 || state.maxConstraint !== void 0) {
|
|
79
|
+
constraints = {};
|
|
80
|
+
if (state.minConstraint !== void 0) constraints.min = state.minConstraint;
|
|
81
|
+
if (state.maxConstraint !== void 0) constraints.max = state.maxConstraint;
|
|
82
|
+
}
|
|
83
|
+
const result = { isRequired: state.isRequired };
|
|
84
|
+
if (state.annotatedType !== void 0) result.annotatedType = state.annotatedType;
|
|
85
|
+
if (state.description !== void 0) result.description = state.description;
|
|
86
|
+
if (state.enumValues !== void 0) result.enumValues = state.enumValues;
|
|
87
|
+
if (constraints !== void 0) result.constraints = constraints;
|
|
88
|
+
if (state.runtime !== void 0) result.runtime = state.runtime;
|
|
89
|
+
if (state.isSecret !== void 0) result.isSecret = state.isSecret;
|
|
44
90
|
return result;
|
|
45
91
|
}
|
|
46
92
|
|
|
@@ -97,7 +143,9 @@ var inferenceRules = [
|
|
|
97
143
|
{
|
|
98
144
|
id: "P8_numeric_literal",
|
|
99
145
|
priority: 8,
|
|
100
|
-
|
|
146
|
+
// Non-capturing group with \d keeps the dot/digit boundary unambiguous,
|
|
147
|
+
// eliminating super-linear backtracking (ReDoS-safe).
|
|
148
|
+
match: (_key, value) => /^\d+(?:\.\d+)?$/.test(value),
|
|
101
149
|
type: "number"
|
|
102
150
|
},
|
|
103
151
|
{
|
|
@@ -115,7 +163,9 @@ var inferenceRules = [
|
|
|
115
163
|
{
|
|
116
164
|
id: "P11_email_literal",
|
|
117
165
|
priority: 11,
|
|
118
|
-
|
|
166
|
+
// Dots are excluded from each domain-segment character class so that the
|
|
167
|
+
// literal \. separators are unambiguous, preventing super-linear backtracking.
|
|
168
|
+
match: (_key, value) => /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/.test(value),
|
|
119
169
|
type: "email"
|
|
120
170
|
},
|
|
121
171
|
{
|
|
@@ -149,7 +199,50 @@ function inferTypesFromParsedVars(parsed, options) {
|
|
|
149
199
|
return parsed.vars.map((item) => inferType(item.key, item.rawValue, options));
|
|
150
200
|
}
|
|
151
201
|
var ENV_VAR_RE = /^([A-Z_][A-Z0-9_]*)=(.*)$/;
|
|
152
|
-
var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(
|
|
202
|
+
var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(\S+(?:\s+\S+)*)\s+[-=]{3,}\s*$/;
|
|
203
|
+
function buildParsedVar(params, commentBlock, options) {
|
|
204
|
+
const annotations = parseCommentBlock(commentBlock);
|
|
205
|
+
const extraRules = options?.inferenceRules;
|
|
206
|
+
const inferredType = inferType(
|
|
207
|
+
params.key,
|
|
208
|
+
params.rawValue,
|
|
209
|
+
...extraRules === void 0 ? [] : [{ extraRules }]
|
|
210
|
+
);
|
|
211
|
+
const isRequired = params.rawValue.length > 0 || annotations.isRequired;
|
|
212
|
+
const isOptional = params.rawValue.length === 0 && !annotations.isRequired;
|
|
213
|
+
const isClientSide = params.key.startsWith("NEXT_PUBLIC_");
|
|
214
|
+
const parsedVar = {
|
|
215
|
+
key: params.key,
|
|
216
|
+
rawValue: params.rawValue,
|
|
217
|
+
inferredType,
|
|
218
|
+
isRequired,
|
|
219
|
+
isOptional,
|
|
220
|
+
isClientSide,
|
|
221
|
+
lineNumber: params.lineNumber
|
|
222
|
+
};
|
|
223
|
+
if (annotations.annotatedType !== void 0) {
|
|
224
|
+
parsedVar.annotatedType = annotations.annotatedType;
|
|
225
|
+
}
|
|
226
|
+
if (annotations.description !== void 0) {
|
|
227
|
+
parsedVar.description = annotations.description;
|
|
228
|
+
}
|
|
229
|
+
if (params.currentGroup !== void 0) {
|
|
230
|
+
parsedVar.group = params.currentGroup;
|
|
231
|
+
}
|
|
232
|
+
if (annotations.enumValues !== void 0) {
|
|
233
|
+
parsedVar.enumValues = annotations.enumValues;
|
|
234
|
+
}
|
|
235
|
+
if (annotations.constraints !== void 0) {
|
|
236
|
+
parsedVar.constraints = annotations.constraints;
|
|
237
|
+
}
|
|
238
|
+
if (annotations.runtime !== void 0) {
|
|
239
|
+
parsedVar.runtime = annotations.runtime;
|
|
240
|
+
}
|
|
241
|
+
if (annotations.isSecret !== void 0) {
|
|
242
|
+
parsedVar.isSecret = annotations.isSecret;
|
|
243
|
+
}
|
|
244
|
+
return parsedVar;
|
|
245
|
+
}
|
|
153
246
|
function parseEnvFileContent(content, filePath, options) {
|
|
154
247
|
const lines = content.split("\n");
|
|
155
248
|
const vars = [];
|
|
@@ -179,41 +272,18 @@ function parseEnvFileContent(content, filePath, options) {
|
|
|
179
272
|
continue;
|
|
180
273
|
}
|
|
181
274
|
const envMatch = ENV_VAR_RE.exec(trimmed);
|
|
182
|
-
if (envMatch
|
|
183
|
-
const key = envMatch[1] ?? "";
|
|
184
|
-
const rawValue = envMatch[2] ?? "";
|
|
185
|
-
const annotations = parseCommentBlock(commentBlock);
|
|
186
|
-
const inferredType = inferType(
|
|
187
|
-
key,
|
|
188
|
-
rawValue,
|
|
189
|
-
...options?.inferenceRules !== void 0 ? [{ extraRules: options.inferenceRules }] : []
|
|
190
|
-
);
|
|
191
|
-
const isRequired = rawValue.length > 0 || annotations.isRequired;
|
|
192
|
-
const isOptional = rawValue.length === 0 && !annotations.isRequired;
|
|
193
|
-
const isClientSide = key.startsWith("NEXT_PUBLIC_");
|
|
194
|
-
const parsedVar = {
|
|
195
|
-
key,
|
|
196
|
-
rawValue,
|
|
197
|
-
inferredType,
|
|
198
|
-
isRequired,
|
|
199
|
-
isOptional,
|
|
200
|
-
isClientSide,
|
|
201
|
-
lineNumber
|
|
202
|
-
};
|
|
203
|
-
if (annotations.annotatedType !== void 0) {
|
|
204
|
-
parsedVar.annotatedType = annotations.annotatedType;
|
|
205
|
-
}
|
|
206
|
-
if (annotations.description !== void 0) {
|
|
207
|
-
parsedVar.description = annotations.description;
|
|
208
|
-
}
|
|
209
|
-
if (currentGroup !== void 0) {
|
|
210
|
-
parsedVar.group = currentGroup;
|
|
211
|
-
}
|
|
212
|
-
vars.push(parsedVar);
|
|
213
|
-
commentBlock = [];
|
|
214
|
-
} else {
|
|
275
|
+
if (envMatch === null) {
|
|
215
276
|
commentBlock = [];
|
|
277
|
+
continue;
|
|
216
278
|
}
|
|
279
|
+
vars.push(
|
|
280
|
+
buildParsedVar(
|
|
281
|
+
{ key: envMatch[1] ?? "", rawValue: envMatch[2] ?? "", lineNumber, currentGroup },
|
|
282
|
+
commentBlock,
|
|
283
|
+
options
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
commentBlock = [];
|
|
217
287
|
}
|
|
218
288
|
return { filePath, vars, groups };
|
|
219
289
|
}
|
|
@@ -229,15 +299,17 @@ function toTsType(envVarType) {
|
|
|
229
299
|
function generateTypeScriptTypes(parsed) {
|
|
230
300
|
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
231
301
|
const hasClientVars = clientVars.length > 0;
|
|
232
|
-
const fileName =
|
|
302
|
+
const fileName = path6.basename(parsed.filePath);
|
|
233
303
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
234
304
|
const lines = [];
|
|
235
|
-
lines.push(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
305
|
+
lines.push(
|
|
306
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
307
|
+
`// Source: ${fileName}`,
|
|
308
|
+
`// Generated at: ${timestamp}`,
|
|
309
|
+
"",
|
|
310
|
+
"declare namespace NodeJS {",
|
|
311
|
+
" interface ProcessEnv {"
|
|
312
|
+
);
|
|
241
313
|
for (const variable of parsed.vars) {
|
|
242
314
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
243
315
|
const optional = variable.isOptional ? "?" : "";
|
|
@@ -252,10 +324,7 @@ function generateTypeScriptTypes(parsed) {
|
|
|
252
324
|
}
|
|
253
325
|
lines.push(propLine);
|
|
254
326
|
}
|
|
255
|
-
lines.push(" }");
|
|
256
|
-
lines.push("}");
|
|
257
|
-
lines.push("");
|
|
258
|
-
lines.push("export type EnvVars = {");
|
|
327
|
+
lines.push(" }", "}", "", "export type EnvVars = {");
|
|
259
328
|
for (const variable of parsed.vars) {
|
|
260
329
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
261
330
|
const tsType = toTsType(effectiveType);
|
|
@@ -265,28 +334,34 @@ function generateTypeScriptTypes(parsed) {
|
|
|
265
334
|
lines.push("};");
|
|
266
335
|
if (hasClientVars) {
|
|
267
336
|
const clientKeyUnion = clientVars.map((v) => `"${v.key}"`).join(" | ");
|
|
268
|
-
lines.push(
|
|
269
|
-
|
|
270
|
-
|
|
337
|
+
lines.push(
|
|
338
|
+
"",
|
|
339
|
+
`export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`,
|
|
340
|
+
`export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`
|
|
341
|
+
);
|
|
271
342
|
}
|
|
272
343
|
return lines.join("\n") + "\n";
|
|
273
344
|
}
|
|
274
345
|
function generateEnvValidation(parsed) {
|
|
275
346
|
const required = parsed.vars.filter((v) => v.isRequired).map((v) => v.key);
|
|
276
347
|
const lines = [];
|
|
277
|
-
lines.push(
|
|
278
|
-
|
|
279
|
-
|
|
348
|
+
lines.push(
|
|
349
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
350
|
+
"",
|
|
351
|
+
"export function validateEnv(): void {"
|
|
352
|
+
);
|
|
280
353
|
if (required.length === 0) {
|
|
281
354
|
lines.push(" // No required environment variables defined");
|
|
282
355
|
} else {
|
|
283
356
|
const keyList = required.map((k) => `"${k}"`).join(", ");
|
|
284
|
-
lines.push(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
357
|
+
lines.push(
|
|
358
|
+
` const required = [${keyList}];`,
|
|
359
|
+
" for (const key of required) {",
|
|
360
|
+
" if (!process.env[key]) {",
|
|
361
|
+
" throw new Error(`Missing required environment variable: ${key}`);",
|
|
362
|
+
" }",
|
|
363
|
+
" }"
|
|
364
|
+
);
|
|
290
365
|
}
|
|
291
366
|
lines.push("}");
|
|
292
367
|
return lines.join("\n") + "\n";
|
|
@@ -304,37 +379,40 @@ function generateZodSchema(parsed) {
|
|
|
304
379
|
const serverVars = parsed.vars.filter((v) => !v.isClientSide);
|
|
305
380
|
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
306
381
|
const lines = [];
|
|
307
|
-
lines.push(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
382
|
+
lines.push(
|
|
383
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
384
|
+
'import { z } from "zod";',
|
|
385
|
+
"",
|
|
386
|
+
"export const serverEnvSchema = z.object({"
|
|
387
|
+
);
|
|
311
388
|
for (const variable of serverVars) {
|
|
312
389
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
313
390
|
const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
|
|
314
391
|
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
315
392
|
}
|
|
316
|
-
lines.push("});");
|
|
317
|
-
lines.push("");
|
|
318
|
-
lines.push("export const clientEnvSchema = z.object({");
|
|
393
|
+
lines.push("});", "", "export const clientEnvSchema = z.object({");
|
|
319
394
|
for (const variable of clientVars) {
|
|
320
395
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
321
396
|
const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
|
|
322
397
|
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
323
398
|
}
|
|
324
|
-
lines.push(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
399
|
+
lines.push(
|
|
400
|
+
"});",
|
|
401
|
+
"",
|
|
402
|
+
"export const envSchema = serverEnvSchema.merge(clientEnvSchema);",
|
|
403
|
+
"export type Env = z.infer<typeof envSchema>;"
|
|
404
|
+
);
|
|
328
405
|
return lines.join("\n") + "\n";
|
|
329
406
|
}
|
|
330
407
|
function generateDeclaration(parsed) {
|
|
331
|
-
const fileName =
|
|
332
|
-
const lines = [
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
408
|
+
const fileName = path6.basename(parsed.filePath);
|
|
409
|
+
const lines = [
|
|
410
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
411
|
+
`// Source: ${fileName}`,
|
|
412
|
+
"",
|
|
413
|
+
"declare namespace NodeJS {",
|
|
414
|
+
" interface ProcessEnv {"
|
|
415
|
+
];
|
|
338
416
|
for (const variable of parsed.vars) {
|
|
339
417
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
340
418
|
const optional = variable.isOptional ? "?" : "";
|
|
@@ -349,12 +427,14 @@ function generateDeclaration(parsed) {
|
|
|
349
427
|
}
|
|
350
428
|
lines.push(propLine);
|
|
351
429
|
}
|
|
352
|
-
lines.push(" }");
|
|
353
|
-
lines.push("}");
|
|
430
|
+
lines.push(" }", "}");
|
|
354
431
|
return lines.join("\n") + "\n";
|
|
355
432
|
}
|
|
356
433
|
|
|
357
434
|
// src/generators/t3-generator.ts
|
|
435
|
+
function escapeJsStringLiteral(value) {
|
|
436
|
+
return value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`);
|
|
437
|
+
}
|
|
358
438
|
function toT3ZodType(envVarType) {
|
|
359
439
|
if (envVarType === "number") return "z.coerce.number()";
|
|
360
440
|
if (envVarType === "boolean") return "z.coerce.boolean()";
|
|
@@ -362,65 +442,93 @@ function toT3ZodType(envVarType) {
|
|
|
362
442
|
if (envVarType === "email") return "z.string().email()";
|
|
363
443
|
return "z.string()";
|
|
364
444
|
}
|
|
445
|
+
function buildZodExpr(variable) {
|
|
446
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
447
|
+
let zodExpr = toT3ZodType(effectiveType);
|
|
448
|
+
if (variable.description !== void 0) {
|
|
449
|
+
zodExpr += `.describe("${escapeJsStringLiteral(variable.description)}")`;
|
|
450
|
+
}
|
|
451
|
+
if (variable.isOptional) {
|
|
452
|
+
zodExpr += ".optional()";
|
|
453
|
+
}
|
|
454
|
+
return zodExpr;
|
|
455
|
+
}
|
|
365
456
|
function generateT3Env(parsed) {
|
|
366
457
|
const serverVars = parsed.vars.filter((v) => !v.isClientSide);
|
|
367
458
|
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
368
|
-
const lines = [
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
459
|
+
const lines = [
|
|
460
|
+
"// Generated by env-typegen \u2014 do not edit manually",
|
|
461
|
+
'import { createEnv } from "@t3-oss/env-nextjs";',
|
|
462
|
+
'import { z } from "zod";',
|
|
463
|
+
"",
|
|
464
|
+
"export const env = createEnv({"
|
|
465
|
+
];
|
|
374
466
|
if (serverVars.length > 0) {
|
|
375
|
-
lines.push(
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
|
|
381
|
-
}
|
|
382
|
-
if (variable.isOptional) {
|
|
383
|
-
zodExpr += ".optional()";
|
|
384
|
-
}
|
|
385
|
-
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
386
|
-
}
|
|
387
|
-
lines.push(" },");
|
|
467
|
+
lines.push(
|
|
468
|
+
" server: {",
|
|
469
|
+
...serverVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
|
|
470
|
+
" },"
|
|
471
|
+
);
|
|
388
472
|
}
|
|
389
473
|
if (clientVars.length > 0) {
|
|
390
|
-
lines.push(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
|
|
396
|
-
}
|
|
397
|
-
if (variable.isOptional) {
|
|
398
|
-
zodExpr += ".optional()";
|
|
399
|
-
}
|
|
400
|
-
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
401
|
-
}
|
|
402
|
-
lines.push(" },");
|
|
403
|
-
}
|
|
404
|
-
lines.push(" runtimeEnv: {");
|
|
405
|
-
for (const variable of parsed.vars) {
|
|
406
|
-
lines.push(` ${variable.key}: process.env.${variable.key},`);
|
|
474
|
+
lines.push(
|
|
475
|
+
" client: {",
|
|
476
|
+
...clientVars.map((v) => ` ${v.key}: ${buildZodExpr(v)},`),
|
|
477
|
+
" },"
|
|
478
|
+
);
|
|
407
479
|
}
|
|
408
|
-
lines.push(
|
|
409
|
-
|
|
480
|
+
lines.push(
|
|
481
|
+
" runtimeEnv: {",
|
|
482
|
+
...parsed.vars.map((v) => ` ${v.key}: process.env.${v.key},`),
|
|
483
|
+
" },",
|
|
484
|
+
"});"
|
|
485
|
+
);
|
|
410
486
|
return lines.join("\n") + "\n";
|
|
411
487
|
}
|
|
488
|
+
var CONTRACT_FILE_NAMES = [
|
|
489
|
+
"env.contract.ts",
|
|
490
|
+
"env.contract.mjs",
|
|
491
|
+
"env.contract.js"
|
|
492
|
+
];
|
|
493
|
+
function defineContract(contract) {
|
|
494
|
+
return contract;
|
|
495
|
+
}
|
|
496
|
+
async function loadContract(cwd = process.cwd()) {
|
|
497
|
+
for (const name of CONTRACT_FILE_NAMES) {
|
|
498
|
+
const filePath = path6.resolve(cwd, name);
|
|
499
|
+
if (existsSync(filePath)) {
|
|
500
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
501
|
+
const mod = await import(fileUrl);
|
|
502
|
+
return mod.default;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return void 0;
|
|
506
|
+
}
|
|
412
507
|
var CONFIG_FILE_NAMES = [
|
|
413
|
-
"env-typegen.config.ts",
|
|
414
508
|
"env-typegen.config.mjs",
|
|
415
|
-
"env-typegen.config.js"
|
|
509
|
+
"env-typegen.config.js",
|
|
510
|
+
"env-typegen.config.ts"
|
|
416
511
|
];
|
|
417
512
|
function defineConfig(config) {
|
|
418
513
|
return config;
|
|
419
514
|
}
|
|
420
515
|
async function loadConfig(cwd = process.cwd()) {
|
|
421
516
|
for (const name of CONFIG_FILE_NAMES) {
|
|
422
|
-
const filePath =
|
|
517
|
+
const filePath = path6.resolve(cwd, name);
|
|
423
518
|
if (existsSync(filePath)) {
|
|
519
|
+
if (filePath.endsWith(".ts")) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Config file "${name}" was found but TypeScript files cannot be loaded directly at runtime.
|
|
522
|
+
Rename it to "env-typegen.config.mjs" and use ESM export syntax:
|
|
523
|
+
|
|
524
|
+
// env-typegen.config.mjs
|
|
525
|
+
import { defineConfig } from "@xlameiro/env-typegen";
|
|
526
|
+
export default defineConfig({ input: ".env.example" });
|
|
527
|
+
|
|
528
|
+
Tip: keep env-typegen.config.ts for IDE autocompletion and create a sibling
|
|
529
|
+
env-typegen.config.mjs for runtime loading.`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
424
532
|
const fileUrl = pathToFileURL(filePath).href;
|
|
425
533
|
const mod = await import(fileUrl);
|
|
426
534
|
return mod.default;
|
|
@@ -429,11 +537,11 @@ async function loadConfig(cwd = process.cwd()) {
|
|
|
429
537
|
return void 0;
|
|
430
538
|
}
|
|
431
539
|
async function readEnvFile(filePath) {
|
|
432
|
-
return readFile(
|
|
540
|
+
return readFile(path6.resolve(filePath), "utf8");
|
|
433
541
|
}
|
|
434
542
|
async function writeOutput(filePath, content) {
|
|
435
|
-
const resolved =
|
|
436
|
-
await mkdir(
|
|
543
|
+
const resolved = path6.resolve(filePath);
|
|
544
|
+
await mkdir(path6.dirname(resolved), { recursive: true });
|
|
437
545
|
await writeFile(resolved, content, "utf8");
|
|
438
546
|
}
|
|
439
547
|
async function formatOutput(content, parser = "typescript") {
|
|
@@ -447,6 +555,15 @@ async function formatOutput(content, parser = "typescript") {
|
|
|
447
555
|
return content;
|
|
448
556
|
}
|
|
449
557
|
}
|
|
558
|
+
function log(message) {
|
|
559
|
+
console.log(message);
|
|
560
|
+
}
|
|
561
|
+
function warn(message) {
|
|
562
|
+
console.warn(yellow(`\u26A0 ${message}`));
|
|
563
|
+
}
|
|
564
|
+
function error(message) {
|
|
565
|
+
console.error(red(`\u2716 ${message}`));
|
|
566
|
+
}
|
|
450
567
|
function success(message) {
|
|
451
568
|
console.log(green(`\u2714 ${message}`));
|
|
452
569
|
}
|
|
@@ -454,17 +571,17 @@ function success(message) {
|
|
|
454
571
|
// src/pipeline.ts
|
|
455
572
|
function deriveOutputPath(base, generator, isSingle) {
|
|
456
573
|
if (isSingle) return base;
|
|
457
|
-
const ext =
|
|
574
|
+
const ext = path6.extname(base);
|
|
458
575
|
const noExt = ext.length > 0 ? base.slice(0, -ext.length) : base;
|
|
459
576
|
const baseExt = ext.length > 0 ? ext : ".ts";
|
|
460
577
|
const outExt = generator === "declaration" ? ".d.ts" : baseExt;
|
|
461
578
|
return `${noExt}.${generator}${outExt}`;
|
|
462
579
|
}
|
|
463
580
|
function deriveOutputBaseForInput(output, inputPath) {
|
|
464
|
-
const dir =
|
|
465
|
-
const ext =
|
|
466
|
-
const stem =
|
|
467
|
-
return
|
|
581
|
+
const dir = path6.dirname(output);
|
|
582
|
+
const ext = path6.extname(output);
|
|
583
|
+
const stem = path6.basename(inputPath, path6.extname(inputPath));
|
|
584
|
+
return path6.join(dir, `${stem}${ext}`);
|
|
468
585
|
}
|
|
469
586
|
function buildOutput(generator, parsed) {
|
|
470
587
|
switch (generator) {
|
|
@@ -524,7 +641,7 @@ async function runGenerate(options) {
|
|
|
524
641
|
const parsed = parseEnvFileContent(
|
|
525
642
|
content,
|
|
526
643
|
inputPath,
|
|
527
|
-
inferenceRules2
|
|
644
|
+
inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
|
|
528
645
|
);
|
|
529
646
|
for (const generator of generators) {
|
|
530
647
|
let generated = buildOutput(generator, parsed);
|
|
@@ -545,6 +662,1442 @@ async function runGenerate(options) {
|
|
|
545
662
|
}
|
|
546
663
|
}
|
|
547
664
|
|
|
548
|
-
|
|
665
|
+
// src/validator/contract-validator.ts
|
|
666
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
|
|
667
|
+
function buildExpected(entry) {
|
|
668
|
+
if (entry.enumValues !== void 0 && entry.enumValues.length > 0) {
|
|
669
|
+
return { type: "enum", values: entry.enumValues };
|
|
670
|
+
}
|
|
671
|
+
switch (entry.expectedType) {
|
|
672
|
+
case "number":
|
|
673
|
+
return {
|
|
674
|
+
type: "number",
|
|
675
|
+
...entry.constraints?.min !== void 0 && { min: entry.constraints.min },
|
|
676
|
+
...entry.constraints?.max !== void 0 && { max: entry.constraints.max }
|
|
677
|
+
};
|
|
678
|
+
case "boolean":
|
|
679
|
+
return { type: "boolean" };
|
|
680
|
+
case "url":
|
|
681
|
+
return { type: "url" };
|
|
682
|
+
case "email":
|
|
683
|
+
return { type: "email" };
|
|
684
|
+
case "json":
|
|
685
|
+
return { type: "json" };
|
|
686
|
+
case "semver":
|
|
687
|
+
return { type: "semver" };
|
|
688
|
+
case "unknown":
|
|
689
|
+
return { type: "unknown" };
|
|
690
|
+
default:
|
|
691
|
+
return { type: "string" };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function checkSecretExposed(variable, entry, environment) {
|
|
695
|
+
if (entry.isSecret !== true) return void 0;
|
|
696
|
+
if (variable.rawValue === "") return void 0;
|
|
697
|
+
return {
|
|
698
|
+
code: "ENV_SECRET_EXPOSED",
|
|
699
|
+
key: variable.key,
|
|
700
|
+
expected: buildExpected(entry),
|
|
701
|
+
environment,
|
|
702
|
+
severity: "error"
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function checkInvalidType(variable, entry, environment) {
|
|
706
|
+
const value = variable.rawValue;
|
|
707
|
+
const expected = buildExpected(entry);
|
|
708
|
+
switch (entry.expectedType) {
|
|
709
|
+
case "number": {
|
|
710
|
+
if (!Number.isFinite(Number(value))) {
|
|
711
|
+
return {
|
|
712
|
+
code: "ENV_INVALID_TYPE",
|
|
713
|
+
key: variable.key,
|
|
714
|
+
expected,
|
|
715
|
+
environment,
|
|
716
|
+
severity: "error"
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
case "boolean": {
|
|
722
|
+
if (value !== "true" && value !== "false") {
|
|
723
|
+
return {
|
|
724
|
+
code: "ENV_INVALID_TYPE",
|
|
725
|
+
key: variable.key,
|
|
726
|
+
expected,
|
|
727
|
+
environment,
|
|
728
|
+
severity: "error"
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
case "url": {
|
|
734
|
+
try {
|
|
735
|
+
new URL(value);
|
|
736
|
+
} catch {
|
|
737
|
+
return {
|
|
738
|
+
code: "ENV_INVALID_TYPE",
|
|
739
|
+
key: variable.key,
|
|
740
|
+
expected,
|
|
741
|
+
environment,
|
|
742
|
+
severity: "error"
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
case "email": {
|
|
748
|
+
if (!value.includes("@") || !value.includes(".")) {
|
|
749
|
+
return {
|
|
750
|
+
code: "ENV_INVALID_TYPE",
|
|
751
|
+
key: variable.key,
|
|
752
|
+
expected,
|
|
753
|
+
environment,
|
|
754
|
+
severity: "error"
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
case "semver": {
|
|
760
|
+
if (!SEMVER_RE.test(value)) {
|
|
761
|
+
return {
|
|
762
|
+
code: "ENV_INVALID_TYPE",
|
|
763
|
+
key: variable.key,
|
|
764
|
+
expected,
|
|
765
|
+
environment,
|
|
766
|
+
severity: "error"
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return void 0;
|
|
773
|
+
}
|
|
774
|
+
function checkNumericConstraints(variable, entry, environment) {
|
|
775
|
+
if (entry.expectedType !== "number" || entry.constraints === void 0) return void 0;
|
|
776
|
+
const num = Number(variable.rawValue);
|
|
777
|
+
if (!Number.isFinite(num)) return void 0;
|
|
778
|
+
const { min, max } = entry.constraints;
|
|
779
|
+
const outsideMin = min !== void 0 && num < min;
|
|
780
|
+
const outsideMax = max !== void 0 && num > max;
|
|
781
|
+
if (outsideMin || outsideMax) {
|
|
782
|
+
return {
|
|
783
|
+
code: "ENV_INVALID_VALUE",
|
|
784
|
+
key: variable.key,
|
|
785
|
+
expected: buildExpected(entry),
|
|
786
|
+
environment,
|
|
787
|
+
severity: "error"
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
return void 0;
|
|
791
|
+
}
|
|
792
|
+
function checkInvalidValue(variable, entry, environment) {
|
|
793
|
+
const value = variable.rawValue;
|
|
794
|
+
if (entry.enumValues !== void 0 && entry.enumValues.length > 0) {
|
|
795
|
+
if (!entry.enumValues.includes(value)) {
|
|
796
|
+
return {
|
|
797
|
+
code: "ENV_INVALID_VALUE",
|
|
798
|
+
key: variable.key,
|
|
799
|
+
expected: { type: "enum", values: entry.enumValues },
|
|
800
|
+
environment,
|
|
801
|
+
severity: "error"
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
return void 0;
|
|
805
|
+
}
|
|
806
|
+
return checkNumericConstraints(variable, entry, environment);
|
|
807
|
+
}
|
|
808
|
+
function checkVariable(variable, context) {
|
|
809
|
+
const { contractByName, environment, strict } = context;
|
|
810
|
+
const entry = contractByName.get(variable.key);
|
|
811
|
+
if (entry === void 0) {
|
|
812
|
+
return [
|
|
813
|
+
{
|
|
814
|
+
code: "ENV_EXTRA",
|
|
815
|
+
key: variable.key,
|
|
816
|
+
expected: { type: "string" },
|
|
817
|
+
environment,
|
|
818
|
+
severity: strict ? "error" : "warning"
|
|
819
|
+
}
|
|
820
|
+
];
|
|
821
|
+
}
|
|
822
|
+
const found = [];
|
|
823
|
+
const secretIssue = checkSecretExposed(variable, entry, environment);
|
|
824
|
+
if (secretIssue !== void 0) found.push(secretIssue);
|
|
825
|
+
if (variable.rawValue === "") return found;
|
|
826
|
+
const typeIssue = checkInvalidType(variable, entry, environment);
|
|
827
|
+
if (typeIssue !== void 0) {
|
|
828
|
+
found.push(typeIssue);
|
|
829
|
+
return found;
|
|
830
|
+
}
|
|
831
|
+
const valueIssue = checkInvalidValue(variable, entry, environment);
|
|
832
|
+
if (valueIssue !== void 0) found.push(valueIssue);
|
|
833
|
+
return found;
|
|
834
|
+
}
|
|
835
|
+
function validateContract(parsed, contract, opts = {}) {
|
|
836
|
+
const environment = opts.environment ?? "local";
|
|
837
|
+
const strict = opts.strict ?? true;
|
|
838
|
+
const issues = [];
|
|
839
|
+
const contractByName = new Map(
|
|
840
|
+
contract.vars.map((entry) => [entry.name, entry])
|
|
841
|
+
);
|
|
842
|
+
const parsedByKey = new Map(parsed.vars.map((v) => [v.key, v]));
|
|
843
|
+
for (const entry of contract.vars) {
|
|
844
|
+
if (!entry.required) continue;
|
|
845
|
+
if (parsedByKey.has(entry.name)) continue;
|
|
846
|
+
issues.push({
|
|
847
|
+
code: "ENV_MISSING",
|
|
848
|
+
key: entry.name,
|
|
849
|
+
expected: buildExpected(entry),
|
|
850
|
+
environment,
|
|
851
|
+
severity: "error"
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
const context = { contractByName, environment, strict };
|
|
855
|
+
for (const variable of parsed.vars) {
|
|
856
|
+
const perVarIssues = checkVariable(variable, context);
|
|
857
|
+
for (const issue of perVarIssues) {
|
|
858
|
+
issues.push(issue);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return { issues };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/reporting/ci-reporter.ts
|
|
865
|
+
var ISSUE_TYPE_MAP = {
|
|
866
|
+
ENV_MISSING: "Required variable is missing from the env file",
|
|
867
|
+
ENV_EXTRA: "Variable is present in the env file but not declared in the contract",
|
|
868
|
+
ENV_INVALID_TYPE: "Value cannot be coerced to the declared type",
|
|
869
|
+
ENV_INVALID_VALUE: "Value fails a constraint check (enum, min, max)",
|
|
870
|
+
ENV_CONFLICT: "Variable has inconsistent values across environments",
|
|
871
|
+
ENV_SECRET_EXPOSED: "Secret variable has a non-empty value in a public env file"
|
|
872
|
+
};
|
|
873
|
+
function toIssue(vi) {
|
|
874
|
+
const issue = {
|
|
875
|
+
code: vi.code,
|
|
876
|
+
type: ISSUE_TYPE_MAP[vi.code],
|
|
877
|
+
key: vi.key,
|
|
878
|
+
expected: vi.expected,
|
|
879
|
+
environment: vi.environment,
|
|
880
|
+
severity: vi.severity,
|
|
881
|
+
value: null
|
|
882
|
+
};
|
|
883
|
+
if (vi.received !== void 0) {
|
|
884
|
+
issue.received = vi.received;
|
|
885
|
+
}
|
|
886
|
+
return issue;
|
|
887
|
+
}
|
|
888
|
+
function buildCiReport(result, meta) {
|
|
889
|
+
const issues = result.issues.map(toIssue);
|
|
890
|
+
const errors = issues.filter((i) => i.severity === "error").length;
|
|
891
|
+
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
892
|
+
return {
|
|
893
|
+
schemaVersion: 1,
|
|
894
|
+
status: errors > 0 ? "fail" : "ok",
|
|
895
|
+
summary: { errors, warnings },
|
|
896
|
+
issues,
|
|
897
|
+
meta
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function formatCiReport(report, opts) {
|
|
901
|
+
return JSON.stringify(report, null, opts?.pretty === true ? 2 : void 0);
|
|
902
|
+
}
|
|
903
|
+
async function resolveContract(contractPath, cwd) {
|
|
904
|
+
if (contractPath !== void 0) {
|
|
905
|
+
const absPath = path6.resolve(cwd ?? process.cwd(), contractPath);
|
|
906
|
+
if (!existsSync(absPath)) {
|
|
907
|
+
throw new Error(`Contract file not found: ${absPath}`);
|
|
908
|
+
}
|
|
909
|
+
const mod = await import(pathToFileURL(absPath).href);
|
|
910
|
+
if (mod.default === void 0) {
|
|
911
|
+
throw new Error(`Contract file has no default export: ${absPath}`);
|
|
912
|
+
}
|
|
913
|
+
return mod.default;
|
|
914
|
+
}
|
|
915
|
+
const contract = await loadContract(cwd);
|
|
916
|
+
if (contract === void 0) {
|
|
917
|
+
throw new Error("No contract file found");
|
|
918
|
+
}
|
|
919
|
+
return contract;
|
|
920
|
+
}
|
|
921
|
+
function outputJson(result, input, pretty) {
|
|
922
|
+
const report = buildCiReport(result, {
|
|
923
|
+
env: input,
|
|
924
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
925
|
+
});
|
|
926
|
+
const formatOpts = {};
|
|
927
|
+
if (pretty) formatOpts.pretty = true;
|
|
928
|
+
log(formatCiReport(report, formatOpts));
|
|
929
|
+
}
|
|
930
|
+
function outputHuman(result) {
|
|
931
|
+
if (result.issues.length === 0) {
|
|
932
|
+
success("No issues found");
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
for (const issue of result.issues) {
|
|
936
|
+
const msg = `[${issue.code}] ${issue.key}`;
|
|
937
|
+
if (issue.severity === "error") {
|
|
938
|
+
error(msg);
|
|
939
|
+
} else {
|
|
940
|
+
warn(msg);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
async function runCheck(opts) {
|
|
945
|
+
const contract = await resolveContract(opts.contract, opts.cwd);
|
|
946
|
+
const parsed = parseEnvFile(opts.input);
|
|
947
|
+
const validatorOpts = {};
|
|
948
|
+
if (opts.environment !== void 0) validatorOpts.environment = opts.environment;
|
|
949
|
+
if (opts.strict !== void 0) validatorOpts.strict = opts.strict;
|
|
950
|
+
const result = validateContract(parsed, contract, validatorOpts);
|
|
951
|
+
const hasErrors = result.issues.some((issue) => issue.severity === "error");
|
|
952
|
+
const status = hasErrors ? "fail" : "ok";
|
|
953
|
+
if (!opts.silent) {
|
|
954
|
+
if (opts.json) {
|
|
955
|
+
outputJson(result, opts.input, opts.pretty);
|
|
956
|
+
} else {
|
|
957
|
+
outputHuman(result);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return status;
|
|
961
|
+
}
|
|
962
|
+
function isRecord(value) {
|
|
963
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
964
|
+
}
|
|
965
|
+
function readEntryValue(entry, keys) {
|
|
966
|
+
for (const key of keys) {
|
|
967
|
+
const value = entry[key];
|
|
968
|
+
if (typeof value === "string") return value;
|
|
969
|
+
}
|
|
970
|
+
return void 0;
|
|
971
|
+
}
|
|
972
|
+
function getVercelEntries(value) {
|
|
973
|
+
if (Array.isArray(value)) return value;
|
|
974
|
+
if (isRecord(value) && Array.isArray(value.envs)) return value.envs;
|
|
975
|
+
return [];
|
|
976
|
+
}
|
|
977
|
+
function parseVercelPayload(value) {
|
|
978
|
+
const entries = getVercelEntries(value);
|
|
979
|
+
const result = {};
|
|
980
|
+
for (const entry of entries) {
|
|
981
|
+
if (!isRecord(entry)) continue;
|
|
982
|
+
const key = readEntryValue(entry, ["key", "name"]);
|
|
983
|
+
if (key === void 0) continue;
|
|
984
|
+
const envValue = readEntryValue(entry, ["value", "targetValue", "content"]) ?? "";
|
|
985
|
+
result[key] = envValue;
|
|
986
|
+
}
|
|
987
|
+
return result;
|
|
988
|
+
}
|
|
989
|
+
function getCloudflareEntries(value) {
|
|
990
|
+
if (Array.isArray(value)) return value;
|
|
991
|
+
if (isRecord(value) && Array.isArray(value.result)) return value.result;
|
|
992
|
+
return [];
|
|
993
|
+
}
|
|
994
|
+
function parseCloudflarePayload(value) {
|
|
995
|
+
const entries = getCloudflareEntries(value);
|
|
996
|
+
const result = {};
|
|
997
|
+
for (const entry of entries) {
|
|
998
|
+
if (!isRecord(entry)) continue;
|
|
999
|
+
const key = readEntryValue(entry, ["name", "key"]);
|
|
1000
|
+
if (key === void 0) continue;
|
|
1001
|
+
const envValue = readEntryValue(entry, ["text", "value", "secret"]) ?? "";
|
|
1002
|
+
result[key] = envValue;
|
|
1003
|
+
}
|
|
1004
|
+
return result;
|
|
1005
|
+
}
|
|
1006
|
+
function parseAwsPayload(value) {
|
|
1007
|
+
const entries = isRecord(value) && Array.isArray(value.Parameters) ? value.Parameters : [];
|
|
1008
|
+
const result = {};
|
|
1009
|
+
for (const entry of entries) {
|
|
1010
|
+
if (!isRecord(entry)) continue;
|
|
1011
|
+
const name = readEntryValue(entry, ["Name", "name"]);
|
|
1012
|
+
if (name === void 0) continue;
|
|
1013
|
+
const key = name.split("/").findLast((part) => part.length > 0) ?? name;
|
|
1014
|
+
const envValue = readEntryValue(entry, ["Value", "value"]) ?? "";
|
|
1015
|
+
result[key] = envValue;
|
|
1016
|
+
}
|
|
1017
|
+
return result;
|
|
1018
|
+
}
|
|
1019
|
+
function parseProviderPayload(provider, value) {
|
|
1020
|
+
if (provider === "vercel") return parseVercelPayload(value);
|
|
1021
|
+
if (provider === "cloudflare") return parseCloudflarePayload(value);
|
|
1022
|
+
return parseAwsPayload(value);
|
|
1023
|
+
}
|
|
1024
|
+
async function loadCloudSource(options) {
|
|
1025
|
+
const resolvedPath = path6.resolve(options.filePath);
|
|
1026
|
+
const raw = await readFile(resolvedPath, "utf8");
|
|
1027
|
+
const parsed = JSON.parse(raw);
|
|
1028
|
+
return parseProviderPayload(options.provider, parsed);
|
|
1029
|
+
}
|
|
1030
|
+
var SECRET_KEY_RE = /(SECRET|TOKEN|PASSWORD|PRIVATE|API_KEY|ACCESS_KEY|CLIENT_SECRET)/i;
|
|
1031
|
+
var CONTRACT_FILE_NAMES2 = ["env.contract.ts", "env.contract.mjs", "env.contract.js"];
|
|
1032
|
+
function isRecord2(value) {
|
|
1033
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1034
|
+
}
|
|
1035
|
+
function isExpected(value) {
|
|
1036
|
+
if (!isRecord2(value)) return false;
|
|
1037
|
+
const typeValue = value.type;
|
|
1038
|
+
if (typeof typeValue !== "string") return false;
|
|
1039
|
+
const validTypes = /* @__PURE__ */ new Set([
|
|
1040
|
+
"string",
|
|
1041
|
+
"number",
|
|
1042
|
+
"boolean",
|
|
1043
|
+
"enum",
|
|
1044
|
+
"url",
|
|
1045
|
+
"email",
|
|
1046
|
+
"json",
|
|
1047
|
+
"semver",
|
|
1048
|
+
"unknown"
|
|
1049
|
+
]);
|
|
1050
|
+
if (!validTypes.has(typeValue)) return false;
|
|
1051
|
+
if (typeValue === "enum") {
|
|
1052
|
+
return Array.isArray(value.values) && value.values.every((item) => typeof item === "string");
|
|
1053
|
+
}
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
function isEnvContractVariable(value) {
|
|
1057
|
+
if (!isRecord2(value)) return false;
|
|
1058
|
+
if (!isExpected(value.expected)) return false;
|
|
1059
|
+
if (typeof value.required !== "boolean") return false;
|
|
1060
|
+
if (typeof value.clientSide !== "boolean") return false;
|
|
1061
|
+
if (value.description !== void 0 && typeof value.description !== "string") return false;
|
|
1062
|
+
if (value.secret !== void 0 && typeof value.secret !== "boolean") return false;
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
1065
|
+
function isEnvContract(value) {
|
|
1066
|
+
if (!isRecord2(value)) return false;
|
|
1067
|
+
if (value.schemaVersion !== 1) return false;
|
|
1068
|
+
if (!isRecord2(value.variables)) return false;
|
|
1069
|
+
return Object.values(value.variables).every((item) => isEnvContractVariable(item));
|
|
1070
|
+
}
|
|
1071
|
+
function isLegacyContract(value) {
|
|
1072
|
+
if (!isRecord2(value)) return false;
|
|
1073
|
+
if (!Array.isArray(value.vars)) return false;
|
|
1074
|
+
return value.vars.every((entry) => isRecord2(entry) && typeof entry.name === "string");
|
|
1075
|
+
}
|
|
1076
|
+
function mapEnvVarTypeToExpected(type) {
|
|
1077
|
+
if (type === "number") return { type: "number" };
|
|
1078
|
+
if (type === "boolean") return { type: "boolean" };
|
|
1079
|
+
if (type === "url") return { type: "url" };
|
|
1080
|
+
if (type === "email") return { type: "email" };
|
|
1081
|
+
if (type === "json") return { type: "json" };
|
|
1082
|
+
if (type === "semver") return { type: "semver" };
|
|
1083
|
+
if (type === "unknown") return { type: "unknown" };
|
|
1084
|
+
return { type: "string" };
|
|
1085
|
+
}
|
|
1086
|
+
function shouldTreatAsSecret(key) {
|
|
1087
|
+
return SECRET_KEY_RE.test(key);
|
|
1088
|
+
}
|
|
1089
|
+
function toExpectedFromLegacyEntry(entry) {
|
|
1090
|
+
if (entry.enumValues !== void 0 && entry.enumValues.length > 0) {
|
|
1091
|
+
return { type: "enum", values: entry.enumValues };
|
|
1092
|
+
}
|
|
1093
|
+
if (entry.expectedType === "number") {
|
|
1094
|
+
return {
|
|
1095
|
+
type: "number",
|
|
1096
|
+
...entry.constraints?.min !== void 0 && { min: entry.constraints.min },
|
|
1097
|
+
...entry.constraints?.max !== void 0 && { max: entry.constraints.max }
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
if (entry.expectedType === "boolean") return { type: "boolean" };
|
|
1101
|
+
if (entry.expectedType === "url") return { type: "url" };
|
|
1102
|
+
if (entry.expectedType === "email") return { type: "email" };
|
|
1103
|
+
if (entry.expectedType === "json") return { type: "json" };
|
|
1104
|
+
if (entry.expectedType === "semver") return { type: "semver" };
|
|
1105
|
+
if (entry.expectedType === "unknown") return { type: "unknown" };
|
|
1106
|
+
return { type: "string" };
|
|
1107
|
+
}
|
|
1108
|
+
function convertLegacyContract(contract) {
|
|
1109
|
+
const variables = {};
|
|
1110
|
+
for (const entry of contract.vars) {
|
|
1111
|
+
const clientSide = entry.runtime === "client" || entry.name.startsWith("NEXT_PUBLIC_");
|
|
1112
|
+
variables[entry.name] = {
|
|
1113
|
+
expected: toExpectedFromLegacyEntry(entry),
|
|
1114
|
+
required: entry.required,
|
|
1115
|
+
clientSide,
|
|
1116
|
+
...entry.description !== void 0 && { description: entry.description },
|
|
1117
|
+
...(entry.isSecret ?? shouldTreatAsSecret(entry.name)) && { secret: true }
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
schemaVersion: 1,
|
|
1122
|
+
variables
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function buildContractFromExample(examplePath) {
|
|
1126
|
+
const parsed = parseEnvFile(examplePath);
|
|
1127
|
+
const variables = {};
|
|
1128
|
+
for (const variable of parsed.vars) {
|
|
1129
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
1130
|
+
variables[variable.key] = {
|
|
1131
|
+
expected: mapEnvVarTypeToExpected(effectiveType),
|
|
1132
|
+
required: variable.isRequired,
|
|
1133
|
+
clientSide: variable.isClientSide,
|
|
1134
|
+
...variable.description !== void 0 && { description: variable.description },
|
|
1135
|
+
...shouldTreatAsSecret(variable.key) && { secret: true }
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
return {
|
|
1139
|
+
schemaVersion: 1,
|
|
1140
|
+
variables
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
function findDefaultContractPath(cwd) {
|
|
1144
|
+
for (const fileName of CONTRACT_FILE_NAMES2) {
|
|
1145
|
+
const candidatePath = path6.resolve(cwd, fileName);
|
|
1146
|
+
if (existsSync(candidatePath)) return candidatePath;
|
|
1147
|
+
}
|
|
1148
|
+
return void 0;
|
|
1149
|
+
}
|
|
1150
|
+
async function loadValidationContract(options) {
|
|
1151
|
+
const { fallbackExamplePath, contractPath, cwd = process.cwd() } = options;
|
|
1152
|
+
const discoveredContractPath = findDefaultContractPath(cwd);
|
|
1153
|
+
const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path6.resolve(cwd, contractPath);
|
|
1154
|
+
if (resolvedContractPath !== void 0 && existsSync(resolvedContractPath)) {
|
|
1155
|
+
const moduleUrl = pathToFileURL(resolvedContractPath).href;
|
|
1156
|
+
const moduleValue = await import(moduleUrl);
|
|
1157
|
+
const candidate = moduleValue.default ?? moduleValue.contract;
|
|
1158
|
+
if (isEnvContract(candidate)) {
|
|
1159
|
+
return candidate;
|
|
1160
|
+
}
|
|
1161
|
+
if (isLegacyContract(candidate)) {
|
|
1162
|
+
return convertLegacyContract(candidate);
|
|
1163
|
+
}
|
|
1164
|
+
throw new Error(
|
|
1165
|
+
`Invalid contract at ${resolvedContractPath}. Export default must match EnvContract.`
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
return buildContractFromExample(path6.resolve(cwd, fallbackExamplePath));
|
|
1169
|
+
}
|
|
1170
|
+
function isRecord3(value) {
|
|
1171
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1172
|
+
}
|
|
1173
|
+
function isPlugin(value) {
|
|
1174
|
+
if (!isRecord3(value)) return false;
|
|
1175
|
+
if (typeof value.name !== "string") return false;
|
|
1176
|
+
if (value.transformContract !== void 0 && typeof value.transformContract !== "function") {
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
if (value.transformSource !== void 0 && typeof value.transformSource !== "function") {
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
if (value.transformReport !== void 0 && typeof value.transformReport !== "function") {
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
return true;
|
|
1186
|
+
}
|
|
1187
|
+
async function loadPluginFromPath(pluginPath, cwd) {
|
|
1188
|
+
const resolvedPath = path6.resolve(cwd, pluginPath);
|
|
1189
|
+
const moduleValue = await import(pathToFileURL(resolvedPath).href);
|
|
1190
|
+
const candidate = moduleValue.default ?? moduleValue.plugin;
|
|
1191
|
+
if (isPlugin(candidate)) return candidate;
|
|
1192
|
+
throw new Error(
|
|
1193
|
+
`Invalid plugin at ${resolvedPath}.
|
|
1194
|
+
Expected a default export matching:
|
|
1195
|
+
{ name: string,
|
|
1196
|
+
transformSource?(ctx: { environment: string; values: Record<string, string> }): Record<string, string>,
|
|
1197
|
+
transformReport?(report: ValidationReport): ValidationReport,
|
|
1198
|
+
transformContract?(contract: EnvContract): EnvContract }`
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
async function loadPlugins(options) {
|
|
1202
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1203
|
+
const plugins = [];
|
|
1204
|
+
const references = [...options.configPlugins ?? [], ...options.pluginPaths];
|
|
1205
|
+
for (const reference of references) {
|
|
1206
|
+
if (typeof reference === "string") {
|
|
1207
|
+
plugins.push(await loadPluginFromPath(reference, cwd));
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (isPlugin(reference)) {
|
|
1211
|
+
plugins.push(reference);
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
throw new Error("Invalid plugin reference in configuration.");
|
|
1215
|
+
}
|
|
1216
|
+
return plugins;
|
|
1217
|
+
}
|
|
1218
|
+
function applyContractPlugins(contract, plugins) {
|
|
1219
|
+
let next = contract;
|
|
1220
|
+
for (const plugin of plugins) {
|
|
1221
|
+
if (plugin.transformContract === void 0) continue;
|
|
1222
|
+
next = plugin.transformContract(next);
|
|
1223
|
+
}
|
|
1224
|
+
return next;
|
|
1225
|
+
}
|
|
1226
|
+
function applySourcePlugins(params, plugins) {
|
|
1227
|
+
let next = params.values;
|
|
1228
|
+
for (const plugin of plugins) {
|
|
1229
|
+
if (plugin.transformSource === void 0) continue;
|
|
1230
|
+
next = plugin.transformSource({ environment: params.environment, values: next });
|
|
1231
|
+
}
|
|
1232
|
+
return next;
|
|
1233
|
+
}
|
|
1234
|
+
function applyReportPlugins(report, plugins) {
|
|
1235
|
+
let next = report;
|
|
1236
|
+
for (const plugin of plugins) {
|
|
1237
|
+
if (plugin.transformReport === void 0) continue;
|
|
1238
|
+
next = plugin.transformReport(next);
|
|
1239
|
+
}
|
|
1240
|
+
return next;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/validation/engine.ts
|
|
1244
|
+
var EMAIL_RE = /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/;
|
|
1245
|
+
var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/;
|
|
1246
|
+
function detectReceivedType(value) {
|
|
1247
|
+
const normalized = value.trim();
|
|
1248
|
+
if (normalized.length === 0) return "unknown";
|
|
1249
|
+
if (["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) return "boolean";
|
|
1250
|
+
if (!Number.isNaN(Number(normalized)) && Number.isFinite(Number(normalized))) return "number";
|
|
1251
|
+
if (SEMVER_RE2.test(normalized)) return "semver";
|
|
1252
|
+
try {
|
|
1253
|
+
const url = new URL(normalized);
|
|
1254
|
+
if (url.protocol.length > 0) return "url";
|
|
1255
|
+
} catch {
|
|
1256
|
+
}
|
|
1257
|
+
if (EMAIL_RE.test(normalized)) return "email";
|
|
1258
|
+
try {
|
|
1259
|
+
const parsed = JSON.parse(normalized);
|
|
1260
|
+
if (typeof parsed === "object" && parsed !== null) return "json";
|
|
1261
|
+
} catch {
|
|
1262
|
+
}
|
|
1263
|
+
return "string";
|
|
1264
|
+
}
|
|
1265
|
+
function validateNumber(expected, normalized, receivedType) {
|
|
1266
|
+
const parsed = Number(normalized);
|
|
1267
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
1268
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1269
|
+
}
|
|
1270
|
+
if (expected.min !== void 0 && parsed < expected.min) {
|
|
1271
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1272
|
+
}
|
|
1273
|
+
if (expected.max !== void 0 && parsed > expected.max) {
|
|
1274
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1275
|
+
}
|
|
1276
|
+
return { isValid: true, receivedType };
|
|
1277
|
+
}
|
|
1278
|
+
function validateBoolean(normalized, receivedType) {
|
|
1279
|
+
if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
|
|
1280
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1281
|
+
}
|
|
1282
|
+
return { isValid: true, receivedType };
|
|
1283
|
+
}
|
|
1284
|
+
function validateEnum(expected, normalized, receivedType) {
|
|
1285
|
+
if (!expected.values.includes(normalized)) {
|
|
1286
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1287
|
+
}
|
|
1288
|
+
return { isValid: true, receivedType };
|
|
1289
|
+
}
|
|
1290
|
+
function validateUrl(normalized, receivedType) {
|
|
1291
|
+
try {
|
|
1292
|
+
const value = new URL(normalized);
|
|
1293
|
+
if (value.protocol.length === 0)
|
|
1294
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1295
|
+
return { isValid: true, receivedType };
|
|
1296
|
+
} catch {
|
|
1297
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function validateEmail(normalized, receivedType) {
|
|
1301
|
+
if (!EMAIL_RE.test(normalized))
|
|
1302
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1303
|
+
return { isValid: true, receivedType };
|
|
1304
|
+
}
|
|
1305
|
+
function validateJson(normalized, receivedType) {
|
|
1306
|
+
try {
|
|
1307
|
+
const parsed = JSON.parse(normalized);
|
|
1308
|
+
if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
|
|
1309
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1310
|
+
} catch {
|
|
1311
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
function validateSemver(normalized, receivedType) {
|
|
1315
|
+
if (!SEMVER_RE2.test(normalized))
|
|
1316
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1317
|
+
return { isValid: true, receivedType };
|
|
1318
|
+
}
|
|
1319
|
+
function validateValueAgainstExpected(expected, rawValue) {
|
|
1320
|
+
const normalized = rawValue.trim();
|
|
1321
|
+
const receivedType = detectReceivedType(normalized);
|
|
1322
|
+
if (expected.type === "unknown" || expected.type === "string")
|
|
1323
|
+
return { isValid: true, receivedType };
|
|
1324
|
+
if (expected.type === "number") return validateNumber(expected, normalized, receivedType);
|
|
1325
|
+
if (expected.type === "boolean") return validateBoolean(normalized, receivedType);
|
|
1326
|
+
if (expected.type === "enum") return validateEnum(expected, normalized, receivedType);
|
|
1327
|
+
if (expected.type === "url") return validateUrl(normalized, receivedType);
|
|
1328
|
+
if (expected.type === "email") return validateEmail(normalized, receivedType);
|
|
1329
|
+
if (expected.type === "json") return validateJson(normalized, receivedType);
|
|
1330
|
+
if (expected.type === "semver") return validateSemver(normalized, receivedType);
|
|
1331
|
+
return { isValid: true, receivedType };
|
|
1332
|
+
}
|
|
1333
|
+
function toIssueCode(issueType) {
|
|
1334
|
+
if (issueType === "missing") return "ENV_MISSING";
|
|
1335
|
+
if (issueType === "extra") return "ENV_EXTRA";
|
|
1336
|
+
if (issueType === "invalid_type") return "ENV_INVALID_TYPE";
|
|
1337
|
+
if (issueType === "invalid_value") return "ENV_INVALID_VALUE";
|
|
1338
|
+
if (issueType === "conflict") return "ENV_CONFLICT";
|
|
1339
|
+
return "ENV_SECRET_EXPOSED";
|
|
1340
|
+
}
|
|
1341
|
+
function toIssueValue(value, debugValues) {
|
|
1342
|
+
if (!debugValues) return null;
|
|
1343
|
+
if (value === void 0) return null;
|
|
1344
|
+
return value;
|
|
1345
|
+
}
|
|
1346
|
+
function createIssue(params) {
|
|
1347
|
+
return {
|
|
1348
|
+
code: toIssueCode(params.type),
|
|
1349
|
+
type: params.type,
|
|
1350
|
+
severity: params.severity,
|
|
1351
|
+
key: params.key,
|
|
1352
|
+
environment: params.environment,
|
|
1353
|
+
message: params.message,
|
|
1354
|
+
value: toIssueValue(params.value, params.debugValues),
|
|
1355
|
+
...params.expected !== void 0 && { expected: params.expected },
|
|
1356
|
+
...params.receivedType !== void 0 && { receivedType: params.receivedType }
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
function dedupeIssues(issues) {
|
|
1360
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1361
|
+
const unique = [];
|
|
1362
|
+
for (const issue of issues) {
|
|
1363
|
+
const token = [
|
|
1364
|
+
issue.code,
|
|
1365
|
+
issue.type,
|
|
1366
|
+
issue.severity,
|
|
1367
|
+
issue.environment,
|
|
1368
|
+
issue.key,
|
|
1369
|
+
issue.message,
|
|
1370
|
+
issue.receivedType ?? ""
|
|
1371
|
+
].join("|");
|
|
1372
|
+
if (seen.has(token)) continue;
|
|
1373
|
+
seen.add(token);
|
|
1374
|
+
unique.push(issue);
|
|
1375
|
+
}
|
|
1376
|
+
return unique;
|
|
1377
|
+
}
|
|
1378
|
+
function buildReport(env, issues, recommendations) {
|
|
1379
|
+
const dedupedIssues = dedupeIssues(issues);
|
|
1380
|
+
const errors = dedupedIssues.filter((item) => item.severity === "error").length;
|
|
1381
|
+
const warnings = dedupedIssues.filter((item) => item.severity === "warning").length;
|
|
1382
|
+
const status = errors > 0 ? "fail" : "ok";
|
|
1383
|
+
return {
|
|
1384
|
+
schemaVersion: 1,
|
|
1385
|
+
status,
|
|
1386
|
+
summary: {
|
|
1387
|
+
errors,
|
|
1388
|
+
warnings,
|
|
1389
|
+
total: dedupedIssues.length
|
|
1390
|
+
},
|
|
1391
|
+
issues: dedupedIssues,
|
|
1392
|
+
meta: {
|
|
1393
|
+
env,
|
|
1394
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1395
|
+
},
|
|
1396
|
+
...recommendations !== void 0 && recommendations.length > 0 && { recommendations }
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function isClientSecret(variable, key) {
|
|
1400
|
+
return variable.secret === true && (variable.clientSide || key.startsWith("NEXT_PUBLIC_"));
|
|
1401
|
+
}
|
|
1402
|
+
function checkContractVariable(key, variable, context) {
|
|
1403
|
+
const { options, issues } = context;
|
|
1404
|
+
const value = options.values[key];
|
|
1405
|
+
const hasValue = value !== void 0 && value.trim().length > 0;
|
|
1406
|
+
if (variable.required && !hasValue) {
|
|
1407
|
+
issues.push(
|
|
1408
|
+
createIssue({
|
|
1409
|
+
type: "missing",
|
|
1410
|
+
severity: "error",
|
|
1411
|
+
key,
|
|
1412
|
+
environment: options.environment,
|
|
1413
|
+
message: `Required variable ${key} is missing.`,
|
|
1414
|
+
debugValues: options.debugValues,
|
|
1415
|
+
expected: variable.expected
|
|
1416
|
+
})
|
|
1417
|
+
);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (!hasValue) return;
|
|
1421
|
+
const validation = validateValueAgainstExpected(variable.expected, value);
|
|
1422
|
+
if (!validation.isValid) {
|
|
1423
|
+
const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`;
|
|
1424
|
+
issues.push(
|
|
1425
|
+
createIssue({
|
|
1426
|
+
type: validation.issueType,
|
|
1427
|
+
severity: "error",
|
|
1428
|
+
key,
|
|
1429
|
+
environment: options.environment,
|
|
1430
|
+
message,
|
|
1431
|
+
value,
|
|
1432
|
+
debugValues: options.debugValues,
|
|
1433
|
+
expected: variable.expected,
|
|
1434
|
+
receivedType: validation.receivedType
|
|
1435
|
+
})
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
if (isClientSecret(variable, key)) {
|
|
1439
|
+
issues.push(
|
|
1440
|
+
createIssue({
|
|
1441
|
+
type: "secret_exposed",
|
|
1442
|
+
severity: "error",
|
|
1443
|
+
key,
|
|
1444
|
+
environment: options.environment,
|
|
1445
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1446
|
+
value,
|
|
1447
|
+
debugValues: options.debugValues,
|
|
1448
|
+
expected: variable.expected
|
|
1449
|
+
})
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
function validateAgainstContract(options) {
|
|
1454
|
+
const issues = [];
|
|
1455
|
+
const contractKeys = new Set(Object.keys(options.contract.variables));
|
|
1456
|
+
for (const [key, variable] of Object.entries(options.contract.variables)) {
|
|
1457
|
+
checkContractVariable(key, variable, { options, issues });
|
|
1458
|
+
}
|
|
1459
|
+
for (const [key, value] of Object.entries(options.values)) {
|
|
1460
|
+
if (contractKeys.has(key)) continue;
|
|
1461
|
+
const severity = options.strict ? "error" : "warning";
|
|
1462
|
+
issues.push(
|
|
1463
|
+
createIssue({
|
|
1464
|
+
type: "extra",
|
|
1465
|
+
severity,
|
|
1466
|
+
key,
|
|
1467
|
+
environment: options.environment,
|
|
1468
|
+
message: `Variable ${key} is not defined in the contract.`,
|
|
1469
|
+
value,
|
|
1470
|
+
debugValues: options.debugValues
|
|
1471
|
+
})
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
return buildReport(options.environment, issues);
|
|
1475
|
+
}
|
|
1476
|
+
function collectUnionKeys(contract, sources) {
|
|
1477
|
+
const union = new Set(Object.keys(contract.variables));
|
|
1478
|
+
for (const source of Object.values(sources)) {
|
|
1479
|
+
for (const key of Object.keys(source)) {
|
|
1480
|
+
union.add(key);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return union;
|
|
1484
|
+
}
|
|
1485
|
+
function diffMissingEntries(key, missing, context) {
|
|
1486
|
+
const { variable, options, issues } = context;
|
|
1487
|
+
for (const entry of missing) {
|
|
1488
|
+
issues.push(
|
|
1489
|
+
createIssue({
|
|
1490
|
+
type: "missing",
|
|
1491
|
+
severity: "error",
|
|
1492
|
+
key,
|
|
1493
|
+
environment: entry.sourceName,
|
|
1494
|
+
message: `Variable ${key} is missing in ${entry.sourceName}.`,
|
|
1495
|
+
debugValues: options.debugValues,
|
|
1496
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1497
|
+
})
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
function diffTypeConflicts(key, present, context) {
|
|
1502
|
+
const { variable, options, issues } = context;
|
|
1503
|
+
const typeBySource = /* @__PURE__ */ new Map();
|
|
1504
|
+
for (const entry of present) {
|
|
1505
|
+
typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
|
|
1506
|
+
}
|
|
1507
|
+
if (new Set(typeBySource.values()).size <= 1) return;
|
|
1508
|
+
for (const [sourceName, detectedType] of typeBySource.entries()) {
|
|
1509
|
+
issues.push(
|
|
1510
|
+
createIssue({
|
|
1511
|
+
type: "conflict",
|
|
1512
|
+
severity: "error",
|
|
1513
|
+
key,
|
|
1514
|
+
environment: sourceName,
|
|
1515
|
+
message: `Variable ${key} has conflicting inferred type across environments.`,
|
|
1516
|
+
debugValues: options.debugValues,
|
|
1517
|
+
receivedType: detectedType,
|
|
1518
|
+
...options.sources[sourceName]?.[key] !== void 0 && {
|
|
1519
|
+
value: options.sources[sourceName]?.[key]
|
|
1520
|
+
},
|
|
1521
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1522
|
+
})
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function diffPresentEntry(key, entry, context) {
|
|
1527
|
+
const { variable, options, issues } = context;
|
|
1528
|
+
if (entry.value === void 0) return;
|
|
1529
|
+
if (variable === void 0) {
|
|
1530
|
+
const severity = options.strict ? "error" : "warning";
|
|
1531
|
+
issues.push(
|
|
1532
|
+
createIssue({
|
|
1533
|
+
type: "extra",
|
|
1534
|
+
severity,
|
|
1535
|
+
key,
|
|
1536
|
+
environment: entry.sourceName,
|
|
1537
|
+
message: `Variable ${key} is not defined in the contract.`,
|
|
1538
|
+
value: entry.value,
|
|
1539
|
+
debugValues: options.debugValues
|
|
1540
|
+
})
|
|
1541
|
+
);
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
const validation = validateValueAgainstExpected(variable.expected, entry.value);
|
|
1545
|
+
if (!validation.isValid) {
|
|
1546
|
+
const message = validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`;
|
|
1547
|
+
issues.push(
|
|
1548
|
+
createIssue({
|
|
1549
|
+
type: validation.issueType,
|
|
1550
|
+
severity: "error",
|
|
1551
|
+
key,
|
|
1552
|
+
environment: entry.sourceName,
|
|
1553
|
+
message,
|
|
1554
|
+
value: entry.value,
|
|
1555
|
+
debugValues: options.debugValues,
|
|
1556
|
+
expected: variable.expected,
|
|
1557
|
+
receivedType: validation.receivedType
|
|
1558
|
+
})
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
if (isClientSecret(variable, key)) {
|
|
1562
|
+
issues.push(
|
|
1563
|
+
createIssue({
|
|
1564
|
+
type: "secret_exposed",
|
|
1565
|
+
severity: "error",
|
|
1566
|
+
key,
|
|
1567
|
+
environment: entry.sourceName,
|
|
1568
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1569
|
+
value: entry.value,
|
|
1570
|
+
debugValues: options.debugValues,
|
|
1571
|
+
expected: variable.expected
|
|
1572
|
+
})
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
function diffEnvironmentSources(options) {
|
|
1577
|
+
const issues = [];
|
|
1578
|
+
const sourceNames = Object.keys(options.sources);
|
|
1579
|
+
const unionKeys = collectUnionKeys(options.contract, options.sources);
|
|
1580
|
+
for (const key of unionKeys) {
|
|
1581
|
+
const variable = options.contract.variables[key];
|
|
1582
|
+
const valuesBySource = sourceNames.map((sourceName) => ({
|
|
1583
|
+
sourceName,
|
|
1584
|
+
value: options.sources[sourceName]?.[key]
|
|
1585
|
+
}));
|
|
1586
|
+
const present = valuesBySource.filter(
|
|
1587
|
+
(entry) => entry.value !== void 0 && entry.value !== ""
|
|
1588
|
+
);
|
|
1589
|
+
const missing = valuesBySource.filter(
|
|
1590
|
+
(entry) => entry.value === void 0 || entry.value === ""
|
|
1591
|
+
);
|
|
1592
|
+
if (present.length === 0 && variable?.required === true) {
|
|
1593
|
+
for (const entry of missing) {
|
|
1594
|
+
issues.push(
|
|
1595
|
+
createIssue({
|
|
1596
|
+
type: "missing",
|
|
1597
|
+
severity: "error",
|
|
1598
|
+
key,
|
|
1599
|
+
environment: entry.sourceName,
|
|
1600
|
+
message: `Required variable ${key} is missing in ${entry.sourceName}.`,
|
|
1601
|
+
debugValues: options.debugValues,
|
|
1602
|
+
expected: variable.expected
|
|
1603
|
+
})
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
const ctx = { variable, options, issues };
|
|
1609
|
+
if (present.length > 0) {
|
|
1610
|
+
diffMissingEntries(key, missing, ctx);
|
|
1611
|
+
}
|
|
1612
|
+
diffTypeConflicts(key, present, ctx);
|
|
1613
|
+
for (const entry of present) {
|
|
1614
|
+
diffPresentEntry(key, entry, ctx);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
return buildReport("diff", issues);
|
|
1618
|
+
}
|
|
1619
|
+
function buildRecommendations(issues) {
|
|
1620
|
+
const codes = new Set(issues.map((item) => item.code));
|
|
1621
|
+
const recommendations = [];
|
|
1622
|
+
if (codes.has("ENV_MISSING")) {
|
|
1623
|
+
recommendations.push("Add missing required variables to each target environment.");
|
|
1624
|
+
}
|
|
1625
|
+
if (codes.has("ENV_EXTRA")) {
|
|
1626
|
+
recommendations.push(
|
|
1627
|
+
"Remove undeclared variables or add them to env.contract.ts intentionally."
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
if (codes.has("ENV_INVALID_TYPE") || codes.has("ENV_INVALID_VALUE")) {
|
|
1631
|
+
recommendations.push(
|
|
1632
|
+
"Normalize variable values so they match the expected contract types and constraints."
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
if (codes.has("ENV_CONFLICT")) {
|
|
1636
|
+
recommendations.push("Align variable semantics across environments to avoid drift.");
|
|
1637
|
+
}
|
|
1638
|
+
if (codes.has("ENV_SECRET_EXPOSED")) {
|
|
1639
|
+
recommendations.push(
|
|
1640
|
+
"Move secret variables to server-only scope and avoid NEXT_PUBLIC_ exposure for secrets."
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
return recommendations;
|
|
1644
|
+
}
|
|
1645
|
+
function buildDoctorReport(options) {
|
|
1646
|
+
const merged = [...options.checkReport.issues, ...options.diffReport.issues];
|
|
1647
|
+
const recommendations = buildRecommendations(merged);
|
|
1648
|
+
return buildReport("doctor", merged, recommendations);
|
|
1649
|
+
}
|
|
1650
|
+
function stripWrappingQuotes(value) {
|
|
1651
|
+
if (value.length < 2) return value;
|
|
1652
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1653
|
+
return value.slice(1, -1);
|
|
1654
|
+
}
|
|
1655
|
+
return value;
|
|
1656
|
+
}
|
|
1657
|
+
function parseEnvSourceContent(content) {
|
|
1658
|
+
const result = {};
|
|
1659
|
+
const lines = content.split("\n");
|
|
1660
|
+
for (const line of lines) {
|
|
1661
|
+
const trimmed = line.trim();
|
|
1662
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
1663
|
+
const match = /^(?:export\s+)?([A-Za-z_]\w*)=(.*)$/.exec(trimmed);
|
|
1664
|
+
if (match === null) continue;
|
|
1665
|
+
const key = match[1] ?? "";
|
|
1666
|
+
const rawValue = match[2] ?? "";
|
|
1667
|
+
result[key] = stripWrappingQuotes(rawValue.trim());
|
|
1668
|
+
}
|
|
1669
|
+
return result;
|
|
1670
|
+
}
|
|
1671
|
+
async function loadEnvSource(options) {
|
|
1672
|
+
const resolvedPath = path6.resolve(options.filePath);
|
|
1673
|
+
try {
|
|
1674
|
+
const content = await readFile(resolvedPath, "utf8");
|
|
1675
|
+
return parseEnvSourceContent(content);
|
|
1676
|
+
} catch (error_) {
|
|
1677
|
+
if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
|
|
1678
|
+
return {};
|
|
1679
|
+
}
|
|
1680
|
+
throw error_;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
function toJsonString(report, mode) {
|
|
1684
|
+
if (mode === "pretty") return `${JSON.stringify(report, null, 2)}
|
|
1685
|
+
`;
|
|
1686
|
+
return `${JSON.stringify(report)}
|
|
1687
|
+
`;
|
|
1688
|
+
}
|
|
1689
|
+
function formatIssue(issue) {
|
|
1690
|
+
const expected = issue.expected === void 0 ? "" : ` expected=${issue.expected.type}`;
|
|
1691
|
+
const received = issue.receivedType === void 0 ? "" : ` received=${issue.receivedType}`;
|
|
1692
|
+
return `${issue.severity.toUpperCase()} [${issue.code}] ${issue.environment}:${issue.key} ${issue.message}${expected}${received}`;
|
|
1693
|
+
}
|
|
1694
|
+
function formatHumanReport(report) {
|
|
1695
|
+
const lines = [];
|
|
1696
|
+
lines.push(
|
|
1697
|
+
`Status: ${report.status.toUpperCase()} (errors=${report.summary.errors}, warnings=${report.summary.warnings}, total=${report.summary.total})`
|
|
1698
|
+
);
|
|
1699
|
+
if (report.issues.length > 0) {
|
|
1700
|
+
lines.push("", "Issues:");
|
|
1701
|
+
for (const issue of report.issues) {
|
|
1702
|
+
lines.push(`- ${formatIssue(issue)}`);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
if (report.recommendations !== void 0 && report.recommendations.length > 0) {
|
|
1706
|
+
lines.push("", "Recommendations:");
|
|
1707
|
+
for (const recommendation of report.recommendations) {
|
|
1708
|
+
lines.push(`- ${recommendation}`);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
return `${lines.join("\n")}
|
|
1712
|
+
`;
|
|
1713
|
+
}
|
|
1714
|
+
async function persistJsonOutput(outputFile, report) {
|
|
1715
|
+
const resolvedPath = path6.resolve(outputFile);
|
|
1716
|
+
await mkdir(path6.dirname(resolvedPath), { recursive: true });
|
|
1717
|
+
await writeFile(resolvedPath, `${JSON.stringify(report, null, 2)}
|
|
1718
|
+
`, "utf8");
|
|
1719
|
+
}
|
|
1720
|
+
async function emitValidationReport(options) {
|
|
1721
|
+
const { report, outputFile, jsonMode } = options;
|
|
1722
|
+
if (outputFile !== void 0) {
|
|
1723
|
+
await persistJsonOutput(outputFile, report);
|
|
1724
|
+
}
|
|
1725
|
+
if (jsonMode === "off") {
|
|
1726
|
+
process.stdout.write(formatHumanReport(report));
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
process.stdout.write(toJsonString(report, jsonMode));
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// src/validation-command.ts
|
|
1733
|
+
var HELP_TEXT = {
|
|
1734
|
+
check: [
|
|
1735
|
+
"Usage: env-typegen check [options]",
|
|
1736
|
+
"",
|
|
1737
|
+
"Options:",
|
|
1738
|
+
" --env <path> Environment file to validate (default: .env)",
|
|
1739
|
+
" --contract <path> Contract file path (default: env.contract.ts)",
|
|
1740
|
+
" --example <path> Fallback .env.example used to bootstrap contract",
|
|
1741
|
+
" --strict Validate extras as errors (default: true)",
|
|
1742
|
+
" --no-strict Downgrade extras to warnings",
|
|
1743
|
+
" --json Emit machine-readable JSON report",
|
|
1744
|
+
" --json=pretty Emit pretty JSON report",
|
|
1745
|
+
" --output-file <path> Persist JSON report to a file",
|
|
1746
|
+
" --debug-values Include raw values in issues (unsafe for CI logs)",
|
|
1747
|
+
" --cloud-provider <name> vercel | cloudflare | aws",
|
|
1748
|
+
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1749
|
+
" --plugin <path> Plugin module path (repeatable)",
|
|
1750
|
+
" -c, --config <path> Config file path",
|
|
1751
|
+
" -h, --help Show this help",
|
|
1752
|
+
"",
|
|
1753
|
+
"Exit codes:",
|
|
1754
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1755
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1756
|
+
].join("\n"),
|
|
1757
|
+
diff: [
|
|
1758
|
+
"Usage: env-typegen diff [options]",
|
|
1759
|
+
"",
|
|
1760
|
+
"Options:",
|
|
1761
|
+
" --targets <list> Comma-separated targets (default: .env,.env.example,.env.production)",
|
|
1762
|
+
" --contract <path> Contract file path (default: env.contract.ts)",
|
|
1763
|
+
" --example <path> Fallback .env.example used to bootstrap contract",
|
|
1764
|
+
" --strict Validate extras as errors (default: true)",
|
|
1765
|
+
" --no-strict Downgrade extras to warnings",
|
|
1766
|
+
" --json Emit machine-readable JSON report",
|
|
1767
|
+
" --json=pretty Emit pretty JSON report",
|
|
1768
|
+
" --output-file <path> Persist JSON report to a file",
|
|
1769
|
+
" --debug-values Include raw values in issues (unsafe for CI logs)",
|
|
1770
|
+
" --cloud-provider <name> vercel | cloudflare | aws",
|
|
1771
|
+
" --cloud-file <path> Cloud snapshot JSON file added to diff sources",
|
|
1772
|
+
" --plugin <path> Plugin module path (repeatable)",
|
|
1773
|
+
" -c, --config <path> Config file path",
|
|
1774
|
+
" -h, --help Show this help",
|
|
1775
|
+
"",
|
|
1776
|
+
"Exit codes:",
|
|
1777
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1778
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1779
|
+
].join("\n"),
|
|
1780
|
+
doctor: [
|
|
1781
|
+
"Usage: env-typegen doctor [options]",
|
|
1782
|
+
"",
|
|
1783
|
+
"Options:",
|
|
1784
|
+
" --env <path> Environment file to validate (default: .env)",
|
|
1785
|
+
" --targets <list> Comma-separated targets for drift analysis",
|
|
1786
|
+
" --contract <path> Contract file path (default: env.contract.ts)",
|
|
1787
|
+
" --example <path> Fallback .env.example used to bootstrap contract",
|
|
1788
|
+
" --strict Validate extras as errors (default: true)",
|
|
1789
|
+
" --no-strict Downgrade extras to warnings",
|
|
1790
|
+
" --json Emit machine-readable JSON report",
|
|
1791
|
+
" --json=pretty Emit pretty JSON report",
|
|
1792
|
+
" --output-file <path> Persist JSON report to a file",
|
|
1793
|
+
" --debug-values Include raw values in issues (unsafe for CI logs)",
|
|
1794
|
+
" --cloud-provider <name> vercel | cloudflare | aws",
|
|
1795
|
+
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1796
|
+
" --plugin <path> Plugin module path (repeatable)",
|
|
1797
|
+
" -c, --config <path> Config file path",
|
|
1798
|
+
" -h, --help Show this help",
|
|
1799
|
+
"",
|
|
1800
|
+
"Exit codes:",
|
|
1801
|
+
" 0 All checks passed (status: ok or warn)",
|
|
1802
|
+
" 1 One or more checks failed (status: fail) or invalid usage"
|
|
1803
|
+
].join("\n")
|
|
1804
|
+
};
|
|
1805
|
+
function resolveConfigRelative(value, configDir) {
|
|
1806
|
+
if (value === void 0 || path6.isAbsolute(value)) return value;
|
|
1807
|
+
return path6.resolve(configDir, value);
|
|
1808
|
+
}
|
|
1809
|
+
function resolvePluginReference(reference, configDir) {
|
|
1810
|
+
if (typeof reference === "string" && !path6.isAbsolute(reference)) {
|
|
1811
|
+
return path6.resolve(configDir, reference);
|
|
1812
|
+
}
|
|
1813
|
+
return reference;
|
|
1814
|
+
}
|
|
1815
|
+
function applyConfigPaths(config, configDir) {
|
|
1816
|
+
let input;
|
|
1817
|
+
if (Array.isArray(config.input)) {
|
|
1818
|
+
input = config.input.map((item) => resolveConfigRelative(item, configDir) ?? item);
|
|
1819
|
+
} else {
|
|
1820
|
+
input = resolveConfigRelative(config.input, configDir);
|
|
1821
|
+
}
|
|
1822
|
+
const output = resolveConfigRelative(config.output, configDir);
|
|
1823
|
+
const schemaFile = resolveConfigRelative(config.schemaFile, configDir);
|
|
1824
|
+
return {
|
|
1825
|
+
...config,
|
|
1826
|
+
...input !== void 0 && { input },
|
|
1827
|
+
...output !== void 0 && { output },
|
|
1828
|
+
...schemaFile !== void 0 && { schemaFile },
|
|
1829
|
+
...config.diffTargets !== void 0 && {
|
|
1830
|
+
diffTargets: config.diffTargets.map(
|
|
1831
|
+
(target) => resolveConfigRelative(target, configDir) ?? target
|
|
1832
|
+
)
|
|
1833
|
+
},
|
|
1834
|
+
...config.plugins !== void 0 && {
|
|
1835
|
+
plugins: config.plugins.map((reference) => resolvePluginReference(reference, configDir))
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
async function loadCommandConfig(configPath) {
|
|
1840
|
+
if (configPath === void 0) {
|
|
1841
|
+
return loadConfig(process.cwd());
|
|
1842
|
+
}
|
|
1843
|
+
const resolvedPath = path6.resolve(configPath);
|
|
1844
|
+
const configDir = path6.dirname(resolvedPath);
|
|
1845
|
+
const moduleValue = await import(pathToFileURL(resolvedPath).href);
|
|
1846
|
+
if (moduleValue.default === void 0) return void 0;
|
|
1847
|
+
return applyConfigPaths(moduleValue.default, configDir);
|
|
1848
|
+
}
|
|
1849
|
+
function preprocessJsonArguments(argv) {
|
|
1850
|
+
const normalizedArgs = [];
|
|
1851
|
+
let assignedMode = "off";
|
|
1852
|
+
for (const item of argv) {
|
|
1853
|
+
if (item === "--json=pretty") {
|
|
1854
|
+
normalizedArgs.push("--json");
|
|
1855
|
+
assignedMode = "pretty";
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
if (item === "--json=compact") {
|
|
1859
|
+
normalizedArgs.push("--json");
|
|
1860
|
+
assignedMode = "compact";
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
normalizedArgs.push(item);
|
|
1864
|
+
}
|
|
1865
|
+
return { normalizedArgs, assignedMode };
|
|
1866
|
+
}
|
|
1867
|
+
function parseValidationArgs(argv) {
|
|
1868
|
+
const { normalizedArgs, assignedMode } = preprocessJsonArguments(argv);
|
|
1869
|
+
const { values } = parseArgs({
|
|
1870
|
+
args: normalizedArgs,
|
|
1871
|
+
options: {
|
|
1872
|
+
env: { type: "string", multiple: true },
|
|
1873
|
+
targets: { type: "string" },
|
|
1874
|
+
contract: { type: "string" },
|
|
1875
|
+
example: { type: "string" },
|
|
1876
|
+
strict: { type: "boolean" },
|
|
1877
|
+
"no-strict": { type: "boolean" },
|
|
1878
|
+
json: { type: "boolean" },
|
|
1879
|
+
"output-file": { type: "string" },
|
|
1880
|
+
"debug-values": { type: "boolean" },
|
|
1881
|
+
"cloud-provider": { type: "string" },
|
|
1882
|
+
"cloud-file": { type: "string" },
|
|
1883
|
+
plugin: { type: "string", multiple: true },
|
|
1884
|
+
config: { type: "string", short: "c" },
|
|
1885
|
+
help: { type: "boolean", short: "h" }
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
const castValues = values;
|
|
1889
|
+
let jsonMode = "off";
|
|
1890
|
+
if (castValues.json === true) {
|
|
1891
|
+
jsonMode = assignedMode === "off" ? "compact" : assignedMode;
|
|
1892
|
+
}
|
|
1893
|
+
return { values: castValues, jsonMode };
|
|
1894
|
+
}
|
|
1895
|
+
function resolveStrict(values, fileConfig) {
|
|
1896
|
+
if (values["no-strict"] === true) return false;
|
|
1897
|
+
if (values.strict !== void 0) return values.strict;
|
|
1898
|
+
if (fileConfig?.strict !== void 0) return fileConfig.strict;
|
|
1899
|
+
return true;
|
|
1900
|
+
}
|
|
1901
|
+
function parseCloudProvider(value) {
|
|
1902
|
+
if (value === void 0) return void 0;
|
|
1903
|
+
if (value === "vercel" || value === "cloudflare" || value === "aws") return value;
|
|
1904
|
+
throw new Error(`Unknown cloud provider: ${value}. Valid: vercel, cloudflare, aws`);
|
|
1905
|
+
}
|
|
1906
|
+
function parseTargets(values, fileConfig) {
|
|
1907
|
+
const fromCli = values.targets;
|
|
1908
|
+
if (fromCli !== void 0) {
|
|
1909
|
+
return fromCli.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
1910
|
+
}
|
|
1911
|
+
if (fileConfig?.diffTargets !== void 0 && fileConfig.diffTargets.length > 0) {
|
|
1912
|
+
return fileConfig.diffTargets;
|
|
1913
|
+
}
|
|
1914
|
+
return [".env", ".env.example", ".env.production"];
|
|
1915
|
+
}
|
|
1916
|
+
async function prepareCommonContext(values) {
|
|
1917
|
+
const fileConfig = await loadCommandConfig(values.config);
|
|
1918
|
+
const strict = resolveStrict(values, fileConfig);
|
|
1919
|
+
const debugValues = values["debug-values"] ?? false;
|
|
1920
|
+
const contractPath = values.contract ?? fileConfig?.schemaFile;
|
|
1921
|
+
const fallbackExamplePath = values.example ?? ".env.example";
|
|
1922
|
+
const cloudProvider = parseCloudProvider(values["cloud-provider"]);
|
|
1923
|
+
const cloudFile = values["cloud-file"];
|
|
1924
|
+
const pluginLoadOptions = {
|
|
1925
|
+
pluginPaths: values.plugin ?? [],
|
|
1926
|
+
cwd: process.cwd(),
|
|
1927
|
+
...fileConfig?.plugins !== void 0 && { configPlugins: fileConfig.plugins }
|
|
1928
|
+
};
|
|
1929
|
+
const plugins = await loadPlugins(pluginLoadOptions);
|
|
1930
|
+
return {
|
|
1931
|
+
fileConfig,
|
|
1932
|
+
strict,
|
|
1933
|
+
debugValues,
|
|
1934
|
+
outputFile: values["output-file"],
|
|
1935
|
+
contractPath,
|
|
1936
|
+
fallbackExamplePath,
|
|
1937
|
+
cloudProvider,
|
|
1938
|
+
cloudFile,
|
|
1939
|
+
plugins
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
async function emitAndReturnExitCode(report, params) {
|
|
1943
|
+
const emitOptions = {
|
|
1944
|
+
report,
|
|
1945
|
+
jsonMode: params.jsonMode,
|
|
1946
|
+
...params.outputFile !== void 0 && { outputFile: params.outputFile }
|
|
1947
|
+
};
|
|
1948
|
+
await emitValidationReport(emitOptions);
|
|
1949
|
+
return report.status === "fail" ? 1 : 0;
|
|
1950
|
+
}
|
|
1951
|
+
async function runCheckCommand(args) {
|
|
1952
|
+
const context = await prepareCommonContext(args.values);
|
|
1953
|
+
const loadContractOptions = {
|
|
1954
|
+
fallbackExamplePath: context.fallbackExamplePath,
|
|
1955
|
+
cwd: process.cwd(),
|
|
1956
|
+
...context.contractPath !== void 0 && { contractPath: context.contractPath }
|
|
1957
|
+
};
|
|
1958
|
+
const contract = applyContractPlugins(
|
|
1959
|
+
await loadValidationContract(loadContractOptions),
|
|
1960
|
+
context.plugins
|
|
1961
|
+
);
|
|
1962
|
+
const provider = context.cloudProvider;
|
|
1963
|
+
let environment = args.values.env?.[0] ?? ".env";
|
|
1964
|
+
let sourceValues;
|
|
1965
|
+
if (provider === void 0) {
|
|
1966
|
+
sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
|
|
1967
|
+
} else {
|
|
1968
|
+
const cloudFile = context.cloudFile ?? `${provider}.env.json`;
|
|
1969
|
+
sourceValues = await loadCloudSource({ provider, filePath: cloudFile });
|
|
1970
|
+
environment = `cloud:${provider}`;
|
|
1971
|
+
}
|
|
1972
|
+
sourceValues = applySourcePlugins({ environment, values: sourceValues }, context.plugins);
|
|
1973
|
+
const report = applyReportPlugins(
|
|
1974
|
+
validateAgainstContract({
|
|
1975
|
+
contract,
|
|
1976
|
+
values: sourceValues,
|
|
1977
|
+
environment,
|
|
1978
|
+
strict: context.strict,
|
|
1979
|
+
debugValues: context.debugValues
|
|
1980
|
+
}),
|
|
1981
|
+
context.plugins
|
|
1982
|
+
);
|
|
1983
|
+
return emitAndReturnExitCode(report, {
|
|
1984
|
+
jsonMode: args.jsonMode,
|
|
1985
|
+
...context.outputFile !== void 0 && { outputFile: context.outputFile }
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
async function runDiffCommand(args) {
|
|
1989
|
+
const context = await prepareCommonContext(args.values);
|
|
1990
|
+
const loadContractOptions = {
|
|
1991
|
+
fallbackExamplePath: context.fallbackExamplePath,
|
|
1992
|
+
cwd: process.cwd(),
|
|
1993
|
+
...context.contractPath !== void 0 && { contractPath: context.contractPath }
|
|
1994
|
+
};
|
|
1995
|
+
const contract = applyContractPlugins(
|
|
1996
|
+
await loadValidationContract(loadContractOptions),
|
|
1997
|
+
context.plugins
|
|
1998
|
+
);
|
|
1999
|
+
const sources = {};
|
|
2000
|
+
for (const target of parseTargets(args.values, context.fileConfig)) {
|
|
2001
|
+
const values = await loadEnvSource({ filePath: target, allowMissing: true });
|
|
2002
|
+
sources[target] = applySourcePlugins({ environment: target, values }, context.plugins);
|
|
2003
|
+
}
|
|
2004
|
+
if (context.cloudProvider !== void 0) {
|
|
2005
|
+
const cloudFile = context.cloudFile ?? `${context.cloudProvider}.env.json`;
|
|
2006
|
+
const cloudEnvironment = `cloud:${context.cloudProvider}`;
|
|
2007
|
+
const cloudValues = await loadCloudSource({
|
|
2008
|
+
provider: context.cloudProvider,
|
|
2009
|
+
filePath: cloudFile
|
|
2010
|
+
});
|
|
2011
|
+
sources[cloudEnvironment] = applySourcePlugins(
|
|
2012
|
+
{ environment: cloudEnvironment, values: cloudValues },
|
|
2013
|
+
context.plugins
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
const report = applyReportPlugins(
|
|
2017
|
+
diffEnvironmentSources({
|
|
2018
|
+
contract,
|
|
2019
|
+
sources,
|
|
2020
|
+
strict: context.strict,
|
|
2021
|
+
debugValues: context.debugValues
|
|
2022
|
+
}),
|
|
2023
|
+
context.plugins
|
|
2024
|
+
);
|
|
2025
|
+
return emitAndReturnExitCode(report, {
|
|
2026
|
+
jsonMode: args.jsonMode,
|
|
2027
|
+
...context.outputFile !== void 0 && { outputFile: context.outputFile }
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
async function runDoctorCommand(args) {
|
|
2031
|
+
const context = await prepareCommonContext(args.values);
|
|
2032
|
+
const loadContractOptions = {
|
|
2033
|
+
fallbackExamplePath: context.fallbackExamplePath,
|
|
2034
|
+
cwd: process.cwd(),
|
|
2035
|
+
...context.contractPath !== void 0 && { contractPath: context.contractPath }
|
|
2036
|
+
};
|
|
2037
|
+
const contract = applyContractPlugins(
|
|
2038
|
+
await loadValidationContract(loadContractOptions),
|
|
2039
|
+
context.plugins
|
|
2040
|
+
);
|
|
2041
|
+
const checkEnvironment = args.values.env?.[0] ?? ".env";
|
|
2042
|
+
let checkValues = await loadEnvSource({ filePath: checkEnvironment, allowMissing: true });
|
|
2043
|
+
checkValues = applySourcePlugins(
|
|
2044
|
+
{ environment: checkEnvironment, values: checkValues },
|
|
2045
|
+
context.plugins
|
|
2046
|
+
);
|
|
2047
|
+
const checkReport = validateAgainstContract({
|
|
2048
|
+
contract,
|
|
2049
|
+
values: checkValues,
|
|
2050
|
+
environment: checkEnvironment,
|
|
2051
|
+
strict: context.strict,
|
|
2052
|
+
debugValues: context.debugValues
|
|
2053
|
+
});
|
|
2054
|
+
const sources = {};
|
|
2055
|
+
for (const target of parseTargets(args.values, context.fileConfig)) {
|
|
2056
|
+
const values = await loadEnvSource({ filePath: target, allowMissing: true });
|
|
2057
|
+
sources[target] = applySourcePlugins({ environment: target, values }, context.plugins);
|
|
2058
|
+
}
|
|
2059
|
+
if (context.cloudProvider !== void 0) {
|
|
2060
|
+
const cloudFile = context.cloudFile ?? `${context.cloudProvider}.env.json`;
|
|
2061
|
+
const cloudEnvironment = `cloud:${context.cloudProvider}`;
|
|
2062
|
+
const cloudValues = await loadCloudSource({
|
|
2063
|
+
provider: context.cloudProvider,
|
|
2064
|
+
filePath: cloudFile
|
|
2065
|
+
});
|
|
2066
|
+
sources[cloudEnvironment] = applySourcePlugins(
|
|
2067
|
+
{ environment: cloudEnvironment, values: cloudValues },
|
|
2068
|
+
context.plugins
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
const diffReport = diffEnvironmentSources({
|
|
2072
|
+
contract,
|
|
2073
|
+
sources,
|
|
2074
|
+
strict: context.strict,
|
|
2075
|
+
debugValues: context.debugValues
|
|
2076
|
+
});
|
|
2077
|
+
const report = applyReportPlugins(
|
|
2078
|
+
buildDoctorReport({ checkReport, diffReport }),
|
|
2079
|
+
context.plugins
|
|
2080
|
+
);
|
|
2081
|
+
return emitAndReturnExitCode(report, {
|
|
2082
|
+
jsonMode: args.jsonMode,
|
|
2083
|
+
...context.outputFile !== void 0 && { outputFile: context.outputFile }
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
async function runValidationCommand(params) {
|
|
2087
|
+
const parsed = parseValidationArgs(params.argv);
|
|
2088
|
+
if (parsed.values.help === true) {
|
|
2089
|
+
console.log(HELP_TEXT[params.command]);
|
|
2090
|
+
return 0;
|
|
2091
|
+
}
|
|
2092
|
+
if (params.command === "check") {
|
|
2093
|
+
return runCheckCommand(parsed);
|
|
2094
|
+
}
|
|
2095
|
+
if (params.command === "diff") {
|
|
2096
|
+
return runDiffCommand(parsed);
|
|
2097
|
+
}
|
|
2098
|
+
return runDoctorCommand(parsed);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
export { CONTRACT_FILE_NAMES, applyContractPlugins, applyReportPlugins, applySourcePlugins, buildCiReport, defineConfig, defineContract, formatCiReport, generateDeclaration, generateEnvValidation, generateT3Env, generateTypeScriptTypes, generateZodSchema, inferType, inferTypesFromParsedVars as inferTypes, inferenceRules, loadCloudSource, loadConfig, loadContract, loadPlugins, parseCommentBlock, parseEnvFile, parseEnvFileContent, runCheck, runGenerate, runValidationCommand, validateContract };
|
|
549
2102
|
//# sourceMappingURL=index.js.map
|
|
550
2103
|
//# sourceMappingURL=index.js.map
|