@xlameiro/env-typegen 0.1.2 → 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 +6 -0
- package/README.md +82 -1
- package/dist/cli.js +1241 -57
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1513 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +537 -1
- package/dist/index.d.ts +537 -1
- package/dist/index.js +1501 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -99,11 +99,11 @@ var require_picocolors = __commonJS({
|
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
// src/cli.ts
|
|
102
|
-
import { createRequire } from "module";
|
|
103
102
|
import { realpathSync } from "fs";
|
|
104
|
-
import
|
|
105
|
-
import
|
|
106
|
-
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";
|
|
107
107
|
|
|
108
108
|
// src/config.ts
|
|
109
109
|
import { existsSync } from "fs";
|
|
@@ -422,29 +422,73 @@ var VALID_ENV_VAR_TYPES = /* @__PURE__ */ new Set([
|
|
|
422
422
|
function isEnvVarType(value) {
|
|
423
423
|
return VALID_ENV_VAR_TYPES.has(value);
|
|
424
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
|
+
}
|
|
425
474
|
function parseCommentBlock(lines) {
|
|
426
|
-
|
|
427
|
-
let description;
|
|
428
|
-
let isRequired = false;
|
|
475
|
+
const state = { isRequired: false };
|
|
429
476
|
for (const line of lines) {
|
|
430
|
-
|
|
431
|
-
if (content.startsWith("@description ")) {
|
|
432
|
-
description = content.slice("@description ".length).trim();
|
|
433
|
-
} else if (content.startsWith("@type ")) {
|
|
434
|
-
const typeStr = content.slice("@type ".length).trim();
|
|
435
|
-
if (isEnvVarType(typeStr)) {
|
|
436
|
-
annotatedType = typeStr;
|
|
437
|
-
}
|
|
438
|
-
} else if (content.trim() === "@required") {
|
|
439
|
-
isRequired = true;
|
|
440
|
-
} else if (content.trim() === "@optional") {
|
|
441
|
-
} else if (description === void 0 && content.trim().length > 0) {
|
|
442
|
-
description = content.trim();
|
|
443
|
-
}
|
|
477
|
+
processAnnotationContent(state, line.replace(/^#\s*/, "").trimEnd());
|
|
444
478
|
}
|
|
445
|
-
|
|
446
|
-
if (
|
|
447
|
-
|
|
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;
|
|
448
492
|
return result;
|
|
449
493
|
}
|
|
450
494
|
|
|
@@ -510,6 +554,18 @@ function parseEnvFileContent(content, filePath, options) {
|
|
|
510
554
|
if (currentGroup !== void 0) {
|
|
511
555
|
parsedVar.group = currentGroup;
|
|
512
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
|
+
}
|
|
513
569
|
vars.push(parsedVar);
|
|
514
570
|
commentBlock = [];
|
|
515
571
|
} else {
|
|
@@ -518,6 +574,10 @@ function parseEnvFileContent(content, filePath, options) {
|
|
|
518
574
|
}
|
|
519
575
|
return { filePath, vars, groups };
|
|
520
576
|
}
|
|
577
|
+
function parseEnvFile(filePath) {
|
|
578
|
+
const content = readFileSync(filePath, "utf8");
|
|
579
|
+
return parseEnvFileContent(content, filePath);
|
|
580
|
+
}
|
|
521
581
|
|
|
522
582
|
// src/utils/file.ts
|
|
523
583
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
@@ -651,8 +711,1118 @@ async function runGenerate(options) {
|
|
|
651
711
|
}
|
|
652
712
|
}
|
|
653
713
|
|
|
654
|
-
// src/
|
|
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";
|
|
655
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
|
|
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);
|
|
1817
|
+
}
|
|
1818
|
+
if (params.command === "diff") {
|
|
1819
|
+
return runDiffCommand(parsed);
|
|
1820
|
+
}
|
|
1821
|
+
return runDoctorCommand(parsed);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// src/watch.ts
|
|
1825
|
+
import path12 from "path";
|
|
656
1826
|
import { watch } from "chokidar";
|
|
657
1827
|
function debounce(fn, delay) {
|
|
658
1828
|
let timer;
|
|
@@ -702,7 +1872,7 @@ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
|
|
|
702
1872
|
for (const event of ["add", "change", "unlink"]) {
|
|
703
1873
|
inputWatcher.on(event, handleChange);
|
|
704
1874
|
}
|
|
705
|
-
const configPaths = CONFIG_FILE_NAMES.map((name) =>
|
|
1875
|
+
const configPaths = CONFIG_FILE_NAMES.map((name) => path12.resolve(cwd, name));
|
|
706
1876
|
const configWatcher = watch(configPaths, { persistent: true, ignoreInitial: true });
|
|
707
1877
|
for (const event of ["add", "change"]) {
|
|
708
1878
|
configWatcher.on(event, (eventPath) => void handleConfigChange(eventPath));
|
|
@@ -718,11 +1888,14 @@ function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
|
|
|
718
1888
|
// src/cli.ts
|
|
719
1889
|
var _require = createRequire(import.meta.url);
|
|
720
1890
|
var VERSION = _require("../package.json").version;
|
|
721
|
-
var
|
|
1891
|
+
var HELP_TEXT2 = [
|
|
722
1892
|
"env-typegen \u2014 Generate TypeScript types from .env.example",
|
|
723
1893
|
"",
|
|
724
1894
|
"Usage:",
|
|
725
|
-
" 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]",
|
|
726
1899
|
"",
|
|
727
1900
|
"Options:",
|
|
728
1901
|
" -i, --input <path> Path to .env.example file(s). May be specified multiple times.",
|
|
@@ -739,6 +1912,7 @@ var HELP_TEXT = [
|
|
|
739
1912
|
" -v, --version Print version",
|
|
740
1913
|
" -h, --help Show this help"
|
|
741
1914
|
].join("\n");
|
|
1915
|
+
var VALIDATION_SUBCOMMANDS = /* @__PURE__ */ new Set(["check", "diff", "doctor"]);
|
|
742
1916
|
var FORMAT_TO_GENERATOR = {
|
|
743
1917
|
ts: "typescript",
|
|
744
1918
|
typescript: "typescript",
|
|
@@ -749,32 +1923,56 @@ var FORMAT_TO_GENERATOR = {
|
|
|
749
1923
|
function normalizeGenerator(input) {
|
|
750
1924
|
return FORMAT_TO_GENERATOR[input] ?? FORMAT_TO_GENERATOR[input.toLowerCase()];
|
|
751
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
|
+
}
|
|
752
1939
|
function getErrorMessage(errorValue) {
|
|
753
1940
|
if (errorValue instanceof Error) {
|
|
754
1941
|
return errorValue.message;
|
|
755
1942
|
}
|
|
756
1943
|
return inspect(errorValue, { depth: 2 });
|
|
757
1944
|
}
|
|
758
|
-
function
|
|
759
|
-
if (value === void 0 ||
|
|
760
|
-
return
|
|
1945
|
+
function resolveConfigRelative2(value, configDir) {
|
|
1946
|
+
if (value === void 0 || path13.isAbsolute(value)) return value;
|
|
1947
|
+
return path13.resolve(configDir, value);
|
|
761
1948
|
}
|
|
762
|
-
function
|
|
1949
|
+
function applyConfigPaths2(config, configDir) {
|
|
763
1950
|
let input;
|
|
764
1951
|
if (Array.isArray(config.input)) {
|
|
765
|
-
input = config.input.map((v) =>
|
|
1952
|
+
input = config.input.map((v) => resolveConfigRelative2(v, configDir) ?? v);
|
|
766
1953
|
} else {
|
|
767
|
-
input =
|
|
1954
|
+
input = resolveConfigRelative2(config.input, configDir);
|
|
768
1955
|
}
|
|
769
|
-
const output =
|
|
1956
|
+
const output = resolveConfigRelative2(config.output, configDir);
|
|
770
1957
|
return {
|
|
771
1958
|
...config,
|
|
772
1959
|
...input !== void 0 && { input },
|
|
773
1960
|
...output !== void 0 && { output }
|
|
774
1961
|
};
|
|
775
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
|
+
}
|
|
776
1969
|
async function runCli(argv = process.argv.slice(2)) {
|
|
777
|
-
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({
|
|
778
1976
|
args: argv,
|
|
779
1977
|
options: {
|
|
780
1978
|
input: { type: "string", short: "i", multiple: true },
|
|
@@ -796,19 +1994,19 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
796
1994
|
return;
|
|
797
1995
|
}
|
|
798
1996
|
if (values.help === true) {
|
|
799
|
-
console.log(
|
|
1997
|
+
console.log(HELP_TEXT2);
|
|
800
1998
|
return;
|
|
801
1999
|
}
|
|
802
2000
|
let fileConfig;
|
|
803
2001
|
if (values.config === void 0) {
|
|
804
2002
|
fileConfig = await loadConfig(process.cwd());
|
|
805
2003
|
} else {
|
|
806
|
-
const configPath =
|
|
807
|
-
const configDir =
|
|
808
|
-
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);
|
|
809
2007
|
const rawConfig = mod.default;
|
|
810
2008
|
if (rawConfig) {
|
|
811
|
-
fileConfig =
|
|
2009
|
+
fileConfig = applyConfigPaths2(rawConfig, configDir);
|
|
812
2010
|
}
|
|
813
2011
|
}
|
|
814
2012
|
const cliInput = values.input?.length ? values.input : void 0;
|
|
@@ -818,21 +2016,7 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
818
2016
|
process.exit(1);
|
|
819
2017
|
}
|
|
820
2018
|
const output = values.output ?? fileConfig?.output ?? "env.generated.ts";
|
|
821
|
-
const
|
|
822
|
-
const rawGenerators = values.generator;
|
|
823
|
-
const requested = [...rawFormats ?? [], ...rawGenerators ?? []].map(String);
|
|
824
|
-
let generators;
|
|
825
|
-
if (requested.length > 0) {
|
|
826
|
-
const normalizedGenerators = requested.map((item) => normalizeGenerator(item)).filter((item) => item !== void 0);
|
|
827
|
-
const invalid = requested.filter((item) => normalizeGenerator(item) === void 0);
|
|
828
|
-
if (invalid.length > 0) {
|
|
829
|
-
error(`Unknown format(s): ${invalid.join(", ")}. Valid: ts, zod, t3, declaration`);
|
|
830
|
-
process.exit(1);
|
|
831
|
-
}
|
|
832
|
-
generators = [...new Set(normalizedGenerators)];
|
|
833
|
-
} else {
|
|
834
|
-
generators = fileConfig?.generators ?? ["typescript", "zod", "t3", "declaration"];
|
|
835
|
-
}
|
|
2019
|
+
const generators = resolveGenerators(values.format, values.generator, fileConfig?.generators);
|
|
836
2020
|
const shouldFormat = values["no-format"] === true ? false : fileConfig?.format ?? true;
|
|
837
2021
|
const useStdout = values.stdout ?? false;
|
|
838
2022
|
const isDryRun = values["dry-run"] ?? false;
|
|
@@ -856,7 +2040,7 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
856
2040
|
}
|
|
857
2041
|
if (process.argv[1] !== void 0 && (() => {
|
|
858
2042
|
try {
|
|
859
|
-
return realpathSync(
|
|
2043
|
+
return realpathSync(path13.resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url));
|
|
860
2044
|
} catch {
|
|
861
2045
|
return false;
|
|
862
2046
|
}
|