Skip to content

Commit 46a14eb

Browse files
noahtignerCopilot
andauthored
v2: Support Zod Mini; Remove dotenv & dotenv-expand; Improve Logging & Functionality (#189)
* v2: initial refactoring * Support both Zod and Zod Mini (WIP) * Fully support Zod Mini & Zod v4 * Fix linting issues, clean up documentation, etc. * Delete local.ts Signed-off-by: Noah Tigner <46695778+noahtigner@users.noreply.github.com> * Remove dotenv & dotenv-expand (WIP) * Fix deep variable expansion * Simplify variable expansion logic & reorganize tests * Update README to reflect support for Zod v4/v4/mini * Update validateEnvVars docstring * Removed dotenv & dotenv-expand! * Update documentation to clarify inspiration from dotenv-expand * Add CHANGELOG.md; correct which versions of Zod are supported * Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Noah Tigner <46695778+noahtigner@users.noreply.github.com> * Clarify CHANGELOG, Align devDeps and peerDeps, improve test efficacy * Update expandValue to prevent infinite loops with circular env var references * Simplify & Document Regex Patterns --------- Signed-off-by: Noah Tigner <46695778+noahtigner@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 24722c5 commit 46a14eb

21 files changed

Lines changed: 1622 additions & 720 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ node_modules/
22
dist/
33
coverage/
44
local.js
5+
local.ts
6+
.git

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Changelog
2+
3+
## 2.0.0
4+
5+
- Add support for Zod Mini
6+
- Remove dotenv and dotenv-expand peer dependencies
7+
- Made the `envPath` parameter optional. If not provided, no file is read, and only `process.env` is validated
8+
- Improve how field `.meta` is logged (`title`, `description`, and `examples` supported)
9+
- Improve how `schema` is validated and field types are allowed/disallowed
10+
- Refactor the codebase to improve readability and maintainability
11+
- Update README with new instructions and examples
12+
- Updated tests to cover new functionality and edge cases
13+
14+
## 1.0.1
15+
16+
- Update eslint to v9, apply new linting rules, and bump several dev dependencies
17+
18+
## 1.0.0
19+
20+
- Update the tests to reflect new zod v4 error message
21+
- Update methodology of passing description data into a schema
22+
- Update method for pulling descriptions off of schema fields
23+
- Remove some @ts-expect-error comments that were no longer necessary
24+
- Update check for types allowed. (`_def` has been moved here)
25+
26+
## < 1.0.0
27+
28+
Please see the [commit history](https://github.com/noahtigner/validate-env-vars/commits/main/)

README.md

Lines changed: 64 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
</div>
1414

1515
<p align="center">
16-
A lightweight utility to check the presence and validity of environment variables, as specified by a Zod schema
16+
A lightweight utility for checking the presence and validity of environment variables, as specified by a Zod schema.
17+
</p>
18+
19+
<p align="center">
20+
validate-env-vars supports Zod v4 and Zod Mini!
1721
</p>
1822

1923
# Installation
@@ -31,50 +35,41 @@ npm install validate-env-vars --save-dev
3135
```javascript
3236
#!/usr/bin/env node
3337

34-
import validateEnvVars, {
35-
envEnum,
36-
envString,
37-
envNonEmptyString,
38-
} from 'validate-env-vars';
38+
import validateEnvVars from 'validate-env-vars';
39+
import { z } from 'zod';
3940

40-
const envSchema = envObject({
41-
NODE_ENV: envEnum(['development', 'production', 'test']),
42-
API_BASE: envString().url(),
43-
GITHUB_USERNAME: envNonEmptyString(),
41+
const envSchema = z.object({
42+
NODE_ENV: z.enum(['development', 'production', 'test']),
43+
API_BASE: z.url(),
44+
GITHUB_USERNAME: z.string().min(1),
4445
});
4546

4647
validateEnvVars({ schema: envSchema });
4748
```
4849

49-
You may use the predefined `env*` functions, or create your own using Zod
50-
5150
---
5251

5352
### Programmatically check an .env.production file against a Zod schema:
5453

5554
```javascript
56-
import validateEnvVars, {
57-
envEnum,
58-
envString,
59-
envNonEmptyString,
60-
} from 'validate-env-vars';
61-
62-
const envSchema = envObject({
63-
NODE_ENV: envEnum(['development', 'production', 'test']),
64-
API_BASE: envString().url(),
65-
GITHUB_USERNAME: envNonEmptyString(),
55+
import validateEnvVars from 'validate-env-vars';
56+
import { z } from 'zod';
57+
58+
const envSchema = z.object({
59+
NODE_ENV: z.enum(['development', 'production', 'test']),
60+
API_BASE: z.url(),
61+
GITHUB_USERNAME: z.string().min(1),
6662
});
6763

68-
const prefilight() => {
69-
try {
70-
validateEnvVars({ schema: envSchema, envPath: '.env.production' })
71-
// ... other code
72-
}
73-
catch (error) {
74-
console.error(error);
75-
// ... other code
76-
}
77-
}
64+
const preflight = () => {
65+
try {
66+
validateEnvVars({ schema: envSchema, envPath: '.env.production' });
67+
// ... other code
68+
} catch (error) {
69+
console.error(error);
70+
// ... other code
71+
}
72+
};
7873
```
7974

8075
---
@@ -84,16 +79,12 @@ const prefilight() => {
8479
1. Define a Zod schema in a .ts file at the root of your project
8580

8681
```javascript
87-
import validateEnvVars, {
88-
envEnum,
89-
envString,
90-
envNonEmptyString,
91-
} from 'validate-env-vars';
92-
93-
const envSchema = envObject({
94-
NODE_ENV: envEnum(['development', 'production', 'test']),
95-
VITE_API_BASE: envString().url(),
96-
VITE_GITHUB_USERNAME: envNonEmptyString(),
82+
import { z } from 'zod';
83+
84+
const envSchema = z.object({
85+
NODE_ENV: z.enum(['development', 'production', 'test']),
86+
VITE_API_BASE: z.url(),
87+
VITE_GITHUB_USERNAME: z.string().min(1),
9788
});
9889

9990
// make the type of the environment variables available globally
@@ -136,15 +127,40 @@ interface ImportMeta {
136127
137128
4. Add your schema configuration file to your tsconfig's `include`
138129
139-
# Tips:
140-
141-
- If you don't have a `.env` file, you can pass an empty file. This is useful for testing and CI/CD environments, where environment variables may be set programmatically.
142-
143130
# Config Options
144131
145132
| Option | Type | Description | Default |
146133
| ------------------------ | ----------- | -------------------------------------------------------------- | ------- |
147-
| `schema` | `EnvObject` | The schema to validate against | |
148-
| `envPath` (optional) | `string` | The path to the .env file | `.env` |
134+
| `schema` | `EnvObject` | The schema to validate against (must use string-based types) | |
135+
| `envPath` (optional) | `string` | The path to the .env file | |
149136
| `exitOnError` (optional) | `boolean` | Whether to exit the process or throw if validation fails | `false` |
150137
| `logVars` (optional) | `boolean` | Whether to output successfully parsed variables to the console | `true` |
138+
139+
**Note:** The `schema` must be a `z.object()` whose fields use string-based types—such as `z.string()`, `z.enum()`, `z.literal()`, or compositions like union/optional of these types. You may also use any Zod string refinements and formats (e.g., `.min()`, `.max()`, `.url()`, `.email()`, `.regex()`, `.refine()`, etc.) to validate and transform string values. Environment variables are always read as strings.
140+
141+
# Schema Recipes
142+
143+
Since environment variables are always read as strings, you'll need to validate and transform them appropriately. Here are some common patterns:
144+
145+
```javascript
146+
const envNonEmptyString = () =>
147+
z
148+
.string()
149+
.min(1, { message: 'Variable cannot be empty' })
150+
.refine((val) => val !== 'undefined', {
151+
message: "Variable cannot equal 'undefined'",
152+
});
153+
154+
// Integer from string
155+
const envInteger = () =>
156+
z.string().regex(/^-?\d+$/, {
157+
message: 'Variable must be a valid integer',
158+
});
159+
160+
// Boolean from string
161+
const envBoolean = () => z.enum(['true', 'false']);
162+
163+
// Comma-separated list
164+
const envList = () =>
165+
z.string().transform((val) => val.split(',').map((s) => s.trim()));
166+
```

__tests__/.env.test

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
EXPECTED_1=one
2-
EXPECTED_2=true
3-
EXPECTED_3=1
4-
EXPANDED_1=${EXPECTED_1}
5-
EXPANDED_2=${SOME_NON_EXISTENT_ENV_VAR}
1+
TEST_EXPECTED_1=one
2+
TEST_EXPECTED_NUMBER=1
3+
TEST_EXPECTED_INT=42
4+
TEST_EXPECTED_URL=http://example.com
5+
TEST_EXPANDED_1=${TEST_EXPECTED_1}
6+
TEST_EXPANDED_2=${SOME_NON_EXISTENT_ENV_VAR}
7+
TEST_EXPANDED_3=${TEST_EXPANDED_1}+${TEST_EXPECTED_2}
8+
TEST_EXPECTED_2=true
9+
TEST_EXPANDED_WITH_FALLBACK=${SOME_NON_EXISTENT_ENV_VAR:-fallback}

__tests__/compatibility.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { z as zodClassic } from 'zod';
2+
import * as zodMini from 'zod/mini';
3+
4+
import { validate } from '../src/validateInput';
5+
6+
const zodClassicSchema = zodClassic.object({
7+
VAR1: zodClassic.string(),
8+
VAR2: zodClassic.enum(['value1', 'value2']),
9+
});
10+
11+
const zodMiniSchema = zodMini.object({
12+
VAR1: zodMini.string(),
13+
VAR2: zodMini.enum(['value1', 'value2']),
14+
});
15+
16+
describe('Library is compatible with both Zod v4 and Zod Mini', () => {
17+
it('validates schemas', () => {
18+
const data = { VAR1: 'test', VAR2: 'value1' };
19+
expect(() =>
20+
validate({ schema: zodClassicSchema, vars: data, logVars: true })
21+
).not.toThrow();
22+
expect(() =>
23+
validate({ schema: zodMiniSchema, vars: data, logVars: true })
24+
).not.toThrow();
25+
});
26+
it('throws for invalid schemas', () => {
27+
const invalidData = { VAR1: 'test', VAR2: 'invalidValue' };
28+
expect(() =>
29+
validate({
30+
schema: zodClassicSchema,
31+
vars: invalidData,
32+
logVars: true,
33+
})
34+
).toThrow();
35+
expect(() =>
36+
validate({
37+
schema: zodMiniSchema,
38+
vars: invalidData,
39+
logVars: true,
40+
})
41+
).toThrow();
42+
});
43+
});

0 commit comments

Comments
 (0)