dotenv-gad 1.4.0 → 1.4.2
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 +98 -114
- package/dist/client.cjs +32 -0
- package/dist/index.cjs +436 -0
- package/dist/index.js +2 -1
- package/dist/utils.js +2 -1
- package/dist/vite-plugin.js +1 -1
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
# dotenv-gad
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/js/dotenv-gad)
|
|
4
|
+
[](https://github.com/kasimlyee/dotenv-gad/actions/workflows/ci.yml)
|
|
4
5
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://nodejs.org)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://kasimlyee.github.io/dotenv-gad/)
|
|
9
|
+
[](https://www.npmjs.com/package/dotenv-gad)
|
|
10
|
+
|
|
11
|
+
**dotenv-gad** is an environment variable validation library that brings type safety, schema validation, and runtime checks to your Node.js and JavaScript applications. It works with any environment variable source — `.env` files, platform dashboards (Vercel, Railway, Docker), CI/CD pipelines, or `process.env` directly.
|
|
12
|
+
|
|
13
|
+
- Type-safe environment variables with full IntelliSense
|
|
14
|
+
- Schema validation (string, number, boolean, url, email, ip, port, json, array, object)
|
|
15
|
+
- Schema composition for modular configs
|
|
16
|
+
- Automatic documentation and `.env.example` generation
|
|
17
|
+
- First-class TypeScript support
|
|
18
|
+
- CLI tooling (check, sync, types, init, fix, docs)
|
|
19
|
+
- Sensitive value management and redaction
|
|
20
|
+
- Vite plugin with client-safe filtering and HMR
|
|
16
21
|
|
|
17
22
|
## Installation
|
|
18
23
|
|
|
@@ -55,22 +60,13 @@ const env = loadEnv(schema);
|
|
|
55
60
|
console.log(`Server running on port ${env.PORT}`);
|
|
56
61
|
```
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
[](https://kasimlyee.github.io/dotenv-gad/latest/)
|
|
61
|
-
|
|
62
|
-
Full documentation is available via GitHub Pages (published from `docs/`).
|
|
63
|
-
|
|
64
|
-
To preview locally:
|
|
63
|
+
`loadEnv` reads from both `process.env` and your `.env` file (if present). This means it works out of the box on platforms like Vercel, Railway, Docker, and AWS Lambda where variables are injected into `process.env` directly — no `.env` file required.
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
npm ci
|
|
68
|
-
npm run docs:serve
|
|
69
|
-
```
|
|
65
|
+
## Documentation
|
|
70
66
|
|
|
71
|
-
Docs
|
|
67
|
+
[](https://kasimlyee.github.io/dotenv-gad/)
|
|
72
68
|
|
|
73
|
-
|
|
69
|
+
Full documentation is available at [kasimlyee.github.io/dotenv-gad](https://kasimlyee.github.io/dotenv-gad/).
|
|
74
70
|
|
|
75
71
|
## CLI Commands
|
|
76
72
|
|
|
@@ -90,11 +86,12 @@ npx dotenv-gad types
|
|
|
90
86
|
|
|
91
87
|
## Vite Plugin
|
|
92
88
|
|
|
93
|
-
The Vite plugin
|
|
89
|
+
The Vite plugin validates environment variables at build time and exposes a typed, client-safe subset to your browser code via a virtual module.
|
|
94
90
|
|
|
95
|
-
###
|
|
91
|
+
### Setup
|
|
96
92
|
|
|
97
93
|
```typescript
|
|
94
|
+
// vite.config.ts
|
|
98
95
|
import { defineConfig } from "vite";
|
|
99
96
|
import dotenvGad from "dotenv-gad/vite";
|
|
100
97
|
|
|
@@ -102,38 +99,39 @@ export default defineConfig({
|
|
|
102
99
|
plugins: [
|
|
103
100
|
dotenvGad({
|
|
104
101
|
schemaPath: "./env.schema.ts",
|
|
105
|
-
clientPrefix: "VITE_",
|
|
106
|
-
publicKeys: [],
|
|
107
|
-
generatedTypes: true,
|
|
102
|
+
// clientPrefix: "VITE_", // default — keys matching this prefix are exposed
|
|
103
|
+
// publicKeys: [], // additional non-prefixed keys to expose
|
|
104
|
+
// generatedTypes: true, // generate .d.ts for IntelliSense
|
|
108
105
|
}),
|
|
109
106
|
],
|
|
110
107
|
});
|
|
111
108
|
```
|
|
112
109
|
|
|
113
|
-
###
|
|
110
|
+
### Usage
|
|
114
111
|
|
|
115
112
|
```typescript
|
|
116
113
|
import { env } from "dotenv-gad/client";
|
|
117
114
|
|
|
118
|
-
console.log(env.VITE_API_URL); // Full type safety &
|
|
115
|
+
console.log(env.VITE_API_URL); // Full type safety & autocomplete
|
|
119
116
|
```
|
|
120
117
|
|
|
121
118
|
### Key Features
|
|
122
119
|
|
|
123
|
-
- **Build-time validation
|
|
124
|
-
- **Client-safe filtering
|
|
125
|
-
- **
|
|
126
|
-
- **
|
|
127
|
-
- **HMR support
|
|
120
|
+
- **Build-time validation** — environment checked every dev/build cycle
|
|
121
|
+
- **Client-safe filtering** — only `VITE_*` prefixed variables (or custom `publicKeys`) exposed to browser
|
|
122
|
+
- **Sensitive protection** — variables marked `sensitive: true` are always excluded
|
|
123
|
+
- **Auto-generated types** — `dotenv-gad.d.ts` gives full IntelliSense on `env.`
|
|
124
|
+
- **HMR support** — hot reload on `.env` or schema changes during development
|
|
125
|
+
- **SSR safety** — server-side code gets the full env, not the filtered subset
|
|
128
126
|
|
|
129
127
|
## Features
|
|
130
128
|
|
|
131
129
|
### Core Validation
|
|
132
130
|
|
|
133
|
-
- Type checking (string, number, boolean, array, object)
|
|
134
|
-
- Required/optional fields
|
|
135
|
-
- Default values
|
|
131
|
+
- Type checking (string, number, boolean, array, object, url, email, ip, port, json, date)
|
|
132
|
+
- Required/optional fields with defaults
|
|
136
133
|
- Custom validation functions
|
|
134
|
+
- Value transforms
|
|
137
135
|
- Environment-specific rules
|
|
138
136
|
|
|
139
137
|
### Advanced Types
|
|
@@ -150,13 +148,23 @@ console.log(env.VITE_API_URL); // Full type safety & validation
|
|
|
150
148
|
}
|
|
151
149
|
```
|
|
152
150
|
|
|
153
|
-
###
|
|
151
|
+
### Schema Composition
|
|
154
152
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
Merge multiple schemas for modular configuration:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { defineSchema, composeSchema } from "dotenv-gad";
|
|
157
|
+
|
|
158
|
+
const baseSchema = defineSchema({
|
|
159
|
+
NODE_ENV: { type: "string", default: "development" },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const dbSchema = defineSchema({
|
|
163
|
+
DATABASE_URL: { type: "string", required: true, sensitive: true },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const schema = composeSchema(baseSchema, dbSchema);
|
|
167
|
+
```
|
|
160
168
|
|
|
161
169
|
### Secret Management
|
|
162
170
|
|
|
@@ -164,12 +172,37 @@ console.log(env.VITE_API_URL); // Full type safety & validation
|
|
|
164
172
|
{
|
|
165
173
|
API_KEY: {
|
|
166
174
|
type: 'string',
|
|
167
|
-
sensitive: true,
|
|
175
|
+
sensitive: true, // masked in errors, excluded from .env.example
|
|
168
176
|
validate: (val) => val.startsWith('sk_')
|
|
169
177
|
}
|
|
170
178
|
}
|
|
171
179
|
```
|
|
172
180
|
|
|
181
|
+
### Grouping / Namespaced Envs
|
|
182
|
+
|
|
183
|
+
Group related variables into a single validated object:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
const schema = defineSchema({
|
|
187
|
+
DATABASE: {
|
|
188
|
+
type: "object",
|
|
189
|
+
envPrefix: "DATABASE_", // optional; defaults to 'DATABASE_'
|
|
190
|
+
properties: {
|
|
191
|
+
DB_NAME: { type: "string", required: true },
|
|
192
|
+
PORT: { type: "port", default: 5432 },
|
|
193
|
+
PWD: { type: "string", sensitive: true },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Given `DATABASE_DB_NAME=mydb`, `DATABASE_PORT=5432`, `DATABASE_PWD=supersecret`:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const env = loadEnv(schema);
|
|
203
|
+
// { DATABASE: { DB_NAME: 'mydb', PORT: 5432, PWD: 'supersecret' } }
|
|
204
|
+
```
|
|
205
|
+
|
|
173
206
|
## Framework Integrations
|
|
174
207
|
|
|
175
208
|
### Express.js
|
|
@@ -189,8 +222,6 @@ app.listen(env.PORT, () => {
|
|
|
189
222
|
|
|
190
223
|
### Next.js
|
|
191
224
|
|
|
192
|
-
Create `next.config.js`:
|
|
193
|
-
|
|
194
225
|
```javascript
|
|
195
226
|
const { loadEnv } = require("dotenv-gad");
|
|
196
227
|
const schema = require("./env.schema");
|
|
@@ -204,9 +235,7 @@ module.exports = {
|
|
|
204
235
|
};
|
|
205
236
|
```
|
|
206
237
|
|
|
207
|
-
##
|
|
208
|
-
|
|
209
|
-
Example error output:
|
|
238
|
+
## Error Reporting
|
|
210
239
|
|
|
211
240
|
```
|
|
212
241
|
Environment validation failed:
|
|
@@ -215,38 +244,20 @@ Environment validation failed:
|
|
|
215
244
|
- API_KEY: Must start with 'sk_' (received: "invalid")
|
|
216
245
|
```
|
|
217
246
|
|
|
218
|
-
|
|
247
|
+
Sensitive values are always masked in error output. Use `includeRaw` for local debugging:
|
|
219
248
|
|
|
220
|
-
```
|
|
221
|
-
// include raw values in errors (non-sensitive values only)
|
|
222
|
-
import { loadEnv } from "dotenv-gad";
|
|
249
|
+
```typescript
|
|
223
250
|
const env = loadEnv(schema, { includeRaw: true });
|
|
224
251
|
|
|
225
252
|
// or with finer control
|
|
226
253
|
import { EnvValidator } from "dotenv-gad";
|
|
227
|
-
const validator = new EnvValidator(schema, {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
console.error(String(err));
|
|
232
|
-
}
|
|
254
|
+
const validator = new EnvValidator(schema, {
|
|
255
|
+
includeRaw: true,
|
|
256
|
+
includeSensitive: true,
|
|
257
|
+
});
|
|
233
258
|
```
|
|
234
259
|
|
|
235
|
-
##
|
|
236
|
-
|
|
237
|
-
### Environment-Specific Rules
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
{
|
|
241
|
-
DEBUG: {
|
|
242
|
-
type: 'boolean',
|
|
243
|
-
env: {
|
|
244
|
-
development: { default: true },
|
|
245
|
-
production: { default: false }
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
```
|
|
260
|
+
## More Examples
|
|
250
261
|
|
|
251
262
|
### Custom Validators
|
|
252
263
|
|
|
@@ -260,7 +271,7 @@ try {
|
|
|
260
271
|
}
|
|
261
272
|
```
|
|
262
273
|
|
|
263
|
-
###
|
|
274
|
+
### Transforms
|
|
264
275
|
|
|
265
276
|
```typescript
|
|
266
277
|
{
|
|
@@ -271,49 +282,22 @@ try {
|
|
|
271
282
|
}
|
|
272
283
|
```
|
|
273
284
|
|
|
274
|
-
###
|
|
275
|
-
|
|
276
|
-
You can group related environment variables into a single object using `object` with `properties` and an optional `envPrefix` (defaults to `KEY_`):
|
|
285
|
+
### Environment-Specific Rules
|
|
277
286
|
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
type: '
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
PORT: { type: 'port', default: 5432 },
|
|
286
|
-
PWD: { type: 'string', sensitive: true }
|
|
287
|
+
```typescript
|
|
288
|
+
{
|
|
289
|
+
DEBUG: {
|
|
290
|
+
type: 'boolean',
|
|
291
|
+
env: {
|
|
292
|
+
development: { default: true },
|
|
293
|
+
production: { default: false }
|
|
287
294
|
}
|
|
288
295
|
}
|
|
289
|
-
}
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
Given the following environment:
|
|
293
|
-
|
|
294
|
-
```
|
|
295
|
-
DATABASE_DB_NAME=mydb
|
|
296
|
-
DATABASE_PORT=5432
|
|
297
|
-
DATABASE_PWD=supersecret
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
`loadEnv(schema)` will return:
|
|
301
|
-
|
|
302
|
-
```ts
|
|
303
|
-
{ DATABASE: { DB_NAME: 'mydb', PORT: 5432, PWD: 'supersecret' } }
|
|
296
|
+
}
|
|
304
297
|
```
|
|
305
298
|
|
|
306
|
-
Notes and behavior:
|
|
307
|
-
|
|
308
|
-
- The default `envPrefix` is `${KEY}_` (for `DATABASE` it's `DATABASE_`) if you don't specify `envPrefix`.
|
|
309
|
-
- Prefixed variables take precedence over a JSON top-level env var (e.g., `DATABASE` = '{...}'). If both are present, prefixed variables win and a warning is printed.
|
|
310
|
-
- In strict mode (`{ strict: true }`), unexpected subkeys inside a group (e.g., `DATABASE_EXTRA`) will cause validation to fail.
|
|
311
|
-
- `sensitive` and `includeRaw` behavior still applies for grouped properties: sensitive properties are still masked in errors unless `includeSensitive` is explicitly set.
|
|
312
|
-
|
|
313
|
-
The CLI `sync` command will now generate grouped entries in `.env.example` for object properties so it's easier to scaffold grouped configuration.
|
|
314
|
-
|
|
315
299
|
## License
|
|
316
300
|
|
|
317
301
|
MIT © [Kasim Lyee]
|
|
318
302
|
|
|
319
|
-
[Contributions](https://github.com/kasimlyee/dotenv-gad/blob/main/CONTRIBUTING.md) are welcome
|
|
303
|
+
[Contributions](https://github.com/kasimlyee/dotenv-gad/blob/main/CONTRIBUTING.md) are welcome!
|
package/dist/client.cjs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/client.ts
|
|
21
|
+
var client_exports = {};
|
|
22
|
+
__export(client_exports, {
|
|
23
|
+
default: () => client_default,
|
|
24
|
+
env: () => env
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(client_exports);
|
|
27
|
+
var env = {};
|
|
28
|
+
var client_default = env;
|
|
29
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
30
|
+
0 && (module.exports = {
|
|
31
|
+
env
|
|
32
|
+
});
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
AggregateError: () => AggregateError,
|
|
34
|
+
EnvAggregateError: () => EnvAggregateError,
|
|
35
|
+
EnvValidationError: () => EnvValidationError,
|
|
36
|
+
EnvValidator: () => EnvValidator,
|
|
37
|
+
composeSchema: () => composeSchema,
|
|
38
|
+
createEnvProxy: () => createEnvProxy,
|
|
39
|
+
defineSchema: () => defineSchema,
|
|
40
|
+
loadEnv: () => loadEnv,
|
|
41
|
+
validateEnv: () => validateEnv
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(src_exports);
|
|
44
|
+
|
|
45
|
+
// src/errors.ts
|
|
46
|
+
var EnvValidationError = class extends Error {
|
|
47
|
+
constructor(key, message, receiveValue) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.key = key;
|
|
50
|
+
this.message = message;
|
|
51
|
+
this.receiveValue = receiveValue;
|
|
52
|
+
this.name = "EnvValidationError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var EnvAggregateError = class _EnvAggregateError extends Error {
|
|
56
|
+
constructor(errors, message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.errors = errors;
|
|
59
|
+
this.name = "EnvAggregateError";
|
|
60
|
+
Object.setPrototypeOf(this, _EnvAggregateError.prototype);
|
|
61
|
+
}
|
|
62
|
+
toString() {
|
|
63
|
+
const errorList = this.errors.map((e) => {
|
|
64
|
+
let msg = ` - ${e.key}: ${e.message}`;
|
|
65
|
+
if (e.value !== void 0)
|
|
66
|
+
msg += ` (received: ${JSON.stringify(e.value)})`;
|
|
67
|
+
if (e.rule?.docs) msg += `
|
|
68
|
+
${e.rule.docs}`;
|
|
69
|
+
return msg;
|
|
70
|
+
}).join("\n");
|
|
71
|
+
return `${this.message}:
|
|
72
|
+
${errorList}`;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var AggregateError = EnvAggregateError;
|
|
76
|
+
|
|
77
|
+
// src/validator.ts
|
|
78
|
+
var EnvValidator = class {
|
|
79
|
+
/**
|
|
80
|
+
* Constructs a new EnvValidator instance.
|
|
81
|
+
* @param {SchemaDefinition} schema The schema definition for the environment variables.
|
|
82
|
+
* @param {Object} [options] Optional options for the validation process.
|
|
83
|
+
* @param {boolean} [options.strict] When true, environment variables not present in the schema will be rejected.
|
|
84
|
+
*/
|
|
85
|
+
constructor(schema, options) {
|
|
86
|
+
this.schema = schema;
|
|
87
|
+
this.options = options;
|
|
88
|
+
}
|
|
89
|
+
errors = [];
|
|
90
|
+
validate(env) {
|
|
91
|
+
this.errors = [];
|
|
92
|
+
const result = {};
|
|
93
|
+
const groupedEnv = {};
|
|
94
|
+
const prefixes = [];
|
|
95
|
+
for (const [k, r] of Object.entries(this.schema)) {
|
|
96
|
+
const eff = this.getEffectiveRule(k, r);
|
|
97
|
+
if (eff.type === "object" && eff.properties) {
|
|
98
|
+
const prefix = eff.envPrefix ?? `${k}_`;
|
|
99
|
+
prefixes.push({ key: k, prefix });
|
|
100
|
+
groupedEnv[k] = {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const envKeys = Object.keys(env);
|
|
104
|
+
for (let i = 0; i < envKeys.length; i++) {
|
|
105
|
+
const eKey = envKeys[i];
|
|
106
|
+
for (let j = 0; j < prefixes.length; j++) {
|
|
107
|
+
const { key, prefix } = prefixes[j];
|
|
108
|
+
if (eKey.startsWith(prefix)) {
|
|
109
|
+
const subKey = eKey.slice(prefix.length);
|
|
110
|
+
groupedEnv[key][subKey] = env[eKey];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const schemaKeys = Object.keys(this.schema);
|
|
115
|
+
for (let i = 0; i < schemaKeys.length; i++) {
|
|
116
|
+
const key = schemaKeys[i];
|
|
117
|
+
const rule = this.schema[key];
|
|
118
|
+
try {
|
|
119
|
+
const valToValidate = groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0 ? groupedEnv[key] : env[key];
|
|
120
|
+
if (groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0 && env[key] !== void 0) {
|
|
121
|
+
console.warn(`Both prefixed variables and top-level ${key} exist; prefixed vars are used`);
|
|
122
|
+
}
|
|
123
|
+
if (this.options?.strict && groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0) {
|
|
124
|
+
const propNames = rule.properties ? Object.keys(rule.properties) : [];
|
|
125
|
+
const extras = Object.keys(groupedEnv[key]).filter((s) => !propNames.includes(s));
|
|
126
|
+
if (extras.length > 0) {
|
|
127
|
+
this.errors.push({
|
|
128
|
+
key,
|
|
129
|
+
message: `Unexpected grouped environment variables: ${extras.join(", ")}`,
|
|
130
|
+
value: Object.keys(groupedEnv[key]),
|
|
131
|
+
rule
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
result[key] = this.validateKey(key, rule, valToValidate);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error instanceof Error) {
|
|
139
|
+
let displayedValue;
|
|
140
|
+
if (env[key] === void 0) {
|
|
141
|
+
displayedValue = void 0;
|
|
142
|
+
} else if (this.options?.includeRaw) {
|
|
143
|
+
if (rule.sensitive && !this.options?.includeSensitive) {
|
|
144
|
+
displayedValue = "****";
|
|
145
|
+
} else {
|
|
146
|
+
displayedValue = env[key];
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
displayedValue = this.redactValue(env[key], rule.sensitive);
|
|
150
|
+
}
|
|
151
|
+
this.errors.push({
|
|
152
|
+
key,
|
|
153
|
+
message: error.message,
|
|
154
|
+
value: displayedValue,
|
|
155
|
+
rule
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (this.options?.strict) {
|
|
161
|
+
const prefixedKeys = /* @__PURE__ */ new Set();
|
|
162
|
+
for (const p of prefixes) {
|
|
163
|
+
for (const eKey of envKeys) {
|
|
164
|
+
if (eKey.startsWith(p.prefix)) prefixedKeys.add(eKey);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const k of envKeys) {
|
|
168
|
+
if (!(k in this.schema) && !prefixedKeys.has(k)) {
|
|
169
|
+
this.errors.push({
|
|
170
|
+
key: k,
|
|
171
|
+
message: `Unexpected environment variable`
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (this.errors.length > 0) {
|
|
177
|
+
const keys = this.errors.map((e) => e.key).join(", ");
|
|
178
|
+
throw new EnvAggregateError(
|
|
179
|
+
this.errors,
|
|
180
|
+
`Environment validation failed: ${keys}`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
// Redact or trim sensitive values for error reporting
|
|
186
|
+
redactValue(value, sensitive) {
|
|
187
|
+
if (value === void 0) return void 0;
|
|
188
|
+
if (sensitive) return "****";
|
|
189
|
+
if (typeof value !== "string") return value;
|
|
190
|
+
if (value.length > 64) {
|
|
191
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
// Try to quickly determine if a string *might* be JSON before parsing to avoid
|
|
196
|
+
// costly exceptions in the hot path for clearly non-JSON values.
|
|
197
|
+
tryParseJSON(value) {
|
|
198
|
+
if (typeof value !== "string") return { ok: false };
|
|
199
|
+
const s = value.trim();
|
|
200
|
+
if (!s) return { ok: false };
|
|
201
|
+
const c = s[0];
|
|
202
|
+
if (c !== "{" && c !== "[" && c !== '"' && c !== "t" && c !== "f" && c !== "n" && (c < "0" || c > "9") && c !== "-") {
|
|
203
|
+
return { ok: false };
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
return { ok: true, value: JSON.parse(s) };
|
|
207
|
+
} catch {
|
|
208
|
+
return { ok: false };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
validateKey(key, rule, value) {
|
|
212
|
+
const effectiveRule = this.getEffectiveRule(key, rule);
|
|
213
|
+
if (value === void 0 || value === "") {
|
|
214
|
+
if (effectiveRule.required)
|
|
215
|
+
throw new Error(`Missing required environment variable`);
|
|
216
|
+
return effectiveRule.default;
|
|
217
|
+
}
|
|
218
|
+
if (effectiveRule.transform) {
|
|
219
|
+
value = effectiveRule.transform(value);
|
|
220
|
+
}
|
|
221
|
+
switch (effectiveRule.type) {
|
|
222
|
+
case "string":
|
|
223
|
+
value = String(value).trim();
|
|
224
|
+
if (effectiveRule.minLength !== void 0 && value.length < effectiveRule.minLength) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Environment variable ${key} must be at least ${effectiveRule.minLength} characters`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (effectiveRule.maxLength !== void 0 && value.length > effectiveRule.maxLength) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`Environment variable ${key} must be at most ${effectiveRule.maxLength} characters`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
case "number":
|
|
236
|
+
value = Number(value);
|
|
237
|
+
if (isNaN(value)) {
|
|
238
|
+
throw new Error(`Environment variable ${key} must be a number`);
|
|
239
|
+
}
|
|
240
|
+
if (effectiveRule.min !== void 0 && value < effectiveRule.min) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Environment variable ${key} must be at least ${effectiveRule.min}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (effectiveRule.max !== void 0 && value > effectiveRule.max) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Environment variable ${key} must be at most ${effectiveRule.max}`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
case "boolean":
|
|
252
|
+
if (typeof value === "string") {
|
|
253
|
+
value = value.toLowerCase();
|
|
254
|
+
if (value === "true") {
|
|
255
|
+
value = true;
|
|
256
|
+
} else if (value === "false") {
|
|
257
|
+
value = false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (typeof value !== "boolean") {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`Environment variable ${key} must be a boolean (true/false)`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
value = Boolean(value);
|
|
266
|
+
break;
|
|
267
|
+
case "date":
|
|
268
|
+
const date = new Date(value);
|
|
269
|
+
if (isNaN(date.getTime())) {
|
|
270
|
+
throw new Error(`Environment variable ${key} must be a valid date`);
|
|
271
|
+
}
|
|
272
|
+
value = date;
|
|
273
|
+
break;
|
|
274
|
+
case "url":
|
|
275
|
+
try {
|
|
276
|
+
new URL(String(value));
|
|
277
|
+
} catch {
|
|
278
|
+
throw new Error("Must be a valid URL");
|
|
279
|
+
}
|
|
280
|
+
break;
|
|
281
|
+
case "email":
|
|
282
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))) {
|
|
283
|
+
throw new Error("Must be a valid email");
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
case "ip":
|
|
287
|
+
if (!/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(String(value))) {
|
|
288
|
+
throw new Error("Must be a valid IP address");
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
case "port":
|
|
292
|
+
const port = Number(value);
|
|
293
|
+
if (isNaN(port) || !Number.isInteger(port)) throw new Error("Must be an integer");
|
|
294
|
+
if (port < 1 || port > 65535) {
|
|
295
|
+
throw new Error("Must be between 1 and 65535");
|
|
296
|
+
}
|
|
297
|
+
value = port;
|
|
298
|
+
break;
|
|
299
|
+
case "json":
|
|
300
|
+
const maybeJson = this.tryParseJSON(value);
|
|
301
|
+
if (!maybeJson.ok) {
|
|
302
|
+
throw new Error("Must be valid JSON");
|
|
303
|
+
}
|
|
304
|
+
value = maybeJson.value;
|
|
305
|
+
break;
|
|
306
|
+
case "array":
|
|
307
|
+
if (!Array.isArray(value)) {
|
|
308
|
+
const parsed = this.tryParseJSON(value);
|
|
309
|
+
if (!parsed.ok || !Array.isArray(parsed.value)) {
|
|
310
|
+
throw new Error("Must be a valid array or JSON array string");
|
|
311
|
+
}
|
|
312
|
+
value = parsed.value;
|
|
313
|
+
}
|
|
314
|
+
if (effectiveRule.items) {
|
|
315
|
+
value = value.map((item, i) => {
|
|
316
|
+
try {
|
|
317
|
+
return this.validateKey(
|
|
318
|
+
`${key}[${i}]`,
|
|
319
|
+
effectiveRule.items,
|
|
320
|
+
item
|
|
321
|
+
);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
throw new Error(`Array item '${i}':${error.message}`);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
case "object":
|
|
329
|
+
if (typeof value === "string") {
|
|
330
|
+
const parsed = this.tryParseJSON(value);
|
|
331
|
+
if (!parsed.ok || typeof parsed.value !== "object" || Array.isArray(parsed.value)) {
|
|
332
|
+
throw new Error("Must be a valid object or JSON string");
|
|
333
|
+
}
|
|
334
|
+
value = parsed.value;
|
|
335
|
+
}
|
|
336
|
+
if (effectiveRule.properties) {
|
|
337
|
+
const obj = {};
|
|
338
|
+
for (const prop in effectiveRule.properties) {
|
|
339
|
+
if (!Object.prototype.hasOwnProperty.call(effectiveRule.properties, prop)) continue;
|
|
340
|
+
const propRule = effectiveRule.properties[prop];
|
|
341
|
+
try {
|
|
342
|
+
obj[prop] = this.validateKey(
|
|
343
|
+
`${key}.${prop}`,
|
|
344
|
+
propRule,
|
|
345
|
+
value[prop]
|
|
346
|
+
);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
throw new Error(`Property '${prop}':${error.message}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
value = obj;
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
if (effectiveRule.enum && !effectiveRule.enum.includes(value)) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
`Environment variable ${key} must be one of ${effectiveRule.enum.join(
|
|
358
|
+
", "
|
|
359
|
+
)}`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (effectiveRule.regex && !effectiveRule.regex.test(String(value))) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
effectiveRule.regexError || `Environment variable ${key} must match ${effectiveRule.regex}`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (effectiveRule.validate && !effectiveRule.validate(value)) {
|
|
368
|
+
throw new Error(effectiveRule.error || "Custom validation failed");
|
|
369
|
+
}
|
|
370
|
+
return value;
|
|
371
|
+
}
|
|
372
|
+
getEffectiveRule(key, rule) {
|
|
373
|
+
const envName = process.env.NODE_ENV || "development";
|
|
374
|
+
const envRule = rule.env?.[envName] || {};
|
|
375
|
+
return { ...rule, ...envRule };
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// src/schema.ts
|
|
380
|
+
function defineSchema(schema) {
|
|
381
|
+
return schema;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/utils.ts
|
|
385
|
+
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
386
|
+
function loadEnv(schema, options) {
|
|
387
|
+
const fileEnv = import_dotenv.default.config({ debug: false, path: options?.path }).parsed || {};
|
|
388
|
+
const env = { ...process.env, ...fileEnv };
|
|
389
|
+
const validator = new EnvValidator(schema, options);
|
|
390
|
+
return validator.validate(env);
|
|
391
|
+
}
|
|
392
|
+
function createEnvProxy(validatedEnv) {
|
|
393
|
+
return new Proxy(validatedEnv, {
|
|
394
|
+
get(target, prop) {
|
|
395
|
+
if (typeof prop === "string" && !(prop in target)) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
`Environment variable ${prop} is not validated`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return target[prop];
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/compose.ts
|
|
406
|
+
function composeSchema(...schemas) {
|
|
407
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
408
|
+
for (const schema of schemas) {
|
|
409
|
+
for (const key of Object.keys(schema)) {
|
|
410
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
411
|
+
result[key] = schema[key];
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/index.ts
|
|
418
|
+
var import_dotenv2 = __toESM(require("dotenv"), 1);
|
|
419
|
+
function validateEnv(schema, options) {
|
|
420
|
+
const fileEnv = import_dotenv2.default.config({ debug: false, path: options?.path }).parsed || {};
|
|
421
|
+
const env = { ...process.env, ...fileEnv };
|
|
422
|
+
const validator = new EnvValidator(schema, options);
|
|
423
|
+
return validator.validate(env);
|
|
424
|
+
}
|
|
425
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
426
|
+
0 && (module.exports = {
|
|
427
|
+
AggregateError,
|
|
428
|
+
EnvAggregateError,
|
|
429
|
+
EnvValidationError,
|
|
430
|
+
EnvValidator,
|
|
431
|
+
composeSchema,
|
|
432
|
+
createEnvProxy,
|
|
433
|
+
defineSchema,
|
|
434
|
+
loadEnv,
|
|
435
|
+
validateEnv
|
|
436
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,8 @@ export { defineSchema, EnvAggregateError,
|
|
|
8
8
|
/** @deprecated Use `EnvAggregateError` instead. */
|
|
9
9
|
AggregateError, EnvValidationError, EnvValidator, loadEnv, createEnvProxy, composeSchema, };
|
|
10
10
|
export function validateEnv(schema, options) {
|
|
11
|
-
const
|
|
11
|
+
const fileEnv = dotenv.config({ debug: false, path: options?.path }).parsed || {};
|
|
12
|
+
const env = { ...process.env, ...fileEnv };
|
|
12
13
|
const validator = new EnvValidator(schema, options);
|
|
13
14
|
return validator.validate(env);
|
|
14
15
|
}
|
package/dist/utils.js
CHANGED
|
@@ -14,7 +14,8 @@ import { EnvValidator } from "./validator.js";
|
|
|
14
14
|
* @returns {InferEnv<S>} The validated environment variables.
|
|
15
15
|
*/
|
|
16
16
|
export function loadEnv(schema, options) {
|
|
17
|
-
const
|
|
17
|
+
const fileEnv = dotenv.config({ debug: false, path: options?.path }).parsed || {};
|
|
18
|
+
const env = { ...process.env, ...fileEnv };
|
|
18
19
|
const validator = new EnvValidator(schema, options);
|
|
19
20
|
return validator.validate(env);
|
|
20
21
|
}
|
package/dist/vite-plugin.js
CHANGED
|
@@ -71,7 +71,7 @@ function generateDtsContent(filteredKeys, schemaAbsPath, dtsAbsPath) {
|
|
|
71
71
|
"// ──────────────────────────────────────────────────────────────",
|
|
72
72
|
"",
|
|
73
73
|
'declare module "dotenv-gad/client" {',
|
|
74
|
-
` interface DotenvGadEnv extends ${envType} {}`,
|
|
74
|
+
` export interface DotenvGadEnv extends ${envType} {}`,
|
|
75
75
|
" export const env: DotenvGadEnv;",
|
|
76
76
|
" export default env;",
|
|
77
77
|
"}",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotenv-gad",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/types/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"dotenv-gad": "./dist/cli/index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsc -p tsconfig.build.json",
|
|
11
|
+
"build": "tsc -p tsconfig.build.json && tsup",
|
|
12
12
|
"test": "vitest run",
|
|
13
13
|
"test:watch": "vitest",
|
|
14
14
|
"test:coverage": "vitest run --coverage",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"exports": {
|
|
41
41
|
".": {
|
|
42
42
|
"types": "./dist/types/index.d.ts",
|
|
43
|
-
"import": "./dist/index.js"
|
|
43
|
+
"import": "./dist/index.js",
|
|
44
|
+
"require": "./dist/index.cjs"
|
|
44
45
|
},
|
|
45
46
|
"./vite": {
|
|
46
47
|
"types": "./dist/types/vite-plugin.d.ts",
|
|
@@ -48,12 +49,14 @@
|
|
|
48
49
|
},
|
|
49
50
|
"./client": {
|
|
50
51
|
"types": "./dist/types/client.d.ts",
|
|
51
|
-
"import": "./dist/client.js"
|
|
52
|
+
"import": "./dist/client.js",
|
|
53
|
+
"require": "./dist/client.cjs"
|
|
52
54
|
},
|
|
53
55
|
"./package.json": "./package.json"
|
|
54
56
|
},
|
|
55
57
|
"files": [
|
|
56
58
|
"dist/**/*.js",
|
|
59
|
+
"dist/**/*.cjs",
|
|
57
60
|
"dist/**/*.d.ts",
|
|
58
61
|
"README.md",
|
|
59
62
|
"LICENSE"
|
|
@@ -78,6 +81,7 @@
|
|
|
78
81
|
"globals": "^15.14.0",
|
|
79
82
|
"husky": "^9.1.7",
|
|
80
83
|
"prettier": "^3.4.2",
|
|
84
|
+
"tsup": "^8.5.1",
|
|
81
85
|
"typescript": "^5.7.0",
|
|
82
86
|
"typescript-eslint": "^8.19.1",
|
|
83
87
|
"vite": "^6.0.0",
|