@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 +12 -0
- package/README.md +12 -0
- package/dist/cli.js +76 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +85 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -2
- package/dist/index.d.ts +25 -2
- package/dist/index.js +85 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
[](https://npmjs.com/package/@xlameiro/env-typegen)
|
|
8
|
+
[](https://npmjs.com/package/@xlameiro/env-typegen)
|
|
6
9
|
[](https://github.com/xlameiro/env-typegen/actions/workflows/ci.yml)
|
|
10
|
+
[](https://github.com/xlameiro/env-typegen/stargazers)
|
|
11
|
+
[](https://github.com/xlameiro)
|
|
7
12
|
[](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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));
|