@safe-hand/safe-env-check 1.0.1

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 ADDED
@@ -0,0 +1,53 @@
1
+ # safe-env-check
2
+
3
+ A tiny TypeScript library to validate environment variables with schema, strict mode, dotenv and CLI support.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install safe-env-check
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ import { validateEnv } from "safe-env-check";
15
+
16
+ const env = validateEnv({
17
+ PORT: { type: "number", required: true },
18
+ JWT_SECRET: { type: "string", required: true },
19
+ NODE_ENV: {
20
+ type: "enum",
21
+ values: ["development", "production"],
22
+ default: "development",
23
+ },
24
+ });
25
+ ```
26
+
27
+ **With Strict Mode**
28
+
29
+ ```js
30
+ validateEnv(schema, { strict: true });
31
+ ```
32
+
33
+ **With Custom Error Formatter**
34
+
35
+ ```js
36
+ validateEnv(schema, {
37
+ formatError: (errors) => `Config error:\n${errors.join("\n")}`,
38
+ });
39
+ ```
40
+
41
+ ## CLI
42
+
43
+ Create a file called env.schema.js
44
+
45
+ ```js
46
+ module.exports = {
47
+ PORT: { type: "number", required: true },
48
+ NODE_ENV: { type: "enum", values: ["dev", "prod"] },
49
+ };
50
+ ```
51
+ ```bash
52
+ npx safe-env-check env.schema.js
53
+ ```
@@ -0,0 +1,27 @@
1
+ type EnvField = {
2
+ type: "string";
3
+ required?: boolean;
4
+ default?: string;
5
+ } | {
6
+ type: "number";
7
+ required?: boolean;
8
+ default?: number;
9
+ } | {
10
+ type: "boolean";
11
+ required?: boolean;
12
+ default?: boolean;
13
+ } | {
14
+ type: "enum";
15
+ values: readonly string[];
16
+ required?: boolean;
17
+ default?: string;
18
+ };
19
+ type EnvSchema = Record<string, EnvField>;
20
+ interface ValidateEnvOptions {
21
+ strict?: boolean;
22
+ formatError?: (error: string[]) => string;
23
+ }
24
+
25
+ declare const validateEnv: <T extends EnvSchema>(schema: T, options?: ValidateEnvOptions) => { [K in keyof T]: any; };
26
+
27
+ export { type EnvField, type EnvSchema, type ValidateEnvOptions, validateEnv };
package/dist/index.js ADDED
@@ -0,0 +1,82 @@
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
+ validateEnv: () => validateEnv
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/validateEnv.ts
28
+ var validateEnv = (schema, options = {}) => {
29
+ const errors = [];
30
+ const result = {};
31
+ if (options.strict) {
32
+ const schemaKeys = Object.keys(schema);
33
+ const envKeys = Object.keys(process.env);
34
+ const unknownKeys = envKeys.filter((key) => !schemaKeys.includes(key));
35
+ if (unknownKeys.length)
36
+ errors.push(`Unknown evn variables: ${unknownKeys.join(", ")}`);
37
+ }
38
+ for (const key in schema) {
39
+ const rule = schema[key];
40
+ const rawValue = process.env[key];
41
+ if (!rawValue) {
42
+ if (rule.required && rule.default === void 0) {
43
+ errors.push(`${key} is required`);
44
+ continue;
45
+ }
46
+ result[key] = rule.default;
47
+ continue;
48
+ }
49
+ switch (rule.type) {
50
+ case "string":
51
+ result[key] = rawValue;
52
+ break;
53
+ case "number":
54
+ const num = Number(rawValue);
55
+ if (isNaN(num)) errors.push(`${key} must be a number`);
56
+ else result[key] = num;
57
+ break;
58
+ case "boolean":
59
+ result[key] = rawValue === "true";
60
+ break;
61
+ case "enum":
62
+ if (!rule.values.includes(rawValue)) {
63
+ errors.push(`${key} must be one of: ${rule.values.join(", ")}`);
64
+ } else {
65
+ result[key] = rawValue;
66
+ }
67
+ break;
68
+ }
69
+ }
70
+ if (errors.length) {
71
+ const message = options.formatError ? options.formatError(errors) : defaultErrorFormatter(errors);
72
+ throw new Error(message);
73
+ }
74
+ return result;
75
+ };
76
+ var defaultErrorFormatter = (errors) => {
77
+ return "\u274C Environment validation failed:\n" + errors.map((e) => `- ${e}`).join("\n");
78
+ };
79
+ // Annotate the CommonJS export names for ESM import in node:
80
+ 0 && (module.exports = {
81
+ validateEnv
82
+ });
package/jest.config.js ADDED
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ preset: "ts-jest",
3
+ testEnvironment: "node",
4
+ testMatch: ["**/tests/**/*.test.ts"],
5
+ };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@safe-hand/safe-env-check",
3
+ "version": "1.0.1",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "bin": {
7
+ "safe-env-check": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsup src/index.ts --dts",
11
+ "dev": "ts-node src/index.ts",
12
+ "test": "jest",
13
+ "lint": "tsc --noEmit"
14
+ },
15
+ "author": "",
16
+ "license": "ISC",
17
+ "description": "",
18
+ "devDependencies": {
19
+ "@types/jest": "^30.0.0",
20
+ "@types/node": "^25.2.3",
21
+ "jest": "^30.2.0",
22
+ "ts-jest": "^29.4.6",
23
+ "ts-node": "^10.9.2",
24
+ "tsup": "^8.5.1",
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "dependencies": {
28
+ "dotenv": "^17.2.4"
29
+ }
30
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,20 @@
1
+ import path from "path";
2
+ import { validateEnv } from "./validateEnv";
3
+
4
+ const schemaPath = process.argv[2];
5
+
6
+ if (!schemaPath) {
7
+ console.error("Usage: safe-env-check <schema-file>");
8
+ process.exit(1);
9
+ }
10
+
11
+ try {
12
+ const fullPath = path.resolve(process.cwd(), schemaPath);
13
+ const schema = require(fullPath);
14
+
15
+ validateEnv(schema);
16
+ console.log("✅ Environment variables are valid");
17
+ } catch (error: any) {
18
+ console.error(error.message);
19
+ process.exit(1);
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { validateEnv } from "./validateEnv";
2
+
3
+ export * from "./types";
package/src/types.ts ADDED
@@ -0,0 +1,17 @@
1
+ export type EnvField =
2
+ | { type: "string"; required?: boolean; default?: string }
3
+ | { type: "number"; required?: boolean; default?: number }
4
+ | { type: "boolean"; required?: boolean; default?: boolean }
5
+ | {
6
+ type: "enum";
7
+ values: readonly string[];
8
+ required?: boolean;
9
+ default?: string;
10
+ };
11
+
12
+ export type EnvSchema = Record<string, EnvField>;
13
+
14
+ export interface ValidateEnvOptions {
15
+ strict?: boolean;
16
+ formatError?: (error: string[]) => string;
17
+ }
@@ -0,0 +1,74 @@
1
+ import { EnvSchema, ValidateEnvOptions } from "./types";
2
+
3
+ export const validateEnv = <T extends EnvSchema>(
4
+ schema: T,
5
+ options: ValidateEnvOptions = {},
6
+ ): { [K in keyof T]: any } => {
7
+ const errors: string[] = [];
8
+ const result: any = {};
9
+
10
+ if (options.strict) {
11
+ const schemaKeys = Object.keys(schema);
12
+ const envKeys = Object.keys(process.env);
13
+
14
+ const unknownKeys = envKeys.filter((key) => !schemaKeys.includes(key));
15
+
16
+ if (unknownKeys.length)
17
+ errors.push(`Unknown evn variables: ${unknownKeys.join(", ")}`);
18
+ }
19
+
20
+ for (const key in schema) {
21
+ const rule = schema[key];
22
+ const rawValue = process.env[key];
23
+
24
+ if (!rawValue) {
25
+ if (rule.required && rule.default === undefined) {
26
+ errors.push(`${key} is required`);
27
+ continue;
28
+ }
29
+ result[key] = rule.default;
30
+ continue;
31
+ }
32
+
33
+ switch (rule.type) {
34
+ case "string":
35
+ result[key] = rawValue;
36
+ break;
37
+
38
+ case "number":
39
+ const num = Number(rawValue);
40
+ if (isNaN(num)) errors.push(`${key} must be a number`);
41
+ else result[key] = num;
42
+ break;
43
+
44
+ case "boolean":
45
+ result[key] = rawValue === "true";
46
+ break;
47
+
48
+ case "enum":
49
+ if (!rule.values.includes(rawValue)) {
50
+ errors.push(`${key} must be one of: ${rule.values.join(", ")}`);
51
+ } else {
52
+ result[key] = rawValue;
53
+ }
54
+ break;
55
+ }
56
+ }
57
+
58
+ if (errors.length) {
59
+ const message = options.formatError
60
+ ? options.formatError(errors)
61
+ : defaultErrorFormatter(errors);
62
+
63
+ throw new Error(message);
64
+ }
65
+
66
+ return result;
67
+ };
68
+
69
+ const defaultErrorFormatter = (errors: string[]) => {
70
+ return (
71
+ "❌ Environment validation failed:\n" +
72
+ errors.map((e) => `- ${e}`).join("\n")
73
+ );
74
+ };
@@ -0,0 +1,63 @@
1
+ import { validateEnv } from "../src/validateEnv";
2
+
3
+ describe("validateEnv", () => {
4
+ beforeEach(() => {
5
+ process.env = {};
6
+ });
7
+
8
+ it("validates required number", () => {
9
+ process.env.PORT = "3000";
10
+
11
+ const env = validateEnv({
12
+ PORT: { type: "number", required: true },
13
+ });
14
+
15
+ expect(env.PORT).toBe(3000);
16
+ });
17
+
18
+ it("throws on missing required", () => {
19
+ expect(() =>
20
+ validateEnv({
21
+ JWT_SECRET: { type: "string", required: true },
22
+ }),
23
+ ).toThrow("JWT_SECRET is required");
24
+ });
25
+
26
+ it("supports default value", () => {
27
+ const env = validateEnv({
28
+ NODE_ENV: { type: "string", default: "development" },
29
+ });
30
+
31
+ expect(env.NODE_ENV).toBe("development");
32
+ });
33
+
34
+ it("supports enum", () => {
35
+ process.env.MODE = "prod";
36
+
37
+ const env = validateEnv({
38
+ MODE: { type: "enum", values: ["dev", "prod"] },
39
+ });
40
+
41
+ expect(env.MODE).toBe("prod");
42
+ });
43
+
44
+ it("strict mode fails on unknown env", () => {
45
+ process.env.UNKNOWN = "123";
46
+
47
+ expect(() =>
48
+ validateEnv(
49
+ { PORT: { type: "number", required: true } },
50
+ { strict: true },
51
+ ),
52
+ ).toThrow("❌ Environment validation failed:");
53
+ });
54
+
55
+ it("custom error formatter", () => {
56
+ expect(() =>
57
+ validateEnv(
58
+ { PORT: { type: "number", required: true } },
59
+ { formatError: (errs) => "Custom: " + errs.join(",") },
60
+ ),
61
+ ).toThrow("Custom:");
62
+ });
63
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "declaration": true,
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "moduleResolution": "node",
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src"]
14
+ }