@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/dist/index.d.cts CHANGED
@@ -461,9 +461,12 @@ type EnvContract$1 = {
461
461
 
462
462
  /**
463
463
  * Contract file names searched in order when calling {@link loadContract}.
464
- * Mirrors the search strategy used by `loadConfig` in `config.ts`.
464
+ * BUG-01: .mjs and .js are searched before .ts — TypeScript files cannot be
465
+ * loaded by Node.js import() at runtime without a loader like tsx or ts-node.
466
+ * Discovering a .mjs file first prevents a confusing ERR_UNKNOWN_FILE_EXTENSION
467
+ * failure when both .ts and .mjs files coexist in the same directory.
465
468
  */
466
- declare const CONTRACT_FILE_NAMES: readonly ["env.contract.ts", "env.contract.mjs", "env.contract.js"];
469
+ declare const CONTRACT_FILE_NAMES: readonly ["env.contract.mjs", "env.contract.js", "env.contract.ts"];
467
470
  /**
468
471
  * Type-safe contract factory. Returns the contract object unchanged — exists
469
472
  * purely for IDE autocompletion and compile-time validation of the contract shape.
package/dist/index.d.ts CHANGED
@@ -461,9 +461,12 @@ type EnvContract$1 = {
461
461
 
462
462
  /**
463
463
  * Contract file names searched in order when calling {@link loadContract}.
464
- * Mirrors the search strategy used by `loadConfig` in `config.ts`.
464
+ * BUG-01: .mjs and .js are searched before .ts — TypeScript files cannot be
465
+ * loaded by Node.js import() at runtime without a loader like tsx or ts-node.
466
+ * Discovering a .mjs file first prevents a confusing ERR_UNKNOWN_FILE_EXTENSION
467
+ * failure when both .ts and .mjs files coexist in the same directory.
465
468
  */
466
- declare const CONTRACT_FILE_NAMES: readonly ["env.contract.ts", "env.contract.mjs", "env.contract.js"];
469
+ declare const CONTRACT_FILE_NAMES: readonly ["env.contract.mjs", "env.contract.js", "env.contract.ts"];
467
470
  /**
468
471
  * Type-safe contract factory. Returns the contract object unchanged — exists
469
472
  * purely for IDE autocompletion and compile-time validation of the contract shape.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import path6 from 'path';
3
3
  import { pathToFileURL } from 'url';
4
4
  import { readFile, mkdir, writeFile } from 'fs/promises';
5
5
  import { format } from 'prettier';
6
- import { green, red, yellow } from 'picocolors';
6
+ import pc from 'picocolors';
7
7
  import { parseArgs } from 'util';
8
8
 
9
9
  // src/parser/comment-parser.ts
@@ -157,7 +157,9 @@ var inferenceRules = [
157
157
  {
158
158
  id: "P10_url_scheme",
159
159
  priority: 10,
160
- match: (_key, value) => /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value),
160
+ // BUG-02: comma-separated URL lists (e.g. ALLOWED_ORIGINS) must NOT be inferred as
161
+ // a single URL — they don't pass z.string().url() or new URL() validation at runtime.
162
+ match: (_key, value) => !value.includes(",") && /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value),
161
163
  type: "url"
162
164
  },
163
165
  {
@@ -285,7 +287,16 @@ function parseEnvFileContent(content, filePath, options) {
285
287
  );
286
288
  commentBlock = [];
287
289
  }
288
- return { filePath, vars, groups };
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 };
289
300
  }
290
301
  function parseEnvFile(filePath) {
291
302
  const content = readFileSync(filePath, "utf8");
@@ -486,9 +497,9 @@ function generateT3Env(parsed) {
486
497
  return lines.join("\n") + "\n";
487
498
  }
488
499
  var CONTRACT_FILE_NAMES = [
489
- "env.contract.ts",
490
500
  "env.contract.mjs",
491
- "env.contract.js"
501
+ "env.contract.js",
502
+ "env.contract.ts"
492
503
  ];
493
504
  function defineContract(contract) {
494
505
  return contract;
@@ -497,6 +508,12 @@ async function loadContract(cwd = process.cwd()) {
497
508
  for (const name of CONTRACT_FILE_NAMES) {
498
509
  const filePath = path6.resolve(cwd, name);
499
510
  if (existsSync(filePath)) {
511
+ if (filePath.endsWith(".ts")) {
512
+ throw new Error(
513
+ `Contract file ${filePath} is a TypeScript file and cannot be loaded at runtime by Node.js.
514
+ Rename it to ${filePath.replace(/\.ts$/, ".mjs")} and use ESM syntax (export default ...), or run via \`tsx\` / \`ts-node\` as a loader.`
515
+ );
516
+ }
500
517
  const fileUrl = pathToFileURL(filePath).href;
501
518
  const mod = await import(fileUrl);
502
519
  return mod.default;
@@ -537,7 +554,15 @@ env-typegen.config.mjs for runtime loading.`
537
554
  return void 0;
538
555
  }
539
556
  async function readEnvFile(filePath) {
540
- return readFile(path6.resolve(filePath), "utf8");
557
+ const resolved = path6.resolve(filePath);
558
+ try {
559
+ return await readFile(resolved, "utf8");
560
+ } catch (err) {
561
+ if (err instanceof Error && err.code === "ENOENT") {
562
+ throw new Error(`File not found: ${filePath}`);
563
+ }
564
+ throw err;
565
+ }
541
566
  }
542
567
  async function writeOutput(filePath, content) {
543
568
  const resolved = path6.resolve(filePath);
@@ -555,6 +580,7 @@ async function formatOutput(content, parser = "typescript") {
555
580
  return content;
556
581
  }
557
582
  }
583
+ var { green, red, yellow } = pc;
558
584
  function log(message) {
559
585
  console.log(message);
560
586
  }
@@ -570,7 +596,14 @@ function success(message) {
570
596
 
571
597
  // src/pipeline.ts
572
598
  function deriveOutputPath(base, generator, isSingle) {
573
- if (isSingle) return base;
599
+ if (isSingle) {
600
+ if (generator === "declaration" && !base.endsWith(".d.ts")) {
601
+ const ext2 = path6.extname(base);
602
+ const noExt2 = ext2.length > 0 ? base.slice(0, -ext2.length) : base;
603
+ return `${noExt2}.d.ts`;
604
+ }
605
+ return base;
606
+ }
574
607
  const ext = path6.extname(base);
575
608
  const noExt = ext.length > 0 ? base.slice(0, -ext.length) : base;
576
609
  const baseExt = ext.length > 0 ? ext : ".ts";
@@ -608,10 +641,6 @@ async function persistOutput(params) {
608
641
  }
609
642
  if (dryRun) {
610
643
  if (!silent) {
611
- if (!isSingle) {
612
- console.log(`// --- ${generator}: ${outputPath} ---`);
613
- }
614
- console.log(generated);
615
644
  success(`Dry run: would write ${outputPath}`);
616
645
  }
617
646
  return;
@@ -719,7 +748,8 @@ function checkInvalidType(variable, entry, environment) {
719
748
  break;
720
749
  }
721
750
  case "boolean": {
722
- if (value !== "true" && value !== "false") {
751
+ const lower = value.toLowerCase();
752
+ if (!["true", "false", "1", "0", "yes", "no"].includes(lower)) {
723
753
  return {
724
754
  code: "ENV_INVALID_TYPE",
725
755
  key: variable.key,
@@ -842,7 +872,8 @@ function validateContract(parsed, contract, opts = {}) {
842
872
  const parsedByKey = new Map(parsed.vars.map((v) => [v.key, v]));
843
873
  for (const entry of contract.vars) {
844
874
  if (!entry.required) continue;
845
- if (parsedByKey.has(entry.name)) continue;
875
+ const existing = parsedByKey.get(entry.name);
876
+ if (existing !== void 0 && existing.rawValue !== "") continue;
846
877
  issues.push({
847
878
  code: "ENV_MISSING",
848
879
  key: entry.name,
@@ -1023,12 +1054,20 @@ function parseProviderPayload(provider, value) {
1023
1054
  }
1024
1055
  async function loadCloudSource(options) {
1025
1056
  const resolvedPath = path6.resolve(options.filePath);
1026
- const raw = await readFile(resolvedPath, "utf8");
1057
+ let raw;
1058
+ try {
1059
+ raw = await readFile(resolvedPath, "utf8");
1060
+ } catch (err) {
1061
+ if (err instanceof Error && err.code === "ENOENT") {
1062
+ throw new Error(`File not found: ${options.filePath}`);
1063
+ }
1064
+ throw err;
1065
+ }
1027
1066
  const parsed = JSON.parse(raw);
1028
1067
  return parseProviderPayload(options.provider, parsed);
1029
1068
  }
1030
1069
  var SECRET_KEY_RE = /(SECRET|TOKEN|PASSWORD|PRIVATE|API_KEY|ACCESS_KEY|CLIENT_SECRET)/i;
1031
- var CONTRACT_FILE_NAMES2 = ["env.contract.ts", "env.contract.mjs", "env.contract.js"];
1070
+ var CONTRACT_FILE_NAMES2 = ["env.contract.mjs", "env.contract.js", "env.contract.ts"];
1032
1071
  function isRecord2(value) {
1033
1072
  return typeof value === "object" && value !== null && !Array.isArray(value);
1034
1073
  }
@@ -1152,6 +1191,12 @@ async function loadValidationContract(options) {
1152
1191
  const discoveredContractPath = findDefaultContractPath(cwd);
1153
1192
  const resolvedContractPath = contractPath === void 0 ? discoveredContractPath : path6.resolve(cwd, contractPath);
1154
1193
  if (resolvedContractPath !== void 0 && existsSync(resolvedContractPath)) {
1194
+ if (resolvedContractPath.endsWith(".ts")) {
1195
+ throw new Error(
1196
+ `Contract file ${resolvedContractPath} is a TypeScript file and cannot be loaded at runtime by Node.js.
1197
+ Rename it to ${resolvedContractPath.replace(/\.ts$/, ".mjs")} and use ESM syntax (export default ...), or run via \`tsx\` / \`ts-node\` as a loader.`
1198
+ );
1199
+ }
1155
1200
  const moduleUrl = pathToFileURL(resolvedContractPath).href;
1156
1201
  const moduleValue = await import(moduleUrl);
1157
1202
  const candidate = moduleValue.default ?? moduleValue.contract;
@@ -1186,6 +1231,9 @@ function isPlugin(value) {
1186
1231
  }
1187
1232
  async function loadPluginFromPath(pluginPath, cwd) {
1188
1233
  const resolvedPath = path6.resolve(cwd, pluginPath);
1234
+ if (!existsSync(resolvedPath)) {
1235
+ throw new Error(`Plugin not found: ${pluginPath}`);
1236
+ }
1189
1237
  const moduleValue = await import(pathToFileURL(resolvedPath).href);
1190
1238
  const candidate = moduleValue.default ?? moduleValue.plugin;
1191
1239
  if (isPlugin(candidate)) return candidate;
@@ -1402,7 +1450,7 @@ function isClientSecret(variable, key) {
1402
1450
  function checkContractVariable(key, variable, context) {
1403
1451
  const { options, issues } = context;
1404
1452
  const value = options.values[key];
1405
- const hasValue = value !== void 0 && value.trim().length > 0;
1453
+ const hasValue = value !== void 0 && (value !== "" || !variable.required);
1406
1454
  if (variable.required && !hasValue) {
1407
1455
  issues.push(
1408
1456
  createIssue({
@@ -1504,7 +1552,11 @@ function diffTypeConflicts(key, present, context) {
1504
1552
  for (const entry of present) {
1505
1553
  typeBySource.set(entry.sourceName, detectReceivedType(entry.value ?? ""));
1506
1554
  }
1507
- if (new Set(typeBySource.values()).size <= 1) return;
1555
+ const knownTypes = /* @__PURE__ */ new Set();
1556
+ for (const t of typeBySource.values()) {
1557
+ if (t !== "unknown") knownTypes.add(t);
1558
+ }
1559
+ if (knownTypes.size <= 1) return;
1508
1560
  for (const [sourceName, detectedType] of typeBySource.entries()) {
1509
1561
  issues.push(
1510
1562
  createIssue({
@@ -1583,12 +1635,8 @@ function diffEnvironmentSources(options) {
1583
1635
  sourceName,
1584
1636
  value: options.sources[sourceName]?.[key]
1585
1637
  }));
1586
- const present = valuesBySource.filter(
1587
- (entry) => entry.value !== void 0 && entry.value !== ""
1588
- );
1589
- const missing = valuesBySource.filter(
1590
- (entry) => entry.value === void 0 || entry.value === ""
1591
- );
1638
+ const present = valuesBySource.filter((entry) => entry.value !== void 0);
1639
+ const missing = valuesBySource.filter((entry) => entry.value === void 0);
1592
1640
  if (present.length === 0 && variable?.required === true) {
1593
1641
  for (const entry of missing) {
1594
1642
  issues.push(
@@ -1675,6 +1723,7 @@ async function loadEnvSource(options) {
1675
1723
  return parseEnvSourceContent(content);
1676
1724
  } catch (error_) {
1677
1725
  if (options.allowMissing === true && error_ instanceof Error && "code" in error_ && error_.code === "ENOENT") {
1726
+ warn(`Target file not found: ${options.filePath} \u2014 treating as empty`);
1678
1727
  return {};
1679
1728
  }
1680
1729
  throw error_;
@@ -1841,6 +1890,9 @@ async function loadCommandConfig(configPath) {
1841
1890
  return loadConfig(process.cwd());
1842
1891
  }
1843
1892
  const resolvedPath = path6.resolve(configPath);
1893
+ if (!existsSync(resolvedPath)) {
1894
+ throw new Error(`Config file not found: ${configPath}`);
1895
+ }
1844
1896
  const configDir = path6.dirname(resolvedPath);
1845
1897
  const moduleValue = await import(pathToFileURL(resolvedPath).href);
1846
1898
  if (moduleValue.default === void 0) return void 0;