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 +86 -6
- package/dist/index.cjs +126 -88
- package/dist/index.d.cts +13 -12
- package/dist/index.d.ts +13 -12
- package/dist/index.js +130 -88
- package/package.json +1 -1
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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/
|
|
92
|
-
var
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
46
|
-
json: (message?: string) => z.ZodPipe<z.ZodString, z.ZodTransform<
|
|
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.
|
|
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.
|
|
46
|
-
json: (message?: string) => z.ZodPipe<z.ZodString, z.ZodTransform<
|
|
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/
|
|
2
|
-
import { z
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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/
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 =
|
|
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 {
|