zenvx 0.1.2 → 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
@@ -26,6 +26,16 @@ Standard Zod is great, but environment variables are always strings. This leads
26
26
 
27
27
  ---
28
28
 
29
+ ## Features
30
+
31
+ - ✅ Type-safe environment variables
32
+ - ✅ Smart coercion for numbers, booleans, and ports
33
+ - ✅ Strict validation for strings, URLs, emails, JSON, enums
34
+ - ✅ Beautiful, human-readable error reporting
35
+ - ✅ Seamless TypeScript integration
36
+ - ✅ `.env.example` auto-generation
37
+ - ✅ Runtime **and** build-time validation modes
38
+
29
39
  ## Installation
30
40
 
31
41
  ```bash
@@ -42,7 +52,7 @@ yarn add zenvx zod
42
52
 
43
53
  Create a file (e.g., src/env.ts) and export your configuration:
44
54
 
45
- ```javascript
55
+ ```ts
46
56
  import { defineEnv, tx } from "zenvx";
47
57
 
48
58
  export const env = defineEnv({
@@ -61,13 +71,84 @@ export const env = defineEnv({
61
71
 
62
72
  Now use it anywhere in your app:
63
73
 
64
- ```javascript
74
+ ```ts
65
75
  import { env } from "./env";
66
76
 
67
77
  console.log(`Server running on port ${env.PORT}`);
68
78
  // TypeScript knows env.PORT is a number!
69
79
  ```
70
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
+
71
152
  ## Beautiful Error Handling
72
153
 
73
154
  If your .env file is missing values or has invalid types, zenvx stops the process immediately and prints a clear message:
@@ -95,13 +176,12 @@ DATABASE_URL: Must be a valid URL
95
176
  | `tx.email()` | Valid email address. | `"admin@app.com"` | `"admin@..."` |
96
177
  | `tx.json()` | Parses a JSON string into an Object. | `{"foo":"bar"}` | `{foo: "bar"}` |
97
178
  | `tx.enum([...])` | Strict allow-list. | `"PROD"` | `"PROD"` |
98
- | `tx.ip()` | Validates IPv4 format. | `"192.168.1.1"` | `"192..."` |
99
179
 
100
180
  ## Customizing Error Messages
101
181
 
102
182
  Every tx validator accepts an optional custom error message.
103
183
 
104
- ```javascript
184
+ ```ts
105
185
  export const env = defineEnv({
106
186
  API_KEY: tx.string("Please provide a REAL API Key, not just numbers!"),
107
187
  PORT: tx.port("Port is invalid or out of range"),
@@ -114,7 +194,7 @@ Custom .env Path
114
194
 
115
195
  By default, zenvx looks for .env in your project root. You can change this:
116
196
 
117
- ```javascript
197
+ ```ts
118
198
  export const env = defineEnv(
119
199
  {
120
200
  PORT: tx.port(),
@@ -129,7 +209,7 @@ export const env = defineEnv(
129
209
 
130
210
  You can mix tx helpers with standard Zod schemas if you need specific logic.
131
211
 
132
- ```javascript
212
+ ```ts
133
213
  import { defineEnv, tx } from "zenvx";
134
214
  import { z } from "zod";
135
215
 
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,84 +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) => import_zod2.z.string().refine((val) => !/^\d+$/.test(val), {
99
- error: message || "Value should be text, but looks like a number."
100
- }),
101
- /**
102
- * SMART NUMBER:
103
- * Automatically converts "3000" -> 3000.
104
- * Fails if the value is not a valid number (e.g. "abc").
105
- */
106
- number: (message) => import_zod2.z.coerce.number({ error: message || "Must be a number" }),
107
- /**
108
- * PORT VALIDATOR:
109
- * Coerces to number and ensures it is between 1 and 65535.
110
- */
111
- port: (message) => import_zod2.z.coerce.number().min(1).max(65535).catch((ctx) => {
112
- ctx.error;
113
- return -1;
114
- }).refine((val) => val >= 1 && val <= 65535, {
115
- error: message || "Must be a valid port (1-65535)."
116
- }),
117
- /**
118
- * SMART BOOLEAN:
119
- * Handles "true", "TRUE", "1" -> true
120
- * Handles "false", "FALSE", "0" -> false
121
- * Throws error on anything else.
122
- */
123
- bool: (message) => import_zod2.z.string().transform((val, ctx) => {
124
- const v = val.toLowerCase();
125
- if (v === "true" || v === "1") return true;
126
- if (v === "false" || v === "0") return false;
127
- ctx.addIssue({
128
- code: import_zod2.z.ZodIssueCode.custom,
129
- error: message || "Must be a boolean (true/false or 1/0)."
130
- });
131
- return import_zod2.z.NEVER;
132
- }),
133
- /**
134
- * URL:
135
- * Strict URL checking.
136
- */
137
- url: (message) => import_zod2.z.url({ error: message || "Must be a valid URL (e.g. https://...)" }),
138
- /**
139
- * EMAIL:
140
- * Strict Email checking.
141
- */
142
- email: (message) => import_zod2.z.email({ error: message || "Must be a valid email address." }),
143
- positiveNumber: (message) => import_zod2.z.coerce.number().refine((val) => val > 0, { error: message || "Must be > 0" }),
144
- nonEmptyString: (message) => import_zod2.z.string().min(1, { error: message || "Cannot be empty" }),
145
- semver: (message) => import_zod2.z.string().refine((val) => /^\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/.test(val), {
146
- error: message || "Must be valid semver"
147
- }),
148
- path: (message) => import_zod2.z.string().min(1, { error: message || "Must be a valid path" }),
149
- enum: (values, message) => import_zod2.z.string().refine((val) => values.includes(val), {
150
- error: message || `Must be one of: ${values.join(", ")}`
151
- }),
152
- json: (message) => import_zod2.z.string().transform((val, ctx) => {
153
- try {
154
- return JSON.parse(val);
155
- } catch {
156
- ctx.addIssue({
157
- code: import_zod2.z.ZodIssueCode.custom,
158
- message: message || "Must be valid JSON"
159
- });
160
- return import_zod2.z.NEVER;
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;
161
188
  }
162
- })
163
- };
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
164
199
  function defineEnv(shape, options) {
165
200
  const fileEnv = loadDotEnv(options?.path);
166
201
  const merged = { ...fileEnv, ...process.env };
167
- const schema = import_zod2.z.object(shape);
168
- 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
+ }
169
207
  return createTypedProxy(parsed);
170
208
  }
171
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:
@@ -20,31 +17,35 @@ declare const tx: {
20
17
  * PORT VALIDATOR:
21
18
  * Coerces to number and ensures it is between 1 and 65535.
22
19
  */
23
- port: (message?: string) => z.ZodCatch<z.ZodCoercedNumber<unknown>>;
20
+ port: (message?: string) => z.ZodCoercedNumber<unknown>;
24
21
  /**
25
22
  * SMART BOOLEAN:
26
23
  * Handles "true", "TRUE", "1" -> true
27
24
  * Handles "false", "FALSE", "0" -> false
28
25
  * Throws error on anything else.
29
26
  */
30
- bool: (message?: string) => z.ZodPipe<z.ZodString, z.ZodTransform<boolean, string>>;
27
+ bool: (message?: string) => z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodBoolean]>, z.ZodTransform<boolean, string | boolean>>;
31
28
  /**
32
29
  * URL:
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:
@@ -20,31 +17,35 @@ declare const tx: {
20
17
  * PORT VALIDATOR:
21
18
  * Coerces to number and ensures it is between 1 and 65535.
22
19
  */
23
- port: (message?: string) => z.ZodCatch<z.ZodCoercedNumber<unknown>>;
20
+ port: (message?: string) => z.ZodCoercedNumber<unknown>;
24
21
  /**
25
22
  * SMART BOOLEAN:
26
23
  * Handles "true", "TRUE", "1" -> true
27
24
  * Handles "false", "FALSE", "0" -> false
28
25
  * Throws error on anything else.
29
26
  */
30
- bool: (message?: string) => z.ZodPipe<z.ZodString, z.ZodTransform<boolean, string>>;
27
+ bool: (message?: string) => z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodBoolean]>, z.ZodTransform<boolean, string | boolean>>;
31
28
  /**
32
29
  * URL:
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,84 +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) => z2.string().refine((val) => !/^\d+$/.test(val), {
64
- error: message || "Value should be text, but looks like a number."
65
- }),
66
- /**
67
- * SMART NUMBER:
68
- * Automatically converts "3000" -> 3000.
69
- * Fails if the value is not a valid number (e.g. "abc").
70
- */
71
- number: (message) => z2.coerce.number({ error: message || "Must be a number" }),
72
- /**
73
- * PORT VALIDATOR:
74
- * Coerces to number and ensures it is between 1 and 65535.
75
- */
76
- port: (message) => z2.coerce.number().min(1).max(65535).catch((ctx) => {
77
- ctx.error;
78
- return -1;
79
- }).refine((val) => val >= 1 && val <= 65535, {
80
- error: message || "Must be a valid port (1-65535)."
81
- }),
82
- /**
83
- * SMART BOOLEAN:
84
- * Handles "true", "TRUE", "1" -> true
85
- * Handles "false", "FALSE", "0" -> false
86
- * Throws error on anything else.
87
- */
88
- bool: (message) => z2.string().transform((val, ctx) => {
89
- const v = val.toLowerCase();
90
- if (v === "true" || v === "1") return true;
91
- if (v === "false" || v === "0") return false;
92
- ctx.addIssue({
93
- code: z2.ZodIssueCode.custom,
94
- error: message || "Must be a boolean (true/false or 1/0)."
95
- });
96
- return z2.NEVER;
97
- }),
98
- /**
99
- * URL:
100
- * Strict URL checking.
101
- */
102
- url: (message) => z2.url({ error: message || "Must be a valid URL (e.g. https://...)" }),
103
- /**
104
- * EMAIL:
105
- * Strict Email checking.
106
- */
107
- email: (message) => z2.email({ error: message || "Must be a valid email address." }),
108
- positiveNumber: (message) => z2.coerce.number().refine((val) => val > 0, { error: message || "Must be > 0" }),
109
- nonEmptyString: (message) => z2.string().min(1, { error: message || "Cannot be empty" }),
110
- semver: (message) => z2.string().refine((val) => /^\d+\.\d+\.\d+(-[0-9A-Za-z-.]+)?$/.test(val), {
111
- error: message || "Must be valid semver"
112
- }),
113
- path: (message) => z2.string().min(1, { error: message || "Must be a valid path" }),
114
- enum: (values, message) => z2.string().refine((val) => values.includes(val), {
115
- error: message || `Must be one of: ${values.join(", ")}`
116
- }),
117
- json: (message) => z2.string().transform((val, ctx) => {
118
- try {
119
- return JSON.parse(val);
120
- } catch {
121
- ctx.addIssue({
122
- code: z2.ZodIssueCode.custom,
123
- message: message || "Must be valid JSON"
124
- });
125
- return z2.NEVER;
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;
126
157
  }
127
- })
128
- };
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
129
168
  function defineEnv(shape, options) {
130
169
  const fileEnv = loadDotEnv(options?.path);
131
170
  const merged = { ...fileEnv, ...process.env };
132
- const schema = z2.object(shape);
133
- 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
+ }
134
176
  return createTypedProxy(parsed);
135
177
  }
136
178
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zenvx",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",