@systemix/env 0.3.0 → 0.3.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/dist/index.cjs CHANGED
@@ -1,243 +1,234 @@
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
- Config: () => Config,
24
- load: () => load,
25
- parseEnvFile: () => parseEnvFile
26
- });
27
- module.exports = __toCommonJS(index_exports);
28
-
29
- // src/env/parse.ts
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_fs = require("node:fs");
3
+ let node_path = require("node:path");
4
+ //#region src/env/parse.ts
5
+ /**
6
+ * Parse a string to boolean. Accepts: true/false, 1/0, yes/no, empty string.
7
+ * @param raw - Raw string value
8
+ * @throws When value is not a valid boolean
9
+ */
30
10
  function parseBoolean(raw) {
31
- const lower = raw.toLowerCase().trim();
32
- if (lower === "true" || lower === "1" || lower === "yes") return true;
33
- if (lower === "false" || lower === "0" || lower === "no" || lower === "")
34
- return false;
35
- throw new Error(`Invalid boolean value: "${raw}"`);
11
+ const lower = raw.toLowerCase().trim();
12
+ if (lower === "true" || lower === "1" || lower === "yes") return true;
13
+ if (lower === "false" || lower === "0" || lower === "no" || lower === "") return false;
14
+ throw new Error(`Invalid boolean value: "${raw}"`);
36
15
  }
16
+ /**
17
+ * Parse a string to number. Uses Number() and checks isFinite.
18
+ * @param raw - Raw string value
19
+ * @throws When value is not a valid number
20
+ */
37
21
  function parseNumber(raw) {
38
- const n = Number(raw);
39
- if (!Number.isFinite(n)) {
40
- throw new Error(`Invalid number value: "${raw}"`);
41
- }
42
- return n;
22
+ const n = Number(raw);
23
+ if (!Number.isFinite(n)) throw new Error(`Invalid number value: "${raw}"`);
24
+ return n;
43
25
  }
26
+ /**
27
+ * Parse .env file content into key-value pairs.
28
+ * Skips comments (#) and empty lines; unquotes " and ' values.
29
+ *
30
+ * @param content - Raw file content
31
+ * @returns Map of KEY -> value strings
32
+ */
44
33
  function parseEnvFile(content) {
45
- const result = {};
46
- const lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
47
- for (const line of lines) {
48
- const trimmed = line.trim();
49
- if (!trimmed || trimmed.startsWith("#")) continue;
50
- const eqIndex = trimmed.indexOf("=");
51
- if (eqIndex === -1) continue;
52
- const key = trimmed.slice(0, eqIndex).trim();
53
- let value = trimmed.slice(eqIndex + 1).trim();
54
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
55
- value = value.slice(1, -1).replace(/\\(.)/g, "$1");
56
- }
57
- result[key] = value;
58
- }
59
- return result;
34
+ const result = {};
35
+ const lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
36
+ for (const line of lines) {
37
+ const trimmed = line.trim();
38
+ if (!trimmed || trimmed.startsWith("#")) continue;
39
+ const eqIndex = trimmed.indexOf("=");
40
+ if (eqIndex === -1) continue;
41
+ const key = trimmed.slice(0, eqIndex).trim();
42
+ let value = trimmed.slice(eqIndex + 1).trim();
43
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1).replace(/\\(.)/g, "$1");
44
+ result[key] = value;
45
+ }
46
+ return result;
60
47
  }
61
-
62
- // src/env/file.ts
63
- var import_node_fs = require("fs");
64
- var import_node_path = require("path");
48
+ //#endregion
49
+ //#region src/env/file.ts
50
+ /**
51
+ * Load env vars from a .env file. Returns {} if file does not exist.
52
+ *
53
+ * @param filePath - Path relative to cwd or absolute
54
+ * @param cwd - Working directory for resolution
55
+ * @returns Parsed key-value pairs
56
+ */
65
57
  function loadEnvFile(filePath, cwd) {
66
- const resolved = (0, import_node_path.resolve)(cwd, filePath);
67
- if (!(0, import_node_fs.existsSync)(resolved)) {
68
- return {};
69
- }
70
- const content = (0, import_node_fs.readFileSync)(resolved, "utf-8");
71
- return parseEnvFile(content);
58
+ const resolved = (0, node_path.resolve)(cwd, filePath);
59
+ if (!(0, node_fs.existsSync)(resolved)) return {};
60
+ return parseEnvFile((0, node_fs.readFileSync)(resolved, "utf-8"));
72
61
  }
62
+ /** Get current working directory, or '.' if process.cwd is unavailable. */
73
63
  function getCwd() {
74
- return typeof globalThis.process?.cwd === "function" ? globalThis.process.cwd() : ".";
64
+ return typeof globalThis.process?.cwd === "function" ? globalThis.process.cwd() : ".";
75
65
  }
66
+ /** Merge overlay env into base. Overlay keys override base. */
76
67
  function mergeEnv(base, overlay) {
77
- const merged = { ...base };
78
- for (const [k, v] of Object.entries(overlay)) {
79
- merged[k] = v;
80
- }
81
- return merged;
68
+ const merged = { ...base };
69
+ for (const [k, v] of Object.entries(overlay)) merged[k] = v;
70
+ return merged;
82
71
  }
72
+ /** Get process.env as a plain object, or {} if unavailable. */
83
73
  function getEnvSource() {
84
- const env = globalThis.process?.env;
85
- if (env && typeof env === "object") {
86
- return env;
87
- }
88
- return {};
74
+ const env = globalThis.process?.env;
75
+ if (env && typeof env === "object") return env;
76
+ return {};
89
77
  }
90
-
91
- // src/env/load.ts
92
- var MASK = "***";
78
+ //#endregion
79
+ //#region src/env/load.ts
80
+ const MASK = "***";
81
+ /**
82
+ * Load and validate environment variables from a schema.
83
+ * Parses strings, numbers, and booleans; validates min/max, regex, oneOf.
84
+ * Returns a typed object with toSafeLog() for safe logging (masks secrets).
85
+ *
86
+ * @param schema - Map of var names to schema definitions
87
+ * @param options - Source override, fromFile, or strict mode. Legacy: plain object as source.
88
+ * @returns Typed env object with toSafeLog()
89
+ * @throws When required vars are missing or validation fails
90
+ *
91
+ * @example
92
+ * const env = load(
93
+ * { PORT: { type: 'number', default: 3000 }, API_KEY: { type: 'string', required: true } },
94
+ * { source: process.env }
95
+ * );
96
+ */
93
97
  function load(schema, options) {
94
- const opts = options ?? {};
95
- const isLegacySource = opts && typeof opts === "object" && !Array.isArray(opts) && !("source" in opts) && !("fromFile" in opts) && !("strict" in opts);
96
- const normalized = isLegacySource ? { source: opts } : opts;
97
- const { source, fromFile, strict = false } = normalized;
98
- let env = source ?? getEnvSource();
99
- if (fromFile) {
100
- const cwd = getCwd();
101
- const files = Array.isArray(fromFile) ? fromFile : [fromFile];
102
- for (const f of files) {
103
- const loaded = loadEnvFile(f, cwd);
104
- env = mergeEnv(env, loaded);
105
- }
106
- }
107
- if (strict) {
108
- const schemaKeys2 = new Set(Object.keys(schema));
109
- const filtered = {};
110
- for (const [k, v] of Object.entries(env)) {
111
- if (schemaKeys2.has(k)) filtered[k] = v;
112
- }
113
- env = filtered;
114
- }
115
- const schemaKeys = Object.keys(schema);
116
- const errors = [];
117
- const result = {};
118
- for (const key of schemaKeys) {
119
- const def = schema[key];
120
- const raw = env[key];
121
- const { type, required = false, default: defaultValue } = def;
122
- if (raw === void 0 || raw === "") {
123
- if (required && defaultValue === void 0) {
124
- errors.push(`Missing required env var: ${key}`);
125
- continue;
126
- }
127
- result[key] = defaultValue ?? void 0;
128
- continue;
129
- }
130
- try {
131
- let value;
132
- if (def.transform) {
133
- value = def.transform(raw);
134
- } else {
135
- switch (type) {
136
- case "string":
137
- value = raw;
138
- break;
139
- case "number":
140
- value = parseNumber(raw);
141
- break;
142
- case "boolean":
143
- value = parseBoolean(raw);
144
- break;
145
- default:
146
- value = raw;
147
- }
148
- }
149
- if (type === "number" && typeof value === "number") {
150
- if (def.min !== void 0 && value < def.min) {
151
- errors.push(`${key}: must be >= ${def.min}`);
152
- continue;
153
- }
154
- if (def.max !== void 0 && value > def.max) {
155
- errors.push(`${key}: must be <= ${def.max}`);
156
- continue;
157
- }
158
- }
159
- if (type === "string" && typeof value === "string") {
160
- if (def.regex && !def.regex.test(value)) {
161
- errors.push(`${key}: must match ${def.regex}`);
162
- continue;
163
- }
164
- if (def.oneOf && !def.oneOf.includes(value)) {
165
- errors.push(`${key}: must be one of [${def.oneOf.join(", ")}]`);
166
- continue;
167
- }
168
- }
169
- result[key] = value;
170
- } catch (err) {
171
- errors.push(
172
- `${key}: ${err instanceof Error ? err.message : String(err)}`
173
- );
174
- }
175
- }
176
- if (errors.length > 0) {
177
- throw new Error(
178
- `Env validation failed:
179
- ${errors.map((e) => ` - ${e}`).join("\n")}`
180
- );
181
- }
182
- const toSafeLog = () => {
183
- const safe = {};
184
- for (const key of schemaKeys) {
185
- const def = schema[key];
186
- const val = result[key];
187
- safe[key] = def.secret && val !== void 0 ? MASK : val;
188
- }
189
- return safe;
190
- };
191
- return Object.assign(result, { toSafeLog });
98
+ const opts = options ?? {};
99
+ const { source, fromFile, strict = false } = opts && typeof opts === "object" && !Array.isArray(opts) && !("source" in opts) && !("fromFile" in opts) && !("strict" in opts) ? { source: opts } : opts;
100
+ let env = source ?? getEnvSource();
101
+ if (fromFile) {
102
+ const cwd = getCwd();
103
+ const files = Array.isArray(fromFile) ? fromFile : [fromFile];
104
+ for (const f of files) {
105
+ const loaded = loadEnvFile(f, cwd);
106
+ env = mergeEnv(env, loaded);
107
+ }
108
+ }
109
+ if (strict) {
110
+ const schemaKeys = new Set(Object.keys(schema));
111
+ const filtered = {};
112
+ for (const [k, v] of Object.entries(env)) if (schemaKeys.has(k)) filtered[k] = v;
113
+ env = filtered;
114
+ }
115
+ const schemaKeys = Object.keys(schema);
116
+ const errors = [];
117
+ const result = {};
118
+ for (const key of schemaKeys) {
119
+ const def = schema[key];
120
+ const raw = env[key];
121
+ const { type, required = false, default: defaultValue } = def;
122
+ if (raw === void 0 || raw === "") {
123
+ if (required && defaultValue === void 0) {
124
+ errors.push(`Missing required env var: ${key}`);
125
+ continue;
126
+ }
127
+ result[key] = defaultValue ?? void 0;
128
+ continue;
129
+ }
130
+ try {
131
+ let value;
132
+ if (def.transform) value = def.transform(raw);
133
+ else switch (type) {
134
+ case "string":
135
+ value = raw;
136
+ break;
137
+ case "number":
138
+ value = parseNumber(raw);
139
+ break;
140
+ case "boolean":
141
+ value = parseBoolean(raw);
142
+ break;
143
+ default: value = raw;
144
+ }
145
+ if (type === "number" && typeof value === "number") {
146
+ if (def.min !== void 0 && value < def.min) {
147
+ errors.push(`${key}: must be >= ${def.min}`);
148
+ continue;
149
+ }
150
+ if (def.max !== void 0 && value > def.max) {
151
+ errors.push(`${key}: must be <= ${def.max}`);
152
+ continue;
153
+ }
154
+ }
155
+ if (type === "string" && typeof value === "string") {
156
+ if (def.regex && !def.regex.test(value)) {
157
+ errors.push(`${key}: must match ${def.regex}`);
158
+ continue;
159
+ }
160
+ if (def.oneOf && !def.oneOf.includes(value)) {
161
+ errors.push(`${key}: must be one of [${def.oneOf.join(", ")}]`);
162
+ continue;
163
+ }
164
+ }
165
+ result[key] = value;
166
+ } catch (err) {
167
+ errors.push(`${key}: ${err instanceof Error ? err.message : String(err)}`);
168
+ }
169
+ }
170
+ if (errors.length > 0) throw new Error(`Env validation failed:\n${errors.map((e) => ` - ${e}`).join("\n")}`);
171
+ const toSafeLog = () => {
172
+ const safe = {};
173
+ for (const key of schemaKeys) {
174
+ const def = schema[key];
175
+ const val = result[key];
176
+ safe[key] = def.secret && val !== void 0 ? MASK : val;
177
+ }
178
+ return safe;
179
+ };
180
+ return Object.assign(result, { toSafeLog });
192
181
  }
193
-
194
- // src/config.ts
195
- var Config = class _Config {
196
- constructor(config) {
197
- this.config = config;
198
- }
199
- /**
200
- * Create Config from a schema and env source.
201
- * @param schema - Env variable definitions
202
- * @param options - Source, fromFile, or strict. Legacy: plain object as source.
203
- */
204
- static fromEnv(schema, options) {
205
- const config = load(schema, options);
206
- return new _Config(config);
207
- }
208
- /** Create Config from an existing load() result. */
209
- static from(config) {
210
- return new _Config(config);
211
- }
212
- get(key, defaultValue) {
213
- const val = this.config[key];
214
- if (defaultValue !== void 0 && val === void 0) return defaultValue;
215
- return val;
216
- }
217
- /** Get value by key, or throw if not set. */
218
- getOrThrow(key) {
219
- const val = this.config[key];
220
- if (val === void 0) {
221
- throw new Error(`Config key "${String(key)}" is not set`);
222
- }
223
- return val;
224
- }
225
- /** Check if key is set. */
226
- has(key) {
227
- return this.config[key] !== void 0;
228
- }
229
- /** Return a copy with secret values masked as '***'. */
230
- toSafeLog() {
231
- return this.config.toSafeLog();
232
- }
233
- /** Get the raw env result (no masking). */
234
- getRaw() {
235
- return this.config;
236
- }
182
+ //#endregion
183
+ //#region src/config.ts
184
+ /**
185
+ * Typed configuration container built from env vars.
186
+ * Use fromEnv() to load from schema, or from() to wrap an existing result.
187
+ *
188
+ * @template T - Schema type (keys and value types)
189
+ */
190
+ var Config = class Config {
191
+ constructor(config) {
192
+ this.config = config;
193
+ }
194
+ /**
195
+ * Create Config from a schema and env source.
196
+ * @param schema - Env variable definitions
197
+ * @param options - Source, fromFile, or strict. Legacy: plain object as source.
198
+ */
199
+ static fromEnv(schema, options) {
200
+ const config = load(schema, options);
201
+ return new Config(config);
202
+ }
203
+ /** Create Config from an existing load() result. */
204
+ static from(config) {
205
+ return new Config(config);
206
+ }
207
+ get(key, defaultValue) {
208
+ const val = this.config[key];
209
+ if (defaultValue !== void 0 && val === void 0) return defaultValue;
210
+ return val;
211
+ }
212
+ /** Get value by key, or throw if not set. */
213
+ getOrThrow(key) {
214
+ const val = this.config[key];
215
+ if (val === void 0) throw new Error(`Config key "${String(key)}" is not set`);
216
+ return val;
217
+ }
218
+ /** Check if key is set. */
219
+ has(key) {
220
+ return this.config[key] !== void 0;
221
+ }
222
+ /** Return a copy with secret values masked as '***'. */
223
+ toSafeLog() {
224
+ return this.config.toSafeLog();
225
+ }
226
+ /** Get the raw env result (no masking). */
227
+ getRaw() {
228
+ return this.config;
229
+ }
237
230
  };
238
- // Annotate the CommonJS export names for ESM import in node:
239
- 0 && (module.exports = {
240
- Config,
241
- load,
242
- parseEnvFile
243
- });
231
+ //#endregion
232
+ exports.Config = Config;
233
+ exports.load = load;
234
+ exports.parseEnvFile = parseEnvFile;
package/dist/index.d.cts CHANGED
@@ -1,43 +1,45 @@
1
+ //#region src/types/env.d.ts
1
2
  type EnvVarType = 'string' | 'number' | 'boolean';
2
3
  interface EnvVarSchema<T extends EnvVarType = EnvVarType> {
3
- type: T;
4
- required?: boolean;
5
- default?: T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : never;
6
- min?: number;
7
- max?: number;
8
- regex?: RegExp;
9
- oneOf?: readonly string[];
10
- transform?: (raw: string) => string | number | boolean;
11
- secret?: boolean;
4
+ type: T;
5
+ required?: boolean;
6
+ default?: T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : never;
7
+ min?: number;
8
+ max?: number;
9
+ regex?: RegExp;
10
+ oneOf?: readonly string[];
11
+ transform?: (raw: string) => string | number | boolean;
12
+ secret?: boolean;
12
13
  }
13
14
  type EnvSchema = Record<string, EnvVarSchema<'string' | 'number' | 'boolean'>>;
14
15
  type EnvValueFor<S extends EnvVarSchema<EnvVarType>> = S['type'] extends 'string' ? string : S['type'] extends 'number' ? number : S['type'] extends 'boolean' ? boolean : never;
15
- type EnvResult<T extends EnvSchema> = {
16
- [K in keyof T]: T[K]['required'] extends true ? EnvValueFor<T[K]> : T[K]['default'] extends EnvValueFor<T[K]> ? EnvValueFor<T[K]> : EnvValueFor<T[K]> | undefined;
17
- } & {
18
- toSafeLog(): Record<string, string | number | boolean | undefined>;
16
+ type EnvResult<T extends EnvSchema> = { [K in keyof T]: T[K]['required'] extends true ? EnvValueFor<T[K]> : T[K]['default'] extends EnvValueFor<T[K]> ? EnvValueFor<T[K]> : EnvValueFor<T[K]> | undefined; } & {
17
+ toSafeLog(): Record<string, string | number | boolean | undefined>;
19
18
  };
20
19
  interface LoadOptions {
21
- source?: Record<string, string | undefined>;
22
- fromFile?: string | string[];
23
- strict?: boolean;
20
+ source?: Record<string, string | undefined>;
21
+ fromFile?: string | string[];
22
+ strict?: boolean;
24
23
  }
25
-
26
- declare function parseEnvFile(content: string): Record<string, string>;
27
-
28
- declare function load<T extends EnvSchema>(schema: T, options?: LoadOptions | Record<string, string | undefined>): EnvResult<T>;
29
-
30
- declare class Config<T extends EnvSchema> {
31
- private readonly config;
32
- private constructor();
33
- static fromEnv<S extends EnvSchema>(schema: S, options?: LoadOptions | Record<string, string | undefined>): Config<S>;
34
- static from<S extends EnvSchema>(config: EnvResult<S>): Config<S>;
35
- get<K extends keyof T>(key: K): EnvResult<T>[K] | undefined;
36
- get<K extends keyof T>(key: K, defaultValue: EnvResult<T>[K]): EnvResult<T>[K];
37
- getOrThrow<K extends keyof T>(key: K): EnvResult<T>[K];
38
- has(key: keyof T): boolean;
39
- toSafeLog(): Record<string, string | number | boolean | undefined>;
40
- getRaw(): EnvResult<T>;
24
+ //#endregion
25
+ //#region src/env/parse.d.ts
26
+ export declare function parseEnvFile(content: string): Record<string, string>;
27
+ //#endregion
28
+ //#region src/env/load.d.ts
29
+ export declare function load<T extends EnvSchema>(schema: T, options?: LoadOptions | Record<string, string | undefined>): EnvResult<T>;
30
+ //#endregion
31
+ //#region src/config.d.ts
32
+ export declare class Config<T extends EnvSchema> {
33
+ private readonly config;
34
+ private constructor();
35
+ static fromEnv<S extends EnvSchema>(schema: S, options?: LoadOptions | Record<string, string | undefined>): Config<S>;
36
+ static from<S extends EnvSchema>(config: EnvResult<S>): Config<S>;
37
+ get<K extends keyof T>(key: K): EnvResult<T>[K] | undefined;
38
+ get<K extends keyof T>(key: K, defaultValue: EnvResult<T>[K]): EnvResult<T>[K];
39
+ getOrThrow<K extends keyof T>(key: K): EnvResult<T>[K];
40
+ has(key: keyof T): boolean;
41
+ toSafeLog(): Record<string, string | number | boolean | undefined>;
42
+ getRaw(): EnvResult<T>;
41
43
  }
42
-
43
- export { Config, type EnvResult, type EnvSchema, type EnvVarSchema, type EnvVarType, type LoadOptions, load, parseEnvFile };
44
+ //#endregion
45
+ export type { EnvResult, EnvSchema, EnvVarSchema, EnvVarType, LoadOptions };
package/dist/index.d.ts CHANGED
@@ -1,43 +1,45 @@
1
+ //#region src/types/env.d.ts
1
2
  type EnvVarType = 'string' | 'number' | 'boolean';
2
3
  interface EnvVarSchema<T extends EnvVarType = EnvVarType> {
3
- type: T;
4
- required?: boolean;
5
- default?: T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : never;
6
- min?: number;
7
- max?: number;
8
- regex?: RegExp;
9
- oneOf?: readonly string[];
10
- transform?: (raw: string) => string | number | boolean;
11
- secret?: boolean;
4
+ type: T;
5
+ required?: boolean;
6
+ default?: T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : never;
7
+ min?: number;
8
+ max?: number;
9
+ regex?: RegExp;
10
+ oneOf?: readonly string[];
11
+ transform?: (raw: string) => string | number | boolean;
12
+ secret?: boolean;
12
13
  }
13
14
  type EnvSchema = Record<string, EnvVarSchema<'string' | 'number' | 'boolean'>>;
14
15
  type EnvValueFor<S extends EnvVarSchema<EnvVarType>> = S['type'] extends 'string' ? string : S['type'] extends 'number' ? number : S['type'] extends 'boolean' ? boolean : never;
15
- type EnvResult<T extends EnvSchema> = {
16
- [K in keyof T]: T[K]['required'] extends true ? EnvValueFor<T[K]> : T[K]['default'] extends EnvValueFor<T[K]> ? EnvValueFor<T[K]> : EnvValueFor<T[K]> | undefined;
17
- } & {
18
- toSafeLog(): Record<string, string | number | boolean | undefined>;
16
+ type EnvResult<T extends EnvSchema> = { [K in keyof T]: T[K]['required'] extends true ? EnvValueFor<T[K]> : T[K]['default'] extends EnvValueFor<T[K]> ? EnvValueFor<T[K]> : EnvValueFor<T[K]> | undefined; } & {
17
+ toSafeLog(): Record<string, string | number | boolean | undefined>;
19
18
  };
20
19
  interface LoadOptions {
21
- source?: Record<string, string | undefined>;
22
- fromFile?: string | string[];
23
- strict?: boolean;
20
+ source?: Record<string, string | undefined>;
21
+ fromFile?: string | string[];
22
+ strict?: boolean;
24
23
  }
25
-
26
- declare function parseEnvFile(content: string): Record<string, string>;
27
-
28
- declare function load<T extends EnvSchema>(schema: T, options?: LoadOptions | Record<string, string | undefined>): EnvResult<T>;
29
-
30
- declare class Config<T extends EnvSchema> {
31
- private readonly config;
32
- private constructor();
33
- static fromEnv<S extends EnvSchema>(schema: S, options?: LoadOptions | Record<string, string | undefined>): Config<S>;
34
- static from<S extends EnvSchema>(config: EnvResult<S>): Config<S>;
35
- get<K extends keyof T>(key: K): EnvResult<T>[K] | undefined;
36
- get<K extends keyof T>(key: K, defaultValue: EnvResult<T>[K]): EnvResult<T>[K];
37
- getOrThrow<K extends keyof T>(key: K): EnvResult<T>[K];
38
- has(key: keyof T): boolean;
39
- toSafeLog(): Record<string, string | number | boolean | undefined>;
40
- getRaw(): EnvResult<T>;
24
+ //#endregion
25
+ //#region src/env/parse.d.ts
26
+ export declare function parseEnvFile(content: string): Record<string, string>;
27
+ //#endregion
28
+ //#region src/env/load.d.ts
29
+ export declare function load<T extends EnvSchema>(schema: T, options?: LoadOptions | Record<string, string | undefined>): EnvResult<T>;
30
+ //#endregion
31
+ //#region src/config.d.ts
32
+ export declare class Config<T extends EnvSchema> {
33
+ private readonly config;
34
+ private constructor();
35
+ static fromEnv<S extends EnvSchema>(schema: S, options?: LoadOptions | Record<string, string | undefined>): Config<S>;
36
+ static from<S extends EnvSchema>(config: EnvResult<S>): Config<S>;
37
+ get<K extends keyof T>(key: K): EnvResult<T>[K] | undefined;
38
+ get<K extends keyof T>(key: K, defaultValue: EnvResult<T>[K]): EnvResult<T>[K];
39
+ getOrThrow<K extends keyof T>(key: K): EnvResult<T>[K];
40
+ has(key: keyof T): boolean;
41
+ toSafeLog(): Record<string, string | number | boolean | undefined>;
42
+ getRaw(): EnvResult<T>;
41
43
  }
42
-
43
- export { Config, type EnvResult, type EnvSchema, type EnvVarSchema, type EnvVarType, type LoadOptions, load, parseEnvFile };
44
+ //#endregion
45
+ export type { EnvResult, EnvSchema, EnvVarSchema, EnvVarType, LoadOptions };
package/dist/index.js CHANGED
@@ -1,214 +1,231 @@
1
- // src/env/parse.ts
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ //#region src/env/parse.ts
4
+ /**
5
+ * Parse a string to boolean. Accepts: true/false, 1/0, yes/no, empty string.
6
+ * @param raw - Raw string value
7
+ * @throws When value is not a valid boolean
8
+ */
2
9
  function parseBoolean(raw) {
3
- const lower = raw.toLowerCase().trim();
4
- if (lower === "true" || lower === "1" || lower === "yes") return true;
5
- if (lower === "false" || lower === "0" || lower === "no" || lower === "")
6
- return false;
7
- throw new Error(`Invalid boolean value: "${raw}"`);
10
+ const lower = raw.toLowerCase().trim();
11
+ if (lower === "true" || lower === "1" || lower === "yes") return true;
12
+ if (lower === "false" || lower === "0" || lower === "no" || lower === "") return false;
13
+ throw new Error(`Invalid boolean value: "${raw}"`);
8
14
  }
15
+ /**
16
+ * Parse a string to number. Uses Number() and checks isFinite.
17
+ * @param raw - Raw string value
18
+ * @throws When value is not a valid number
19
+ */
9
20
  function parseNumber(raw) {
10
- const n = Number(raw);
11
- if (!Number.isFinite(n)) {
12
- throw new Error(`Invalid number value: "${raw}"`);
13
- }
14
- return n;
21
+ const n = Number(raw);
22
+ if (!Number.isFinite(n)) throw new Error(`Invalid number value: "${raw}"`);
23
+ return n;
15
24
  }
25
+ /**
26
+ * Parse .env file content into key-value pairs.
27
+ * Skips comments (#) and empty lines; unquotes " and ' values.
28
+ *
29
+ * @param content - Raw file content
30
+ * @returns Map of KEY -> value strings
31
+ */
16
32
  function parseEnvFile(content) {
17
- const result = {};
18
- const lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
19
- for (const line of lines) {
20
- const trimmed = line.trim();
21
- if (!trimmed || trimmed.startsWith("#")) continue;
22
- const eqIndex = trimmed.indexOf("=");
23
- if (eqIndex === -1) continue;
24
- const key = trimmed.slice(0, eqIndex).trim();
25
- let value = trimmed.slice(eqIndex + 1).trim();
26
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
27
- value = value.slice(1, -1).replace(/\\(.)/g, "$1");
28
- }
29
- result[key] = value;
30
- }
31
- return result;
33
+ const result = {};
34
+ const lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
35
+ for (const line of lines) {
36
+ const trimmed = line.trim();
37
+ if (!trimmed || trimmed.startsWith("#")) continue;
38
+ const eqIndex = trimmed.indexOf("=");
39
+ if (eqIndex === -1) continue;
40
+ const key = trimmed.slice(0, eqIndex).trim();
41
+ let value = trimmed.slice(eqIndex + 1).trim();
42
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1).replace(/\\(.)/g, "$1");
43
+ result[key] = value;
44
+ }
45
+ return result;
32
46
  }
33
-
34
- // src/env/file.ts
35
- import { readFileSync, existsSync } from "fs";
36
- import { resolve } from "path";
47
+ //#endregion
48
+ //#region src/env/file.ts
49
+ /**
50
+ * Load env vars from a .env file. Returns {} if file does not exist.
51
+ *
52
+ * @param filePath - Path relative to cwd or absolute
53
+ * @param cwd - Working directory for resolution
54
+ * @returns Parsed key-value pairs
55
+ */
37
56
  function loadEnvFile(filePath, cwd) {
38
- const resolved = resolve(cwd, filePath);
39
- if (!existsSync(resolved)) {
40
- return {};
41
- }
42
- const content = readFileSync(resolved, "utf-8");
43
- return parseEnvFile(content);
57
+ const resolved = resolve(cwd, filePath);
58
+ if (!existsSync(resolved)) return {};
59
+ return parseEnvFile(readFileSync(resolved, "utf-8"));
44
60
  }
61
+ /** Get current working directory, or '.' if process.cwd is unavailable. */
45
62
  function getCwd() {
46
- return typeof globalThis.process?.cwd === "function" ? globalThis.process.cwd() : ".";
63
+ return typeof globalThis.process?.cwd === "function" ? globalThis.process.cwd() : ".";
47
64
  }
65
+ /** Merge overlay env into base. Overlay keys override base. */
48
66
  function mergeEnv(base, overlay) {
49
- const merged = { ...base };
50
- for (const [k, v] of Object.entries(overlay)) {
51
- merged[k] = v;
52
- }
53
- return merged;
67
+ const merged = { ...base };
68
+ for (const [k, v] of Object.entries(overlay)) merged[k] = v;
69
+ return merged;
54
70
  }
71
+ /** Get process.env as a plain object, or {} if unavailable. */
55
72
  function getEnvSource() {
56
- const env = globalThis.process?.env;
57
- if (env && typeof env === "object") {
58
- return env;
59
- }
60
- return {};
73
+ const env = globalThis.process?.env;
74
+ if (env && typeof env === "object") return env;
75
+ return {};
61
76
  }
62
-
63
- // src/env/load.ts
64
- var MASK = "***";
77
+ //#endregion
78
+ //#region src/env/load.ts
79
+ const MASK = "***";
80
+ /**
81
+ * Load and validate environment variables from a schema.
82
+ * Parses strings, numbers, and booleans; validates min/max, regex, oneOf.
83
+ * Returns a typed object with toSafeLog() for safe logging (masks secrets).
84
+ *
85
+ * @param schema - Map of var names to schema definitions
86
+ * @param options - Source override, fromFile, or strict mode. Legacy: plain object as source.
87
+ * @returns Typed env object with toSafeLog()
88
+ * @throws When required vars are missing or validation fails
89
+ *
90
+ * @example
91
+ * const env = load(
92
+ * { PORT: { type: 'number', default: 3000 }, API_KEY: { type: 'string', required: true } },
93
+ * { source: process.env }
94
+ * );
95
+ */
65
96
  function load(schema, options) {
66
- const opts = options ?? {};
67
- const isLegacySource = opts && typeof opts === "object" && !Array.isArray(opts) && !("source" in opts) && !("fromFile" in opts) && !("strict" in opts);
68
- const normalized = isLegacySource ? { source: opts } : opts;
69
- const { source, fromFile, strict = false } = normalized;
70
- let env = source ?? getEnvSource();
71
- if (fromFile) {
72
- const cwd = getCwd();
73
- const files = Array.isArray(fromFile) ? fromFile : [fromFile];
74
- for (const f of files) {
75
- const loaded = loadEnvFile(f, cwd);
76
- env = mergeEnv(env, loaded);
77
- }
78
- }
79
- if (strict) {
80
- const schemaKeys2 = new Set(Object.keys(schema));
81
- const filtered = {};
82
- for (const [k, v] of Object.entries(env)) {
83
- if (schemaKeys2.has(k)) filtered[k] = v;
84
- }
85
- env = filtered;
86
- }
87
- const schemaKeys = Object.keys(schema);
88
- const errors = [];
89
- const result = {};
90
- for (const key of schemaKeys) {
91
- const def = schema[key];
92
- const raw = env[key];
93
- const { type, required = false, default: defaultValue } = def;
94
- if (raw === void 0 || raw === "") {
95
- if (required && defaultValue === void 0) {
96
- errors.push(`Missing required env var: ${key}`);
97
- continue;
98
- }
99
- result[key] = defaultValue ?? void 0;
100
- continue;
101
- }
102
- try {
103
- let value;
104
- if (def.transform) {
105
- value = def.transform(raw);
106
- } else {
107
- switch (type) {
108
- case "string":
109
- value = raw;
110
- break;
111
- case "number":
112
- value = parseNumber(raw);
113
- break;
114
- case "boolean":
115
- value = parseBoolean(raw);
116
- break;
117
- default:
118
- value = raw;
119
- }
120
- }
121
- if (type === "number" && typeof value === "number") {
122
- if (def.min !== void 0 && value < def.min) {
123
- errors.push(`${key}: must be >= ${def.min}`);
124
- continue;
125
- }
126
- if (def.max !== void 0 && value > def.max) {
127
- errors.push(`${key}: must be <= ${def.max}`);
128
- continue;
129
- }
130
- }
131
- if (type === "string" && typeof value === "string") {
132
- if (def.regex && !def.regex.test(value)) {
133
- errors.push(`${key}: must match ${def.regex}`);
134
- continue;
135
- }
136
- if (def.oneOf && !def.oneOf.includes(value)) {
137
- errors.push(`${key}: must be one of [${def.oneOf.join(", ")}]`);
138
- continue;
139
- }
140
- }
141
- result[key] = value;
142
- } catch (err) {
143
- errors.push(
144
- `${key}: ${err instanceof Error ? err.message : String(err)}`
145
- );
146
- }
147
- }
148
- if (errors.length > 0) {
149
- throw new Error(
150
- `Env validation failed:
151
- ${errors.map((e) => ` - ${e}`).join("\n")}`
152
- );
153
- }
154
- const toSafeLog = () => {
155
- const safe = {};
156
- for (const key of schemaKeys) {
157
- const def = schema[key];
158
- const val = result[key];
159
- safe[key] = def.secret && val !== void 0 ? MASK : val;
160
- }
161
- return safe;
162
- };
163
- return Object.assign(result, { toSafeLog });
97
+ const opts = options ?? {};
98
+ const { source, fromFile, strict = false } = opts && typeof opts === "object" && !Array.isArray(opts) && !("source" in opts) && !("fromFile" in opts) && !("strict" in opts) ? { source: opts } : opts;
99
+ let env = source ?? getEnvSource();
100
+ if (fromFile) {
101
+ const cwd = getCwd();
102
+ const files = Array.isArray(fromFile) ? fromFile : [fromFile];
103
+ for (const f of files) {
104
+ const loaded = loadEnvFile(f, cwd);
105
+ env = mergeEnv(env, loaded);
106
+ }
107
+ }
108
+ if (strict) {
109
+ const schemaKeys = new Set(Object.keys(schema));
110
+ const filtered = {};
111
+ for (const [k, v] of Object.entries(env)) if (schemaKeys.has(k)) filtered[k] = v;
112
+ env = filtered;
113
+ }
114
+ const schemaKeys = Object.keys(schema);
115
+ const errors = [];
116
+ const result = {};
117
+ for (const key of schemaKeys) {
118
+ const def = schema[key];
119
+ const raw = env[key];
120
+ const { type, required = false, default: defaultValue } = def;
121
+ if (raw === void 0 || raw === "") {
122
+ if (required && defaultValue === void 0) {
123
+ errors.push(`Missing required env var: ${key}`);
124
+ continue;
125
+ }
126
+ result[key] = defaultValue ?? void 0;
127
+ continue;
128
+ }
129
+ try {
130
+ let value;
131
+ if (def.transform) value = def.transform(raw);
132
+ else switch (type) {
133
+ case "string":
134
+ value = raw;
135
+ break;
136
+ case "number":
137
+ value = parseNumber(raw);
138
+ break;
139
+ case "boolean":
140
+ value = parseBoolean(raw);
141
+ break;
142
+ default: value = raw;
143
+ }
144
+ if (type === "number" && typeof value === "number") {
145
+ if (def.min !== void 0 && value < def.min) {
146
+ errors.push(`${key}: must be >= ${def.min}`);
147
+ continue;
148
+ }
149
+ if (def.max !== void 0 && value > def.max) {
150
+ errors.push(`${key}: must be <= ${def.max}`);
151
+ continue;
152
+ }
153
+ }
154
+ if (type === "string" && typeof value === "string") {
155
+ if (def.regex && !def.regex.test(value)) {
156
+ errors.push(`${key}: must match ${def.regex}`);
157
+ continue;
158
+ }
159
+ if (def.oneOf && !def.oneOf.includes(value)) {
160
+ errors.push(`${key}: must be one of [${def.oneOf.join(", ")}]`);
161
+ continue;
162
+ }
163
+ }
164
+ result[key] = value;
165
+ } catch (err) {
166
+ errors.push(`${key}: ${err instanceof Error ? err.message : String(err)}`);
167
+ }
168
+ }
169
+ if (errors.length > 0) throw new Error(`Env validation failed:\n${errors.map((e) => ` - ${e}`).join("\n")}`);
170
+ const toSafeLog = () => {
171
+ const safe = {};
172
+ for (const key of schemaKeys) {
173
+ const def = schema[key];
174
+ const val = result[key];
175
+ safe[key] = def.secret && val !== void 0 ? MASK : val;
176
+ }
177
+ return safe;
178
+ };
179
+ return Object.assign(result, { toSafeLog });
164
180
  }
165
-
166
- // src/config.ts
167
- var Config = class _Config {
168
- constructor(config) {
169
- this.config = config;
170
- }
171
- /**
172
- * Create Config from a schema and env source.
173
- * @param schema - Env variable definitions
174
- * @param options - Source, fromFile, or strict. Legacy: plain object as source.
175
- */
176
- static fromEnv(schema, options) {
177
- const config = load(schema, options);
178
- return new _Config(config);
179
- }
180
- /** Create Config from an existing load() result. */
181
- static from(config) {
182
- return new _Config(config);
183
- }
184
- get(key, defaultValue) {
185
- const val = this.config[key];
186
- if (defaultValue !== void 0 && val === void 0) return defaultValue;
187
- return val;
188
- }
189
- /** Get value by key, or throw if not set. */
190
- getOrThrow(key) {
191
- const val = this.config[key];
192
- if (val === void 0) {
193
- throw new Error(`Config key "${String(key)}" is not set`);
194
- }
195
- return val;
196
- }
197
- /** Check if key is set. */
198
- has(key) {
199
- return this.config[key] !== void 0;
200
- }
201
- /** Return a copy with secret values masked as '***'. */
202
- toSafeLog() {
203
- return this.config.toSafeLog();
204
- }
205
- /** Get the raw env result (no masking). */
206
- getRaw() {
207
- return this.config;
208
- }
209
- };
210
- export {
211
- Config,
212
- load,
213
- parseEnvFile
181
+ //#endregion
182
+ //#region src/config.ts
183
+ /**
184
+ * Typed configuration container built from env vars.
185
+ * Use fromEnv() to load from schema, or from() to wrap an existing result.
186
+ *
187
+ * @template T - Schema type (keys and value types)
188
+ */
189
+ var Config = class Config {
190
+ constructor(config) {
191
+ this.config = config;
192
+ }
193
+ /**
194
+ * Create Config from a schema and env source.
195
+ * @param schema - Env variable definitions
196
+ * @param options - Source, fromFile, or strict. Legacy: plain object as source.
197
+ */
198
+ static fromEnv(schema, options) {
199
+ const config = load(schema, options);
200
+ return new Config(config);
201
+ }
202
+ /** Create Config from an existing load() result. */
203
+ static from(config) {
204
+ return new Config(config);
205
+ }
206
+ get(key, defaultValue) {
207
+ const val = this.config[key];
208
+ if (defaultValue !== void 0 && val === void 0) return defaultValue;
209
+ return val;
210
+ }
211
+ /** Get value by key, or throw if not set. */
212
+ getOrThrow(key) {
213
+ const val = this.config[key];
214
+ if (val === void 0) throw new Error(`Config key "${String(key)}" is not set`);
215
+ return val;
216
+ }
217
+ /** Check if key is set. */
218
+ has(key) {
219
+ return this.config[key] !== void 0;
220
+ }
221
+ /** Return a copy with secret values masked as '***'. */
222
+ toSafeLog() {
223
+ return this.config.toSafeLog();
224
+ }
225
+ /** Get the raw env result (no masking). */
226
+ getRaw() {
227
+ return this.config;
228
+ }
214
229
  };
230
+ //#endregion
231
+ export { Config, load, parseEnvFile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@systemix/env",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -37,13 +37,14 @@
37
37
  "license": "MIT",
38
38
  "description": "Typed environment variable loading and validation. Part of the Systemix toolkit.",
39
39
  "devDependencies": {
40
- "@types/node": "^25.3.5",
41
- "eslint": "^10.0.3",
42
- "tsup": "^8.5.1",
43
- "typescript": "^5.9.3",
44
- "@systemix/eslint": "0.5.0",
45
- "@systemix/runner": "0.3.0",
46
- "@systemix/typescript": "0.5.0"
40
+ "@types/node": "^26.6.5",
41
+ "eslint": "^10.12.0",
42
+ "oxlint": "^1.87.0",
43
+ "oxlint-tsgolint": "^7.0.2004",
44
+ "tsdown": "^0.23.1",
45
+ "typescript": "~7.0.2",
46
+ "@systemix/runner": "0.3.1",
47
+ "@systemix/typescript": "0.6.0"
47
48
  },
48
49
  "directories": {
49
50
  "test": "tests"
@@ -58,12 +59,13 @@
58
59
  "LICENSE",
59
60
  "package.json"
60
61
  ],
62
+ "tsconfig": null,
61
63
  "scripts": {
62
- "build": "tsup src/index.ts --format cjs,esm --dts",
64
+ "build": "tsdown",
63
65
  "test": "pnpm build && node tests/run.mjs",
64
66
  "typecheck": "tsc --noEmit",
65
- "lint": "eslint .",
66
- "lint:fix": "eslint . --fix",
67
+ "lint": "oxlint . --type-aware",
68
+ "lint:fix": "oxlint . --type-aware --fix",
67
69
  "format": "prettier --check . --ignore-path .prettierignore",
68
70
  "format:fix": "prettier --write . --ignore-path .prettierignore",
69
71
  "clean": "rimraf dist node_modules",