@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 +12 -0
- package/README.md +12 -0
- package/dist/cli.js +88 -28
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +79 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +75 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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;
|