@xlameiro/env-typegen 0.1.4 → 0.1.6

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.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 38a33eb: ## Fuzzy Dancers Find — env-typegen QA deficiency fixes (D1-D12)
8
+
9
+ ## 0.1.5
10
+
11
+ ### Patch Changes
12
+
13
+ - 4ad0d2a: ## Fuzzy Dancers Find — env-typegen QA deficiency fixes (D1-D12)
14
+
3
15
  ## 0.1.4
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
@@ -99,7 +99,7 @@ var require_picocolors = __commonJS({
99
99
  });
100
100
 
101
101
  // src/cli.ts
102
- import { realpathSync } from "fs";
102
+ import { existsSync as existsSync6, realpathSync } from "fs";
103
103
  import { createRequire } from "module";
104
104
  import path13 from "path";
105
105
  import { fileURLToPath, pathToFileURL as pathToFileURL5 } from "url";
@@ -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
  {
@@ -599,7 +601,16 @@ function parseEnvFileContent(content, filePath, options) {
599
601
  );
600
602
  commentBlock = [];
601
603
  }
602
- return { filePath, vars, groups };
604
+ const seenKeys = /* @__PURE__ */ new Set();
605
+ const deduped = [];
606
+ for (let i = vars.length - 1; i >= 0; i--) {
607
+ const variable = vars[i];
608
+ if (variable !== void 0 && !seenKeys.has(variable.key)) {
609
+ seenKeys.add(variable.key);
610
+ deduped.unshift(variable);
611
+ }
612
+ }
613
+ return { filePath, vars: deduped, groups };
603
614
  }
604
615
  function parseEnvFile(filePath) {
605
616
  const content = readFileSync(filePath, "utf8");
@@ -610,7 +621,15 @@ function parseEnvFile(filePath) {
610
621
  import { mkdir, readFile, writeFile } from "fs/promises";
611
622
  import path4 from "path";
612
623
  async function readEnvFile(filePath) {
613
- return readFile(path4.resolve(filePath), "utf8");
624
+ const resolved = path4.resolve(filePath);
625
+ try {
626
+ return await readFile(resolved, "utf8");
627
+ } catch (err) {
628
+ if (err instanceof Error && err.code === "ENOENT") {
629
+ throw new Error(`File not found: ${filePath}`);
630
+ }
631
+ throw err;
632
+ }
614
633
  }
615
634
  async function writeOutput(filePath, content) {
616
635
  const resolved = path4.resolve(filePath);
@@ -634,19 +653,30 @@ async function formatOutput(content, parser = "typescript") {
634
653
 
635
654
  // src/utils/logger.ts
636
655
  var import_picocolors = __toESM(require_picocolors(), 1);
656
+ var { green, red, yellow } = import_picocolors.default;
637
657
  function log(message) {
638
658
  console.log(message);
639
659
  }
660
+ function warn(message) {
661
+ console.warn(yellow(`\u26A0 ${message}`));
662
+ }
640
663
  function error(message) {
641
- console.error((0, import_picocolors.red)(`\u2716 ${message}`));
664
+ console.error(red(`\u2716 ${message}`));
642
665
  }
643
666
  function success(message) {
644
- console.log((0, import_picocolors.green)(`\u2714 ${message}`));
667
+ console.log(green(`\u2714 ${message}`));
645
668
  }
646
669
 
647
670
  // src/pipeline.ts
648
671
  function deriveOutputPath(base, generator, isSingle) {
649
- if (isSingle) return base;
672
+ if (isSingle) {
673
+ if (generator === "declaration" && !base.endsWith(".d.ts")) {
674
+ const ext2 = path5.extname(base);
675
+ const noExt2 = ext2.length > 0 ? base.slice(0, -ext2.length) : base;
676
+ return `${noExt2}.d.ts`;
677
+ }
678
+ return base;
679
+ }
650
680
  const ext = path5.extname(base);
651
681
  const noExt = ext.length > 0 ? base.slice(0, -ext.length) : base;
652
682
  const baseExt = ext.length > 0 ? ext : ".ts";
@@ -684,10 +714,6 @@ async function persistOutput(params) {
684
714
  }
685
715
  if (dryRun) {
686
716
  if (!silent) {
687
- if (!isSingle) {
688
- console.log(`// --- ${generator}: ${outputPath} ---`);
689
- }
690
- console.log(generated);
691
717
  success(`Dry run: would write ${outputPath}`);
692
718
  }
693
719
  return;
@@ -739,6 +765,7 @@ async function runGenerate(options) {
739
765
  }
740
766
 
741
767
  // src/validation-command.ts
768
+ import { existsSync as existsSync4 } from "fs";
742
769
  import path11 from "path";
743
770
  import { pathToFileURL as pathToFileURL4 } from "url";
744
771
  import { parseArgs } from "util";
@@ -810,7 +837,15 @@ function parseProviderPayload(provider, value) {
810
837
  }
811
838
  async function loadCloudSource(options) {
812
839
  const resolvedPath = path6.resolve(options.filePath);
813
- const raw = await readFile2(resolvedPath, "utf8");
840
+ let raw;
841
+ try {
842
+ raw = await readFile2(resolvedPath, "utf8");
843
+ } catch (err) {
844
+ if (err instanceof Error && err.code === "ENOENT") {
845
+ throw new Error(`File not found: ${options.filePath}`);
846
+ }
847
+ throw err;
848
+ }
814
849
  const parsed = JSON.parse(raw);
815
850
  return parseProviderPayload(options.provider, parsed);
816
851
  }
@@ -820,7 +855,7 @@ import { existsSync as existsSync2 } from "fs";
820
855
  import path7 from "path";
821
856
  import { pathToFileURL as pathToFileURL2 } from "url";
822
857
  var SECRET_KEY_RE = /(SECRET|TOKEN|PASSWORD|PRIVATE|API_KEY|ACCESS_KEY|CLIENT_SECRET)/i;
823
- var CONTRACT_FILE_NAMES = ["env.contract.ts", "env.contract.mjs", "env.contract.js"];
858
+ var CONTRACT_FILE_NAMES = ["env.contract.mjs", "env.contract.js", "env.contract.ts"];
824
859
  function isRecord2(value) {
825
860
  return typeof value === "object" && value !== null && !Array.isArray(value);
826
861
  }
@@ -944,6 +979,12 @@ async function loadValidationContract(options) {
944
979
  const discoveredContractPath = findDefaultContractPath(cwd);
945
980
  const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path7.resolve(cwd, contractPath);
946
981
  if (resolvedContractPath !== void 0 && existsSync2(resolvedContractPath)) {
982
+ if (resolvedContractPath.endsWith(".ts")) {
983
+ throw new Error(
984
+ `Contract file ${resolvedContractPath} is a TypeScript file and cannot be loaded at runtime by Node.js.
985
+ Rename it to ${resolvedContractPath.replace(/\.ts$/, ".mjs")} and use ESM syntax (export default ...), or run via \`tsx\` / \`ts-node\` as a loader.`
986
+ );
987
+ }
947
988
  const moduleUrl = pathToFileURL2(resolvedContractPath).href;
948
989
  const moduleValue = await import(moduleUrl);
949
990
  const candidate = moduleValue.default ?? moduleValue.contract;
@@ -961,6 +1002,7 @@ async function loadValidationContract(options) {
961
1002
  }
962
1003
 
963
1004
  // src/plugins.ts
1005
+ import { existsSync as existsSync3 } from "fs";
964
1006
  import path8 from "path";
965
1007
  import { pathToFileURL as pathToFileURL3 } from "url";
966
1008
  function isRecord3(value) {
@@ -982,6 +1024,9 @@ function isPlugin(value) {
982
1024
  }
983
1025
  async function loadPluginFromPath(pluginPath, cwd) {
984
1026
  const resolvedPath = path8.resolve(cwd, pluginPath);
1027
+ if (!existsSync3(resolvedPath)) {
1028
+ throw new Error(`Plugin not found: ${pluginPath}`);
1029
+ }
985
1030
  const moduleValue = await import(pathToFileURL3(resolvedPath).href);
986
1031
  const candidate = moduleValue.default ?? moduleValue.plugin;
987
1032
  if (isPlugin(candidate)) return candidate;
@@ -1198,7 +1243,7 @@ function isClientSecret(variable, key) {
1198
1243
  function checkContractVariable(key, variable, context) {
1199
1244
  const { options, issues } = context;
1200
1245
  const value = options.values[key];
1201
- const hasValue = value !== void 0 && value.trim().length > 0;
1246
+ const hasValue = value !== void 0 && (value !== "" || !variable.required);
1202
1247
  if (variable.required && !hasValue) {
1203
1248
  issues.push(
1204
1249
  createIssue({
@@ -1300,7 +1345,11 @@ function diffTypeConflicts(key, present, context) {
1300
1345
  for (const entry of present) {
1301
1346
  typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
1302
1347
  }
1303
- if (new Set(typeBySource.values()).size <= 1) return;
1348
+ const knownTypes = /* @__PURE__ */ new Set();
1349
+ for (const t of typeBySource.values()) {
1350
+ if (t !== "unknown") knownTypes.add(t);
1351
+ }
1352
+ if (knownTypes.size <= 1) return;
1304
1353
  for (const [sourceName, detectedType] of typeBySource.entries()) {
1305
1354
  issues.push(
1306
1355
  createIssue({
@@ -1379,12 +1428,8 @@ function diffEnvironmentSources(options) {
1379
1428
  sourceName,
1380
1429
  value: options.sources[sourceName]?.[key]
1381
1430
  }));
1382
- const present = valuesBySource.filter(
1383
- (entry) => entry.value !== void 0 && entry.value !== ""
1384
- );
1385
- const missing = valuesBySource.filter(
1386
- (entry) => entry.value === void 0 || entry.value === ""
1387
- );
1431
+ const present = valuesBySource.filter((entry) => entry.value !== void 0);
1432
+ const missing = valuesBySource.filter((entry) => entry.value === void 0);
1388
1433
  if (present.length === 0 && variable?.required === true) {
1389
1434
  for (const entry of missing) {
1390
1435
  issues.push(
@@ -1475,6 +1520,7 @@ async function loadEnvSource(options) {
1475
1520
  return parseEnvSourceContent(content);
1476
1521
  } catch (error_) {
1477
1522
  if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
1523
+ warn(`Target file not found: ${options.filePath} \u2014 treating as empty`);
1478
1524
  return {};
1479
1525
  }
1480
1526
  throw error_;
@@ -1645,6 +1691,9 @@ async function loadCommandConfig(configPath) {
1645
1691
  return loadConfig(process.cwd());
1646
1692
  }
1647
1693
  const resolvedPath = path11.resolve(configPath);
1694
+ if (!existsSync4(resolvedPath)) {
1695
+ throw new Error(`Config file not found: ${configPath}`);
1696
+ }
1648
1697
  const configDir = path11.dirname(resolvedPath);
1649
1698
  const moduleValue = await import(pathToFileURL4(resolvedPath).href);
1650
1699
  if (moduleValue.default === void 0) return void 0;
@@ -1904,6 +1953,7 @@ async function runValidationCommand(params) {
1904
1953
 
1905
1954
  // src/watch.ts
1906
1955
  import { watch } from "chokidar";
1956
+ import { existsSync as existsSync5 } from "fs";
1907
1957
  import path12 from "path";
1908
1958
  function debounce(fn, delay) {
1909
1959
  let timer;
@@ -1916,6 +1966,13 @@ function debounce(fn, delay) {
1916
1966
  };
1917
1967
  }
1918
1968
  function startWatch({ inputPath, runOptions, cwd = process.cwd() }) {
1969
+ const inputPaths = Array.isArray(inputPath) ? inputPath : [inputPath];
1970
+ for (const singlePath of inputPaths) {
1971
+ if (!existsSync5(singlePath)) {
1972
+ error(`File not found: ${singlePath}`);
1973
+ return process.exit(1);
1974
+ }
1975
+ }
1919
1976
  const inputLabel = Array.isArray(inputPath) ? inputPath.join(", ") : inputPath;
1920
1977
  log(`Watching ${inputLabel} for changes...`);
1921
1978
  void runGenerate(runOptions).catch((err) => {
@@ -2061,6 +2118,15 @@ async function runValidationSubcommand(subcommand, argv) {
2061
2118
  process.exitCode = exitCode;
2062
2119
  }
2063
2120
  }
2121
+ async function loadExplicitConfig(configPath, userPath) {
2122
+ if (!existsSync6(configPath)) {
2123
+ throw new Error(`Config file not found: ${userPath}`);
2124
+ }
2125
+ const configDir = path13.dirname(configPath);
2126
+ const mod = await import(pathToFileURL5(configPath).href);
2127
+ const rawConfig = mod.default;
2128
+ return rawConfig ? applyConfigPaths2(rawConfig, configDir) : void 0;
2129
+ }
2064
2130
  async function runCli(argv = process.argv.slice(2)) {
2065
2131
  const maybeSubcommand = argv[0];
2066
2132
  if (maybeSubcommand !== void 0 && VALIDATION_SUBCOMMANDS.has(maybeSubcommand)) {
@@ -2096,13 +2162,7 @@ async function runCli(argv = process.argv.slice(2)) {
2096
2162
  if (values.config === void 0) {
2097
2163
  fileConfig = await loadConfig(process.cwd());
2098
2164
  } else {
2099
- const configPath = path13.resolve(values.config);
2100
- const configDir = path13.dirname(configPath);
2101
- const mod = await import(pathToFileURL5(configPath).href);
2102
- const rawConfig = mod.default;
2103
- if (rawConfig) {
2104
- fileConfig = applyConfigPaths2(rawConfig, configDir);
2105
- }
2165
+ fileConfig = await loadExplicitConfig(path13.resolve(values.config), values.config);
2106
2166
  }
2107
2167
  const cliInput = values.input?.length ? values.input : void 0;
2108
2168
  const input = cliInput ?? fileConfig?.input;