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 +73 -0
- package/dist/index.cjs +127 -113
- package/dist/index.d.cts +11 -10
- package/dist/index.d.ts +11 -10
- package/dist/index.js +131 -113
- package/package.json +1 -1
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
|
-
|
|
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,108 +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
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 =
|
|
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.
|
|
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:
|
|
@@ -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.
|
|
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,108 +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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 =
|
|
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 {
|