@xlameiro/env-typegen 0.1.6 → 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.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
  /**
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
  /**
package/dist/index.js CHANGED
@@ -245,6 +245,27 @@ function buildParsedVar(params, commentBlock, options) {
245
245
  }
246
246
  return parsedVar;
247
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
+ }
248
269
  function parseEnvFileContent(content, filePath, options) {
249
270
  const lines = content.split("\n");
250
271
  const vars = [];
@@ -287,16 +308,13 @@ function parseEnvFileContent(content, filePath, options) {
287
308
  );
288
309
  commentBlock = [];
289
310
  }
290
- const seenKeys = /* @__PURE__ */ new Set();
291
- const deduped = [];
292
- for (let i = vars.length - 1; i >= 0; i--) {
293
- const variable = vars[i];
294
- if (variable !== void 0 && !seenKeys.has(variable.key)) {
295
- seenKeys.add(variable.key);
296
- deduped.unshift(variable);
297
- }
298
- }
299
- return { filePath, vars: deduped, groups };
311
+ const { deduped, warnings } = deduplicateVars(vars);
312
+ return {
313
+ filePath,
314
+ vars: deduped,
315
+ groups,
316
+ ...warnings.length > 0 && { warnings }
317
+ };
300
318
  }
301
319
  function parseEnvFile(filePath) {
302
320
  const content = readFileSync(filePath, "utf8");
@@ -559,7 +577,8 @@ async function readEnvFile(filePath) {
559
577
  return await readFile(resolved, "utf8");
560
578
  } catch (err) {
561
579
  if (err instanceof Error && err.code === "ENOENT") {
562
- throw new Error(`File not found: ${filePath}`);
580
+ const displayPath = path6.isAbsolute(filePath) ? filePath : `${filePath} (resolved: ${resolved})`;
581
+ throw new Error(`File not found: ${displayPath}`);
563
582
  }
564
583
  throw err;
565
584
  }
@@ -613,7 +632,8 @@ function deriveOutputPath(base, generator, isSingle) {
613
632
  function deriveOutputBaseForInput(output, inputPath) {
614
633
  const dir = path6.dirname(output);
615
634
  const ext = path6.extname(output);
616
- const stem = path6.basename(inputPath, path6.extname(inputPath));
635
+ const rawBasename = path6.basename(inputPath);
636
+ const stem = rawBasename.startsWith(".") ? rawBasename.slice(1).replaceAll(".", "-") : path6.basename(inputPath, path6.extname(inputPath));
617
637
  return path6.join(dir, `${stem}${ext}`);
618
638
  }
619
639
  function buildOutput(generator, parsed) {
@@ -650,6 +670,12 @@ async function persistOutput(params) {
650
670
  success(`Generated ${outputPath}`);
651
671
  }
652
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
+ }
653
679
  async function runGenerate(options) {
654
680
  const {
655
681
  input,
@@ -672,6 +698,7 @@ async function runGenerate(options) {
672
698
  inputPath,
673
699
  inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
674
700
  );
701
+ emitParserWarnings(parsed);
675
702
  for (const generator of generators) {
676
703
  let generated = buildOutput(generator, parsed);
677
704
  if (shouldFormat && !dryRun) {
@@ -975,6 +1002,11 @@ function outputHuman(result) {
975
1002
  async function runCheck(opts) {
976
1003
  const contract = await resolveContract(opts.contract, opts.cwd);
977
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
+ }
978
1010
  const validatorOpts = {};
979
1011
  if (opts.environment !== void 0) validatorOpts.environment = opts.environment;
980
1012
  if (opts.strict !== void 0) validatorOpts.strict = opts.strict;
@@ -1059,7 +1091,8 @@ async function loadCloudSource(options) {
1059
1091
  raw = await readFile(resolvedPath, "utf8");
1060
1092
  } catch (err) {
1061
1093
  if (err instanceof Error && err.code === "ENOENT") {
1062
- throw new Error(`File not found: ${options.filePath}`);
1094
+ const displayPath = path6.isAbsolute(options.filePath) ? options.filePath : `${options.filePath} (resolved: ${resolvedPath})`;
1095
+ throw new Error(`File not found: ${displayPath}`);
1063
1096
  }
1064
1097
  throw err;
1065
1098
  }
@@ -1891,7 +1924,8 @@ async function loadCommandConfig(configPath) {
1891
1924
  }
1892
1925
  const resolvedPath = path6.resolve(configPath);
1893
1926
  if (!existsSync(resolvedPath)) {
1894
- throw new Error(`Config file not found: ${configPath}`);
1927
+ const displayPath = path6.isAbsolute(configPath) ? configPath : `${configPath} (resolved: ${resolvedPath})`;
1928
+ throw new Error(`Config file not found: ${displayPath}`);
1895
1929
  }
1896
1930
  const configDir = path6.dirname(resolvedPath);
1897
1931
  const moduleValue = await import(pathToFileURL(resolvedPath).href);