@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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 97738bd: ## Fuzzy Dancers Find — env-typegen QA deficiency fixes (D1-D12)
8
+
9
+ ## 0.1.6
10
+
11
+ ### Patch Changes
12
+
13
+ - 38a33eb: ## Fuzzy Dancers Find — env-typegen QA deficiency fixes (D1-D12)
14
+
3
15
  ## 0.1.5
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -1,9 +1,14 @@
1
1
  # env-typegen
2
2
 
3
3
  > From `.env.example` to typed outputs and contract-based environment governance.
4
+ >
5
+ > If this package saves debugging time for your team, consider starring the repository.
4
6
 
5
7
  [![npm version](https://badge.fury.io/js/%40xlameiro%2Fenv-typegen.svg)](https://npmjs.com/package/@xlameiro/env-typegen)
8
+ [![npm downloads](https://img.shields.io/npm/dm/@xlameiro/env-typegen)](https://npmjs.com/package/@xlameiro/env-typegen)
6
9
  [![CI](https://github.com/xlameiro/env-typegen/actions/workflows/ci.yml/badge.svg)](https://github.com/xlameiro/env-typegen/actions/workflows/ci.yml)
10
+ [![GitHub stars](https://img.shields.io/github/stars/xlameiro/env-typegen?style=social)](https://github.com/xlameiro/env-typegen/stargazers)
11
+ [![Maintainer](https://img.shields.io/badge/maintainer-xlameiro-0ea5e9)](https://github.com/xlameiro)
7
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
13
 
9
14
  ## What this package does
@@ -134,6 +139,13 @@ Start with `check`. Add `diff` or `doctor` as your pipeline maturity grows.
134
139
  - Package docs index: [`/packages/env-typegen/docs`](./docs)
135
140
  - Changelog: [`CHANGELOG.md`](./CHANGELOG.md)
136
141
 
142
+ ## Trust signals
143
+
144
+ - Maintained by [@xlameiro](https://github.com/xlameiro)
145
+ - Security policy: [`SECURITY.md`](../../SECURITY.md)
146
+ - Contribution guide: [`CONTRIBUTING.md`](../../CONTRIBUTING.md)
147
+ - Published releases: [npm package page](https://www.npmjs.com/package/@xlameiro/env-typegen)
148
+
137
149
  ## Status
138
150
 
139
151
  `env-typegen` is actively maintained and published on npm.
package/dist/cli.js CHANGED
@@ -388,7 +388,9 @@ var inferenceRules = [
388
388
  {
389
389
  id: "P10_url_scheme",
390
390
  priority: 10,
391
- match: (_key, value) => /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value),
391
+ // BUG-02: comma-separated URL lists (e.g. ALLOWED_ORIGINS) must NOT be inferred as
392
+ // a single URL — they don't pass z.string().url() or new URL() validation at runtime.
393
+ match: (_key, value) => !value.includes(",") && /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value),
392
394
  type: "url"
393
395
  },
394
396
  {
@@ -557,6 +559,27 @@ function buildParsedVar(params, commentBlock, options) {
557
559
  }
558
560
  return parsedVar;
559
561
  }
562
+ function deduplicateVars(vars) {
563
+ const seenKeys = /* @__PURE__ */ new Set();
564
+ const deduped = [];
565
+ const warnings = [];
566
+ for (let i = vars.length - 1; i >= 0; i--) {
567
+ const variable = vars[i];
568
+ if (variable === void 0) continue;
569
+ if (seenKeys.has(variable.key)) {
570
+ warnings.push({
571
+ code: "ENV_DUPLICATE_KEY",
572
+ message: `Duplicate key "${variable.key}" at line ${variable.lineNumber} \u2014 last occurrence wins.`,
573
+ line: variable.lineNumber,
574
+ key: variable.key
575
+ });
576
+ } else {
577
+ seenKeys.add(variable.key);
578
+ deduped.unshift(variable);
579
+ }
580
+ }
581
+ return { deduped, warnings };
582
+ }
560
583
  function parseEnvFileContent(content, filePath, options) {
561
584
  const lines = content.split("\n");
562
585
  const vars = [];
@@ -599,7 +622,13 @@ function parseEnvFileContent(content, filePath, options) {
599
622
  );
600
623
  commentBlock = [];
601
624
  }
602
- return { filePath, vars, groups };
625
+ const { deduped, warnings } = deduplicateVars(vars);
626
+ return {
627
+ filePath,
628
+ vars: deduped,
629
+ groups,
630
+ ...warnings.length > 0 && { warnings }
631
+ };
603
632
  }
604
633
  function parseEnvFile(filePath) {
605
634
  const content = readFileSync(filePath, "utf8");
@@ -615,7 +644,8 @@ async function readEnvFile(filePath) {
615
644
  return await readFile(resolved, "utf8");
616
645
  } catch (err) {
617
646
  if (err instanceof Error && err.code === "ENOENT") {
618
- throw new Error(`File not found: ${filePath}`);
647
+ const displayPath = path4.isAbsolute(filePath) ? filePath : `${filePath} (resolved: ${resolved})`;
648
+ throw new Error(`File not found: ${displayPath}`);
619
649
  }
620
650
  throw err;
621
651
  }
@@ -646,6 +676,9 @@ var { green, red, yellow } = import_picocolors.default;
646
676
  function log(message) {
647
677
  console.log(message);
648
678
  }
679
+ function warn(message) {
680
+ console.warn(yellow(`\u26A0 ${message}`));
681
+ }
649
682
  function error(message) {
650
683
  console.error(red(`\u2716 ${message}`));
651
684
  }
@@ -655,7 +688,14 @@ function success(message) {
655
688
 
656
689
  // src/pipeline.ts
657
690
  function deriveOutputPath(base, generator, isSingle) {
658
- if (isSingle) return base;
691
+ if (isSingle) {
692
+ if (generator === "declaration" && !base.endsWith(".d.ts")) {
693
+ const ext2 = path5.extname(base);
694
+ const noExt2 = ext2.length > 0 ? base.slice(0, -ext2.length) : base;
695
+ return `${noExt2}.d.ts`;
696
+ }
697
+ return base;
698
+ }
659
699
  const ext = path5.extname(base);
660
700
  const noExt = ext.length > 0 ? base.slice(0, -ext.length) : base;
661
701
  const baseExt = ext.length > 0 ? ext : ".ts";
@@ -665,7 +705,8 @@ function deriveOutputPath(base, generator, isSingle) {
665
705
  function deriveOutputBaseForInput(output, inputPath) {
666
706
  const dir = path5.dirname(output);
667
707
  const ext = path5.extname(output);
668
- const stem = path5.basename(inputPath, path5.extname(inputPath));
708
+ const rawBasename = path5.basename(inputPath);
709
+ const stem = rawBasename.startsWith(".") ? rawBasename.slice(1).replaceAll(".", "-") : path5.basename(inputPath, path5.extname(inputPath));
669
710
  return path5.join(dir, `${stem}${ext}`);
670
711
  }
671
712
  function buildOutput(generator, parsed) {
@@ -702,6 +743,12 @@ async function persistOutput(params) {
702
743
  success(`Generated ${outputPath}`);
703
744
  }
704
745
  }
746
+ function emitParserWarnings(parsed) {
747
+ if (parsed.warnings === void 0) return;
748
+ for (const w of parsed.warnings) {
749
+ warn(`[${w.code}] ${w.message}`);
750
+ }
751
+ }
705
752
  async function runGenerate(options) {
706
753
  const {
707
754
  input,
@@ -724,6 +771,7 @@ async function runGenerate(options) {
724
771
  inputPath,
725
772
  inferenceRules2 === void 0 ? void 0 : { inferenceRules: inferenceRules2 }
726
773
  );
774
+ emitParserWarnings(parsed);
727
775
  for (const generator of generators) {
728
776
  let generated = buildOutput(generator, parsed);
729
777
  if (shouldFormat && !dryRun) {
@@ -821,7 +869,8 @@ async function loadCloudSource(options) {
821
869
  raw = await readFile2(resolvedPath, "utf8");
822
870
  } catch (err) {
823
871
  if (err instanceof Error && err.code === "ENOENT") {
824
- throw new Error(`File not found: ${options.filePath}`);
872
+ const displayPath = path6.isAbsolute(options.filePath) ? options.filePath : `${options.filePath} (resolved: ${resolvedPath})`;
873
+ throw new Error(`File not found: ${displayPath}`);
825
874
  }
826
875
  throw err;
827
876
  }
@@ -834,7 +883,7 @@ import { existsSync as existsSync2 } from "fs";
834
883
  import path7 from "path";
835
884
  import { pathToFileURL as pathToFileURL2 } from "url";
836
885
  var SECRET_KEY_RE = /(SECRET|TOKEN|PASSWORD|PRIVATE|API_KEY|ACCESS_KEY|CLIENT_SECRET)/i;
837
- var CONTRACT_FILE_NAMES = ["env.contract.ts", "env.contract.mjs", "env.contract.js"];
886
+ var CONTRACT_FILE_NAMES = ["env.contract.mjs", "env.contract.js", "env.contract.ts"];
838
887
  function isRecord2(value) {
839
888
  return typeof value === "object" && value !== null && !Array.isArray(value);
840
889
  }
@@ -958,6 +1007,12 @@ async function loadValidationContract(options) {
958
1007
  const discoveredContractPath = findDefaultContractPath(cwd);
959
1008
  const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path7.resolve(cwd, contractPath);
960
1009
  if (resolvedContractPath !== void 0 && existsSync2(resolvedContractPath)) {
1010
+ if (resolvedContractPath.endsWith(".ts")) {
1011
+ throw new Error(
1012
+ `Contract file ${resolvedContractPath} is a TypeScript file and cannot be loaded at runtime by Node.js.
1013
+ Rename it to ${resolvedContractPath.replace(/\.ts$/, ".mjs")} and use ESM syntax (export default ...), or run via \`tsx\` / \`ts-node\` as a loader.`
1014
+ );
1015
+ }
961
1016
  const moduleUrl = pathToFileURL2(resolvedContractPath).href;
962
1017
  const moduleValue = await import(moduleUrl);
963
1018
  const candidate = moduleValue.default ?? moduleValue.contract;
@@ -1216,7 +1271,7 @@ function isClientSecret(variable, key) {
1216
1271
  function checkContractVariable(key, variable, context) {
1217
1272
  const { options, issues } = context;
1218
1273
  const value = options.values[key];
1219
- const hasValue = value !== void 0;
1274
+ const hasValue = value !== void 0 && (value !== "" || !variable.required);
1220
1275
  if (variable.required && !hasValue) {
1221
1276
  issues.push(
1222
1277
  createIssue({
@@ -1318,7 +1373,11 @@ function diffTypeConflicts(key, present, context) {
1318
1373
  for (const entry of present) {
1319
1374
  typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
1320
1375
  }
1321
- if (new Set(typeBySource.values()).size <= 1) return;
1376
+ const knownTypes = /* @__PURE__ */ new Set();
1377
+ for (const t of typeBySource.values()) {
1378
+ if (t !== "unknown") knownTypes.add(t);
1379
+ }
1380
+ if (knownTypes.size <= 1) return;
1322
1381
  for (const [sourceName, detectedType] of typeBySource.entries()) {
1323
1382
  issues.push(
1324
1383
  createIssue({
@@ -1489,6 +1548,7 @@ async function loadEnvSource(options) {
1489
1548
  return parseEnvSourceContent(content);
1490
1549
  } catch (error_) {
1491
1550
  if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
1551
+ warn(`Target file not found: ${options.filePath} \u2014 treating as empty`);
1492
1552
  return {};
1493
1553
  }
1494
1554
  throw error_;
@@ -1660,7 +1720,8 @@ async function loadCommandConfig(configPath) {
1660
1720
  }
1661
1721
  const resolvedPath = path11.resolve(configPath);
1662
1722
  if (!existsSync4(resolvedPath)) {
1663
- throw new Error(`Config file not found: ${configPath}`);
1723
+ const displayPath = path11.isAbsolute(configPath) ? configPath : `${configPath} (resolved: ${resolvedPath})`;
1724
+ throw new Error(`Config file not found: ${displayPath}`);
1664
1725
  }
1665
1726
  const configDir = path11.dirname(resolvedPath);
1666
1727
  const moduleValue = await import(pathToFileURL4(resolvedPath).href);
@@ -2088,7 +2149,8 @@ async function runValidationSubcommand(subcommand, argv) {
2088
2149
  }
2089
2150
  async function loadExplicitConfig(configPath, userPath) {
2090
2151
  if (!existsSync6(configPath)) {
2091
- throw new Error(`Config file not found: ${userPath}`);
2152
+ const displayPath = path13.isAbsolute(userPath) ? userPath : `${userPath} (resolved: ${configPath})`;
2153
+ throw new Error(`Config file not found: ${displayPath}`);
2092
2154
  }
2093
2155
  const configDir = path13.dirname(configPath);
2094
2156
  const mod = await import(pathToFileURL5(configPath).href);
@@ -2096,6 +2158,9 @@ async function loadExplicitConfig(configPath, userPath) {
2096
2158
  return rawConfig ? applyConfigPaths2(rawConfig, configDir) : void 0;
2097
2159
  }
2098
2160
  async function runCli(argv = process.argv.slice(2)) {
2161
+ if (argv[0] === "generate") {
2162
+ argv = argv.slice(1);
2163
+ }
2099
2164
  const maybeSubcommand = argv[0];
2100
2165
  if (maybeSubcommand !== void 0 && VALIDATION_SUBCOMMANDS.has(maybeSubcommand)) {
2101
2166
  await runValidationSubcommand(maybeSubcommand, argv.slice(1));