@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.cjs
CHANGED
|
@@ -164,7 +164,9 @@ var inferenceRules = [
|
|
|
164
164
|
{
|
|
165
165
|
id: "P10_url_scheme",
|
|
166
166
|
priority: 10,
|
|
167
|
-
|
|
167
|
+
// BUG-02: comma-separated URL lists (e.g. ALLOWED_ORIGINS) must NOT be inferred as
|
|
168
|
+
// a single URL — they don't pass z.string().url() or new URL() validation at runtime.
|
|
169
|
+
match: (_key, value) => !value.includes(",") && /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value),
|
|
168
170
|
type: "url"
|
|
169
171
|
},
|
|
170
172
|
{
|
|
@@ -250,6 +252,27 @@ function buildParsedVar(params, commentBlock, options) {
|
|
|
250
252
|
}
|
|
251
253
|
return parsedVar;
|
|
252
254
|
}
|
|
255
|
+
function deduplicateVars(vars) {
|
|
256
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
257
|
+
const deduped = [];
|
|
258
|
+
const warnings = [];
|
|
259
|
+
for (let i = vars.length - 1; i >= 0; i--) {
|
|
260
|
+
const variable = vars[i];
|
|
261
|
+
if (variable === void 0) continue;
|
|
262
|
+
if (seenKeys.has(variable.key)) {
|
|
263
|
+
warnings.push({
|
|
264
|
+
code: "ENV_DUPLICATE_KEY",
|
|
265
|
+
message: `Duplicate key "${variable.key}" at line ${variable.lineNumber} \u2014 last occurrence wins.`,
|
|
266
|
+
line: variable.lineNumber,
|
|
267
|
+
key: variable.key
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
seenKeys.add(variable.key);
|
|
271
|
+
deduped.unshift(variable);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { deduped, warnings };
|
|
275
|
+
}
|
|
253
276
|
function parseEnvFileContent(content, filePath, options) {
|
|
254
277
|
const lines = content.split("\n");
|
|
255
278
|
const vars = [];
|
|
@@ -292,7 +315,13 @@ function parseEnvFileContent(content, filePath, options) {
|
|
|
292
315
|
);
|
|
293
316
|
commentBlock = [];
|
|
294
317
|
}
|
|
295
|
-
|
|
318
|
+
const { deduped, warnings } = deduplicateVars(vars);
|
|
319
|
+
return {
|
|
320
|
+
filePath,
|
|
321
|
+
vars: deduped,
|
|
322
|
+
groups,
|
|
323
|
+
...warnings.length > 0 && { warnings }
|
|
324
|
+
};
|
|
296
325
|
}
|
|
297
326
|
function parseEnvFile(filePath) {
|
|
298
327
|
const content = fs.readFileSync(filePath, "utf8");
|
|
@@ -493,9 +522,9 @@ function generateT3Env(parsed) {
|
|
|
493
522
|
return lines.join("\n") + "\n";
|
|
494
523
|
}
|
|
495
524
|
var CONTRACT_FILE_NAMES = [
|
|
496
|
-
"env.contract.ts",
|
|
497
525
|
"env.contract.mjs",
|
|
498
|
-
"env.contract.js"
|
|
526
|
+
"env.contract.js",
|
|
527
|
+
"env.contract.ts"
|
|
499
528
|
];
|
|
500
529
|
function defineContract(contract) {
|
|
501
530
|
return contract;
|
|
@@ -504,6 +533,12 @@ async function loadContract(cwd = process.cwd()) {
|
|
|
504
533
|
for (const name of CONTRACT_FILE_NAMES) {
|
|
505
534
|
const filePath = path6__default.default.resolve(cwd, name);
|
|
506
535
|
if (fs.existsSync(filePath)) {
|
|
536
|
+
if (filePath.endsWith(".ts")) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
`Contract file ${filePath} is a TypeScript file and cannot be loaded at runtime by Node.js.
|
|
539
|
+
Rename it to ${filePath.replace(/\.ts$/, ".mjs")} and use ESM syntax (export default ...), or run via \`tsx\` / \`ts-node\` as a loader.`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
507
542
|
const fileUrl = url.pathToFileURL(filePath).href;
|
|
508
543
|
const mod = await import(fileUrl);
|
|
509
544
|
return mod.default;
|
|
@@ -549,7 +584,8 @@ async function readEnvFile(filePath) {
|
|
|
549
584
|
return await promises.readFile(resolved, "utf8");
|
|
550
585
|
} catch (err) {
|
|
551
586
|
if (err instanceof Error && err.code === "ENOENT") {
|
|
552
|
-
|
|
587
|
+
const displayPath = path6__default.default.isAbsolute(filePath) ? filePath : `${filePath} (resolved: ${resolved})`;
|
|
588
|
+
throw new Error(`File not found: ${displayPath}`);
|
|
553
589
|
}
|
|
554
590
|
throw err;
|
|
555
591
|
}
|
|
@@ -586,7 +622,14 @@ function success(message) {
|
|
|
586
622
|
|
|
587
623
|
// src/pipeline.ts
|
|
588
624
|
function deriveOutputPath(base, generator, isSingle) {
|
|
589
|
-
if (isSingle)
|
|
625
|
+
if (isSingle) {
|
|
626
|
+
if (generator === "declaration" && !base.endsWith(".d.ts")) {
|
|
627
|
+
const ext2 = path6__default.default.extname(base);
|
|
628
|
+
const noExt2 = ext2.length > 0 ? base.slice(0, -ext2.length) : base;
|
|
629
|
+
return `${noExt2}.d.ts`;
|
|
630
|
+
}
|
|
631
|
+
return base;
|
|
632
|
+
}
|
|
590
633
|
const ext = path6__default.default.extname(base);
|
|
591
634
|
const noExt = ext.length > 0 ? base.slice(0, -ext.length) : base;
|
|
592
635
|
const baseExt = ext.length > 0 ? ext : ".ts";
|
|
@@ -596,7 +639,8 @@ function deriveOutputPath(base, generator, isSingle) {
|
|
|
596
639
|
function deriveOutputBaseForInput(output, inputPath) {
|
|
597
640
|
const dir = path6__default.default.dirname(output);
|
|
598
641
|
const ext = path6__default.default.extname(output);
|
|
599
|
-
const
|
|
642
|
+
const rawBasename = path6__default.default.basename(inputPath);
|
|
643
|
+
const stem = rawBasename.startsWith(".") ? rawBasename.slice(1).replaceAll(".", "-") : path6__default.default.basename(inputPath, path6__default.default.extname(inputPath));
|
|
600
644
|
return path6__default.default.join(dir, `${stem}${ext}`);
|
|
601
645
|
}
|
|
602
646
|
function buildOutput(generator, parsed) {
|
|
@@ -633,6 +677,12 @@ async function persistOutput(params) {
|
|
|
633
677
|
success(`Generated ${outputPath}`);
|
|
634
678
|
}
|
|
635
679
|
}
|
|
680
|
+
function emitParserWarnings(parsed) {
|
|
681
|
+
if (parsed.warnings === void 0) return;
|
|
682
|
+
for (const w of parsed.warnings) {
|
|
683
|
+
warn(`[${w.code}] ${w.message}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
636
686
|
async function runGenerate(options) {
|
|
637
687
|
const {
|
|
638
688
|
input,
|
|
@@ -655,6 +705,7 @@ async function runGenerate(options) {
|
|
|
655
705
|
inputPath,
|
|
656
706
|
inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
|
|
657
707
|
);
|
|
708
|
+
emitParserWarnings(parsed);
|
|
658
709
|
for (const generator of generators) {
|
|
659
710
|
let generated = buildOutput(generator, parsed);
|
|
660
711
|
if (shouldFormat && !dryRun) {
|
|
@@ -731,7 +782,8 @@ function checkInvalidType(variable, entry, environment) {
|
|
|
731
782
|
break;
|
|
732
783
|
}
|
|
733
784
|
case "boolean": {
|
|
734
|
-
|
|
785
|
+
const lower = value.toLowerCase();
|
|
786
|
+
if (!["true", "false", "1", "0", "yes", "no"].includes(lower)) {
|
|
735
787
|
return {
|
|
736
788
|
code: "ENV_INVALID_TYPE",
|
|
737
789
|
key: variable.key,
|
|
@@ -854,7 +906,8 @@ function validateContract(parsed, contract, opts = {}) {
|
|
|
854
906
|
const parsedByKey = new Map(parsed.vars.map((v) => [v.key, v]));
|
|
855
907
|
for (const entry of contract.vars) {
|
|
856
908
|
if (!entry.required) continue;
|
|
857
|
-
|
|
909
|
+
const existing = parsedByKey.get(entry.name);
|
|
910
|
+
if (existing !== void 0 && existing.rawValue !== "") continue;
|
|
858
911
|
issues.push({
|
|
859
912
|
code: "ENV_MISSING",
|
|
860
913
|
key: entry.name,
|
|
@@ -956,6 +1009,11 @@ function outputHuman(result) {
|
|
|
956
1009
|
async function runCheck(opts) {
|
|
957
1010
|
const contract = await resolveContract(opts.contract, opts.cwd);
|
|
958
1011
|
const parsed = parseEnvFile(opts.input);
|
|
1012
|
+
if (parsed.warnings !== void 0) {
|
|
1013
|
+
for (const w of parsed.warnings) {
|
|
1014
|
+
warn(`[${w.code}] ${w.message}`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
959
1017
|
const validatorOpts = {};
|
|
960
1018
|
if (opts.environment !== void 0) validatorOpts.environment = opts.environment;
|
|
961
1019
|
if (opts.strict !== void 0) validatorOpts.strict = opts.strict;
|
|
@@ -1040,7 +1098,8 @@ async function loadCloudSource(options) {
|
|
|
1040
1098
|
raw = await promises.readFile(resolvedPath, "utf8");
|
|
1041
1099
|
} catch (err) {
|
|
1042
1100
|
if (err instanceof Error && err.code === "ENOENT") {
|
|
1043
|
-
|
|
1101
|
+
const displayPath = path6__default.default.isAbsolute(options.filePath) ? options.filePath : `${options.filePath} (resolved: ${resolvedPath})`;
|
|
1102
|
+
throw new Error(`File not found: ${displayPath}`);
|
|
1044
1103
|
}
|
|
1045
1104
|
throw err;
|
|
1046
1105
|
}
|
|
@@ -1048,7 +1107,7 @@ async function loadCloudSource(options) {
|
|
|
1048
1107
|
return parseProviderPayload(options.provider, parsed);
|
|
1049
1108
|
}
|
|
1050
1109
|
var SECRET_KEY_RE = /(SECRET|TOKEN|PASSWORD|PRIVATE|API_KEY|ACCESS_KEY|CLIENT_SECRET)/i;
|
|
1051
|
-
var CONTRACT_FILE_NAMES2 = ["env.contract.
|
|
1110
|
+
var CONTRACT_FILE_NAMES2 = ["env.contract.mjs", "env.contract.js", "env.contract.ts"];
|
|
1052
1111
|
function isRecord2(value) {
|
|
1053
1112
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1054
1113
|
}
|
|
@@ -1172,6 +1231,12 @@ async function loadValidationContract(options) {
|
|
|
1172
1231
|
const discoveredContractPath = findDefaultContractPath(cwd);
|
|
1173
1232
|
const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path6__default.default.resolve(cwd, contractPath);
|
|
1174
1233
|
if (resolvedContractPath !== void 0 && fs.existsSync(resolvedContractPath)) {
|
|
1234
|
+
if (resolvedContractPath.endsWith(".ts")) {
|
|
1235
|
+
throw new Error(
|
|
1236
|
+
`Contract file ${resolvedContractPath} is a TypeScript file and cannot be loaded at runtime by Node.js.
|
|
1237
|
+
Rename it to ${resolvedContractPath.replace(/\.ts$/, ".mjs")} and use ESM syntax (export default ...), or run via \`tsx\` / \`ts-node\` as a loader.`
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1175
1240
|
const moduleUrl = url.pathToFileURL(resolvedContractPath).href;
|
|
1176
1241
|
const moduleValue = await import(moduleUrl);
|
|
1177
1242
|
const candidate = moduleValue.default ?? moduleValue.contract;
|
|
@@ -1425,7 +1490,7 @@ function isClientSecret(variable, key) {
|
|
|
1425
1490
|
function checkContractVariable(key, variable, context) {
|
|
1426
1491
|
const { options, issues } = context;
|
|
1427
1492
|
const value = options.values[key];
|
|
1428
|
-
const hasValue = value !== void 0;
|
|
1493
|
+
const hasValue = value !== void 0 && (value !== "" || !variable.required);
|
|
1429
1494
|
if (variable.required && !hasValue) {
|
|
1430
1495
|
issues.push(
|
|
1431
1496
|
createIssue({
|
|
@@ -1527,7 +1592,11 @@ function diffTypeConflicts(key, present, context) {
|
|
|
1527
1592
|
for (const entry of present) {
|
|
1528
1593
|
typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
|
|
1529
1594
|
}
|
|
1530
|
-
|
|
1595
|
+
const knownTypes = /* @__PURE__ */ new Set();
|
|
1596
|
+
for (const t of typeBySource.values()) {
|
|
1597
|
+
if (t !== "unknown") knownTypes.add(t);
|
|
1598
|
+
}
|
|
1599
|
+
if (knownTypes.size <= 1) return;
|
|
1531
1600
|
for (const [sourceName, detectedType] of typeBySource.entries()) {
|
|
1532
1601
|
issues.push(
|
|
1533
1602
|
createIssue({
|
|
@@ -1694,6 +1763,7 @@ async function loadEnvSource(options) {
|
|
|
1694
1763
|
return parseEnvSourceContent(content);
|
|
1695
1764
|
} catch (error_) {
|
|
1696
1765
|
if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
|
|
1766
|
+
warn(`Target file not found: ${options.filePath} \u2014 treating as empty`);
|
|
1697
1767
|
return {};
|
|
1698
1768
|
}
|
|
1699
1769
|
throw error_;
|
|
@@ -1861,7 +1931,8 @@ async function loadCommandConfig(configPath) {
|
|
|
1861
1931
|
}
|
|
1862
1932
|
const resolvedPath = path6__default.default.resolve(configPath);
|
|
1863
1933
|
if (!fs.existsSync(resolvedPath)) {
|
|
1864
|
-
|
|
1934
|
+
const displayPath = path6__default.default.isAbsolute(configPath) ? configPath : `${configPath} (resolved: ${resolvedPath})`;
|
|
1935
|
+
throw new Error(`Config file not found: ${displayPath}`);
|
|
1865
1936
|
}
|
|
1866
1937
|
const configDir = path6__default.default.dirname(resolvedPath);
|
|
1867
1938
|
const moduleValue = await import(url.pathToFileURL(resolvedPath).href);
|