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