zenvx 0.1.3 → 0.2.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/README.md CHANGED
@@ -33,6 +33,8 @@ Standard Zod is great, but environment variables are always strings. This leads
33
33
  - ✅ Strict validation for strings, URLs, emails, JSON, enums
34
34
  - ✅ Beautiful, human-readable error reporting
35
35
  - ✅ Seamless TypeScript integration
36
+ - ✅ `.env.example` auto-generation
37
+ - ✅ Runtime **and** build-time validation modes
36
38
 
37
39
  ## Installation
38
40
 
@@ -76,6 +78,77 @@ console.log(`Server running on port ${env.PORT}`);
76
78
  // TypeScript knows env.PORT is a number!
77
79
  ```
78
80
 
81
+ ## .env.example Auto-Generation
82
+
83
+ zenvx can automatically generate a .env.example file from your schema — keeping your documentation always in sync.
84
+
85
+ ```ts
86
+ defineEnv(
87
+ {
88
+ DATABASE_URL: tx.url(),
89
+ PORT: tx.port().default(3000),
90
+ DEBUG: tx.bool(),
91
+ },
92
+ {
93
+ generateExample: true,
94
+ },
95
+ );
96
+ ```
97
+
98
+ This produces:
99
+
100
+ ```
101
+ # Example environment variables
102
+ # Copy this file to .env and fill in the values
103
+
104
+ DATABASE_URL= #string
105
+ PORT=3000 # number
106
+ DEBUG_MODE= # boolean
107
+
108
+ ```
109
+
110
+ No more forgotten or outdated .env.example files.
111
+
112
+ ## Runtime vs Build-Time Validation
113
+
114
+ Different environments need different failure behavior. zenvx supports both.
115
+
116
+ ### Runtime Validation (default)
117
+
118
+ In runtime mode, environment variables are validated as soon as your application starts.
119
+
120
+ ```ts
121
+ defineEnv(schema, { mode: "runtime" });
122
+ ```
123
+
124
+ Behavior:
125
+
126
+ - Environment variables are loaded and validated immediately
127
+ - If validation fails:
128
+ - A formatted error message is printed to the console
129
+ - The process exits using process.exit(1)
130
+ - Prevents the application from running with invalid configuration
131
+
132
+ This mode ensures misconfigured environments are caught early and stop execution entirely.
133
+
134
+ ### Build-Time Validation
135
+
136
+ IIn build-time mode, validation errors are thrown instead of terminating the process.
137
+
138
+ ```ts
139
+ defineEnv(schema, { mode: "build" });
140
+ ```
141
+
142
+ Behavior:
143
+
144
+ - Environment variables are validated during execution
145
+ - If validation fails:
146
+ - An error is thrown
147
+ - process.exit() is not called
148
+ - Allows the calling environment (bundler, test runner, or script) to handle the failure
149
+
150
+ This mode avoids hard exits and lets external tooling decide how to respond to configuration errors.
151
+
79
152
  ## Beautiful Error Handling
80
153
 
81
154
  If your .env file is missing values or has invalid types, zenvx stops the process immediately and prints a clear message:
package/dist/index.cjs CHANGED
@@ -34,7 +34,73 @@ __export(index_exports, {
34
34
  tx: () => tx
35
35
  });
36
36
  module.exports = __toCommonJS(index_exports);
37
- var import_zod2 = require("zod");
37
+
38
+ // src/core/tx.ts
39
+ var import_zod = require("zod");
40
+ var SEMVER_REGEX = /^\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/;
41
+ var tx = {
42
+ /**
43
+ * STRICT STRING:
44
+ * Rejects purely numeric strings (e.g. "12345").
45
+ * Good for API Keys.
46
+ */
47
+ string: (message = "Value should be text, but looks like a number.") => import_zod.z.string().trim().regex(/^(?!\d+$).+$/, { error: message }).describe("Non-numeric string"),
48
+ /**
49
+ * SMART NUMBER:
50
+ * Automatically converts "3000" -> 3000.
51
+ * Fails if the value is not a valid number (e.g. "abc").
52
+ */
53
+ number: (message = "Must be a number") => import_zod.z.coerce.number({ error: message }).finite().describe("Numeric value"),
54
+ /**
55
+ * PORT VALIDATOR:
56
+ * Coerces to number and ensures it is between 1 and 65535.
57
+ */
58
+ port: (message = "Must be a valid port (1\u201365535)") => import_zod.z.coerce.number({ error: message }).int().min(1).max(65535).describe("TCP/UDP port number (1\u201365535)"),
59
+ /**
60
+ * SMART BOOLEAN:
61
+ * Handles "true", "TRUE", "1" -> true
62
+ * Handles "false", "FALSE", "0" -> false
63
+ * Throws error on anything else.
64
+ */
65
+ bool: (message) => {
66
+ return import_zod.z.union([import_zod.z.string(), import_zod.z.boolean()]).transform((val, ctx) => {
67
+ if (typeof val === "boolean") return val;
68
+ const v = val.toLowerCase();
69
+ if (v === "true" || v === "1") return true;
70
+ if (v === "false" || v === "0") return false;
71
+ ctx.addIssue({
72
+ code: import_zod.z.ZodIssueCode.custom,
73
+ message: message || 'Must be a boolean ("true"/"false"/"1"/"0")'
74
+ });
75
+ return import_zod.z.NEVER;
76
+ });
77
+ },
78
+ /**
79
+ * URL:
80
+ * Strict URL checking.
81
+ */
82
+ url: (message = "Must be a valid URL (e.g. https://...)") => import_zod.z.url({ error: message }).describe("A valid URL including protocol"),
83
+ email: (message = "Must be a valid email address") => import_zod.z.email({ error: message }).describe("A valid email address"),
84
+ positiveNumber: (message = "Must be > 0") => import_zod.z.coerce.number().gt(0, { error: message }).describe("A positive number"),
85
+ nonEmptyString: (message = "Cannot be empty") => import_zod.z.string().trim().min(1, { error: message }).describe("A non-empty string"),
86
+ semver: (message = "Must be valid semver") => import_zod.z.string().regex(SEMVER_REGEX, { error: message }).describe("Semantic version (x.y.z)"),
87
+ path: (message = "Must be a valid path") => import_zod.z.string().trim().min(1, { error: message }).describe("Filesystem path"),
88
+ enum: (values, message = `Must be one of: ${values.join(", ")}`) => import_zod.z.enum(values, { error: message }).describe(`One of: ${values.join(", ")}`),
89
+ json: (message = "Must be valid JSON") => import_zod.z.string().transform((val, ctx) => {
90
+ try {
91
+ return JSON.parse(val);
92
+ } catch {
93
+ ctx.addIssue({
94
+ code: "custom",
95
+ error: message
96
+ });
97
+ return import_zod.z.NEVER;
98
+ }
99
+ }).describe("JSON-encoded value")
100
+ };
101
+
102
+ // src/core/define-env.ts
103
+ var import_zod4 = require("zod");
38
104
 
39
105
  // src/loaders/dotenv.ts
40
106
  var import_dotenv = __toESM(require("dotenv"), 1);
@@ -54,23 +120,29 @@ Tip: Set { dotenv: false } if you manage environment variables yourself.`
54
120
  }
55
121
 
56
122
  // src/core/parser.ts
57
- var import_zod = require("zod");
58
- function parseEnv(schema, source) {
123
+ var import_zod2 = require("zod");
124
+ function parseEnv(schema, source, mode = "runtime") {
59
125
  const result = schema.safeParse(source);
60
126
  if (!result.success) {
61
127
  const issues = result.error.issues.map((i) => `\u274C ${i.path.join(".")}: ${i.message}`).join("\n");
62
- throw new Error(
63
- [
64
- "",
65
- "\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
66
- "\u2502 \u274C INVALID ENVIRONMENT VARIABLES DETECTED \u2502",
67
- "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
68
- issues,
69
- ""
70
- ].join("\n")
71
- );
128
+ const header = mode === "build" ? [
129
+ "\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
130
+ "\u2502 \u274C INVALID ENVIRONMENT VARIABLES DETECTED \u2502",
131
+ "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"
132
+ ] : [
133
+ "\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
134
+ "\u2502 \u26A0 WARNING INVALID ENVIRONMENT VARIABLES \u26A0 \u2502",
135
+ "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"
136
+ ];
137
+ const box = ["", ...header, issues, ""].join("\n");
138
+ if (mode === "build") {
139
+ throw new Error(box);
140
+ } else {
141
+ console.warn(box);
142
+ process.exit(1);
143
+ }
72
144
  }
73
- return result.data;
145
+ return result.success ? result.data : {};
74
146
  }
75
147
 
76
148
  // src/core/proxy.ts
@@ -88,108 +160,50 @@ function createTypedProxy(obj) {
88
160
  });
89
161
  }
90
162
 
91
- // src/index.ts
92
- var tx = {
93
- /**
94
- * STRICT STRING:
95
- * Rejects purely numeric strings (e.g. "12345").
96
- * Good for API Keys.
97
- */
98
- string: (message) => {
99
- return import_zod2.z.string().refine((val) => !/^\d+$/.test(val), {
100
- error: message || "Value should be text, but looks like a number."
101
- });
102
- },
103
- /**
104
- * SMART NUMBER:
105
- * Automatically converts "3000" -> 3000.
106
- * Fails if the value is not a valid number (e.g. "abc").
107
- */
108
- number: (message) => {
109
- return import_zod2.z.coerce.number({ error: message || "Must be a number" });
110
- },
111
- /**
112
- * PORT VALIDATOR:
113
- * Coerces to number and ensures it is between 1 and 65535.
114
- */
115
- port: (message) => {
116
- return import_zod2.z.coerce.number({
117
- error: message || "Must be a valid port (1-65535)"
118
- }).min(1, { error: message || "Port must be >= 1" }).max(65535, { error: message || "Port must be <= 65535" });
119
- },
120
- /**
121
- * SMART BOOLEAN:
122
- * Handles "true", "TRUE", "1" -> true
123
- * Handles "false", "FALSE", "0" -> false
124
- * Throws error on anything else.
125
- */
126
- bool: (message) => {
127
- return import_zod2.z.union([import_zod2.z.string(), import_zod2.z.boolean()]).transform((val, ctx) => {
128
- if (typeof val === "boolean") return val;
129
- const v = val.toLowerCase();
130
- if (v === "true" || v === "1") return true;
131
- if (v === "false" || v === "0") return false;
132
- ctx.addIssue({
133
- code: import_zod2.z.ZodIssueCode.custom,
134
- message: message || 'Must be a boolean ("true"/"false"/"1"/"0")'
135
- });
136
- return import_zod2.z.NEVER;
137
- });
138
- },
139
- /**
140
- * URL:
141
- * Strict URL checking.
142
- */
143
- url: (message) => {
144
- return import_zod2.z.url({
145
- error: message || "Must be a valid URL (e.g. https://...)"
146
- });
147
- },
148
- /**
149
- * EMAIL:
150
- * Strict Email checking.
151
- */
152
- email: (message) => {
153
- return import_zod2.z.email({ error: message || "Must be a valid email address." });
154
- },
155
- positiveNumber: (message) => {
156
- return import_zod2.z.coerce.number().gt(0, { message: message || "Must be > 0" });
157
- },
158
- nonEmptyString: (message) => {
159
- return import_zod2.z.string().trim().min(1, { message: message || "Cannot be empty" });
160
- },
161
- semver: (message) => {
162
- return import_zod2.z.string().refine((val) => /^\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/.test(val), {
163
- error: message || "Must be valid semver"
164
- });
165
- },
166
- path: (message) => {
167
- return import_zod2.z.string().min(1, { error: message || "Must be a valid path" });
168
- },
169
- enum: (values, message) => {
170
- return import_zod2.z.string().refine((val) => values.includes(val), {
171
- error: message || `Must be one of: ${values.join(", ")}`
172
- });
173
- },
174
- json: (message) => {
175
- return import_zod2.z.string().transform((val, ctx) => {
176
- try {
177
- return JSON.parse(val);
178
- } catch {
179
- ctx.addIssue({
180
- code: import_zod2.z.ZodIssueCode.custom,
181
- message: message || "Must be valid JSON"
182
- });
183
- return import_zod2.z.NEVER;
184
- }
185
- });
186
- }
187
- };
163
+ // src/core/generate-example.ts
164
+ var import_zod3 = require("zod");
165
+ var import_fs2 = __toESM(require("fs"), 1);
166
+ function getZodTypeName(schema) {
167
+ let type = "unknown";
168
+ if (schema instanceof import_zod3.ZodString) type = "string";
169
+ else if (schema instanceof import_zod3.ZodNumber) type = "number";
170
+ else if (schema instanceof import_zod3.ZodBoolean) type = "boolean";
171
+ return type;
172
+ }
173
+ function generateExample(schema, path = ".env.example") {
174
+ const header = [
175
+ "# Example environment variables",
176
+ "# Copy this file to .env and fill in the values",
177
+ ""
178
+ ];
179
+ const lines = Object.entries(schema.shape).map(([key, zodSchema]) => {
180
+ let placeholder = "";
181
+ let othertype = null;
182
+ let actualSchema = zodSchema;
183
+ if (zodSchema instanceof import_zod3.ZodOptional) actualSchema = zodSchema.unwrap();
184
+ if (zodSchema instanceof import_zod3.ZodDefault) {
185
+ actualSchema = zodSchema.def.innerType;
186
+ placeholder = zodSchema.def.defaultValue ?? "";
187
+ othertype = typeof zodSchema.def.defaultValue;
188
+ }
189
+ const type = getZodTypeName(actualSchema);
190
+ return `${key}=${placeholder ?? ""} # ${othertype ?? type}`;
191
+ });
192
+ const content = [...header, ...lines].join("\n") + "\n";
193
+ import_fs2.default.writeFileSync(path, content);
194
+ console.log(content);
195
+ console.log(`\u2705 .env.example generated at ${path}`);
196
+ }
197
+
198
+ // src/core/define-env.ts
188
199
  function defineEnv(shape, options) {
189
200
  const fileEnv = loadDotEnv(options?.path);
190
201
  const merged = { ...fileEnv, ...process.env };
191
- const schema = import_zod2.z.object(shape);
192
- const parsed = parseEnv(schema, merged);
202
+ const schema = import_zod4.z.object(shape);
203
+ const parsed = parseEnv(schema, merged, options?.mode ?? "runtime");
204
+ if (options?.generateExample) {
205
+ generateExample(schema);
206
+ }
193
207
  return createTypedProxy(parsed);
194
208
  }
195
209
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.d.cts CHANGED
@@ -1,8 +1,5 @@
1
1
  import { z } from 'zod';
2
2
 
3
- interface DefineEnvOptions {
4
- path?: string;
5
- }
6
3
  declare const tx: {
7
4
  /**
8
5
  * STRICT STRING:
@@ -33,18 +30,22 @@ declare const tx: {
33
30
  * Strict URL checking.
34
31
  */
35
32
  url: (message?: string) => z.ZodURL;
36
- /**
37
- * EMAIL:
38
- * Strict Email checking.
39
- */
40
33
  email: (message?: string) => z.ZodEmail;
41
34
  positiveNumber: (message?: string) => z.ZodCoercedNumber<unknown>;
42
35
  nonEmptyString: (message?: string) => z.ZodString;
43
36
  semver: (message?: string) => z.ZodString;
44
37
  path: (message?: string) => z.ZodString;
45
- enum: <T extends readonly [string, ...string[]]>(values: T, message?: string) => z.ZodString;
46
- json: (message?: string) => z.ZodPipe<z.ZodString, z.ZodTransform<any, string>>;
38
+ enum: <T extends readonly [string, ...string[]]>(values: T, message?: string) => z.ZodEnum<{ [k_1 in T[number]]: k_1; } extends infer T_1 ? { [k in keyof T_1]: T_1[k]; } : never>;
39
+ json: <T = unknown>(message?: string) => z.ZodPipe<z.ZodString, z.ZodTransform<Awaited<T>, string>>;
47
40
  };
41
+
42
+ type ValidationMode = "runtime" | "build";
43
+ interface DefineEnvOptions {
44
+ path?: string;
45
+ mode?: ValidationMode;
46
+ generateExample?: boolean;
47
+ }
48
+
48
49
  declare function defineEnv<T extends z.ZodRawShape>(shape: T, options?: DefineEnvOptions): { [K in keyof T]: z.core.output<T[K]>; };
49
50
 
50
- export { defineEnv, tx };
51
+ export { type DefineEnvOptions, defineEnv, tx };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,5 @@
1
1
  import { z } from 'zod';
2
2
 
3
- interface DefineEnvOptions {
4
- path?: string;
5
- }
6
3
  declare const tx: {
7
4
  /**
8
5
  * STRICT STRING:
@@ -33,18 +30,22 @@ declare const tx: {
33
30
  * Strict URL checking.
34
31
  */
35
32
  url: (message?: string) => z.ZodURL;
36
- /**
37
- * EMAIL:
38
- * Strict Email checking.
39
- */
40
33
  email: (message?: string) => z.ZodEmail;
41
34
  positiveNumber: (message?: string) => z.ZodCoercedNumber<unknown>;
42
35
  nonEmptyString: (message?: string) => z.ZodString;
43
36
  semver: (message?: string) => z.ZodString;
44
37
  path: (message?: string) => z.ZodString;
45
- enum: <T extends readonly [string, ...string[]]>(values: T, message?: string) => z.ZodString;
46
- json: (message?: string) => z.ZodPipe<z.ZodString, z.ZodTransform<any, string>>;
38
+ enum: <T extends readonly [string, ...string[]]>(values: T, message?: string) => z.ZodEnum<{ [k_1 in T[number]]: k_1; } extends infer T_1 ? { [k in keyof T_1]: T_1[k]; } : never>;
39
+ json: <T = unknown>(message?: string) => z.ZodPipe<z.ZodString, z.ZodTransform<Awaited<T>, string>>;
47
40
  };
41
+
42
+ type ValidationMode = "runtime" | "build";
43
+ interface DefineEnvOptions {
44
+ path?: string;
45
+ mode?: ValidationMode;
46
+ generateExample?: boolean;
47
+ }
48
+
48
49
  declare function defineEnv<T extends z.ZodRawShape>(shape: T, options?: DefineEnvOptions): { [K in keyof T]: z.core.output<T[K]>; };
49
50
 
50
- export { defineEnv, tx };
51
+ export { type DefineEnvOptions, defineEnv, tx };
package/dist/index.js CHANGED
@@ -1,5 +1,69 @@
1
- // src/index.ts
2
- import { z as z2 } from "zod";
1
+ // src/core/tx.ts
2
+ import { z } from "zod";
3
+ var SEMVER_REGEX = /^\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/;
4
+ var tx = {
5
+ /**
6
+ * STRICT STRING:
7
+ * Rejects purely numeric strings (e.g. "12345").
8
+ * Good for API Keys.
9
+ */
10
+ string: (message = "Value should be text, but looks like a number.") => z.string().trim().regex(/^(?!\d+$).+$/, { error: message }).describe("Non-numeric string"),
11
+ /**
12
+ * SMART NUMBER:
13
+ * Automatically converts "3000" -> 3000.
14
+ * Fails if the value is not a valid number (e.g. "abc").
15
+ */
16
+ number: (message = "Must be a number") => z.coerce.number({ error: message }).finite().describe("Numeric value"),
17
+ /**
18
+ * PORT VALIDATOR:
19
+ * Coerces to number and ensures it is between 1 and 65535.
20
+ */
21
+ port: (message = "Must be a valid port (1\u201365535)") => z.coerce.number({ error: message }).int().min(1).max(65535).describe("TCP/UDP port number (1\u201365535)"),
22
+ /**
23
+ * SMART BOOLEAN:
24
+ * Handles "true", "TRUE", "1" -> true
25
+ * Handles "false", "FALSE", "0" -> false
26
+ * Throws error on anything else.
27
+ */
28
+ bool: (message) => {
29
+ return z.union([z.string(), z.boolean()]).transform((val, ctx) => {
30
+ if (typeof val === "boolean") return val;
31
+ const v = val.toLowerCase();
32
+ if (v === "true" || v === "1") return true;
33
+ if (v === "false" || v === "0") return false;
34
+ ctx.addIssue({
35
+ code: z.ZodIssueCode.custom,
36
+ message: message || 'Must be a boolean ("true"/"false"/"1"/"0")'
37
+ });
38
+ return z.NEVER;
39
+ });
40
+ },
41
+ /**
42
+ * URL:
43
+ * Strict URL checking.
44
+ */
45
+ url: (message = "Must be a valid URL (e.g. https://...)") => z.url({ error: message }).describe("A valid URL including protocol"),
46
+ email: (message = "Must be a valid email address") => z.email({ error: message }).describe("A valid email address"),
47
+ positiveNumber: (message = "Must be > 0") => z.coerce.number().gt(0, { error: message }).describe("A positive number"),
48
+ nonEmptyString: (message = "Cannot be empty") => z.string().trim().min(1, { error: message }).describe("A non-empty string"),
49
+ semver: (message = "Must be valid semver") => z.string().regex(SEMVER_REGEX, { error: message }).describe("Semantic version (x.y.z)"),
50
+ path: (message = "Must be a valid path") => z.string().trim().min(1, { error: message }).describe("Filesystem path"),
51
+ enum: (values, message = `Must be one of: ${values.join(", ")}`) => z.enum(values, { error: message }).describe(`One of: ${values.join(", ")}`),
52
+ json: (message = "Must be valid JSON") => z.string().transform((val, ctx) => {
53
+ try {
54
+ return JSON.parse(val);
55
+ } catch {
56
+ ctx.addIssue({
57
+ code: "custom",
58
+ error: message
59
+ });
60
+ return z.NEVER;
61
+ }
62
+ }).describe("JSON-encoded value")
63
+ };
64
+
65
+ // src/core/define-env.ts
66
+ import { z as z3 } from "zod";
3
67
 
4
68
  // src/loaders/dotenv.ts
5
69
  import dotenv from "dotenv";
@@ -20,22 +84,28 @@ Tip: Set { dotenv: false } if you manage environment variables yourself.`
20
84
 
21
85
  // src/core/parser.ts
22
86
  import "zod";
23
- function parseEnv(schema, source) {
87
+ function parseEnv(schema, source, mode = "runtime") {
24
88
  const result = schema.safeParse(source);
25
89
  if (!result.success) {
26
90
  const issues = result.error.issues.map((i) => `\u274C ${i.path.join(".")}: ${i.message}`).join("\n");
27
- throw new Error(
28
- [
29
- "",
30
- "\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
31
- "\u2502 \u274C INVALID ENVIRONMENT VARIABLES DETECTED \u2502",
32
- "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518",
33
- issues,
34
- ""
35
- ].join("\n")
36
- );
91
+ const header = mode === "build" ? [
92
+ "\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
93
+ "\u2502 \u274C INVALID ENVIRONMENT VARIABLES DETECTED \u2502",
94
+ "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"
95
+ ] : [
96
+ "\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
97
+ "\u2502 \u26A0 WARNING INVALID ENVIRONMENT VARIABLES \u26A0 \u2502",
98
+ "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"
99
+ ];
100
+ const box = ["", ...header, issues, ""].join("\n");
101
+ if (mode === "build") {
102
+ throw new Error(box);
103
+ } else {
104
+ console.warn(box);
105
+ process.exit(1);
106
+ }
37
107
  }
38
- return result.data;
108
+ return result.success ? result.data : {};
39
109
  }
40
110
 
41
111
  // src/core/proxy.ts
@@ -53,108 +123,56 @@ function createTypedProxy(obj) {
53
123
  });
54
124
  }
55
125
 
56
- // src/index.ts
57
- var tx = {
58
- /**
59
- * STRICT STRING:
60
- * Rejects purely numeric strings (e.g. "12345").
61
- * Good for API Keys.
62
- */
63
- string: (message) => {
64
- return z2.string().refine((val) => !/^\d+$/.test(val), {
65
- error: message || "Value should be text, but looks like a number."
66
- });
67
- },
68
- /**
69
- * SMART NUMBER:
70
- * Automatically converts "3000" -> 3000.
71
- * Fails if the value is not a valid number (e.g. "abc").
72
- */
73
- number: (message) => {
74
- return z2.coerce.number({ error: message || "Must be a number" });
75
- },
76
- /**
77
- * PORT VALIDATOR:
78
- * Coerces to number and ensures it is between 1 and 65535.
79
- */
80
- port: (message) => {
81
- return z2.coerce.number({
82
- error: message || "Must be a valid port (1-65535)"
83
- }).min(1, { error: message || "Port must be >= 1" }).max(65535, { error: message || "Port must be <= 65535" });
84
- },
85
- /**
86
- * SMART BOOLEAN:
87
- * Handles "true", "TRUE", "1" -> true
88
- * Handles "false", "FALSE", "0" -> false
89
- * Throws error on anything else.
90
- */
91
- bool: (message) => {
92
- return z2.union([z2.string(), z2.boolean()]).transform((val, ctx) => {
93
- if (typeof val === "boolean") return val;
94
- const v = val.toLowerCase();
95
- if (v === "true" || v === "1") return true;
96
- if (v === "false" || v === "0") return false;
97
- ctx.addIssue({
98
- code: z2.ZodIssueCode.custom,
99
- message: message || 'Must be a boolean ("true"/"false"/"1"/"0")'
100
- });
101
- return z2.NEVER;
102
- });
103
- },
104
- /**
105
- * URL:
106
- * Strict URL checking.
107
- */
108
- url: (message) => {
109
- return z2.url({
110
- error: message || "Must be a valid URL (e.g. https://...)"
111
- });
112
- },
113
- /**
114
- * EMAIL:
115
- * Strict Email checking.
116
- */
117
- email: (message) => {
118
- return z2.email({ error: message || "Must be a valid email address." });
119
- },
120
- positiveNumber: (message) => {
121
- return z2.coerce.number().gt(0, { message: message || "Must be > 0" });
122
- },
123
- nonEmptyString: (message) => {
124
- return z2.string().trim().min(1, { message: message || "Cannot be empty" });
125
- },
126
- semver: (message) => {
127
- return z2.string().refine((val) => /^\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/.test(val), {
128
- error: message || "Must be valid semver"
129
- });
130
- },
131
- path: (message) => {
132
- return z2.string().min(1, { error: message || "Must be a valid path" });
133
- },
134
- enum: (values, message) => {
135
- return z2.string().refine((val) => values.includes(val), {
136
- error: message || `Must be one of: ${values.join(", ")}`
137
- });
138
- },
139
- json: (message) => {
140
- return z2.string().transform((val, ctx) => {
141
- try {
142
- return JSON.parse(val);
143
- } catch {
144
- ctx.addIssue({
145
- code: z2.ZodIssueCode.custom,
146
- message: message || "Must be valid JSON"
147
- });
148
- return z2.NEVER;
149
- }
150
- });
151
- }
152
- };
126
+ // src/core/generate-example.ts
127
+ import {
128
+ ZodDefault,
129
+ ZodOptional,
130
+ ZodString,
131
+ ZodNumber,
132
+ ZodBoolean
133
+ } from "zod";
134
+ import fs2 from "fs";
135
+ function getZodTypeName(schema) {
136
+ let type = "unknown";
137
+ if (schema instanceof ZodString) type = "string";
138
+ else if (schema instanceof ZodNumber) type = "number";
139
+ else if (schema instanceof ZodBoolean) type = "boolean";
140
+ return type;
141
+ }
142
+ function generateExample(schema, path = ".env.example") {
143
+ const header = [
144
+ "# Example environment variables",
145
+ "# Copy this file to .env and fill in the values",
146
+ ""
147
+ ];
148
+ const lines = Object.entries(schema.shape).map(([key, zodSchema]) => {
149
+ let placeholder = "";
150
+ let othertype = null;
151
+ let actualSchema = zodSchema;
152
+ if (zodSchema instanceof ZodOptional) actualSchema = zodSchema.unwrap();
153
+ if (zodSchema instanceof ZodDefault) {
154
+ actualSchema = zodSchema.def.innerType;
155
+ placeholder = zodSchema.def.defaultValue ?? "";
156
+ othertype = typeof zodSchema.def.defaultValue;
157
+ }
158
+ const type = getZodTypeName(actualSchema);
159
+ return `${key}=${placeholder ?? ""} # ${othertype ?? type}`;
160
+ });
161
+ const content = [...header, ...lines].join("\n") + "\n";
162
+ fs2.writeFileSync(path, content);
163
+ console.log(content);
164
+ console.log(`\u2705 .env.example generated at ${path}`);
165
+ }
166
+
167
+ // src/core/define-env.ts
153
168
  function defineEnv(shape, options) {
154
169
  const fileEnv = loadDotEnv(options?.path);
155
170
  const merged = { ...fileEnv, ...process.env };
156
- const schema = z2.object(shape);
157
- const parsed = parseEnv(schema, merged);
171
+ const schema = z3.object(shape);
172
+ const parsed = parseEnv(schema, merged, options?.mode ?? "runtime");
173
+ if (options?.generateExample) {
174
+ generateExample(schema);
175
+ }
158
176
  return createTypedProxy(parsed);
159
177
  }
160
178
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zenvx",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",