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 +8 -0
- package/ENV.md +7 -0
- package/README.md +207 -0
- package/dist/cli.cjs +110 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +87 -0
- package/dist/index.cjs +40 -0
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/env.meta.json +5 -0
- package/package.json +47 -0
- package/src/cli.ts +26 -0
- package/src/generate.ts +130 -0
- package/src/index.ts +23 -0
- package/test/basic.test.ts +15 -0
- package/tsconfig.json +10 -0
package/.env.example
ADDED
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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
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();
|
package/src/generate.ts
ADDED
|
@@ -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
|
+
});
|