@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/dist/index.cjs CHANGED
@@ -164,7 +164,9 @@ var inferenceRules = [
164
164
  {
165
165
  id: "P10_url_scheme",
166
166
  priority: 10,
167
- match: (_key, value) => /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value),
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
- return { filePath, vars, groups };
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
- throw new Error(`File not found: ${filePath}`);
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) return base;
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 stem = path6__default.default.basename(inputPath, path6__default.default.extname(inputPath));
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
- if (value !== "true" && value !== "false") {
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
- if (parsedByKey.has(entry.name)) continue;
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
- throw new Error(`File not found: ${options.filePath}`);
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.ts", "env.contract.mjs", "env.contract.js"];
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
- if (new Set(typeBySource.values()).size <= 1) return;
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
- throw new Error(`Config file not found: ${configPath}`);
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);