dotenv-gad 1.0.2 → 1.2.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 CHANGED
@@ -100,7 +100,7 @@ npx dotenv-gad types
100
100
  - Interactive fixes
101
101
  - Strict mode
102
102
  - Custom schema paths
103
- - CI/CD friendly (comming soon!)
103
+ - CI/CD friendly
104
104
 
105
105
  ### Secret Management
106
106
 
@@ -202,4 +202,4 @@ Environment validation failed:
202
202
 
203
203
  MIT © [Kasim Lyee]
204
204
 
205
- [![contribution](https://github.com/kasimlyee/dotenv-gad/blob/main/CONTRIBUTING.md)] are welcome!!
205
+ [Contributions](https://github.com/kasimlyee/dotenv-gad/blob/main/CONTRIBUTING.md) 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("Validating 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,22 @@
1
+ import { Command } from "commander";
2
+ import { loadSchema } from "./utils.js";
3
+ import { writeFileSync } from "fs";
4
+ export default function (program) {
5
+ return new Command("docs")
6
+ .description("Generate Markdown documentation")
7
+ .action(async (program, command) => {
8
+ const schema = await loadSchema(command.parent.opts().schema);
9
+ let md = `# Environment Variables\n\n`;
10
+ Object.entries(schema).forEach(([key, rule]) => {
11
+ md += `## \'${key}\'\n\n`;
12
+ md += `- **Type**: ${rule.type}\n`;
13
+ if (rule.docs)
14
+ md += `- **Description**: ${rule.docs}\n`;
15
+ if (rule.default) {
16
+ md += `- **Default**: \'${rule.default}\'}\n`;
17
+ }
18
+ md += "\n";
19
+ });
20
+ writeFileSync("ENV.md", md);
21
+ });
22
+ }
@@ -0,0 +1,57 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import inquirer from "inquirer";
4
+ import { loadSchema, applyFix } from "./utils.js";
5
+ import { validateEnv } from "../../index.js";
6
+ import { AggregateError } from "../../errors.js";
7
+ import dotenv from "dotenv";
8
+ export default function (program) {
9
+ return new Command("fix")
10
+ .description("Interactively fix environment issues")
11
+ .action(async (options, command) => {
12
+ const rootOpts = command.parent.opts();
13
+ const schema = await loadSchema(rootOpts.schema);
14
+ try {
15
+ // Load the current .env file
16
+ const envPath = rootOpts.env || ".env";
17
+ dotenv.config({ path: envPath });
18
+ // Try to validate - if it succeeds, there are no issues
19
+ validateEnv(schema);
20
+ console.log(chalk.green("✓ No issues found! Environment is valid."));
21
+ return;
22
+ }
23
+ catch (error) {
24
+ if (error instanceof AggregateError) {
25
+ console.log(chalk.yellow(`\nFound ${error.errors.length} issue(s):\n`));
26
+ error.errors.forEach((e) => {
27
+ console.log(` ${chalk.red("✗")} ${chalk.yellow(e.key)}: ${e.message}`);
28
+ if (e.rule?.docs) {
29
+ console.log(chalk.dim(` ${e.rule.docs}`));
30
+ }
31
+ });
32
+ const { confirmed } = await inquirer.prompt({
33
+ type: "confirm",
34
+ name: "confirmed",
35
+ message: `\nWould you like to fix these issues interactively?`,
36
+ default: true,
37
+ });
38
+ if (confirmed) {
39
+ // Convert errors array to issues object format expected by applyFix
40
+ const issues = {};
41
+ error.errors.forEach((e) => {
42
+ issues[e.key] = { value: e.value, key: e.key };
43
+ });
44
+ await applyFix(issues, schema, rootOpts.env || ".env");
45
+ }
46
+ else {
47
+ console.log(chalk.dim("\nFix cancelled."));
48
+ process.exit(1);
49
+ }
50
+ }
51
+ else {
52
+ console.error(chalk.red("Unexpected error:"), error);
53
+ process.exit(2);
54
+ }
55
+ }
56
+ });
57
+ }
@@ -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,87 @@
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
+ function getTypeForRule(rule) {
7
+ switch (rule.type) {
8
+ case "number":
9
+ case "port":
10
+ return "number";
11
+ case "boolean":
12
+ return "boolean";
13
+ case "date":
14
+ return "Date";
15
+ case "array":
16
+ return rule.items ? `${getTypeForRule(rule.items)}[]` : "any[]";
17
+ case "object":
18
+ case "json":
19
+ return "Record<string, any>";
20
+ case "string":
21
+ case "email":
22
+ case "url":
23
+ case "ip":
24
+ default:
25
+ return "string";
26
+ }
27
+ }
28
+ export default function (program) {
29
+ return new Command("types")
30
+ .description("Generate Typescript types")
31
+ .option("--output <file>", "Output file path", "env.d.ts")
32
+ .action(async (options, command) => {
33
+ const rootOpts = command.parent.opts();
34
+ const spinner = ora("Generating type definitions.......").start();
35
+ try {
36
+ const schema = await loadSchema(rootOpts.schema);
37
+ let typeContent = "// Auto-generated by dotenv-gad\n\ndeclare namespace NodeJS{\n interface ProcessEnv{\n";
38
+ Object.entries(schema).forEach(([key, rule]) => {
39
+ const schemaRule = rule;
40
+ let type;
41
+ switch (schemaRule.type) {
42
+ case "number":
43
+ case "port":
44
+ type = "number";
45
+ break;
46
+ case "boolean":
47
+ type = "boolean";
48
+ break;
49
+ case "date":
50
+ type = "Date";
51
+ break;
52
+ case "array":
53
+ if (schemaRule.items) {
54
+ const itemType = getTypeForRule(schemaRule.items);
55
+ type = `${itemType}[]`;
56
+ }
57
+ else {
58
+ type = "any[]";
59
+ }
60
+ break;
61
+ case "object":
62
+ case "json":
63
+ type = "Record<string, any>";
64
+ break;
65
+ case "string":
66
+ case "email":
67
+ case "url":
68
+ case "ip":
69
+ default:
70
+ type = "string";
71
+ }
72
+ if (schemaRule.enum) {
73
+ type = schemaRule.enum.map((v) => JSON.stringify(v)).join(" | ");
74
+ }
75
+ typeContent += ` ${key}${schemaRule.required ? "" : "?"}: ${type};\n`;
76
+ });
77
+ typeContent += " }\n}\n";
78
+ writeFileSync(options.output, typeContent);
79
+ spinner.succeed(chalk.green(`Generated ${options.output} successfully!`));
80
+ }
81
+ catch (error) {
82
+ spinner.fail(chalk.red("Failed to generate type definitions"));
83
+ console.error(error);
84
+ process.exit(1);
85
+ }
86
+ });
87
+ }
@@ -0,0 +1,91 @@
1
+ import { readFileSync, writeFileSync, unlinkSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { transformSync } from "esbuild";
5
+ import Chalk from "chalk";
6
+ import inquirer from "inquirer";
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ /**
9
+ * Loads a schema from a file.
10
+ * @param schemaPath Path to the schema file.
11
+ * @returns The loaded schema.
12
+ * @throws If the schema file is malformed or cannot be loaded.
13
+ */
14
+ export async function loadSchema(schemaPath) {
15
+ const loadTsModule = async (tsFilePath) => {
16
+ const tempFile = join(__dirname, "../../temp-schema.mjs");
17
+ try {
18
+ const tsCode = readFileSync(tsFilePath, "utf-8");
19
+ const { code } = transformSync(tsCode, {
20
+ format: "esm",
21
+ loader: "ts",
22
+ target: "esnext",
23
+ });
24
+ writeFileSync(tempFile, code);
25
+ return (await import(`${tempFile}?t=${Date.now()}`)).default;
26
+ }
27
+ finally {
28
+ unlinkSync(tempFile);
29
+ }
30
+ };
31
+ try {
32
+ if (schemaPath.endsWith(".ts")) {
33
+ return await loadTsModule(schemaPath);
34
+ }
35
+ else if (schemaPath.endsWith(".js")) {
36
+ return (await import(`${schemaPath}?t=${Date.now()}`)).default;
37
+ }
38
+ else if (schemaPath.endsWith(".json")) {
39
+ return JSON.parse(readFileSync(schemaPath, "utf-8"));
40
+ }
41
+ throw new Error(`Unsupported schema format. Use .ts, .js or .json`);
42
+ }
43
+ catch (error) {
44
+ throw new Error(`Failed to load schema from ${schemaPath}: ${error.message}`);
45
+ }
46
+ }
47
+ /**
48
+ * Applies fixes to the given environment file by prompting the user to input
49
+ * values for missing variables and variables that do not match the schema.
50
+ *
51
+ * @param issues An object where each key is the name of an environment variable
52
+ * and the value is an object containing the `value` property (which is the
53
+ * invalid value) and the `key` property (which is the name of the variable).
54
+ * @param schema The schema definition for the environment variables.
55
+ * @param envPath The path to the environment file. Defaults to `.env`.
56
+ * @throws If the schema is malformed or if the user cancels the prompt.
57
+ */
58
+ export async function applyFix(issues, schema, envPath = ".env") {
59
+ const envLines = readFileSync(envPath, "utf-8").split("\n");
60
+ for (const key in issues) {
61
+ if (!Object.prototype.hasOwnProperty.call(issues, key))
62
+ continue;
63
+ const issue = issues[key];
64
+ const rule = schema[key];
65
+ if (!rule) {
66
+ console.error(Chalk.red(`Error: Could not find rule for key ${key} in schema`));
67
+ continue;
68
+ }
69
+ const { value } = await inquirer.prompt({
70
+ type: "input",
71
+ name: "value",
72
+ message: `${Chalk.yellow(key)} (${rule.docs || "No description"})`,
73
+ default: rule.default !== undefined ? String(rule.default) : "",
74
+ validate: (input) => {
75
+ if (rule.required && !input) {
76
+ return "Value is required";
77
+ }
78
+ return true;
79
+ },
80
+ });
81
+ const lineIndex = envLines.findIndex((line) => line.startsWith(`${key}=`));
82
+ if (lineIndex >= 0) {
83
+ envLines[lineIndex] = `${key}=${value}`;
84
+ }
85
+ else {
86
+ envLines.push(`${key}=${value}`);
87
+ }
88
+ }
89
+ writeFileSync(envPath, envLines.join("\n"));
90
+ console.log(Chalk.green(`Updated ${envPath} successfully!`));
91
+ }
package/dist/cli/index.js CHANGED
File without changes
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Compose multiple schema definitions into a single one.
3
+ *
4
+ * @example
5
+ * const dbSchema = {
6
+ * DB_HOST: { type: "string" },
7
+ * DB_USER: { type: "string" },
8
+ * DB_PASSWORD: { type: "string", sensitive: true },
9
+ * };
10
+ *
11
+ * const appSchema = {
12
+ * APP_NAME: { type: "string" },
13
+ * APP_PORT: { type: "number" },
14
+ * };
15
+ *
16
+ * const fullSchema = composeSchema(dbSchema, appSchema);
17
+ *
18
+ * @param {...SchemaDefinition[]} schemas - schema definitions to compose
19
+ * @returns {SchemaDefinition} - a single schema definition with all the properties
20
+ */
21
+ export function composeSchema(...schemas) {
22
+ return Object.assign({}, ...schemas);
23
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,34 @@
1
+ export class EnvValidationError extends Error {
2
+ key;
3
+ message;
4
+ receiveValue;
5
+ constructor(key, message, receiveValue) {
6
+ super(message);
7
+ this.key = key;
8
+ this.message = message;
9
+ this.receiveValue = receiveValue;
10
+ this.name = "EnvValidationError";
11
+ }
12
+ }
13
+ export class AggregateError extends Error {
14
+ errors;
15
+ constructor(errors, message) {
16
+ super(message);
17
+ this.errors = errors;
18
+ this.name = "AggregateError";
19
+ Object.setPrototypeOf(this, AggregateError.prototype);
20
+ }
21
+ toString() {
22
+ const errorList = this.errors
23
+ .map((e) => {
24
+ let msg = ` - ${e.key}: ${e.message}`;
25
+ if (e.value !== undefined)
26
+ msg += ` (received: ${JSON.stringify(e.value)})`;
27
+ if (e.rule?.docs)
28
+ msg += `\n ${e.rule.docs}`;
29
+ return msg;
30
+ })
31
+ .join("\n");
32
+ return `${this.message}:\n${errorList}`;
33
+ }
34
+ }
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
+ }
@@ -3,6 +3,12 @@ export declare class EnvValidator {
3
3
  private schema;
4
4
  private options?;
5
5
  private errors;
6
+ /**
7
+ * Constructs a new EnvValidator instance.
8
+ * @param {SchemaDefinition} schema The schema definition for the environment variables.
9
+ * @param {Object} [options] Optional options for the validation process.
10
+ * @param {boolean} [options.strict] When true, environment variables not present in the schema will be rejected.
11
+ */
6
12
  constructor(schema: SchemaDefinition, options?: {
7
13
  strict?: boolean;
8
14
  } | undefined);
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,194 @@
1
+ import { AggregateError } from "./errors.js";
2
+ export class EnvValidator {
3
+ schema;
4
+ options;
5
+ errors = [];
6
+ /**
7
+ * Constructs a new EnvValidator instance.
8
+ * @param {SchemaDefinition} schema The schema definition for the environment variables.
9
+ * @param {Object} [options] Optional options for the validation process.
10
+ * @param {boolean} [options.strict] When true, environment variables not present in the schema will be rejected.
11
+ */
12
+ constructor(schema, options) {
13
+ this.schema = schema;
14
+ this.options = options;
15
+ }
16
+ validate(env) {
17
+ this.errors = [];
18
+ const result = {};
19
+ for (const [key, rule] of Object.entries(this.schema)) {
20
+ try {
21
+ result[key] = this.validateKey(key, rule, env[key]);
22
+ }
23
+ catch (error) {
24
+ if (error instanceof Error) {
25
+ this.errors.push({
26
+ key,
27
+ message: error.message,
28
+ value: env[key],
29
+ rule,
30
+ });
31
+ }
32
+ }
33
+ }
34
+ if (this.errors.length > 0) {
35
+ const keys = this.errors.map((e) => e.key).join(", ");
36
+ throw new AggregateError(this.errors, `Environment validation failed: ${keys}`);
37
+ }
38
+ if (this.options?.strict) {
39
+ const extraVars = Object.keys(env).filter((k) => !(k in this.schema));
40
+ if (extraVars.length > 0) {
41
+ throw new Error(`Unexpected environment variables: ${extraVars.join(", ")}`);
42
+ }
43
+ }
44
+ return result;
45
+ }
46
+ validateKey(key, rule, value) {
47
+ const effectiveRule = this.getEffectiveRule(key, rule);
48
+ if (value === undefined || value === "") {
49
+ if (effectiveRule.required)
50
+ throw new Error(`Missing required environment variable`);
51
+ return effectiveRule.default;
52
+ }
53
+ if (effectiveRule.transform) {
54
+ value = effectiveRule.transform(value);
55
+ }
56
+ switch (effectiveRule.type) {
57
+ case "string":
58
+ value = String(value).trim();
59
+ if (effectiveRule.minLength && value.length < effectiveRule.minLength) {
60
+ throw new Error(`Environment variable ${key} must be at least ${effectiveRule.minLength} characters`);
61
+ }
62
+ if (effectiveRule.maxLength && value.length > effectiveRule.maxLength) {
63
+ throw new Error(`Environment variable ${key} must be at most ${effectiveRule.maxLength} characters`);
64
+ }
65
+ break;
66
+ case "number":
67
+ value = Number(value);
68
+ if (isNaN(value)) {
69
+ throw new Error(`Environment variable ${key} must be a number`);
70
+ }
71
+ if (effectiveRule.min !== undefined && value < effectiveRule.min) {
72
+ throw new Error(`Environment variable ${key} must be at least ${effectiveRule.min}`);
73
+ }
74
+ if (effectiveRule.max !== undefined && value > effectiveRule.max) {
75
+ throw new Error(`Environment variable ${key} must be at most ${effectiveRule.max}`);
76
+ }
77
+ break;
78
+ case "boolean":
79
+ if (typeof value === "string") {
80
+ value = value.toLowerCase();
81
+ if (value === "true") {
82
+ value = true;
83
+ }
84
+ else if (value === "false") {
85
+ value = false;
86
+ }
87
+ }
88
+ if (typeof value !== "boolean") {
89
+ throw new Error(`Environment variable ${key} must be a boolean (true/false)`);
90
+ }
91
+ value = Boolean(value);
92
+ break;
93
+ case "date":
94
+ const date = new Date(value);
95
+ if (isNaN(date.getTime())) {
96
+ throw new Error(`Environment variable ${key} must be a valid date`);
97
+ }
98
+ value = date;
99
+ break;
100
+ case "url":
101
+ try {
102
+ new URL(String(value));
103
+ }
104
+ catch {
105
+ throw new Error("Must be a valid URL");
106
+ }
107
+ break;
108
+ case "email":
109
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))) {
110
+ throw new Error("Must be a valid email");
111
+ }
112
+ break;
113
+ case "ip":
114
+ if (!/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(String(value))) {
115
+ throw new Error("Must be a valid IP address");
116
+ }
117
+ break;
118
+ case "port":
119
+ const port = Number(value);
120
+ if (isNaN(port))
121
+ throw new Error("Must be a number");
122
+ if (port < 1 || port > 65535) {
123
+ throw new Error("Must be between 1 and 65535");
124
+ }
125
+ break;
126
+ case "json":
127
+ try {
128
+ value = JSON.parse(value);
129
+ }
130
+ catch {
131
+ throw new Error("Must be valid JSON");
132
+ }
133
+ break;
134
+ case "array":
135
+ if (!Array.isArray(value)) {
136
+ try {
137
+ value = JSON.parse(value);
138
+ }
139
+ catch {
140
+ throw new Error("Must be a valid array or JSON array string");
141
+ }
142
+ }
143
+ if (effectiveRule.items) {
144
+ value = value.map((item, i) => {
145
+ try {
146
+ return this.validateKey(`${key}[${i}]`, effectiveRule.items, item);
147
+ }
148
+ catch (error) {
149
+ throw new Error(`Array item '${i}':${error.message}`);
150
+ }
151
+ });
152
+ }
153
+ break;
154
+ case "object":
155
+ if (typeof value === "string") {
156
+ try {
157
+ value = JSON.parse(value);
158
+ }
159
+ catch {
160
+ throw new Error("Must be a valid object or JSON string");
161
+ }
162
+ }
163
+ if (effectiveRule.properties) {
164
+ const obj = {};
165
+ for (const [prop, propRule] of Object.entries(effectiveRule.properties)) {
166
+ try {
167
+ obj[prop] = this.validateKey(`${key}.${prop}`, propRule, value[prop]);
168
+ }
169
+ catch (error) {
170
+ throw new Error(`Property '${prop}':${error.message}`);
171
+ }
172
+ }
173
+ value = obj;
174
+ }
175
+ break;
176
+ }
177
+ if (effectiveRule.enum && !effectiveRule.enum.includes(value)) {
178
+ throw new Error(`Environment variable ${key} must be one of ${effectiveRule.enum.join(", ")}`);
179
+ }
180
+ if (effectiveRule.regex && !effectiveRule.regex.test(String(value))) {
181
+ throw new Error(effectiveRule.regexError ||
182
+ `Environment variable ${key} must match ${effectiveRule.regex}`);
183
+ }
184
+ if (effectiveRule.validate && !effectiveRule.validate(value)) {
185
+ throw new Error(effectiveRule.error || "Custom validation failed");
186
+ }
187
+ return value;
188
+ }
189
+ getEffectiveRule(key, rule) {
190
+ const envName = process.env.NODE_ENV || "development";
191
+ const envRule = rule.env?.[envName] || {};
192
+ return { ...rule, ...envRule };
193
+ }
194
+ }
package/package.json CHANGED
@@ -1,17 +1,24 @@
1
1
  {
2
2
  "name": "dotenv-gad",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "main": "dist/index.js",
5
- "types": "dist/index.d.ts",
5
+ "types": "dist/types/index.d.ts",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "dotenv-guard": "./dist/cli/index.js"
9
9
  },
10
10
  "scripts": {
11
- "build": "tsc && chmod +x ./dist/cli/index.js",
12
- "test": "NODE_OPTIONS=--experimental-vm-modules jest",
11
+ "build": "tsc",
12
+ "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
13
+ "test:watch": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --watch",
14
+ "test:coverage": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage",
13
15
  "dev": "tsc --watch",
14
- "prepublishOnly": "npm run build && npm test"
16
+ "lint": "eslint src --ext .ts",
17
+ "lint:fix": "eslint src --ext .ts --fix",
18
+ "format": "prettier --write \"src/**/*.ts\" \"__tests__/**/*.ts\"",
19
+ "format:check": "prettier --check \"src/**/*.ts\" \"__tests__/**/*.ts\"",
20
+ "prepublishOnly": "npm run build && npm test",
21
+ "prepare": "husky"
15
22
  },
16
23
  "keywords": [
17
24
  "dotenv",
@@ -19,16 +26,25 @@
19
26
  "validation",
20
27
  "typescript",
21
28
  "schema",
22
- "configuration"
29
+ "configuration",
30
+ "env",
31
+ "config",
32
+ "type-safe",
33
+ "nodejs",
34
+ "cli"
23
35
  ],
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
24
39
  "exports": {
25
40
  ".": {
26
41
  "import": "./dist/index.js",
27
- "types": "./dist/index.d.ts"
42
+ "types": "./dist/types/index.d.ts"
28
43
  },
29
44
  "./package.json": "./package.json"
30
45
  },
31
46
  "files": [
47
+ "dist/**/*.js",
32
48
  "dist/**/*.d.ts",
33
49
  "README.md",
34
50
  "LICENSE"
@@ -45,19 +61,34 @@
45
61
  },
46
62
  "homepage": "https://github.com/kasimlyee/dotenv-gad#readme",
47
63
  "devDependencies": {
64
+ "@eslint/js": "^9.17.0",
48
65
  "@types/chalk": "^0.4.31",
49
66
  "@types/commander": "^2.12.0",
50
67
  "@types/dotenv": "^6.1.1",
68
+ "@types/eslint__js": "^8.42.3",
51
69
  "@types/figlet": "^1.7.0",
52
70
  "@types/inquirer": "^9.0.8",
53
71
  "@types/jest": "^30.0.0",
54
72
  "@types/node": "^24.0.3",
55
73
  "@types/ora": "^3.1.0",
74
+ "cross-env": "^7.0.3",
75
+ "cross-env-shell": "^7.0.3",
76
+ "esbuild": "^0.25.5",
77
+ "eslint": "^9.17.0",
78
+ "globals": "^15.14.0",
79
+ "husky": "^9.1.7",
80
+ "jest": "^30.0.3",
56
81
  "jest-environment-jsdom": "^30.0.2",
57
- "ts-jest": "^29.4.0"
82
+ "prettier": "^3.4.2",
83
+ "ts-jest": "^29.4.0",
84
+ "typescript-eslint": "^8.19.1"
58
85
  },
59
86
  "dependencies": {
87
+ "chalk": "^5.3.0",
88
+ "commander": "^12.1.0",
60
89
  "dotenv": "^16.5.0",
61
- "esbuild": "^0.25.5"
90
+ "figlet": "^1.8.0",
91
+ "inquirer": "^12.3.0",
92
+ "ora": "^8.1.1"
62
93
  }
63
94
  }