env-typed-checker 0.2.0 → 0.2.3

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 CHANGED
@@ -6,19 +6,25 @@ It helps your app fail fast when configuration is wrong:
6
6
 
7
7
  - ❌ Missing required variables
8
8
  - ❌ Wrong types (e.g. `PORT="abc"`)
9
- - ❌ Invalid URLs or JSON
10
- - ❌ Silent mistakes that only crash later
9
+ - ❌ Invalid URLs / emails / JSON
10
+ - ❌ Values not matching allowed enums or regex patterns
11
+ - ✅ Optional values + defaults
12
+ - ✅ CLI checks for CI + .env generation
13
+
11
14
 
12
15
  ---
13
16
 
14
17
  ## ✨ Features
15
18
 
16
- - Simple schema syntax (`"number"`, `"boolean?"`, `"url"`, `"json"`, `"string"`)
17
- - Loads `.env` automatically via `dotenv` (optional)
18
- - Aggregated, friendly error output (shows all issues at once)
19
- - TypeScript types inferred from the schema
20
- - CLI for quick checks (great for CI)
21
- - Minimal dependencies (runtime: `dotenv` only)
19
+ - Simple schema syntax (`"number"`, `"boolean?"`, `"email"` `"url"`, `"json"`, `"string"`)
20
+ - Advanced specs: `enum` and `regex`
21
+ - Optional values with `?` and `optional: true`
22
+ - Defaults (typed + validated)
23
+ - CLI:
24
+ - `check` validate env
25
+ - `generate` → generate/update `.env` from schema (no overwrite by default)
26
+ - Uses `.env` via `dotenv` (optional)
27
+ - Friendly aggregated errors (see everything that’s wrong at once)
22
28
 
23
29
  ---
24
30
 
@@ -28,6 +34,12 @@ It helps your app fail fast when configuration is wrong:
28
34
  npm install env-typed-checker
29
35
  ```
30
36
 
37
+ Or run via npx:
38
+
39
+ ```bash
40
+ npx env-typed-checker --help
41
+ ```
42
+
31
43
  ## 🚀 Quick Start (Code)
32
44
 
33
45
  ```ts
@@ -36,17 +48,18 @@ import { envDoctor } from "env-typed-checker";
36
48
  export const config = envDoctor({
37
49
  PORT: "number",
38
50
  DB_URL: "url",
39
- DEBUG: "boolean?"
51
+ ADMIN_EMAIL: "email",
52
+ DEBUG: "boolean?",
40
53
  });
54
+
41
55
  ```
42
56
 
43
57
  ### What you get
44
58
 
45
- * PORT → number
46
-
47
- * DB_URL → string (validated as URL)
48
-
49
- * DEBUG → boolean | undefined (optional)
59
+ * PORT → `number`
60
+ * DB_URL → `string` (validated as URL)
61
+ * ADMIN_EMAIL`string` (validated as email)
62
+ * DEBUG → `boolean` | `undefined` (optional)
50
63
 
51
64
 
52
65
  ### 🧩 Supported Types
@@ -57,6 +70,7 @@ export const config = envDoctor({
57
70
  | **boolean** | Supports `true` / `false`, `1` / `0`, and `yes` / `no` |
58
71
  | **json** | Validates and parses a valid JSON string |
59
72
  | **url** | Validates for a properly formatted URL |
73
+ | **email** | Validates for a properly formatted email |
60
74
 
61
75
 
62
76
  ### Optional Values
@@ -64,8 +78,53 @@ Add ? to make a variable optional:
64
78
  ```ts
65
79
  envDoctor({ DEBUG: "boolean?" });
66
80
  ```
67
- If missing → value will be undefined.
68
81
 
82
+ Or use object-style:
83
+
84
+ ```ts
85
+ envDoctor({
86
+ DEBUG: { type: "boolean", optional: true },
87
+ });
88
+ ```
89
+ Missing optional vars become `undefined` unless you provide a default.
90
+
91
+ ### 🎯 Defaults
92
+ Defaults can be provided in object-style specs.
93
+
94
+ ```ts
95
+ import { envDoctor } from "env-typed-checker";
96
+
97
+ const config = envDoctor({
98
+ PORT: { type: "number", default: 3000 },
99
+ DEBUG: { type: "boolean", default: "false" }, // string is allowed (parsed)
100
+ NODE_ENV: { type: "enum", values: ["dev", "prod"], default: "dev" },
101
+ });
102
+ ```
103
+
104
+ ### Notes:
105
+
106
+ - `number` default: number or string (string will be parsed)
107
+ - `boolean` default: boolean or string (string will be parsed)
108
+ - `url/email` defaults must be strings and must validate
109
+ - `json` defaults can be any JSON-like value
110
+
111
+ ### ✅ Enum and Regex
112
+
113
+ ### Enum
114
+
115
+ ```ts
116
+ const config = envDoctor({
117
+ NODE_ENV: { type: "enum", values: ["dev", "prod"] },
118
+ });
119
+ ```
120
+
121
+ ### Regex
122
+
123
+ ```ts
124
+ const config = envDoctor({
125
+ SLUG: { type: "regex", pattern: "^[a-z0-9-]+$", flags: "i" },
126
+ });
127
+ ```
69
128
 
70
129
  ### ⚙️ Options
71
130
 
@@ -76,6 +135,7 @@ envDoctor(schema, {
76
135
  });
77
136
  ```
78
137
 
138
+
79
139
  ### 🧪 Testing with custom env
80
140
 
81
141
  ```ts
@@ -96,6 +156,7 @@ Given a `.env` like:
96
156
  ```env
97
157
  PORT=abc
98
158
  DB_URL=not-a-url
159
+ NODE_ENV=staging
99
160
  ```
100
161
 
101
162
  ```ts
@@ -104,6 +165,7 @@ import { envDoctor } from "env-typed-checker";
104
165
  envDoctor({
105
166
  PORT: "number",
106
167
  DB_URL: "url"
168
+ NODE_ENV: { type: "enum", values: ["dev", "prod"] },
107
169
  });
108
170
  ```
109
171
 
@@ -113,6 +175,7 @@ envDoctor({
113
175
  ENV validation failed
114
176
  - PORT: expected number, got "abc"
115
177
  - DB_URL: expected url, got "not-a-url"
178
+ - NODE_ENV: must be one of [dev, prod]
116
179
  ```
117
180
  All errors are shown together so you can fix them in one go.
118
181
 
@@ -127,7 +190,10 @@ Validate your environment without writing code — perfect for CI pipelines.
127
190
  {
128
191
  "PORT": "number",
129
192
  "DB_URL": "url",
130
- "DEBUG": "boolean?"
193
+ "ADMIN_EMAIL": "email",
194
+ "DEBUG": "boolean?",
195
+ "NODE_ENV": { "type": "enum", "values": ["dev", "prod"] },
196
+ "SLUG": { "type": "regex", "pattern": "^[a-z0-9-]+$", "flags": "i" }
131
197
  }
132
198
  ```
133
199
 
@@ -137,22 +203,51 @@ Validate your environment without writing code — perfect for CI pipelines.
137
203
  npx env-typed-checker check --schema env.schema.json
138
204
  ```
139
205
 
140
- ### Options
206
+ Useful flags
141
207
 
142
208
  ```bash
143
- # Custom env file
209
+ # Use a specific env file (instead of .env)
144
210
  npx env-typed-checker check --schema env.schema.json --env-file .env.production
145
211
 
146
- # Skip dotenv (use process.env only)
212
+ # Skip dotenv loading and validate only process.env
147
213
  npx env-typed-checker check --schema env.schema.json --no-dotenv
148
214
  ```
149
215
 
150
- ## Exit codes
216
+ ### `generate` — generate or update `.env`
151
217
 
152
- * `0` = OK
218
+ Generate values from schema (writes missing keys; does not overwrite existing values).
153
219
 
154
- * `1` = validation failed
220
+ ```bash
221
+ npx env-typed-checker generate --schema env.schema.json
222
+ ```
223
+ ### By default:
224
+
225
+ - output file: .env
226
+ - mode: update
227
+
228
+ ### Flags
229
+
230
+ ```bash
231
+ # Custom output file
232
+ npx env-typed-checker generate --schema env.schema.json --out .env.example
233
+
234
+ # Create mode: refuse to overwrite an existing file
235
+ npx env-typed-checker generate --schema env.schema.json --out .env --mode=create
236
+
237
+ # Update mode: append only missing keys (safe)
238
+ npx env-typed-checker generate --schema env.schema.json --out .env --mode=update
239
+
240
+ # Do not write defaults (leave blank values)
241
+ npx env-typed-checker generate --schema env.schema.json --no-defaults
242
+
243
+ # Add inline type comments (useful for .env.example)
244
+ npx env-typed-checker generate --schema env.schema.json --comment-types
245
+ ```
155
246
 
247
+ ### Exit codes
248
+
249
+ * `0` = OK
250
+ * `1` = validation failed
156
251
  * `2` = CLI usage / unexpected error
157
252
 
158
253
  ## ✅ CI Example (GitHub Actions)
@@ -163,14 +258,20 @@ Add this to your workflow to fail the build if env is invalid:
163
258
  - name: Validate env
164
259
  run: npx env-typed-checker check --schema env.schema.json
165
260
  ```
166
- (If you use a different env file in CI, pass --env-file.)
261
+ If you use a specific env file in CI:
262
+
263
+ ```yml
264
+ - name: Validate env
265
+ run: npx env-typed-checker check --schema env.schema.json --env-file .env.ci
266
+ ```
167
267
 
168
268
  ## 🛠 Development
169
269
  Clone the repo and install:
170
270
  ```bash
171
271
  npm install
272
+ npm test
172
273
  ```
173
- ### Available Scripts
274
+ ### Common scripts:
174
275
  ```bash
175
276
  npm run build # build package
176
277
  npm run test # run tests
@@ -181,24 +282,10 @@ npm run dev # watch build
181
282
  ### 🤝 Contributing
182
283
  PRs are welcome!
183
284
 
184
- * Add new validators (e.g. `enum`, `regex`, `email`)
185
-
285
+ * Improve docs / examples
286
+ * Add more schema features
186
287
  * Improve CLI output formatting
187
-
188
- * Add .env.example generator
189
-
190
- * Improve docs & examples
191
-
192
- * Please read CONTRIBUTING.md before opening a PR.
193
-
194
- ### 📌 Roadmap
195
- * `.env.example` generator
196
-
197
- * Strict mode: warn on unknown variables
198
-
199
- * More schema features: enums, defaults, min/max
200
-
201
- * Framework helpers (Next.js / Express / etc.)
288
+ * Add integrations / templates
202
289
 
203
290
 
204
291
  # 📝 License
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ "use strict";
2
3
 
3
- const { runCli } = require("../dist/cli.js");
4
+ const { runCli } = require("../dist/cli/index.js");
4
5
 
5
6
  const code = runCli(process.argv.slice(2), console);
6
- process.exitCode = code;
7
+ process.exitCode = typeof code === "number" ? code : 1;
@@ -0,0 +1,262 @@
1
+ // src/core/envDoctor.ts
2
+ import * as dotenv from "dotenv";
3
+
4
+ // src/errors/EnvDoctorError.ts
5
+ var EnvDoctorError = class extends Error {
6
+ constructor(issues) {
7
+ const header = "ENV validation failed";
8
+ const lines = issues.map((i) => `- ${i.key}: ${i.message}`);
9
+ super([header, ...lines].join("\n"));
10
+ this.name = "EnvDoctorError";
11
+ this.issues = issues;
12
+ }
13
+ };
14
+
15
+ // src/validators/primitives.ts
16
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
17
+ function parseByType(type, raw) {
18
+ switch (type) {
19
+ case "string":
20
+ return raw;
21
+ case "number": {
22
+ const n = Number(raw.trim());
23
+ if (!Number.isFinite(n)) throw new Error(`expected number, got "${raw}"`);
24
+ return n;
25
+ }
26
+ case "boolean": {
27
+ const v = raw.trim().toLowerCase();
28
+ if (["true", "1", "yes", "y", "on"].includes(v)) return true;
29
+ if (["false", "0", "no", "n", "off"].includes(v)) return false;
30
+ throw new Error(
31
+ `expected boolean (true/false/1/0/yes/no/on/off), got "${raw}"`
32
+ );
33
+ }
34
+ case "json": {
35
+ try {
36
+ return JSON.parse(raw);
37
+ } catch {
38
+ throw new Error(`expected json, got "${raw}"`);
39
+ }
40
+ }
41
+ case "url": {
42
+ try {
43
+ new URL(raw);
44
+ return raw;
45
+ } catch {
46
+ throw new Error(`expected url, got "${raw}"`);
47
+ }
48
+ }
49
+ case "email": {
50
+ const s = raw.trim();
51
+ if (!EMAIL_RE.test(s)) {
52
+ throw new Error(`expected email, got "${raw}"`);
53
+ }
54
+ return s;
55
+ }
56
+ default: {
57
+ throw new Error(`unsupported primitive type: ${String(type)}`);
58
+ }
59
+ }
60
+ }
61
+ function hasOwn(obj, key) {
62
+ return Object.prototype.hasOwnProperty.call(obj, key);
63
+ }
64
+ function coerceDefault(kind, def) {
65
+ if (def === void 0) return void 0;
66
+ switch (kind) {
67
+ case "string": {
68
+ if (typeof def !== "string")
69
+ throw new Error(`default for string must be a string`);
70
+ return def;
71
+ }
72
+ case "number": {
73
+ if (typeof def === "number") {
74
+ if (!Number.isFinite(def))
75
+ throw new Error(`default for number must be finite`);
76
+ return def;
77
+ }
78
+ if (typeof def === "string") return parseByType("number", def);
79
+ throw new Error(`default for number must be number or string`);
80
+ }
81
+ case "boolean": {
82
+ if (typeof def === "boolean") return def;
83
+ if (typeof def === "string") return parseByType("boolean", def);
84
+ throw new Error(`default for boolean must be boolean or string`);
85
+ }
86
+ case "json": {
87
+ return def;
88
+ }
89
+ case "url": {
90
+ if (typeof def !== "string")
91
+ throw new Error(`default for url must be a string`);
92
+ return parseByType("url", def);
93
+ }
94
+ case "email": {
95
+ if (typeof def !== "string")
96
+ throw new Error(`default for email must be a string`);
97
+ return parseByType("email", def);
98
+ }
99
+ /* c8 ignore next */
100
+ default: {
101
+ throw new Error(`unsupported default kind: ${String(kind)}`);
102
+ }
103
+ }
104
+ }
105
+ function normalizeSpec(schemaValue) {
106
+ if (typeof schemaValue === "string") {
107
+ const optional = schemaValue.endsWith("?");
108
+ const base = optional ? schemaValue.slice(0, -1) : schemaValue;
109
+ const allowed = [
110
+ "string",
111
+ "number",
112
+ "boolean",
113
+ "json",
114
+ "url",
115
+ "email"
116
+ ];
117
+ if (!allowed.includes(base)) {
118
+ throw new Error(
119
+ `Unsupported type "${schemaValue}". Supported: string, number, boolean, json, url, email (optional with ?)`
120
+ );
121
+ }
122
+ return { kind: base, optional };
123
+ }
124
+ if (!schemaValue || typeof schemaValue !== "object" || Array.isArray(schemaValue)) {
125
+ throw new Error("Schema value must be a string or object spec.");
126
+ }
127
+ const t = schemaValue.type;
128
+ const primitiveAllowed = [
129
+ "string",
130
+ "number",
131
+ "boolean",
132
+ "json",
133
+ "url",
134
+ "email"
135
+ ];
136
+ if (primitiveAllowed.includes(t)) {
137
+ const optional = !!schemaValue.optional;
138
+ const defaultValue = hasOwn(schemaValue, "default") ? coerceDefault(t, schemaValue.default) : void 0;
139
+ return { kind: t, optional, defaultValue };
140
+ }
141
+ if (t === "enum") {
142
+ const values = schemaValue.values;
143
+ if (!Array.isArray(values) || values.length === 0 || !values.every((v) => typeof v === "string")) {
144
+ throw new Error(`enum spec requires "values": string[] (non-empty)`);
145
+ }
146
+ const optional = !!schemaValue.optional;
147
+ let defaultValue = void 0;
148
+ if (hasOwn(schemaValue, "default")) {
149
+ const def = schemaValue.default;
150
+ if (typeof def !== "string") {
151
+ throw new Error(`default for enum must be a string`);
152
+ }
153
+ if (!values.includes(def)) {
154
+ throw new Error(
155
+ `default "${def}" must be one of [${values.join(", ")}]`
156
+ );
157
+ }
158
+ defaultValue = def;
159
+ }
160
+ return { kind: "enum", optional, values, defaultValue };
161
+ }
162
+ if (t === "regex") {
163
+ const pattern = schemaValue.pattern;
164
+ const flags = schemaValue.flags;
165
+ if (typeof pattern !== "string" || pattern.length === 0) {
166
+ throw new Error(`regex spec requires "pattern": string`);
167
+ }
168
+ if (flags !== void 0 && typeof flags !== "string") {
169
+ throw new Error(`regex spec "flags" must be a string if provided`);
170
+ }
171
+ let re;
172
+ try {
173
+ re = new RegExp(pattern, flags);
174
+ } catch (e) {
175
+ throw new Error(`invalid regex: ${String(e)}`);
176
+ }
177
+ const display = `/${pattern}/${flags ?? ""}`;
178
+ const optional = !!schemaValue.optional;
179
+ let defaultValue = void 0;
180
+ if (hasOwn(schemaValue, "default")) {
181
+ const def = schemaValue.default;
182
+ if (typeof def !== "string") {
183
+ throw new Error(`default for regex must be a string`);
184
+ }
185
+ if (!re.test(def)) {
186
+ throw new Error(`default "${def}" does not match ${display}`);
187
+ }
188
+ defaultValue = def;
189
+ }
190
+ return { kind: "regex", optional, re, display, defaultValue };
191
+ }
192
+ throw new Error(
193
+ `Unsupported object spec type "${String(t)}". Supported: primitives (string/number/boolean/json/url/email), enum, regex`
194
+ );
195
+ }
196
+
197
+ // src/core/runner.ts
198
+ function validateAndParse(schema, env) {
199
+ const issues = [];
200
+ const out = {};
201
+ for (const [key, schemaValue] of Object.entries(schema)) {
202
+ let spec;
203
+ try {
204
+ spec = normalizeSpec(schemaValue);
205
+ } catch (e) {
206
+ const msg = e instanceof Error ? e.message : String(e);
207
+ issues.push({ key, kind: "invalid", message: msg });
208
+ continue;
209
+ }
210
+ const raw = env[key];
211
+ if (raw === void 0 || raw === "") {
212
+ if (spec.defaultValue !== void 0) {
213
+ out[key] = spec.defaultValue;
214
+ } else if (spec.optional) {
215
+ out[key] = void 0;
216
+ } else {
217
+ issues.push({
218
+ key,
219
+ kind: "missing",
220
+ message: "missing required environment variable"
221
+ });
222
+ }
223
+ continue;
224
+ }
225
+ try {
226
+ if (spec.kind === "enum") {
227
+ if (!spec.values.includes(raw)) {
228
+ throw new Error(
229
+ `expected one of [${spec.values.join(", ")}], got "${raw}"`
230
+ );
231
+ }
232
+ out[key] = raw;
233
+ } else if (spec.kind === "regex") {
234
+ if (!spec.re.test(raw)) {
235
+ throw new Error(`does not match ${spec.display}`);
236
+ }
237
+ out[key] = raw;
238
+ } else {
239
+ out[key] = parseByType(spec.kind, raw);
240
+ }
241
+ } catch (e) {
242
+ const msg = e instanceof Error ? e.message : String(e);
243
+ issues.push({ key, kind: "invalid", message: msg });
244
+ }
245
+ }
246
+ if (issues.length > 0) throw new EnvDoctorError(issues);
247
+ return out;
248
+ }
249
+
250
+ // src/core/envDoctor.ts
251
+ function envDoctor(schema, options = {}) {
252
+ const { loadDotEnv = true, env = process.env } = options;
253
+ if (loadDotEnv) dotenv.config();
254
+ return validateAndParse(schema, env);
255
+ }
256
+
257
+ export {
258
+ EnvDoctorError,
259
+ normalizeSpec,
260
+ envDoctor
261
+ };
262
+ //# sourceMappingURL=chunk-L5DK6LRX.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/envDoctor.ts","../src/errors/EnvDoctorError.ts","../src/validators/primitives.ts","../src/core/runner.ts"],"sourcesContent":["import * as dotenv from \"dotenv\";\nimport { validateAndParse } from \"./runner\";\nimport type { EnvDoctorOptions, EnvDoctorResult, EnvDoctorSchema } from \"../types\";\n\nexport type { EnvDoctorOptions, EnvDoctorSchema } from \"../types\";\nexport { EnvDoctorError } from \"../errors/EnvDoctorError\";\n\nexport function envDoctor<TSchema extends EnvDoctorSchema>(\n schema: TSchema,\n options: EnvDoctorOptions = {}\n): EnvDoctorResult<TSchema> {\n const { loadDotEnv = true, env = process.env } = options;\n\n if (loadDotEnv) dotenv.config();\n\n return validateAndParse(schema, env);\n}\n","export type EnvDoctorIssue =\n | { key: string; kind: \"missing\"; message: string }\n | { key: string; kind: \"invalid\"; message: string };\n\nexport class EnvDoctorError extends Error {\n public readonly issues: EnvDoctorIssue[];\n\n constructor(issues: EnvDoctorIssue[]) {\n const header = \"ENV validation failed\";\n const lines = issues.map((i) => `- ${i.key}: ${i.message}`);\n super([header, ...lines].join(\"\\n\"));\n this.name = \"EnvDoctorError\";\n this.issues = issues;\n }\n}\n","import type { EnvBaseType, EnvSchemaValue } from \"../types\";\n\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nexport function parseByType(type: EnvBaseType, raw: string): unknown {\n switch (type) {\n case \"string\":\n return raw;\n\n case \"number\": {\n const n = Number(raw.trim());\n if (!Number.isFinite(n)) throw new Error(`expected number, got \"${raw}\"`);\n return n;\n }\n\n case \"boolean\": {\n const v = raw.trim().toLowerCase();\n if ([\"true\", \"1\", \"yes\", \"y\", \"on\"].includes(v)) return true;\n if ([\"false\", \"0\", \"no\", \"n\", \"off\"].includes(v)) return false;\n throw new Error(\n `expected boolean (true/false/1/0/yes/no/on/off), got \"${raw}\"`,\n );\n }\n\n case \"json\": {\n try {\n return JSON.parse(raw);\n } catch {\n throw new Error(`expected json, got \"${raw}\"`);\n }\n }\n\n case \"url\": {\n try {\n new URL(raw);\n return raw;\n } catch {\n throw new Error(`expected url, got \"${raw}\"`);\n }\n }\n\n case \"email\": {\n const s = raw.trim();\n if (!EMAIL_RE.test(s)) {\n throw new Error(`expected email, got \"${raw}\"`);\n }\n return s;\n }\n\n default: {\n throw new Error(`unsupported primitive type: ${String(type)}`);\n }\n }\n}\n\nfunction hasOwn(obj: unknown, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(obj, key);\n}\n\n/**\n * Type-check + normalize a default value into the right runtime type.\n * - Allows string defaults for number/boolean (parsed)\n * - Validates url/email defaults\n * - json defaults can be any value\n */\nfunction coerceDefault(kind: EnvBaseType, def: unknown): unknown {\n if (def === undefined) return undefined;\n\n switch (kind) {\n case \"string\": {\n if (typeof def !== \"string\")\n throw new Error(`default for string must be a string`);\n return def;\n }\n\n case \"number\": {\n if (typeof def === \"number\") {\n if (!Number.isFinite(def))\n throw new Error(`default for number must be finite`);\n return def;\n }\n if (typeof def === \"string\") return parseByType(\"number\", def);\n throw new Error(`default for number must be number or string`);\n }\n\n case \"boolean\": {\n if (typeof def === \"boolean\") return def;\n if (typeof def === \"string\") return parseByType(\"boolean\", def);\n throw new Error(`default for boolean must be boolean or string`);\n }\n\n case \"json\": {\n // allow any JSON-ish value\n return def;\n }\n\n case \"url\": {\n if (typeof def !== \"string\")\n throw new Error(`default for url must be a string`);\n return parseByType(\"url\", def);\n }\n\n case \"email\": {\n if (typeof def !== \"string\")\n throw new Error(`default for email must be a string`);\n return parseByType(\"email\", def);\n }\n\n /* c8 ignore next */\n default: {\n throw new Error(`unsupported default kind: ${String(kind)}`);\n }\n }\n}\n\n/** Normalized spec used by the runner */\nexport type NormalizedSpec =\n | { kind: EnvBaseType; optional: boolean; defaultValue?: unknown }\n | {\n kind: \"enum\";\n optional: boolean;\n values: readonly string[];\n defaultValue?: string;\n }\n | {\n kind: \"regex\";\n optional: boolean;\n re: RegExp;\n display: string;\n defaultValue?: string;\n };\n\nexport function normalizeSpec(schemaValue: EnvSchemaValue): NormalizedSpec {\n // --------------------\n // String style: \"number?\" etc.\n // --------------------\n if (typeof schemaValue === \"string\") {\n const optional = schemaValue.endsWith(\"?\");\n const base = optional ? schemaValue.slice(0, -1) : schemaValue;\n\n const allowed: readonly EnvBaseType[] = [\n \"string\",\n \"number\",\n \"boolean\",\n \"json\",\n \"url\",\n \"email\",\n ];\n\n if (!allowed.includes(base as EnvBaseType)) {\n throw new Error(\n `Unsupported type \"${schemaValue}\". Supported: string, number, boolean, json, url, email (optional with ?)`,\n );\n }\n\n return { kind: base as EnvBaseType, optional };\n }\n\n // --------------------\n // Object style\n // --------------------\n if (\n !schemaValue ||\n typeof schemaValue !== \"object\" ||\n Array.isArray(schemaValue)\n ) {\n throw new Error(\"Schema value must be a string or object spec.\");\n }\n\n const t = (schemaValue as any).type;\n\n // --------------------\n // Primitive object spec: { type: \"number\", optional?: true, default?: ... }\n // --------------------\n const primitiveAllowed: readonly EnvBaseType[] = [\n \"string\",\n \"number\",\n \"boolean\",\n \"json\",\n \"url\",\n \"email\",\n ];\n\n if (primitiveAllowed.includes(t)) {\n const optional = !!(schemaValue as any).optional;\n const defaultValue = hasOwn(schemaValue, \"default\")\n ? coerceDefault(t as EnvBaseType, (schemaValue as any).default)\n : undefined;\n\n return { kind: t as EnvBaseType, optional, defaultValue };\n }\n\n // --------------------\n // Enum: { type: \"enum\", values: [...], optional?: true, default?: \"dev\" }\n // --------------------\n if (t === \"enum\") {\n const values = (schemaValue as any).values;\n if (\n !Array.isArray(values) ||\n values.length === 0 ||\n !values.every((v: any) => typeof v === \"string\")\n ) {\n throw new Error(`enum spec requires \"values\": string[] (non-empty)`);\n }\n\n const optional = !!(schemaValue as any).optional;\n\n let defaultValue: string | undefined = undefined;\n if (hasOwn(schemaValue, \"default\")) {\n const def = (schemaValue as any).default;\n if (typeof def !== \"string\") {\n throw new Error(`default for enum must be a string`);\n }\n if (!values.includes(def)) {\n throw new Error(\n `default \"${def}\" must be one of [${values.join(\", \")}]`,\n );\n }\n defaultValue = def;\n }\n\n return { kind: \"enum\", optional, values, defaultValue };\n }\n\n // --------------------\n // Regex: { type: \"regex\", pattern: \"...\", flags?: \"...\", optional?: true, default?: \"abc\" }\n // --------------------\n if (t === \"regex\") {\n const pattern = (schemaValue as any).pattern;\n const flags = (schemaValue as any).flags;\n\n if (typeof pattern !== \"string\" || pattern.length === 0) {\n throw new Error(`regex spec requires \"pattern\": string`);\n }\n if (flags !== undefined && typeof flags !== \"string\") {\n throw new Error(`regex spec \"flags\" must be a string if provided`);\n }\n\n let re: RegExp;\n try {\n re = new RegExp(pattern, flags);\n } catch (e) {\n throw new Error(`invalid regex: ${String(e)}`);\n }\n\n const display = `/${pattern}/${flags ?? \"\"}`;\n const optional = !!(schemaValue as any).optional;\n\n let defaultValue: string | undefined = undefined;\n if (hasOwn(schemaValue, \"default\")) {\n const def = (schemaValue as any).default;\n if (typeof def !== \"string\") {\n throw new Error(`default for regex must be a string`);\n }\n if (!re.test(def)) {\n throw new Error(`default \"${def}\" does not match ${display}`);\n }\n defaultValue = def;\n }\n\n return { kind: \"regex\", optional, re, display, defaultValue };\n }\n\n throw new Error(\n `Unsupported object spec type \"${String(t)}\". Supported: primitives (string/number/boolean/json/url/email), enum, regex`,\n );\n}\n","import { EnvDoctorError, type EnvDoctorIssue } from \"../errors/EnvDoctorError\";\nimport { normalizeSpec, parseByType } from \"../validators/primitives\";\nimport type { EnvDoctorResult, EnvDoctorSchema } from \"../types\";\n\nexport function validateAndParse<TSchema extends EnvDoctorSchema>(\n schema: TSchema,\n env: Record<string, string | undefined>,\n): EnvDoctorResult<TSchema> {\n const issues: EnvDoctorIssue[] = [];\n const out: Record<string, unknown> = {};\n\n for (const [key, schemaValue] of Object.entries(schema)) {\n let spec: ReturnType<typeof normalizeSpec>;\n\n try {\n spec = normalizeSpec(schemaValue);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n issues.push({ key, kind: \"invalid\", message: msg });\n continue;\n }\n\n const raw = env[key];\n\n // treat undefined or empty string as \"missing\"\n if (raw === undefined || raw === \"\") {\n if (spec.defaultValue !== undefined) {\n out[key] = spec.defaultValue;\n } else if (spec.optional) {\n out[key] = undefined;\n } else {\n issues.push({\n key,\n kind: \"missing\",\n message: \"missing required environment variable\",\n });\n }\n continue;\n }\n\n try {\n if (spec.kind === \"enum\") {\n if (!spec.values.includes(raw)) {\n throw new Error(\n `expected one of [${spec.values.join(\", \")}], got \"${raw}\"`,\n );\n }\n out[key] = raw;\n } else if (spec.kind === \"regex\") {\n if (!spec.re.test(raw)) {\n throw new Error(`does not match ${spec.display}`);\n }\n out[key] = raw;\n } else {\n // primitives: string/number/boolean/json/url/email\n out[key] = parseByType(spec.kind, raw);\n }\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n issues.push({ key, kind: \"invalid\", message: msg });\n }\n }\n\n if (issues.length > 0) throw new EnvDoctorError(issues);\n\n return out as EnvDoctorResult<TSchema>;\n}\n"],"mappings":"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","names":[]}