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