@xlameiro/env-typegen 0.1.5 → 0.1.7
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 +12 -0
- package/README.md +12 -0
- package/dist/cli.js +76 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +85 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -2
- package/dist/index.d.ts +25 -2
- package/dist/index.js +85 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -43,6 +43,21 @@ type ParsedEnvVar = {
|
|
|
43
43
|
*/
|
|
44
44
|
isSecret?: boolean;
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* A non-fatal issue detected during parsing (e.g. a duplicate key).
|
|
48
|
+
* Collected in {@link ParsedEnvFile.warnings} when the parser encounters
|
|
49
|
+
* degenerate-but-valid input.
|
|
50
|
+
*/
|
|
51
|
+
type ParsedEnvWarning = {
|
|
52
|
+
/** Machine-readable code for programmatic handling. */
|
|
53
|
+
code: "ENV_DUPLICATE_KEY";
|
|
54
|
+
/** Human-readable description of the issue. */
|
|
55
|
+
message: string;
|
|
56
|
+
/** 1-based line number of the earlier (discarded) occurrence. */
|
|
57
|
+
line: number;
|
|
58
|
+
/** The duplicated variable name. */
|
|
59
|
+
key: string;
|
|
60
|
+
};
|
|
46
61
|
/**
|
|
47
62
|
* The complete result of parsing a single .env.example file.
|
|
48
63
|
* Passed to generators to produce TypeScript / Zod / t3-env output.
|
|
@@ -54,6 +69,11 @@ type ParsedEnvFile = {
|
|
|
54
69
|
vars: ParsedEnvVar[];
|
|
55
70
|
/** Unique group names found in the file, in order of appearance */
|
|
56
71
|
groups: string[];
|
|
72
|
+
/**
|
|
73
|
+
* Non-fatal issues detected during parsing (e.g. duplicate keys).
|
|
74
|
+
* Undefined when no issues were found so the field is omitted in clean parses.
|
|
75
|
+
*/
|
|
76
|
+
warnings?: ParsedEnvWarning[];
|
|
57
77
|
};
|
|
58
78
|
|
|
59
79
|
/**
|
|
@@ -461,9 +481,12 @@ type EnvContract$1 = {
|
|
|
461
481
|
|
|
462
482
|
/**
|
|
463
483
|
* Contract file names searched in order when calling {@link loadContract}.
|
|
464
|
-
*
|
|
484
|
+
* BUG-01: .mjs and .js are searched before .ts — TypeScript files cannot be
|
|
485
|
+
* loaded by Node.js import() at runtime without a loader like tsx or ts-node.
|
|
486
|
+
* Discovering a .mjs file first prevents a confusing ERR_UNKNOWN_FILE_EXTENSION
|
|
487
|
+
* failure when both .ts and .mjs files coexist in the same directory.
|
|
465
488
|
*/
|
|
466
|
-
declare const CONTRACT_FILE_NAMES: readonly ["env.contract.
|
|
489
|
+
declare const CONTRACT_FILE_NAMES: readonly ["env.contract.mjs", "env.contract.js", "env.contract.ts"];
|
|
467
490
|
/**
|
|
468
491
|
* Type-safe contract factory. Returns the contract object unchanged — exists
|
|
469
492
|
* purely for IDE autocompletion and compile-time validation of the contract shape.
|
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,21 @@ type ParsedEnvVar = {
|
|
|
43
43
|
*/
|
|
44
44
|
isSecret?: boolean;
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* A non-fatal issue detected during parsing (e.g. a duplicate key).
|
|
48
|
+
* Collected in {@link ParsedEnvFile.warnings} when the parser encounters
|
|
49
|
+
* degenerate-but-valid input.
|
|
50
|
+
*/
|
|
51
|
+
type ParsedEnvWarning = {
|
|
52
|
+
/** Machine-readable code for programmatic handling. */
|
|
53
|
+
code: "ENV_DUPLICATE_KEY";
|
|
54
|
+
/** Human-readable description of the issue. */
|
|
55
|
+
message: string;
|
|
56
|
+
/** 1-based line number of the earlier (discarded) occurrence. */
|
|
57
|
+
line: number;
|
|
58
|
+
/** The duplicated variable name. */
|
|
59
|
+
key: string;
|
|
60
|
+
};
|
|
46
61
|
/**
|
|
47
62
|
* The complete result of parsing a single .env.example file.
|
|
48
63
|
* Passed to generators to produce TypeScript / Zod / t3-env output.
|
|
@@ -54,6 +69,11 @@ type ParsedEnvFile = {
|
|
|
54
69
|
vars: ParsedEnvVar[];
|
|
55
70
|
/** Unique group names found in the file, in order of appearance */
|
|
56
71
|
groups: string[];
|
|
72
|
+
/**
|
|
73
|
+
* Non-fatal issues detected during parsing (e.g. duplicate keys).
|
|
74
|
+
* Undefined when no issues were found so the field is omitted in clean parses.
|
|
75
|
+
*/
|
|
76
|
+
warnings?: ParsedEnvWarning[];
|
|
57
77
|
};
|
|
58
78
|
|
|
59
79
|
/**
|
|
@@ -461,9 +481,12 @@ type EnvContract$1 = {
|
|
|
461
481
|
|
|
462
482
|
/**
|
|
463
483
|
* Contract file names searched in order when calling {@link loadContract}.
|
|
464
|
-
*
|
|
484
|
+
* BUG-01: .mjs and .js are searched before .ts — TypeScript files cannot be
|
|
485
|
+
* loaded by Node.js import() at runtime without a loader like tsx or ts-node.
|
|
486
|
+
* Discovering a .mjs file first prevents a confusing ERR_UNKNOWN_FILE_EXTENSION
|
|
487
|
+
* failure when both .ts and .mjs files coexist in the same directory.
|
|
465
488
|
*/
|
|
466
|
-
declare const CONTRACT_FILE_NAMES: readonly ["env.contract.
|
|
489
|
+
declare const CONTRACT_FILE_NAMES: readonly ["env.contract.mjs", "env.contract.js", "env.contract.ts"];
|
|
467
490
|
/**
|
|
468
491
|
* Type-safe contract factory. Returns the contract object unchanged — exists
|
|
469
492
|
* purely for IDE autocompletion and compile-time validation of the contract shape.
|
package/dist/index.js
CHANGED
|
@@ -157,7 +157,9 @@ var inferenceRules = [
|
|
|
157
157
|
{
|
|
158
158
|
id: "P10_url_scheme",
|
|
159
159
|
priority: 10,
|
|
160
|
-
|
|
160
|
+
// BUG-02: comma-separated URL lists (e.g. ALLOWED_ORIGINS) must NOT be inferred as
|
|
161
|
+
// a single URL — they don't pass z.string().url() or new URL() validation at runtime.
|
|
162
|
+
match: (_key, value) => !value.includes(",") && /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value),
|
|
161
163
|
type: "url"
|
|
162
164
|
},
|
|
163
165
|
{
|
|
@@ -243,6 +245,27 @@ function buildParsedVar(params, commentBlock, options) {
|
|
|
243
245
|
}
|
|
244
246
|
return parsedVar;
|
|
245
247
|
}
|
|
248
|
+
function deduplicateVars(vars) {
|
|
249
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
250
|
+
const deduped = [];
|
|
251
|
+
const warnings = [];
|
|
252
|
+
for (let i = vars.length - 1; i >= 0; i--) {
|
|
253
|
+
const variable = vars[i];
|
|
254
|
+
if (variable === void 0) continue;
|
|
255
|
+
if (seenKeys.has(variable.key)) {
|
|
256
|
+
warnings.push({
|
|
257
|
+
code: "ENV_DUPLICATE_KEY",
|
|
258
|
+
message: `Duplicate key "${variable.key}" at line ${variable.lineNumber} \u2014 last occurrence wins.`,
|
|
259
|
+
line: variable.lineNumber,
|
|
260
|
+
key: variable.key
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
seenKeys.add(variable.key);
|
|
264
|
+
deduped.unshift(variable);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return { deduped, warnings };
|
|
268
|
+
}
|
|
246
269
|
function parseEnvFileContent(content, filePath, options) {
|
|
247
270
|
const lines = content.split("\n");
|
|
248
271
|
const vars = [];
|
|
@@ -285,7 +308,13 @@ function parseEnvFileContent(content, filePath, options) {
|
|
|
285
308
|
);
|
|
286
309
|
commentBlock = [];
|
|
287
310
|
}
|
|
288
|
-
|
|
311
|
+
const { deduped, warnings } = deduplicateVars(vars);
|
|
312
|
+
return {
|
|
313
|
+
filePath,
|
|
314
|
+
vars: deduped,
|
|
315
|
+
groups,
|
|
316
|
+
...warnings.length > 0 && { warnings }
|
|
317
|
+
};
|
|
289
318
|
}
|
|
290
319
|
function parseEnvFile(filePath) {
|
|
291
320
|
const content = readFileSync(filePath, "utf8");
|
|
@@ -486,9 +515,9 @@ function generateT3Env(parsed) {
|
|
|
486
515
|
return lines.join("\n") + "\n";
|
|
487
516
|
}
|
|
488
517
|
var CONTRACT_FILE_NAMES = [
|
|
489
|
-
"env.contract.ts",
|
|
490
518
|
"env.contract.mjs",
|
|
491
|
-
"env.contract.js"
|
|
519
|
+
"env.contract.js",
|
|
520
|
+
"env.contract.ts"
|
|
492
521
|
];
|
|
493
522
|
function defineContract(contract) {
|
|
494
523
|
return contract;
|
|
@@ -497,6 +526,12 @@ async function loadContract(cwd = process.cwd()) {
|
|
|
497
526
|
for (const name of CONTRACT_FILE_NAMES) {
|
|
498
527
|
const filePath = path6.resolve(cwd, name);
|
|
499
528
|
if (existsSync(filePath)) {
|
|
529
|
+
if (filePath.endsWith(".ts")) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
`Contract file ${filePath} is a TypeScript file and cannot be loaded at runtime by Node.js.
|
|
532
|
+
Rename it to ${filePath.replace(/\.ts$/, ".mjs")} and use ESM syntax (export default ...), or run via \`tsx\` / \`ts-node\` as a loader.`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
500
535
|
const fileUrl = pathToFileURL(filePath).href;
|
|
501
536
|
const mod = await import(fileUrl);
|
|
502
537
|
return mod.default;
|
|
@@ -542,7 +577,8 @@ async function readEnvFile(filePath) {
|
|
|
542
577
|
return await readFile(resolved, "utf8");
|
|
543
578
|
} catch (err) {
|
|
544
579
|
if (err instanceof Error && err.code === "ENOENT") {
|
|
545
|
-
|
|
580
|
+
const displayPath = path6.isAbsolute(filePath) ? filePath : `${filePath} (resolved: ${resolved})`;
|
|
581
|
+
throw new Error(`File not found: ${displayPath}`);
|
|
546
582
|
}
|
|
547
583
|
throw err;
|
|
548
584
|
}
|
|
@@ -579,7 +615,14 @@ function success(message) {
|
|
|
579
615
|
|
|
580
616
|
// src/pipeline.ts
|
|
581
617
|
function deriveOutputPath(base, generator, isSingle) {
|
|
582
|
-
if (isSingle)
|
|
618
|
+
if (isSingle) {
|
|
619
|
+
if (generator === "declaration" && !base.endsWith(".d.ts")) {
|
|
620
|
+
const ext2 = path6.extname(base);
|
|
621
|
+
const noExt2 = ext2.length > 0 ? base.slice(0, -ext2.length) : base;
|
|
622
|
+
return `${noExt2}.d.ts`;
|
|
623
|
+
}
|
|
624
|
+
return base;
|
|
625
|
+
}
|
|
583
626
|
const ext = path6.extname(base);
|
|
584
627
|
const noExt = ext.length > 0 ? base.slice(0, -ext.length) : base;
|
|
585
628
|
const baseExt = ext.length > 0 ? ext : ".ts";
|
|
@@ -589,7 +632,8 @@ function deriveOutputPath(base, generator, isSingle) {
|
|
|
589
632
|
function deriveOutputBaseForInput(output, inputPath) {
|
|
590
633
|
const dir = path6.dirname(output);
|
|
591
634
|
const ext = path6.extname(output);
|
|
592
|
-
const
|
|
635
|
+
const rawBasename = path6.basename(inputPath);
|
|
636
|
+
const stem = rawBasename.startsWith(".") ? rawBasename.slice(1).replaceAll(".", "-") : path6.basename(inputPath, path6.extname(inputPath));
|
|
593
637
|
return path6.join(dir, `${stem}${ext}`);
|
|
594
638
|
}
|
|
595
639
|
function buildOutput(generator, parsed) {
|
|
@@ -626,6 +670,12 @@ async function persistOutput(params) {
|
|
|
626
670
|
success(`Generated ${outputPath}`);
|
|
627
671
|
}
|
|
628
672
|
}
|
|
673
|
+
function emitParserWarnings(parsed) {
|
|
674
|
+
if (parsed.warnings === void 0) return;
|
|
675
|
+
for (const w of parsed.warnings) {
|
|
676
|
+
warn(`[${w.code}] ${w.message}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
629
679
|
async function runGenerate(options) {
|
|
630
680
|
const {
|
|
631
681
|
input,
|
|
@@ -648,6 +698,7 @@ async function runGenerate(options) {
|
|
|
648
698
|
inputPath,
|
|
649
699
|
inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
|
|
650
700
|
);
|
|
701
|
+
emitParserWarnings(parsed);
|
|
651
702
|
for (const generator of generators) {
|
|
652
703
|
let generated = buildOutput(generator, parsed);
|
|
653
704
|
if (shouldFormat && !dryRun) {
|
|
@@ -724,7 +775,8 @@ function checkInvalidType(variable, entry, environment) {
|
|
|
724
775
|
break;
|
|
725
776
|
}
|
|
726
777
|
case "boolean": {
|
|
727
|
-
|
|
778
|
+
const lower = value.toLowerCase();
|
|
779
|
+
if (!["true", "false", "1", "0", "yes", "no"].includes(lower)) {
|
|
728
780
|
return {
|
|
729
781
|
code: "ENV_INVALID_TYPE",
|
|
730
782
|
key: variable.key,
|
|
@@ -847,7 +899,8 @@ function validateContract(parsed, contract, opts = {}) {
|
|
|
847
899
|
const parsedByKey = new Map(parsed.vars.map((v) => [v.key, v]));
|
|
848
900
|
for (const entry of contract.vars) {
|
|
849
901
|
if (!entry.required) continue;
|
|
850
|
-
|
|
902
|
+
const existing = parsedByKey.get(entry.name);
|
|
903
|
+
if (existing !== void 0 && existing.rawValue !== "") continue;
|
|
851
904
|
issues.push({
|
|
852
905
|
code: "ENV_MISSING",
|
|
853
906
|
key: entry.name,
|
|
@@ -949,6 +1002,11 @@ function outputHuman(result) {
|
|
|
949
1002
|
async function runCheck(opts) {
|
|
950
1003
|
const contract = await resolveContract(opts.contract, opts.cwd);
|
|
951
1004
|
const parsed = parseEnvFile(opts.input);
|
|
1005
|
+
if (parsed.warnings !== void 0) {
|
|
1006
|
+
for (const w of parsed.warnings) {
|
|
1007
|
+
warn(`[${w.code}] ${w.message}`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
952
1010
|
const validatorOpts = {};
|
|
953
1011
|
if (opts.environment !== void 0) validatorOpts.environment = opts.environment;
|
|
954
1012
|
if (opts.strict !== void 0) validatorOpts.strict = opts.strict;
|
|
@@ -1033,7 +1091,8 @@ async function loadCloudSource(options) {
|
|
|
1033
1091
|
raw = await readFile(resolvedPath, "utf8");
|
|
1034
1092
|
} catch (err) {
|
|
1035
1093
|
if (err instanceof Error && err.code === "ENOENT") {
|
|
1036
|
-
|
|
1094
|
+
const displayPath = path6.isAbsolute(options.filePath) ? options.filePath : `${options.filePath} (resolved: ${resolvedPath})`;
|
|
1095
|
+
throw new Error(`File not found: ${displayPath}`);
|
|
1037
1096
|
}
|
|
1038
1097
|
throw err;
|
|
1039
1098
|
}
|
|
@@ -1041,7 +1100,7 @@ async function loadCloudSource(options) {
|
|
|
1041
1100
|
return parseProviderPayload(options.provider, parsed);
|
|
1042
1101
|
}
|
|
1043
1102
|
var SECRET_KEY_RE = /(SECRET|TOKEN|PASSWORD|PRIVATE|API_KEY|ACCESS_KEY|CLIENT_SECRET)/i;
|
|
1044
|
-
var CONTRACT_FILE_NAMES2 = ["env.contract.
|
|
1103
|
+
var CONTRACT_FILE_NAMES2 = ["env.contract.mjs", "env.contract.js", "env.contract.ts"];
|
|
1045
1104
|
function isRecord2(value) {
|
|
1046
1105
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1047
1106
|
}
|
|
@@ -1165,6 +1224,12 @@ async function loadValidationContract(options) {
|
|
|
1165
1224
|
const discoveredContractPath = findDefaultContractPath(cwd);
|
|
1166
1225
|
const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path6.resolve(cwd, contractPath);
|
|
1167
1226
|
if (resolvedContractPath !== void 0 && existsSync(resolvedContractPath)) {
|
|
1227
|
+
if (resolvedContractPath.endsWith(".ts")) {
|
|
1228
|
+
throw new Error(
|
|
1229
|
+
`Contract file ${resolvedContractPath} is a TypeScript file and cannot be loaded at runtime by Node.js.
|
|
1230
|
+
Rename it to ${resolvedContractPath.replace(/\.ts$/, ".mjs")} and use ESM syntax (export default ...), or run via \`tsx\` / \`ts-node\` as a loader.`
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1168
1233
|
const moduleUrl = pathToFileURL(resolvedContractPath).href;
|
|
1169
1234
|
const moduleValue = await import(moduleUrl);
|
|
1170
1235
|
const candidate = moduleValue.default ?? moduleValue.contract;
|
|
@@ -1418,7 +1483,7 @@ function isClientSecret(variable, key) {
|
|
|
1418
1483
|
function checkContractVariable(key, variable, context) {
|
|
1419
1484
|
const { options, issues } = context;
|
|
1420
1485
|
const value = options.values[key];
|
|
1421
|
-
const hasValue = value !== void 0;
|
|
1486
|
+
const hasValue = value !== void 0 && (value !== "" || !variable.required);
|
|
1422
1487
|
if (variable.required && !hasValue) {
|
|
1423
1488
|
issues.push(
|
|
1424
1489
|
createIssue({
|
|
@@ -1520,7 +1585,11 @@ function diffTypeConflicts(key, present, context) {
|
|
|
1520
1585
|
for (const entry of present) {
|
|
1521
1586
|
typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
|
|
1522
1587
|
}
|
|
1523
|
-
|
|
1588
|
+
const knownTypes = /* @__PURE__ */ new Set();
|
|
1589
|
+
for (const t of typeBySource.values()) {
|
|
1590
|
+
if (t !== "unknown") knownTypes.add(t);
|
|
1591
|
+
}
|
|
1592
|
+
if (knownTypes.size <= 1) return;
|
|
1524
1593
|
for (const [sourceName, detectedType] of typeBySource.entries()) {
|
|
1525
1594
|
issues.push(
|
|
1526
1595
|
createIssue({
|
|
@@ -1687,6 +1756,7 @@ async function loadEnvSource(options) {
|
|
|
1687
1756
|
return parseEnvSourceContent(content);
|
|
1688
1757
|
} catch (error_) {
|
|
1689
1758
|
if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
|
|
1759
|
+
warn(`Target file not found: ${options.filePath} \u2014 treating as empty`);
|
|
1690
1760
|
return {};
|
|
1691
1761
|
}
|
|
1692
1762
|
throw error_;
|
|
@@ -1854,7 +1924,8 @@ async function loadCommandConfig(configPath) {
|
|
|
1854
1924
|
}
|
|
1855
1925
|
const resolvedPath = path6.resolve(configPath);
|
|
1856
1926
|
if (!existsSync(resolvedPath)) {
|
|
1857
|
-
|
|
1927
|
+
const displayPath = path6.isAbsolute(configPath) ? configPath : `${configPath} (resolved: ${resolvedPath})`;
|
|
1928
|
+
throw new Error(`Config file not found: ${displayPath}`);
|
|
1858
1929
|
}
|
|
1859
1930
|
const configDir = path6.dirname(resolvedPath);
|
|
1860
1931
|
const moduleValue = await import(pathToFileURL(resolvedPath).href);
|