env-health 0.1.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 +309 -0
- package/dist/index.cjs +397 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +186 -0
- package/dist/index.d.ts +186 -0
- package/dist/index.js +387 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# env-health
|
|
2
|
+
|
|
3
|
+
Tiny runtime validator for `process.env` with great error messages and comprehensive utilities.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Type-safe** - Full TypeScript support with inference
|
|
8
|
+
- ✅ **Great error messages** - Clear, actionable error messages with example `.env` files
|
|
9
|
+
- ✅ **Multiple type parsers** - Supports `string`, `int`, `float`, `bool`, `url`, `json`, and `enum`
|
|
10
|
+
- ✅ **Comprehensive utilities** - Helper functions for common env var operations
|
|
11
|
+
- ✅ **Zero dependencies** - Lightweight and fast
|
|
12
|
+
- ✅ **Flexible** - Works with any environment object, not just `process.env`
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm i env-health
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { envHealth } from "env-health";
|
|
24
|
+
|
|
25
|
+
const env = envHealth({
|
|
26
|
+
PORT: "int",
|
|
27
|
+
DEBUG: "bool",
|
|
28
|
+
NODE_ENV: ["dev", "prod", "test"] as const,
|
|
29
|
+
DATABASE_URL: "url",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log(env.PORT); // number: 3000
|
|
33
|
+
console.log(env.DEBUG); // boolean: true
|
|
34
|
+
console.log(env.NODE_ENV); // "dev" | "prod" | "test"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API Reference
|
|
38
|
+
|
|
39
|
+
### `envHealth(schema, options?)`
|
|
40
|
+
|
|
41
|
+
The main validation function. Validates and parses environment variables according to a schema.
|
|
42
|
+
|
|
43
|
+
#### Schema Types
|
|
44
|
+
|
|
45
|
+
**Primitive Types:**
|
|
46
|
+
|
|
47
|
+
- `"string"` - String value (default)
|
|
48
|
+
- `"int"` - Integer number
|
|
49
|
+
- `"float"` - Floating point number
|
|
50
|
+
- `"bool"` - Boolean (accepts: `true/false`, `1/0`, `yes/no`, `y/n`, `on/off`)
|
|
51
|
+
- `"url"` - Valid URL string
|
|
52
|
+
- `"json"` - Valid JSON (parsed to `unknown`)
|
|
53
|
+
|
|
54
|
+
**Enum Types:**
|
|
55
|
+
|
|
56
|
+
- Array of strings: `["dev", "prod"] as const`
|
|
57
|
+
- Object with `type: "enum"` and `values` array
|
|
58
|
+
|
|
59
|
+
**Detailed Spec:**
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
{
|
|
63
|
+
type: "int" | "float" | "bool" | "string" | "url" | "json",
|
|
64
|
+
optional?: true,
|
|
65
|
+
default?: string | number | boolean,
|
|
66
|
+
example?: string
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Examples
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { envHealth } from "env-health";
|
|
74
|
+
|
|
75
|
+
// Simple types
|
|
76
|
+
const env = envHealth({
|
|
77
|
+
PORT: "int",
|
|
78
|
+
DEBUG: "bool",
|
|
79
|
+
API_URL: "url",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// With defaults
|
|
83
|
+
const env = envHealth({
|
|
84
|
+
PORT: { type: "int", default: 3000 },
|
|
85
|
+
TIMEOUT: { type: "int", default: 5000 },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Optional variables
|
|
89
|
+
const env = envHealth({
|
|
90
|
+
REQUIRED: "string",
|
|
91
|
+
OPTIONAL: { type: "string", optional: true },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Enums
|
|
95
|
+
const env = envHealth({
|
|
96
|
+
NODE_ENV: ["dev", "prod", "test"] as const,
|
|
97
|
+
LOG_LEVEL: {
|
|
98
|
+
type: "enum",
|
|
99
|
+
values: ["debug", "info", "warn", "error"],
|
|
100
|
+
default: "info",
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Custom environment object
|
|
105
|
+
const env = envHealth(
|
|
106
|
+
{
|
|
107
|
+
PORT: "int",
|
|
108
|
+
DEBUG: "bool",
|
|
109
|
+
},
|
|
110
|
+
{ env: { PORT: "3000", DEBUG: "true" } },
|
|
111
|
+
);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Error Messages
|
|
115
|
+
|
|
116
|
+
When validation fails, `envHealth` throws an `EnvHealthError` with:
|
|
117
|
+
|
|
118
|
+
- List of missing required variables
|
|
119
|
+
- List of invalid variables with expected vs received values
|
|
120
|
+
- Example `.env` file showing the correct format
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { envHealth, EnvHealthError } from "env-health";
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const env = envHealth({
|
|
127
|
+
DATABASE_URL: "url",
|
|
128
|
+
PORT: "int",
|
|
129
|
+
});
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (err instanceof EnvHealthError) {
|
|
132
|
+
console.error(err.message);
|
|
133
|
+
// Environment validation failed:
|
|
134
|
+
//
|
|
135
|
+
// Missing required variables:
|
|
136
|
+
// - DATABASE_URL (expected: url)
|
|
137
|
+
//
|
|
138
|
+
// Example .env:
|
|
139
|
+
// ------------
|
|
140
|
+
// DATABASE_URL=postgres://user:pass@localhost:5432/db
|
|
141
|
+
// PORT=3000
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### `requireEnv(key, env?)`
|
|
147
|
+
|
|
148
|
+
Require an environment variable. Throws if missing.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { requireEnv } from "env-health";
|
|
152
|
+
|
|
153
|
+
const apiKey = requireEnv("API_KEY");
|
|
154
|
+
// Throws if API_KEY is missing or empty
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### `getEnv(key, options?)`
|
|
158
|
+
|
|
159
|
+
Get an environment variable with optional default and type conversion.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { getEnv } from "env-health";
|
|
163
|
+
|
|
164
|
+
// String (default)
|
|
165
|
+
const apiKey = getEnv("API_KEY", { default: "default-key" });
|
|
166
|
+
|
|
167
|
+
// Integer
|
|
168
|
+
const port = getEnv("PORT", { type: "int", default: 3000 });
|
|
169
|
+
|
|
170
|
+
// Float
|
|
171
|
+
const ratio = getEnv("RATIO", { type: "float", default: 0.5 });
|
|
172
|
+
|
|
173
|
+
// Boolean
|
|
174
|
+
const debug = getEnv("DEBUG", { type: "bool", default: false });
|
|
175
|
+
|
|
176
|
+
// Custom environment
|
|
177
|
+
const value = getEnv("KEY", { env: customEnv, default: "fallback" });
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### `envExists(keys, env?)`
|
|
181
|
+
|
|
182
|
+
Check if one or more environment variables exist.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { envExists } from "env-health";
|
|
186
|
+
|
|
187
|
+
// Single key
|
|
188
|
+
if (envExists("API_KEY")) {
|
|
189
|
+
// API_KEY exists
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Multiple keys (all must exist)
|
|
193
|
+
if (envExists(["DB_HOST", "DB_PORT", "DB_NAME"])) {
|
|
194
|
+
// All database vars exist
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Custom environment
|
|
198
|
+
if (envExists("KEY", customEnv)) {
|
|
199
|
+
// ...
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### `envPrefix(prefix, options?)`
|
|
204
|
+
|
|
205
|
+
Get all environment variables with a specific prefix.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import { envPrefix } from "env-health";
|
|
209
|
+
|
|
210
|
+
// Get all DB_* vars
|
|
211
|
+
const dbVars = envPrefix("DB_");
|
|
212
|
+
// { DB_HOST: "localhost", DB_PORT: "5432", DB_NAME: "mydb" }
|
|
213
|
+
|
|
214
|
+
// Strip prefix from keys
|
|
215
|
+
const dbConfig = envPrefix("DB_", { stripPrefix: true });
|
|
216
|
+
// { HOST: "localhost", PORT: "5432", NAME: "mydb" }
|
|
217
|
+
|
|
218
|
+
// Custom environment
|
|
219
|
+
const vars = envPrefix("PREFIX_", { env: customEnv });
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### `mergeEnv(...sources)`
|
|
223
|
+
|
|
224
|
+
Merge multiple environment sources. Later sources override earlier ones.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { mergeEnv } from "env-health";
|
|
228
|
+
|
|
229
|
+
const merged = mergeEnv(
|
|
230
|
+
{ PORT: "3000" }, // defaults
|
|
231
|
+
process.env, // system env
|
|
232
|
+
{ DEBUG: "true" }, // overrides
|
|
233
|
+
);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### `envToObject(keys, options?)`
|
|
237
|
+
|
|
238
|
+
Convert environment variables to a typed object, optionally with prefix handling.
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import { envToObject } from "env-health";
|
|
242
|
+
|
|
243
|
+
// Basic usage
|
|
244
|
+
const config = envToObject(["HOST", "PORT", "NAME"], {
|
|
245
|
+
env: {
|
|
246
|
+
HOST: "localhost",
|
|
247
|
+
PORT: "5432",
|
|
248
|
+
NAME: "mydb",
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
// { HOST: "localhost", PORT: "5432", NAME: "mydb" }
|
|
252
|
+
|
|
253
|
+
// With prefix
|
|
254
|
+
const dbConfig = envToObject(["HOST", "PORT"], {
|
|
255
|
+
prefix: "DB_",
|
|
256
|
+
env: {
|
|
257
|
+
DB_HOST: "localhost",
|
|
258
|
+
DB_PORT: "5432",
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
// { HOST: "localhost", PORT: "5432" }
|
|
262
|
+
|
|
263
|
+
// With required keys
|
|
264
|
+
const config = envToObject(["HOST", "PORT"], {
|
|
265
|
+
prefix: "DB_",
|
|
266
|
+
required: ["HOST", "PORT"],
|
|
267
|
+
env: { DB_HOST: "localhost" },
|
|
268
|
+
});
|
|
269
|
+
// Throws: Missing required environment variable: DB_PORT
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### `loadEnvFile(content)`
|
|
273
|
+
|
|
274
|
+
Load and parse a `.env` file content string.
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { loadEnvFile } from "env-health";
|
|
278
|
+
import { readFileSync } from "fs";
|
|
279
|
+
|
|
280
|
+
const content = readFileSync(".env", "utf-8");
|
|
281
|
+
const env = loadEnvFile(content);
|
|
282
|
+
|
|
283
|
+
// Supports:
|
|
284
|
+
// - KEY=value
|
|
285
|
+
// - Comments (#)
|
|
286
|
+
// - Quoted values ("value" or 'value')
|
|
287
|
+
// - Empty lines
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## TypeScript
|
|
291
|
+
|
|
292
|
+
Full TypeScript support with type inference:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { envHealth } from "env-health";
|
|
296
|
+
|
|
297
|
+
const env = envHealth({
|
|
298
|
+
PORT: "int",
|
|
299
|
+
DEBUG: "bool",
|
|
300
|
+
NODE_ENV: ["dev", "prod"] as const,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Type: { PORT: number; DEBUG: boolean; NODE_ENV: "dev" | "prod" }
|
|
304
|
+
type EnvType = typeof env;
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## License
|
|
308
|
+
|
|
309
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/error.ts
|
|
4
|
+
var EnvHealthError = class extends Error {
|
|
5
|
+
issues;
|
|
6
|
+
exampleEnv;
|
|
7
|
+
constructor(message, issues, exampleEnv) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "EnvHealthError";
|
|
10
|
+
this.issues = issues;
|
|
11
|
+
this.exampleEnv = exampleEnv;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/parse.ts
|
|
16
|
+
var URL_RE = /^https?:\/\/.+/i;
|
|
17
|
+
function parseIntStrict(raw) {
|
|
18
|
+
if (!/^-?\d+$/.test(raw.trim())) return null;
|
|
19
|
+
const n = Number(raw);
|
|
20
|
+
return Number.isFinite(n) ? n : null;
|
|
21
|
+
}
|
|
22
|
+
function parseFloatStrict(raw) {
|
|
23
|
+
const s = raw.trim();
|
|
24
|
+
if (s.length === 0) return null;
|
|
25
|
+
const n = Number(s);
|
|
26
|
+
return Number.isFinite(n) ? n : null;
|
|
27
|
+
}
|
|
28
|
+
function parseBoolStrict(raw) {
|
|
29
|
+
const v = raw.trim().toLowerCase();
|
|
30
|
+
if (["true", "1", "yes", "y", "on"].includes(v)) return true;
|
|
31
|
+
if (["false", "0", "no", "n", "off"].includes(v)) return false;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function parseUrlStrict(raw) {
|
|
35
|
+
const s = raw.trim();
|
|
36
|
+
if (!URL_RE.test(s)) return null;
|
|
37
|
+
try {
|
|
38
|
+
new URL(s);
|
|
39
|
+
return s;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function parseJsonStrict(raw) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(raw);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/utils.ts
|
|
53
|
+
function requireEnv(key, env = process.env) {
|
|
54
|
+
const value = env[key];
|
|
55
|
+
if (value == null || value === "") {
|
|
56
|
+
throw new Error(`Missing required environment variable: ${key}`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function getEnv(key, options) {
|
|
61
|
+
const env = options?.env ?? process.env;
|
|
62
|
+
const raw = env[key];
|
|
63
|
+
const defaultValue = options?.default;
|
|
64
|
+
if (raw == null || raw === "") {
|
|
65
|
+
if (defaultValue !== void 0) {
|
|
66
|
+
return defaultValue;
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`Missing environment variable: ${key}`);
|
|
69
|
+
}
|
|
70
|
+
const type = options?.type ?? "string";
|
|
71
|
+
if (type === "string") {
|
|
72
|
+
return raw;
|
|
73
|
+
}
|
|
74
|
+
if (type === "int") {
|
|
75
|
+
const parsed = parseIntStrict(raw);
|
|
76
|
+
if (parsed === null) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Invalid integer value for ${key}: "${raw}". Use a valid integer.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return parsed;
|
|
82
|
+
}
|
|
83
|
+
if (type === "float") {
|
|
84
|
+
const parsed = parseFloatStrict(raw);
|
|
85
|
+
if (parsed === null) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Invalid float value for ${key}: "${raw}". Use a valid number.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
if (type === "bool") {
|
|
93
|
+
const parsed = parseBoolStrict(raw);
|
|
94
|
+
if (parsed === null) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Invalid boolean value for ${key}: "${raw}". Use true/false, 1/0, yes/no, y/n, or on/off.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
return raw;
|
|
102
|
+
}
|
|
103
|
+
function envExists(keys, env = process.env) {
|
|
104
|
+
const keysArray = Array.isArray(keys) ? keys : [keys];
|
|
105
|
+
return keysArray.every((key) => {
|
|
106
|
+
const value = env[key];
|
|
107
|
+
return value != null && value !== "";
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function envPrefix(prefix, options) {
|
|
111
|
+
const env = options?.env ?? process.env;
|
|
112
|
+
const result = {};
|
|
113
|
+
const stripPrefix = options?.stripPrefix ?? false;
|
|
114
|
+
for (const [key, value] of Object.entries(env)) {
|
|
115
|
+
if (value != null && typeof value === "string" && key.startsWith(prefix) && value !== "") {
|
|
116
|
+
const newKey = stripPrefix ? key.slice(prefix.length) : key;
|
|
117
|
+
result[newKey] = value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function mergeEnv(...sources) {
|
|
123
|
+
const result = {};
|
|
124
|
+
for (const source of sources) {
|
|
125
|
+
if (source) {
|
|
126
|
+
Object.assign(result, source);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
function envToObject(keys, options) {
|
|
132
|
+
const env = options?.env ?? process.env;
|
|
133
|
+
const prefix = options?.prefix ?? "";
|
|
134
|
+
const stripPrefix = options?.stripPrefix ?? false;
|
|
135
|
+
const required = new Set(options?.required ?? []);
|
|
136
|
+
const result = {};
|
|
137
|
+
for (const key of keys) {
|
|
138
|
+
const envKey = prefix + key;
|
|
139
|
+
const value = env[envKey];
|
|
140
|
+
if (value == null || value === "") {
|
|
141
|
+
if (required.has(key)) {
|
|
142
|
+
throw new Error(`Missing required environment variable: ${envKey}`);
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const resultKey = stripPrefix ? key : envKey;
|
|
147
|
+
result[resultKey] = value;
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
function loadEnvFile(content) {
|
|
152
|
+
const result = {};
|
|
153
|
+
const lines = content.split(/\r?\n/);
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
const trimmed = line.trim();
|
|
156
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const equalIndex = trimmed.indexOf("=");
|
|
160
|
+
if (equalIndex === -1) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const key = trimmed.slice(0, equalIndex).trim();
|
|
164
|
+
let value = trimmed.slice(equalIndex + 1).trim();
|
|
165
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
166
|
+
value = value.slice(1, -1);
|
|
167
|
+
}
|
|
168
|
+
if (!key) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
result[key] = value;
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/index.ts
|
|
177
|
+
function isArraySpec(spec) {
|
|
178
|
+
return Array.isArray(spec);
|
|
179
|
+
}
|
|
180
|
+
function isStringSpec(spec) {
|
|
181
|
+
return typeof spec === "string";
|
|
182
|
+
}
|
|
183
|
+
function isEnumDetailedSpec(spec) {
|
|
184
|
+
return spec.type === "enum";
|
|
185
|
+
}
|
|
186
|
+
function isPrimitiveDetailedSpec(spec) {
|
|
187
|
+
return spec.type !== "enum";
|
|
188
|
+
}
|
|
189
|
+
function expectedLabel(spec) {
|
|
190
|
+
if (isArraySpec(spec)) {
|
|
191
|
+
return `one of (${spec.join(", ")})`;
|
|
192
|
+
}
|
|
193
|
+
if (isStringSpec(spec)) {
|
|
194
|
+
return spec;
|
|
195
|
+
}
|
|
196
|
+
if (isEnumDetailedSpec(spec)) {
|
|
197
|
+
return `one of (${spec.values.join(", ")})`;
|
|
198
|
+
}
|
|
199
|
+
if (isPrimitiveDetailedSpec(spec)) {
|
|
200
|
+
return spec.type;
|
|
201
|
+
}
|
|
202
|
+
return "unknown";
|
|
203
|
+
}
|
|
204
|
+
function normalizeSpec(spec) {
|
|
205
|
+
if (isArraySpec(spec)) {
|
|
206
|
+
return { kind: "enum", values: spec, optional: false };
|
|
207
|
+
}
|
|
208
|
+
if (isStringSpec(spec)) {
|
|
209
|
+
return { kind: "primitive", type: spec, optional: false };
|
|
210
|
+
}
|
|
211
|
+
if (isEnumDetailedSpec(spec)) {
|
|
212
|
+
return {
|
|
213
|
+
kind: "enum",
|
|
214
|
+
values: spec.values,
|
|
215
|
+
optional: spec.optional === true,
|
|
216
|
+
defaultValue: spec.default,
|
|
217
|
+
example: spec.example ?? void 0
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (isPrimitiveDetailedSpec(spec)) {
|
|
221
|
+
return {
|
|
222
|
+
kind: "primitive",
|
|
223
|
+
type: spec.type,
|
|
224
|
+
optional: spec.optional === true,
|
|
225
|
+
defaultValue: spec.default,
|
|
226
|
+
example: spec.example ?? void 0
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return { kind: "primitive", type: "string", optional: false };
|
|
230
|
+
}
|
|
231
|
+
var EXAMPLE_CACHE = /* @__PURE__ */ new Map();
|
|
232
|
+
function suggestExample(key, spec) {
|
|
233
|
+
const cacheKey = `${key}:${JSON.stringify(spec)}`;
|
|
234
|
+
const cached = EXAMPLE_CACHE.get(cacheKey);
|
|
235
|
+
if (cached) return cached;
|
|
236
|
+
const n = normalizeSpec(spec);
|
|
237
|
+
let example;
|
|
238
|
+
if (n.example) {
|
|
239
|
+
example = n.example;
|
|
240
|
+
} else if (n.kind === "enum" && n.values && n.values.length > 0) {
|
|
241
|
+
example = String(n.values[0]);
|
|
242
|
+
} else {
|
|
243
|
+
const upperKey = key.toUpperCase();
|
|
244
|
+
switch (n.type) {
|
|
245
|
+
case "int":
|
|
246
|
+
example = "3000";
|
|
247
|
+
break;
|
|
248
|
+
case "float":
|
|
249
|
+
example = "0.5";
|
|
250
|
+
break;
|
|
251
|
+
case "bool":
|
|
252
|
+
example = "true";
|
|
253
|
+
break;
|
|
254
|
+
case "url":
|
|
255
|
+
example = upperKey.includes("DATABASE") && upperKey.includes("URL") ? "postgres://user:pass@localhost:5432/db" : "https://example.com";
|
|
256
|
+
break;
|
|
257
|
+
case "json":
|
|
258
|
+
example = '{"key":"value"}';
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
example = "your_value_here";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
EXAMPLE_CACHE.set(cacheKey, example);
|
|
265
|
+
return example;
|
|
266
|
+
}
|
|
267
|
+
function parseByType(type, raw) {
|
|
268
|
+
switch (type) {
|
|
269
|
+
case "string":
|
|
270
|
+
return { ok: true, value: raw };
|
|
271
|
+
case "int": {
|
|
272
|
+
const n = parseIntStrict(raw);
|
|
273
|
+
return n === null ? { ok: false } : { ok: true, value: n };
|
|
274
|
+
}
|
|
275
|
+
case "float": {
|
|
276
|
+
const n = parseFloatStrict(raw);
|
|
277
|
+
return n === null ? { ok: false } : { ok: true, value: n };
|
|
278
|
+
}
|
|
279
|
+
case "bool": {
|
|
280
|
+
const b = parseBoolStrict(raw);
|
|
281
|
+
return b === null ? { ok: false } : { ok: true, value: b };
|
|
282
|
+
}
|
|
283
|
+
case "url": {
|
|
284
|
+
const u = parseUrlStrict(raw);
|
|
285
|
+
return u === null ? { ok: false } : { ok: true, value: u };
|
|
286
|
+
}
|
|
287
|
+
case "json": {
|
|
288
|
+
const j = parseJsonStrict(raw);
|
|
289
|
+
return j === null ? { ok: false } : { ok: true, value: j };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function buildExampleEnv(schema, opts) {
|
|
294
|
+
const lines = [];
|
|
295
|
+
for (const [key, spec] of Object.entries(schema)) {
|
|
296
|
+
const n = normalizeSpec(spec);
|
|
297
|
+
const example = opts.blankExampleValues ? "" : suggestExample(key, spec);
|
|
298
|
+
const optionalSuffix = n.optional ? " # optional" : "";
|
|
299
|
+
lines.push(`${key}=${example}${optionalSuffix}`.trimEnd());
|
|
300
|
+
}
|
|
301
|
+
return lines.join("\n");
|
|
302
|
+
}
|
|
303
|
+
function envHealth(schema, options = {}) {
|
|
304
|
+
const env = options.env ?? process.env;
|
|
305
|
+
const issues = [];
|
|
306
|
+
const out = {};
|
|
307
|
+
for (const [key, spec] of Object.entries(schema)) {
|
|
308
|
+
const n = normalizeSpec(spec);
|
|
309
|
+
const expected = expectedLabel(spec);
|
|
310
|
+
const raw = env[key];
|
|
311
|
+
if (raw == null || raw === "") {
|
|
312
|
+
if (n.defaultValue !== void 0) {
|
|
313
|
+
out[key] = n.defaultValue;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (n.optional) {
|
|
317
|
+
out[key] = void 0;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
issues.push({ kind: "missing", key, expected });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (n.kind === "enum") {
|
|
324
|
+
const values = n.values;
|
|
325
|
+
if (values && values.includes(raw)) {
|
|
326
|
+
out[key] = raw;
|
|
327
|
+
} else {
|
|
328
|
+
issues.push({ kind: "invalid", key, expected, received: raw });
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const type = n.type ?? "string";
|
|
333
|
+
const parsed = parseByType(type, raw);
|
|
334
|
+
if (parsed.ok) {
|
|
335
|
+
out[key] = parsed.value;
|
|
336
|
+
} else {
|
|
337
|
+
issues.push({ kind: "invalid", key, expected, received: raw });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (issues.length > 0) {
|
|
341
|
+
const exampleEnv = buildExampleEnv(schema, options);
|
|
342
|
+
const header = options.header ? `${options.header}
|
|
343
|
+
` : "";
|
|
344
|
+
const lines = [
|
|
345
|
+
header ? `${header}Environment validation failed:
|
|
346
|
+
` : "Environment validation failed:\n"
|
|
347
|
+
];
|
|
348
|
+
const missing = [];
|
|
349
|
+
const invalid = [];
|
|
350
|
+
for (let i = 0; i < issues.length; i++) {
|
|
351
|
+
const issue = issues[i];
|
|
352
|
+
if (!issue) continue;
|
|
353
|
+
if (issue.kind === "missing") {
|
|
354
|
+
missing.push(issue);
|
|
355
|
+
} else {
|
|
356
|
+
invalid.push(issue);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (missing.length > 0) {
|
|
360
|
+
lines.push("Missing required variables:");
|
|
361
|
+
for (let i = 0; i < missing.length; i++) {
|
|
362
|
+
const m = missing[i];
|
|
363
|
+
if (m) {
|
|
364
|
+
lines.push(` - ${m.key} (expected: ${m.expected})`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
lines.push("");
|
|
368
|
+
}
|
|
369
|
+
if (invalid.length > 0) {
|
|
370
|
+
lines.push("Invalid variables:");
|
|
371
|
+
for (let i = 0; i < invalid.length; i++) {
|
|
372
|
+
const v = invalid[i];
|
|
373
|
+
if (v) {
|
|
374
|
+
lines.push(` - ${v.key}="${v.received}" (expected: ${v.expected})`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
lines.push("");
|
|
378
|
+
}
|
|
379
|
+
lines.push("Example .env:");
|
|
380
|
+
lines.push("------------");
|
|
381
|
+
lines.push(exampleEnv);
|
|
382
|
+
throw new EnvHealthError(lines.join("\n"), issues, exampleEnv);
|
|
383
|
+
}
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
exports.EnvHealthError = EnvHealthError;
|
|
388
|
+
exports.envExists = envExists;
|
|
389
|
+
exports.envHealth = envHealth;
|
|
390
|
+
exports.envPrefix = envPrefix;
|
|
391
|
+
exports.envToObject = envToObject;
|
|
392
|
+
exports.getEnv = getEnv;
|
|
393
|
+
exports.loadEnvFile = loadEnvFile;
|
|
394
|
+
exports.mergeEnv = mergeEnv;
|
|
395
|
+
exports.requireEnv = requireEnv;
|
|
396
|
+
//# sourceMappingURL=index.cjs.map
|
|
397
|
+
//# sourceMappingURL=index.cjs.map
|