env-typed-checker 0.1.1 β†’ 0.2.1

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
@@ -1,34 +1,47 @@
1
- # env-typed-checker 🩺
1
+ # env-typed-checker
2
2
 
3
- A tiny, developer-friendly library to **validate and parse environment variables** using a simple schema.
3
+ Validate and parse environment variables using a tiny schema β€” with both a **TypeScript/Node API** and a **CLI**.
4
4
 
5
- env-typed-checker prevents your application from starting with:
5
+ It helps your app fail fast when configuration is wrong:
6
+
7
+ - ❌ Missing required variables
8
+ - ❌ Wrong types (e.g. `PORT="abc"`)
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
6
13
 
7
- - ❌ missing environment variables
8
- - ❌ wrong types (e.g. PORT="abc")
9
- - ❌ invalid URLs or JSON
10
- - ❌ silent configuration mistakes
11
14
 
12
15
  ---
13
16
 
14
17
  ## ✨ Features
15
18
 
16
- - Simple schema syntax
17
- - Automatic `.env` loading
18
- - Type parsing (number, boolean, json, url)
19
- - Optional variables with `?`
20
- - Friendly aggregated error messages
21
- - TypeScript support out of the box
22
- - Zero dependencies except `dotenv`
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
+
25
+ - `check` β†’ validate env
26
+ - `generate` β†’ generate/update `.env` from schema (no overwrite by default)
27
+ - Uses `.env` via `dotenv` (optional)
28
+ - Friendly aggregated errors (see everything that’s wrong at once)
23
29
 
24
30
  ---
25
31
 
26
- ## πŸ“¦ Installation
32
+ ## πŸ“¦ Install
27
33
 
28
34
  ```bash
29
35
  npm install env-typed-checker
30
36
  ```
31
- ### πŸš€ Basic Usage
37
+
38
+ Or run via npx:
39
+
40
+ ```bash
41
+ npx env-typed-checker --help
42
+ ```
43
+
44
+ ## πŸš€ Quick Start (Code)
32
45
 
33
46
  ```ts
34
47
  import { envDoctor } from "env-typed-checker";
@@ -36,13 +49,22 @@ import { envDoctor } from "env-typed-checker";
36
49
  export const config = envDoctor({
37
50
  PORT: "number",
38
51
  DB_URL: "url",
39
- DEBUG: "boolean?"
52
+ ADMIN_EMAIL: "email",
53
+ DEBUG: "boolean?",
40
54
  });
55
+
41
56
  ```
42
- ### Result
43
- * **PORT** β†’ `number` (e.g., `"3000"` becomes `3000`)
44
- * **DB_URL** β†’ `string` (validated as a proper URL string)
45
- * **DEBUG** β†’ `boolean | undefined` (optional field; parses `"true"`, `"1"`, etc.)
57
+
58
+ ### What you get
59
+
60
+ * PORT β†’ number
61
+
62
+ * DB_URL β†’ string (validated as URL)
63
+
64
+ * ADMIN_EMAIL β†’ string (validated as email)
65
+
66
+ * DEBUG β†’ boolean | undefined (optional)
67
+
46
68
 
47
69
  ### 🧩 Supported Types
48
70
  | Type | Description |
@@ -52,87 +74,225 @@ export const config = envDoctor({
52
74
  | **boolean** | Supports `true` / `false`, `1` / `0`, and `yes` / `no` |
53
75
  | **json** | Validates and parses a valid JSON string |
54
76
  | **url** | Validates for a properly formatted URL |
77
+ | **email** | Validates for a properly formatted email |
78
+
55
79
 
56
80
  ### Optional Values
57
81
  Add ? to make a variable optional:
58
82
  ```ts
59
- { DEBUG: "boolean?" }
83
+ envDoctor({ DEBUG: "boolean?" });
84
+ ```
85
+
86
+ Or use object-style:
87
+
88
+ ```ts
89
+ envDoctor({
90
+ DEBUG: { type: "boolean", optional: true },
91
+ });
92
+ ```
93
+ Missing optional vars become `undefined` unless you provide a default.
94
+
95
+ ### 🎯 Defaults
96
+ Defaults can be provided in object-style specs.
97
+
98
+ ```ts
99
+ import { envDoctor } from "env-typed-checker";
100
+
101
+ const config = envDoctor({
102
+ PORT: { type: "number", default: 3000 },
103
+ DEBUG: { type: "boolean", default: "false" }, // string is allowed (parsed)
104
+ NODE_ENV: { type: "enum", values: ["dev", "prod"], default: "dev" },
105
+ });
106
+ ```
107
+
108
+ ### Notes:
109
+
110
+ - `number` default: number or string (string will be parsed)
111
+ - `boolean` default: boolean or string (string will be parsed)
112
+ - `url/email` defaults must be strings and must validate
113
+ - `json` defaults can be any JSON-like value
114
+
115
+ ### βœ… Enum and Regex
116
+
117
+ ### Enum
118
+
119
+ ```ts
120
+ const config = envDoctor({
121
+ NODE_ENV: { type: "enum", values: ["dev", "prod"] },
122
+ });
123
+ ```
124
+
125
+ ### Regex
126
+
127
+ ```ts
128
+ const config = envDoctor({
129
+ SLUG: { type: "regex", pattern: "^[a-z0-9-]+$", flags: "i" },
130
+ });
131
+ ```
132
+
133
+ ### βš™οΈ Options
134
+
135
+ ```ts
136
+ envDoctor(schema, {
137
+ loadDotEnv: true, // default: true (loads .env)
138
+ env: process.env // default: process.env (override for tests)
139
+ });
140
+ ```
141
+
142
+
143
+ ### πŸ§ͺ Testing with custom env
144
+
145
+ ```ts
146
+ import { envDoctor } from "env-typed-checker";
147
+
148
+ const cfg = envDoctor(
149
+ { PORT: "number" },
150
+ { loadDotEnv: false, env: { PORT: "3000" } }
151
+ );
152
+
153
+ console.log(cfg.PORT); // 3000
60
154
  ```
61
- If missing β†’ value will be undefined.
62
155
 
63
156
  ### ❌ Error Example
64
- Given this .env:
65
- ```.env
157
+
158
+ Given a `.env` like:
159
+
160
+ ```env
66
161
  PORT=abc
67
162
  DB_URL=not-a-url
68
- Code:
163
+ NODE_ENV=staging
69
164
  ```
165
+
70
166
  ```ts
167
+ import { envDoctor } from "env-typed-checker";
168
+
71
169
  envDoctor({
72
170
  PORT: "number",
73
171
  DB_URL: "url"
172
+ NODE_ENV: { type: "enum", values: ["dev", "prod"] },
74
173
  });
75
174
  ```
175
+
76
176
  ### Output:
77
177
 
78
178
  ```ts
79
179
  ENV validation failed
80
180
  - PORT: expected number, got "abc"
81
181
  - DB_URL: expected url, got "not-a-url"
182
+ - NODE_ENV: must be one of [dev, prod]
82
183
  ```
83
184
  All errors are shown together so you can fix them in one go.
84
185
 
85
- ### βš™οΈ Options
86
- ```ts
87
- envDoctor(schema, {
88
- loadDotEnv: true, // auto load .env (default)
89
- env: process.env // custom env source (useful for tests)
90
- });
186
+ # πŸ–₯️ CLI
187
+
188
+ Validate your environment without writing code β€” perfect for CI pipelines.
189
+
190
+ ## 1) Create a schema file
191
+
192
+ `env.schema.json`
193
+ ```json
194
+ {
195
+ "PORT": "number",
196
+ "DB_URL": "url",
197
+ "ADMIN_EMAIL": "email",
198
+ "DEBUG": "boolean?",
199
+ "NODE_ENV": { "type": "enum", "values": ["dev", "prod"] },
200
+ "SLUG": { "type": "regex", "pattern": "^[a-z0-9-]+$", "flags": "i" }
201
+ }
91
202
  ```
92
- ### πŸ§ͺ Example with Custom Env (Testing)
93
203
 
94
- ```ts
95
- const cfg = envDoctor(
96
- { PORT: "number" },
97
- { loadDotEnv: false, env: { PORT: "3000" } }
98
- );
204
+ ## 2) Run the check
99
205
 
100
- console.log(cfg.PORT); // 3000 (number)
206
+ ```bash
207
+ npx env-typed-checker check --schema env.schema.json
101
208
  ```
102
- ### πŸ›  Development
209
+
210
+ Useful flags
211
+
212
+ ```bash
213
+ # Use a specific env file (instead of .env)
214
+ npx env-typed-checker check --schema env.schema.json --env-file .env.production
215
+
216
+ # Skip dotenv loading and validate only process.env
217
+ npx env-typed-checker check --schema env.schema.json --no-dotenv
218
+ ```
219
+
220
+ ### `generate` β€” generate or update `.env`
221
+
222
+ Generate values from schema (writes missing keys; does not overwrite existing values).
223
+
224
+ ```bash
225
+ npx env-typed-checker generate --schema env.schema.json
226
+ ```
227
+ ### By default:
228
+
229
+ - output file: .env
230
+ - mode: update
231
+
232
+ ### Flags
233
+
234
+ ```bash
235
+ # Custom output file
236
+ npx env-typed-checker generate --schema env.schema.json --out .env.example
237
+
238
+ # Create mode: refuse to overwrite an existing file
239
+ npx env-typed-checker generate --schema env.schema.json --out .env --mode=create
240
+
241
+ # Update mode: append only missing keys (safe)
242
+ npx env-typed-checker generate --schema env.schema.json --out .env --mode=update
243
+
244
+ # Do not write defaults (leave blank values)
245
+ npx env-typed-checker generate --schema env.schema.json --no-defaults
246
+
247
+ # Add inline type comments (useful for .env.example)
248
+ npx env-typed-checker generate --schema env.schema.json --comment-types
249
+ ```
250
+
251
+ ### Exit codes
252
+
253
+ * `0` = OK
254
+
255
+ * `1` = validation failed
256
+
257
+ * `2` = CLI usage / unexpected error
258
+
259
+ ## βœ… CI Example (GitHub Actions)
260
+
261
+ Add this to your workflow to fail the build if env is invalid:
262
+
263
+ ```yml
264
+ - name: Validate env
265
+ run: npx env-typed-checker check --schema env.schema.json
266
+ ```
267
+ If you use a specific env file in CI:
268
+
269
+ ```yml
270
+ - name: Validate env
271
+ run: npx env-typed-checker check --schema env.schema.json --env-file .env.ci
272
+ ```
273
+
274
+ ## πŸ›  Development
103
275
  Clone the repo and install:
104
276
  ```bash
105
277
  npm install
278
+ npm test
106
279
  ```
107
- ### Available Scripts
280
+ ### Common scripts:
108
281
  ```bash
109
282
  npm run build # build package
110
283
  npm run test # run tests
111
284
  npm run typecheck # TypeScript check
112
285
  npm run dev # watch build
113
286
  ```
287
+
114
288
  ### 🀝 Contributing
115
- Contributions are welcome!
116
-
117
- * Improve error messages
118
- * Add more boolean variants
119
- * Enhance URL validation
120
- * Add JSON schema validation
121
- * Write better docs & examples
122
- * Please read CONTRIBUTING.md before opening a PR.
123
-
124
- ### πŸ“Œ Roadmap
125
- #### v1 (current)
126
- * Schema validation
127
- * Type parsing
128
- * Optional values
129
- * Friendly errors
130
-
131
- #### v2 (planned)
132
- * CLI support
133
- * .env.example generator
134
- * Strict unknown variable check
135
- * Framework integrations
289
+ PRs are welcome!
290
+
291
+ * Improve docs / examples
292
+ * Add more schema features
293
+ * Improve CLI output formatting
294
+ * Add integrations / templates
295
+
136
296
 
137
297
  # πŸ“ License
138
298
  MIT
@@ -141,13 +301,5 @@ MIT
141
301
  ---
142
302
 
143
303
  ```yml
144
- If you want, I can help you add:
145
-
146
- - badges (npm version, CI, coverage)
147
- - a small logo
148
- - example project section
149
-
150
- Just tell me πŸ‘
151
- ```
152
-
153
-
304
+ ::contentReference[oaicite:0]{index=0}
305
+ ```
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { runCli } = require("../dist/cli.js");
4
+
5
+ const code = runCli(process.argv.slice(2), console);
6
+ process.exitCode = code;
@@ -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":[]}
@@ -0,0 +1,7 @@
1
+ type Io = {
2
+ log: (msg: string) => void;
3
+ error: (msg: string) => void;
4
+ };
5
+ declare function runCli(argv: string[], io?: Io): number;
6
+
7
+ export { runCli };