UNPKG

env-schema-checker

Version:

A zero-config, type-safe, schema-driven .env validator with automatic error reporting, autocomplete, and runtime checks

222 lines (221 loc) 7.19 kB
#!/usr/bin/env node import { Command } from 'commander'; import { config } from 'dotenv'; import { z } from 'zod'; import fs from 'fs'; import path from 'path'; function createZodSchema(schema) { const zodSchema = {}; for (const [key, value] of Object.entries(schema)) { if (value instanceof z.ZodType) { zodSchema[key] = value; } else if (Array.isArray(value)) { zodSchema[key] = z.enum(value); } else { switch (value) { case "string": zodSchema[key] = z.string(); break; case "number": zodSchema[key] = z.string().refine((val) => !isNaN(Number(val)), { message: "Invalid number format" }).transform((val) => Number(val)); break; case "boolean": zodSchema[key] = z.enum(["true", "false"]).transform((val) => val === "true"); break; case "array": zodSchema[key] = z.string().refine((val) => { try { const parsed = JSON.parse(val); return Array.isArray(parsed); } catch (e) { return val.includes(","); } }, { message: "Must be a JSON array or comma-separated values" }).transform((val) => { try { const parsed = JSON.parse(val); if (Array.isArray(parsed)) return parsed; } catch (e) { } return val.split(","); }); break; case "object": zodSchema[key] = z.string().transform((val) => { const parsed = JSON.parse(val); if (typeof parsed !== "object" || Array.isArray(parsed) || parsed === null) { throw new Error("Must be a JSON object"); } return parsed; }); break; default: zodSchema[key] = z.string(); } } } return z.object(zodSchema); } function loadEnv(options) { const { schema, path: path2, encoding, override, debug } = options; if (path2) { try { const result = config({ path: path2, encoding, override, debug }); if (result.error) { throw result.error; } } catch (error) { const nodeError = error; if (nodeError.code !== "ENOENT") { throw error; } } } const zodSchema = createZodSchema(schema); const validationResult = zodSchema.safeParse(process.env); if (!validationResult.success) { const errors = validationResult.error.errors.map((err) => ({ key: err.path.join("."), message: err.message, value: process.env[err.path[0]] })); return { success: false, errors, env: {} }; } return { success: true, env: validationResult.data }; } function generateTypes(options) { const { schema} = options; let typeDefinitions = "declare global {\n namespace NodeJS {\n interface ProcessEnv {\n"; for (const [key, value] of Object.entries(schema)) { let type; if (value instanceof z.ZodType) { type = "string"; } else if (Array.isArray(value)) { type = value.map((v) => `'${v}'`).join(" | "); } else { switch (value) { case "number": type = "number"; break; case "boolean": type = "boolean"; break; case "array": type = "string[]"; break; case "object": type = "Record<string, unknown>"; break; default: type = "string"; } } typeDefinitions += ` ${key}: ${type}; `; } typeDefinitions += " }\n }\n}\n\nexport {};"; return typeDefinitions; } var program = new Command(); program.name("env-schema-checker").description("A zero-config, type-safe, schema-driven .env validator").version("0.1.1"); program.command("validate").description("Validate environment variables against a schema").option("-p, --path <path>", "Path to .env file").option("-s, --schema <path>", "Path to schema file (JSON)").action(async (options) => { var _a; try { const schemaPath = options.schema || ".env.schema.json"; if (!fs.existsSync(schemaPath)) { console.error(`Schema file not found: ${schemaPath}`); process.exit(1); } const schema = JSON.parse(fs.readFileSync(schemaPath, "utf-8")); const result = loadEnv({ schema, path: options.path || ".env" }); if (!result.success) { console.error("Environment validation failed:"); (_a = result.errors) == null ? void 0 : _a.forEach((err) => { console.error(` ${err.key}: ${err.message} (received: ${err.value})`); }); process.exit(1); } console.log("\u2705 Environment validation successful!"); } catch (error) { if (error instanceof Error) { console.error("Error:", error.message); } else { console.error("An unknown error occurred"); } process.exit(1); } }); program.command("gen-types").description("Generate TypeScript type definitions from schema").option("-s, --schema <path>", "Path to schema file (JSON)").option("-o, --output <path>", "Output path for type definitions").action(async (options) => { try { const schemaPath = options.schema || ".env.schema.json"; if (!fs.existsSync(schemaPath)) { console.error(`Schema file not found: ${schemaPath}`); process.exit(1); } const schema = JSON.parse(fs.readFileSync(schemaPath, "utf-8")); const types = generateTypes({ schema, outputPath: options.output }); if (options.output) { const outputDir = path.dirname(options.output); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } fs.writeFileSync(options.output, types); console.log(`\u2705 Type definitions written to ${options.output}`); } else { console.log(types); } } catch (error) { if (error instanceof Error) { console.error("Error:", error.message); } else { console.error("An unknown error occurred"); } process.exit(1); } }); program.command("init").description("Initialize a new schema file").option("-o, --output <path>", "Output path for schema file", ".env.schema.json").action(async (options) => { try { const defaultSchema = { NODE_ENV: ["development", "production", "test"], PORT: "number", HOST: "string", DEBUG: "boolean" }; const outputDir = path.dirname(options.output); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } fs.writeFileSync( options.output, JSON.stringify(defaultSchema, null, 2) + "\n" ); console.log(`\u2705 Schema file created at ${options.output}`); console.log("\nExample usage:"); console.log(" $ env-schema-checker validate"); console.log(" $ env-schema-checker gen-types -o types/env.d.ts"); } catch (error) { if (error instanceof Error) { console.error("Error:", error.message); } else { console.error("An unknown error occurred"); } process.exit(1); } }); program.parse(process.argv); //# sourceMappingURL=cli.mjs.map //# sourceMappingURL=cli.mjs.map