dotenv-gad 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -0
- package/dist/cli/commands/check.js +41 -0
- package/dist/cli/commands/init.js +61 -0
- package/dist/cli/commands/sync.js +37 -0
- package/dist/cli/commands/types.js +40 -0
- package/dist/cli/commands/utils.js +40 -0
- package/dist/cli/index.js +34 -0
- package/dist/errors.js +22 -0
- package/dist/index.js +11 -0
- package/dist/schema.js +10 -0
- package/dist/utils.js +36 -0
- package/dist/validator.js +180 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# dotenv-gad
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/dotenv-guard)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
**dotenv-gad** is an environment variable validation tool that brings type safety and schema validation to your Node.js and JavaScript applications. It extends `dotenv` with features like:
|
|
7
|
+
|
|
8
|
+
- Type-safe environment variables
|
|
9
|
+
- Schema validation
|
|
10
|
+
- Automatic documentation generation
|
|
11
|
+
- TypeScript support
|
|
12
|
+
- CLI tooling
|
|
13
|
+
- Secret management
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install dotenv-gad
|
|
19
|
+
# or
|
|
20
|
+
yarn add dotenv-gad
|
|
21
|
+
# or
|
|
22
|
+
pnpm add dotenv-gad
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
1. Create a schema file (`env.schema.ts`):
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { defineSchema } from "dotenv-gad";
|
|
31
|
+
|
|
32
|
+
export default defineSchema({
|
|
33
|
+
PORT: {
|
|
34
|
+
type: "number",
|
|
35
|
+
default: 3000,
|
|
36
|
+
docs: "Port to run the server on",
|
|
37
|
+
},
|
|
38
|
+
DATABASE_URL: {
|
|
39
|
+
type: "string",
|
|
40
|
+
required: true,
|
|
41
|
+
sensitive: true,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2. Validate your environment:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { loadEnv } from "dotenv-gad";
|
|
50
|
+
import schema from "./env.schema";
|
|
51
|
+
|
|
52
|
+
const env = loadEnv(schema);
|
|
53
|
+
console.log(`Server running on port ${env.PORT}`);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## CLI Commands
|
|
57
|
+
|
|
58
|
+
| Command | Description |
|
|
59
|
+
| ------- | ---------------------------------- |
|
|
60
|
+
| `check` | Validate .env against schema |
|
|
61
|
+
| `sync` | Generate/update .env.example |
|
|
62
|
+
| `types` | Generate env.d.ts TypeScript types |
|
|
63
|
+
| `init` | Create starter schema |
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx dotenv-gad check
|
|
67
|
+
npx dotenv-gad types
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Features
|
|
71
|
+
|
|
72
|
+
### Core Validation
|
|
73
|
+
|
|
74
|
+
- Type checking (string, number, boolean, array, object)
|
|
75
|
+
- Required/optional fields
|
|
76
|
+
- Default values
|
|
77
|
+
- Custom validation functions
|
|
78
|
+
- Environment-specific rules
|
|
79
|
+
|
|
80
|
+
### Advanced Types
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
{
|
|
84
|
+
API_URL: { type: 'url' },
|
|
85
|
+
EMAIL: { type: 'email' },
|
|
86
|
+
CONFIG: { type: 'json' },
|
|
87
|
+
TAGS: {
|
|
88
|
+
type: 'array',
|
|
89
|
+
items: { type: 'string' }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### CLI Features
|
|
95
|
+
|
|
96
|
+
- Color-coded output
|
|
97
|
+
- Interactive fixes
|
|
98
|
+
- Strict mode
|
|
99
|
+
- Custom schema paths
|
|
100
|
+
- CI/CD friendly (comming soon!)
|
|
101
|
+
|
|
102
|
+
### Secret Management
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
{
|
|
106
|
+
API_KEY: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
sensitive: true, // Excluded from .env.example
|
|
109
|
+
validate: (val) => val.startsWith('sk_')
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Framework Integrations
|
|
115
|
+
|
|
116
|
+
### Express.js
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import express from "express";
|
|
120
|
+
import { loadEnv } from "dotenv-gad";
|
|
121
|
+
import schema from "./env.schema";
|
|
122
|
+
|
|
123
|
+
const env = loadEnv(schema);
|
|
124
|
+
const app = express();
|
|
125
|
+
|
|
126
|
+
app.listen(env.PORT, () => {
|
|
127
|
+
console.log(`Server running on port ${env.PORT}`);
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Next.js
|
|
132
|
+
|
|
133
|
+
Create `next.config.js`:
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
const { loadEnv } = require("dotenv-gad");
|
|
137
|
+
const schema = require("./env.schema");
|
|
138
|
+
|
|
139
|
+
const env = loadEnv(schema);
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
env: {
|
|
143
|
+
API_URL: env.API_URL,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Validation Reports
|
|
149
|
+
|
|
150
|
+
Example error output:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
Environment validation failed:
|
|
154
|
+
- DATABASE_URL: Missing required environment variable
|
|
155
|
+
- PORT: Must be a number (received: "abc")
|
|
156
|
+
- API_KEY: Must start with 'sk_' (received: "invalid")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## more usages
|
|
160
|
+
|
|
161
|
+
### Environment-Specific Rules
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
{
|
|
165
|
+
DEBUG: {
|
|
166
|
+
type: 'boolean',
|
|
167
|
+
env: {
|
|
168
|
+
development: { default: true },
|
|
169
|
+
production: { default: false }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Custom Validators
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
{
|
|
179
|
+
PASSWORD: {
|
|
180
|
+
type: 'string',
|
|
181
|
+
validate: (val) => val.length >= 8,
|
|
182
|
+
error: 'Password must be at least 8 characters'
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Transformations
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
{
|
|
191
|
+
FEATURES: {
|
|
192
|
+
type: 'array',
|
|
193
|
+
transform: (val) => val.split(',')
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## 📜 License
|
|
199
|
+
|
|
200
|
+
MIT © [Kasim Lyee]
|
|
201
|
+
|
|
202
|
+
Contributions are welcome!!
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { validateEnv } from "../../index.js";
|
|
5
|
+
import { AggregateError } from "../../errors.js";
|
|
6
|
+
import { loadSchema } from "./utils.js";
|
|
7
|
+
export default function (program) {
|
|
8
|
+
return new Command("check")
|
|
9
|
+
.description("Validate .env against schema")
|
|
10
|
+
.option("--strict", "Fail on extra env vars not in schema")
|
|
11
|
+
.option("--fix", "Attempt to fix errors interactively")
|
|
12
|
+
.action(async (option, command) => {
|
|
13
|
+
const rootOpts = command.parent.opts();
|
|
14
|
+
const spinner = ora("Validatng environment.......").start();
|
|
15
|
+
try {
|
|
16
|
+
const schema = await loadSchema(rootOpts.schema);
|
|
17
|
+
const env = validateEnv(schema, {
|
|
18
|
+
strict: option.strict,
|
|
19
|
+
});
|
|
20
|
+
spinner.succeed(chalk.green("Environment validation passed!"));
|
|
21
|
+
console.log(chalk.dim(`Found ${Object.keys(env).length} valid variables`));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
spinner.stop();
|
|
25
|
+
if (error instanceof AggregateError) {
|
|
26
|
+
console.error(chalk.red("\nEnvironment validation failed:"));
|
|
27
|
+
error.errors.forEach((e) => {
|
|
28
|
+
console.log(` ${chalk.yellow(e.key)}: ${e.message}`);
|
|
29
|
+
if (e.rule?.docs) {
|
|
30
|
+
console.log(chalk.dim(` ${e.rule.docs}`));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
console.error(chalk.red("Unexpected error:"), error);
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { writeFileSync, existsSync } from "fs";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
import { dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
export default function (program) {
|
|
10
|
+
return new Command("init")
|
|
11
|
+
.description("Initialize new schema file")
|
|
12
|
+
.option("--force", "Overwrite existing files")
|
|
13
|
+
.action(async (options, command) => {
|
|
14
|
+
const rootOpts = command.parent.opts();
|
|
15
|
+
const schemaPath = rootOpts.schema;
|
|
16
|
+
if (existsSync(schemaPath)) {
|
|
17
|
+
if (!options.force) {
|
|
18
|
+
const { overwrite } = await inquirer.prompt({
|
|
19
|
+
type: "confirm",
|
|
20
|
+
name: "overwrite",
|
|
21
|
+
message: "Schema file already exists. Overwrite?",
|
|
22
|
+
default: false,
|
|
23
|
+
});
|
|
24
|
+
if (!overwrite)
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const spinner = ora("Creating new schema...").start();
|
|
29
|
+
try {
|
|
30
|
+
const template = `import { defineSchema } from 'dotenv-gad';
|
|
31
|
+
|
|
32
|
+
export default defineSchema({
|
|
33
|
+
// Add your environment variables here
|
|
34
|
+
PORT: {
|
|
35
|
+
type: 'number',
|
|
36
|
+
default: 3000,
|
|
37
|
+
docs: 'Port to run the server on'
|
|
38
|
+
},
|
|
39
|
+
NODE_ENV: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
enum: ['development', 'production', 'test'],
|
|
42
|
+
default: 'development'
|
|
43
|
+
},
|
|
44
|
+
DB_URL: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
required: true,
|
|
47
|
+
docs: 'Database connection URL'
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
`;
|
|
51
|
+
writeFileSync(schemaPath, template);
|
|
52
|
+
spinner.succeed(chalk.green(`Created ${schemaPath} successfully!`));
|
|
53
|
+
console.log(chalk.dim("\nEdit this file to define your environment schema"));
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
spinner.fail(chalk.red("Failed to create schema file"));
|
|
57
|
+
console.error(error);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { writeFileSync } from "fs";
|
|
5
|
+
import { loadSchema } from "./utils.js";
|
|
6
|
+
export default function (program) {
|
|
7
|
+
return new Command("sync")
|
|
8
|
+
.description("Genearte/update .env.example file")
|
|
9
|
+
.option("--output <file>", "Output file path", ".env.example")
|
|
10
|
+
.action(async (options, command) => {
|
|
11
|
+
const rootOpts = command.parent.opts();
|
|
12
|
+
const spinner = ora("Generating .env.example.......").start();
|
|
13
|
+
try {
|
|
14
|
+
const schema = await loadSchema(rootOpts.schema);
|
|
15
|
+
let exampleContent = "# Auto-generated by dotenv-gad\n\n";
|
|
16
|
+
Object.entries(schema).forEach(([key, rule]) => {
|
|
17
|
+
if (rule.sensitive)
|
|
18
|
+
return;
|
|
19
|
+
exampleContent += `# ${rule.docs || "No description available"}\n`;
|
|
20
|
+
exampleContent += `# Type: ${rule.type}\n`;
|
|
21
|
+
if (rule.default !== undefined) {
|
|
22
|
+
exampleContent += `# Default: ${JSON.stringify(rule.default)}\n`;
|
|
23
|
+
}
|
|
24
|
+
exampleContent += `${key}=${rule.default
|
|
25
|
+
? JSON.stringify(rule.default)
|
|
26
|
+
: ""}\n\n`;
|
|
27
|
+
});
|
|
28
|
+
writeFileSync(options.output, exampleContent.trim());
|
|
29
|
+
spinner.succeed(chalk.green(`Generated ${options.output} successfully!`));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
spinner.fail(chalk.red(`Failed to generate .env.example`));
|
|
33
|
+
console.error(error);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { writeFileSync } from "fs";
|
|
5
|
+
import { loadSchema } from "./utils.js";
|
|
6
|
+
export default function (program) {
|
|
7
|
+
return new Command("types")
|
|
8
|
+
.description("Generate Typescript types")
|
|
9
|
+
.option("--output <file>", "Output file path", "env.d.ts")
|
|
10
|
+
.action(async (options, command) => {
|
|
11
|
+
const rootOpts = command.parent.opts();
|
|
12
|
+
const spinner = ora("Generating type definitions.......").start();
|
|
13
|
+
try {
|
|
14
|
+
const schema = await loadSchema(rootOpts.schema);
|
|
15
|
+
let typeContent = "// Auto-generated by dotenv-gad\n\ndeclare namespace NodeJS{\n interface ProcessEnv{\n";
|
|
16
|
+
Object.entries(schema).forEach(([key, rule]) => {
|
|
17
|
+
let type;
|
|
18
|
+
switch (rule.type) {
|
|
19
|
+
case "number":
|
|
20
|
+
type = "number";
|
|
21
|
+
break;
|
|
22
|
+
case "boolean":
|
|
23
|
+
type = "boolean";
|
|
24
|
+
break;
|
|
25
|
+
default:
|
|
26
|
+
type = "string";
|
|
27
|
+
}
|
|
28
|
+
typeContent += ` ${key}${rule.required ? "" : "?"}:${type};\n`;
|
|
29
|
+
});
|
|
30
|
+
typeContent += " }\n}\n";
|
|
31
|
+
writeFileSync(options.output, typeContent);
|
|
32
|
+
spinner.succeed(chalk.green(`Generated ${options.output} successfully!`));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
spinner.fail(chalk.red("Failed to generate type definitions"));
|
|
36
|
+
console.error(error);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { transformSync } from "esbuild";
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
export async function loadSchema(schemaPath) {
|
|
7
|
+
try {
|
|
8
|
+
if (schemaPath.endsWith(".ts")) {
|
|
9
|
+
const code = readFileSync(schemaPath, "utf-8");
|
|
10
|
+
const result = transformSync(code, {
|
|
11
|
+
format: "esm",
|
|
12
|
+
loader: "ts",
|
|
13
|
+
target: "esnext",
|
|
14
|
+
});
|
|
15
|
+
// Create temporary file
|
|
16
|
+
const tempFile = join(__dirname, "../../temp-schema.mjs");
|
|
17
|
+
writeFileSync(tempFile, result.code);
|
|
18
|
+
try {
|
|
19
|
+
// Import with query string to bust cache
|
|
20
|
+
const module = await import(`${tempFile}?t=${Date.now()}`);
|
|
21
|
+
return module.default;
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
// Clean up temp file
|
|
25
|
+
unlinkSync(tempFile);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (schemaPath.endsWith(".js")) {
|
|
29
|
+
const module = await import(schemaPath);
|
|
30
|
+
return module.default;
|
|
31
|
+
}
|
|
32
|
+
else if (schemaPath.endsWith(".json")) {
|
|
33
|
+
return JSON.parse(readFileSync(schemaPath, "utf-8"));
|
|
34
|
+
}
|
|
35
|
+
throw new Error("Unsupported schema format. Use .ts, .js or .json");
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new Error(`Failed to load schema: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import figlet from "figlet";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { dirname, join } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import checkCommand from "./commands/check.js";
|
|
9
|
+
import syncCommand from "./commands/sync.js";
|
|
10
|
+
import typesCommand from "./commands/types.js";
|
|
11
|
+
import initCommand from "./commands/init.js";
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf-8"));
|
|
14
|
+
export function createCLI() {
|
|
15
|
+
const program = new Command();
|
|
16
|
+
program
|
|
17
|
+
.version(pkg.version)
|
|
18
|
+
.description(chalk.green(figlet.textSync("dotenv-gad", {
|
|
19
|
+
font: "Standard",
|
|
20
|
+
horizontalLayout: "fitted",
|
|
21
|
+
})))
|
|
22
|
+
.option("--debug", "Enable debug output")
|
|
23
|
+
.option("--env <file>", "Specify env file path", ".env")
|
|
24
|
+
.option("--schema <file>", "Specify schema file path", "env.schema.ts");
|
|
25
|
+
const commands = [checkCommand, syncCommand, typesCommand, initCommand];
|
|
26
|
+
commands.forEach((command) => {
|
|
27
|
+
const cmd = command(program);
|
|
28
|
+
program.addCommand(cmd);
|
|
29
|
+
});
|
|
30
|
+
return program;
|
|
31
|
+
}
|
|
32
|
+
const program = createCLI();
|
|
33
|
+
program.parse(process.argv);
|
|
34
|
+
export default program;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class AggregateError extends Error {
|
|
2
|
+
errors;
|
|
3
|
+
constructor(errors, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.errors = errors;
|
|
6
|
+
this.name = "AggregateError";
|
|
7
|
+
Object.setPrototypeOf(this, AggregateError.prototype);
|
|
8
|
+
}
|
|
9
|
+
toString() {
|
|
10
|
+
const errorList = this.errors
|
|
11
|
+
.map((e) => {
|
|
12
|
+
let msg = ` - ${e.key}:${e.message}`;
|
|
13
|
+
if (e.value !== undefined)
|
|
14
|
+
msg += ` (reaceived: ${JSON.stringify(e.value)})`;
|
|
15
|
+
if (e.rule?.docs)
|
|
16
|
+
msg += `\n (${e.rule.docs})`;
|
|
17
|
+
return msg;
|
|
18
|
+
})
|
|
19
|
+
.join("\n");
|
|
20
|
+
return `${this.message}:\n${errorList}`;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EnvValidator } from "./validator.js";
|
|
2
|
+
import { defineSchema } from "./schema.js";
|
|
3
|
+
import { AggregateError } from "./errors.js";
|
|
4
|
+
import { loadEnv, createEnvProxy } from "./utils.js";
|
|
5
|
+
import dotenv from "dotenv";
|
|
6
|
+
export { defineSchema, AggregateError, EnvValidator, loadEnv, createEnvProxy };
|
|
7
|
+
export function validateEnv(schema, options) {
|
|
8
|
+
const env = dotenv.config().parsed || {};
|
|
9
|
+
const validator = new EnvValidator(schema, options);
|
|
10
|
+
return validator.validate(env);
|
|
11
|
+
}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Define a schema for a set of environment variables.
|
|
3
|
+
*
|
|
4
|
+
* @param schema A record where each key is the name of an environment variable
|
|
5
|
+
* and the value is a `SchemaRule` object that defines the rules for that
|
|
6
|
+
* variable.
|
|
7
|
+
*/
|
|
8
|
+
export function defineSchema(schema) {
|
|
9
|
+
return schema;
|
|
10
|
+
}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import { EnvValidator } from "./validator.js";
|
|
3
|
+
/**
|
|
4
|
+
* Load the environment variables from a .env file, validate them against the schema
|
|
5
|
+
* and return an object with the validated values.
|
|
6
|
+
*
|
|
7
|
+
* @param schema The schema definition for the environment variables.
|
|
8
|
+
* @param options Options for the validation process.
|
|
9
|
+
*
|
|
10
|
+
* @returns A validated object with the environment variables.
|
|
11
|
+
*/
|
|
12
|
+
export function loadEnv(schema, options) {
|
|
13
|
+
const env = dotenv.config().parsed || {};
|
|
14
|
+
const validator = new EnvValidator(schema, options);
|
|
15
|
+
return validator.validate(env);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a proxy around the validated environment variables. The proxy will
|
|
19
|
+
* throw an error if you try to access a variable that is not validated.
|
|
20
|
+
*
|
|
21
|
+
* @param validatedEnv The validated environment variables.
|
|
22
|
+
*
|
|
23
|
+
* @returns A proxy object that throws an error if you access an
|
|
24
|
+
* unvalidated variable.
|
|
25
|
+
*/
|
|
26
|
+
export function createEnvProxy(validatedEnv) {
|
|
27
|
+
return new Proxy(validatedEnv, {
|
|
28
|
+
get(target, prop) {
|
|
29
|
+
const value = target[prop];
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
throw new Error(`Environment variable ${String(prop)} is not validated`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { AggregateError } from "./errors.js";
|
|
2
|
+
export class EnvValidator {
|
|
3
|
+
schema;
|
|
4
|
+
options;
|
|
5
|
+
errors = [];
|
|
6
|
+
constructor(schema, options) {
|
|
7
|
+
this.schema = schema;
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
validate(env) {
|
|
11
|
+
this.errors = [];
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const [key, rule] of Object.entries(this.schema)) {
|
|
14
|
+
try {
|
|
15
|
+
result[key] = this.validateKey(key, rule, env[key]);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error instanceof Error) {
|
|
19
|
+
this.errors.push({
|
|
20
|
+
key,
|
|
21
|
+
message: error.message,
|
|
22
|
+
value: env[key],
|
|
23
|
+
rule,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (this.errors.length > 0) {
|
|
29
|
+
const keys = this.errors.map((e) => e.key).join(", ");
|
|
30
|
+
throw new AggregateError(this.errors, `Environment validation failed: ${keys}`);
|
|
31
|
+
}
|
|
32
|
+
if (this.options?.strict) {
|
|
33
|
+
const extraVars = Object.keys(env).filter((k) => !(k in this.schema));
|
|
34
|
+
if (extraVars.length > 0) {
|
|
35
|
+
throw new Error(`Unexpected environment variables: ${extraVars.join(", ")}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
validateKey(key, rule, value) {
|
|
41
|
+
const effectiveRule = this.getEffectiveRule(key, rule);
|
|
42
|
+
if (value === undefined) {
|
|
43
|
+
if (effectiveRule.required)
|
|
44
|
+
throw new Error(`Missing required environment variable`);
|
|
45
|
+
return effectiveRule.default;
|
|
46
|
+
}
|
|
47
|
+
if (effectiveRule.transform) {
|
|
48
|
+
value = effectiveRule.transform(value);
|
|
49
|
+
}
|
|
50
|
+
switch (effectiveRule.type) {
|
|
51
|
+
case "string":
|
|
52
|
+
value = String(value).trim();
|
|
53
|
+
if (effectiveRule.minLength && value.length < effectiveRule.minLength) {
|
|
54
|
+
throw new Error(`Environment variable ${key} must be at least ${effectiveRule.minLength} characters`);
|
|
55
|
+
}
|
|
56
|
+
if (effectiveRule.maxLength && value.length > effectiveRule.maxLength) {
|
|
57
|
+
throw new Error(`Environment variable ${key} must be at most ${effectiveRule.maxLength} characters`);
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
case "number":
|
|
61
|
+
value = Number(value);
|
|
62
|
+
if (isNaN(value)) {
|
|
63
|
+
throw new Error(`Environment variable ${key} must be a number`);
|
|
64
|
+
}
|
|
65
|
+
if (effectiveRule.min !== undefined && value < effectiveRule.min) {
|
|
66
|
+
throw new Error(`Environment variable ${key} must be at least ${effectiveRule.min}`);
|
|
67
|
+
}
|
|
68
|
+
if (effectiveRule.max !== undefined && value > effectiveRule.max) {
|
|
69
|
+
throw new Error(`Environment variable ${key} must be at most ${effectiveRule.max}`);
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
case "boolean":
|
|
73
|
+
if (typeof value === "string") {
|
|
74
|
+
value = value.toLowerCase();
|
|
75
|
+
if (value === "true") {
|
|
76
|
+
value = true;
|
|
77
|
+
}
|
|
78
|
+
else if (value === "false") {
|
|
79
|
+
value = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (typeof value !== "boolean") {
|
|
83
|
+
throw new Error(`Environment variable ${key} must be a boolean (true/false)`);
|
|
84
|
+
}
|
|
85
|
+
value = Boolean(value);
|
|
86
|
+
break;
|
|
87
|
+
case "url":
|
|
88
|
+
try {
|
|
89
|
+
new URL(String(value));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new Error("Must be a valid URL");
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case "email":
|
|
96
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))) {
|
|
97
|
+
throw new Error("Must be a valid email");
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case "ip":
|
|
101
|
+
if (!/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(String(value))) {
|
|
102
|
+
throw new Error("Must be a valid IP address");
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case "port":
|
|
106
|
+
const port = Number(value);
|
|
107
|
+
if (isNaN(port))
|
|
108
|
+
throw new Error("Must be a number");
|
|
109
|
+
if (port < 1 || port > 65535) {
|
|
110
|
+
throw new Error("Must be between 1 and 65535");
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
case "json":
|
|
114
|
+
try {
|
|
115
|
+
value = JSON.parse(value);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw new Error("Must be valid JSON");
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
case "array":
|
|
122
|
+
if (!Array.isArray(value)) {
|
|
123
|
+
try {
|
|
124
|
+
value = JSON.parse(value);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new Error("Must be a valid array or JSON array string");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (effectiveRule.items) {
|
|
131
|
+
value = value.map((item, i) => {
|
|
132
|
+
try {
|
|
133
|
+
return this.validateKey(`${key}[${i}]`, effectiveRule.items, item);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
throw new Error(`Array item '${i}':${error.message}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
case "object":
|
|
142
|
+
if (typeof value === "string") {
|
|
143
|
+
try {
|
|
144
|
+
value = JSON.parse(value);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
throw new Error("Must be a valid object or JSON string");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (effectiveRule.properties) {
|
|
151
|
+
const obj = {};
|
|
152
|
+
for (const [prop, propRule] of Object.entries(effectiveRule.properties)) {
|
|
153
|
+
try {
|
|
154
|
+
obj[prop] = this.validateKey(`${key}.${prop}`, propRule, value[prop]);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
throw new Error(`Property '${prop}':${error.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
value = obj;
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
if (effectiveRule.enum && !effectiveRule.enum.includes(value)) {
|
|
165
|
+
throw new Error(`Environment variable ${key} must be one of ${effectiveRule.enum.join(", ")}`);
|
|
166
|
+
}
|
|
167
|
+
if (effectiveRule.regex && !effectiveRule.regex.test(value)) {
|
|
168
|
+
throw new Error(`Environment variable ${key} must match ${effectiveRule.regex}`);
|
|
169
|
+
}
|
|
170
|
+
if (effectiveRule.validate && !effectiveRule.validate(value)) {
|
|
171
|
+
throw new Error(effectiveRule.error || "Custom validation failed");
|
|
172
|
+
}
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
getEffectiveRule(key, rule) {
|
|
176
|
+
const envName = process.env.NODE_ENV || "development";
|
|
177
|
+
const envRule = rule.env?.[envName] || {};
|
|
178
|
+
return { ...rule, ...envRule };
|
|
179
|
+
}
|
|
180
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dotenv-gad",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"dotenv-guard": "./dist/cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc && chmod +x ./dist/cli/index.js",
|
|
12
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"prepublishOnly": "npm run build && npm test"
|
|
15
|
+
},
|
|
16
|
+
"jest": {
|
|
17
|
+
"extensionsToTreatAsEsm": [
|
|
18
|
+
".ts"
|
|
19
|
+
],
|
|
20
|
+
"transform": {}
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"dotenv",
|
|
24
|
+
"environment",
|
|
25
|
+
"validation",
|
|
26
|
+
"typescript",
|
|
27
|
+
"schema",
|
|
28
|
+
"configuration"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist/**/*",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"author": "Kasim Lyee",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"description": "Environment variable validation and type safety for Node.js and modern JavaScript applications",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/kasimlyee/dotenv-gad.git"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/kasimlyee/dotenv-gad/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/kasimlyee/dotenv-gad#readme",
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/chalk": "^0.4.31",
|
|
55
|
+
"@types/commander": "^2.12.0",
|
|
56
|
+
"@types/dotenv": "^6.1.1",
|
|
57
|
+
"@types/figlet": "^1.7.0",
|
|
58
|
+
"@types/inquirer": "^9.0.8",
|
|
59
|
+
"@types/jest": "^30.0.0",
|
|
60
|
+
"@types/node": "^24.0.3",
|
|
61
|
+
"@types/ora": "^3.1.0",
|
|
62
|
+
"jest-environment-jsdom": "^30.0.2",
|
|
63
|
+
"ts-jest": "^29.4.0"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"esbuild": "^0.25.5"
|
|
67
|
+
}
|
|
68
|
+
}
|