zod-envkit 1.0.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/.env.example ADDED
@@ -0,0 +1,8 @@
1
+ # Runtime mode
2
+ NODE_ENV=development
3
+
4
+ # HTTP port
5
+ PORT=3000
6
+
7
+ # Postgres connection string
8
+ DATABASE_URL=postgresql://user:pass@localhost:5432/db
package/ENV.md ADDED
@@ -0,0 +1,7 @@
1
+ # Environment variables
2
+
3
+ | Key | Required | Example | Description |
4
+ | :------------: | :--------: | :----------------------------------------: | :--------------------------: |
5
+ | NODE_ENV | yes | development | Runtime mode |
6
+ | PORT | yes | 3000 | HTTP port |
7
+ | DATABASE_URL | yes | postgresql://user:pass@localhost:5432/db | Postgres connection string |
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ Type-safe environment variable validation and documentation using Zod.
2
+
3
+ `zod-envkit` is a small library and CLI tool that helps you:
4
+ - validate `process.env`
5
+ - get **fully typed environment variables**
6
+ - automatically generate `.env.example`
7
+ - generate environment documentation (`ENV.md`)
8
+ - treat environment variables as an **explicit contract**, not guesswork
9
+
10
+ No cloud. No magic. Just code.
11
+
12
+ ---
13
+
14
+ ## Why
15
+
16
+ The problem:
17
+ - `process.env` is just `string | undefined`
18
+ - environment errors often appear **only at runtime**
19
+ - `.env.example` and documentation are often outdated or missing
20
+
21
+ The solution:
22
+ - define your environment **once**
23
+ - get:
24
+ - early validation
25
+ - TypeScript types
26
+ - up-to-date `.env.example`
27
+ - up-to-date documentation
28
+
29
+ ---
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ npm install zod-envkit
35
+ ````
36
+
37
+ or
38
+
39
+ ```bash
40
+ pnpm add zod-envkit
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Quick start
46
+
47
+ ```ts
48
+ import "dotenv/config";
49
+ import { z } from "zod";
50
+ import { loadEnv, formatZodError } from "zod-envkit";
51
+
52
+ const EnvSchema = z.object({
53
+ NODE_ENV: z.enum(["development", "test", "production"]),
54
+ PORT: z.coerce.number().int().min(1).max(65535),
55
+ DATABASE_URL: z.string().url()
56
+ });
57
+
58
+ const result = loadEnv(EnvSchema);
59
+
60
+ if (!result.ok) {
61
+ console.error("Invalid environment:\n" + formatZodError(result.error));
62
+ process.exit(1);
63
+ }
64
+
65
+ export const env = result.env;
66
+ ```
67
+
68
+ Now:
69
+
70
+ * `env.PORT` is a **number**
71
+ * `env.DATABASE_URL` is a **string**
72
+ * TypeScript knows everything at compile time
73
+
74
+ ---
75
+
76
+ ## CLI: generate `.env.example` and `ENV.md`
77
+
78
+ ### 1️⃣ Create `env.meta.json` in your project root
79
+
80
+ ```json
81
+ {
82
+ "NODE_ENV": {
83
+ "description": "Application runtime environment",
84
+ "example": "development"
85
+ },
86
+ "PORT": {
87
+ "description": "HTTP server port",
88
+ "example": "3000"
89
+ },
90
+ "DATABASE_URL": {
91
+ "description": "PostgreSQL connection string",
92
+ "example": "postgresql://user:pass@localhost:5432/db"
93
+ }
94
+ }
95
+ ```
96
+
97
+ ---
98
+
99
+ ### 2️⃣ Run the CLI
100
+
101
+ ```bash
102
+ npx zod-envkit
103
+ ```
104
+
105
+ Or locally during development:
106
+
107
+ ```bash
108
+ node dist/cli.js
109
+ ```
110
+
111
+ ---
112
+
113
+ ### 3️⃣ Output
114
+
115
+ #### `.env.example`
116
+
117
+ ```env
118
+ # Application runtime environment
119
+ NODE_ENV=development
120
+
121
+ # HTTP server port
122
+ PORT=3000
123
+
124
+ # PostgreSQL connection string
125
+ DATABASE_URL=postgresql://user:pass@localhost:5432/db
126
+ ```
127
+
128
+ #### `ENV.md`
129
+
130
+ ```md
131
+ # Environment variables
132
+
133
+ | Key | Required | Example | Description |
134
+ |---|---:|---|---|
135
+ | NODE_ENV | yes | development | Application runtime environment |
136
+ | PORT | yes | 3000 | HTTP server port |
137
+ | DATABASE_URL | yes | postgresql://... | PostgreSQL connection string |
138
+ ```
139
+
140
+ ---
141
+
142
+ ## CLI options
143
+
144
+ ```bash
145
+ zod-envkit --help
146
+ ```
147
+
148
+ ```bash
149
+ zod-envkit \
150
+ --config env.meta.json \
151
+ --out-example .env.example \
152
+ --out-docs ENV.md
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Why not just dotenv?
158
+
159
+ `dotenv`:
160
+
161
+ * ❌ no validation
162
+ * ❌ no types
163
+ * ❌ no documentation
164
+
165
+ `zod-envkit`:
166
+
167
+ * ✅ validation
168
+ * ✅ TypeScript inference
169
+ * ✅ documentation
170
+ * ✅ CLI tooling
171
+
172
+ They work **great together**.
173
+
174
+ ---
175
+
176
+ ## Design decisions
177
+
178
+ * ❌ does not try to automatically parse Zod schemas
179
+ * ❌ does not couple to any framework
180
+ * ✅ explicit configuration over magic
181
+ * ✅ small, predictable API
182
+ * ✅ library + CLI, both optional
183
+
184
+ ---
185
+
186
+ ## Roadmap
187
+
188
+ * [ ] schema ↔ meta consistency checks
189
+ * [ ] `zod-envkit check` (validation only)
190
+ * [ ] grouping / sections in docs
191
+ * [ ] prettier, human-friendly error output
192
+ * [ ] JSON schema export
193
+
194
+ ---
195
+
196
+ ## Who is this for?
197
+
198
+ * backend and fullstack projects
199
+ * Node.js / Bun services
200
+ * CI/CD pipelines
201
+ * teams that treat env as part of their contract
202
+
203
+ ---
204
+
205
+ ## License
206
+
207
+ MIT
package/dist/cli.cjs ADDED
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_node_fs = __toESM(require("fs"), 1);
28
+ var import_node_path = __toESM(require("path"), 1);
29
+ var import_commander = require("commander");
30
+
31
+ // src/generate.ts
32
+ function strLen(s) {
33
+ return s.length;
34
+ }
35
+ function padCenter(text, width) {
36
+ const t = text ?? "";
37
+ const diff = width - strLen(t);
38
+ if (diff <= 0) return t;
39
+ const left = Math.floor(diff / 2);
40
+ const right = diff - left;
41
+ return " ".repeat(left) + t + " ".repeat(right);
42
+ }
43
+ function makeDivider(width, align) {
44
+ if (width < 3) width = 3;
45
+ if (align === "center") return ":" + "-".repeat(width - 2) + ":";
46
+ if (align === "right") return "-".repeat(width - 1) + ":";
47
+ return "-".repeat(width);
48
+ }
49
+ function generateEnvExample(meta) {
50
+ const lines = [];
51
+ for (const [key, m] of Object.entries(meta)) {
52
+ if (m.description) lines.push(`# ${m.description}`);
53
+ lines.push(`${key}=${m.example ?? ""}`);
54
+ lines.push("");
55
+ }
56
+ return lines.join("\n").trim() + "\n";
57
+ }
58
+ function generateEnvDocs(meta) {
59
+ const headers = ["Key", "Required", "Example", "Description"];
60
+ const rows = Object.entries(meta).map(([key, m]) => {
61
+ const req = m.required === false ? "no" : "yes";
62
+ return {
63
+ Key: key,
64
+ Required: req,
65
+ Example: m.example ?? "",
66
+ Description: m.description ?? ""
67
+ };
68
+ });
69
+ const widths = {
70
+ Key: headers[0].length,
71
+ Required: headers[1].length,
72
+ Example: headers[2].length,
73
+ Description: headers[3].length
74
+ };
75
+ for (const r of rows) {
76
+ widths.Key = Math.max(widths.Key, strLen(r.Key));
77
+ widths.Required = Math.max(widths.Required, strLen(r.Required));
78
+ widths.Example = Math.max(widths.Example, strLen(r.Example));
79
+ widths.Description = Math.max(widths.Description, strLen(r.Description));
80
+ }
81
+ widths.Key += 2;
82
+ widths.Required += 2;
83
+ widths.Example += 2;
84
+ widths.Description += 2;
85
+ const headerLine = `| ${padCenter(headers[0], widths.Key)} | ${padCenter(headers[1], widths.Required)} | ${padCenter(headers[2], widths.Example)} | ${padCenter(headers[3], widths.Description)} |`;
86
+ const dividerLine = `| ${makeDivider(widths.Key, "center")} | ${makeDivider(widths.Required, "center")} | ${makeDivider(widths.Example, "center")} | ${makeDivider(widths.Description, "center")} |`;
87
+ const bodyLines = rows.map((r) => {
88
+ return `| ${padCenter(r.Key, widths.Key)} | ${padCenter(r.Required, widths.Required)} | ${padCenter(r.Example, widths.Example)} | ${padCenter(r.Description, widths.Description)} |`;
89
+ });
90
+ return [
91
+ `# Environment variables`,
92
+ ``,
93
+ headerLine,
94
+ dividerLine,
95
+ ...bodyLines,
96
+ ``
97
+ ].join("\n");
98
+ }
99
+
100
+ // src/cli.ts
101
+ var program = new import_commander.Command();
102
+ program.name("zod-envkit").description("Generate .env.example and ENV.md from env.meta.json").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--out-example <file>", "Output file for .env.example", ".env.example").option("--out-docs <file>", "Output file for docs", "ENV.md").action((opts) => {
103
+ const configPath = import_node_path.default.resolve(process.cwd(), opts.config);
104
+ const raw = import_node_fs.default.readFileSync(configPath, "utf8");
105
+ const meta = JSON.parse(raw);
106
+ import_node_fs.default.writeFileSync(opts.outExample, generateEnvExample(meta), "utf8");
107
+ import_node_fs.default.writeFileSync(opts.outDocs, generateEnvDocs(meta), "utf8");
108
+ console.log(`Generated: ${opts.outExample}, ${opts.outDocs}`);
109
+ });
110
+ program.parse();
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { Command } from "commander";
7
+
8
+ // src/generate.ts
9
+ function strLen(s) {
10
+ return s.length;
11
+ }
12
+ function padCenter(text, width) {
13
+ const t = text ?? "";
14
+ const diff = width - strLen(t);
15
+ if (diff <= 0) return t;
16
+ const left = Math.floor(diff / 2);
17
+ const right = diff - left;
18
+ return " ".repeat(left) + t + " ".repeat(right);
19
+ }
20
+ function makeDivider(width, align) {
21
+ if (width < 3) width = 3;
22
+ if (align === "center") return ":" + "-".repeat(width - 2) + ":";
23
+ if (align === "right") return "-".repeat(width - 1) + ":";
24
+ return "-".repeat(width);
25
+ }
26
+ function generateEnvExample(meta) {
27
+ const lines = [];
28
+ for (const [key, m] of Object.entries(meta)) {
29
+ if (m.description) lines.push(`# ${m.description}`);
30
+ lines.push(`${key}=${m.example ?? ""}`);
31
+ lines.push("");
32
+ }
33
+ return lines.join("\n").trim() + "\n";
34
+ }
35
+ function generateEnvDocs(meta) {
36
+ const headers = ["Key", "Required", "Example", "Description"];
37
+ const rows = Object.entries(meta).map(([key, m]) => {
38
+ const req = m.required === false ? "no" : "yes";
39
+ return {
40
+ Key: key,
41
+ Required: req,
42
+ Example: m.example ?? "",
43
+ Description: m.description ?? ""
44
+ };
45
+ });
46
+ const widths = {
47
+ Key: headers[0].length,
48
+ Required: headers[1].length,
49
+ Example: headers[2].length,
50
+ Description: headers[3].length
51
+ };
52
+ for (const r of rows) {
53
+ widths.Key = Math.max(widths.Key, strLen(r.Key));
54
+ widths.Required = Math.max(widths.Required, strLen(r.Required));
55
+ widths.Example = Math.max(widths.Example, strLen(r.Example));
56
+ widths.Description = Math.max(widths.Description, strLen(r.Description));
57
+ }
58
+ widths.Key += 2;
59
+ widths.Required += 2;
60
+ widths.Example += 2;
61
+ widths.Description += 2;
62
+ const headerLine = `| ${padCenter(headers[0], widths.Key)} | ${padCenter(headers[1], widths.Required)} | ${padCenter(headers[2], widths.Example)} | ${padCenter(headers[3], widths.Description)} |`;
63
+ const dividerLine = `| ${makeDivider(widths.Key, "center")} | ${makeDivider(widths.Required, "center")} | ${makeDivider(widths.Example, "center")} | ${makeDivider(widths.Description, "center")} |`;
64
+ const bodyLines = rows.map((r) => {
65
+ return `| ${padCenter(r.Key, widths.Key)} | ${padCenter(r.Required, widths.Required)} | ${padCenter(r.Example, widths.Example)} | ${padCenter(r.Description, widths.Description)} |`;
66
+ });
67
+ return [
68
+ `# Environment variables`,
69
+ ``,
70
+ headerLine,
71
+ dividerLine,
72
+ ...bodyLines,
73
+ ``
74
+ ].join("\n");
75
+ }
76
+
77
+ // src/cli.ts
78
+ var program = new Command();
79
+ program.name("zod-envkit").description("Generate .env.example and ENV.md from env.meta.json").option("-c, --config <file>", "Path to env meta json", "env.meta.json").option("--out-example <file>", "Output file for .env.example", ".env.example").option("--out-docs <file>", "Output file for docs", "ENV.md").action((opts) => {
80
+ const configPath = path.resolve(process.cwd(), opts.config);
81
+ const raw = fs.readFileSync(configPath, "utf8");
82
+ const meta = JSON.parse(raw);
83
+ fs.writeFileSync(opts.outExample, generateEnvExample(meta), "utf8");
84
+ fs.writeFileSync(opts.outDocs, generateEnvDocs(meta), "utf8");
85
+ console.log(`Generated: ${opts.outExample}, ${opts.outDocs}`);
86
+ });
87
+ program.parse();
package/dist/index.cjs ADDED
@@ -0,0 +1,40 @@
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/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ formatZodError: () => formatZodError,
24
+ loadEnv: () => loadEnv
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ function loadEnv(schema, opts) {
28
+ const parsed = schema.safeParse(process.env);
29
+ if (parsed.success) return { ok: true, env: parsed.data };
30
+ if (opts?.throwOnError) throw parsed.error;
31
+ return { ok: false, error: parsed.error };
32
+ }
33
+ function formatZodError(err) {
34
+ return err.issues.map((i) => `- ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
35
+ }
36
+ // Annotate the CommonJS export names for ESM import in node:
37
+ 0 && (module.exports = {
38
+ formatZodError,
39
+ loadEnv
40
+ });
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+
3
+ type EnvResult<T extends z.ZodTypeAny> = z.infer<T>;
4
+ declare function loadEnv<T extends z.ZodTypeAny>(schema: T, opts?: {
5
+ throwOnError?: boolean;
6
+ }): {
7
+ ok: true;
8
+ env: z.infer<T>;
9
+ } | {
10
+ ok: false;
11
+ error: z.ZodError;
12
+ };
13
+ declare function formatZodError(err: z.ZodError): string;
14
+
15
+ export { type EnvResult, formatZodError, loadEnv };
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+
3
+ type EnvResult<T extends z.ZodTypeAny> = z.infer<T>;
4
+ declare function loadEnv<T extends z.ZodTypeAny>(schema: T, opts?: {
5
+ throwOnError?: boolean;
6
+ }): {
7
+ ok: true;
8
+ env: z.infer<T>;
9
+ } | {
10
+ ok: false;
11
+ error: z.ZodError;
12
+ };
13
+ declare function formatZodError(err: z.ZodError): string;
14
+
15
+ export { type EnvResult, formatZodError, loadEnv };
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ // src/index.ts
2
+ function loadEnv(schema, opts) {
3
+ const parsed = schema.safeParse(process.env);
4
+ if (parsed.success) return { ok: true, env: parsed.data };
5
+ if (opts?.throwOnError) throw parsed.error;
6
+ return { ok: false, error: parsed.error };
7
+ }
8
+ function formatZodError(err) {
9
+ return err.issues.map((i) => `- ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
10
+ }
11
+ export {
12
+ formatZodError,
13
+ loadEnv
14
+ };
package/env.meta.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "NODE_ENV": { "description": "Runtime mode", "example": "development" },
3
+ "PORT": { "description": "HTTP port", "example": "3000" },
4
+ "DATABASE_URL": { "description": "Postgres connection string", "example": "postgresql://user:pass@localhost:5432/db" }
5
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "zod-envkit",
3
+ "version": "1.0.0",
4
+ "description": "Validate environment variables with Zod and generate .env.example",
5
+ "type": "module",
6
+
7
+ "main": "dist/index.cjs",
8
+ "module": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+
19
+ "bin": {
20
+ "zod-envkit": "dist/cli.js"
21
+ },
22
+
23
+ "scripts": {
24
+ "build": "tsup src/index.ts src/cli.ts --format esm,cjs --dts",
25
+ "dev": "tsup src/index.ts src/cli.ts --watch --dts",
26
+ "test": "vitest run",
27
+ "prepublishOnly": "npm run test && npm run build"
28
+ },
29
+
30
+ "keywords": ["env", "dotenv", "zod", "validation", "cli"],
31
+ "author": "",
32
+ "license": "MIT",
33
+
34
+ "dependencies": {
35
+ "commander": "^13.1.0",
36
+ "dotenv": "^17.2.3",
37
+ "zod": "^4.3.6"
38
+ },
39
+
40
+ "devDependencies": {
41
+ "@types/node": "^25.0.10",
42
+ "eslint": "^9.39.2",
43
+ "tsup": "^8.5.1",
44
+ "typescript": "^5.9.3",
45
+ "vitest": "^3.2.4"
46
+ }
47
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { Command } from "commander";
5
+ import { generateEnvDocs, generateEnvExample, type EnvMeta } from "./generate.js";
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name("zod-envkit")
11
+ .description("Generate .env.example and ENV.md from env.meta.json")
12
+ .option("-c, --config <file>", "Path to env meta json", "env.meta.json")
13
+ .option("--out-example <file>", "Output file for .env.example", ".env.example")
14
+ .option("--out-docs <file>", "Output file for docs", "ENV.md")
15
+ .action((opts) => {
16
+ const configPath = path.resolve(process.cwd(), opts.config);
17
+ const raw = fs.readFileSync(configPath, "utf8");
18
+ const meta: EnvMeta = JSON.parse(raw);
19
+
20
+ fs.writeFileSync(opts.outExample, generateEnvExample(meta), "utf8");
21
+ fs.writeFileSync(opts.outDocs, generateEnvDocs(meta), "utf8");
22
+
23
+ console.log(`Generated: ${opts.outExample}, ${opts.outDocs}`);
24
+ });
25
+
26
+ program.parse();
@@ -0,0 +1,130 @@
1
+ // src/generate.ts
2
+
3
+ export type EnvMeta = Record<
4
+ string,
5
+ {
6
+ description?: string;
7
+ example?: string;
8
+ required?: boolean; // default: true
9
+ }
10
+ >;
11
+
12
+ function strLen(s: string): number {
13
+ return s.length;
14
+ }
15
+
16
+ function padCenter(text: string, width: number): string {
17
+ const t = text ?? "";
18
+ const diff = width - strLen(t);
19
+ if (diff <= 0) return t;
20
+
21
+ const left = Math.floor(diff / 2);
22
+ const right = diff - left;
23
+ return " ".repeat(left) + t + " ".repeat(right);
24
+ }
25
+
26
+ function padRight(text: string, width: number): string {
27
+ const t = text ?? "";
28
+ const diff = width - strLen(t);
29
+ if (diff <= 0) return t;
30
+ return t + " ".repeat(diff);
31
+ }
32
+
33
+ function makeDivider(width: number, align: "left" | "center" | "right"): string {
34
+ // Markdown alignment:
35
+ // left : ---
36
+ // center : :---:
37
+ // right : ---:
38
+ if (width < 3) width = 3;
39
+
40
+ if (align === "center") return ":" + "-".repeat(width - 2) + ":";
41
+ if (align === "right") return "-".repeat(width - 1) + ":";
42
+ return "-".repeat(width);
43
+ }
44
+
45
+ /**
46
+ * Генерит .env.example красиво:
47
+ * - комментарий с description
48
+ * - KEY=example
49
+ */
50
+ export function generateEnvExample(meta: EnvMeta): string {
51
+ const lines: string[] = [];
52
+
53
+ for (const [key, m] of Object.entries(meta)) {
54
+ if (m.description) lines.push(`# ${m.description}`);
55
+ lines.push(`${key}=${m.example ?? ""}`);
56
+ lines.push("");
57
+ }
58
+
59
+ return lines.join("\n").trim() + "\n";
60
+ }
61
+
62
+ /**
63
+ * Генерит ENV.md как ровную таблицу:
64
+ * - вычисляет ширины колонок по максимальной длине
65
+ * - паддит значения пробелами
66
+ * - ставит выравнивание :---: чтобы центрировалось в Markdown
67
+ */
68
+ export function generateEnvDocs(meta: EnvMeta): string {
69
+ const headers = ["Key", "Required", "Example", "Description"] as const;
70
+
71
+ const rows = Object.entries(meta).map(([key, m]) => {
72
+ const req = m.required === false ? "no" : "yes";
73
+ return {
74
+ Key: key,
75
+ Required: req,
76
+ Example: m.example ?? "",
77
+ Description: m.description ?? "",
78
+ };
79
+ });
80
+
81
+ const widths = {
82
+ Key: headers[0].length,
83
+ Required: headers[1].length,
84
+ Example: headers[2].length,
85
+ Description: headers[3].length,
86
+ };
87
+
88
+ for (const r of rows) {
89
+ widths.Key = Math.max(widths.Key, strLen(r.Key));
90
+ widths.Required = Math.max(widths.Required, strLen(r.Required));
91
+ widths.Example = Math.max(widths.Example, strLen(r.Example));
92
+ widths.Description = Math.max(widths.Description, strLen(r.Description));
93
+ }
94
+
95
+ widths.Key += 2;
96
+ widths.Required += 2;
97
+ widths.Example += 2;
98
+ widths.Description += 2;
99
+
100
+ const headerLine =
101
+ `| ${padCenter(headers[0], widths.Key)} ` +
102
+ `| ${padCenter(headers[1], widths.Required)} ` +
103
+ `| ${padCenter(headers[2], widths.Example)} ` +
104
+ `| ${padCenter(headers[3], widths.Description)} |`;
105
+
106
+ const dividerLine =
107
+ `| ${makeDivider(widths.Key, "center")} ` +
108
+ `| ${makeDivider(widths.Required, "center")} ` +
109
+ `| ${makeDivider(widths.Example, "center")} ` +
110
+ `| ${makeDivider(widths.Description, "center")} |`;
111
+
112
+ const bodyLines = rows.map((r) => {
113
+ return (
114
+ `| ${padCenter(r.Key, widths.Key)} ` +
115
+ `| ${padCenter(r.Required, widths.Required)} ` +
116
+ `| ${padCenter(r.Example, widths.Example)} ` +
117
+ `| ${padCenter(r.Description, widths.Description)} |`
118
+ );
119
+ });
120
+
121
+
122
+ return [
123
+ `# Environment variables`,
124
+ ``,
125
+ headerLine,
126
+ dividerLine,
127
+ ...bodyLines,
128
+ ``,
129
+ ].join("\n");
130
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+
3
+ export type EnvResult<T extends z.ZodTypeAny> = z.infer<T>;
4
+
5
+ export function loadEnv<T extends z.ZodTypeAny>(
6
+ schema: T,
7
+ opts?: { throwOnError?: boolean }
8
+ ):
9
+ | { ok: true; env: z.infer<T> }
10
+ | { ok: false; error: z.ZodError } {
11
+ const parsed = schema.safeParse(process.env);
12
+
13
+ if (parsed.success) return { ok: true, env: parsed.data };
14
+
15
+ if (opts?.throwOnError) throw parsed.error;
16
+ return { ok: false, error: parsed.error };
17
+ }
18
+
19
+ export function formatZodError(err: z.ZodError): string {
20
+ return err.issues
21
+ .map((i) => `- ${i.path.join(".") || "(root)"}: ${i.message}`)
22
+ .join("\n");
23
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { z } from "zod";
3
+ import { loadEnv } from "../src/index";
4
+
5
+ describe("loadEnv", () => {
6
+ it("parses valid env", () => {
7
+ process.env.PORT = "3000";
8
+ const schema = z.object({ PORT: z.coerce.number() });
9
+
10
+ const res = loadEnv(schema);
11
+ expect(res.ok).toBe(true);
12
+
13
+ if (res.ok) expect(res.env.PORT).toBe(3000);
14
+ });
15
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "declaration": true,
8
+ "skipLibCheck": true
9
+ }
10
+ }