@xlameiro/env-typegen 0.1.0
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 +57 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/cli.js +768 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +539 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +271 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.js +518 -0
- package/dist/index.js.map +1 -0
- package/package.json +107 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
|
|
28
|
+
// ../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js
|
|
29
|
+
var require_picocolors = __commonJS({
|
|
30
|
+
"../../node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js"(exports, module) {
|
|
31
|
+
"use strict";
|
|
32
|
+
var p = process || {};
|
|
33
|
+
var argv = p.argv || [];
|
|
34
|
+
var env = p.env || {};
|
|
35
|
+
var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
36
|
+
var formatter = (open, close, replace = open) => (input) => {
|
|
37
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
38
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
39
|
+
};
|
|
40
|
+
var replaceClose = (string, close, replace, index) => {
|
|
41
|
+
let result = "", cursor = 0;
|
|
42
|
+
do {
|
|
43
|
+
result += string.substring(cursor, index) + replace;
|
|
44
|
+
cursor = index + close.length;
|
|
45
|
+
index = string.indexOf(close, cursor);
|
|
46
|
+
} while (~index);
|
|
47
|
+
return result + string.substring(cursor);
|
|
48
|
+
};
|
|
49
|
+
var createColors = (enabled = isColorSupported) => {
|
|
50
|
+
let f = enabled ? formatter : () => String;
|
|
51
|
+
return {
|
|
52
|
+
isColorSupported: enabled,
|
|
53
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
54
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
55
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
56
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
57
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
58
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
59
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
60
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
61
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
62
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
63
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
64
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
65
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
66
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
67
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
68
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
69
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
70
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
71
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
72
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
73
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
74
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
75
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
76
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
77
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
78
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
79
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
80
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
81
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
82
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
83
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
84
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
85
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
86
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
87
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
88
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
89
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
90
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
91
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
92
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
93
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
module.exports = createColors();
|
|
97
|
+
module.exports.createColors = createColors;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// src/cli.ts
|
|
102
|
+
import path6 from "path";
|
|
103
|
+
import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "url";
|
|
104
|
+
import { inspect, parseArgs } from "util";
|
|
105
|
+
|
|
106
|
+
// src/config.ts
|
|
107
|
+
import { existsSync } from "fs";
|
|
108
|
+
import path from "path";
|
|
109
|
+
import { pathToFileURL } from "url";
|
|
110
|
+
var CONFIG_FILE_NAMES = [
|
|
111
|
+
"env-typegen.config.ts",
|
|
112
|
+
"env-typegen.config.mjs",
|
|
113
|
+
"env-typegen.config.js"
|
|
114
|
+
];
|
|
115
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
116
|
+
for (const name of CONFIG_FILE_NAMES) {
|
|
117
|
+
const filePath = path.resolve(cwd, name);
|
|
118
|
+
if (existsSync(filePath)) {
|
|
119
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
120
|
+
const mod = await import(fileUrl);
|
|
121
|
+
return mod.default;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return void 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/pipeline.ts
|
|
128
|
+
import path5 from "path";
|
|
129
|
+
|
|
130
|
+
// src/generators/declaration-generator.ts
|
|
131
|
+
import path2 from "path";
|
|
132
|
+
function generateDeclaration(parsed) {
|
|
133
|
+
const fileName = path2.basename(parsed.filePath);
|
|
134
|
+
const lines = [];
|
|
135
|
+
lines.push("// Generated by env-typegen \u2014 do not edit manually");
|
|
136
|
+
lines.push(`// Source: ${fileName}`);
|
|
137
|
+
lines.push("");
|
|
138
|
+
lines.push("declare namespace NodeJS {");
|
|
139
|
+
lines.push(" interface ProcessEnv {");
|
|
140
|
+
for (const variable of parsed.vars) {
|
|
141
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
142
|
+
const optional = variable.isOptional ? "?" : "";
|
|
143
|
+
if (variable.description !== void 0) {
|
|
144
|
+
lines.push(` /** ${variable.description} */`);
|
|
145
|
+
}
|
|
146
|
+
let propLine = ` readonly ${variable.key}${optional}: string;`;
|
|
147
|
+
if (effectiveType === "number") {
|
|
148
|
+
propLine += ` // coerce to number: Number(process.env.${variable.key})`;
|
|
149
|
+
} else if (effectiveType === "boolean") {
|
|
150
|
+
propLine += ` // coerce to boolean: process.env.${variable.key} === 'true'`;
|
|
151
|
+
}
|
|
152
|
+
lines.push(propLine);
|
|
153
|
+
}
|
|
154
|
+
lines.push(" }");
|
|
155
|
+
lines.push("}");
|
|
156
|
+
return lines.join("\n") + "\n";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/generators/t3-generator.ts
|
|
160
|
+
function toT3ZodType(envVarType) {
|
|
161
|
+
if (envVarType === "number") return "z.coerce.number()";
|
|
162
|
+
if (envVarType === "boolean") return "z.coerce.boolean()";
|
|
163
|
+
if (envVarType === "url") return "z.string().url()";
|
|
164
|
+
if (envVarType === "email") return "z.string().email()";
|
|
165
|
+
return "z.string()";
|
|
166
|
+
}
|
|
167
|
+
function generateT3Env(parsed) {
|
|
168
|
+
const serverVars = parsed.vars.filter((v) => !v.isClientSide);
|
|
169
|
+
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
170
|
+
const lines = [];
|
|
171
|
+
lines.push("// Generated by env-typegen \u2014 do not edit manually");
|
|
172
|
+
lines.push('import { createEnv } from "@t3-oss/env-nextjs";');
|
|
173
|
+
lines.push('import { z } from "zod";');
|
|
174
|
+
lines.push("");
|
|
175
|
+
lines.push("export const env = createEnv({");
|
|
176
|
+
if (serverVars.length > 0) {
|
|
177
|
+
lines.push(" server: {");
|
|
178
|
+
for (const variable of serverVars) {
|
|
179
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
180
|
+
let zodExpr = toT3ZodType(effectiveType);
|
|
181
|
+
if (variable.description !== void 0) {
|
|
182
|
+
zodExpr += `.describe("${variable.description}")`;
|
|
183
|
+
}
|
|
184
|
+
if (variable.isOptional) {
|
|
185
|
+
zodExpr += ".optional()";
|
|
186
|
+
}
|
|
187
|
+
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
188
|
+
}
|
|
189
|
+
lines.push(" },");
|
|
190
|
+
}
|
|
191
|
+
if (clientVars.length > 0) {
|
|
192
|
+
lines.push(" client: {");
|
|
193
|
+
for (const variable of clientVars) {
|
|
194
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
195
|
+
let zodExpr = toT3ZodType(effectiveType);
|
|
196
|
+
if (variable.description !== void 0) {
|
|
197
|
+
zodExpr += `.describe("${variable.description}")`;
|
|
198
|
+
}
|
|
199
|
+
if (variable.isOptional) {
|
|
200
|
+
zodExpr += ".optional()";
|
|
201
|
+
}
|
|
202
|
+
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
203
|
+
}
|
|
204
|
+
lines.push(" },");
|
|
205
|
+
}
|
|
206
|
+
lines.push(" runtimeEnv: {");
|
|
207
|
+
for (const variable of parsed.vars) {
|
|
208
|
+
lines.push(` ${variable.key}: process.env.${variable.key},`);
|
|
209
|
+
}
|
|
210
|
+
lines.push(" },");
|
|
211
|
+
lines.push("});");
|
|
212
|
+
return lines.join("\n") + "\n";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/generators/typescript-generator.ts
|
|
216
|
+
import path3 from "path";
|
|
217
|
+
function toTsType(envVarType) {
|
|
218
|
+
if (envVarType === "number") return "number";
|
|
219
|
+
if (envVarType === "boolean") return "boolean";
|
|
220
|
+
return "string";
|
|
221
|
+
}
|
|
222
|
+
function generateTypeScriptTypes(parsed) {
|
|
223
|
+
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
224
|
+
const hasClientVars = clientVars.length > 0;
|
|
225
|
+
const fileName = path3.basename(parsed.filePath);
|
|
226
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
227
|
+
const lines = [];
|
|
228
|
+
lines.push("// Generated by env-typegen \u2014 do not edit manually");
|
|
229
|
+
lines.push(`// Source: ${fileName}`);
|
|
230
|
+
lines.push(`// Generated at: ${timestamp}`);
|
|
231
|
+
lines.push("");
|
|
232
|
+
lines.push("declare namespace NodeJS {");
|
|
233
|
+
lines.push(" interface ProcessEnv {");
|
|
234
|
+
for (const variable of parsed.vars) {
|
|
235
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
236
|
+
const optional = variable.isOptional ? "?" : "";
|
|
237
|
+
if (variable.description !== void 0) {
|
|
238
|
+
lines.push(` /** ${variable.description} */`);
|
|
239
|
+
}
|
|
240
|
+
let propLine = ` readonly ${variable.key}${optional}: string;`;
|
|
241
|
+
if (effectiveType === "number") {
|
|
242
|
+
propLine += ` // coerce to number: Number(process.env.${variable.key})`;
|
|
243
|
+
} else if (effectiveType === "boolean") {
|
|
244
|
+
propLine += ` // coerce to boolean: process.env.${variable.key} === 'true'`;
|
|
245
|
+
}
|
|
246
|
+
lines.push(propLine);
|
|
247
|
+
}
|
|
248
|
+
lines.push(" }");
|
|
249
|
+
lines.push("}");
|
|
250
|
+
lines.push("");
|
|
251
|
+
lines.push("export type EnvVars = {");
|
|
252
|
+
for (const variable of parsed.vars) {
|
|
253
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
254
|
+
const tsType = toTsType(effectiveType);
|
|
255
|
+
const optional = variable.isOptional ? "?" : "";
|
|
256
|
+
lines.push(` ${variable.key}${optional}: ${tsType};`);
|
|
257
|
+
}
|
|
258
|
+
lines.push("};");
|
|
259
|
+
if (hasClientVars) {
|
|
260
|
+
const clientKeyUnion = clientVars.map((v) => `"${v.key}"`).join(" | ");
|
|
261
|
+
lines.push("");
|
|
262
|
+
lines.push(`export type ServerEnvVars = Omit<EnvVars, ${clientKeyUnion}>;`);
|
|
263
|
+
lines.push(`export type ClientEnvVars = Pick<EnvVars, ${clientKeyUnion}>;`);
|
|
264
|
+
}
|
|
265
|
+
return lines.join("\n") + "\n";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/generators/zod-generator.ts
|
|
269
|
+
function toZodType(envVarType) {
|
|
270
|
+
if (envVarType === "number") return "z.coerce.number()";
|
|
271
|
+
if (envVarType === "boolean") return "z.coerce.boolean()";
|
|
272
|
+
if (envVarType === "url") return "z.string().url()";
|
|
273
|
+
if (envVarType === "email") return "z.string().email()";
|
|
274
|
+
return "z.string()";
|
|
275
|
+
}
|
|
276
|
+
function generateZodSchema(parsed) {
|
|
277
|
+
const serverVars = parsed.vars.filter((v) => !v.isClientSide);
|
|
278
|
+
const clientVars = parsed.vars.filter((v) => v.isClientSide);
|
|
279
|
+
const lines = [];
|
|
280
|
+
lines.push("// Generated by env-typegen \u2014 do not edit manually");
|
|
281
|
+
lines.push('import { z } from "zod";');
|
|
282
|
+
lines.push("");
|
|
283
|
+
lines.push("export const serverEnvSchema = z.object({");
|
|
284
|
+
for (const variable of serverVars) {
|
|
285
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
286
|
+
const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
|
|
287
|
+
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
288
|
+
}
|
|
289
|
+
lines.push("});");
|
|
290
|
+
lines.push("");
|
|
291
|
+
lines.push("export const clientEnvSchema = z.object({");
|
|
292
|
+
for (const variable of clientVars) {
|
|
293
|
+
const effectiveType = variable.annotatedType ?? variable.inferredType;
|
|
294
|
+
const zodExpr = variable.isOptional ? `${toZodType(effectiveType)}.optional()` : toZodType(effectiveType);
|
|
295
|
+
lines.push(` ${variable.key}: ${zodExpr},`);
|
|
296
|
+
}
|
|
297
|
+
lines.push("});");
|
|
298
|
+
lines.push("");
|
|
299
|
+
lines.push("export const envSchema = serverEnvSchema.merge(clientEnvSchema);");
|
|
300
|
+
lines.push("export type Env = z.infer<typeof envSchema>;");
|
|
301
|
+
return lines.join("\n") + "\n";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/parser/env-parser.ts
|
|
305
|
+
import { readFileSync } from "fs";
|
|
306
|
+
|
|
307
|
+
// src/inferrer/rules.ts
|
|
308
|
+
var inferenceRules = [
|
|
309
|
+
{
|
|
310
|
+
id: "P2_key_url_suffix",
|
|
311
|
+
priority: 2,
|
|
312
|
+
match: (key) => key.toUpperCase().endsWith("_URL"),
|
|
313
|
+
type: "url"
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: "P3_key_email_from_suffix",
|
|
317
|
+
priority: 3,
|
|
318
|
+
match: (key) => {
|
|
319
|
+
const normalizedKey = key.toUpperCase();
|
|
320
|
+
return normalizedKey.endsWith("_EMAIL") || normalizedKey.endsWith("_FROM");
|
|
321
|
+
},
|
|
322
|
+
type: "email"
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: "P4_boolean_prefix",
|
|
326
|
+
priority: 4,
|
|
327
|
+
match: (key) => {
|
|
328
|
+
const normalizedKey = key.toUpperCase();
|
|
329
|
+
return normalizedKey.startsWith("ENABLE_") || normalizedKey.startsWith("DISABLE_") || normalizedKey.startsWith("IS_") || normalizedKey.startsWith("DEBUG") || normalizedKey.startsWith("FEATURE_");
|
|
330
|
+
},
|
|
331
|
+
type: "boolean"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: "P5_key_port",
|
|
335
|
+
priority: 5,
|
|
336
|
+
match: (key) => {
|
|
337
|
+
const normalizedKey = key.toUpperCase();
|
|
338
|
+
return normalizedKey.endsWith("_PORT") || normalizedKey === "PORT";
|
|
339
|
+
},
|
|
340
|
+
type: "number"
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: "P6_empty_unknown",
|
|
344
|
+
priority: 6,
|
|
345
|
+
match: (_key, value) => value.length === 0,
|
|
346
|
+
type: "unknown"
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
id: "P7_boolean_literal",
|
|
350
|
+
priority: 7,
|
|
351
|
+
match: (_key, value) => {
|
|
352
|
+
const lower = value.toLowerCase();
|
|
353
|
+
return lower === "true" || lower === "false";
|
|
354
|
+
},
|
|
355
|
+
type: "boolean"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: "P8_numeric_literal",
|
|
359
|
+
priority: 8,
|
|
360
|
+
match: (_key, value) => /^\d+(\.\d+)?$/.test(value),
|
|
361
|
+
type: "number"
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
id: "P9_semver",
|
|
365
|
+
priority: 9,
|
|
366
|
+
match: (_key, value) => /^\d+\.\d+\.\d+/.test(value),
|
|
367
|
+
type: "semver"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
id: "P10_url_scheme",
|
|
371
|
+
priority: 10,
|
|
372
|
+
match: (_key, value) => /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value),
|
|
373
|
+
type: "url"
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: "P11_email_literal",
|
|
377
|
+
priority: 11,
|
|
378
|
+
match: (_key, value) => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value),
|
|
379
|
+
type: "email"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
id: "P12_json_object_array",
|
|
383
|
+
priority: 12,
|
|
384
|
+
match: (_key, value) => {
|
|
385
|
+
try {
|
|
386
|
+
const parsed = JSON.parse(value);
|
|
387
|
+
return typeof parsed === "object" && parsed !== null;
|
|
388
|
+
} catch {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
type: "json"
|
|
393
|
+
}
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
// src/inferrer/type-inferrer.ts
|
|
397
|
+
var sortedRules = [...inferenceRules].sort((left, right) => left.priority - right.priority);
|
|
398
|
+
function inferType(key, value, options) {
|
|
399
|
+
for (const rule of sortedRules) {
|
|
400
|
+
if (rule.match(key, value)) {
|
|
401
|
+
return rule.type;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return options?.fallbackType ?? "string";
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/parser/comment-parser.ts
|
|
408
|
+
var VALID_ENV_VAR_TYPES = /* @__PURE__ */ new Set([
|
|
409
|
+
"string",
|
|
410
|
+
"number",
|
|
411
|
+
"boolean",
|
|
412
|
+
"url",
|
|
413
|
+
"email",
|
|
414
|
+
"semver",
|
|
415
|
+
"json",
|
|
416
|
+
"unknown"
|
|
417
|
+
]);
|
|
418
|
+
function isEnvVarType(value) {
|
|
419
|
+
return VALID_ENV_VAR_TYPES.has(value);
|
|
420
|
+
}
|
|
421
|
+
function parseCommentBlock(lines) {
|
|
422
|
+
let annotatedType;
|
|
423
|
+
let description;
|
|
424
|
+
let isRequired = false;
|
|
425
|
+
for (const line of lines) {
|
|
426
|
+
const content = line.replace(/^#\s*/, "").trimEnd();
|
|
427
|
+
if (content.startsWith("@description ")) {
|
|
428
|
+
description = content.slice("@description ".length).trim();
|
|
429
|
+
} else if (content.startsWith("@type ")) {
|
|
430
|
+
const typeStr = content.slice("@type ".length).trim();
|
|
431
|
+
if (isEnvVarType(typeStr)) {
|
|
432
|
+
annotatedType = typeStr;
|
|
433
|
+
}
|
|
434
|
+
} else if (content.trim() === "@required") {
|
|
435
|
+
isRequired = true;
|
|
436
|
+
} else if (content.trim() === "@optional") {
|
|
437
|
+
} else if (description === void 0 && content.trim().length > 0) {
|
|
438
|
+
description = content.trim();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const result = { isRequired };
|
|
442
|
+
if (annotatedType !== void 0) result.annotatedType = annotatedType;
|
|
443
|
+
if (description !== void 0) result.description = description;
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/parser/env-parser.ts
|
|
448
|
+
var ENV_VAR_RE = /^([A-Z_][A-Z0-9_]*)=(.*)$/;
|
|
449
|
+
var SECTION_HEADER_RE = /^#\s+[-=]{3,}\s+(.+?)\s+[-=]{3,}\s*$/;
|
|
450
|
+
function parseEnvFileContent(content, filePath) {
|
|
451
|
+
const lines = content.split("\n");
|
|
452
|
+
const vars = [];
|
|
453
|
+
const groups = [];
|
|
454
|
+
let currentGroup;
|
|
455
|
+
let commentBlock = [];
|
|
456
|
+
for (let i = 0; i < lines.length; i++) {
|
|
457
|
+
const line = lines[i] ?? "";
|
|
458
|
+
const lineNumber = i + 1;
|
|
459
|
+
const trimmed = line.trim();
|
|
460
|
+
if (trimmed === "") {
|
|
461
|
+
commentBlock = [];
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const sectionMatch = SECTION_HEADER_RE.exec(trimmed);
|
|
465
|
+
if (sectionMatch !== null) {
|
|
466
|
+
const groupName = (sectionMatch[1] ?? "").trim();
|
|
467
|
+
currentGroup = groupName;
|
|
468
|
+
if (!groups.includes(groupName)) {
|
|
469
|
+
groups.push(groupName);
|
|
470
|
+
}
|
|
471
|
+
commentBlock = [];
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (trimmed.startsWith("#")) {
|
|
475
|
+
commentBlock.push(line);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const envMatch = ENV_VAR_RE.exec(trimmed);
|
|
479
|
+
if (envMatch !== null) {
|
|
480
|
+
const key = envMatch[1] ?? "";
|
|
481
|
+
const rawValue = envMatch[2] ?? "";
|
|
482
|
+
const annotations = parseCommentBlock(commentBlock);
|
|
483
|
+
const inferredType = inferType(key, rawValue);
|
|
484
|
+
const isRequired = rawValue.length > 0 || annotations.isRequired;
|
|
485
|
+
const isOptional = rawValue.length === 0 && !annotations.isRequired;
|
|
486
|
+
const isClientSide = key.startsWith("NEXT_PUBLIC_");
|
|
487
|
+
const parsedVar = {
|
|
488
|
+
key,
|
|
489
|
+
rawValue,
|
|
490
|
+
inferredType,
|
|
491
|
+
isRequired,
|
|
492
|
+
isOptional,
|
|
493
|
+
isClientSide,
|
|
494
|
+
lineNumber
|
|
495
|
+
};
|
|
496
|
+
if (annotations.annotatedType !== void 0) {
|
|
497
|
+
parsedVar.annotatedType = annotations.annotatedType;
|
|
498
|
+
}
|
|
499
|
+
if (annotations.description !== void 0) {
|
|
500
|
+
parsedVar.description = annotations.description;
|
|
501
|
+
}
|
|
502
|
+
if (currentGroup !== void 0) {
|
|
503
|
+
parsedVar.group = currentGroup;
|
|
504
|
+
}
|
|
505
|
+
vars.push(parsedVar);
|
|
506
|
+
commentBlock = [];
|
|
507
|
+
} else {
|
|
508
|
+
commentBlock = [];
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return { filePath, vars, groups };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/utils/file.ts
|
|
515
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
516
|
+
import path4 from "path";
|
|
517
|
+
async function readEnvFile(filePath) {
|
|
518
|
+
return readFile(path4.resolve(filePath), "utf8");
|
|
519
|
+
}
|
|
520
|
+
async function writeOutput(filePath, content) {
|
|
521
|
+
const resolved = path4.resolve(filePath);
|
|
522
|
+
await mkdir(path4.dirname(resolved), { recursive: true });
|
|
523
|
+
await writeFile(resolved, content, "utf8");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/utils/format.ts
|
|
527
|
+
import { format } from "prettier";
|
|
528
|
+
async function formatOutput(content, parser = "typescript") {
|
|
529
|
+
try {
|
|
530
|
+
return await format(content, { parser });
|
|
531
|
+
} catch {
|
|
532
|
+
return content;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/utils/logger.ts
|
|
537
|
+
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
538
|
+
function log(message) {
|
|
539
|
+
console.log(message);
|
|
540
|
+
}
|
|
541
|
+
function error(message) {
|
|
542
|
+
console.error((0, import_picocolors.red)(`\u2716 ${message}`));
|
|
543
|
+
}
|
|
544
|
+
function success(message) {
|
|
545
|
+
console.log((0, import_picocolors.green)(`\u2714 ${message}`));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/pipeline.ts
|
|
549
|
+
function deriveOutputPath(base, generator, isSingle) {
|
|
550
|
+
if (isSingle) return base;
|
|
551
|
+
const ext = path5.extname(base);
|
|
552
|
+
const noExt = ext.length > 0 ? base.slice(0, -ext.length) : base;
|
|
553
|
+
return `${noExt}.${generator}${ext.length > 0 ? ext : ".ts"}`;
|
|
554
|
+
}
|
|
555
|
+
function buildOutput(generator, parsed) {
|
|
556
|
+
switch (generator) {
|
|
557
|
+
case "typescript":
|
|
558
|
+
return generateTypeScriptTypes(parsed);
|
|
559
|
+
case "zod":
|
|
560
|
+
return generateZodSchema(parsed);
|
|
561
|
+
case "t3":
|
|
562
|
+
return generateT3Env(parsed);
|
|
563
|
+
case "declaration":
|
|
564
|
+
return generateDeclaration(parsed);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async function persistOutput(params) {
|
|
568
|
+
const { generated, generator, outputPath, isSingle, stdout, dryRun, silent } = params;
|
|
569
|
+
if (stdout) {
|
|
570
|
+
if (isSingle) {
|
|
571
|
+
console.log(generated);
|
|
572
|
+
} else {
|
|
573
|
+
console.log(`// --- ${generator}:${outputPath} ---`);
|
|
574
|
+
console.log(generated);
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (dryRun) {
|
|
579
|
+
if (!silent) {
|
|
580
|
+
success(`Dry run: ${outputPath}`);
|
|
581
|
+
}
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
await writeOutput(outputPath, generated);
|
|
585
|
+
if (!silent) {
|
|
586
|
+
success(`Generated ${outputPath}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function runGenerate(options) {
|
|
590
|
+
const {
|
|
591
|
+
input,
|
|
592
|
+
output,
|
|
593
|
+
generators,
|
|
594
|
+
format: shouldFormat,
|
|
595
|
+
stdout = false,
|
|
596
|
+
dryRun = false,
|
|
597
|
+
silent = false
|
|
598
|
+
} = options;
|
|
599
|
+
const isSingle = generators.length === 1;
|
|
600
|
+
const content = await readEnvFile(input);
|
|
601
|
+
const parsed = parseEnvFileContent(content, input);
|
|
602
|
+
for (const generator of generators) {
|
|
603
|
+
let generated = buildOutput(generator, parsed);
|
|
604
|
+
if (shouldFormat) {
|
|
605
|
+
generated = await formatOutput(generated);
|
|
606
|
+
}
|
|
607
|
+
const outputPath = deriveOutputPath(output, generator, isSingle);
|
|
608
|
+
await persistOutput({
|
|
609
|
+
generated,
|
|
610
|
+
generator,
|
|
611
|
+
outputPath,
|
|
612
|
+
isSingle,
|
|
613
|
+
stdout,
|
|
614
|
+
dryRun,
|
|
615
|
+
silent
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/watch.ts
|
|
621
|
+
import { watch } from "chokidar";
|
|
622
|
+
function startWatch({ inputPath, runOptions }) {
|
|
623
|
+
log(`Watching ${inputPath} for changes...`);
|
|
624
|
+
void runGenerate(runOptions).catch((err) => {
|
|
625
|
+
const message = err instanceof Error ? err.message : JSON.stringify(err);
|
|
626
|
+
error(message);
|
|
627
|
+
});
|
|
628
|
+
const watcher = watch(inputPath, { persistent: true });
|
|
629
|
+
watcher.on("change", () => {
|
|
630
|
+
log(`${inputPath} changed, regenerating...`);
|
|
631
|
+
void runGenerate(runOptions).catch((err) => {
|
|
632
|
+
const message = err instanceof Error ? err.message : JSON.stringify(err);
|
|
633
|
+
error(message);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
process.on("SIGINT", () => {
|
|
637
|
+
void watcher.close().then(() => {
|
|
638
|
+
log("Watcher stopped.");
|
|
639
|
+
process.exit(0);
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/cli.ts
|
|
645
|
+
var VERSION = "0.1.0";
|
|
646
|
+
var HELP_TEXT = [
|
|
647
|
+
"env-typegen \u2014 Generate TypeScript types from .env.example",
|
|
648
|
+
"",
|
|
649
|
+
"Usage:",
|
|
650
|
+
" env-typegen -i <path> [options]",
|
|
651
|
+
"",
|
|
652
|
+
"Options:",
|
|
653
|
+
" -i, --input <path> Path to .env.example file",
|
|
654
|
+
" -o, --output <path> Output file path (default: env.generated.ts)",
|
|
655
|
+
" -f, --format <name> Generator format: ts|zod|t3|declaration",
|
|
656
|
+
" May be specified multiple times.",
|
|
657
|
+
" -g, --generator <name> Backward-compatible alias for --format",
|
|
658
|
+
" --stdout Print generated output to stdout",
|
|
659
|
+
" --dry-run Parse and generate without writing files",
|
|
660
|
+
" --no-format Disable prettier formatting",
|
|
661
|
+
" -s, --silent Suppress success logs",
|
|
662
|
+
" -w, --watch Watch for changes and regenerate",
|
|
663
|
+
" -c, --config <path> Path to config file",
|
|
664
|
+
" -v, --version Print version",
|
|
665
|
+
" -h, --help Show this help"
|
|
666
|
+
].join("\n");
|
|
667
|
+
var FORMAT_TO_GENERATOR = {
|
|
668
|
+
ts: "typescript",
|
|
669
|
+
typescript: "typescript",
|
|
670
|
+
zod: "zod",
|
|
671
|
+
t3: "t3",
|
|
672
|
+
declaration: "declaration"
|
|
673
|
+
};
|
|
674
|
+
function normalizeGenerator(input) {
|
|
675
|
+
return FORMAT_TO_GENERATOR[input] ?? FORMAT_TO_GENERATOR[input.toLowerCase()];
|
|
676
|
+
}
|
|
677
|
+
function getErrorMessage(errorValue) {
|
|
678
|
+
if (errorValue instanceof Error) {
|
|
679
|
+
return errorValue.message;
|
|
680
|
+
}
|
|
681
|
+
return inspect(errorValue, { depth: 2 });
|
|
682
|
+
}
|
|
683
|
+
async function runCli(argv = process.argv.slice(2)) {
|
|
684
|
+
const { values } = parseArgs({
|
|
685
|
+
args: argv,
|
|
686
|
+
options: {
|
|
687
|
+
input: { type: "string", short: "i" },
|
|
688
|
+
output: { type: "string", short: "o" },
|
|
689
|
+
generator: { type: "string", short: "g", multiple: true },
|
|
690
|
+
format: { type: "string", short: "f", multiple: true },
|
|
691
|
+
"no-format": { type: "boolean" },
|
|
692
|
+
stdout: { type: "boolean" },
|
|
693
|
+
"dry-run": { type: "boolean" },
|
|
694
|
+
silent: { type: "boolean", short: "s" },
|
|
695
|
+
watch: { type: "boolean", short: "w" },
|
|
696
|
+
config: { type: "string", short: "c" },
|
|
697
|
+
version: { type: "boolean", short: "v" },
|
|
698
|
+
help: { type: "boolean", short: "h" }
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
if (values.version === true) {
|
|
702
|
+
console.log(VERSION);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (values.help === true) {
|
|
706
|
+
console.log(HELP_TEXT);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
let fileConfig;
|
|
710
|
+
if (values.config !== void 0) {
|
|
711
|
+
const configPath = path6.resolve(values.config);
|
|
712
|
+
const mod = await import(pathToFileURL2(configPath).href);
|
|
713
|
+
fileConfig = mod.default;
|
|
714
|
+
} else {
|
|
715
|
+
fileConfig = await loadConfig(process.cwd());
|
|
716
|
+
}
|
|
717
|
+
const input = values.input ?? fileConfig?.input;
|
|
718
|
+
if (input === void 0) {
|
|
719
|
+
error("No input file specified. Use -i <path> or set input in env-typegen.config.ts");
|
|
720
|
+
process.exit(1);
|
|
721
|
+
}
|
|
722
|
+
const output = values.output ?? fileConfig?.output ?? "env.generated.ts";
|
|
723
|
+
const rawFormats = values.format;
|
|
724
|
+
const rawGenerators = values.generator;
|
|
725
|
+
const requested = [...rawFormats ?? [], ...rawGenerators ?? []].map(String);
|
|
726
|
+
let generators;
|
|
727
|
+
if (requested.length > 0) {
|
|
728
|
+
const normalizedGenerators = requested.map((item) => normalizeGenerator(item)).filter((item) => item !== void 0);
|
|
729
|
+
const invalid = requested.filter((item) => normalizeGenerator(item) === void 0);
|
|
730
|
+
if (invalid.length > 0) {
|
|
731
|
+
error(`Unknown format(s): ${invalid.join(", ")}. Valid: ts, zod, t3, declaration`);
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
generators = [...new Set(normalizedGenerators)];
|
|
735
|
+
} else {
|
|
736
|
+
generators = fileConfig?.generators ?? ["typescript"];
|
|
737
|
+
}
|
|
738
|
+
const shouldFormat = values["no-format"] === true ? false : fileConfig?.format ?? true;
|
|
739
|
+
const useStdout = values.stdout ?? false;
|
|
740
|
+
const isDryRun = values["dry-run"] ?? false;
|
|
741
|
+
const isSilent = values.silent ?? false;
|
|
742
|
+
const shouldWatch = values.watch ?? false;
|
|
743
|
+
const options = {
|
|
744
|
+
input,
|
|
745
|
+
output,
|
|
746
|
+
generators,
|
|
747
|
+
format: shouldFormat,
|
|
748
|
+
stdout: useStdout,
|
|
749
|
+
dryRun: isDryRun,
|
|
750
|
+
silent: isSilent
|
|
751
|
+
};
|
|
752
|
+
if (shouldWatch) {
|
|
753
|
+
startWatch({ inputPath: input, runOptions: options });
|
|
754
|
+
} else {
|
|
755
|
+
await runGenerate(options);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (process.argv[1] !== void 0 && path6.resolve(process.argv[1]) === path6.resolve(fileURLToPath(import.meta.url))) {
|
|
759
|
+
await runCli().catch((err) => {
|
|
760
|
+
error(getErrorMessage(err));
|
|
761
|
+
process.exit(1);
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
export {
|
|
765
|
+
runCli,
|
|
766
|
+
runGenerate
|
|
767
|
+
};
|
|
768
|
+
//# sourceMappingURL=cli.js.map
|