@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/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
|
[](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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
664
|
+
console.error(red(`\u2716 ${message}`));
|
|
642
665
|
}
|
|
643
666
|
function success(message) {
|
|
644
|
-
console.log(
|
|
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)
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|