@kasoa/env 0.0.3 → 0.0.6

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
@@ -1 +1,69 @@
1
- # `@kasoa/env`
1
+ # @kasoa/env
2
+
3
+ Type-safe environment variable validation using [Standard Schema](https://standardschema.dev/).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @kasoa/env valibot
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { defineEnv } from "@kasoa/env";
15
+ import * as v from "valibot";
16
+
17
+ const env = defineEnv(
18
+ v.object({
19
+ DATABASE_URL: v.pipe(v.string(), v.url()),
20
+ PORT: v.pipe(v.string(), v.transform(Number)),
21
+ NODE_ENV: v.optional(
22
+ v.picklist(["development", "production"]),
23
+ "development",
24
+ ),
25
+ }),
26
+ );
27
+
28
+ // env is fully typed:
29
+ // { DATABASE_URL: string; PORT: number; NODE_ENV: "development" | "production" }
30
+ ```
31
+
32
+ ## Features
33
+
34
+ - Works with any [Standard Schema](https://standardschema.dev/) compatible library (Valibot, Zod, ArkType, etc.)
35
+ - Treats empty strings as missing values
36
+ - Throws with formatted error messages listing all invalid variables
37
+ - Full TypeScript inference
38
+
39
+ ## API
40
+
41
+ ### `defineEnv(schema, env?)`
42
+
43
+ Validates environment variables against the provided schema.
44
+
45
+ - `schema` - A Standard Schema compatible schema
46
+ - `env` - Optional env object (defaults to `process.env`)
47
+
48
+ Returns the validated and typed environment object.
49
+
50
+ Throws an `Error` if validation fails:
51
+
52
+ ```
53
+ Environment validation failed:
54
+
55
+ DATABASE_URL: Invalid URL
56
+ PORT: Required
57
+ ```
58
+
59
+ ### `InferEnv<T>`
60
+
61
+ Type helper to infer the output type of a schema:
62
+
63
+ ```ts
64
+ import type { InferEnv } from "@kasoa/env";
65
+
66
+ const schema = v.object({ PORT: v.string() });
67
+ type Env = InferEnv<typeof schema>;
68
+ // { PORT: string }
69
+ ```
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=define-env.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-env.test.d.ts","sourceRoot":"","sources":["../src/define-env.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,82 @@
1
+ import * as v from "valibot";
2
+ import { describe, expect, it } from "vitest";
3
+ import { defineEnv } from "./index.js";
4
+ describe("defineEnv()", () => {
5
+ it("returns validated env object", () => {
6
+ expect.assertions(1);
7
+ const schema = v.object({
8
+ DATABASE_URL: v.string(),
9
+ PORT: v.pipe(v.string(), v.toNumber()),
10
+ });
11
+ const env = defineEnv(schema, {
12
+ DATABASE_URL: "postgres://localhost",
13
+ PORT: "3000",
14
+ });
15
+ expect(env).toStrictEqual({
16
+ DATABASE_URL: "postgres://localhost",
17
+ PORT: 3000,
18
+ });
19
+ });
20
+ it("throws on missing required variable", () => {
21
+ expect.assertions(2);
22
+ const schema = v.object({
23
+ DATABASE_URL: v.string(),
24
+ });
25
+ expect(() => defineEnv(schema, {})).toThrowError("Environment validation failed");
26
+ expect(() => defineEnv(schema, {})).toThrowError("DATABASE_URL");
27
+ });
28
+ it("treats empty string as undefined", () => {
29
+ expect.assertions(1);
30
+ const schema = v.object({
31
+ API_KEY: v.string(),
32
+ });
33
+ expect(() => defineEnv(schema, { API_KEY: "" })).toThrowError("Environment validation failed");
34
+ });
35
+ it("supports optional variables with defaults", () => {
36
+ expect.assertions(1);
37
+ const schema = v.object({
38
+ PORT: v.optional(v.string(), "3000"),
39
+ });
40
+ const env = defineEnv(schema, {});
41
+ expect(env.PORT).toBe("3000");
42
+ });
43
+ it("validates nested paths correctly", () => {
44
+ expect.assertions(1);
45
+ const schema = v.object({
46
+ DATABASE_URL: v.pipe(v.string(), v.url()),
47
+ });
48
+ expect(() => defineEnv(schema, { DATABASE_URL: "not-a-url" })).toThrowError("DATABASE_URL");
49
+ });
50
+ it("throws TypeError for async schemas", () => {
51
+ expect.assertions(2);
52
+ const asyncSchema = {
53
+ "~standard": {
54
+ version: 1,
55
+ vendor: "test",
56
+ validate: () => Promise.resolve({ value: {} }),
57
+ },
58
+ };
59
+ expect(() => defineEnv(asyncSchema, {})).toThrowError(TypeError);
60
+ expect(() => defineEnv(asyncSchema, {})).toThrowError("Async schema validation is not supported");
61
+ });
62
+ it("uses process.env by default", () => {
63
+ expect.assertions(1);
64
+ const originalEnv = process.env.TEST_VAR;
65
+ process.env.TEST_VAR = "test-value";
66
+ try {
67
+ const schema = v.object({
68
+ TEST_VAR: v.string(),
69
+ });
70
+ const env = defineEnv(schema);
71
+ expect(env.TEST_VAR).toBe("test-value");
72
+ }
73
+ finally {
74
+ if (originalEnv === undefined) {
75
+ delete process.env.TEST_VAR;
76
+ }
77
+ else {
78
+ process.env.TEST_VAR = originalEnv;
79
+ }
80
+ }
81
+ });
82
+ });
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
- export * from "./define-env.js";
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ export type InferEnv<T extends StandardSchemaV1> = StandardSchemaV1.InferOutput<T>;
3
+ export declare function defineEnv<T extends StandardSchemaV1>(schema: T, env?: Record<string, string | undefined>): InferEnv<T>;
2
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,gBAAgB,IAC7C,gBAAgB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;AAElC,wBAAgB,SAAS,CAAC,CAAC,SAAS,gBAAgB,EAClD,MAAM,EAAE,CAAC,EACT,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,QAAQ,CAAC,CAAC,CAAC,CAab"}
package/dist/index.js CHANGED
@@ -1 +1,31 @@
1
- export * from "./define-env.js";
1
+ export function defineEnv(schema, env = process.env) {
2
+ const normalizedEnv = normalizeEnv(env);
3
+ const result = schema["~standard"].validate(normalizedEnv);
4
+ if (result instanceof Promise) {
5
+ throw new TypeError("Async schema validation is not supported");
6
+ }
7
+ if (result.issues) {
8
+ throw new Error(formatIssues(result.issues));
9
+ }
10
+ return result.value;
11
+ }
12
+ function normalizeEnv(env) {
13
+ const normalizedEnv = {};
14
+ for (const key of Object.keys(env)) {
15
+ const value = env[key];
16
+ normalizedEnv[key] = value === "" ? undefined : value;
17
+ }
18
+ return normalizedEnv;
19
+ }
20
+ function formatIssues(issues) {
21
+ const lines = issues.map((issue) => {
22
+ const key = issue.path ? getPathString(issue.path) : "root";
23
+ return ` ${key}: ${issue.message}`;
24
+ });
25
+ return `Environment validation failed:\n\n${lines.join("\n")}`;
26
+ }
27
+ function getPathString(path) {
28
+ return path
29
+ .map((segment) => (typeof segment === "object" ? segment.key : segment))
30
+ .join(".");
31
+ }
package/package.json CHANGED
@@ -1,59 +1,62 @@
1
1
  {
2
2
  "name": "@kasoa/env",
3
- "version": "0.0.3",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
- "sideEffects": false,
6
5
  "license": "MIT",
7
- "description": "Manage environment variables, simply",
6
+ "description": "Type-safe environment variable validation using Standard Schema",
8
7
  "keywords": [
9
- "kasoa",
10
8
  "env",
11
9
  "environment",
12
- "variables",
13
- "valibot",
14
- "zod",
15
- "arktype"
10
+ "validation",
11
+ "standard-schema",
12
+ "kasoa"
16
13
  ],
17
14
  "author": {
18
15
  "name": "Emmanuel Chucks",
16
+ "email": "hi@emmanuelchucks.com",
19
17
  "url": "https://emmanuelchucks.com"
20
18
  },
21
- "homepage": "https://github.com/emmanuelchucks/kasoa/tree/main/packages/package-env",
19
+ "homepage": "https://github.com/emmanuelchucks/kasoa/tree/main/packages/env",
22
20
  "repository": {
23
21
  "type": "git",
24
- "url": "https://github.com/emmanuelchucks/kasoa.git",
25
- "directory": "packages/package-env"
22
+ "url": "git+https://github.com/emmanuelchucks/kasoa.git",
23
+ "directory": "packages/env"
26
24
  },
27
25
  "bugs": {
28
26
  "url": "https://github.com/emmanuelchucks/kasoa/issues"
29
27
  },
30
- "main": "dist/index.js",
31
- "types": "dist/index.d.ts",
32
28
  "files": [
29
+ "src",
33
30
  "dist"
34
31
  ],
35
32
  "exports": {
36
33
  ".": "./dist/index.js"
37
34
  },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
38
  "dependencies": {
39
- "@standard-schema/spec": "1.0.0-rc.0"
39
+ "@standard-schema/spec": "^1.1.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@types/node": "^22.10.7",
43
- "arktype": "2.0.0",
44
- "typescript": "^5.7.3",
45
- "valibot": "^1.0.0-beta.12",
46
- "zod": "^3.24.1",
47
- "@kasoa/config-biome": "0.0.0",
48
- "@kasoa/config-typescript": "0.0.0"
42
+ "@types/node": "^25.0.3",
43
+ "@typescript/native-preview": "^7.0.0-dev.20251216.1",
44
+ "eslint": "^9.39.2",
45
+ "prettier": "^3.7.4",
46
+ "valibot": "^1.2.0",
47
+ "vitest": "^4.0.16",
48
+ "@kasoa/eslint-config": "0.0.7",
49
+ "@kasoa/tsconfig": "0.0.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=24"
49
53
  },
50
54
  "scripts": {
51
- "dev": "tsc --watch",
52
- "build": "tsc",
53
- "test": "node --test --experimental-strip-types tests/*.test.ts",
54
- "check": "biome check --write",
55
- "format": "biome format --write",
56
- "check-types": "tsc --noEmit",
57
- "clean": "rm -rf dist"
55
+ "dev": "tsgo --watch",
56
+ "build": "tsgo",
57
+ "lint": "eslint --fix --cache .",
58
+ "format": "prettier --write --cache .",
59
+ "typecheck": "tsgo --noEmit",
60
+ "test": "vitest run"
58
61
  }
59
62
  }
@@ -0,0 +1,113 @@
1
+ import * as v from "valibot";
2
+ import { describe, expect, it } from "vitest";
3
+ import { defineEnv } from "./index.js";
4
+
5
+ describe("defineEnv()", () => {
6
+ it("returns validated env object", () => {
7
+ expect.assertions(1);
8
+
9
+ const schema = v.object({
10
+ DATABASE_URL: v.string(),
11
+ PORT: v.pipe(v.string(), v.toNumber()),
12
+ });
13
+
14
+ const env = defineEnv(schema, {
15
+ DATABASE_URL: "postgres://localhost",
16
+ PORT: "3000",
17
+ });
18
+
19
+ expect(env).toStrictEqual({
20
+ DATABASE_URL: "postgres://localhost",
21
+ PORT: 3000,
22
+ });
23
+ });
24
+
25
+ it("throws on missing required variable", () => {
26
+ expect.assertions(2);
27
+
28
+ const schema = v.object({
29
+ DATABASE_URL: v.string(),
30
+ });
31
+
32
+ expect(() => defineEnv(schema, {})).toThrowError(
33
+ "Environment validation failed",
34
+ );
35
+ expect(() => defineEnv(schema, {})).toThrowError("DATABASE_URL");
36
+ });
37
+
38
+ it("treats empty string as undefined", () => {
39
+ expect.assertions(1);
40
+
41
+ const schema = v.object({
42
+ API_KEY: v.string(),
43
+ });
44
+
45
+ expect(() => defineEnv(schema, { API_KEY: "" })).toThrowError(
46
+ "Environment validation failed",
47
+ );
48
+ });
49
+
50
+ it("supports optional variables with defaults", () => {
51
+ expect.assertions(1);
52
+
53
+ const schema = v.object({
54
+ PORT: v.optional(v.string(), "3000"),
55
+ });
56
+
57
+ const env = defineEnv(schema, {});
58
+
59
+ expect(env.PORT).toBe("3000");
60
+ });
61
+
62
+ it("validates nested paths correctly", () => {
63
+ expect.assertions(1);
64
+
65
+ const schema = v.object({
66
+ DATABASE_URL: v.pipe(v.string(), v.url()),
67
+ });
68
+
69
+ expect(() => defineEnv(schema, { DATABASE_URL: "not-a-url" })).toThrowError(
70
+ "DATABASE_URL",
71
+ );
72
+ });
73
+
74
+ it("throws TypeError for async schemas", () => {
75
+ expect.assertions(2);
76
+
77
+ const asyncSchema = {
78
+ "~standard": {
79
+ version: 1 as const,
80
+ vendor: "test",
81
+ validate: () => Promise.resolve({ value: {} }),
82
+ },
83
+ };
84
+
85
+ expect(() => defineEnv(asyncSchema, {})).toThrowError(TypeError);
86
+ expect(() => defineEnv(asyncSchema, {})).toThrowError(
87
+ "Async schema validation is not supported",
88
+ );
89
+ });
90
+
91
+ it("uses process.env by default", () => {
92
+ expect.assertions(1);
93
+
94
+ const originalEnv = process.env.TEST_VAR;
95
+ process.env.TEST_VAR = "test-value";
96
+
97
+ try {
98
+ const schema = v.object({
99
+ TEST_VAR: v.string(),
100
+ });
101
+
102
+ const env = defineEnv(schema);
103
+
104
+ expect(env.TEST_VAR).toBe("test-value");
105
+ } finally {
106
+ if (originalEnv === undefined) {
107
+ delete process.env.TEST_VAR;
108
+ } else {
109
+ process.env.TEST_VAR = originalEnv;
110
+ }
111
+ }
112
+ });
113
+ });
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+
3
+ export type InferEnv<T extends StandardSchemaV1> =
4
+ StandardSchemaV1.InferOutput<T>;
5
+
6
+ export function defineEnv<T extends StandardSchemaV1>(
7
+ schema: T,
8
+ env: Record<string, string | undefined> = process.env,
9
+ ): InferEnv<T> {
10
+ const normalizedEnv = normalizeEnv(env);
11
+ const result = schema["~standard"].validate(normalizedEnv);
12
+
13
+ if (result instanceof Promise) {
14
+ throw new TypeError("Async schema validation is not supported");
15
+ }
16
+
17
+ if (result.issues) {
18
+ throw new Error(formatIssues(result.issues));
19
+ }
20
+
21
+ return result.value as InferEnv<T>;
22
+ }
23
+
24
+ function normalizeEnv(
25
+ env: Record<string, string | undefined>,
26
+ ): Record<string, string | undefined> {
27
+ const normalizedEnv: Record<string, string | undefined> = {};
28
+ for (const key of Object.keys(env)) {
29
+ const value = env[key];
30
+ normalizedEnv[key] = value === "" ? undefined : value;
31
+ }
32
+ return normalizedEnv;
33
+ }
34
+
35
+ function formatIssues(issues: readonly StandardSchemaV1.Issue[]): string {
36
+ const lines = issues.map((issue) => {
37
+ const key = issue.path ? getPathString(issue.path) : "root";
38
+ return ` ${key}: ${issue.message}`;
39
+ });
40
+
41
+ return `Environment validation failed:\n\n${lines.join("\n")}`;
42
+ }
43
+
44
+ function getPathString(
45
+ path: readonly (PropertyKey | StandardSchemaV1.PathSegment)[],
46
+ ): string {
47
+ return path
48
+ .map((segment) => (typeof segment === "object" ? segment.key : segment))
49
+ .join(".");
50
+ }
@@ -1,3 +0,0 @@
1
- import type { StandardSchemaV1 } from "@standard-schema/spec";
2
- export declare function defineEnv<TSchema extends StandardSchemaV1>(schema: TSchema): (env: unknown) => StandardSchemaV1.InferOutput<TSchema>;
3
- //# sourceMappingURL=define-env.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"define-env.d.ts","sourceRoot":"","sources":["../src/define-env.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D,wBAAgB,SAAS,CAAC,OAAO,SAAS,gBAAgB,EACzD,MAAM,EAAE,OAAO,GACb,CAAC,GAAG,EAAE,OAAO,KAAK,gBAAgB,CAAC,WAAW,CAAC,OAAO,CAAC,CA2BzD"}
@@ -1,22 +0,0 @@
1
- import { styleText } from "node:util";
2
- export function defineEnv(schema) {
3
- return (env) => {
4
- const result = schema["~standard"].validate(env);
5
- if (result instanceof Promise) {
6
- throw new TypeError("Schema validation must be synchronous");
7
- }
8
- if (result.issues) {
9
- const issues = result.issues.flatMap((issue) => (issue.path ?? []).map((item) => {
10
- const key = typeof item === "object" ? item.key : item;
11
- const message = issue.message;
12
- return `${String(key)}: ${message}`;
13
- }));
14
- const error = [
15
- `${styleText("red", "Invalid environment variables")}`,
16
- ...issues,
17
- ].join("\n");
18
- throw new Error(error);
19
- }
20
- return result.value;
21
- };
22
- }