@xlameiro/env-typegen 0.1.1 → 0.1.3
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 +46 -2
- package/README.md +118 -20
- package/dist/cli.js +1352 -89
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1563 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +560 -6
- package/dist/index.d.ts +560 -6
- package/dist/index.js +1551 -53
- package/dist/index.js.map +1 -1
- package/package.json +19 -23
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
|
|
|
@@ -136,7 +182,9 @@ var inferenceRules = [
|
|
|
136
182
|
// src/inferrer/type-inferrer.ts
|
|
137
183
|
var sortedRules = [...inferenceRules].sort((left, right) => left.priority - right.priority);
|
|
138
184
|
function inferType(key, value, options) {
|
|
139
|
-
|
|
185
|
+
const extra = options?.extraRules;
|
|
186
|
+
const rules = extra && extra.length > 0 ? [...extra].sort((a, b) => a.priority - b.priority).concat(sortedRules) : sortedRules;
|
|
187
|
+
for (const rule of rules) {
|
|
140
188
|
if (rule.match(key, value)) {
|
|
141
189
|
return rule.type;
|
|
142
190
|
}
|
|
@@ -148,7 +196,7 @@ function inferTypesFromParsedVars(parsed, options) {
|
|
|
148
196
|
}
|
|
149
197
|
var ENV_VAR_RE = /^([A-Z_][A-Z0-9_]*)=(.*)$/;
|
|
150
198
|
var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(.+?)\s+[-=]{3,}\s*$/;
|
|
151
|
-
function parseEnvFileContent(content, filePath) {
|
|
199
|
+
function parseEnvFileContent(content, filePath, options) {
|
|
152
200
|
const lines = content.split("\n");
|
|
153
201
|
const vars = [];
|
|
154
202
|
const groups = [];
|
|
@@ -181,7 +229,11 @@ function parseEnvFileContent(content, filePath) {
|
|
|
181
229
|
const key = envMatch[1] ?? "";
|
|
182
230
|
const rawValue = envMatch[2] ?? "";
|
|
183
231
|
const annotations = parseCommentBlock(commentBlock);
|
|
184
|
-
const inferredType = inferType(
|
|
232
|
+
const inferredType = inferType(
|
|
233
|
+
key,
|
|
234
|
+
rawValue,
|
|
235
|
+
...options?.inferenceRules !== void 0 ? [{ extraRules: options.inferenceRules }] : []
|
|
236
|
+
);
|
|
185
237
|
const isRequired = rawValue.length > 0 || annotations.isRequired;
|
|
186
238
|
const isOptional = rawValue.length === 0 && !annotations.isRequired;
|
|
187
239
|
const isClientSide = key.startsWith("NEXT_PUBLIC_");
|
|
@@ -203,6 +255,18 @@ function parseEnvFileContent(content, filePath) {
|
|
|
203
255
|
if (currentGroup !== void 0) {
|
|
204
256
|
parsedVar.group = currentGroup;
|
|
205
257
|
}
|
|
258
|
+
if (annotations.enumValues !== void 0) {
|
|
259
|
+
parsedVar.enumValues = annotations.enumValues;
|
|
260
|
+
}
|
|
261
|
+
if (annotations.constraints !== void 0) {
|
|
262
|
+
parsedVar.constraints = annotations.constraints;
|
|
263
|
+
}
|
|
264
|
+
if (annotations.runtime !== void 0) {
|
|
265
|
+
parsedVar.runtime = annotations.runtime;
|
|
266
|
+
}
|
|
267
|
+
if (annotations.isSecret !== void 0) {
|
|
268
|
+
parsedVar.isSecret = annotations.isSecret;
|
|
269
|
+
}
|
|
206
270
|
vars.push(parsedVar);
|
|
207
271
|
commentBlock = [];
|
|
208
272
|
} else {
|
|
@@ -223,7 +287,7 @@ function toTsType(envVarType) {
|
|
|
223
287
|
function generateTypeScriptTypes(parsed) {
|
|
224
288
|
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
225
289
|
const hasClientVars = clientVars.length > 0;
|
|
226
|
-
const fileName =
|
|
290
|
+
const fileName = path6.basename(parsed.filePath);
|
|
227
291
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
228
292
|
const lines = [];
|
|
229
293
|
lines.push("// Generated by env-typegen \u2014 do not edit manually");
|
|
@@ -322,7 +386,7 @@ function generateZodSchema(parsed) {
|
|
|
322
386
|
return lines.join("\n") + "\n";
|
|
323
387
|
}
|
|
324
388
|
function generateDeclaration(parsed) {
|
|
325
|
-
const fileName =
|
|
389
|
+
const fileName = path6.basename(parsed.filePath);
|
|
326
390
|
const lines = [];
|
|
327
391
|
lines.push("// Generated by env-typegen \u2014 do not edit manually");
|
|
328
392
|
lines.push(`// Source: ${fileName}`);
|
|
@@ -371,7 +435,7 @@ function generateT3Env(parsed) {
|
|
|
371
435
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
372
436
|
let zodExpr = toT3ZodType(effectiveType);
|
|
373
437
|
if (variable.description !== void 0) {
|
|
374
|
-
zodExpr += `.describe("${variable.description}")`;
|
|
438
|
+
zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
|
|
375
439
|
}
|
|
376
440
|
if (variable.isOptional) {
|
|
377
441
|
zodExpr += ".optional()";
|
|
@@ -386,7 +450,7 @@ function generateT3Env(parsed) {
|
|
|
386
450
|
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
387
451
|
let zodExpr = toT3ZodType(effectiveType);
|
|
388
452
|
if (variable.description !== void 0) {
|
|
389
|
-
zodExpr += `.describe("${variable.description}")`;
|
|
453
|
+
zodExpr += `.describe("${variable.description.replace(/"/g, '\\"')}")`;
|
|
390
454
|
}
|
|
391
455
|
if (variable.isOptional) {
|
|
392
456
|
zodExpr += ".optional()";
|
|
@@ -403,6 +467,25 @@ function generateT3Env(parsed) {
|
|
|
403
467
|
lines.push("});");
|
|
404
468
|
return lines.join("\n") + "\n";
|
|
405
469
|
}
|
|
470
|
+
var CONTRACT_FILE_NAMES = [
|
|
471
|
+
"env.contract.ts",
|
|
472
|
+
"env.contract.mjs",
|
|
473
|
+
"env.contract.js"
|
|
474
|
+
];
|
|
475
|
+
function defineContract(contract) {
|
|
476
|
+
return contract;
|
|
477
|
+
}
|
|
478
|
+
async function loadContract(cwd = process.cwd()) {
|
|
479
|
+
for (const name of CONTRACT_FILE_NAMES) {
|
|
480
|
+
const filePath = path6.resolve(cwd, name);
|
|
481
|
+
if (existsSync(filePath)) {
|
|
482
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
483
|
+
const mod = await import(fileUrl);
|
|
484
|
+
return mod.default;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return void 0;
|
|
488
|
+
}
|
|
406
489
|
var CONFIG_FILE_NAMES = [
|
|
407
490
|
"env-typegen.config.ts",
|
|
408
491
|
"env-typegen.config.mjs",
|
|
@@ -413,7 +496,7 @@ function defineConfig(config) {
|
|
|
413
496
|
}
|
|
414
497
|
async function loadConfig(cwd = process.cwd()) {
|
|
415
498
|
for (const name of CONFIG_FILE_NAMES) {
|
|
416
|
-
const filePath =
|
|
499
|
+
const filePath = path6.resolve(cwd, name);
|
|
417
500
|
if (existsSync(filePath)) {
|
|
418
501
|
const fileUrl = pathToFileURL(filePath).href;
|
|
419
502
|
const mod = await import(fileUrl);
|
|
@@ -423,20 +506,33 @@ async function loadConfig(cwd = process.cwd()) {
|
|
|
423
506
|
return void 0;
|
|
424
507
|
}
|
|
425
508
|
async function readEnvFile(filePath) {
|
|
426
|
-
return readFile(
|
|
509
|
+
return readFile(path6.resolve(filePath), "utf8");
|
|
427
510
|
}
|
|
428
511
|
async function writeOutput(filePath, content) {
|
|
429
|
-
const resolved =
|
|
430
|
-
await mkdir(
|
|
512
|
+
const resolved = path6.resolve(filePath);
|
|
513
|
+
await mkdir(path6.dirname(resolved), { recursive: true });
|
|
431
514
|
await writeFile(resolved, content, "utf8");
|
|
432
515
|
}
|
|
433
516
|
async function formatOutput(content, parser = "typescript") {
|
|
434
517
|
try {
|
|
435
518
|
return await format(content, { parser });
|
|
436
|
-
} catch {
|
|
519
|
+
} catch (err) {
|
|
520
|
+
console.warn(
|
|
521
|
+
"env-typegen: Prettier formatting failed, writing unformatted output.",
|
|
522
|
+
err instanceof Error ? err.message : String(err)
|
|
523
|
+
);
|
|
437
524
|
return content;
|
|
438
525
|
}
|
|
439
526
|
}
|
|
527
|
+
function log(message) {
|
|
528
|
+
console.log(message);
|
|
529
|
+
}
|
|
530
|
+
function warn(message) {
|
|
531
|
+
console.warn(yellow(`\u26A0 ${message}`));
|
|
532
|
+
}
|
|
533
|
+
function error(message) {
|
|
534
|
+
console.error(red(`\u2716 ${message}`));
|
|
535
|
+
}
|
|
440
536
|
function success(message) {
|
|
441
537
|
console.log(green(`\u2714 ${message}`));
|
|
442
538
|
}
|
|
@@ -444,12 +540,18 @@ function success(message) {
|
|
|
444
540
|
// src/pipeline.ts
|
|
445
541
|
function deriveOutputPath(base, generator, isSingle) {
|
|
446
542
|
if (isSingle) return base;
|
|
447
|
-
const ext =
|
|
543
|
+
const ext = path6.extname(base);
|
|
448
544
|
const noExt = ext.length > 0 ? base.slice(0, -ext.length) : base;
|
|
449
545
|
const baseExt = ext.length > 0 ? ext : ".ts";
|
|
450
546
|
const outExt = generator === "declaration" ? ".d.ts" : baseExt;
|
|
451
547
|
return `${noExt}.${generator}${outExt}`;
|
|
452
548
|
}
|
|
549
|
+
function deriveOutputBaseForInput(output, inputPath) {
|
|
550
|
+
const dir = path6.dirname(output);
|
|
551
|
+
const ext = path6.extname(output);
|
|
552
|
+
const stem = path6.basename(inputPath, path6.extname(inputPath));
|
|
553
|
+
return path6.join(dir, `${stem}${ext}`);
|
|
554
|
+
}
|
|
453
555
|
function buildOutput(generator, parsed) {
|
|
454
556
|
switch (generator) {
|
|
455
557
|
case "typescript":
|
|
@@ -475,7 +577,11 @@ async function persistOutput(params) {
|
|
|
475
577
|
}
|
|
476
578
|
if (dryRun) {
|
|
477
579
|
if (!silent) {
|
|
478
|
-
|
|
580
|
+
if (!isSingle) {
|
|
581
|
+
console.log(`// --- ${generator}: ${outputPath} ---`);
|
|
582
|
+
}
|
|
583
|
+
console.log(generated);
|
|
584
|
+
success(`Dry run: would write ${outputPath}`);
|
|
479
585
|
}
|
|
480
586
|
return;
|
|
481
587
|
}
|
|
@@ -492,29 +598,1421 @@ async function runGenerate(options) {
|
|
|
492
598
|
format: shouldFormat,
|
|
493
599
|
stdout = false,
|
|
494
600
|
dryRun = false,
|
|
495
|
-
silent = false
|
|
601
|
+
silent = false,
|
|
602
|
+
inferenceRules: inferenceRules2
|
|
496
603
|
} = options;
|
|
604
|
+
const inputs = Array.isArray(input) ? input : [input];
|
|
605
|
+
const hasMultipleInputs = inputs.length > 1;
|
|
497
606
|
const isSingle = generators.length === 1;
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
generated,
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
607
|
+
for (const inputPath of inputs) {
|
|
608
|
+
const outputBase = hasMultipleInputs ? deriveOutputBaseForInput(output, inputPath) : output;
|
|
609
|
+
const content = await readEnvFile(inputPath);
|
|
610
|
+
const parsed = parseEnvFileContent(
|
|
611
|
+
content,
|
|
612
|
+
inputPath,
|
|
613
|
+
inferenceRules2 !== void 0 ? { inferenceRules: inferenceRules2 } : void 0
|
|
614
|
+
);
|
|
615
|
+
for (const generator of generators) {
|
|
616
|
+
let generated = buildOutput(generator, parsed);
|
|
617
|
+
if (shouldFormat && !dryRun) {
|
|
618
|
+
generated = await formatOutput(generated);
|
|
619
|
+
}
|
|
620
|
+
const outputPath = deriveOutputPath(outputBase, generator, isSingle);
|
|
621
|
+
await persistOutput({
|
|
622
|
+
generated,
|
|
623
|
+
generator,
|
|
624
|
+
outputPath,
|
|
625
|
+
isSingle,
|
|
626
|
+
stdout,
|
|
627
|
+
dryRun,
|
|
628
|
+
silent
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/validator/contract-validator.ts
|
|
635
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
|
|
636
|
+
function buildExpected(entry) {
|
|
637
|
+
if (entry.enumValues !== void 0 && entry.enumValues.length > 0) {
|
|
638
|
+
return { type: "enum", values: entry.enumValues };
|
|
639
|
+
}
|
|
640
|
+
switch (entry.expectedType) {
|
|
641
|
+
case "number":
|
|
642
|
+
return {
|
|
643
|
+
type: "number",
|
|
644
|
+
...entry.constraints?.min !== void 0 && { min: entry.constraints.min },
|
|
645
|
+
...entry.constraints?.max !== void 0 && { max: entry.constraints.max }
|
|
646
|
+
};
|
|
647
|
+
case "boolean":
|
|
648
|
+
return { type: "boolean" };
|
|
649
|
+
case "url":
|
|
650
|
+
return { type: "url" };
|
|
651
|
+
case "email":
|
|
652
|
+
return { type: "email" };
|
|
653
|
+
case "json":
|
|
654
|
+
return { type: "json" };
|
|
655
|
+
case "semver":
|
|
656
|
+
return { type: "semver" };
|
|
657
|
+
case "unknown":
|
|
658
|
+
return { type: "unknown" };
|
|
659
|
+
default:
|
|
660
|
+
return { type: "string" };
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function checkSecretExposed(variable, entry, environment) {
|
|
664
|
+
if (entry.isSecret !== true) return void 0;
|
|
665
|
+
if (variable.rawValue === "") return void 0;
|
|
666
|
+
return {
|
|
667
|
+
code: "ENV_SECRET_EXPOSED",
|
|
668
|
+
key: variable.key,
|
|
669
|
+
expected: buildExpected(entry),
|
|
670
|
+
environment,
|
|
671
|
+
severity: "error"
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function checkInvalidType(variable, entry, environment) {
|
|
675
|
+
const value = variable.rawValue;
|
|
676
|
+
const expected = buildExpected(entry);
|
|
677
|
+
switch (entry.expectedType) {
|
|
678
|
+
case "number": {
|
|
679
|
+
if (!Number.isFinite(Number(value))) {
|
|
680
|
+
return {
|
|
681
|
+
code: "ENV_INVALID_TYPE",
|
|
682
|
+
key: variable.key,
|
|
683
|
+
expected,
|
|
684
|
+
environment,
|
|
685
|
+
severity: "error"
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
case "boolean": {
|
|
691
|
+
if (value !== "true" && value !== "false") {
|
|
692
|
+
return {
|
|
693
|
+
code: "ENV_INVALID_TYPE",
|
|
694
|
+
key: variable.key,
|
|
695
|
+
expected,
|
|
696
|
+
environment,
|
|
697
|
+
severity: "error"
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
case "url": {
|
|
703
|
+
try {
|
|
704
|
+
new URL(value);
|
|
705
|
+
} catch {
|
|
706
|
+
return {
|
|
707
|
+
code: "ENV_INVALID_TYPE",
|
|
708
|
+
key: variable.key,
|
|
709
|
+
expected,
|
|
710
|
+
environment,
|
|
711
|
+
severity: "error"
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
case "email": {
|
|
717
|
+
if (!value.includes("@") || !value.includes(".")) {
|
|
718
|
+
return {
|
|
719
|
+
code: "ENV_INVALID_TYPE",
|
|
720
|
+
key: variable.key,
|
|
721
|
+
expected,
|
|
722
|
+
environment,
|
|
723
|
+
severity: "error"
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
case "semver": {
|
|
729
|
+
if (!SEMVER_RE.test(value)) {
|
|
730
|
+
return {
|
|
731
|
+
code: "ENV_INVALID_TYPE",
|
|
732
|
+
key: variable.key,
|
|
733
|
+
expected,
|
|
734
|
+
environment,
|
|
735
|
+
severity: "error"
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return void 0;
|
|
742
|
+
}
|
|
743
|
+
function checkNumericConstraints(variable, entry, environment) {
|
|
744
|
+
if (entry.expectedType !== "number" || entry.constraints === void 0) return void 0;
|
|
745
|
+
const num = Number(variable.rawValue);
|
|
746
|
+
if (!Number.isFinite(num)) return void 0;
|
|
747
|
+
const { min, max } = entry.constraints;
|
|
748
|
+
const outsideMin = min !== void 0 && num < min;
|
|
749
|
+
const outsideMax = max !== void 0 && num > max;
|
|
750
|
+
if (outsideMin || outsideMax) {
|
|
751
|
+
return {
|
|
752
|
+
code: "ENV_INVALID_VALUE",
|
|
753
|
+
key: variable.key,
|
|
754
|
+
expected: buildExpected(entry),
|
|
755
|
+
environment,
|
|
756
|
+
severity: "error"
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
return void 0;
|
|
760
|
+
}
|
|
761
|
+
function checkInvalidValue(variable, entry, environment) {
|
|
762
|
+
const value = variable.rawValue;
|
|
763
|
+
if (entry.enumValues !== void 0 && entry.enumValues.length > 0) {
|
|
764
|
+
if (!entry.enumValues.includes(value)) {
|
|
765
|
+
return {
|
|
766
|
+
code: "ENV_INVALID_VALUE",
|
|
767
|
+
key: variable.key,
|
|
768
|
+
expected: { type: "enum", values: entry.enumValues },
|
|
769
|
+
environment,
|
|
770
|
+
severity: "error"
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
return void 0;
|
|
774
|
+
}
|
|
775
|
+
return checkNumericConstraints(variable, entry, environment);
|
|
776
|
+
}
|
|
777
|
+
function checkVariable(variable, context) {
|
|
778
|
+
const { contractByName, environment, strict } = context;
|
|
779
|
+
const entry = contractByName.get(variable.key);
|
|
780
|
+
if (entry === void 0) {
|
|
781
|
+
return [
|
|
782
|
+
{
|
|
783
|
+
code: "ENV_EXTRA",
|
|
784
|
+
key: variable.key,
|
|
785
|
+
expected: { type: "string" },
|
|
786
|
+
environment,
|
|
787
|
+
severity: strict ? "error" : "warning"
|
|
788
|
+
}
|
|
789
|
+
];
|
|
790
|
+
}
|
|
791
|
+
const found = [];
|
|
792
|
+
const secretIssue = checkSecretExposed(variable, entry, environment);
|
|
793
|
+
if (secretIssue !== void 0) found.push(secretIssue);
|
|
794
|
+
if (variable.rawValue === "") return found;
|
|
795
|
+
const typeIssue = checkInvalidType(variable, entry, environment);
|
|
796
|
+
if (typeIssue !== void 0) {
|
|
797
|
+
found.push(typeIssue);
|
|
798
|
+
return found;
|
|
799
|
+
}
|
|
800
|
+
const valueIssue = checkInvalidValue(variable, entry, environment);
|
|
801
|
+
if (valueIssue !== void 0) found.push(valueIssue);
|
|
802
|
+
return found;
|
|
803
|
+
}
|
|
804
|
+
function validateContract(parsed, contract, opts = {}) {
|
|
805
|
+
const environment = opts.environment ?? "local";
|
|
806
|
+
const strict = opts.strict ?? true;
|
|
807
|
+
const issues = [];
|
|
808
|
+
const contractByName = new Map(
|
|
809
|
+
contract.vars.map((entry) => [entry.name, entry])
|
|
810
|
+
);
|
|
811
|
+
const parsedByKey = new Map(parsed.vars.map((v) => [v.key, v]));
|
|
812
|
+
for (const entry of contract.vars) {
|
|
813
|
+
if (!entry.required) continue;
|
|
814
|
+
if (parsedByKey.has(entry.name)) continue;
|
|
815
|
+
issues.push({
|
|
816
|
+
code: "ENV_MISSING",
|
|
817
|
+
key: entry.name,
|
|
818
|
+
expected: buildExpected(entry),
|
|
819
|
+
environment,
|
|
820
|
+
severity: "error"
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
const context = { contractByName, environment, strict };
|
|
824
|
+
for (const variable of parsed.vars) {
|
|
825
|
+
const perVarIssues = checkVariable(variable, context);
|
|
826
|
+
for (const issue of perVarIssues) {
|
|
827
|
+
issues.push(issue);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return { issues };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/reporting/ci-reporter.ts
|
|
834
|
+
var ISSUE_TYPE_MAP = {
|
|
835
|
+
ENV_MISSING: "Required variable is missing from the env file",
|
|
836
|
+
ENV_EXTRA: "Variable is present in the env file but not declared in the contract",
|
|
837
|
+
ENV_INVALID_TYPE: "Value cannot be coerced to the declared type",
|
|
838
|
+
ENV_INVALID_VALUE: "Value fails a constraint check (enum, min, max)",
|
|
839
|
+
ENV_CONFLICT: "Variable has inconsistent values across environments",
|
|
840
|
+
ENV_SECRET_EXPOSED: "Secret variable has a non-empty value in a public env file"
|
|
841
|
+
};
|
|
842
|
+
function toIssue(vi) {
|
|
843
|
+
const issue = {
|
|
844
|
+
code: vi.code,
|
|
845
|
+
type: ISSUE_TYPE_MAP[vi.code],
|
|
846
|
+
key: vi.key,
|
|
847
|
+
expected: vi.expected,
|
|
848
|
+
environment: vi.environment,
|
|
849
|
+
severity: vi.severity,
|
|
850
|
+
value: null
|
|
851
|
+
};
|
|
852
|
+
if (vi.received !== void 0) {
|
|
853
|
+
issue.received = vi.received;
|
|
854
|
+
}
|
|
855
|
+
return issue;
|
|
856
|
+
}
|
|
857
|
+
function buildCiReport(result, meta) {
|
|
858
|
+
const issues = result.issues.map(toIssue);
|
|
859
|
+
const errors = issues.filter((i) => i.severity === "error").length;
|
|
860
|
+
const warnings = issues.filter((i) => i.severity === "warning").length;
|
|
861
|
+
return {
|
|
862
|
+
schemaVersion: 1,
|
|
863
|
+
status: errors > 0 ? "fail" : "ok",
|
|
864
|
+
summary: { errors, warnings },
|
|
865
|
+
issues,
|
|
866
|
+
meta
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function formatCiReport(report, opts) {
|
|
870
|
+
return JSON.stringify(report, null, opts?.pretty === true ? 2 : void 0);
|
|
871
|
+
}
|
|
872
|
+
async function resolveContract(contractPath, cwd) {
|
|
873
|
+
if (contractPath !== void 0) {
|
|
874
|
+
const absPath = path6.resolve(cwd ?? process.cwd(), contractPath);
|
|
875
|
+
if (!existsSync(absPath)) {
|
|
876
|
+
throw new Error(`Contract file not found: ${absPath}`);
|
|
877
|
+
}
|
|
878
|
+
const mod = await import(pathToFileURL(absPath).href);
|
|
879
|
+
if (mod.default === void 0) {
|
|
880
|
+
throw new Error(`Contract file has no default export: ${absPath}`);
|
|
881
|
+
}
|
|
882
|
+
return mod.default;
|
|
883
|
+
}
|
|
884
|
+
const contract = await loadContract(cwd);
|
|
885
|
+
if (contract === void 0) {
|
|
886
|
+
throw new Error("No contract file found");
|
|
887
|
+
}
|
|
888
|
+
return contract;
|
|
889
|
+
}
|
|
890
|
+
function outputJson(result, input, pretty) {
|
|
891
|
+
const report = buildCiReport(result, {
|
|
892
|
+
env: input,
|
|
893
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
894
|
+
});
|
|
895
|
+
const formatOpts = {};
|
|
896
|
+
if (pretty) formatOpts.pretty = true;
|
|
897
|
+
log(formatCiReport(report, formatOpts));
|
|
898
|
+
}
|
|
899
|
+
function outputHuman(result) {
|
|
900
|
+
if (result.issues.length === 0) {
|
|
901
|
+
success("No issues found");
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
for (const issue of result.issues) {
|
|
905
|
+
const msg = `[${issue.code}] ${issue.key}`;
|
|
906
|
+
if (issue.severity === "error") {
|
|
907
|
+
error(msg);
|
|
908
|
+
} else {
|
|
909
|
+
warn(msg);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
async function runCheck(opts) {
|
|
914
|
+
const contract = await resolveContract(opts.contract, opts.cwd);
|
|
915
|
+
const parsed = parseEnvFile(opts.input);
|
|
916
|
+
const validatorOpts = {};
|
|
917
|
+
if (opts.environment !== void 0) validatorOpts.environment = opts.environment;
|
|
918
|
+
if (opts.strict !== void 0) validatorOpts.strict = opts.strict;
|
|
919
|
+
const result = validateContract(parsed, contract, validatorOpts);
|
|
920
|
+
const hasErrors = result.issues.some((issue) => issue.severity === "error");
|
|
921
|
+
const status = hasErrors ? "fail" : "ok";
|
|
922
|
+
if (!opts.silent) {
|
|
923
|
+
if (opts.json) {
|
|
924
|
+
outputJson(result, opts.input, opts.pretty);
|
|
925
|
+
} else {
|
|
926
|
+
outputHuman(result);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return status;
|
|
930
|
+
}
|
|
931
|
+
function isRecord(value) {
|
|
932
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
933
|
+
}
|
|
934
|
+
function readEntryValue(entry, keys) {
|
|
935
|
+
for (const key of keys) {
|
|
936
|
+
const value = entry[key];
|
|
937
|
+
if (typeof value === "string") return value;
|
|
938
|
+
}
|
|
939
|
+
return void 0;
|
|
940
|
+
}
|
|
941
|
+
function parseVercelPayload(value) {
|
|
942
|
+
const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.envs) ? value.envs : [];
|
|
943
|
+
const result = {};
|
|
944
|
+
for (const entry of entries) {
|
|
945
|
+
if (!isRecord(entry)) continue;
|
|
946
|
+
const key = readEntryValue(entry, ["key", "name"]);
|
|
947
|
+
if (key === void 0) continue;
|
|
948
|
+
const envValue = readEntryValue(entry, ["value", "targetValue", "content"]) ?? "";
|
|
949
|
+
result[key] = envValue;
|
|
950
|
+
}
|
|
951
|
+
return result;
|
|
952
|
+
}
|
|
953
|
+
function parseCloudflarePayload(value) {
|
|
954
|
+
const entries = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.result) ? value.result : [];
|
|
955
|
+
const result = {};
|
|
956
|
+
for (const entry of entries) {
|
|
957
|
+
if (!isRecord(entry)) continue;
|
|
958
|
+
const key = readEntryValue(entry, ["name", "key"]);
|
|
959
|
+
if (key === void 0) continue;
|
|
960
|
+
const envValue = readEntryValue(entry, ["text", "value", "secret"]) ?? "";
|
|
961
|
+
result[key] = envValue;
|
|
962
|
+
}
|
|
963
|
+
return result;
|
|
964
|
+
}
|
|
965
|
+
function parseAwsPayload(value) {
|
|
966
|
+
const entries = isRecord(value) && Array.isArray(value.Parameters) ? value.Parameters : [];
|
|
967
|
+
const result = {};
|
|
968
|
+
for (const entry of entries) {
|
|
969
|
+
if (!isRecord(entry)) continue;
|
|
970
|
+
const name = readEntryValue(entry, ["Name", "name"]);
|
|
971
|
+
if (name === void 0) continue;
|
|
972
|
+
const key = name.split("/").filter((part) => part.length > 0).pop() ?? name;
|
|
973
|
+
const envValue = readEntryValue(entry, ["Value", "value"]) ?? "";
|
|
974
|
+
result[key] = envValue;
|
|
975
|
+
}
|
|
976
|
+
return result;
|
|
977
|
+
}
|
|
978
|
+
function parseProviderPayload(provider, value) {
|
|
979
|
+
if (provider === "vercel") return parseVercelPayload(value);
|
|
980
|
+
if (provider === "cloudflare") return parseCloudflarePayload(value);
|
|
981
|
+
return parseAwsPayload(value);
|
|
982
|
+
}
|
|
983
|
+
async function loadCloudSource(options) {
|
|
984
|
+
const resolvedPath = path6.resolve(options.filePath);
|
|
985
|
+
const raw = await readFile(resolvedPath, "utf8");
|
|
986
|
+
const parsed = JSON.parse(raw);
|
|
987
|
+
return parseProviderPayload(options.provider, parsed);
|
|
988
|
+
}
|
|
989
|
+
var SECRET_KEY_RE = /(SECRET|TOKEN|PASSWORD|PRIVATE|API_KEY|ACCESS_KEY|CLIENT_SECRET)/i;
|
|
990
|
+
var CONTRACT_FILE_NAMES2 = ["env.contract.ts", "env.contract.mjs", "env.contract.js"];
|
|
991
|
+
function isRecord2(value) {
|
|
992
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
993
|
+
}
|
|
994
|
+
function isExpected(value) {
|
|
995
|
+
if (!isRecord2(value)) return false;
|
|
996
|
+
const typeValue = value.type;
|
|
997
|
+
if (typeof typeValue !== "string") return false;
|
|
998
|
+
const validTypes = /* @__PURE__ */ new Set([
|
|
999
|
+
"string",
|
|
1000
|
+
"number",
|
|
1001
|
+
"boolean",
|
|
1002
|
+
"enum",
|
|
1003
|
+
"url",
|
|
1004
|
+
"email",
|
|
1005
|
+
"json",
|
|
1006
|
+
"semver",
|
|
1007
|
+
"unknown"
|
|
1008
|
+
]);
|
|
1009
|
+
if (!validTypes.has(typeValue)) return false;
|
|
1010
|
+
if (typeValue === "enum") {
|
|
1011
|
+
return Array.isArray(value.values) && value.values.every((item) => typeof item === "string");
|
|
1012
|
+
}
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
function isEnvContractVariable(value) {
|
|
1016
|
+
if (!isRecord2(value)) return false;
|
|
1017
|
+
if (!isExpected(value.expected)) return false;
|
|
1018
|
+
if (typeof value.required !== "boolean") return false;
|
|
1019
|
+
if (typeof value.clientSide !== "boolean") return false;
|
|
1020
|
+
if (value.description !== void 0 && typeof value.description !== "string") return false;
|
|
1021
|
+
if (value.secret !== void 0 && typeof value.secret !== "boolean") return false;
|
|
1022
|
+
return true;
|
|
1023
|
+
}
|
|
1024
|
+
function isEnvContract(value) {
|
|
1025
|
+
if (!isRecord2(value)) return false;
|
|
1026
|
+
if (value.schemaVersion !== 1) return false;
|
|
1027
|
+
if (!isRecord2(value.variables)) return false;
|
|
1028
|
+
return Object.values(value.variables).every((item) => isEnvContractVariable(item));
|
|
1029
|
+
}
|
|
1030
|
+
function isLegacyContract(value) {
|
|
1031
|
+
if (!isRecord2(value)) return false;
|
|
1032
|
+
if (!Array.isArray(value.vars)) return false;
|
|
1033
|
+
return value.vars.every((entry) => isRecord2(entry) && typeof entry.name === "string");
|
|
1034
|
+
}
|
|
1035
|
+
function mapEnvVarTypeToExpected(type) {
|
|
1036
|
+
if (type === "number") return { type: "number" };
|
|
1037
|
+
if (type === "boolean") return { type: "boolean" };
|
|
1038
|
+
if (type === "url") return { type: "url" };
|
|
1039
|
+
if (type === "email") return { type: "email" };
|
|
1040
|
+
if (type === "json") return { type: "json" };
|
|
1041
|
+
if (type === "semver") return { type: "semver" };
|
|
1042
|
+
if (type === "unknown") return { type: "unknown" };
|
|
1043
|
+
return { type: "string" };
|
|
1044
|
+
}
|
|
1045
|
+
function shouldTreatAsSecret(key) {
|
|
1046
|
+
return SECRET_KEY_RE.test(key);
|
|
1047
|
+
}
|
|
1048
|
+
function toExpectedFromLegacyEntry(entry) {
|
|
1049
|
+
if (entry.enumValues !== void 0 && entry.enumValues.length > 0) {
|
|
1050
|
+
return { type: "enum", values: entry.enumValues };
|
|
1051
|
+
}
|
|
1052
|
+
if (entry.expectedType === "number") {
|
|
1053
|
+
return {
|
|
1054
|
+
type: "number",
|
|
1055
|
+
...entry.constraints?.min !== void 0 && { min: entry.constraints.min },
|
|
1056
|
+
...entry.constraints?.max !== void 0 && { max: entry.constraints.max }
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
if (entry.expectedType === "boolean") return { type: "boolean" };
|
|
1060
|
+
if (entry.expectedType === "url") return { type: "url" };
|
|
1061
|
+
if (entry.expectedType === "email") return { type: "email" };
|
|
1062
|
+
if (entry.expectedType === "json") return { type: "json" };
|
|
1063
|
+
if (entry.expectedType === "semver") return { type: "semver" };
|
|
1064
|
+
if (entry.expectedType === "unknown") return { type: "unknown" };
|
|
1065
|
+
return { type: "string" };
|
|
1066
|
+
}
|
|
1067
|
+
function convertLegacyContract(contract) {
|
|
1068
|
+
const variables = {};
|
|
1069
|
+
for (const entry of contract.vars) {
|
|
1070
|
+
const clientSide = entry.runtime === "client" || entry.name.startsWith("NEXT_PUBLIC_");
|
|
1071
|
+
variables[entry.name] = {
|
|
1072
|
+
expected: toExpectedFromLegacyEntry(entry),
|
|
1073
|
+
required: entry.required,
|
|
1074
|
+
clientSide,
|
|
1075
|
+
...entry.description !== void 0 && { description: entry.description },
|
|
1076
|
+
...(entry.isSecret ?? shouldTreatAsSecret(entry.name)) && { secret: true }
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
schemaVersion: 1,
|
|
1081
|
+
variables
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function buildContractFromExample(examplePath) {
|
|
1085
|
+
const parsed = parseEnvFile(examplePath);
|
|
1086
|
+
const variables = {};
|
|
1087
|
+
for (const variable of parsed.vars) {
|
|
1088
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
1089
|
+
variables[variable.key] = {
|
|
1090
|
+
expected: mapEnvVarTypeToExpected(effectiveType),
|
|
1091
|
+
required: variable.isRequired,
|
|
1092
|
+
clientSide: variable.isClientSide,
|
|
1093
|
+
...variable.description !== void 0 && { description: variable.description },
|
|
1094
|
+
...shouldTreatAsSecret(variable.key) && { secret: true }
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
schemaVersion: 1,
|
|
1099
|
+
variables
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function findDefaultContractPath(cwd) {
|
|
1103
|
+
for (const fileName of CONTRACT_FILE_NAMES2) {
|
|
1104
|
+
const candidatePath = path6.resolve(cwd, fileName);
|
|
1105
|
+
if (existsSync(candidatePath)) return candidatePath;
|
|
1106
|
+
}
|
|
1107
|
+
return void 0;
|
|
1108
|
+
}
|
|
1109
|
+
async function loadValidationContract(options) {
|
|
1110
|
+
const { fallbackExamplePath, contractPath, cwd = process.cwd() } = options;
|
|
1111
|
+
const discoveredContractPath = findDefaultContractPath(cwd);
|
|
1112
|
+
const resolvedContractPath = contractPath !== void 0 ? path6.resolve(cwd, contractPath) : discoveredContractPath;
|
|
1113
|
+
if (resolvedContractPath !== void 0 && existsSync(resolvedContractPath)) {
|
|
1114
|
+
const moduleUrl = pathToFileURL(resolvedContractPath).href;
|
|
1115
|
+
const moduleValue = await import(moduleUrl);
|
|
1116
|
+
const candidate = moduleValue.default ?? moduleValue.contract;
|
|
1117
|
+
if (isEnvContract(candidate)) {
|
|
1118
|
+
return candidate;
|
|
1119
|
+
}
|
|
1120
|
+
if (isLegacyContract(candidate)) {
|
|
1121
|
+
return convertLegacyContract(candidate);
|
|
1122
|
+
}
|
|
1123
|
+
throw new Error(
|
|
1124
|
+
`Invalid contract at ${resolvedContractPath}. Export default must match EnvContract.`
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
return buildContractFromExample(path6.resolve(cwd, fallbackExamplePath));
|
|
1128
|
+
}
|
|
1129
|
+
function isRecord3(value) {
|
|
1130
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1131
|
+
}
|
|
1132
|
+
function isPlugin(value) {
|
|
1133
|
+
if (!isRecord3(value)) return false;
|
|
1134
|
+
if (typeof value.name !== "string") return false;
|
|
1135
|
+
if (value.transformContract !== void 0 && typeof value.transformContract !== "function") {
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
if (value.transformSource !== void 0 && typeof value.transformSource !== "function") {
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
if (value.transformReport !== void 0 && typeof value.transformReport !== "function") {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
async function loadPluginFromPath(pluginPath, cwd) {
|
|
1147
|
+
const resolvedPath = path6.resolve(cwd, pluginPath);
|
|
1148
|
+
const moduleValue = await import(pathToFileURL(resolvedPath).href);
|
|
1149
|
+
const candidate = moduleValue.default ?? moduleValue.plugin;
|
|
1150
|
+
if (isPlugin(candidate)) return candidate;
|
|
1151
|
+
throw new Error(`Invalid plugin at ${resolvedPath}. Expected a plugin object export.`);
|
|
1152
|
+
}
|
|
1153
|
+
async function loadPlugins(options) {
|
|
1154
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1155
|
+
const plugins = [];
|
|
1156
|
+
const references = [...options.configPlugins ?? [], ...options.pluginPaths];
|
|
1157
|
+
for (const reference of references) {
|
|
1158
|
+
if (typeof reference === "string") {
|
|
1159
|
+
plugins.push(await loadPluginFromPath(reference, cwd));
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
if (isPlugin(reference)) {
|
|
1163
|
+
plugins.push(reference);
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
throw new Error("Invalid plugin reference in configuration.");
|
|
1167
|
+
}
|
|
1168
|
+
return plugins;
|
|
1169
|
+
}
|
|
1170
|
+
function applyContractPlugins(contract, plugins) {
|
|
1171
|
+
let next = contract;
|
|
1172
|
+
for (const plugin of plugins) {
|
|
1173
|
+
if (plugin.transformContract === void 0) continue;
|
|
1174
|
+
next = plugin.transformContract(next);
|
|
1175
|
+
}
|
|
1176
|
+
return next;
|
|
1177
|
+
}
|
|
1178
|
+
function applySourcePlugins(params, plugins) {
|
|
1179
|
+
let next = params.values;
|
|
1180
|
+
for (const plugin of plugins) {
|
|
1181
|
+
if (plugin.transformSource === void 0) continue;
|
|
1182
|
+
next = plugin.transformSource({ environment: params.environment, values: next });
|
|
1183
|
+
}
|
|
1184
|
+
return next;
|
|
1185
|
+
}
|
|
1186
|
+
function applyReportPlugins(report, plugins) {
|
|
1187
|
+
let next = report;
|
|
1188
|
+
for (const plugin of plugins) {
|
|
1189
|
+
if (plugin.transformReport === void 0) continue;
|
|
1190
|
+
next = plugin.transformReport(next);
|
|
1191
|
+
}
|
|
1192
|
+
return next;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// src/validation/engine.ts
|
|
1196
|
+
var EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
1197
|
+
var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
|
|
1198
|
+
function detectReceivedType(value) {
|
|
1199
|
+
const normalized = value.trim();
|
|
1200
|
+
if (normalized.length === 0) return "unknown";
|
|
1201
|
+
if (["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) return "boolean";
|
|
1202
|
+
if (!Number.isNaN(Number(normalized)) && Number.isFinite(Number(normalized))) return "number";
|
|
1203
|
+
if (SEMVER_RE2.test(normalized)) return "semver";
|
|
1204
|
+
try {
|
|
1205
|
+
const url = new URL(normalized);
|
|
1206
|
+
if (url.protocol.length > 0) return "url";
|
|
1207
|
+
} catch {
|
|
1208
|
+
}
|
|
1209
|
+
if (EMAIL_RE.test(normalized)) return "email";
|
|
1210
|
+
try {
|
|
1211
|
+
const parsed = JSON.parse(normalized);
|
|
1212
|
+
if (typeof parsed === "object" && parsed !== null) return "json";
|
|
1213
|
+
} catch {
|
|
1214
|
+
}
|
|
1215
|
+
return "string";
|
|
1216
|
+
}
|
|
1217
|
+
function validateValueAgainstExpected(expected, rawValue) {
|
|
1218
|
+
const normalized = rawValue.trim();
|
|
1219
|
+
const receivedType = detectReceivedType(normalized);
|
|
1220
|
+
if (expected.type === "unknown") return { isValid: true, receivedType };
|
|
1221
|
+
if (expected.type === "string") return { isValid: true, receivedType };
|
|
1222
|
+
if (expected.type === "number") {
|
|
1223
|
+
const parsed = Number(normalized);
|
|
1224
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
1225
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1226
|
+
}
|
|
1227
|
+
if (expected.min !== void 0 && parsed < expected.min) {
|
|
1228
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1229
|
+
}
|
|
1230
|
+
if (expected.max !== void 0 && parsed > expected.max) {
|
|
1231
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1232
|
+
}
|
|
1233
|
+
return { isValid: true, receivedType };
|
|
1234
|
+
}
|
|
1235
|
+
if (expected.type === "boolean") {
|
|
1236
|
+
if (!["true", "false", "1", "0", "yes", "no"].includes(normalized.toLowerCase())) {
|
|
1237
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1238
|
+
}
|
|
1239
|
+
return { isValid: true, receivedType };
|
|
1240
|
+
}
|
|
1241
|
+
if (expected.type === "enum") {
|
|
1242
|
+
if (!expected.values.includes(normalized)) {
|
|
1243
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1244
|
+
}
|
|
1245
|
+
return { isValid: true, receivedType };
|
|
1246
|
+
}
|
|
1247
|
+
if (expected.type === "url") {
|
|
1248
|
+
try {
|
|
1249
|
+
const value = new URL(normalized);
|
|
1250
|
+
if (value.protocol.length === 0)
|
|
1251
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1252
|
+
return { isValid: true, receivedType };
|
|
1253
|
+
} catch {
|
|
1254
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
if (expected.type === "email") {
|
|
1258
|
+
if (!EMAIL_RE.test(normalized))
|
|
1259
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1260
|
+
return { isValid: true, receivedType };
|
|
1261
|
+
}
|
|
1262
|
+
if (expected.type === "json") {
|
|
1263
|
+
try {
|
|
1264
|
+
const parsed = JSON.parse(normalized);
|
|
1265
|
+
if (typeof parsed === "object" && parsed !== null) return { isValid: true, receivedType };
|
|
1266
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1267
|
+
} catch {
|
|
1268
|
+
return { isValid: false, receivedType, issueType: "invalid_type" };
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (expected.type === "semver") {
|
|
1272
|
+
if (!SEMVER_RE2.test(normalized))
|
|
1273
|
+
return { isValid: false, receivedType, issueType: "invalid_value" };
|
|
1274
|
+
return { isValid: true, receivedType };
|
|
1275
|
+
}
|
|
1276
|
+
return { isValid: true, receivedType };
|
|
1277
|
+
}
|
|
1278
|
+
function toIssueCode(issueType) {
|
|
1279
|
+
if (issueType === "missing") return "ENV_MISSING";
|
|
1280
|
+
if (issueType === "extra") return "ENV_EXTRA";
|
|
1281
|
+
if (issueType === "invalid_type") return "ENV_INVALID_TYPE";
|
|
1282
|
+
if (issueType === "invalid_value") return "ENV_INVALID_VALUE";
|
|
1283
|
+
if (issueType === "conflict") return "ENV_CONFLICT";
|
|
1284
|
+
return "ENV_SECRET_EXPOSED";
|
|
1285
|
+
}
|
|
1286
|
+
function toIssueValue(value, debugValues) {
|
|
1287
|
+
if (!debugValues) return null;
|
|
1288
|
+
if (value === void 0) return null;
|
|
1289
|
+
return value;
|
|
1290
|
+
}
|
|
1291
|
+
function createIssue(params) {
|
|
1292
|
+
return {
|
|
1293
|
+
code: toIssueCode(params.type),
|
|
1294
|
+
type: params.type,
|
|
1295
|
+
severity: params.severity,
|
|
1296
|
+
key: params.key,
|
|
1297
|
+
environment: params.environment,
|
|
1298
|
+
message: params.message,
|
|
1299
|
+
value: toIssueValue(params.value, params.debugValues),
|
|
1300
|
+
...params.expected !== void 0 && { expected: params.expected },
|
|
1301
|
+
...params.receivedType !== void 0 && { receivedType: params.receivedType }
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
function dedupeIssues(issues) {
|
|
1305
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1306
|
+
const unique = [];
|
|
1307
|
+
for (const issue of issues) {
|
|
1308
|
+
const token = [
|
|
1309
|
+
issue.code,
|
|
1310
|
+
issue.type,
|
|
1311
|
+
issue.severity,
|
|
1312
|
+
issue.environment,
|
|
1313
|
+
issue.key,
|
|
1314
|
+
issue.message,
|
|
1315
|
+
issue.receivedType ?? ""
|
|
1316
|
+
].join("|");
|
|
1317
|
+
if (seen.has(token)) continue;
|
|
1318
|
+
seen.add(token);
|
|
1319
|
+
unique.push(issue);
|
|
1320
|
+
}
|
|
1321
|
+
return unique;
|
|
1322
|
+
}
|
|
1323
|
+
function buildReport(env, issues, recommendations) {
|
|
1324
|
+
const dedupedIssues = dedupeIssues(issues);
|
|
1325
|
+
const errors = dedupedIssues.filter((item) => item.severity === "error").length;
|
|
1326
|
+
const warnings = dedupedIssues.filter((item) => item.severity === "warning").length;
|
|
1327
|
+
const status = errors > 0 ? "fail" : "ok";
|
|
1328
|
+
return {
|
|
1329
|
+
schemaVersion: 1,
|
|
1330
|
+
status,
|
|
1331
|
+
summary: {
|
|
1332
|
+
errors,
|
|
1333
|
+
warnings,
|
|
1334
|
+
total: dedupedIssues.length
|
|
1335
|
+
},
|
|
1336
|
+
issues: dedupedIssues,
|
|
1337
|
+
meta: {
|
|
1338
|
+
env,
|
|
1339
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1340
|
+
},
|
|
1341
|
+
...recommendations !== void 0 && recommendations.length > 0 && { recommendations }
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function isClientSecret(variable, key) {
|
|
1345
|
+
return variable.secret === true && (variable.clientSide || key.startsWith("NEXT_PUBLIC_"));
|
|
1346
|
+
}
|
|
1347
|
+
function validateAgainstContract(options) {
|
|
1348
|
+
const issues = [];
|
|
1349
|
+
const contractKeys = new Set(Object.keys(options.contract.variables));
|
|
1350
|
+
for (const [key, variable] of Object.entries(options.contract.variables)) {
|
|
1351
|
+
const value = options.values[key];
|
|
1352
|
+
const hasValue = value !== void 0 && value.trim().length > 0;
|
|
1353
|
+
if (variable.required && !hasValue) {
|
|
1354
|
+
issues.push(
|
|
1355
|
+
createIssue({
|
|
1356
|
+
type: "missing",
|
|
1357
|
+
severity: "error",
|
|
1358
|
+
key,
|
|
1359
|
+
environment: options.environment,
|
|
1360
|
+
message: `Required variable ${key} is missing.`,
|
|
1361
|
+
debugValues: options.debugValues,
|
|
1362
|
+
expected: variable.expected
|
|
1363
|
+
})
|
|
1364
|
+
);
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
if (!hasValue) continue;
|
|
1368
|
+
const validation = validateValueAgainstExpected(variable.expected, value);
|
|
1369
|
+
if (!validation.isValid) {
|
|
1370
|
+
issues.push(
|
|
1371
|
+
createIssue({
|
|
1372
|
+
type: validation.issueType,
|
|
1373
|
+
severity: "error",
|
|
1374
|
+
key,
|
|
1375
|
+
environment: options.environment,
|
|
1376
|
+
message: validation.issueType === "invalid_type" ? `Variable ${key} has invalid type.` : `Variable ${key} has invalid value.`,
|
|
1377
|
+
value,
|
|
1378
|
+
debugValues: options.debugValues,
|
|
1379
|
+
expected: variable.expected,
|
|
1380
|
+
receivedType: validation.receivedType
|
|
1381
|
+
})
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
if (isClientSecret(variable, key)) {
|
|
1385
|
+
issues.push(
|
|
1386
|
+
createIssue({
|
|
1387
|
+
type: "secret_exposed",
|
|
1388
|
+
severity: "error",
|
|
1389
|
+
key,
|
|
1390
|
+
environment: options.environment,
|
|
1391
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1392
|
+
value,
|
|
1393
|
+
debugValues: options.debugValues,
|
|
1394
|
+
expected: variable.expected
|
|
1395
|
+
})
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
for (const [key, value] of Object.entries(options.values)) {
|
|
1400
|
+
if (contractKeys.has(key)) continue;
|
|
1401
|
+
const severity = options.strict ? "error" : "warning";
|
|
1402
|
+
issues.push(
|
|
1403
|
+
createIssue({
|
|
1404
|
+
type: "extra",
|
|
1405
|
+
severity,
|
|
1406
|
+
key,
|
|
1407
|
+
environment: options.environment,
|
|
1408
|
+
message: `Variable ${key} is not defined in the contract.`,
|
|
1409
|
+
value,
|
|
1410
|
+
debugValues: options.debugValues
|
|
1411
|
+
})
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
return buildReport(options.environment, issues);
|
|
1415
|
+
}
|
|
1416
|
+
function collectUnionKeys(contract, sources) {
|
|
1417
|
+
const union = new Set(Object.keys(contract.variables));
|
|
1418
|
+
for (const source of Object.values(sources)) {
|
|
1419
|
+
for (const key of Object.keys(source)) {
|
|
1420
|
+
union.add(key);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
return union;
|
|
1424
|
+
}
|
|
1425
|
+
function diffEnvironmentSources(options) {
|
|
1426
|
+
const issues = [];
|
|
1427
|
+
const sourceNames = Object.keys(options.sources);
|
|
1428
|
+
const unionKeys = collectUnionKeys(options.contract, options.sources);
|
|
1429
|
+
for (const key of unionKeys) {
|
|
1430
|
+
const variable = options.contract.variables[key];
|
|
1431
|
+
const valuesBySource = sourceNames.map((sourceName) => ({
|
|
1432
|
+
sourceName,
|
|
1433
|
+
value: options.sources[sourceName]?.[key]
|
|
1434
|
+
}));
|
|
1435
|
+
const present = valuesBySource.filter(
|
|
1436
|
+
(entry) => entry.value !== void 0 && entry.value !== ""
|
|
1437
|
+
);
|
|
1438
|
+
const missing = valuesBySource.filter(
|
|
1439
|
+
(entry) => entry.value === void 0 || entry.value === ""
|
|
1440
|
+
);
|
|
1441
|
+
if (present.length === 0 && variable?.required === true) {
|
|
1442
|
+
for (const entry of missing) {
|
|
1443
|
+
issues.push(
|
|
1444
|
+
createIssue({
|
|
1445
|
+
type: "missing",
|
|
1446
|
+
severity: "error",
|
|
1447
|
+
key,
|
|
1448
|
+
environment: entry.sourceName,
|
|
1449
|
+
message: `Required variable ${key} is missing in ${entry.sourceName}.`,
|
|
1450
|
+
debugValues: options.debugValues,
|
|
1451
|
+
expected: variable.expected
|
|
1452
|
+
})
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
if (present.length > 0) {
|
|
1458
|
+
for (const entry of missing) {
|
|
1459
|
+
issues.push(
|
|
1460
|
+
createIssue({
|
|
1461
|
+
type: "missing",
|
|
1462
|
+
severity: "error",
|
|
1463
|
+
key,
|
|
1464
|
+
environment: entry.sourceName,
|
|
1465
|
+
message: `Variable ${key} is missing in ${entry.sourceName}.`,
|
|
1466
|
+
debugValues: options.debugValues,
|
|
1467
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1468
|
+
})
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
const typeBySource = /* @__PURE__ */ new Map();
|
|
1473
|
+
for (const entry of present) {
|
|
1474
|
+
const detected = detectReceivedType(entry.value ?? "");
|
|
1475
|
+
typeBySource.set(entry.sourceName, detected);
|
|
1476
|
+
}
|
|
1477
|
+
if (new Set(typeBySource.values()).size > 1) {
|
|
1478
|
+
for (const [sourceName, detectedType] of typeBySource.entries()) {
|
|
1479
|
+
issues.push(
|
|
1480
|
+
createIssue({
|
|
1481
|
+
type: "conflict",
|
|
1482
|
+
severity: "error",
|
|
1483
|
+
key,
|
|
1484
|
+
environment: sourceName,
|
|
1485
|
+
message: `Variable ${key} has conflicting inferred type across environments.`,
|
|
1486
|
+
debugValues: options.debugValues,
|
|
1487
|
+
receivedType: detectedType,
|
|
1488
|
+
...options.sources[sourceName]?.[key] !== void 0 && {
|
|
1489
|
+
value: options.sources[sourceName]?.[key]
|
|
1490
|
+
},
|
|
1491
|
+
...variable !== void 0 && { expected: variable.expected }
|
|
1492
|
+
})
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
for (const entry of present) {
|
|
1497
|
+
if (entry.value === void 0) continue;
|
|
1498
|
+
if (variable === void 0) {
|
|
1499
|
+
const severity = options.strict ? "error" : "warning";
|
|
1500
|
+
issues.push(
|
|
1501
|
+
createIssue({
|
|
1502
|
+
type: "extra",
|
|
1503
|
+
severity,
|
|
1504
|
+
key,
|
|
1505
|
+
environment: entry.sourceName,
|
|
1506
|
+
message: `Variable ${key} is not defined in the contract.`,
|
|
1507
|
+
value: entry.value,
|
|
1508
|
+
debugValues: options.debugValues
|
|
1509
|
+
})
|
|
1510
|
+
);
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
const validation = validateValueAgainstExpected(variable.expected, entry.value);
|
|
1514
|
+
if (!validation.isValid) {
|
|
1515
|
+
issues.push(
|
|
1516
|
+
createIssue({
|
|
1517
|
+
type: validation.issueType,
|
|
1518
|
+
severity: "error",
|
|
1519
|
+
key,
|
|
1520
|
+
environment: entry.sourceName,
|
|
1521
|
+
message: validation.issueType === "invalid_type" ? `Variable ${key} has invalid type in ${entry.sourceName}.` : `Variable ${key} has invalid value in ${entry.sourceName}.`,
|
|
1522
|
+
value: entry.value,
|
|
1523
|
+
debugValues: options.debugValues,
|
|
1524
|
+
expected: variable.expected,
|
|
1525
|
+
receivedType: validation.receivedType
|
|
1526
|
+
})
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
if (isClientSecret(variable, key)) {
|
|
1530
|
+
issues.push(
|
|
1531
|
+
createIssue({
|
|
1532
|
+
type: "secret_exposed",
|
|
1533
|
+
severity: "error",
|
|
1534
|
+
key,
|
|
1535
|
+
environment: entry.sourceName,
|
|
1536
|
+
message: `Secret variable ${key} is marked as client-side.`,
|
|
1537
|
+
value: entry.value,
|
|
1538
|
+
debugValues: options.debugValues,
|
|
1539
|
+
expected: variable.expected
|
|
1540
|
+
})
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return buildReport("diff", issues);
|
|
1546
|
+
}
|
|
1547
|
+
function buildRecommendations(issues) {
|
|
1548
|
+
const codes = new Set(issues.map((item) => item.code));
|
|
1549
|
+
const recommendations = [];
|
|
1550
|
+
if (codes.has("ENV_MISSING")) {
|
|
1551
|
+
recommendations.push("Add missing required variables to each target environment.");
|
|
1552
|
+
}
|
|
1553
|
+
if (codes.has("ENV_EXTRA")) {
|
|
1554
|
+
recommendations.push(
|
|
1555
|
+
"Remove undeclared variables or add them to env.contract.ts intentionally."
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
if (codes.has("ENV_INVALID_TYPE") || codes.has("ENV_INVALID_VALUE")) {
|
|
1559
|
+
recommendations.push(
|
|
1560
|
+
"Normalize variable values so they match the expected contract types and constraints."
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
if (codes.has("ENV_CONFLICT")) {
|
|
1564
|
+
recommendations.push("Align variable semantics across environments to avoid drift.");
|
|
1565
|
+
}
|
|
1566
|
+
if (codes.has("ENV_SECRET_EXPOSED")) {
|
|
1567
|
+
recommendations.push(
|
|
1568
|
+
"Move secret variables to server-only scope and avoid NEXT_PUBLIC_ exposure for secrets."
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
return recommendations;
|
|
1572
|
+
}
|
|
1573
|
+
function buildDoctorReport(options) {
|
|
1574
|
+
const merged = [...options.checkReport.issues, ...options.diffReport.issues];
|
|
1575
|
+
const recommendations = buildRecommendations(merged);
|
|
1576
|
+
return buildReport("doctor", merged, recommendations);
|
|
1577
|
+
}
|
|
1578
|
+
function stripWrappingQuotes(value) {
|
|
1579
|
+
if (value.length < 2) return value;
|
|
1580
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1581
|
+
return value.slice(1, -1);
|
|
1582
|
+
}
|
|
1583
|
+
return value;
|
|
1584
|
+
}
|
|
1585
|
+
function parseEnvSourceContent(content) {
|
|
1586
|
+
const result = {};
|
|
1587
|
+
const lines = content.split("\n");
|
|
1588
|
+
for (const line of lines) {
|
|
1589
|
+
const trimmed = line.trim();
|
|
1590
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
1591
|
+
const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(trimmed);
|
|
1592
|
+
if (match === null) continue;
|
|
1593
|
+
const key = match[1] ?? "";
|
|
1594
|
+
const rawValue = match[2] ?? "";
|
|
1595
|
+
result[key] = stripWrappingQuotes(rawValue.trim());
|
|
1596
|
+
}
|
|
1597
|
+
return result;
|
|
1598
|
+
}
|
|
1599
|
+
async function loadEnvSource(options) {
|
|
1600
|
+
const resolvedPath = path6.resolve(options.filePath);
|
|
1601
|
+
try {
|
|
1602
|
+
const content = await readFile(resolvedPath, "utf8");
|
|
1603
|
+
return parseEnvSourceContent(content);
|
|
1604
|
+
} catch (errorValue) {
|
|
1605
|
+
if (options.allowMissing === true && errorValue instanceof Error && "code" in errorValue && errorValue.code === "ENOENT") {
|
|
1606
|
+
return {};
|
|
1607
|
+
}
|
|
1608
|
+
throw errorValue;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
function toJsonString(report, mode) {
|
|
1612
|
+
if (mode === "pretty") return `${JSON.stringify(report, null, 2)}
|
|
1613
|
+
`;
|
|
1614
|
+
return `${JSON.stringify(report)}
|
|
1615
|
+
`;
|
|
1616
|
+
}
|
|
1617
|
+
function formatIssue(issue) {
|
|
1618
|
+
const expected = issue.expected !== void 0 ? ` expected=${issue.expected.type}` : "";
|
|
1619
|
+
const received = issue.receivedType !== void 0 ? ` received=${issue.receivedType}` : "";
|
|
1620
|
+
return `${issue.severity.toUpperCase()} [${issue.code}] ${issue.environment}:${issue.key} ${issue.message}${expected}${received}`;
|
|
1621
|
+
}
|
|
1622
|
+
function formatHumanReport(report) {
|
|
1623
|
+
const lines = [];
|
|
1624
|
+
lines.push(
|
|
1625
|
+
`Status: ${report.status.toUpperCase()} (errors=${report.summary.errors}, warnings=${report.summary.warnings}, total=${report.summary.total})`
|
|
1626
|
+
);
|
|
1627
|
+
if (report.issues.length > 0) {
|
|
1628
|
+
lines.push("");
|
|
1629
|
+
lines.push("Issues:");
|
|
1630
|
+
for (const issue of report.issues) {
|
|
1631
|
+
lines.push(`- ${formatIssue(issue)}`);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
if (report.recommendations !== void 0 && report.recommendations.length > 0) {
|
|
1635
|
+
lines.push("");
|
|
1636
|
+
lines.push("Recommendations:");
|
|
1637
|
+
for (const recommendation of report.recommendations) {
|
|
1638
|
+
lines.push(`- ${recommendation}`);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return `${lines.join("\n")}
|
|
1642
|
+
`;
|
|
1643
|
+
}
|
|
1644
|
+
async function persistJsonOutput(outputFile, report) {
|
|
1645
|
+
const resolvedPath = path6.resolve(outputFile);
|
|
1646
|
+
await mkdir(path6.dirname(resolvedPath), { recursive: true });
|
|
1647
|
+
await writeFile(resolvedPath, `${JSON.stringify(report, null, 2)}
|
|
1648
|
+
`, "utf8");
|
|
1649
|
+
}
|
|
1650
|
+
async function emitValidationReport(options) {
|
|
1651
|
+
const { report, outputFile, jsonMode } = options;
|
|
1652
|
+
if (outputFile !== void 0) {
|
|
1653
|
+
await persistJsonOutput(outputFile, report);
|
|
1654
|
+
}
|
|
1655
|
+
if (jsonMode === "off") {
|
|
1656
|
+
process.stdout.write(formatHumanReport(report));
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
process.stdout.write(toJsonString(report, jsonMode));
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// src/validation-command.ts
|
|
1663
|
+
var HELP_TEXT = {
|
|
1664
|
+
check: [
|
|
1665
|
+
"Usage: env-typegen check [options]",
|
|
1666
|
+
"",
|
|
1667
|
+
"Options:",
|
|
1668
|
+
" --env <path> Environment file to validate (default: .env)",
|
|
1669
|
+
" --contract <path> Contract file path (default: env.contract.ts)",
|
|
1670
|
+
" --example <path> Fallback .env.example used to bootstrap contract",
|
|
1671
|
+
" --strict Validate extras as errors (default: true)",
|
|
1672
|
+
" --no-strict Downgrade extras to warnings",
|
|
1673
|
+
" --json Emit machine-readable JSON report",
|
|
1674
|
+
" --json=pretty Emit pretty JSON report",
|
|
1675
|
+
" --output-file <path> Persist JSON report to a file",
|
|
1676
|
+
" --debug-values Include raw values in issues (unsafe for CI logs)",
|
|
1677
|
+
" --cloud-provider <name> vercel | cloudflare | aws",
|
|
1678
|
+
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1679
|
+
" --plugin <path> Plugin module path (repeatable)",
|
|
1680
|
+
" -c, --config <path> Config file path",
|
|
1681
|
+
" -h, --help Show this help"
|
|
1682
|
+
].join("\n"),
|
|
1683
|
+
diff: [
|
|
1684
|
+
"Usage: env-typegen diff [options]",
|
|
1685
|
+
"",
|
|
1686
|
+
"Options:",
|
|
1687
|
+
" --targets <list> Comma-separated targets (default: .env,.env.example,.env.production)",
|
|
1688
|
+
" --contract <path> Contract file path (default: env.contract.ts)",
|
|
1689
|
+
" --example <path> Fallback .env.example used to bootstrap contract",
|
|
1690
|
+
" --strict Validate extras as errors (default: true)",
|
|
1691
|
+
" --no-strict Downgrade extras to warnings",
|
|
1692
|
+
" --json Emit machine-readable JSON report",
|
|
1693
|
+
" --json=pretty Emit pretty JSON report",
|
|
1694
|
+
" --output-file <path> Persist JSON report to a file",
|
|
1695
|
+
" --debug-values Include raw values in issues (unsafe for CI logs)",
|
|
1696
|
+
" --cloud-provider <name> vercel | cloudflare | aws",
|
|
1697
|
+
" --cloud-file <path> Cloud snapshot JSON file added to diff sources",
|
|
1698
|
+
" --plugin <path> Plugin module path (repeatable)",
|
|
1699
|
+
" -c, --config <path> Config file path",
|
|
1700
|
+
" -h, --help Show this help"
|
|
1701
|
+
].join("\n"),
|
|
1702
|
+
doctor: [
|
|
1703
|
+
"Usage: env-typegen doctor [options]",
|
|
1704
|
+
"",
|
|
1705
|
+
"Options:",
|
|
1706
|
+
" --env <path> Environment file to validate (default: .env)",
|
|
1707
|
+
" --targets <list> Comma-separated targets for drift analysis",
|
|
1708
|
+
" --contract <path> Contract file path (default: env.contract.ts)",
|
|
1709
|
+
" --example <path> Fallback .env.example used to bootstrap contract",
|
|
1710
|
+
" --strict Validate extras as errors (default: true)",
|
|
1711
|
+
" --no-strict Downgrade extras to warnings",
|
|
1712
|
+
" --json Emit machine-readable JSON report",
|
|
1713
|
+
" --json=pretty Emit pretty JSON report",
|
|
1714
|
+
" --output-file <path> Persist JSON report to a file",
|
|
1715
|
+
" --debug-values Include raw values in issues (unsafe for CI logs)",
|
|
1716
|
+
" --cloud-provider <name> vercel | cloudflare | aws",
|
|
1717
|
+
" --cloud-file <path> Cloud snapshot JSON file",
|
|
1718
|
+
" --plugin <path> Plugin module path (repeatable)",
|
|
1719
|
+
" -c, --config <path> Config file path",
|
|
1720
|
+
" -h, --help Show this help"
|
|
1721
|
+
].join("\n")
|
|
1722
|
+
};
|
|
1723
|
+
function resolveConfigRelative(value, configDir) {
|
|
1724
|
+
if (value === void 0 || path6.isAbsolute(value)) return value;
|
|
1725
|
+
return path6.resolve(configDir, value);
|
|
1726
|
+
}
|
|
1727
|
+
function resolvePluginReference(reference, configDir) {
|
|
1728
|
+
if (typeof reference === "string" && !path6.isAbsolute(reference)) {
|
|
1729
|
+
return path6.resolve(configDir, reference);
|
|
1730
|
+
}
|
|
1731
|
+
return reference;
|
|
1732
|
+
}
|
|
1733
|
+
function applyConfigPaths(config, configDir) {
|
|
1734
|
+
let input;
|
|
1735
|
+
if (Array.isArray(config.input)) {
|
|
1736
|
+
input = config.input.map((item) => resolveConfigRelative(item, configDir) ?? item);
|
|
1737
|
+
} else {
|
|
1738
|
+
input = resolveConfigRelative(config.input, configDir);
|
|
1739
|
+
}
|
|
1740
|
+
const output = resolveConfigRelative(config.output, configDir);
|
|
1741
|
+
const schemaFile = resolveConfigRelative(config.schemaFile, configDir);
|
|
1742
|
+
return {
|
|
1743
|
+
...config,
|
|
1744
|
+
...input !== void 0 && { input },
|
|
1745
|
+
...output !== void 0 && { output },
|
|
1746
|
+
...schemaFile !== void 0 && { schemaFile },
|
|
1747
|
+
...config.diffTargets !== void 0 && {
|
|
1748
|
+
diffTargets: config.diffTargets.map(
|
|
1749
|
+
(target) => resolveConfigRelative(target, configDir) ?? target
|
|
1750
|
+
)
|
|
1751
|
+
},
|
|
1752
|
+
...config.plugins !== void 0 && {
|
|
1753
|
+
plugins: config.plugins.map((reference) => resolvePluginReference(reference, configDir))
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
async function loadCommandConfig(configPath) {
|
|
1758
|
+
if (configPath === void 0) {
|
|
1759
|
+
return loadConfig(process.cwd());
|
|
1760
|
+
}
|
|
1761
|
+
const resolvedPath = path6.resolve(configPath);
|
|
1762
|
+
const configDir = path6.dirname(resolvedPath);
|
|
1763
|
+
const moduleValue = await import(pathToFileURL(resolvedPath).href);
|
|
1764
|
+
if (moduleValue.default === void 0) return void 0;
|
|
1765
|
+
return applyConfigPaths(moduleValue.default, configDir);
|
|
1766
|
+
}
|
|
1767
|
+
function preprocessJsonArguments(argv) {
|
|
1768
|
+
const normalizedArgs = [];
|
|
1769
|
+
let assignedMode = "off";
|
|
1770
|
+
for (const item of argv) {
|
|
1771
|
+
if (item === "--json=pretty") {
|
|
1772
|
+
normalizedArgs.push("--json");
|
|
1773
|
+
assignedMode = "pretty";
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
if (item === "--json=compact") {
|
|
1777
|
+
normalizedArgs.push("--json");
|
|
1778
|
+
assignedMode = "compact";
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
normalizedArgs.push(item);
|
|
1782
|
+
}
|
|
1783
|
+
return { normalizedArgs, assignedMode };
|
|
1784
|
+
}
|
|
1785
|
+
function parseValidationArgs(argv) {
|
|
1786
|
+
const { normalizedArgs, assignedMode } = preprocessJsonArguments(argv);
|
|
1787
|
+
const { values } = parseArgs({
|
|
1788
|
+
args: normalizedArgs,
|
|
1789
|
+
options: {
|
|
1790
|
+
env: { type: "string", multiple: true },
|
|
1791
|
+
targets: { type: "string" },
|
|
1792
|
+
contract: { type: "string" },
|
|
1793
|
+
example: { type: "string" },
|
|
1794
|
+
strict: { type: "boolean" },
|
|
1795
|
+
"no-strict": { type: "boolean" },
|
|
1796
|
+
json: { type: "boolean" },
|
|
1797
|
+
"output-file": { type: "string" },
|
|
1798
|
+
"debug-values": { type: "boolean" },
|
|
1799
|
+
"cloud-provider": { type: "string" },
|
|
1800
|
+
"cloud-file": { type: "string" },
|
|
1801
|
+
plugin: { type: "string", multiple: true },
|
|
1802
|
+
config: { type: "string", short: "c" },
|
|
1803
|
+
help: { type: "boolean", short: "h" }
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
const castValues = values;
|
|
1807
|
+
const jsonMode = castValues.json === true ? assignedMode === "off" ? "compact" : assignedMode : "off";
|
|
1808
|
+
return { values: castValues, jsonMode };
|
|
1809
|
+
}
|
|
1810
|
+
function resolveStrict(values, fileConfig) {
|
|
1811
|
+
if (values["no-strict"] === true) return false;
|
|
1812
|
+
if (values.strict !== void 0) return values.strict;
|
|
1813
|
+
if (fileConfig?.strict !== void 0) return fileConfig.strict;
|
|
1814
|
+
return true;
|
|
1815
|
+
}
|
|
1816
|
+
function parseCloudProvider(value) {
|
|
1817
|
+
if (value === void 0) return void 0;
|
|
1818
|
+
if (value === "vercel" || value === "cloudflare" || value === "aws") return value;
|
|
1819
|
+
throw new Error(`Unknown cloud provider: ${value}. Valid: vercel, cloudflare, aws`);
|
|
1820
|
+
}
|
|
1821
|
+
function parseTargets(values, fileConfig) {
|
|
1822
|
+
const fromCli = values.targets;
|
|
1823
|
+
if (fromCli !== void 0) {
|
|
1824
|
+
return fromCli.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
1825
|
+
}
|
|
1826
|
+
if (fileConfig?.diffTargets !== void 0 && fileConfig.diffTargets.length > 0) {
|
|
1827
|
+
return fileConfig.diffTargets;
|
|
1828
|
+
}
|
|
1829
|
+
return [".env", ".env.example", ".env.production"];
|
|
1830
|
+
}
|
|
1831
|
+
async function prepareCommonContext(values) {
|
|
1832
|
+
const fileConfig = await loadCommandConfig(values.config);
|
|
1833
|
+
const strict = resolveStrict(values, fileConfig);
|
|
1834
|
+
const debugValues = values["debug-values"] ?? false;
|
|
1835
|
+
const contractPath = values.contract ?? fileConfig?.schemaFile;
|
|
1836
|
+
const fallbackExamplePath = values.example ?? ".env.example";
|
|
1837
|
+
const cloudProvider = parseCloudProvider(values["cloud-provider"]);
|
|
1838
|
+
const cloudFile = values["cloud-file"];
|
|
1839
|
+
const pluginLoadOptions = {
|
|
1840
|
+
pluginPaths: values.plugin ?? [],
|
|
1841
|
+
cwd: process.cwd(),
|
|
1842
|
+
...fileConfig?.plugins !== void 0 && { configPlugins: fileConfig.plugins }
|
|
1843
|
+
};
|
|
1844
|
+
const plugins = await loadPlugins(pluginLoadOptions);
|
|
1845
|
+
return {
|
|
1846
|
+
fileConfig,
|
|
1847
|
+
strict,
|
|
1848
|
+
debugValues,
|
|
1849
|
+
outputFile: values["output-file"],
|
|
1850
|
+
contractPath,
|
|
1851
|
+
fallbackExamplePath,
|
|
1852
|
+
cloudProvider,
|
|
1853
|
+
cloudFile,
|
|
1854
|
+
plugins
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
async function emitAndReturnExitCode(report, params) {
|
|
1858
|
+
const emitOptions = {
|
|
1859
|
+
report,
|
|
1860
|
+
jsonMode: params.jsonMode,
|
|
1861
|
+
...params.outputFile !== void 0 && { outputFile: params.outputFile }
|
|
1862
|
+
};
|
|
1863
|
+
await emitValidationReport(emitOptions);
|
|
1864
|
+
return report.status === "fail" ? 1 : 0;
|
|
1865
|
+
}
|
|
1866
|
+
async function runCheckCommand(args) {
|
|
1867
|
+
const context = await prepareCommonContext(args.values);
|
|
1868
|
+
const loadContractOptions = {
|
|
1869
|
+
fallbackExamplePath: context.fallbackExamplePath,
|
|
1870
|
+
cwd: process.cwd(),
|
|
1871
|
+
...context.contractPath !== void 0 && { contractPath: context.contractPath }
|
|
1872
|
+
};
|
|
1873
|
+
const contract = applyContractPlugins(
|
|
1874
|
+
await loadValidationContract(loadContractOptions),
|
|
1875
|
+
context.plugins
|
|
1876
|
+
);
|
|
1877
|
+
const provider = context.cloudProvider;
|
|
1878
|
+
let environment = args.values.env?.[0] ?? ".env";
|
|
1879
|
+
let sourceValues;
|
|
1880
|
+
if (provider !== void 0) {
|
|
1881
|
+
const cloudFile = context.cloudFile ?? `${provider}.env.json`;
|
|
1882
|
+
sourceValues = await loadCloudSource({ provider, filePath: cloudFile });
|
|
1883
|
+
environment = `cloud:${provider}`;
|
|
1884
|
+
} else {
|
|
1885
|
+
sourceValues = await loadEnvSource({ filePath: environment, allowMissing: true });
|
|
1886
|
+
}
|
|
1887
|
+
sourceValues = applySourcePlugins({ environment, values: sourceValues }, context.plugins);
|
|
1888
|
+
const report = applyReportPlugins(
|
|
1889
|
+
validateAgainstContract({
|
|
1890
|
+
contract,
|
|
1891
|
+
values: sourceValues,
|
|
1892
|
+
environment,
|
|
1893
|
+
strict: context.strict,
|
|
1894
|
+
debugValues: context.debugValues
|
|
1895
|
+
}),
|
|
1896
|
+
context.plugins
|
|
1897
|
+
);
|
|
1898
|
+
return emitAndReturnExitCode(report, {
|
|
1899
|
+
jsonMode: args.jsonMode,
|
|
1900
|
+
...context.outputFile !== void 0 && { outputFile: context.outputFile }
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
async function runDiffCommand(args) {
|
|
1904
|
+
const context = await prepareCommonContext(args.values);
|
|
1905
|
+
const loadContractOptions = {
|
|
1906
|
+
fallbackExamplePath: context.fallbackExamplePath,
|
|
1907
|
+
cwd: process.cwd(),
|
|
1908
|
+
...context.contractPath !== void 0 && { contractPath: context.contractPath }
|
|
1909
|
+
};
|
|
1910
|
+
const contract = applyContractPlugins(
|
|
1911
|
+
await loadValidationContract(loadContractOptions),
|
|
1912
|
+
context.plugins
|
|
1913
|
+
);
|
|
1914
|
+
const sources = {};
|
|
1915
|
+
for (const target of parseTargets(args.values, context.fileConfig)) {
|
|
1916
|
+
const values = await loadEnvSource({ filePath: target, allowMissing: true });
|
|
1917
|
+
sources[target] = applySourcePlugins({ environment: target, values }, context.plugins);
|
|
1918
|
+
}
|
|
1919
|
+
if (context.cloudProvider !== void 0) {
|
|
1920
|
+
const cloudFile = context.cloudFile ?? `${context.cloudProvider}.env.json`;
|
|
1921
|
+
const cloudEnvironment = `cloud:${context.cloudProvider}`;
|
|
1922
|
+
const cloudValues = await loadCloudSource({
|
|
1923
|
+
provider: context.cloudProvider,
|
|
1924
|
+
filePath: cloudFile
|
|
1925
|
+
});
|
|
1926
|
+
sources[cloudEnvironment] = applySourcePlugins(
|
|
1927
|
+
{ environment: cloudEnvironment, values: cloudValues },
|
|
1928
|
+
context.plugins
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
const report = applyReportPlugins(
|
|
1932
|
+
diffEnvironmentSources({
|
|
1933
|
+
contract,
|
|
1934
|
+
sources,
|
|
1935
|
+
strict: context.strict,
|
|
1936
|
+
debugValues: context.debugValues
|
|
1937
|
+
}),
|
|
1938
|
+
context.plugins
|
|
1939
|
+
);
|
|
1940
|
+
return emitAndReturnExitCode(report, {
|
|
1941
|
+
jsonMode: args.jsonMode,
|
|
1942
|
+
...context.outputFile !== void 0 && { outputFile: context.outputFile }
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
async function runDoctorCommand(args) {
|
|
1946
|
+
const context = await prepareCommonContext(args.values);
|
|
1947
|
+
const loadContractOptions = {
|
|
1948
|
+
fallbackExamplePath: context.fallbackExamplePath,
|
|
1949
|
+
cwd: process.cwd(),
|
|
1950
|
+
...context.contractPath !== void 0 && { contractPath: context.contractPath }
|
|
1951
|
+
};
|
|
1952
|
+
const contract = applyContractPlugins(
|
|
1953
|
+
await loadValidationContract(loadContractOptions),
|
|
1954
|
+
context.plugins
|
|
1955
|
+
);
|
|
1956
|
+
const checkEnvironment = args.values.env?.[0] ?? ".env";
|
|
1957
|
+
let checkValues = await loadEnvSource({ filePath: checkEnvironment, allowMissing: true });
|
|
1958
|
+
checkValues = applySourcePlugins(
|
|
1959
|
+
{ environment: checkEnvironment, values: checkValues },
|
|
1960
|
+
context.plugins
|
|
1961
|
+
);
|
|
1962
|
+
const checkReport = validateAgainstContract({
|
|
1963
|
+
contract,
|
|
1964
|
+
values: checkValues,
|
|
1965
|
+
environment: checkEnvironment,
|
|
1966
|
+
strict: context.strict,
|
|
1967
|
+
debugValues: context.debugValues
|
|
1968
|
+
});
|
|
1969
|
+
const sources = {};
|
|
1970
|
+
for (const target of parseTargets(args.values, context.fileConfig)) {
|
|
1971
|
+
const values = await loadEnvSource({ filePath: target, allowMissing: true });
|
|
1972
|
+
sources[target] = applySourcePlugins({ environment: target, values }, context.plugins);
|
|
1973
|
+
}
|
|
1974
|
+
if (context.cloudProvider !== void 0) {
|
|
1975
|
+
const cloudFile = context.cloudFile ?? `${context.cloudProvider}.env.json`;
|
|
1976
|
+
const cloudEnvironment = `cloud:${context.cloudProvider}`;
|
|
1977
|
+
const cloudValues = await loadCloudSource({
|
|
1978
|
+
provider: context.cloudProvider,
|
|
1979
|
+
filePath: cloudFile
|
|
514
1980
|
});
|
|
1981
|
+
sources[cloudEnvironment] = applySourcePlugins(
|
|
1982
|
+
{ environment: cloudEnvironment, values: cloudValues },
|
|
1983
|
+
context.plugins
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
const diffReport = diffEnvironmentSources({
|
|
1987
|
+
contract,
|
|
1988
|
+
sources,
|
|
1989
|
+
strict: context.strict,
|
|
1990
|
+
debugValues: context.debugValues
|
|
1991
|
+
});
|
|
1992
|
+
const report = applyReportPlugins(
|
|
1993
|
+
buildDoctorReport({ checkReport, diffReport }),
|
|
1994
|
+
context.plugins
|
|
1995
|
+
);
|
|
1996
|
+
return emitAndReturnExitCode(report, {
|
|
1997
|
+
jsonMode: args.jsonMode,
|
|
1998
|
+
...context.outputFile !== void 0 && { outputFile: context.outputFile }
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
async function runValidationCommand(params) {
|
|
2002
|
+
const parsed = parseValidationArgs(params.argv);
|
|
2003
|
+
if (parsed.values.help === true) {
|
|
2004
|
+
console.log(HELP_TEXT[params.command]);
|
|
2005
|
+
return 0;
|
|
2006
|
+
}
|
|
2007
|
+
if (params.command === "check") {
|
|
2008
|
+
return runCheckCommand(parsed);
|
|
2009
|
+
}
|
|
2010
|
+
if (params.command === "diff") {
|
|
2011
|
+
return runDiffCommand(parsed);
|
|
515
2012
|
}
|
|
2013
|
+
return runDoctorCommand(parsed);
|
|
516
2014
|
}
|
|
517
2015
|
|
|
518
|
-
export {
|
|
2016
|
+
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 };
|
|
519
2017
|
//# sourceMappingURL=index.js.map
|
|
520
2018
|
//# sourceMappingURL=index.js.map
|